Workflow Copilot: backend side of the first version (#4401)

This commit is contained in:
Stanislav Novosad
2026-01-06 14:58:44 -07:00
committed by GitHub
parent 1e314ce149
commit e3dd75d7c1
10 changed files with 1440 additions and 0 deletions

View File

@@ -47,6 +47,8 @@ from skyvern.forge.sdk.db.models import (
TaskV2Model,
ThoughtModel,
TOTPCodeModel,
WorkflowCopilotChatMessageModel,
WorkflowCopilotChatModel,
WorkflowModel,
WorkflowParameterModel,
WorkflowRunBlockModel,
@@ -72,6 +74,7 @@ from skyvern.forge.sdk.db.utils import (
convert_to_task,
convert_to_task_v2,
convert_to_workflow,
convert_to_workflow_copilot_chat_message,
convert_to_workflow_parameter,
convert_to_workflow_run,
convert_to_workflow_run_block,
@@ -100,6 +103,11 @@ from skyvern.forge.sdk.schemas.task_generations import TaskGeneration
from skyvern.forge.sdk.schemas.task_v2 import TaskV2, TaskV2Status, Thought, ThoughtType
from skyvern.forge.sdk.schemas.tasks import OrderBy, SortDirection, Task, TaskStatus
from skyvern.forge.sdk.schemas.totp_codes import OTPType, TOTPCode
from skyvern.forge.sdk.schemas.workflow_copilot import (
WorkflowCopilotChat,
WorkflowCopilotChatMessage,
WorkflowCopilotChatSender,
)
from skyvern.forge.sdk.schemas.workflow_runs import WorkflowRunBlock
from skyvern.forge.sdk.workflow.models.parameter import (
AWSSecretParameter,
@@ -3640,6 +3648,91 @@ class AgentDB(BaseAlchemyDB):
await session.refresh(new_ai_suggestion)
return AISuggestion.model_validate(new_ai_suggestion)
async def create_workflow_copilot_chat(
self,
organization_id: str,
workflow_permanent_id: str,
) -> WorkflowCopilotChat:
async with self.Session() as session:
new_chat = WorkflowCopilotChatModel(
organization_id=organization_id,
workflow_permanent_id=workflow_permanent_id,
)
session.add(new_chat)
await session.commit()
await session.refresh(new_chat)
return WorkflowCopilotChat.model_validate(new_chat)
async def create_workflow_copilot_chat_message(
self,
organization_id: str,
workflow_copilot_chat_id: str,
sender: WorkflowCopilotChatSender,
content: str,
global_llm_context: str | None = None,
) -> WorkflowCopilotChatMessage:
async with self.Session() as session:
new_message = WorkflowCopilotChatMessageModel(
workflow_copilot_chat_id=workflow_copilot_chat_id,
organization_id=organization_id,
sender=sender,
content=content,
global_llm_context=global_llm_context,
)
session.add(new_message)
await session.commit()
await session.refresh(new_message)
return convert_to_workflow_copilot_chat_message(new_message, self.debug_enabled)
async def get_workflow_copilot_chat_messages(
self,
workflow_copilot_chat_id: str,
) -> list[WorkflowCopilotChatMessage]:
async with self.Session() as session:
query = (
select(WorkflowCopilotChatMessageModel)
.filter(WorkflowCopilotChatMessageModel.workflow_copilot_chat_id == workflow_copilot_chat_id)
.order_by(WorkflowCopilotChatMessageModel.workflow_copilot_chat_message_id.asc())
)
messages = (await session.scalars(query)).all()
return [convert_to_workflow_copilot_chat_message(message, self.debug_enabled) for message in messages]
async def get_workflow_copilot_chat_by_id(
self,
organization_id: str,
workflow_copilot_chat_id: str,
) -> WorkflowCopilotChat | None:
async with self.Session() as session:
query = (
select(WorkflowCopilotChatModel)
.filter(WorkflowCopilotChatModel.organization_id == organization_id)
.filter(WorkflowCopilotChatModel.workflow_copilot_chat_id == workflow_copilot_chat_id)
.order_by(WorkflowCopilotChatModel.created_at.desc())
.limit(1)
)
chat = (await session.scalars(query)).first()
if not chat:
return None
return WorkflowCopilotChat.model_validate(chat)
async def get_latest_workflow_copilot_chat(
self,
organization_id: str,
workflow_permanent_id: str,
) -> WorkflowCopilotChat | None:
async with self.Session() as session:
query = (
select(WorkflowCopilotChatModel)
.filter(WorkflowCopilotChatModel.organization_id == organization_id)
.filter(WorkflowCopilotChatModel.workflow_permanent_id == workflow_permanent_id)
.order_by(WorkflowCopilotChatModel.created_at.desc())
.limit(1)
)
chat = (await session.scalars(query)).first()
if not chat:
return None
return WorkflowCopilotChat.model_validate(chat)
async def get_task_generation_by_prompt_hash(
self,
user_prompt_hash: str,

View File

@@ -69,6 +69,8 @@ WORKFLOW_RUN_PREFIX = "wr"
WORKFLOW_SCRIPT_PREFIX = "ws"
WORKFLOW_TEMPLATE_PREFIX = "wt"
ORGANIZATION_BILLING_PREFIX = "ob"
WORKFLOW_COPILOT_CHAT_PREFIX = "wcc"
WORKFLOW_COPILOT_CHAT_MESSAGE_PREFIX = "wccm"
def generate_workflow_id() -> str:
@@ -266,6 +268,16 @@ def generate_billing_id() -> str:
return f"{ORGANIZATION_BILLING_PREFIX}_{int_id}"
def generate_workflow_copilot_chat_id() -> str:
int_id = generate_id()
return f"{WORKFLOW_COPILOT_CHAT_PREFIX}_{int_id}"
def generate_workflow_copilot_chat_message_id() -> str:
int_id = generate_id()
return f"{WORKFLOW_COPILOT_CHAT_MESSAGE_PREFIX}_{int_id}"
############# Helper functions below ##############
def generate_id() -> int:
"""

View File

@@ -51,6 +51,8 @@ from skyvern.forge.sdk.db.id import (
generate_task_v2_id,
generate_thought_id,
generate_totp_code_id,
generate_workflow_copilot_chat_id,
generate_workflow_copilot_chat_message_id,
generate_workflow_id,
generate_workflow_parameter_id,
generate_workflow_permanent_id,
@@ -1081,3 +1083,40 @@ class ScriptBlockModel(Base):
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
modified_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow, nullable=False)
deleted_at = Column(DateTime, nullable=True)
class WorkflowCopilotChatModel(Base):
__tablename__ = "workflow_copilot_chats"
workflow_copilot_chat_id = Column(String, primary_key=True, default=generate_workflow_copilot_chat_id)
organization_id = Column(String, nullable=False)
workflow_permanent_id = Column(String, nullable=False, index=True)
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
modified_at = Column(
DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
nullable=False,
)
class WorkflowCopilotChatMessageModel(Base):
__tablename__ = "workflow_copilot_chat_messages"
workflow_copilot_chat_message_id = Column(
String, primary_key=True, default=generate_workflow_copilot_chat_message_id
)
workflow_copilot_chat_id = Column(String, nullable=False, index=True)
organization_id = Column(String, nullable=False)
sender = Column(String, nullable=False)
content = Column(UnicodeText, nullable=False)
global_llm_context = Column(UnicodeText, nullable=True)
created_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
modified_at = Column(
DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
nullable=False,
)

View File

@@ -21,6 +21,7 @@ from skyvern.forge.sdk.db.models import (
StepModel,
TaskModel,
TaskV2Model,
WorkflowCopilotChatMessageModel,
WorkflowModel,
WorkflowParameterModel,
WorkflowRunBlockModel,
@@ -39,6 +40,7 @@ from skyvern.forge.sdk.schemas.organizations import (
)
from skyvern.forge.sdk.schemas.task_v2 import TaskV2
from skyvern.forge.sdk.schemas.tasks import Task, TaskStatus
from skyvern.forge.sdk.schemas.workflow_copilot import WorkflowCopilotChatMessage as WorkflowCopilotChatMessageSchema
from skyvern.forge.sdk.schemas.workflow_runs import WorkflowRunBlock
from skyvern.forge.sdk.workflow.models.parameter import (
AWSSecretParameter,
@@ -217,6 +219,17 @@ def convert_to_task_v2(task_v2_model: TaskV2Model, debug_enabled: bool = False)
return TaskV2.model_validate(task_v2_data)
def convert_to_workflow_copilot_chat_message(
message_model: WorkflowCopilotChatMessageModel, debug_enabled: bool = False
) -> WorkflowCopilotChatMessageSchema:
if debug_enabled:
LOG.debug(
"Converting WorkflowCopilotChatMessage to WorkflowCopilotChatMessageSchema",
workflow_copilot_chat_message_id=message_model.workflow_copilot_chat_message_id,
)
return WorkflowCopilotChatMessageSchema.model_validate(message_model)
def convert_to_step(step_model: StepModel, debug_enabled: bool = False) -> Step:
if debug_enabled:
LOG.debug("Converting StepModel to Step", step_id=step_model.step_id)