mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: create tools using MCP
This commit is contained in:
parent
5c29b6ed94
commit
fcb7004c7a
17 changed files with 1989 additions and 572 deletions
|
|
@ -36,6 +36,11 @@ The guide tool is the authoritative source for prompt-authoring craft (turn-taki
|
|||
|
||||
## Call order
|
||||
|
||||
### Creating a reusable tool
|
||||
1. If authentication is needed, call `list_credentials` and use an existing `credential_uuid`; the user creates credential secrets in the UI.
|
||||
2. Build a typed tool definition and call `create_tool`. The request schema is authoritative for allowed tool categories and config fields.
|
||||
3. Use the returned `tool_uuid` in workflow node `tool_uuids`, then call `create_workflow` or `save_workflow`.
|
||||
|
||||
### Reading documentation
|
||||
1. `search_docs` — use first for keyword or acronym lookup when the user is asking how Dograh works or how to configure something.
|
||||
2. `read_doc` — fetch the full page once one result looks likely. Prefer this over reasoning from search summaries alone.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from api.mcp_server.tools.docs_search import list_docs, read_doc, search_docs
|
|||
from api.mcp_server.tools.get_workflow_code import get_workflow_code
|
||||
from api.mcp_server.tools.node_types import get_node_type, list_node_types
|
||||
from api.mcp_server.tools.save_workflow import save_workflow
|
||||
from api.mcp_server.tools.tool_creation import create_tool
|
||||
from api.mcp_server.tools.voice_prompting_guide import get_voice_prompting_guide
|
||||
from api.mcp_server.tools.workflows import get_workflow, list_workflows
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ mcp = FastMCP("dograh", instructions=DOGRAH_MCP_INSTRUCTIONS)
|
|||
|
||||
for _tool in (
|
||||
create_workflow,
|
||||
create_tool,
|
||||
get_node_type,
|
||||
get_workflow,
|
||||
get_workflow_code,
|
||||
|
|
|
|||
63
api/mcp_server/tools/tool_creation.py
Normal file
63
api/mcp_server/tools/tool_creation.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""MCP tool for creating reusable Dograh tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
|
||||
from api.mcp_server.auth import authenticate_mcp_request
|
||||
from api.mcp_server.tracing import traced_tool
|
||||
from api.schemas.tool import CreateToolRequest
|
||||
from api.services.tool_management import ToolManagementError, create_tool_for_user
|
||||
|
||||
|
||||
def _error_result(code: str, message: str, **extra: Any) -> dict[str, Any]:
|
||||
return {"created": False, "error_code": code, "error": message, **extra}
|
||||
|
||||
|
||||
@traced_tool
|
||||
async def create_tool(request: CreateToolRequest) -> dict[str, Any]:
|
||||
"""Create a reusable tool the agent can invoke during calls.
|
||||
|
||||
The request schema is the same `CreateToolRequest` used by the REST API
|
||||
and generated SDKs. Use it to create HTTP API, end-call, transfer-call,
|
||||
calculator, or MCP-server tools. For authenticated HTTP or MCP tools,
|
||||
reference an existing `credential_uuid` from `list_credentials`; users
|
||||
create credential secrets in the UI, and this flow only stores the UUID
|
||||
reference. For MCP tools, the server best-effort discovers the remote
|
||||
tool catalog and caches it in `definition.config.discovered_tools`.
|
||||
|
||||
On success, returns `created: true` and the new `tool_uuid`; use that
|
||||
UUID in workflow node `tool_uuids`. On failure, returns `created: false`,
|
||||
a machine-readable `error_code`, and a human-readable `error`. Possible
|
||||
`error_code` values:
|
||||
- `validation_error` — the request failed schema validation.
|
||||
- `credential_not_found` — a supplied credential_uuid is not in this
|
||||
organization; ask the user to create/select it in the UI first.
|
||||
- `organization_required` — the API key user has no selected organization.
|
||||
- `create_failed` — unexpected persistence or backend failure; retry once,
|
||||
then surface the error.
|
||||
"""
|
||||
user = await authenticate_mcp_request()
|
||||
|
||||
try:
|
||||
parsed_request = CreateToolRequest.model_validate(request)
|
||||
except PydanticValidationError as e:
|
||||
return _error_result("validation_error", str(e))
|
||||
|
||||
try:
|
||||
tool = await create_tool_for_user(parsed_request, user, source="mcp")
|
||||
except ToolManagementError as e:
|
||||
return _error_result(e.error_code, e.message)
|
||||
except Exception as e: # noqa: BLE001
|
||||
return _error_result("create_failed", str(e))
|
||||
|
||||
return {
|
||||
"created": True,
|
||||
"tool_uuid": tool.tool_uuid,
|
||||
"name": tool.name,
|
||||
"category": tool.category,
|
||||
"status": tool.status,
|
||||
"definition": tool.definition,
|
||||
}
|
||||
|
|
@ -1,303 +1,68 @@
|
|||
"""API routes for managing tools."""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from api.db import db_client
|
||||
from api.db.models import UserModel
|
||||
from api.enums import PostHogEvent, ToolCategory, ToolStatus
|
||||
from api.enums import ToolCategory, ToolStatus
|
||||
from api.schemas.tool import (
|
||||
CalculatorToolDefinition,
|
||||
CreatedByResponse,
|
||||
CreateToolRequest,
|
||||
EndCallConfig,
|
||||
EndCallToolDefinition,
|
||||
HttpApiConfig,
|
||||
HttpApiToolDefinition,
|
||||
McpRefreshResponse,
|
||||
McpToolConfig,
|
||||
McpToolDefinition,
|
||||
PresetToolParameter,
|
||||
ToolDefinition,
|
||||
ToolParameter,
|
||||
ToolResponse,
|
||||
TransferCallConfig,
|
||||
TransferCallToolDefinition,
|
||||
UpdateToolRequest,
|
||||
)
|
||||
from api.sdk_expose import sdk_expose
|
||||
from api.services.auth.depends import get_user
|
||||
from api.services.posthog_client import capture_event
|
||||
from api.services.workflow.mcp_tool_session import discover_mcp_tools
|
||||
from api.services.workflow.tools.mcp_tool import (
|
||||
McpDefinitionError,
|
||||
validate_mcp_definition,
|
||||
from api.services.tool_management import (
|
||||
ToolManagementError,
|
||||
build_tool_response,
|
||||
create_tool_for_user,
|
||||
refresh_mcp_tool_for_user,
|
||||
validate_tool_credential_references,
|
||||
)
|
||||
from api.services.workflow.tools.mcp_tool import (
|
||||
McpToolConfig as SharedMcpToolConfig,
|
||||
)
|
||||
from api.services.workflow.tools.mcp_tool import (
|
||||
McpToolDefinition as SharedMcpToolDefinition,
|
||||
from api.services.tool_management import (
|
||||
populate_discovered_tools as _populate_discovered_tools,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/tools")
|
||||
|
||||
McpToolConfig = SharedMcpToolConfig
|
||||
McpToolDefinition = SharedMcpToolDefinition
|
||||
|
||||
|
||||
# Request/Response schemas
|
||||
class ToolParameter(BaseModel):
|
||||
"""A parameter that the tool accepts."""
|
||||
|
||||
name: str = Field(description="Parameter name (used as key in request body)")
|
||||
type: str = Field(description="Parameter type: string, number, or boolean")
|
||||
description: str = Field(description="Description of what this parameter is for")
|
||||
required: bool = Field(
|
||||
default=True, description="Whether this parameter is required"
|
||||
)
|
||||
|
||||
|
||||
class PresetToolParameter(BaseModel):
|
||||
"""A parameter injected by Dograh at runtime."""
|
||||
|
||||
name: str = Field(description="Parameter name (used as key in request body)")
|
||||
type: str = Field(description="Parameter type: string, number, or boolean")
|
||||
value_template: str = Field(
|
||||
description="Fixed value or template, e.g. {{initial_context.phone_number}}"
|
||||
)
|
||||
required: bool = Field(
|
||||
default=True,
|
||||
description="Whether the parameter must resolve to a non-empty value",
|
||||
)
|
||||
|
||||
|
||||
class HttpApiConfig(BaseModel):
|
||||
"""Configuration for HTTP API tools."""
|
||||
|
||||
method: str = Field(description="HTTP method (GET, POST, PUT, PATCH, DELETE)")
|
||||
url: str = Field(description="Target URL")
|
||||
headers: Optional[Dict[str, str]] = Field(
|
||||
default=None, description="Static headers to include"
|
||||
)
|
||||
credential_uuid: Optional[str] = Field(
|
||||
default=None, description="Reference to ExternalCredentialModel for auth"
|
||||
)
|
||||
parameters: Optional[List[ToolParameter]] = Field(
|
||||
default=None, description="Parameters that the tool accepts from LLM"
|
||||
)
|
||||
preset_parameters: Optional[List[PresetToolParameter]] = Field(
|
||||
default=None,
|
||||
description="Parameters injected by Dograh from fixed values or workflow context templates",
|
||||
)
|
||||
timeout_ms: Optional[int] = Field(
|
||||
default=5000, description="Request timeout in milliseconds"
|
||||
)
|
||||
customMessage: Optional[str] = Field(
|
||||
default=None, description="Custom message to play after tool execution"
|
||||
)
|
||||
customMessageType: Optional[Literal["text", "audio"]] = Field(
|
||||
default=None, description="Type of custom message: text or audio"
|
||||
)
|
||||
customMessageRecordingId: Optional[str] = Field(
|
||||
default=None, description="Recording ID for audio custom message"
|
||||
)
|
||||
|
||||
|
||||
class EndCallConfig(BaseModel):
|
||||
"""Configuration for End Call tools."""
|
||||
|
||||
messageType: Literal["none", "custom", "audio"] = Field(
|
||||
default="none", description="Type of goodbye message"
|
||||
)
|
||||
customMessage: Optional[str] = Field(
|
||||
default=None, description="Custom message to play before ending the call"
|
||||
)
|
||||
audioRecordingId: Optional[str] = Field(
|
||||
default=None, description="Recording ID for audio goodbye message"
|
||||
)
|
||||
endCallReason: bool = Field(
|
||||
default=False,
|
||||
description="When enabled, LLM must provide a reason for ending the call. "
|
||||
"The reason is set as call disposition and added to call tags.",
|
||||
)
|
||||
endCallReasonDescription: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Description shown to the LLM for the reason parameter. "
|
||||
"Used only when endCallReason is enabled.",
|
||||
)
|
||||
|
||||
|
||||
class TransferCallConfig(BaseModel):
|
||||
"""Configuration for Transfer Call tools."""
|
||||
|
||||
destination: str = Field(
|
||||
description="Phone number or SIP endpoint to transfer the call to (E.164 format e.g., +1234567890, or SIP endpoint e.g., PJSIP/1234)"
|
||||
)
|
||||
messageType: Literal["none", "custom", "audio"] = Field(
|
||||
default="none", description="Type of message to play before transfer"
|
||||
)
|
||||
customMessage: Optional[str] = Field(
|
||||
default=None, description="Custom message to play before transferring the call"
|
||||
)
|
||||
audioRecordingId: Optional[str] = Field(
|
||||
default=None, description="Recording ID for audio message before transfer"
|
||||
)
|
||||
timeout: int = Field(
|
||||
default=30,
|
||||
ge=5,
|
||||
le=120,
|
||||
description="Maximum time in seconds to wait for destination to answer (5-120 seconds)",
|
||||
)
|
||||
|
||||
@field_validator("destination")
|
||||
@classmethod
|
||||
def validate_destination(cls, v: str) -> str:
|
||||
"""Validate that destination is a valid E.164 phone number or SIP endpoint."""
|
||||
# Allow empty string for initial creation (like HTTP API tools with empty URL)
|
||||
if not v.strip():
|
||||
return v
|
||||
|
||||
# E.164 format: +[1-9]\d{1,14}
|
||||
e164_pattern = r"^\+[1-9]\d{1,14}$"
|
||||
|
||||
# SIP endpoint format: PJSIP/extension or SIP/extension
|
||||
sip_pattern = r"^(PJSIP|SIP)/[\w\-\.@]+$"
|
||||
|
||||
is_valid_e164 = re.match(e164_pattern, v)
|
||||
is_valid_sip = re.match(sip_pattern, v, re.IGNORECASE)
|
||||
|
||||
if not (is_valid_e164 or is_valid_sip):
|
||||
raise ValueError(
|
||||
"Destination must be a valid E.164 phone number (e.g., +1234567890) or SIP endpoint (e.g., PJSIP/1234)"
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class HttpApiToolDefinition(BaseModel):
|
||||
"""Tool definition for HTTP API tools."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version")
|
||||
type: Literal["http_api"] = Field(description="Tool type")
|
||||
config: HttpApiConfig = Field(description="HTTP API configuration")
|
||||
|
||||
|
||||
class EndCallToolDefinition(BaseModel):
|
||||
"""Tool definition for End Call tools."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version")
|
||||
type: Literal["end_call"] = Field(description="Tool type")
|
||||
config: EndCallConfig = Field(description="End Call configuration")
|
||||
|
||||
|
||||
class TransferCallToolDefinition(BaseModel):
|
||||
"""Tool definition for Transfer Call tools."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version")
|
||||
type: Literal["transfer_call"] = Field(description="Tool type")
|
||||
config: TransferCallConfig = Field(description="Transfer Call configuration")
|
||||
|
||||
|
||||
class CalculatorToolDefinition(BaseModel):
|
||||
"""Tool definition for Calculator tools (no configuration needed)."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version")
|
||||
type: Literal["calculator"] = Field(description="Tool type")
|
||||
|
||||
|
||||
# Union type for tool definitions - Pydantic will discriminate based on 'type' field
|
||||
ToolDefinition = Annotated[
|
||||
Union[
|
||||
HttpApiToolDefinition,
|
||||
EndCallToolDefinition,
|
||||
TransferCallToolDefinition,
|
||||
CalculatorToolDefinition,
|
||||
McpToolDefinition,
|
||||
],
|
||||
Field(discriminator="type"),
|
||||
__all__ = [
|
||||
"CalculatorToolDefinition",
|
||||
"CreateToolRequest",
|
||||
"CreatedByResponse",
|
||||
"EndCallConfig",
|
||||
"EndCallToolDefinition",
|
||||
"HttpApiConfig",
|
||||
"HttpApiToolDefinition",
|
||||
"McpRefreshResponse",
|
||||
"McpToolConfig",
|
||||
"McpToolDefinition",
|
||||
"PresetToolParameter",
|
||||
"ToolDefinition",
|
||||
"ToolParameter",
|
||||
"ToolResponse",
|
||||
"TransferCallConfig",
|
||||
"TransferCallToolDefinition",
|
||||
"UpdateToolRequest",
|
||||
"_populate_discovered_tools",
|
||||
]
|
||||
|
||||
|
||||
class CreateToolRequest(BaseModel):
|
||||
"""Request schema for creating a tool."""
|
||||
|
||||
name: str = Field(max_length=255)
|
||||
description: Optional[str] = None
|
||||
category: str = Field(default=ToolCategory.HTTP_API.value)
|
||||
icon: Optional[str] = Field(default="globe", max_length=50)
|
||||
icon_color: Optional[str] = Field(default="#3B82F6", max_length=7)
|
||||
definition: ToolDefinition
|
||||
|
||||
@field_validator("category")
|
||||
@classmethod
|
||||
def validate_category(cls, v: str) -> str:
|
||||
"""Validate that category is a valid ToolCategory value."""
|
||||
valid_categories = [c.value for c in ToolCategory]
|
||||
if v not in valid_categories:
|
||||
raise ValueError(
|
||||
f"Invalid category '{v}'. Must be one of: {', '.join(valid_categories)}"
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class UpdateToolRequest(BaseModel):
|
||||
"""Request schema for updating a tool."""
|
||||
|
||||
name: Optional[str] = Field(default=None, max_length=255)
|
||||
description: Optional[str] = None
|
||||
icon: Optional[str] = Field(default=None, max_length=50)
|
||||
icon_color: Optional[str] = Field(default=None, max_length=7)
|
||||
definition: Optional[ToolDefinition] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class CreatedByResponse(BaseModel):
|
||||
"""Response schema for the user who created a tool."""
|
||||
|
||||
id: int
|
||||
provider_id: str
|
||||
|
||||
|
||||
class ToolResponse(BaseModel):
|
||||
"""Response schema for a tool."""
|
||||
|
||||
id: int
|
||||
tool_uuid: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
category: str
|
||||
icon: Optional[str]
|
||||
icon_color: Optional[str]
|
||||
status: str
|
||||
definition: Dict[str, Any]
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
created_by: Optional[CreatedByResponse] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class McpRefreshResponse(BaseModel):
|
||||
"""Result of re-discovering an MCP server's tool catalog."""
|
||||
|
||||
tool_uuid: str
|
||||
discovered_tools: list = Field(default_factory=list)
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def build_tool_response(tool, include_created_by: bool = False) -> ToolResponse:
|
||||
"""Build a response from a tool model."""
|
||||
created_by = None
|
||||
if include_created_by and tool.created_by_user:
|
||||
created_by = CreatedByResponse(
|
||||
id=tool.created_by_user.id,
|
||||
provider_id=tool.created_by_user.provider_id,
|
||||
)
|
||||
|
||||
return ToolResponse(
|
||||
id=tool.id,
|
||||
tool_uuid=tool.tool_uuid,
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
category=tool.category,
|
||||
icon=tool.icon,
|
||||
icon_color=tool.icon_color,
|
||||
status=tool.status,
|
||||
definition=tool.definition,
|
||||
created_at=tool.created_at,
|
||||
updated_at=tool.updated_at,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
|
||||
def validate_category(category: str) -> None:
|
||||
"""Validate that the category is valid."""
|
||||
valid_categories = [c.value for c in ToolCategory]
|
||||
|
|
@ -361,53 +126,13 @@ async def list_tools(
|
|||
return [build_tool_response(tool) for tool in tools]
|
||||
|
||||
|
||||
async def _fetch_credential(credential_uuid: Optional[str], organization_id: int):
|
||||
"""Best-effort credential lookup for MCP auth. A missing/failed credential
|
||||
degrades to ``None`` (unauthenticated) rather than failing the request."""
|
||||
if not credential_uuid:
|
||||
return None
|
||||
try:
|
||||
return await db_client.get_credential_by_uuid(credential_uuid, organization_id)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"MCP: credential fetch failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _populate_discovered_tools(definition: dict, *, organization_id: int) -> dict:
|
||||
"""Best-effort: for an MCP definition, connect to the server, list its
|
||||
tools, and overwrite ``config.discovered_tools``. Never raises and never
|
||||
blocks tool save — a dead server yields ``discovered_tools: []``. Non-MCP
|
||||
definitions pass through untouched."""
|
||||
if not isinstance(definition, dict) or definition.get("type") != "mcp":
|
||||
return definition
|
||||
try:
|
||||
cfg = validate_mcp_definition(definition)
|
||||
except McpDefinitionError:
|
||||
return definition
|
||||
|
||||
credential = await _fetch_credential(cfg.get("credential_uuid"), organization_id)
|
||||
|
||||
# Run discovery in an isolated asyncio task so an anyio cancel-scope
|
||||
# CancelledError doesn't bleed into the parent task and corrupt the
|
||||
# subsequent DB write. _run() never raises (degrades to []).
|
||||
async def _run() -> list:
|
||||
try:
|
||||
return await discover_mcp_tools(
|
||||
url=cfg["url"],
|
||||
credential=credential,
|
||||
timeout_secs=cfg["timeout_secs"],
|
||||
sse_read_timeout_secs=cfg["sse_read_timeout_secs"],
|
||||
)
|
||||
except BaseException as e: # noqa: BLE001
|
||||
logger.warning(f"MCP discovery failed; caching empty list: {e}")
|
||||
return []
|
||||
|
||||
discovered = await asyncio.ensure_future(_run())
|
||||
definition["config"]["discovered_tools"] = discovered
|
||||
return definition
|
||||
|
||||
|
||||
@router.post("/")
|
||||
@router.post(
|
||||
"/",
|
||||
**sdk_expose(
|
||||
method="create_tool",
|
||||
description="Create a reusable tool for the authenticated organization.",
|
||||
),
|
||||
)
|
||||
async def create_tool(
|
||||
request: CreateToolRequest,
|
||||
user: UserModel = Depends(get_user),
|
||||
|
|
@ -421,40 +146,10 @@ async def create_tool(
|
|||
Returns:
|
||||
The created tool
|
||||
"""
|
||||
if not user.selected_organization_id:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="No organization selected for the user"
|
||||
)
|
||||
|
||||
validate_category(request.category)
|
||||
|
||||
definition = await _populate_discovered_tools(
|
||||
request.definition.model_dump(),
|
||||
organization_id=user.selected_organization_id,
|
||||
)
|
||||
|
||||
tool = await db_client.create_tool(
|
||||
organization_id=user.selected_organization_id,
|
||||
user_id=user.id,
|
||||
name=request.name,
|
||||
definition=definition,
|
||||
category=request.category,
|
||||
description=request.description,
|
||||
icon=request.icon,
|
||||
icon_color=request.icon_color,
|
||||
)
|
||||
|
||||
capture_event(
|
||||
distinct_id=str(user.provider_id),
|
||||
event=PostHogEvent.TOOL_CREATED,
|
||||
properties={
|
||||
"tool_name": request.name,
|
||||
"tool_category": request.category,
|
||||
"organization_id": user.selected_organization_id,
|
||||
},
|
||||
)
|
||||
|
||||
return build_tool_response(tool)
|
||||
try:
|
||||
return await create_tool_for_user(request, user, source="api")
|
||||
except ToolManagementError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message) from e
|
||||
|
||||
|
||||
@router.get("/{tool_uuid}")
|
||||
|
|
@ -494,57 +189,10 @@ async def refresh_mcp_tools(
|
|||
"""Re-discover an MCP tool's server catalog and overwrite the cached
|
||||
``definition.config.discovered_tools``. Server down → 200 with error
|
||||
(cache not overwritten on transient failure)."""
|
||||
if not user.selected_organization_id:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="No organization selected for the user"
|
||||
)
|
||||
|
||||
tool = await db_client.get_tool_by_uuid(
|
||||
tool_uuid, user.selected_organization_id, include_archived=True
|
||||
)
|
||||
if not tool:
|
||||
raise HTTPException(status_code=404, detail="Tool not found")
|
||||
if tool.category != ToolCategory.MCP.value:
|
||||
raise HTTPException(status_code=400, detail="Tool is not an MCP tool")
|
||||
|
||||
try:
|
||||
cfg = validate_mcp_definition(tool.definition)
|
||||
except McpDefinitionError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid MCP definition: {e}")
|
||||
|
||||
credential = await _fetch_credential(
|
||||
cfg.get("credential_uuid"), user.selected_organization_id
|
||||
)
|
||||
|
||||
try:
|
||||
discovered = await discover_mcp_tools(
|
||||
url=cfg["url"],
|
||||
credential=credential,
|
||||
timeout_secs=cfg["timeout_secs"],
|
||||
sse_read_timeout_secs=cfg["sse_read_timeout_secs"],
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"MCP refresh discovery failed: {e}")
|
||||
discovered = []
|
||||
|
||||
if not discovered:
|
||||
error = (
|
||||
f"Could not reach the MCP server at {cfg['url']} "
|
||||
f"(or it exposes no tools). Previously cached list retained."
|
||||
)
|
||||
# Do NOT clobber a previously-good cache with [] on a transient outage.
|
||||
return McpRefreshResponse(tool_uuid=tool_uuid, discovered_tools=[], error=error)
|
||||
|
||||
new_def = dict(tool.definition or {})
|
||||
new_def["config"] = {**new_def.get("config", {}), "discovered_tools": discovered}
|
||||
await db_client.update_tool(
|
||||
tool_uuid=tool_uuid,
|
||||
organization_id=user.selected_organization_id,
|
||||
definition=new_def,
|
||||
)
|
||||
return McpRefreshResponse(
|
||||
tool_uuid=tool_uuid, discovered_tools=discovered, error=None
|
||||
)
|
||||
return await refresh_mcp_tool_for_user(tool_uuid, user)
|
||||
except ToolManagementError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message) from e
|
||||
|
||||
|
||||
@router.put("/{tool_uuid}")
|
||||
|
|
@ -571,14 +219,20 @@ async def update_tool(
|
|||
if request.status:
|
||||
validate_status(request.status)
|
||||
|
||||
definition = (
|
||||
await _populate_discovered_tools(
|
||||
request.definition.model_dump(),
|
||||
organization_id=user.selected_organization_id,
|
||||
)
|
||||
if request.definition
|
||||
else None
|
||||
)
|
||||
definition = None
|
||||
if request.definition:
|
||||
definition = request.definition.model_dump()
|
||||
try:
|
||||
await validate_tool_credential_references(
|
||||
definition,
|
||||
organization_id=user.selected_organization_id,
|
||||
)
|
||||
definition = await _populate_discovered_tools(
|
||||
definition,
|
||||
organization_id=user.selected_organization_id,
|
||||
)
|
||||
except ToolManagementError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message) from e
|
||||
|
||||
tool = await db_client.update_tool(
|
||||
tool_uuid=tool_uuid,
|
||||
|
|
|
|||
440
api/schemas/tool.py
Normal file
440
api/schemas/tool.py
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
"""Pydantic schemas for reusable Dograh tools.
|
||||
|
||||
These models are the single contract for tool creation/update across the
|
||||
REST API, generated SDKs, and the MCP authoring surface. Field descriptions
|
||||
are human/API-facing; ``llm_hint`` JSON schema extras are guidance for LLMs
|
||||
when the same schema is surfaced through MCP or SDK authoring flows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from api.enums import ToolCategory
|
||||
|
||||
DEFAULT_MCP_TIMEOUT_SECS = 30
|
||||
DEFAULT_MCP_SSE_READ_TIMEOUT_SECS = 300
|
||||
|
||||
ToolParameterType = Literal["string", "number", "boolean"]
|
||||
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
|
||||
ToolCategoryValue = Literal[
|
||||
"http_api",
|
||||
"end_call",
|
||||
"transfer_call",
|
||||
"calculator",
|
||||
"native",
|
||||
"integration",
|
||||
"mcp",
|
||||
]
|
||||
|
||||
|
||||
def _llm_hint(text: str) -> dict[str, str]:
|
||||
return {"llm_hint": text}
|
||||
|
||||
|
||||
class ToolParameter(BaseModel):
|
||||
"""A parameter that the tool accepts from the model at call time."""
|
||||
|
||||
name: str = Field(
|
||||
description="Parameter name used as a key in the tool request body.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Use a stable snake_case name the agent can naturally fill."
|
||||
),
|
||||
)
|
||||
type: ToolParameterType = Field(
|
||||
description="JSON type for the parameter value.",
|
||||
json_schema_extra=_llm_hint("Allowed values are string, number, and boolean."),
|
||||
)
|
||||
description: str = Field(
|
||||
description="Description shown to the model for this parameter.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Write this as an instruction to the agent: what value to provide and when."
|
||||
),
|
||||
)
|
||||
required: bool = Field(
|
||||
default=True,
|
||||
description="Whether this parameter is required when the tool is called.",
|
||||
)
|
||||
|
||||
|
||||
class PresetToolParameter(BaseModel):
|
||||
"""A parameter injected by Dograh at runtime."""
|
||||
|
||||
name: str = Field(description="Parameter name used as a key in the request body.")
|
||||
type: ToolParameterType = Field(description="JSON type for the resolved value.")
|
||||
value_template: str = Field(
|
||||
description="Fixed value or template, e.g. {{initial_context.phone_number}}.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Use {{initial_context.*}} for call-start context and "
|
||||
"{{gathered_context.*}} for values extracted during the call."
|
||||
),
|
||||
)
|
||||
required: bool = Field(
|
||||
default=True,
|
||||
description="Whether the parameter must resolve to a non-empty value.",
|
||||
)
|
||||
|
||||
|
||||
class HttpApiConfig(BaseModel):
|
||||
"""Configuration for HTTP API tools."""
|
||||
|
||||
method: HttpMethod = Field(
|
||||
description="HTTP method to use for the request.",
|
||||
json_schema_extra=_llm_hint("Use one of GET, POST, PUT, PATCH, DELETE."),
|
||||
)
|
||||
url: str = Field(
|
||||
description="Target HTTP or HTTPS URL.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Use the final endpoint URL. Authentication belongs in credential_uuid, "
|
||||
"not embedded in the URL."
|
||||
),
|
||||
)
|
||||
headers: Optional[Dict[str, str]] = Field(
|
||||
default=None,
|
||||
description="Static headers to include with every request.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Do not place secrets here. Store secrets in the UI credential manager "
|
||||
"and reference them with credential_uuid."
|
||||
),
|
||||
)
|
||||
credential_uuid: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Reference to an external credential for request authentication.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Use a credential_uuid returned by list_credentials. The MCP flow does "
|
||||
"not create credential secrets."
|
||||
),
|
||||
)
|
||||
parameters: Optional[List[ToolParameter]] = Field(
|
||||
default=None,
|
||||
description="Parameters the model must provide when calling this tool.",
|
||||
)
|
||||
preset_parameters: Optional[List[PresetToolParameter]] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Parameters injected by Dograh from fixed values or workflow context "
|
||||
"templates."
|
||||
),
|
||||
)
|
||||
timeout_ms: Optional[int] = Field(
|
||||
default=5000,
|
||||
ge=1,
|
||||
description="Request timeout in milliseconds.",
|
||||
)
|
||||
customMessage: Optional[str] = Field(
|
||||
default=None, description="Custom message to play after tool execution."
|
||||
)
|
||||
customMessageType: Optional[Literal["text", "audio"]] = Field(
|
||||
default=None, description="Type of custom message."
|
||||
)
|
||||
customMessageRecordingId: Optional[str] = Field(
|
||||
default=None, description="Recording ID for an audio custom message."
|
||||
)
|
||||
|
||||
@field_validator("method", mode="before")
|
||||
@classmethod
|
||||
def validate_method(cls, v: Any) -> str:
|
||||
if not isinstance(v, str):
|
||||
raise ValueError("method must be one of GET, POST, PUT, PATCH, DELETE")
|
||||
method = v.upper()
|
||||
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
|
||||
raise ValueError("method must be one of GET, POST, PUT, PATCH, DELETE")
|
||||
return method
|
||||
|
||||
|
||||
class EndCallConfig(BaseModel):
|
||||
"""Configuration for End Call tools."""
|
||||
|
||||
messageType: Literal["none", "custom", "audio"] = Field(
|
||||
default="none", description="Type of goodbye message."
|
||||
)
|
||||
customMessage: Optional[str] = Field(
|
||||
default=None, description="Custom message to play before ending the call."
|
||||
)
|
||||
audioRecordingId: Optional[str] = Field(
|
||||
default=None, description="Recording ID for audio goodbye message."
|
||||
)
|
||||
endCallReason: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"When enabled, the model must provide a reason for ending the call. "
|
||||
"The reason is set as call disposition and added to call tags."
|
||||
),
|
||||
)
|
||||
endCallReasonDescription: Optional[str] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Description shown to the model for the reason parameter. Used only "
|
||||
"when endCallReason is enabled."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TransferCallConfig(BaseModel):
|
||||
"""Configuration for Transfer Call tools."""
|
||||
|
||||
destination: str = Field(
|
||||
description=(
|
||||
"Phone number or SIP endpoint to transfer the call to, e.g. "
|
||||
"+1234567890 or PJSIP/1234."
|
||||
)
|
||||
)
|
||||
messageType: Literal["none", "custom", "audio"] = Field(
|
||||
default="none", description="Type of message to play before transfer."
|
||||
)
|
||||
customMessage: Optional[str] = Field(
|
||||
default=None, description="Custom message to play before transferring."
|
||||
)
|
||||
audioRecordingId: Optional[str] = Field(
|
||||
default=None, description="Recording ID for audio message before transfer."
|
||||
)
|
||||
timeout: int = Field(
|
||||
default=30,
|
||||
ge=5,
|
||||
le=120,
|
||||
description="Maximum seconds to wait for the destination to answer.",
|
||||
)
|
||||
|
||||
@field_validator("destination")
|
||||
@classmethod
|
||||
def validate_destination(cls, v: str) -> str:
|
||||
"""Validate that destination is a valid E.164 phone number or SIP endpoint."""
|
||||
if not v.strip():
|
||||
return v
|
||||
|
||||
e164_pattern = r"^\+[1-9]\d{1,14}$"
|
||||
sip_pattern = r"^(PJSIP|SIP)/[\w\-\.@]+$"
|
||||
|
||||
is_valid_e164 = re.match(e164_pattern, v)
|
||||
is_valid_sip = re.match(sip_pattern, v, re.IGNORECASE)
|
||||
|
||||
if not (is_valid_e164 or is_valid_sip):
|
||||
raise ValueError(
|
||||
"Destination must be a valid E.164 phone number "
|
||||
"(e.g., +1234567890) or SIP endpoint (e.g., PJSIP/1234)"
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class McpToolConfig(BaseModel):
|
||||
"""Configuration for a customer MCP server tool definition."""
|
||||
|
||||
transport: Literal["streamable_http"] = Field(
|
||||
default="streamable_http",
|
||||
description="MCP transport protocol.",
|
||||
)
|
||||
url: str = Field(
|
||||
description="MCP server URL. Must use http:// or https://.",
|
||||
json_schema_extra=_llm_hint("Use the server's streamable HTTP MCP endpoint."),
|
||||
)
|
||||
credential_uuid: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Reference to an external credential for MCP server auth.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Use a credential_uuid returned by list_credentials. Credentials are "
|
||||
"created by the user in the UI."
|
||||
),
|
||||
)
|
||||
tools_filter: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Allowlist of MCP tool names to expose. Empty exposes all tools.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Use exact MCP tool names from the remote server catalog when you need "
|
||||
"to restrict the exposed tools."
|
||||
),
|
||||
)
|
||||
timeout_secs: int = Field(
|
||||
default=DEFAULT_MCP_TIMEOUT_SECS,
|
||||
ge=0,
|
||||
description="Connection timeout in seconds.",
|
||||
)
|
||||
sse_read_timeout_secs: int = Field(
|
||||
default=DEFAULT_MCP_SSE_READ_TIMEOUT_SECS,
|
||||
ge=0,
|
||||
description="SSE read timeout in seconds.",
|
||||
)
|
||||
discovered_tools: list[dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Server-managed cache of the MCP server's tool catalog "
|
||||
"[{name, description}]. Populated best-effort by the backend."
|
||||
),
|
||||
json_schema_extra=_llm_hint("Do not author this field; the server fills it."),
|
||||
)
|
||||
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_url(cls, v: str) -> str:
|
||||
if not isinstance(v, str) or not v.startswith(("http://", "https://")):
|
||||
raise ValueError("config.url must be an http(s) URL")
|
||||
return v
|
||||
|
||||
@field_validator("tools_filter")
|
||||
@classmethod
|
||||
def validate_tools_filter(cls, v: list[str]) -> list[str]:
|
||||
if not all(isinstance(tool_name, str) for tool_name in v):
|
||||
raise ValueError("config.tools_filter must be a list of strings")
|
||||
return v
|
||||
|
||||
|
||||
class HttpApiToolDefinition(BaseModel):
|
||||
"""Tool definition for HTTP API tools."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version.")
|
||||
type: Literal["http_api"] = Field(description="Tool type.")
|
||||
config: HttpApiConfig = Field(description="HTTP API configuration.")
|
||||
|
||||
|
||||
class EndCallToolDefinition(BaseModel):
|
||||
"""Tool definition for End Call tools."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version.")
|
||||
type: Literal["end_call"] = Field(description="Tool type.")
|
||||
config: EndCallConfig = Field(description="End Call configuration.")
|
||||
|
||||
|
||||
class TransferCallToolDefinition(BaseModel):
|
||||
"""Tool definition for Transfer Call tools."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version.")
|
||||
type: Literal["transfer_call"] = Field(description="Tool type.")
|
||||
config: TransferCallConfig = Field(description="Transfer Call configuration.")
|
||||
|
||||
|
||||
class CalculatorToolDefinition(BaseModel):
|
||||
"""Tool definition for Calculator tools."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version.")
|
||||
type: Literal["calculator"] = Field(description="Tool type.")
|
||||
|
||||
|
||||
class McpToolDefinition(BaseModel):
|
||||
"""Persisted MCP tool definition."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version.")
|
||||
type: Literal["mcp"] = Field(description="Tool type.")
|
||||
config: McpToolConfig = Field(description="MCP server configuration.")
|
||||
|
||||
|
||||
ToolDefinition = Annotated[
|
||||
Union[
|
||||
HttpApiToolDefinition,
|
||||
EndCallToolDefinition,
|
||||
TransferCallToolDefinition,
|
||||
CalculatorToolDefinition,
|
||||
McpToolDefinition,
|
||||
],
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
|
||||
class CreateToolRequest(BaseModel):
|
||||
"""Request schema for creating a reusable tool."""
|
||||
|
||||
name: str = Field(
|
||||
max_length=255,
|
||||
description="Display name for the tool.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"Use a concise action-oriented name; this influences the function "
|
||||
"name shown to the agent."
|
||||
),
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Description shown to the agent when deciding whether to call it.",
|
||||
json_schema_extra=_llm_hint(
|
||||
"State exactly when the agent should call the tool and what result it gets."
|
||||
),
|
||||
)
|
||||
category: ToolCategoryValue = Field(
|
||||
default=ToolCategory.HTTP_API.value,
|
||||
description="Tool category. Must match definition.type.",
|
||||
)
|
||||
icon: Optional[str] = Field(
|
||||
default="globe", max_length=50, description="Lucide icon identifier."
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
default="#3B82F6", max_length=7, description="Hex color for the tool icon."
|
||||
)
|
||||
definition: ToolDefinition = Field(description="Typed tool definition.")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def default_category_from_definition(cls, data: Any) -> Any:
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
if data.get("category"):
|
||||
return data
|
||||
definition = data.get("definition")
|
||||
if isinstance(definition, dict) and definition.get("type"):
|
||||
return {**data, "category": definition["type"]}
|
||||
return data
|
||||
|
||||
@field_validator("category")
|
||||
@classmethod
|
||||
def validate_category(cls, v: str) -> str:
|
||||
valid_categories = [c.value for c in ToolCategory]
|
||||
if v not in valid_categories:
|
||||
raise ValueError(
|
||||
f"Invalid category '{v}'. Must be one of: {', '.join(valid_categories)}"
|
||||
)
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_category_matches_definition(self) -> "CreateToolRequest":
|
||||
definition_type = self.definition.type
|
||||
if self.category != definition_type:
|
||||
raise ValueError(
|
||||
f"category '{self.category}' must match definition.type "
|
||||
f"'{definition_type}'"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class UpdateToolRequest(BaseModel):
|
||||
"""Request schema for updating a reusable tool."""
|
||||
|
||||
name: Optional[str] = Field(default=None, max_length=255)
|
||||
description: Optional[str] = None
|
||||
icon: Optional[str] = Field(default=None, max_length=50)
|
||||
icon_color: Optional[str] = Field(default=None, max_length=7)
|
||||
definition: Optional[ToolDefinition] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class CreatedByResponse(BaseModel):
|
||||
"""Response schema for the user who created a tool."""
|
||||
|
||||
id: int
|
||||
provider_id: str
|
||||
|
||||
|
||||
class ToolResponse(BaseModel):
|
||||
"""Response schema for a reusable tool."""
|
||||
|
||||
id: int
|
||||
tool_uuid: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
category: str
|
||||
icon: Optional[str]
|
||||
icon_color: Optional[str]
|
||||
status: str
|
||||
definition: Dict[str, Any]
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime]
|
||||
created_by: Optional[CreatedByResponse] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class McpRefreshResponse(BaseModel):
|
||||
"""Result of re-discovering an MCP server's tool catalog."""
|
||||
|
||||
tool_uuid: str
|
||||
discovered_tools: list = Field(default_factory=list)
|
||||
error: Optional[str] = None
|
||||
251
api/services/tool_management.py
Normal file
251
api/services/tool_management.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
"""Service layer for reusable tool management.
|
||||
|
||||
Routes and MCP tools both use this module so validation, credential
|
||||
scoping, MCP discovery, and analytics stay consistent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from api.db import db_client
|
||||
from api.db.models import UserModel
|
||||
from api.enums import PostHogEvent, ToolCategory
|
||||
from api.schemas.tool import (
|
||||
CreatedByResponse,
|
||||
CreateToolRequest,
|
||||
McpRefreshResponse,
|
||||
ToolResponse,
|
||||
)
|
||||
from api.services.posthog_client import capture_event
|
||||
from api.services.workflow.mcp_tool_session import discover_mcp_tools
|
||||
from api.services.workflow.tools.mcp_tool import (
|
||||
McpDefinitionError,
|
||||
validate_mcp_definition,
|
||||
)
|
||||
|
||||
|
||||
class ToolManagementError(ValueError):
|
||||
"""Recoverable tool-management error with an MCP/HTTP friendly code."""
|
||||
|
||||
def __init__(self, error_code: str, message: str, *, status_code: int = 400):
|
||||
super().__init__(message)
|
||||
self.error_code = error_code
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def build_tool_response(tool: Any, include_created_by: bool = False) -> ToolResponse:
|
||||
"""Build a public response from a ToolModel-like object."""
|
||||
created_by = None
|
||||
if include_created_by and tool.created_by_user:
|
||||
created_by = CreatedByResponse(
|
||||
id=tool.created_by_user.id,
|
||||
provider_id=tool.created_by_user.provider_id,
|
||||
)
|
||||
|
||||
return ToolResponse(
|
||||
id=tool.id,
|
||||
tool_uuid=tool.tool_uuid,
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
category=tool.category,
|
||||
icon=tool.icon,
|
||||
icon_color=tool.icon_color,
|
||||
status=tool.status,
|
||||
definition=tool.definition,
|
||||
created_at=tool.created_at,
|
||||
updated_at=tool.updated_at,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
|
||||
def _credential_uuid_from_definition(definition: dict[str, Any]) -> Optional[str]:
|
||||
config = definition.get("config")
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
credential_uuid = config.get("credential_uuid")
|
||||
return credential_uuid if isinstance(credential_uuid, str) else None
|
||||
|
||||
|
||||
async def fetch_credential(credential_uuid: Optional[str], organization_id: int):
|
||||
"""Best-effort credential lookup for MCP auth/discovery."""
|
||||
if not credential_uuid:
|
||||
return None
|
||||
try:
|
||||
return await db_client.get_credential_by_uuid(credential_uuid, organization_id)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"Tool credential fetch failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def validate_tool_credential_references(
|
||||
definition: dict[str, Any], *, organization_id: int
|
||||
) -> None:
|
||||
"""Ensure credential UUID references belong to the caller's organization."""
|
||||
credential_uuid = _credential_uuid_from_definition(definition)
|
||||
if not credential_uuid:
|
||||
return
|
||||
|
||||
credential = await db_client.get_credential_by_uuid(
|
||||
credential_uuid, organization_id
|
||||
)
|
||||
if not credential:
|
||||
raise ToolManagementError(
|
||||
"credential_not_found",
|
||||
(
|
||||
f"Credential '{credential_uuid}' was not found in this organization. "
|
||||
"Create it in the UI first, then retry with its credential_uuid."
|
||||
),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
|
||||
async def populate_discovered_tools(
|
||||
definition: dict[str, Any], *, organization_id: int
|
||||
) -> dict[str, Any]:
|
||||
"""Best-effort MCP discovery before saving a tool definition.
|
||||
|
||||
Non-MCP definitions pass through untouched. For MCP definitions, a dead
|
||||
server yields ``discovered_tools: []`` and does not block creation.
|
||||
"""
|
||||
if not isinstance(definition, dict) or definition.get("type") != "mcp":
|
||||
return definition
|
||||
try:
|
||||
cfg = validate_mcp_definition(definition)
|
||||
except McpDefinitionError:
|
||||
return definition
|
||||
|
||||
credential = await fetch_credential(cfg.get("credential_uuid"), organization_id)
|
||||
|
||||
async def _run() -> list:
|
||||
try:
|
||||
return await discover_mcp_tools(
|
||||
url=cfg["url"],
|
||||
credential=credential,
|
||||
timeout_secs=cfg["timeout_secs"],
|
||||
sse_read_timeout_secs=cfg["sse_read_timeout_secs"],
|
||||
)
|
||||
except BaseException as e: # noqa: BLE001
|
||||
logger.warning(f"MCP discovery failed; caching empty list: {e}")
|
||||
return []
|
||||
|
||||
discovered = await asyncio.ensure_future(_run())
|
||||
definition["config"]["discovered_tools"] = discovered
|
||||
return definition
|
||||
|
||||
|
||||
async def create_tool_for_user(
|
||||
request: CreateToolRequest,
|
||||
user: UserModel,
|
||||
*,
|
||||
source: str = "api",
|
||||
) -> ToolResponse:
|
||||
"""Create a reusable tool for the authenticated user's selected org."""
|
||||
if not user.selected_organization_id:
|
||||
raise ToolManagementError(
|
||||
"organization_required",
|
||||
"No organization selected for the user",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
definition = request.definition.model_dump()
|
||||
await validate_tool_credential_references(
|
||||
definition, organization_id=user.selected_organization_id
|
||||
)
|
||||
definition = await populate_discovered_tools(
|
||||
definition,
|
||||
organization_id=user.selected_organization_id,
|
||||
)
|
||||
|
||||
tool = await db_client.create_tool(
|
||||
organization_id=user.selected_organization_id,
|
||||
user_id=user.id,
|
||||
name=request.name,
|
||||
definition=definition,
|
||||
category=request.category,
|
||||
description=request.description,
|
||||
icon=request.icon,
|
||||
icon_color=request.icon_color,
|
||||
)
|
||||
|
||||
capture_event(
|
||||
distinct_id=str(user.provider_id),
|
||||
event=PostHogEvent.TOOL_CREATED,
|
||||
properties={
|
||||
"tool_name": request.name,
|
||||
"tool_category": request.category,
|
||||
"source": source,
|
||||
"organization_id": user.selected_organization_id,
|
||||
},
|
||||
)
|
||||
|
||||
return build_tool_response(tool)
|
||||
|
||||
|
||||
async def refresh_mcp_tool_for_user(
|
||||
tool_uuid: str,
|
||||
user: UserModel,
|
||||
) -> McpRefreshResponse:
|
||||
"""Refresh cached MCP catalog for a tool owned by the user's org."""
|
||||
if not user.selected_organization_id:
|
||||
raise ToolManagementError(
|
||||
"organization_required",
|
||||
"No organization selected for the user",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
tool = await db_client.get_tool_by_uuid(
|
||||
tool_uuid, user.selected_organization_id, include_archived=True
|
||||
)
|
||||
if not tool:
|
||||
raise ToolManagementError("tool_not_found", "Tool not found", status_code=404)
|
||||
if tool.category != ToolCategory.MCP.value:
|
||||
raise ToolManagementError(
|
||||
"not_mcp_tool", "Tool is not an MCP tool", status_code=400
|
||||
)
|
||||
|
||||
try:
|
||||
cfg = validate_mcp_definition(tool.definition)
|
||||
except McpDefinitionError as e:
|
||||
raise ToolManagementError(
|
||||
"invalid_mcp_definition",
|
||||
f"Invalid MCP definition: {e}",
|
||||
status_code=400,
|
||||
) from e
|
||||
|
||||
credential = await fetch_credential(
|
||||
cfg.get("credential_uuid"), user.selected_organization_id
|
||||
)
|
||||
|
||||
try:
|
||||
discovered = await discover_mcp_tools(
|
||||
url=cfg["url"],
|
||||
credential=credential,
|
||||
timeout_secs=cfg["timeout_secs"],
|
||||
sse_read_timeout_secs=cfg["sse_read_timeout_secs"],
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"MCP refresh discovery failed: {e}")
|
||||
discovered = []
|
||||
|
||||
if not discovered:
|
||||
error = (
|
||||
f"Could not reach the MCP server at {cfg['url']} "
|
||||
f"(or it exposes no tools). Previously cached list retained."
|
||||
)
|
||||
return McpRefreshResponse(tool_uuid=tool_uuid, discovered_tools=[], error=error)
|
||||
|
||||
new_def = dict(tool.definition or {})
|
||||
new_def["config"] = {**new_def.get("config", {}), "discovered_tools": discovered}
|
||||
await db_client.update_tool(
|
||||
tool_uuid=tool_uuid,
|
||||
organization_id=user.selected_organization_id,
|
||||
definition=new_def,
|
||||
)
|
||||
return McpRefreshResponse(
|
||||
tool_uuid=tool_uuid, discovered_tools=discovered, error=None
|
||||
)
|
||||
|
|
@ -4,70 +4,27 @@ LLM-function-name namespacing. No I/O, no MCP protocol here."""
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, Literal, Optional
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError, field_validator
|
||||
from pydantic import ValidationError
|
||||
|
||||
DEFAULT_TIMEOUT_SECS = 30
|
||||
DEFAULT_SSE_READ_TIMEOUT_SECS = 300
|
||||
from api.schemas.tool import (
|
||||
DEFAULT_MCP_SSE_READ_TIMEOUT_SECS,
|
||||
DEFAULT_MCP_TIMEOUT_SECS,
|
||||
McpToolDefinition,
|
||||
)
|
||||
from api.schemas.tool import (
|
||||
McpToolConfig as McpToolConfig,
|
||||
)
|
||||
|
||||
DEFAULT_TIMEOUT_SECS = DEFAULT_MCP_TIMEOUT_SECS
|
||||
DEFAULT_SSE_READ_TIMEOUT_SECS = DEFAULT_MCP_SSE_READ_TIMEOUT_SECS
|
||||
|
||||
|
||||
class McpDefinitionError(ValueError):
|
||||
"""Raised when an MCP tool definition is structurally invalid."""
|
||||
|
||||
|
||||
class McpToolConfig(BaseModel):
|
||||
"""Configuration for an MCP tool definition."""
|
||||
|
||||
transport: Literal["streamable_http"] = Field(
|
||||
default="streamable_http", description="MCP transport protocol"
|
||||
)
|
||||
url: str = Field(description="MCP server URL (must be http:// or https://)")
|
||||
credential_uuid: Optional[str] = Field(
|
||||
default=None, description="Reference to ExternalCredentialModel for auth"
|
||||
)
|
||||
tools_filter: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Allowlist of MCP tool names to expose (empty = all tools)",
|
||||
)
|
||||
timeout_secs: int = Field(
|
||||
default=DEFAULT_TIMEOUT_SECS, description="Connection timeout in seconds"
|
||||
)
|
||||
sse_read_timeout_secs: int = Field(
|
||||
default=DEFAULT_SSE_READ_TIMEOUT_SECS,
|
||||
description="SSE read timeout in seconds",
|
||||
)
|
||||
discovered_tools: list[dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Server-managed cache of the MCP server's tool catalog "
|
||||
"[{name, description}]. Populated best-effort by the backend."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("url")
|
||||
@classmethod
|
||||
def validate_url(cls, v: str) -> str:
|
||||
if not isinstance(v, str) or not v.startswith(("http://", "https://")):
|
||||
raise ValueError("config.url must be an http(s) URL")
|
||||
return v
|
||||
|
||||
@field_validator("tools_filter")
|
||||
@classmethod
|
||||
def validate_tools_filter(cls, v: list[str]) -> list[str]:
|
||||
if not all(isinstance(tool_name, str) for tool_name in v):
|
||||
raise ValueError("config.tools_filter must be a list of strings")
|
||||
return v
|
||||
|
||||
|
||||
class McpToolDefinition(BaseModel):
|
||||
"""Persisted MCP tool definition."""
|
||||
|
||||
schema_version: int = Field(default=1, description="Schema version")
|
||||
type: Literal["mcp"] = Field(description="Tool type")
|
||||
config: McpToolConfig = Field(description="MCP server configuration")
|
||||
|
||||
|
||||
def _format_validation_error(error: ValidationError) -> str:
|
||||
parts: list[str] = []
|
||||
for item in error.errors():
|
||||
|
|
|
|||
164
api/tests/test_mcp_tool_creation.py
Normal file
164
api/tests/test_mcp_tool_creation.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
from api.app import app
|
||||
from api.mcp_server.server import mcp
|
||||
from api.mcp_server.tools.tool_creation import create_tool
|
||||
from api.schemas.tool import CreateToolRequest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authed_user() -> MagicMock:
|
||||
user = MagicMock()
|
||||
user.id = 11
|
||||
user.provider_id = "provider-11"
|
||||
user.selected_organization_id = 22
|
||||
return user
|
||||
|
||||
|
||||
def _tool_model(**overrides):
|
||||
now = datetime.now(UTC)
|
||||
values = {
|
||||
"id": 3,
|
||||
"tool_uuid": "tool-uuid-3",
|
||||
"name": "Lookup Account",
|
||||
"description": "Lookup an account by phone number",
|
||||
"category": "http_api",
|
||||
"icon": "globe",
|
||||
"icon_color": "#3B82F6",
|
||||
"status": "active",
|
||||
"definition": {
|
||||
"schema_version": 1,
|
||||
"type": "http_api",
|
||||
"config": {"method": "POST", "url": "https://api.example.com/lookup"},
|
||||
},
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
def _http_tool_request(**config_overrides) -> CreateToolRequest:
|
||||
config = {"method": "post", "url": "https://api.example.com/lookup"}
|
||||
config.update(config_overrides)
|
||||
return CreateToolRequest(
|
||||
name="Lookup Account",
|
||||
description="Lookup an account by phone number",
|
||||
definition={
|
||||
"schema_version": 1,
|
||||
"type": "http_api",
|
||||
"config": config,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_create_tool_creates_reusable_tool(authed_user: MagicMock):
|
||||
create_tool_mock = AsyncMock(return_value=_tool_model())
|
||||
|
||||
with (
|
||||
patch(
|
||||
"api.mcp_server.tools.tool_creation.authenticate_mcp_request",
|
||||
AsyncMock(return_value=authed_user),
|
||||
),
|
||||
patch(
|
||||
"api.services.tool_management.db_client.create_tool",
|
||||
create_tool_mock,
|
||||
),
|
||||
patch("api.services.tool_management.capture_event") as capture_event_mock,
|
||||
):
|
||||
result = await create_tool(_http_tool_request())
|
||||
|
||||
assert result["created"] is True
|
||||
assert result["tool_uuid"] == "tool-uuid-3"
|
||||
assert result["category"] == "http_api"
|
||||
create_tool_mock.assert_awaited_once()
|
||||
assert create_tool_mock.call_args.kwargs["organization_id"] == 22
|
||||
assert create_tool_mock.call_args.kwargs["user_id"] == 11
|
||||
assert create_tool_mock.call_args.kwargs["definition"]["config"]["method"] == "POST"
|
||||
capture_event_mock.assert_called_once()
|
||||
assert capture_event_mock.call_args.kwargs["properties"]["source"] == "mcp"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_create_tool_rejects_unknown_credential(authed_user: MagicMock):
|
||||
create_tool_mock = AsyncMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"api.mcp_server.tools.tool_creation.authenticate_mcp_request",
|
||||
AsyncMock(return_value=authed_user),
|
||||
),
|
||||
patch(
|
||||
"api.services.tool_management.db_client.get_credential_by_uuid",
|
||||
AsyncMock(return_value=None),
|
||||
),
|
||||
patch(
|
||||
"api.services.tool_management.db_client.create_tool",
|
||||
create_tool_mock,
|
||||
),
|
||||
):
|
||||
result = await create_tool(_http_tool_request(credential_uuid="cred-missing"))
|
||||
|
||||
assert result["created"] is False
|
||||
assert result["error_code"] == "credential_not_found"
|
||||
create_tool_mock.assert_not_awaited()
|
||||
|
||||
|
||||
def test_sdk_openapi_exposes_create_tool_schema_and_llm_hints():
|
||||
sdk_routes = [
|
||||
r
|
||||
for r in app.routes
|
||||
if getattr(r, "openapi_extra", None)
|
||||
and "x-sdk-method" in (r.openapi_extra or {})
|
||||
]
|
||||
spec = get_openapi(title=app.title, version=app.version, routes=sdk_routes)
|
||||
operations = [
|
||||
op
|
||||
for path_item in spec["paths"].values()
|
||||
for op in path_item.values()
|
||||
if isinstance(op, dict)
|
||||
]
|
||||
assert any(op.get("x-sdk-method") == "create_tool" for op in operations)
|
||||
|
||||
credential_schema = spec["components"]["schemas"]["HttpApiConfig"]["properties"][
|
||||
"credential_uuid"
|
||||
]
|
||||
assert "list_credentials" in credential_schema["llm_hint"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_create_tool_schema_includes_validation_and_llm_hints():
|
||||
tools = await mcp.list_tools()
|
||||
create_tool_spec = next(t for t in tools if t.name == "create_tool")
|
||||
|
||||
request_schema = create_tool_spec.parameters["properties"]["request"]
|
||||
definition_schema = request_schema["properties"]["definition"]
|
||||
http_config = definition_schema["oneOf"][0]["properties"]["config"]
|
||||
|
||||
assert request_schema["properties"]["category"]["enum"] == [
|
||||
"http_api",
|
||||
"end_call",
|
||||
"transfer_call",
|
||||
"calculator",
|
||||
"native",
|
||||
"integration",
|
||||
"mcp",
|
||||
]
|
||||
assert http_config["properties"]["method"]["enum"] == [
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
]
|
||||
assert (
|
||||
"list_credentials" in http_config["properties"]["credential_uuid"]["llm_hint"]
|
||||
)
|
||||
|
|
@ -16,10 +16,20 @@ Test coverage:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from api.routes.tool import CreateToolRequest, McpToolDefinition, UpdateToolRequest
|
||||
from api.routes.tool import (
|
||||
CreateToolRequest,
|
||||
McpToolConfig,
|
||||
McpToolDefinition,
|
||||
UpdateToolRequest,
|
||||
_populate_discovered_tools,
|
||||
refresh_mcp_tools,
|
||||
)
|
||||
from api.services.workflow.tools.mcp_tool import (
|
||||
validate_mcp_definition,
|
||||
)
|
||||
|
|
@ -279,10 +289,6 @@ async def test_post_tool_mcp_invalid_url_returns_422(test_client_factory, db_ses
|
|||
|
||||
# ── Task 6: discovered_tools field and _populate_discovered_tools helper ──────
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from api.routes.tool import McpToolConfig, _populate_discovered_tools
|
||||
|
||||
|
||||
def test_mcp_config_accepts_discovered_tools():
|
||||
cfg = McpToolConfig(
|
||||
|
|
@ -296,10 +302,10 @@ def test_mcp_config_accepts_discovered_tools():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_populate_discovered_tools_overwrites_cache(monkeypatch):
|
||||
import api.routes.tool as tool_mod
|
||||
import api.services.tool_management as tool_svc
|
||||
|
||||
monkeypatch.setattr(
|
||||
tool_mod,
|
||||
tool_svc,
|
||||
"discover_mcp_tools",
|
||||
AsyncMock(return_value=[{"name": "echo", "description": "Echo"}]),
|
||||
)
|
||||
|
|
@ -327,10 +333,10 @@ async def test_populate_discovered_tools_non_mcp_is_noop():
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_populate_discovered_tools_server_down_sets_empty(monkeypatch):
|
||||
import api.routes.tool as tool_mod
|
||||
import api.services.tool_management as tool_svc
|
||||
|
||||
monkeypatch.setattr(
|
||||
tool_mod,
|
||||
tool_svc,
|
||||
"discover_mcp_tools",
|
||||
AsyncMock(side_effect=RuntimeError("connection refused")),
|
||||
)
|
||||
|
|
@ -345,10 +351,6 @@ async def test_populate_discovered_tools_server_down_sets_empty(monkeypatch):
|
|||
|
||||
# ── Task 7: POST /{tool_uuid}/mcp/refresh ─────────────────────────────────────
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from api.routes.tool import refresh_mcp_tools
|
||||
|
||||
|
||||
def _fake_user(org_id=1):
|
||||
u = MagicMock()
|
||||
|
|
@ -373,19 +375,19 @@ def _mcp_tool_model(org_id=1):
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_success(monkeypatch):
|
||||
import api.routes.tool as tool_mod
|
||||
import api.services.tool_management as tool_svc
|
||||
|
||||
tool = _mcp_tool_model()
|
||||
monkeypatch.setattr(
|
||||
tool_mod.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
|
||||
tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
tool_mod.db_client,
|
||||
tool_svc.db_client,
|
||||
"update_tool",
|
||||
AsyncMock(return_value=tool),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
tool_mod,
|
||||
tool_svc,
|
||||
"discover_mcp_tools",
|
||||
AsyncMock(return_value=[{"name": "echo", "description": "Echo"}]),
|
||||
)
|
||||
|
|
@ -396,29 +398,29 @@ async def test_refresh_success(monkeypatch):
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_server_down_returns_200_with_error(monkeypatch):
|
||||
import api.routes.tool as tool_mod
|
||||
import api.services.tool_management as tool_svc
|
||||
|
||||
tool = _mcp_tool_model()
|
||||
monkeypatch.setattr(
|
||||
tool_mod.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
|
||||
tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
|
||||
)
|
||||
monkeypatch.setattr(tool_mod.db_client, "update_tool", AsyncMock(return_value=tool))
|
||||
monkeypatch.setattr(tool_mod, "discover_mcp_tools", AsyncMock(return_value=[]))
|
||||
monkeypatch.setattr(tool_svc.db_client, "update_tool", AsyncMock(return_value=tool))
|
||||
monkeypatch.setattr(tool_svc, "discover_mcp_tools", AsyncMock(return_value=[]))
|
||||
resp = await refresh_mcp_tools("tu-mcp", user=_fake_user())
|
||||
assert resp.discovered_tools == []
|
||||
assert resp.error # non-empty human-readable message
|
||||
# update_tool should NOT be called when discovery returns empty
|
||||
tool_mod.db_client.update_tool.assert_not_called()
|
||||
tool_svc.db_client.update_tool.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_non_mcp_is_400(monkeypatch):
|
||||
import api.routes.tool as tool_mod
|
||||
import api.services.tool_management as tool_svc
|
||||
|
||||
tool = _mcp_tool_model()
|
||||
tool.category = "http_api"
|
||||
monkeypatch.setattr(
|
||||
tool_mod.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
|
||||
tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=tool)
|
||||
)
|
||||
with pytest.raises(HTTPException) as ei:
|
||||
await refresh_mcp_tools("tu-mcp", user=_fake_user())
|
||||
|
|
@ -427,10 +429,10 @@ async def test_refresh_non_mcp_is_400(monkeypatch):
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_not_found_is_404(monkeypatch):
|
||||
import api.routes.tool as tool_mod
|
||||
import api.services.tool_management as tool_svc
|
||||
|
||||
monkeypatch.setattr(
|
||||
tool_mod.db_client, "get_tool_by_uuid", AsyncMock(return_value=None)
|
||||
tool_svc.db_client, "get_tool_by_uuid", AsyncMock(return_value=None)
|
||||
)
|
||||
with pytest.raises(HTTPException) as ei:
|
||||
await refresh_mcp_tools("nope", user=_fake_user())
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -12,6 +12,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from dograh_sdk._generated_models import (
|
||||
CreateToolRequest,
|
||||
CreateWorkflowRequest,
|
||||
CredentialResponse,
|
||||
DocumentListResponseSchema,
|
||||
|
|
@ -29,6 +30,11 @@ from dograh_sdk._generated_models import (
|
|||
class _GeneratedClient:
|
||||
# `DograhClient.__init__` installs `self._request` (see client.py).
|
||||
|
||||
def create_tool(self, *, body: CreateToolRequest) -> ToolResponse:
|
||||
"""Create a reusable tool for the authenticated organization."""
|
||||
data = self._request("POST", "/tools/", json=body.model_dump(mode="json", exclude_none=True))
|
||||
return ToolResponse.model_validate(data)
|
||||
|
||||
def create_workflow(self, *, body: CreateWorkflowRequest) -> WorkflowResponse:
|
||||
"""Create a new workflow from a workflow definition."""
|
||||
data = self._request("POST", "/workflow/create/definition", json=body.model_dump(mode="json", exclude_none=True))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,28 @@
|
|||
# generated by datamodel-codegen:
|
||||
# filename: dograh-openapi-aBVTJk.json
|
||||
# timestamp: 2026-05-27T10:06:12+00:00
|
||||
# filename: dograh-openapi-EZT8BU.json
|
||||
# timestamp: 2026-05-31T10:54:36+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Annotated, Any
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
|
||||
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, RootModel
|
||||
|
||||
|
||||
class CalculatorToolDefinition(BaseModel):
|
||||
"""
|
||||
Tool definition for Calculator tools.
|
||||
"""
|
||||
|
||||
schema_version: Annotated[int | None, Field(title='Schema Version')] = 1
|
||||
"""
|
||||
Schema version.
|
||||
"""
|
||||
type: Annotated[Literal['calculator'], Field(title='Type')]
|
||||
"""
|
||||
Tool type.
|
||||
"""
|
||||
|
||||
|
||||
class CallDispositionCodes(BaseModel):
|
||||
|
|
@ -16,6 +31,34 @@ class CallDispositionCodes(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class Category(Enum):
|
||||
"""
|
||||
Tool category. Must match definition.type.
|
||||
"""
|
||||
|
||||
http_api = 'http_api'
|
||||
end_call = 'end_call'
|
||||
transfer_call = 'transfer_call'
|
||||
calculator = 'calculator'
|
||||
native = 'native'
|
||||
integration = 'integration'
|
||||
mcp = 'mcp'
|
||||
|
||||
|
||||
class Icon(RootModel[str]):
|
||||
root: Annotated[str, Field(max_length=50, title='Icon')] = 'globe'
|
||||
"""
|
||||
Lucide icon identifier.
|
||||
"""
|
||||
|
||||
|
||||
class IconColor(RootModel[str]):
|
||||
root: Annotated[str, Field(max_length=7, title='Icon Color')] = '#3B82F6'
|
||||
"""
|
||||
Hex color for the tool icon.
|
||||
"""
|
||||
|
||||
|
||||
class CreateWorkflowRequest(BaseModel):
|
||||
name: Annotated[str, Field(title='Name')]
|
||||
workflow_definition: Annotated[dict[str, Any], Field(title='Workflow Definition')]
|
||||
|
|
@ -90,6 +133,64 @@ class DocumentResponseSchema(BaseModel):
|
|||
is_active: Annotated[bool, Field(title='Is Active')]
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
"""
|
||||
Type of goodbye message.
|
||||
"""
|
||||
|
||||
none = 'none'
|
||||
custom = 'custom'
|
||||
audio = 'audio'
|
||||
|
||||
|
||||
class EndCallConfig(BaseModel):
|
||||
"""
|
||||
Configuration for End Call tools.
|
||||
"""
|
||||
|
||||
messageType: Annotated[MessageType | None, Field(title='Messagetype')] = 'none'
|
||||
"""
|
||||
Type of goodbye message.
|
||||
"""
|
||||
customMessage: Annotated[str | None, Field(title='Custommessage')] = None
|
||||
"""
|
||||
Custom message to play before ending the call.
|
||||
"""
|
||||
audioRecordingId: Annotated[str | None, Field(title='Audiorecordingid')] = None
|
||||
"""
|
||||
Recording ID for audio goodbye message.
|
||||
"""
|
||||
endCallReason: Annotated[bool | None, Field(title='Endcallreason')] = False
|
||||
"""
|
||||
When enabled, the model must provide a reason for ending the call. The reason is set as call disposition and added to call tags.
|
||||
"""
|
||||
endCallReasonDescription: Annotated[
|
||||
str | None, Field(title='Endcallreasondescription')
|
||||
] = None
|
||||
"""
|
||||
Description shown to the model for the reason parameter. Used only when endCallReason is enabled.
|
||||
"""
|
||||
|
||||
|
||||
class EndCallToolDefinition(BaseModel):
|
||||
"""
|
||||
Tool definition for End Call tools.
|
||||
"""
|
||||
|
||||
schema_version: Annotated[int | None, Field(title='Schema Version')] = 1
|
||||
"""
|
||||
Schema version.
|
||||
"""
|
||||
type: Annotated[Literal['end_call'], Field(title='Type')]
|
||||
"""
|
||||
Tool type.
|
||||
"""
|
||||
config: EndCallConfig
|
||||
"""
|
||||
End Call configuration.
|
||||
"""
|
||||
|
||||
|
||||
class GraphConstraints(BaseModel):
|
||||
"""
|
||||
Per-node-type graph rules. WorkflowGraph enforces these at validation.
|
||||
|
|
@ -104,6 +205,34 @@ class GraphConstraints(BaseModel):
|
|||
max_outgoing: Annotated[int | None, Field(title='Max Outgoing')] = None
|
||||
|
||||
|
||||
class Method(Enum):
|
||||
"""
|
||||
HTTP method to use for the request.
|
||||
"""
|
||||
|
||||
GET = 'GET'
|
||||
POST = 'POST'
|
||||
PUT = 'PUT'
|
||||
PATCH = 'PATCH'
|
||||
DELETE = 'DELETE'
|
||||
|
||||
|
||||
class TimeoutMs(RootModel[int]):
|
||||
root: Annotated[int, Field(ge=1, title='Timeout Ms')] = 5000
|
||||
"""
|
||||
Request timeout in milliseconds.
|
||||
"""
|
||||
|
||||
|
||||
class CustomMessageType(Enum):
|
||||
"""
|
||||
Type of custom message.
|
||||
"""
|
||||
|
||||
text = 'text'
|
||||
audio = 'audio'
|
||||
|
||||
|
||||
class InitiateCallRequest(BaseModel):
|
||||
workflow_id: Annotated[int, Field(title='Workflow Id')]
|
||||
workflow_run_id: Annotated[int | None, Field(title='Workflow Run Id')] = None
|
||||
|
|
@ -116,6 +245,66 @@ class InitiateCallRequest(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class McpToolConfig(BaseModel):
|
||||
"""
|
||||
Configuration for a customer MCP server tool definition.
|
||||
"""
|
||||
|
||||
transport: Annotated[Literal['streamable_http'], Field(title='Transport')] = (
|
||||
'streamable_http'
|
||||
)
|
||||
"""
|
||||
MCP transport protocol.
|
||||
"""
|
||||
url: Annotated[str, Field(title='Url')]
|
||||
"""
|
||||
MCP server URL. Must use http:// or https://.
|
||||
"""
|
||||
credential_uuid: Annotated[str | None, Field(title='Credential Uuid')] = None
|
||||
"""
|
||||
Reference to an external credential for MCP server auth.
|
||||
"""
|
||||
tools_filter: Annotated[list[str] | None, Field(title='Tools Filter')] = None
|
||||
"""
|
||||
Allowlist of MCP tool names to expose. Empty exposes all tools.
|
||||
"""
|
||||
timeout_secs: Annotated[int | None, Field(ge=0, title='Timeout Secs')] = 30
|
||||
"""
|
||||
Connection timeout in seconds.
|
||||
"""
|
||||
sse_read_timeout_secs: Annotated[
|
||||
int | None, Field(ge=0, title='Sse Read Timeout Secs')
|
||||
] = 300
|
||||
"""
|
||||
SSE read timeout in seconds.
|
||||
"""
|
||||
discovered_tools: Annotated[
|
||||
list[dict[str, Any]] | None, Field(title='Discovered Tools')
|
||||
] = None
|
||||
"""
|
||||
Server-managed cache of the MCP server's tool catalog [{name, description}]. Populated best-effort by the backend.
|
||||
"""
|
||||
|
||||
|
||||
class McpToolDefinition(BaseModel):
|
||||
"""
|
||||
Persisted MCP tool definition.
|
||||
"""
|
||||
|
||||
schema_version: Annotated[int | None, Field(title='Schema Version')] = 1
|
||||
"""
|
||||
Schema version.
|
||||
"""
|
||||
type: Annotated[Literal['mcp'], Field(title='Type')]
|
||||
"""
|
||||
Tool type.
|
||||
"""
|
||||
config: McpToolConfig
|
||||
"""
|
||||
MCP server configuration.
|
||||
"""
|
||||
|
||||
|
||||
class NodeCategory(Enum):
|
||||
"""
|
||||
Drives grouping in the AddNodePanel UI.
|
||||
|
|
@ -140,6 +329,39 @@ class NodeExample(BaseModel):
|
|||
data: Annotated[dict[str, Any], Field(title='Data')]
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
"""
|
||||
JSON type for the resolved value.
|
||||
"""
|
||||
|
||||
string = 'string'
|
||||
number = 'number'
|
||||
boolean = 'boolean'
|
||||
|
||||
|
||||
class PresetToolParameter(BaseModel):
|
||||
"""
|
||||
A parameter injected by Dograh at runtime.
|
||||
"""
|
||||
|
||||
name: Annotated[str, Field(title='Name')]
|
||||
"""
|
||||
Parameter name used as a key in the request body.
|
||||
"""
|
||||
type: Annotated[Type, Field(title='Type')]
|
||||
"""
|
||||
JSON type for the resolved value.
|
||||
"""
|
||||
value_template: Annotated[str, Field(title='Value Template')]
|
||||
"""
|
||||
Fixed value or template, e.g. {{initial_context.phone_number}}.
|
||||
"""
|
||||
required: Annotated[bool | None, Field(title='Required')] = True
|
||||
"""
|
||||
Whether the parameter must resolve to a non-empty value.
|
||||
"""
|
||||
|
||||
|
||||
class PropertyOption(BaseModel):
|
||||
"""
|
||||
An option in an `options` or `multi_options` dropdown.
|
||||
|
|
@ -197,9 +419,42 @@ class RecordingResponseSchema(BaseModel):
|
|||
is_active: Annotated[bool, Field(title='Is Active')]
|
||||
|
||||
|
||||
class Type1(Enum):
|
||||
"""
|
||||
JSON type for the parameter value.
|
||||
"""
|
||||
|
||||
string = 'string'
|
||||
number = 'number'
|
||||
boolean = 'boolean'
|
||||
|
||||
|
||||
class ToolParameter(BaseModel):
|
||||
"""
|
||||
A parameter that the tool accepts from the model at call time.
|
||||
"""
|
||||
|
||||
name: Annotated[str, Field(title='Name')]
|
||||
"""
|
||||
Parameter name used as a key in the tool request body.
|
||||
"""
|
||||
type: Annotated[Type1, Field(title='Type')]
|
||||
"""
|
||||
JSON type for the parameter value.
|
||||
"""
|
||||
description: Annotated[str, Field(title='Description')]
|
||||
"""
|
||||
Description shown to the model for this parameter.
|
||||
"""
|
||||
required: Annotated[bool | None, Field(title='Required')] = True
|
||||
"""
|
||||
Whether this parameter is required when the tool is called.
|
||||
"""
|
||||
|
||||
|
||||
class ToolResponse(BaseModel):
|
||||
"""
|
||||
Response schema for a tool.
|
||||
Response schema for a reusable tool.
|
||||
"""
|
||||
|
||||
id: Annotated[int, Field(title='Id')]
|
||||
|
|
@ -216,6 +471,62 @@ class ToolResponse(BaseModel):
|
|||
created_by: CreatedByResponse | None = None
|
||||
|
||||
|
||||
class MessageType1(Enum):
|
||||
"""
|
||||
Type of message to play before transfer.
|
||||
"""
|
||||
|
||||
none = 'none'
|
||||
custom = 'custom'
|
||||
audio = 'audio'
|
||||
|
||||
|
||||
class TransferCallConfig(BaseModel):
|
||||
"""
|
||||
Configuration for Transfer Call tools.
|
||||
"""
|
||||
|
||||
destination: Annotated[str, Field(title='Destination')]
|
||||
"""
|
||||
Phone number or SIP endpoint to transfer the call to, e.g. +1234567890 or PJSIP/1234.
|
||||
"""
|
||||
messageType: Annotated[MessageType1 | None, Field(title='Messagetype')] = 'none'
|
||||
"""
|
||||
Type of message to play before transfer.
|
||||
"""
|
||||
customMessage: Annotated[str | None, Field(title='Custommessage')] = None
|
||||
"""
|
||||
Custom message to play before transferring.
|
||||
"""
|
||||
audioRecordingId: Annotated[str | None, Field(title='Audiorecordingid')] = None
|
||||
"""
|
||||
Recording ID for audio message before transfer.
|
||||
"""
|
||||
timeout: Annotated[int | None, Field(ge=5, le=120, title='Timeout')] = 30
|
||||
"""
|
||||
Maximum seconds to wait for the destination to answer.
|
||||
"""
|
||||
|
||||
|
||||
class TransferCallToolDefinition(BaseModel):
|
||||
"""
|
||||
Tool definition for Transfer Call tools.
|
||||
"""
|
||||
|
||||
schema_version: Annotated[int | None, Field(title='Schema Version')] = 1
|
||||
"""
|
||||
Schema version.
|
||||
"""
|
||||
type: Annotated[Literal['transfer_call'], Field(title='Type')]
|
||||
"""
|
||||
Tool type.
|
||||
"""
|
||||
config: TransferCallConfig
|
||||
"""
|
||||
Transfer Call configuration.
|
||||
"""
|
||||
|
||||
|
||||
class UpdateWorkflowRequest(BaseModel):
|
||||
name: Annotated[str | None, Field(title='Name')] = None
|
||||
workflow_definition: Annotated[
|
||||
|
|
@ -286,6 +597,80 @@ class HTTPValidationError(BaseModel):
|
|||
detail: Annotated[list[ValidationError] | None, Field(title='Detail')] = None
|
||||
|
||||
|
||||
class HttpApiConfig(BaseModel):
|
||||
"""
|
||||
Configuration for HTTP API tools.
|
||||
"""
|
||||
|
||||
method: Annotated[Method, Field(title='Method')]
|
||||
"""
|
||||
HTTP method to use for the request.
|
||||
"""
|
||||
url: Annotated[str, Field(title='Url')]
|
||||
"""
|
||||
Target HTTP or HTTPS URL.
|
||||
"""
|
||||
headers: Annotated[dict[str, str] | None, Field(title='Headers')] = None
|
||||
"""
|
||||
Static headers to include with every request.
|
||||
"""
|
||||
credential_uuid: Annotated[str | None, Field(title='Credential Uuid')] = None
|
||||
"""
|
||||
Reference to an external credential for request authentication.
|
||||
"""
|
||||
parameters: Annotated[list[ToolParameter] | None, Field(title='Parameters')] = None
|
||||
"""
|
||||
Parameters the model must provide when calling this tool.
|
||||
"""
|
||||
preset_parameters: Annotated[
|
||||
list[PresetToolParameter] | None, Field(title='Preset Parameters')
|
||||
] = None
|
||||
"""
|
||||
Parameters injected by Dograh from fixed values or workflow context templates.
|
||||
"""
|
||||
timeout_ms: Annotated[
|
||||
TimeoutMs | None, Field(title='Timeout Ms', validate_default=True)
|
||||
] = 5000
|
||||
"""
|
||||
Request timeout in milliseconds.
|
||||
"""
|
||||
customMessage: Annotated[str | None, Field(title='Custommessage')] = None
|
||||
"""
|
||||
Custom message to play after tool execution.
|
||||
"""
|
||||
customMessageType: Annotated[
|
||||
CustomMessageType | None, Field(title='Custommessagetype')
|
||||
] = None
|
||||
"""
|
||||
Type of custom message.
|
||||
"""
|
||||
customMessageRecordingId: Annotated[
|
||||
str | None, Field(title='Custommessagerecordingid')
|
||||
] = None
|
||||
"""
|
||||
Recording ID for an audio custom message.
|
||||
"""
|
||||
|
||||
|
||||
class HttpApiToolDefinition(BaseModel):
|
||||
"""
|
||||
Tool definition for HTTP API tools.
|
||||
"""
|
||||
|
||||
schema_version: Annotated[int | None, Field(title='Schema Version')] = 1
|
||||
"""
|
||||
Schema version.
|
||||
"""
|
||||
type: Annotated[Literal['http_api'], Field(title='Type')]
|
||||
"""
|
||||
Tool type.
|
||||
"""
|
||||
config: HttpApiConfig
|
||||
"""
|
||||
HTTP API configuration.
|
||||
"""
|
||||
|
||||
|
||||
class PropertySpec(BaseModel):
|
||||
"""
|
||||
Single field on a node.
|
||||
|
|
@ -338,6 +723,46 @@ class RecordingListResponseSchema(BaseModel):
|
|||
total: Annotated[int, Field(title='Total')]
|
||||
|
||||
|
||||
class CreateToolRequest(BaseModel):
|
||||
"""
|
||||
Request schema for creating a reusable tool.
|
||||
"""
|
||||
|
||||
name: Annotated[str, Field(max_length=255, title='Name')]
|
||||
"""
|
||||
Display name for the tool.
|
||||
"""
|
||||
description: Annotated[str | None, Field(title='Description')] = None
|
||||
"""
|
||||
Description shown to the agent when deciding whether to call it.
|
||||
"""
|
||||
category: Annotated[Category | None, Field(title='Category')] = 'http_api'
|
||||
"""
|
||||
Tool category. Must match definition.type.
|
||||
"""
|
||||
icon: Annotated[Icon | None, Field(title='Icon', validate_default=True)] = 'globe'
|
||||
"""
|
||||
Lucide icon identifier.
|
||||
"""
|
||||
icon_color: Annotated[
|
||||
IconColor | None, Field(title='Icon Color', validate_default=True)
|
||||
] = '#3B82F6'
|
||||
"""
|
||||
Hex color for the tool icon.
|
||||
"""
|
||||
definition: Annotated[
|
||||
HttpApiToolDefinition
|
||||
| EndCallToolDefinition
|
||||
| TransferCallToolDefinition
|
||||
| CalculatorToolDefinition
|
||||
| McpToolDefinition,
|
||||
Field(discriminator='type', title='Definition'),
|
||||
]
|
||||
"""
|
||||
Typed tool definition.
|
||||
"""
|
||||
|
||||
|
||||
class NodeSpec(BaseModel):
|
||||
"""
|
||||
Single source of truth for a node type.
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ class StartCall(TypedNode):
|
|||
greeting: Optional[str] = None
|
||||
"""
|
||||
Text spoken via TTS at the start of the call. Supports
|
||||
{{template_variables}}. Leave empty to skip the greeting.
|
||||
{{template_variables}}. Leave empty to skip the greeting. Not supported
|
||||
with realtime (speech-to-speech) models.
|
||||
"""
|
||||
|
||||
greeting_recording_id: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
// `_generated_models` (openapi-typescript output, --root-types).
|
||||
|
||||
import type {
|
||||
CreateToolRequest,
|
||||
CreateWorkflowRequest,
|
||||
CredentialResponse,
|
||||
DocumentListResponseSchema,
|
||||
|
|
@ -27,6 +28,11 @@ export abstract class _GeneratedClient {
|
|||
opts?: { json?: unknown; params?: Record<string, unknown> },
|
||||
): Promise<T>;
|
||||
|
||||
/** Create a reusable tool for the authenticated organization. */
|
||||
async createTool(opts: { body: CreateToolRequest }): Promise<ToolResponse> {
|
||||
return this.request<ToolResponse>("POST", "/tools/", { json: opts.body });
|
||||
}
|
||||
|
||||
/** Create a new workflow from a workflow definition. */
|
||||
async createWorkflow(opts: { body: CreateWorkflowRequest }): Promise<WorkflowResponse> {
|
||||
return this.request<WorkflowResponse>("POST", "/workflow/create/definition", { json: opts.body });
|
||||
|
|
|
|||
|
|
@ -168,7 +168,17 @@ export interface paths {
|
|||
*/
|
||||
get: operations["list_tools_api_v1_tools__get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
/**
|
||||
* Create Tool
|
||||
* @description Create a new tool.
|
||||
*
|
||||
* Args:
|
||||
* request: The tool creation request
|
||||
*
|
||||
* Returns:
|
||||
* The created tool
|
||||
*/
|
||||
post: operations["create_tool_api_v1_tools__post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
|
|
@ -262,6 +272,23 @@ export interface paths {
|
|||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
/**
|
||||
* CalculatorToolDefinition
|
||||
* @description Tool definition for Calculator tools.
|
||||
*/
|
||||
CalculatorToolDefinition: {
|
||||
/**
|
||||
* Schema Version
|
||||
* @description Schema version.
|
||||
* @default 1
|
||||
*/
|
||||
schema_version: number;
|
||||
/**
|
||||
* @description Tool type. (enum property replaced by openapi-typescript)
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "calculator";
|
||||
};
|
||||
/** CallDispositionCodes */
|
||||
CallDispositionCodes: {
|
||||
/**
|
||||
|
|
@ -270,6 +297,46 @@ export interface components {
|
|||
*/
|
||||
disposition_codes: string[];
|
||||
};
|
||||
/**
|
||||
* CreateToolRequest
|
||||
* @description Request schema for creating a reusable tool.
|
||||
*/
|
||||
CreateToolRequest: {
|
||||
/**
|
||||
* Name
|
||||
* @description Display name for the tool.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Description
|
||||
* @description Description shown to the agent when deciding whether to call it.
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Category
|
||||
* @description Tool category. Must match definition.type.
|
||||
* @default http_api
|
||||
* @enum {string}
|
||||
*/
|
||||
category: "http_api" | "end_call" | "transfer_call" | "calculator" | "native" | "integration" | "mcp";
|
||||
/**
|
||||
* Icon
|
||||
* @description Lucide icon identifier.
|
||||
* @default globe
|
||||
*/
|
||||
icon: string | null;
|
||||
/**
|
||||
* Icon Color
|
||||
* @description Hex color for the tool icon.
|
||||
* @default #3B82F6
|
||||
*/
|
||||
icon_color: string | null;
|
||||
/**
|
||||
* Definition
|
||||
* @description Typed tool definition.
|
||||
*/
|
||||
definition: components["schemas"]["HttpApiToolDefinition"] | components["schemas"]["EndCallToolDefinition"] | components["schemas"]["TransferCallToolDefinition"] | components["schemas"]["CalculatorToolDefinition"] | components["schemas"]["McpToolDefinition"];
|
||||
};
|
||||
/** CreateWorkflowRequest */
|
||||
CreateWorkflowRequest: {
|
||||
/** Name */
|
||||
|
|
@ -403,6 +470,59 @@ export interface components {
|
|||
/** Is Active */
|
||||
is_active: boolean;
|
||||
};
|
||||
/**
|
||||
* EndCallConfig
|
||||
* @description Configuration for End Call tools.
|
||||
*/
|
||||
EndCallConfig: {
|
||||
/**
|
||||
* Messagetype
|
||||
* @description Type of goodbye message.
|
||||
* @default none
|
||||
* @enum {string}
|
||||
*/
|
||||
messageType: "none" | "custom" | "audio";
|
||||
/**
|
||||
* Custommessage
|
||||
* @description Custom message to play before ending the call.
|
||||
*/
|
||||
customMessage?: string | null;
|
||||
/**
|
||||
* Audiorecordingid
|
||||
* @description Recording ID for audio goodbye message.
|
||||
*/
|
||||
audioRecordingId?: string | null;
|
||||
/**
|
||||
* Endcallreason
|
||||
* @description When enabled, the model must provide a reason for ending the call. The reason is set as call disposition and added to call tags.
|
||||
* @default false
|
||||
*/
|
||||
endCallReason: boolean;
|
||||
/**
|
||||
* Endcallreasondescription
|
||||
* @description Description shown to the model for the reason parameter. Used only when endCallReason is enabled.
|
||||
*/
|
||||
endCallReasonDescription?: string | null;
|
||||
};
|
||||
/**
|
||||
* EndCallToolDefinition
|
||||
* @description Tool definition for End Call tools.
|
||||
*/
|
||||
EndCallToolDefinition: {
|
||||
/**
|
||||
* Schema Version
|
||||
* @description Schema version.
|
||||
* @default 1
|
||||
*/
|
||||
schema_version: number;
|
||||
/**
|
||||
* @description Tool type. (enum property replaced by openapi-typescript)
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "end_call";
|
||||
/** @description End Call configuration. */
|
||||
config: components["schemas"]["EndCallConfig"];
|
||||
};
|
||||
/**
|
||||
* GraphConstraints
|
||||
* @description Per-node-type graph rules. WorkflowGraph enforces these at validation.
|
||||
|
|
@ -422,6 +542,85 @@ export interface components {
|
|||
/** Detail */
|
||||
detail?: components["schemas"]["ValidationError"][];
|
||||
};
|
||||
/**
|
||||
* HttpApiConfig
|
||||
* @description Configuration for HTTP API tools.
|
||||
*/
|
||||
HttpApiConfig: {
|
||||
/**
|
||||
* Method
|
||||
* @description HTTP method to use for the request.
|
||||
* @enum {string}
|
||||
*/
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
/**
|
||||
* Url
|
||||
* @description Target HTTP or HTTPS URL.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Headers
|
||||
* @description Static headers to include with every request.
|
||||
*/
|
||||
headers?: {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
/**
|
||||
* Credential Uuid
|
||||
* @description Reference to an external credential for request authentication.
|
||||
*/
|
||||
credential_uuid?: string | null;
|
||||
/**
|
||||
* Parameters
|
||||
* @description Parameters the model must provide when calling this tool.
|
||||
*/
|
||||
parameters?: components["schemas"]["ToolParameter"][] | null;
|
||||
/**
|
||||
* Preset Parameters
|
||||
* @description Parameters injected by Dograh from fixed values or workflow context templates.
|
||||
*/
|
||||
preset_parameters?: components["schemas"]["PresetToolParameter"][] | null;
|
||||
/**
|
||||
* Timeout Ms
|
||||
* @description Request timeout in milliseconds.
|
||||
* @default 5000
|
||||
*/
|
||||
timeout_ms: number | null;
|
||||
/**
|
||||
* Custommessage
|
||||
* @description Custom message to play after tool execution.
|
||||
*/
|
||||
customMessage?: string | null;
|
||||
/**
|
||||
* Custommessagetype
|
||||
* @description Type of custom message.
|
||||
*/
|
||||
customMessageType?: ("text" | "audio") | null;
|
||||
/**
|
||||
* Custommessagerecordingid
|
||||
* @description Recording ID for an audio custom message.
|
||||
*/
|
||||
customMessageRecordingId?: string | null;
|
||||
};
|
||||
/**
|
||||
* HttpApiToolDefinition
|
||||
* @description Tool definition for HTTP API tools.
|
||||
*/
|
||||
HttpApiToolDefinition: {
|
||||
/**
|
||||
* Schema Version
|
||||
* @description Schema version.
|
||||
* @default 1
|
||||
*/
|
||||
schema_version: number;
|
||||
/**
|
||||
* @description Tool type. (enum property replaced by openapi-typescript)
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "http_api";
|
||||
/** @description HTTP API configuration. */
|
||||
config: components["schemas"]["HttpApiConfig"];
|
||||
};
|
||||
/** InitiateCallRequest */
|
||||
InitiateCallRequest: {
|
||||
/** Workflow Id */
|
||||
|
|
@ -435,6 +634,72 @@ export interface components {
|
|||
/** From Phone Number Id */
|
||||
from_phone_number_id?: number | null;
|
||||
};
|
||||
/**
|
||||
* McpToolConfig
|
||||
* @description Configuration for a customer MCP server tool definition.
|
||||
*/
|
||||
McpToolConfig: {
|
||||
/**
|
||||
* Transport
|
||||
* @description MCP transport protocol.
|
||||
* @default streamable_http
|
||||
* @constant
|
||||
*/
|
||||
transport: "streamable_http";
|
||||
/**
|
||||
* Url
|
||||
* @description MCP server URL. Must use http:// or https://.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Credential Uuid
|
||||
* @description Reference to an external credential for MCP server auth.
|
||||
*/
|
||||
credential_uuid?: string | null;
|
||||
/**
|
||||
* Tools Filter
|
||||
* @description Allowlist of MCP tool names to expose. Empty exposes all tools.
|
||||
*/
|
||||
tools_filter?: string[];
|
||||
/**
|
||||
* Timeout Secs
|
||||
* @description Connection timeout in seconds.
|
||||
* @default 30
|
||||
*/
|
||||
timeout_secs: number;
|
||||
/**
|
||||
* Sse Read Timeout Secs
|
||||
* @description SSE read timeout in seconds.
|
||||
* @default 300
|
||||
*/
|
||||
sse_read_timeout_secs: number;
|
||||
/**
|
||||
* Discovered Tools
|
||||
* @description Server-managed cache of the MCP server's tool catalog [{name, description}]. Populated best-effort by the backend.
|
||||
*/
|
||||
discovered_tools?: {
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
};
|
||||
/**
|
||||
* McpToolDefinition
|
||||
* @description Persisted MCP tool definition.
|
||||
*/
|
||||
McpToolDefinition: {
|
||||
/**
|
||||
* Schema Version
|
||||
* @description Schema version.
|
||||
* @default 1
|
||||
*/
|
||||
schema_version: number;
|
||||
/**
|
||||
* @description Tool type. (enum property replaced by openapi-typescript)
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "mcp";
|
||||
/** @description MCP server configuration. */
|
||||
config: components["schemas"]["McpToolConfig"];
|
||||
};
|
||||
/**
|
||||
* NodeCategory
|
||||
* @description Drives grouping in the AddNodePanel UI.
|
||||
|
|
@ -495,6 +760,34 @@ export interface components {
|
|||
/** Node Types */
|
||||
node_types: components["schemas"]["NodeSpec"][];
|
||||
};
|
||||
/**
|
||||
* PresetToolParameter
|
||||
* @description A parameter injected by Dograh at runtime.
|
||||
*/
|
||||
PresetToolParameter: {
|
||||
/**
|
||||
* Name
|
||||
* @description Parameter name used as a key in the request body.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
* @description JSON type for the resolved value.
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "string" | "number" | "boolean";
|
||||
/**
|
||||
* Value Template
|
||||
* @description Fixed value or template, e.g. {{initial_context.phone_number}}.
|
||||
*/
|
||||
value_template: string;
|
||||
/**
|
||||
* Required
|
||||
* @description Whether the parameter must resolve to a non-empty value.
|
||||
* @default true
|
||||
*/
|
||||
required: boolean;
|
||||
};
|
||||
/**
|
||||
* PropertyOption
|
||||
* @description An option in an `options` or `multi_options` dropdown.
|
||||
|
|
@ -625,9 +918,37 @@ export interface components {
|
|||
/** Is Active */
|
||||
is_active: boolean;
|
||||
};
|
||||
/**
|
||||
* ToolParameter
|
||||
* @description A parameter that the tool accepts from the model at call time.
|
||||
*/
|
||||
ToolParameter: {
|
||||
/**
|
||||
* Name
|
||||
* @description Parameter name used as a key in the tool request body.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
* @description JSON type for the parameter value.
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "string" | "number" | "boolean";
|
||||
/**
|
||||
* Description
|
||||
* @description Description shown to the model for this parameter.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Required
|
||||
* @description Whether this parameter is required when the tool is called.
|
||||
* @default true
|
||||
*/
|
||||
required: boolean;
|
||||
};
|
||||
/**
|
||||
* ToolResponse
|
||||
* @description Response schema for a tool.
|
||||
* @description Response schema for a reusable tool.
|
||||
*/
|
||||
ToolResponse: {
|
||||
/** Id */
|
||||
|
|
@ -659,6 +980,59 @@ export interface components {
|
|||
updated_at: string | null;
|
||||
created_by?: components["schemas"]["CreatedByResponse"] | null;
|
||||
};
|
||||
/**
|
||||
* TransferCallConfig
|
||||
* @description Configuration for Transfer Call tools.
|
||||
*/
|
||||
TransferCallConfig: {
|
||||
/**
|
||||
* Destination
|
||||
* @description Phone number or SIP endpoint to transfer the call to, e.g. +1234567890 or PJSIP/1234.
|
||||
*/
|
||||
destination: string;
|
||||
/**
|
||||
* Messagetype
|
||||
* @description Type of message to play before transfer.
|
||||
* @default none
|
||||
* @enum {string}
|
||||
*/
|
||||
messageType: "none" | "custom" | "audio";
|
||||
/**
|
||||
* Custommessage
|
||||
* @description Custom message to play before transferring.
|
||||
*/
|
||||
customMessage?: string | null;
|
||||
/**
|
||||
* Audiorecordingid
|
||||
* @description Recording ID for audio message before transfer.
|
||||
*/
|
||||
audioRecordingId?: string | null;
|
||||
/**
|
||||
* Timeout
|
||||
* @description Maximum seconds to wait for the destination to answer.
|
||||
* @default 30
|
||||
*/
|
||||
timeout: number;
|
||||
};
|
||||
/**
|
||||
* TransferCallToolDefinition
|
||||
* @description Tool definition for Transfer Call tools.
|
||||
*/
|
||||
TransferCallToolDefinition: {
|
||||
/**
|
||||
* Schema Version
|
||||
* @description Schema version.
|
||||
* @default 1
|
||||
*/
|
||||
schema_version: number;
|
||||
/**
|
||||
* @description Tool type. (enum property replaced by openapi-typescript)
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "transfer_call";
|
||||
/** @description Transfer Call configuration. */
|
||||
config: components["schemas"]["TransferCallConfig"];
|
||||
};
|
||||
/** UpdateWorkflowRequest */
|
||||
UpdateWorkflowRequest: {
|
||||
/** Name */
|
||||
|
|
@ -756,26 +1130,38 @@ export interface components {
|
|||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type CalculatorToolDefinition = components['schemas']['CalculatorToolDefinition'];
|
||||
export type CallDispositionCodes = components['schemas']['CallDispositionCodes'];
|
||||
export type CreateToolRequest = components['schemas']['CreateToolRequest'];
|
||||
export type CreateWorkflowRequest = components['schemas']['CreateWorkflowRequest'];
|
||||
export type CreatedByResponse = components['schemas']['CreatedByResponse'];
|
||||
export type CredentialResponse = components['schemas']['CredentialResponse'];
|
||||
export type DisplayOptions = components['schemas']['DisplayOptions'];
|
||||
export type DocumentListResponseSchema = components['schemas']['DocumentListResponseSchema'];
|
||||
export type DocumentResponseSchema = components['schemas']['DocumentResponseSchema'];
|
||||
export type EndCallConfig = components['schemas']['EndCallConfig'];
|
||||
export type EndCallToolDefinition = components['schemas']['EndCallToolDefinition'];
|
||||
export type GraphConstraints = components['schemas']['GraphConstraints'];
|
||||
export type HttpValidationError = components['schemas']['HTTPValidationError'];
|
||||
export type HttpApiConfig = components['schemas']['HttpApiConfig'];
|
||||
export type HttpApiToolDefinition = components['schemas']['HttpApiToolDefinition'];
|
||||
export type InitiateCallRequest = components['schemas']['InitiateCallRequest'];
|
||||
export type McpToolConfig = components['schemas']['McpToolConfig'];
|
||||
export type McpToolDefinition = components['schemas']['McpToolDefinition'];
|
||||
export type NodeCategory = components['schemas']['NodeCategory'];
|
||||
export type NodeExample = components['schemas']['NodeExample'];
|
||||
export type NodeSpec = components['schemas']['NodeSpec'];
|
||||
export type NodeTypesResponse = components['schemas']['NodeTypesResponse'];
|
||||
export type PresetToolParameter = components['schemas']['PresetToolParameter'];
|
||||
export type PropertyOption = components['schemas']['PropertyOption'];
|
||||
export type PropertySpec = components['schemas']['PropertySpec'];
|
||||
export type PropertyType = components['schemas']['PropertyType'];
|
||||
export type RecordingListResponseSchema = components['schemas']['RecordingListResponseSchema'];
|
||||
export type RecordingResponseSchema = components['schemas']['RecordingResponseSchema'];
|
||||
export type ToolParameter = components['schemas']['ToolParameter'];
|
||||
export type ToolResponse = components['schemas']['ToolResponse'];
|
||||
export type TransferCallConfig = components['schemas']['TransferCallConfig'];
|
||||
export type TransferCallToolDefinition = components['schemas']['TransferCallToolDefinition'];
|
||||
export type UpdateWorkflowRequest = components['schemas']['UpdateWorkflowRequest'];
|
||||
export type ValidationError = components['schemas']['ValidationError'];
|
||||
export type WorkflowListResponse = components['schemas']['WorkflowListResponse'];
|
||||
|
|
@ -1077,6 +1463,49 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
create_tool_api_v1_tools__post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: {
|
||||
authorization?: string | null;
|
||||
"X-API-Key"?: string | null;
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateToolRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ToolResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
list_documents_api_v1_knowledge_base_documents_get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface StartCall {
|
|||
*/
|
||||
greeting_type?: "text" | "audio";
|
||||
/**
|
||||
* Text spoken via TTS at the start of the call. Supports {{template_variables}}. Leave empty to skip the greeting.
|
||||
* Text spoken via TTS at the start of the call. Supports {{template_variables}}. Leave empty to skip the greeting. Not supported with realtime (speech-to-speech) models.
|
||||
*/
|
||||
greeting?: string;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -290,19 +290,19 @@ export type BodyTranscribeAudioApiV1WorkflowRecordingsTranscribePost = {
|
|||
/**
|
||||
* CalculatorToolDefinition
|
||||
*
|
||||
* Tool definition for Calculator tools (no configuration needed).
|
||||
* Tool definition for Calculator tools.
|
||||
*/
|
||||
export type CalculatorToolDefinition = {
|
||||
/**
|
||||
* Schema Version
|
||||
*
|
||||
* Schema version
|
||||
* Schema version.
|
||||
*/
|
||||
schema_version?: number;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* Tool type
|
||||
* Tool type.
|
||||
*/
|
||||
type: 'calculator';
|
||||
};
|
||||
|
|
@ -939,31 +939,43 @@ export type CreateTextChatSessionRequest = {
|
|||
/**
|
||||
* CreateToolRequest
|
||||
*
|
||||
* Request schema for creating a tool.
|
||||
* Request schema for creating a reusable tool.
|
||||
*/
|
||||
export type CreateToolRequest = {
|
||||
/**
|
||||
* Name
|
||||
*
|
||||
* Display name for the tool.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Description
|
||||
*
|
||||
* Description shown to the agent when deciding whether to call it.
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Category
|
||||
*
|
||||
* Tool category. Must match definition.type.
|
||||
*/
|
||||
category?: string;
|
||||
category?: 'http_api' | 'end_call' | 'transfer_call' | 'calculator' | 'native' | 'integration' | 'mcp';
|
||||
/**
|
||||
* Icon
|
||||
*
|
||||
* Lucide icon identifier.
|
||||
*/
|
||||
icon?: string | null;
|
||||
/**
|
||||
* Icon Color
|
||||
*
|
||||
* Hex color for the tool icon.
|
||||
*/
|
||||
icon_color?: string | null;
|
||||
/**
|
||||
* Definition
|
||||
*
|
||||
* Typed tool definition.
|
||||
*/
|
||||
definition: ({
|
||||
type: 'http_api';
|
||||
|
|
@ -1633,31 +1645,31 @@ export type EndCallConfig = {
|
|||
/**
|
||||
* Messagetype
|
||||
*
|
||||
* Type of goodbye message
|
||||
* Type of goodbye message.
|
||||
*/
|
||||
messageType?: 'none' | 'custom' | 'audio';
|
||||
/**
|
||||
* Custommessage
|
||||
*
|
||||
* Custom message to play before ending the call
|
||||
* Custom message to play before ending the call.
|
||||
*/
|
||||
customMessage?: string | null;
|
||||
/**
|
||||
* Audiorecordingid
|
||||
*
|
||||
* Recording ID for audio goodbye message
|
||||
* Recording ID for audio goodbye message.
|
||||
*/
|
||||
audioRecordingId?: string | null;
|
||||
/**
|
||||
* Endcallreason
|
||||
*
|
||||
* When enabled, LLM must provide a reason for ending the call. The reason is set as call disposition and added to call tags.
|
||||
* When enabled, the model must provide a reason for ending the call. The reason is set as call disposition and added to call tags.
|
||||
*/
|
||||
endCallReason?: boolean;
|
||||
/**
|
||||
* Endcallreasondescription
|
||||
*
|
||||
* Description shown to the LLM for the reason parameter. Used only when endCallReason is enabled.
|
||||
* Description shown to the model for the reason parameter. Used only when endCallReason is enabled.
|
||||
*/
|
||||
endCallReasonDescription?: string | null;
|
||||
};
|
||||
|
|
@ -1671,17 +1683,17 @@ export type EndCallToolDefinition = {
|
|||
/**
|
||||
* Schema Version
|
||||
*
|
||||
* Schema version
|
||||
* Schema version.
|
||||
*/
|
||||
schema_version?: number;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* Tool type
|
||||
* Tool type.
|
||||
*/
|
||||
type: 'end_call';
|
||||
/**
|
||||
* End Call configuration
|
||||
* End Call configuration.
|
||||
*/
|
||||
config: EndCallConfig;
|
||||
};
|
||||
|
|
@ -1823,19 +1835,19 @@ export type HttpApiConfig = {
|
|||
/**
|
||||
* Method
|
||||
*
|
||||
* HTTP method (GET, POST, PUT, PATCH, DELETE)
|
||||
* HTTP method to use for the request.
|
||||
*/
|
||||
method: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
/**
|
||||
* Url
|
||||
*
|
||||
* Target URL
|
||||
* Target HTTP or HTTPS URL.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Headers
|
||||
*
|
||||
* Static headers to include
|
||||
* Static headers to include with every request.
|
||||
*/
|
||||
headers?: {
|
||||
[key: string]: string;
|
||||
|
|
@ -1843,43 +1855,43 @@ export type HttpApiConfig = {
|
|||
/**
|
||||
* Credential Uuid
|
||||
*
|
||||
* Reference to ExternalCredentialModel for auth
|
||||
* Reference to an external credential for request authentication.
|
||||
*/
|
||||
credential_uuid?: string | null;
|
||||
/**
|
||||
* Parameters
|
||||
*
|
||||
* Parameters that the tool accepts from LLM
|
||||
* Parameters the model must provide when calling this tool.
|
||||
*/
|
||||
parameters?: Array<ToolParameter> | null;
|
||||
/**
|
||||
* Preset Parameters
|
||||
*
|
||||
* Parameters injected by Dograh from fixed values or workflow context templates
|
||||
* Parameters injected by Dograh from fixed values or workflow context templates.
|
||||
*/
|
||||
preset_parameters?: Array<PresetToolParameter> | null;
|
||||
/**
|
||||
* Timeout Ms
|
||||
*
|
||||
* Request timeout in milliseconds
|
||||
* Request timeout in milliseconds.
|
||||
*/
|
||||
timeout_ms?: number | null;
|
||||
/**
|
||||
* Custommessage
|
||||
*
|
||||
* Custom message to play after tool execution
|
||||
* Custom message to play after tool execution.
|
||||
*/
|
||||
customMessage?: string | null;
|
||||
/**
|
||||
* Custommessagetype
|
||||
*
|
||||
* Type of custom message: text or audio
|
||||
* Type of custom message.
|
||||
*/
|
||||
customMessageType?: 'text' | 'audio' | null;
|
||||
/**
|
||||
* Custommessagerecordingid
|
||||
*
|
||||
* Recording ID for audio custom message
|
||||
* Recording ID for an audio custom message.
|
||||
*/
|
||||
customMessageRecordingId?: string | null;
|
||||
};
|
||||
|
|
@ -1893,17 +1905,17 @@ export type HttpApiToolDefinition = {
|
|||
/**
|
||||
* Schema Version
|
||||
*
|
||||
* Schema version
|
||||
* Schema version.
|
||||
*/
|
||||
schema_version?: number;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* Tool type
|
||||
* Tool type.
|
||||
*/
|
||||
type: 'http_api';
|
||||
/**
|
||||
* HTTP API configuration
|
||||
* HTTP API configuration.
|
||||
*/
|
||||
config: HttpApiConfig;
|
||||
};
|
||||
|
|
@ -2120,43 +2132,43 @@ export type McpRefreshResponse = {
|
|||
/**
|
||||
* McpToolConfig
|
||||
*
|
||||
* Configuration for an MCP tool definition.
|
||||
* Configuration for a customer MCP server tool definition.
|
||||
*/
|
||||
export type McpToolConfig = {
|
||||
/**
|
||||
* Transport
|
||||
*
|
||||
* MCP transport protocol
|
||||
* MCP transport protocol.
|
||||
*/
|
||||
transport?: 'streamable_http';
|
||||
/**
|
||||
* Url
|
||||
*
|
||||
* MCP server URL (must be http:// or https://)
|
||||
* MCP server URL. Must use http:// or https://.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Credential Uuid
|
||||
*
|
||||
* Reference to ExternalCredentialModel for auth
|
||||
* Reference to an external credential for MCP server auth.
|
||||
*/
|
||||
credential_uuid?: string | null;
|
||||
/**
|
||||
* Tools Filter
|
||||
*
|
||||
* Allowlist of MCP tool names to expose (empty = all tools)
|
||||
* Allowlist of MCP tool names to expose. Empty exposes all tools.
|
||||
*/
|
||||
tools_filter?: Array<string>;
|
||||
/**
|
||||
* Timeout Secs
|
||||
*
|
||||
* Connection timeout in seconds
|
||||
* Connection timeout in seconds.
|
||||
*/
|
||||
timeout_secs?: number;
|
||||
/**
|
||||
* Sse Read Timeout Secs
|
||||
*
|
||||
* SSE read timeout in seconds
|
||||
* SSE read timeout in seconds.
|
||||
*/
|
||||
sse_read_timeout_secs?: number;
|
||||
/**
|
||||
|
|
@ -2178,17 +2190,17 @@ export type McpToolDefinition = {
|
|||
/**
|
||||
* Schema Version
|
||||
*
|
||||
* Schema version
|
||||
* Schema version.
|
||||
*/
|
||||
schema_version?: number;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* Tool type
|
||||
* Tool type.
|
||||
*/
|
||||
type: 'mcp';
|
||||
/**
|
||||
* MCP server configuration
|
||||
* MCP server configuration.
|
||||
*/
|
||||
config: McpToolConfig;
|
||||
};
|
||||
|
|
@ -2519,25 +2531,25 @@ export type PresetToolParameter = {
|
|||
/**
|
||||
* Name
|
||||
*
|
||||
* Parameter name (used as key in request body)
|
||||
* Parameter name used as a key in the request body.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* Parameter type: string, number, or boolean
|
||||
* JSON type for the resolved value.
|
||||
*/
|
||||
type: string;
|
||||
type: 'string' | 'number' | 'boolean';
|
||||
/**
|
||||
* Value Template
|
||||
*
|
||||
* Fixed value or template, e.g. {{initial_context.phone_number}}
|
||||
* Fixed value or template, e.g. {{initial_context.phone_number}}.
|
||||
*/
|
||||
value_template: string;
|
||||
/**
|
||||
* Required
|
||||
*
|
||||
* Whether the parameter must resolve to a non-empty value
|
||||
* Whether the parameter must resolve to a non-empty value.
|
||||
*/
|
||||
required?: boolean;
|
||||
};
|
||||
|
|
@ -3599,31 +3611,31 @@ export type TimeSlotResponse = {
|
|||
/**
|
||||
* ToolParameter
|
||||
*
|
||||
* A parameter that the tool accepts.
|
||||
* A parameter that the tool accepts from the model at call time.
|
||||
*/
|
||||
export type ToolParameter = {
|
||||
/**
|
||||
* Name
|
||||
*
|
||||
* Parameter name (used as key in request body)
|
||||
* Parameter name used as a key in the tool request body.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* Parameter type: string, number, or boolean
|
||||
* JSON type for the parameter value.
|
||||
*/
|
||||
type: string;
|
||||
type: 'string' | 'number' | 'boolean';
|
||||
/**
|
||||
* Description
|
||||
*
|
||||
* Description of what this parameter is for
|
||||
* Description shown to the model for this parameter.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Required
|
||||
*
|
||||
* Whether this parameter is required
|
||||
* Whether this parameter is required when the tool is called.
|
||||
*/
|
||||
required?: boolean;
|
||||
};
|
||||
|
|
@ -3631,7 +3643,7 @@ export type ToolParameter = {
|
|||
/**
|
||||
* ToolResponse
|
||||
*
|
||||
* Response schema for a tool.
|
||||
* Response schema for a reusable tool.
|
||||
*/
|
||||
export type ToolResponse = {
|
||||
/**
|
||||
|
|
@ -3692,31 +3704,31 @@ export type TransferCallConfig = {
|
|||
/**
|
||||
* Destination
|
||||
*
|
||||
* Phone number or SIP endpoint to transfer the call to (E.164 format e.g., +1234567890, or SIP endpoint e.g., PJSIP/1234)
|
||||
* Phone number or SIP endpoint to transfer the call to, e.g. +1234567890 or PJSIP/1234.
|
||||
*/
|
||||
destination: string;
|
||||
/**
|
||||
* Messagetype
|
||||
*
|
||||
* Type of message to play before transfer
|
||||
* Type of message to play before transfer.
|
||||
*/
|
||||
messageType?: 'none' | 'custom' | 'audio';
|
||||
/**
|
||||
* Custommessage
|
||||
*
|
||||
* Custom message to play before transferring the call
|
||||
* Custom message to play before transferring.
|
||||
*/
|
||||
customMessage?: string | null;
|
||||
/**
|
||||
* Audiorecordingid
|
||||
*
|
||||
* Recording ID for audio message before transfer
|
||||
* Recording ID for audio message before transfer.
|
||||
*/
|
||||
audioRecordingId?: string | null;
|
||||
/**
|
||||
* Timeout
|
||||
*
|
||||
* Maximum time in seconds to wait for destination to answer (5-120 seconds)
|
||||
* Maximum seconds to wait for the destination to answer.
|
||||
*/
|
||||
timeout?: number;
|
||||
};
|
||||
|
|
@ -3730,17 +3742,17 @@ export type TransferCallToolDefinition = {
|
|||
/**
|
||||
* Schema Version
|
||||
*
|
||||
* Schema version
|
||||
* Schema version.
|
||||
*/
|
||||
schema_version?: number;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* Tool type
|
||||
* Tool type.
|
||||
*/
|
||||
type: 'transfer_call';
|
||||
/**
|
||||
* Transfer Call configuration
|
||||
* Transfer Call configuration.
|
||||
*/
|
||||
config: TransferCallConfig;
|
||||
};
|
||||
|
|
@ -3918,7 +3930,7 @@ export type UpdateFolderRequest = {
|
|||
/**
|
||||
* UpdateToolRequest
|
||||
*
|
||||
* Request schema for updating a tool.
|
||||
* Request schema for updating a reusable tool.
|
||||
*/
|
||||
export type UpdateToolRequest = {
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue