mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-28 08:49:42 +02:00
fix: fix projection to TS when fetching agnet in MCP
This commit is contained in:
parent
3892b58486
commit
bbb4f91a27
12 changed files with 392 additions and 63 deletions
55
api/mcp_server/tools/_workflow_projection.py
Normal file
55
api/mcp_server/tools/_workflow_projection.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
|
||||
from api.db import db_client
|
||||
from api.mcp_server.ts_bridge import generate_code
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkflowProjectionSource:
|
||||
payload: dict[str, Any] | None
|
||||
version: Literal["draft", "published", "legacy"]
|
||||
version_number: int | None
|
||||
|
||||
|
||||
async def select_workflow_projection_source(workflow: Any) -> WorkflowProjectionSource:
|
||||
"""Choose the same working copy across read and save MCP tools.
|
||||
|
||||
Draft wins over published because that's what a human editor would
|
||||
be mutating. Legacy `workflow_definition` is the final fallback for
|
||||
older rows that predate versioned definitions.
|
||||
"""
|
||||
draft = await db_client.get_draft_version(workflow.id)
|
||||
if draft is not None and draft.workflow_json:
|
||||
return WorkflowProjectionSource(
|
||||
payload=draft.workflow_json,
|
||||
version="draft",
|
||||
version_number=draft.version_number,
|
||||
)
|
||||
|
||||
released = workflow.released_definition
|
||||
if released is not None and released.workflow_json:
|
||||
return WorkflowProjectionSource(
|
||||
payload=released.workflow_json,
|
||||
version="published",
|
||||
version_number=released.version_number,
|
||||
)
|
||||
|
||||
return WorkflowProjectionSource(
|
||||
payload=workflow.workflow_definition or None,
|
||||
version="legacy",
|
||||
version_number=None,
|
||||
)
|
||||
|
||||
|
||||
async def project_workflow_to_sdk_view(workflow: Any) -> dict[str, Any]:
|
||||
source = await select_workflow_projection_source(workflow)
|
||||
code = await generate_code(source.payload or {}, workflow_name=workflow.name or "")
|
||||
return {
|
||||
"name": workflow.name or "",
|
||||
"version": source.version,
|
||||
"version_number": source.version_number,
|
||||
"code": code,
|
||||
}
|
||||
|
|
@ -18,8 +18,9 @@ from fastapi import HTTPException
|
|||
|
||||
from api.db import db_client
|
||||
from api.mcp_server.auth import authenticate_mcp_request
|
||||
from api.mcp_server.tools._workflow_projection import project_workflow_to_sdk_view
|
||||
from api.mcp_server.tracing import traced_tool
|
||||
from api.mcp_server.ts_bridge import TsBridgeError, generate_code
|
||||
from api.mcp_server.ts_bridge import TsBridgeError
|
||||
|
||||
|
||||
@traced_tool
|
||||
|
|
@ -39,31 +40,14 @@ async def get_workflow_code(workflow_id: int) -> dict[str, Any]:
|
|||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail=f"Workflow {workflow_id} not found")
|
||||
|
||||
# Draft wins over published — editing a draft is the normal flow.
|
||||
# `current_definition` (is_current=True) is the published row, so we
|
||||
# fetch the draft explicitly. If the latest draft was just published,
|
||||
# no draft row exists and we fall through to `released_definition`.
|
||||
draft = await db_client.get_draft_version(workflow_id)
|
||||
released = workflow.released_definition
|
||||
|
||||
if draft is not None and draft.workflow_json:
|
||||
payload = draft.workflow_json
|
||||
source = "draft"
|
||||
elif released is not None and released.workflow_json:
|
||||
payload = released.workflow_json
|
||||
source = "published"
|
||||
else:
|
||||
payload = workflow.workflow_definition or {}
|
||||
source = "legacy"
|
||||
|
||||
try:
|
||||
code = await generate_code(payload, workflow_name=workflow.name or "")
|
||||
view = await project_workflow_to_sdk_view(workflow)
|
||||
except TsBridgeError as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate code: {e}")
|
||||
|
||||
return {
|
||||
"workflow_id": workflow_id,
|
||||
"name": workflow.name or "",
|
||||
"version": source,
|
||||
"code": code,
|
||||
"name": view["name"],
|
||||
"version": view["version"],
|
||||
"code": view["code"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ from pydantic import ValidationError as PydanticValidationError
|
|||
|
||||
from api.db import db_client
|
||||
from api.mcp_server.auth import authenticate_mcp_request
|
||||
from api.mcp_server.tools._workflow_projection import (
|
||||
select_workflow_projection_source,
|
||||
)
|
||||
from api.mcp_server.tracing import traced_tool
|
||||
from api.mcp_server.ts_bridge import TsBridgeError, parse_code
|
||||
from api.services.workflow.dto import ReactFlowDTO
|
||||
|
|
@ -37,20 +40,9 @@ from api.services.workflow.workflow_graph import WorkflowGraph
|
|||
|
||||
|
||||
async def _previous_workflow_json(workflow: Any) -> dict[str, Any] | None:
|
||||
"""Same selection priority as `get_workflow_code` — the version the
|
||||
LLM saw is the version we reconcile against.
|
||||
|
||||
`current_definition` (is_current=True) is the published row, so the
|
||||
draft must be fetched explicitly. If no draft exists (e.g. the last
|
||||
draft was just published), fall through to `released_definition`.
|
||||
"""
|
||||
draft = await db_client.get_draft_version(workflow.id)
|
||||
if draft is not None and draft.workflow_json:
|
||||
return draft.workflow_json
|
||||
released = workflow.released_definition
|
||||
if released is not None and released.workflow_json:
|
||||
return released.workflow_json
|
||||
return workflow.workflow_definition or None
|
||||
"""Match the agent-facing read tools' source selection."""
|
||||
source = await select_workflow_projection_source(workflow)
|
||||
return source.payload
|
||||
|
||||
|
||||
def _error_result(code: str, message: str, **extra: Any) -> dict[str, Any]:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ from fastapi import HTTPException
|
|||
|
||||
from api.db import db_client
|
||||
from api.mcp_server.auth import authenticate_mcp_request
|
||||
from api.mcp_server.tools._workflow_projection import project_workflow_to_sdk_view
|
||||
from api.mcp_server.tracing import traced_tool
|
||||
from api.mcp_server.ts_bridge import TsBridgeError
|
||||
|
||||
|
||||
@traced_tool
|
||||
|
|
@ -10,9 +12,9 @@ async def list_workflows(status: str | None = "active") -> list[dict]:
|
|||
"""List agents (workflows) in the caller's organization.
|
||||
|
||||
Returns id, name, status, and created_at for each agent. Use
|
||||
`get_workflow` to fetch a single agent's full definition. Defaults
|
||||
to active agents; pass `status="archived"` to list archived agents,
|
||||
or `status=None` to list all.
|
||||
`get_workflow` to fetch a single agent's current SDK view and
|
||||
metadata. Defaults to active agents; pass `status="archived"` to
|
||||
list archived agents, or `status=None` to list all.
|
||||
"""
|
||||
user = await authenticate_mcp_request()
|
||||
workflows = await db_client.get_all_workflows_for_listing(
|
||||
|
|
@ -32,7 +34,11 @@ async def list_workflows(status: str | None = "active") -> list[dict]:
|
|||
|
||||
@traced_tool
|
||||
async def get_workflow(workflow_id: int) -> dict:
|
||||
"""Fetch a single agent by id, including its current published definition."""
|
||||
"""Fetch a single agent by id, projected into the SDK code view.
|
||||
|
||||
Output shape:
|
||||
{"id": int, "name": str, "status": str, "version": "draft" | "published" | "legacy", "version_number": int | None, "code": "<TS source>"}
|
||||
"""
|
||||
user = await authenticate_mcp_request()
|
||||
workflow = await db_client.get_workflow(
|
||||
workflow_id, organization_id=user.selected_organization_id
|
||||
|
|
@ -40,11 +46,16 @@ async def get_workflow(workflow_id: int) -> dict:
|
|||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail=f"Workflow {workflow_id} not found")
|
||||
|
||||
current = workflow.current_definition
|
||||
try:
|
||||
view = await project_workflow_to_sdk_view(workflow)
|
||||
except TsBridgeError as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate code: {e}")
|
||||
|
||||
return {
|
||||
"id": workflow.id,
|
||||
"name": workflow.name,
|
||||
"name": view["name"],
|
||||
"status": workflow.status,
|
||||
"definition": current.workflow_json if current else None,
|
||||
"version_number": current.version_number if current else None,
|
||||
"version": view["version"],
|
||||
"version_number": view["version_number"],
|
||||
"code": view["code"],
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue