mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
Merge branch 'main' into feat/vici-dial
This commit is contained in:
commit
9458fdb67e
27 changed files with 2118 additions and 157 deletions
|
|
@ -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')"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue