mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: agent versioning and model configurations override (#227)
* feat: add tests and migrations * feat: workflow versioning among published and draft * feat: add a new settings page to simplify workflow detail page * fix: fix tsclient generation
This commit is contained in:
parent
f5fa9ce717
commit
38d1d928b7
62 changed files with 10158 additions and 3131 deletions
|
|
@ -15,10 +15,12 @@ from api.db.workflow_template_client import WorkflowTemplateClient
|
|||
from api.enums import CallType
|
||||
from api.schemas.workflow import WorkflowRunResponseSchema
|
||||
from api.services.auth.depends import get_user
|
||||
from api.services.configuration.check_validity import UserConfigurationValidator
|
||||
from api.services.configuration.masking import (
|
||||
mask_workflow_definition,
|
||||
merge_workflow_api_keys,
|
||||
)
|
||||
from api.services.configuration.resolve import resolve_effective_config
|
||||
from api.services.mps_service_key_client import mps_service_key_client
|
||||
from api.services.workflow.dto import ReactFlowDTO
|
||||
from api.services.workflow.duplicate import duplicate_workflow
|
||||
|
|
@ -104,6 +106,8 @@ class WorkflowResponse(BaseModel):
|
|||
call_disposition_codes: CallDispositionCodes | None = None
|
||||
total_runs: int | None = None
|
||||
workflow_configurations: dict | None = None
|
||||
version_number: int | None = None
|
||||
version_status: str | None = None
|
||||
|
||||
|
||||
class WorkflowListResponse(BaseModel):
|
||||
|
|
@ -149,6 +153,17 @@ class UpdateWorkflowRequest(BaseModel):
|
|||
workflow_configurations: dict | None = None
|
||||
|
||||
|
||||
class WorkflowVersionResponse(BaseModel):
|
||||
id: int
|
||||
version_number: int
|
||||
status: str
|
||||
created_at: datetime
|
||||
published_at: datetime | None = None
|
||||
workflow_json: dict
|
||||
workflow_configurations: dict | None = None
|
||||
template_context_variables: dict | None = None
|
||||
|
||||
|
||||
class UpdateWorkflowStatusRequest(BaseModel):
|
||||
status: str # "active" or "archived"
|
||||
|
||||
|
|
@ -200,8 +215,11 @@ async def validate_workflow(
|
|||
|
||||
errors: list[WorkflowError] = []
|
||||
|
||||
# Get workflow definition from WorkflowDefinition table, fallback to workflow_definition field
|
||||
workflow_definition = workflow.workflow_definition_with_fallback
|
||||
# Validate draft if it exists (user is editing), else validate published
|
||||
draft = await db_client.get_draft_version(workflow_id)
|
||||
workflow_definition = (
|
||||
draft.workflow_json if draft else workflow.released_definition.workflow_json
|
||||
)
|
||||
|
||||
# ----------- DTO Validation ------------
|
||||
dto: Optional[ReactFlowDTO] = None
|
||||
|
|
@ -282,9 +300,7 @@ async def create_workflow(
|
|||
"name": workflow.name,
|
||||
"status": workflow.status,
|
||||
"created_at": workflow.created_at,
|
||||
"workflow_definition": mask_workflow_definition(
|
||||
workflow.workflow_definition_with_fallback
|
||||
),
|
||||
"workflow_definition": mask_workflow_definition(request.workflow_definition),
|
||||
"current_definition_id": workflow.current_definition_id,
|
||||
"template_context_variables": workflow.template_context_variables,
|
||||
"call_disposition_codes": workflow.call_disposition_codes,
|
||||
|
|
@ -362,9 +378,7 @@ async def create_workflow_from_template(
|
|||
"name": workflow.name,
|
||||
"status": workflow.status,
|
||||
"created_at": workflow.created_at,
|
||||
"workflow_definition": mask_workflow_definition(
|
||||
workflow.workflow_definition_with_fallback
|
||||
),
|
||||
"workflow_definition": mask_workflow_definition(workflow_def),
|
||||
"current_definition_id": workflow.current_definition_id,
|
||||
"template_context_variables": workflow.template_context_variables,
|
||||
"call_disposition_codes": workflow.call_disposition_codes,
|
||||
|
|
@ -461,7 +475,11 @@ async def get_workflow(
|
|||
workflow_id: int,
|
||||
user: UserModel = Depends(get_user),
|
||||
) -> WorkflowResponse:
|
||||
"""Get a single workflow by ID"""
|
||||
"""Get a single workflow by ID.
|
||||
|
||||
If a draft version exists, returns the draft content for editing.
|
||||
Otherwise returns the published version's content.
|
||||
"""
|
||||
workflow = await db_client.get_workflow(
|
||||
workflow_id, organization_id=user.selected_organization_id
|
||||
)
|
||||
|
|
@ -470,21 +488,123 @@ async def get_workflow(
|
|||
status_code=404, detail=f"Workflow with id {workflow_id} not found"
|
||||
)
|
||||
|
||||
# Check for draft — editor should show draft content if it exists
|
||||
draft = await db_client.get_draft_version(workflow_id)
|
||||
|
||||
if draft:
|
||||
workflow_def = draft.workflow_json
|
||||
workflow_configs = draft.workflow_configurations
|
||||
template_vars = draft.template_context_variables
|
||||
else:
|
||||
published = workflow.released_definition
|
||||
workflow_def = published.workflow_json
|
||||
workflow_configs = published.workflow_configurations
|
||||
template_vars = published.template_context_variables
|
||||
|
||||
active_def = draft or workflow.released_definition
|
||||
return {
|
||||
"id": workflow.id,
|
||||
"name": workflow.name,
|
||||
"status": workflow.status,
|
||||
"created_at": workflow.created_at,
|
||||
"workflow_definition": mask_workflow_definition(
|
||||
workflow.workflow_definition_with_fallback
|
||||
),
|
||||
"workflow_definition": mask_workflow_definition(workflow_def),
|
||||
"current_definition_id": workflow.current_definition_id,
|
||||
"template_context_variables": workflow.template_context_variables,
|
||||
"template_context_variables": template_vars,
|
||||
"call_disposition_codes": workflow.call_disposition_codes,
|
||||
"workflow_configurations": workflow.workflow_configurations,
|
||||
"workflow_configurations": workflow_configs,
|
||||
"version_number": active_def.version_number if active_def else None,
|
||||
"version_status": active_def.status if active_def else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{workflow_id}/versions")
|
||||
async def get_workflow_versions(
|
||||
workflow_id: int,
|
||||
user: UserModel = Depends(get_user),
|
||||
) -> list[WorkflowVersionResponse]:
|
||||
"""List all versions for a workflow, newest first."""
|
||||
workflow = await db_client.get_workflow(
|
||||
workflow_id, organization_id=user.selected_organization_id
|
||||
)
|
||||
if workflow is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Workflow with id {workflow_id} not found"
|
||||
)
|
||||
|
||||
versions = await db_client.get_workflow_versions(workflow_id)
|
||||
return [
|
||||
WorkflowVersionResponse(
|
||||
id=v.id,
|
||||
version_number=v.version_number,
|
||||
status=v.status,
|
||||
created_at=v.created_at,
|
||||
published_at=v.published_at,
|
||||
workflow_json=mask_workflow_definition(v.workflow_json),
|
||||
workflow_configurations=v.workflow_configurations,
|
||||
template_context_variables=v.template_context_variables,
|
||||
)
|
||||
for v in versions
|
||||
if v.version_number is not None
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/publish")
|
||||
async def publish_workflow(
|
||||
workflow_id: int,
|
||||
user: UserModel = Depends(get_user),
|
||||
):
|
||||
"""Publish the current draft version of a workflow."""
|
||||
workflow = await db_client.get_workflow(
|
||||
workflow_id, organization_id=user.selected_organization_id
|
||||
)
|
||||
if workflow is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Workflow with id {workflow_id} not found"
|
||||
)
|
||||
|
||||
try:
|
||||
published = await db_client.publish_workflow_draft(workflow_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
return {
|
||||
"id": published.id,
|
||||
"version_number": published.version_number,
|
||||
"status": published.status,
|
||||
"published_at": published.published_at,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{workflow_id}/create-draft")
|
||||
async def create_workflow_draft(
|
||||
workflow_id: int,
|
||||
user: UserModel = Depends(get_user),
|
||||
) -> WorkflowVersionResponse:
|
||||
"""Create a draft version from the current published version.
|
||||
|
||||
If a draft already exists, returns the existing draft.
|
||||
"""
|
||||
workflow = await db_client.get_workflow(
|
||||
workflow_id, organization_id=user.selected_organization_id
|
||||
)
|
||||
if workflow is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Workflow with id {workflow_id} not found"
|
||||
)
|
||||
|
||||
draft = await db_client.save_workflow_draft(workflow_id)
|
||||
return WorkflowVersionResponse(
|
||||
id=draft.id,
|
||||
version_number=draft.version_number,
|
||||
status=draft.status,
|
||||
created_at=draft.created_at,
|
||||
published_at=draft.published_at,
|
||||
workflow_json=mask_workflow_definition(draft.workflow_json),
|
||||
workflow_configurations=draft.workflow_configurations,
|
||||
template_context_variables=draft.template_context_variables,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def get_workflows_summary(
|
||||
user: UserModel = Depends(get_user),
|
||||
|
|
@ -528,7 +648,7 @@ async def update_workflow_status(
|
|||
"status": workflow.status,
|
||||
"created_at": workflow.created_at,
|
||||
"workflow_definition": mask_workflow_definition(
|
||||
workflow.workflow_definition_with_fallback
|
||||
workflow.released_definition.workflow_json
|
||||
),
|
||||
"current_definition_id": workflow.current_definition_id,
|
||||
"template_context_variables": workflow.template_context_variables,
|
||||
|
|
@ -569,11 +689,37 @@ async def update_workflow(
|
|||
workflow_id, organization_id=user.selected_organization_id
|
||||
)
|
||||
if existing_workflow:
|
||||
# Merge against what the user was editing (draft or published)
|
||||
existing_draft = await db_client.get_draft_version(workflow_id)
|
||||
existing_def = (
|
||||
existing_draft.workflow_json
|
||||
if existing_draft
|
||||
else existing_workflow.released_definition.workflow_json
|
||||
)
|
||||
workflow_definition = merge_workflow_api_keys(
|
||||
workflow_definition,
|
||||
existing_workflow.workflow_definition_with_fallback,
|
||||
existing_def,
|
||||
)
|
||||
|
||||
# Validate model_overrides: resolve onto global config, then
|
||||
# run the same validator used by the user-configurations endpoint.
|
||||
if request.workflow_configurations and request.workflow_configurations.get(
|
||||
"model_overrides"
|
||||
):
|
||||
user_config = await db_client.get_user_configurations(user.id)
|
||||
try:
|
||||
effective = resolve_effective_config(
|
||||
user_config,
|
||||
request.workflow_configurations["model_overrides"],
|
||||
)
|
||||
await UserConfigurationValidator().validate(
|
||||
effective,
|
||||
organization_id=user.selected_organization_id,
|
||||
created_by=user.provider_id,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
|
||||
workflow = await db_client.update_workflow(
|
||||
workflow_id=workflow_id,
|
||||
name=request.name,
|
||||
|
|
@ -592,19 +738,35 @@ async def update_workflow(
|
|||
trigger_paths=trigger_paths,
|
||||
)
|
||||
|
||||
# Return draft content if one exists (save creates a draft)
|
||||
draft = await db_client.get_draft_version(workflow_id)
|
||||
if draft:
|
||||
workflow_def = draft.workflow_json
|
||||
workflow_configs = draft.workflow_configurations
|
||||
template_vars = draft.template_context_variables
|
||||
else:
|
||||
published = workflow.released_definition
|
||||
workflow_def = published.workflow_json
|
||||
workflow_configs = published.workflow_configurations
|
||||
template_vars = published.template_context_variables
|
||||
|
||||
# Include version info from the active definition (draft or published)
|
||||
active_def = draft or workflow.released_definition
|
||||
return {
|
||||
"id": workflow.id,
|
||||
"name": workflow.name,
|
||||
"status": workflow.status,
|
||||
"created_at": workflow.created_at,
|
||||
"workflow_definition": mask_workflow_definition(
|
||||
workflow.workflow_definition_with_fallback
|
||||
),
|
||||
"workflow_definition": mask_workflow_definition(workflow_def),
|
||||
"current_definition_id": workflow.current_definition_id,
|
||||
"template_context_variables": workflow.template_context_variables,
|
||||
"template_context_variables": template_vars,
|
||||
"call_disposition_codes": workflow.call_disposition_codes,
|
||||
"workflow_configurations": workflow.workflow_configurations,
|
||||
"workflow_configurations": workflow_configs,
|
||||
"version_number": active_def.version_number if active_def else None,
|
||||
"version_status": active_def.status if active_def else None,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
|
|
@ -629,7 +791,7 @@ async def duplicate_workflow_endpoint(
|
|||
"status": workflow.status,
|
||||
"created_at": workflow.created_at,
|
||||
"workflow_definition": mask_workflow_definition(
|
||||
workflow.workflow_definition_with_fallback
|
||||
workflow.released_definition.workflow_json
|
||||
),
|
||||
"current_definition_id": workflow.current_definition_id,
|
||||
"template_context_variables": workflow.template_context_variables,
|
||||
|
|
@ -658,7 +820,7 @@ async def create_workflow_run(
|
|||
user: The user to create the workflow run for
|
||||
"""
|
||||
run = await db_client.create_workflow_run(
|
||||
request.name, workflow_id, request.mode, user.id
|
||||
request.name, workflow_id, request.mode, user.id, use_draft=True
|
||||
)
|
||||
return {
|
||||
"id": run.id,
|
||||
|
|
@ -862,9 +1024,7 @@ async def duplicate_workflow_template(
|
|||
"name": workflow.name,
|
||||
"status": workflow.status,
|
||||
"created_at": workflow.created_at,
|
||||
"workflow_definition": mask_workflow_definition(
|
||||
workflow.workflow_definition_with_fallback
|
||||
),
|
||||
"workflow_definition": mask_workflow_definition(workflow_def),
|
||||
"current_definition_id": workflow.current_definition_id,
|
||||
"template_context_variables": workflow.template_context_variables,
|
||||
"call_disposition_codes": workflow.call_disposition_codes,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue