Merge branch 'main' into feat/vici-dial

This commit is contained in:
Abhishek Kumar 2026-07-20 21:22:26 +05:30
commit 9458fdb67e
27 changed files with 2118 additions and 157 deletions

View file

@ -696,8 +696,8 @@ async def update_campaign(
@router.get("/{campaign_id}/runs")
async def get_campaign_runs(
campaign_id: int,
page: int = 1,
limit: int = 50,
page: int = Query(1, ge=1, description="Page number (starts from 1)"),
limit: int = Query(50, ge=1, le=100, description="Number of items per page"),
filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"),
sort_by: Optional[str] = Query(
None, description="Field to sort by (e.g., 'duration', 'created_at')"

View file

@ -1,5 +1,6 @@
"""API routes for managing tools."""
import time
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException
@ -22,6 +23,8 @@ from api.schemas.tool import (
ToolDefinition,
ToolParameter,
ToolResponse,
ToolTestRequest,
ToolTestResponse,
TransferCallConfig,
TransferCallToolDefinition,
UpdateToolRequest,
@ -39,6 +42,10 @@ from api.services.tool_management import (
from api.services.tool_management import (
populate_discovered_tools as _populate_discovered_tools,
)
from api.services.workflow.tools.custom_tool import (
execute_http_tool,
serialize_query_params,
)
router = APIRouter(prefix="/tools")
@ -57,6 +64,8 @@ __all__ = [
"ToolDefinition",
"ToolParameter",
"ToolResponse",
"ToolTestRequest",
"ToolTestResponse",
"TransferCallConfig",
"TransferCallToolDefinition",
"UpdateToolRequest",
@ -196,6 +205,149 @@ async def refresh_mcp_tools(
raise HTTPException(status_code=e.status_code, detail=e.message) from e
@router.post("/{tool_uuid}/test")
async def test_tool(
tool_uuid: str,
request: ToolTestRequest,
user: UserModel = Depends(get_user),
) -> ToolTestResponse:
"""Execute an HTTP API tool with sample LLM and preset parameters."""
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.HTTP_API.value:
raise HTTPException(status_code=400, detail="Only HTTP API tools can be tested")
tool_config = (
tool.definition.get("config", {}) if isinstance(tool.definition, dict) else {}
)
configured_method = tool_config.get("method", "?")
configured_url = tool_config.get("url", "?")
started_at = time.perf_counter()
result = await execute_http_tool(
tool,
request.llm_params,
preset_params=request.preset_params,
organization_id=user.selected_organization_id,
include_request_headers=True,
)
duration_ms = max(0, round((time.perf_counter() - started_at) * 1000))
status = result.get("status", "error")
status_code = result.get("status_code")
if status_code is not None and status_code >= 400:
status = "error"
hint = _hint_for_status_code(status_code, configured_method)
# Preset values take precedence over model-supplied values, matching live
# execution after configured preset templates have been resolved.
resolved_arguments = {**request.llm_params, **request.preset_params}
# Mirror execute_http_tool's own branch: POST/PUT/PATCH send the
# resolved arguments as a JSON body; GET/DELETE send them as query
# params. Never both.
request_body = None
request_params = None
if configured_method in ("POST", "PUT", "PATCH"):
request_body = resolved_arguments # keep {} so preview matches wire request
elif resolved_arguments:
request_params = serialize_query_params(resolved_arguments)
return ToolTestResponse(
status=status,
status_code=status_code,
data=result.get("data"),
error=result.get("error"),
duration_ms=duration_ms,
hint=hint,
request_method=configured_method,
request_url=configured_url,
request_headers=result.get("request_headers", {}),
request_body=request_body,
request_params=request_params,
)
def _hint_for_status_code(
status_code: Optional[int], configured_method: str
) -> Optional[str]:
"""Human-readable explanation for a status code a misconfigured tool
is likely to hit. Returns None for 2xx and any code not covered."""
if status_code == 400:
return (
"HTTP 400 Bad Request — the server rejected the request payload. "
"Verify the arguments/body match what this endpoint expects."
)
if status_code == 401:
return (
"HTTP 401 Unauthorized — the request wasn't authenticated. Check "
"the credential configured on the Authentication tab is present "
"and valid."
)
if status_code == 403:
return (
"HTTP 403 Forbidden — authenticated, but the configured "
"credential doesn't have permission for this endpoint/action."
)
if status_code == 404:
return (
f"HTTP 404 Not Found — verify the endpoint URL is correct and "
f"that {configured_method} is a valid method for it."
)
if status_code == 405:
return (
f"HTTP 405 Method Not Allowed — the endpoint rejected the "
f"configured method ({configured_method}). Verify the API expects "
f"{configured_method} for this URL."
)
if status_code == 408:
return (
"HTTP 408 Request Timeout — the endpoint didn't respond in time. "
"Check the endpoint is reachable, or increase Timeout (ms) if it's "
"just slow."
)
if status_code == 409:
return (
"HTTP 409 Conflict — the endpoint rejected the request due to a "
"conflicting resource state (e.g. duplicate create). Not "
"necessarily a configuration problem."
)
if status_code == 415:
return (
"HTTP 415 Unsupported Media Type — check the Content-Type header "
"matches the format this endpoint expects for the body."
)
if status_code == 422:
return (
"HTTP 422 Unprocessable Entity — the request was well-formed but "
"the payload's structure or field types don't match what this "
"endpoint expects. Compare your arguments against the API's "
"documented schema."
)
if status_code == 429:
return (
"HTTP 429 Too Many Requests — the endpoint is rate-limiting. Wait "
"and retry; not a configuration problem."
)
if status_code is not None and 500 <= status_code < 600:
return (
f"HTTP {status_code} — the endpoint itself errored. This is "
"likely an issue on the API's side, not your tool configuration."
)
return None
@router.put("/{tool_uuid}")
async def update_tool(
tool_uuid: str,

View file

@ -1407,8 +1407,8 @@ class WorkflowRunsResponse(BaseModel):
@router.get("/{workflow_id}/runs")
async def get_workflow_runs(
workflow_id: int,
page: int = 1,
limit: int = 50,
page: int = Query(1, ge=1, description="Page number (starts from 1)"),
limit: int = Query(50, ge=1, le=100, description="Number of items per page"),
filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"),
sort_by: Optional[str] = Query(
None, description="Field to sort by (e.g., 'duration', 'created_at')"