You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

workflow.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  1. import json
  2. from collections.abc import Mapping, Sequence
  3. from datetime import UTC, datetime
  4. from enum import Enum, StrEnum
  5. from typing import TYPE_CHECKING, Any, Optional, Self, Union
  6. from uuid import uuid4
  7. if TYPE_CHECKING:
  8. from models.model import AppMode
  9. import sqlalchemy as sa
  10. from sqlalchemy import Index, PrimaryKeyConstraint, func
  11. from sqlalchemy.orm import Mapped, mapped_column
  12. import contexts
  13. from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE
  14. from core.helper import encrypter
  15. from core.variables import SecretVariable, Variable
  16. from factories import variable_factory
  17. from libs import helper
  18. from models.base import Base
  19. from models.enums import CreatedByRole
  20. from .account import Account
  21. from .engine import db
  22. from .types import StringUUID
  23. if TYPE_CHECKING:
  24. from models.model import AppMode
  25. class WorkflowType(Enum):
  26. """
  27. Workflow Type Enum
  28. """
  29. WORKFLOW = "workflow"
  30. CHAT = "chat"
  31. @classmethod
  32. def value_of(cls, value: str) -> "WorkflowType":
  33. """
  34. Get value of given mode.
  35. :param value: mode value
  36. :return: mode
  37. """
  38. for mode in cls:
  39. if mode.value == value:
  40. return mode
  41. raise ValueError(f"invalid workflow type value {value}")
  42. @classmethod
  43. def from_app_mode(cls, app_mode: Union[str, "AppMode"]) -> "WorkflowType":
  44. """
  45. Get workflow type from app mode.
  46. :param app_mode: app mode
  47. :return: workflow type
  48. """
  49. from models.model import AppMode
  50. app_mode = app_mode if isinstance(app_mode, AppMode) else AppMode.value_of(app_mode)
  51. return cls.WORKFLOW if app_mode == AppMode.WORKFLOW else cls.CHAT
  52. class Workflow(Base):
  53. """
  54. Workflow, for `Workflow App` and `Chat App workflow mode`.
  55. Attributes:
  56. - id (uuid) Workflow ID, pk
  57. - tenant_id (uuid) Workspace ID
  58. - app_id (uuid) App ID
  59. - type (string) Workflow type
  60. `workflow` for `Workflow App`
  61. `chat` for `Chat App workflow mode`
  62. - version (string) Version
  63. `draft` for draft version (only one for each app), other for version number (redundant)
  64. - graph (text) Workflow canvas configuration (JSON)
  65. The entire canvas configuration JSON, including Node, Edge, and other configurations
  66. - nodes (array[object]) Node list, see Node Schema
  67. - edges (array[object]) Edge list, see Edge Schema
  68. - created_by (uuid) Creator ID
  69. - created_at (timestamp) Creation time
  70. - updated_by (uuid) `optional` Last updater ID
  71. - updated_at (timestamp) `optional` Last update time
  72. """
  73. __tablename__ = "workflows"
  74. __table_args__ = (
  75. db.PrimaryKeyConstraint("id", name="workflow_pkey"),
  76. db.Index("workflow_version_idx", "tenant_id", "app_id", "version"),
  77. )
  78. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  79. tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  80. app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  81. type: Mapped[str] = mapped_column(db.String(255), nullable=False)
  82. version: Mapped[str] = mapped_column(db.String(255), nullable=False)
  83. marked_name: Mapped[str] = mapped_column(default="", server_default="")
  84. marked_comment: Mapped[str] = mapped_column(default="", server_default="")
  85. graph: Mapped[str] = mapped_column(sa.Text)
  86. _features: Mapped[str] = mapped_column("features", sa.TEXT)
  87. created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
  88. created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  89. updated_by: Mapped[Optional[str]] = mapped_column(StringUUID)
  90. updated_at: Mapped[datetime] = mapped_column(
  91. db.DateTime,
  92. nullable=False,
  93. default=datetime.now(UTC).replace(tzinfo=None),
  94. server_onupdate=func.current_timestamp(),
  95. )
  96. _environment_variables: Mapped[str] = mapped_column(
  97. "environment_variables", db.Text, nullable=False, server_default="{}"
  98. )
  99. _conversation_variables: Mapped[str] = mapped_column(
  100. "conversation_variables", db.Text, nullable=False, server_default="{}"
  101. )
  102. @classmethod
  103. def new(
  104. cls,
  105. *,
  106. tenant_id: str,
  107. app_id: str,
  108. type: str,
  109. version: str,
  110. graph: str,
  111. features: str,
  112. created_by: str,
  113. environment_variables: Sequence[Variable],
  114. conversation_variables: Sequence[Variable],
  115. marked_name: str = "",
  116. marked_comment: str = "",
  117. ) -> Self:
  118. workflow = Workflow()
  119. workflow.id = str(uuid4())
  120. workflow.tenant_id = tenant_id
  121. workflow.app_id = app_id
  122. workflow.type = type
  123. workflow.version = version
  124. workflow.graph = graph
  125. workflow.features = features
  126. workflow.created_by = created_by
  127. workflow.environment_variables = environment_variables or []
  128. workflow.conversation_variables = conversation_variables or []
  129. workflow.marked_name = marked_name
  130. workflow.marked_comment = marked_comment
  131. workflow.created_at = datetime.now(UTC).replace(tzinfo=None)
  132. workflow.updated_at = workflow.created_at
  133. return workflow
  134. @property
  135. def created_by_account(self):
  136. return db.session.get(Account, self.created_by)
  137. @property
  138. def updated_by_account(self):
  139. return db.session.get(Account, self.updated_by) if self.updated_by else None
  140. @property
  141. def graph_dict(self) -> Mapping[str, Any]:
  142. return json.loads(self.graph) if self.graph else {}
  143. @property
  144. def features(self) -> str:
  145. """
  146. Convert old features structure to new features structure.
  147. """
  148. if not self._features:
  149. return self._features
  150. features = json.loads(self._features)
  151. if features.get("file_upload", {}).get("image", {}).get("enabled", False):
  152. image_enabled = True
  153. image_number_limits = int(features["file_upload"]["image"].get("number_limits", DEFAULT_FILE_NUMBER_LIMITS))
  154. image_transfer_methods = features["file_upload"]["image"].get(
  155. "transfer_methods", ["remote_url", "local_file"]
  156. )
  157. features["file_upload"]["enabled"] = image_enabled
  158. features["file_upload"]["number_limits"] = image_number_limits
  159. features["file_upload"]["allowed_file_upload_methods"] = image_transfer_methods
  160. features["file_upload"]["allowed_file_types"] = features["file_upload"].get("allowed_file_types", ["image"])
  161. features["file_upload"]["allowed_file_extensions"] = []
  162. del features["file_upload"]["image"]
  163. self._features = json.dumps(features)
  164. return self._features
  165. @features.setter
  166. def features(self, value: str) -> None:
  167. self._features = value
  168. @property
  169. def features_dict(self) -> dict[str, Any]:
  170. return json.loads(self.features) if self.features else {}
  171. def user_input_form(self, to_old_structure: bool = False) -> list:
  172. # get start node from graph
  173. if not self.graph:
  174. return []
  175. graph_dict = self.graph_dict
  176. if "nodes" not in graph_dict:
  177. return []
  178. start_node = next((node for node in graph_dict["nodes"] if node["data"]["type"] == "start"), None)
  179. if not start_node:
  180. return []
  181. # get user_input_form from start node
  182. variables: list[Any] = start_node.get("data", {}).get("variables", [])
  183. if to_old_structure:
  184. old_structure_variables = []
  185. for variable in variables:
  186. old_structure_variables.append({variable["type"]: variable})
  187. return old_structure_variables
  188. return variables
  189. @property
  190. def unique_hash(self) -> str:
  191. """
  192. Get hash of workflow.
  193. :return: hash
  194. """
  195. entity = {"graph": self.graph_dict, "features": self.features_dict}
  196. return helper.generate_text_hash(json.dumps(entity, sort_keys=True))
  197. @property
  198. def tool_published(self) -> bool:
  199. """
  200. DEPRECATED: This property is not accurate for determining if a workflow is published as a tool.
  201. It only checks if there's a WorkflowToolProvider for the app, not if this specific workflow version
  202. is the one being used by the tool.
  203. For accurate checking, use a direct query with tenant_id, app_id, and version.
  204. """
  205. from models.tools import WorkflowToolProvider
  206. return (
  207. db.session.query(WorkflowToolProvider)
  208. .filter(WorkflowToolProvider.tenant_id == self.tenant_id, WorkflowToolProvider.app_id == self.app_id)
  209. .count()
  210. > 0
  211. )
  212. @property
  213. def environment_variables(self) -> Sequence[Variable]:
  214. # TODO: find some way to init `self._environment_variables` when instance created.
  215. if self._environment_variables is None:
  216. self._environment_variables = "{}"
  217. tenant_id = contexts.tenant_id.get()
  218. environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables)
  219. results = [
  220. variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values()
  221. ]
  222. # decrypt secret variables value
  223. def decrypt_func(var):
  224. if isinstance(var, SecretVariable):
  225. return var.model_copy(update={"value": encrypter.decrypt_token(tenant_id=tenant_id, token=var.value)})
  226. else:
  227. return var
  228. results = list(map(decrypt_func, results))
  229. return results
  230. @environment_variables.setter
  231. def environment_variables(self, value: Sequence[Variable]):
  232. if not value:
  233. self._environment_variables = "{}"
  234. return
  235. tenant_id = contexts.tenant_id.get()
  236. value = list(value)
  237. if any(var for var in value if not var.id):
  238. raise ValueError("environment variable require a unique id")
  239. # Compare inputs and origin variables,
  240. # if the value is HIDDEN_VALUE, use the origin variable value (only update `name`).
  241. origin_variables_dictionary = {var.id: var for var in self.environment_variables}
  242. for i, variable in enumerate(value):
  243. if variable.id in origin_variables_dictionary and variable.value == HIDDEN_VALUE:
  244. value[i] = origin_variables_dictionary[variable.id].model_copy(update={"name": variable.name})
  245. # encrypt secret variables value
  246. def encrypt_func(var):
  247. if isinstance(var, SecretVariable):
  248. return var.model_copy(update={"value": encrypter.encrypt_token(tenant_id=tenant_id, token=var.value)})
  249. else:
  250. return var
  251. encrypted_vars = list(map(encrypt_func, value))
  252. environment_variables_json = json.dumps(
  253. {var.name: var.model_dump() for var in encrypted_vars},
  254. ensure_ascii=False,
  255. )
  256. self._environment_variables = environment_variables_json
  257. def to_dict(self, *, include_secret: bool = False) -> Mapping[str, Any]:
  258. environment_variables = list(self.environment_variables)
  259. environment_variables = [
  260. v if not isinstance(v, SecretVariable) or include_secret else v.model_copy(update={"value": ""})
  261. for v in environment_variables
  262. ]
  263. result = {
  264. "graph": self.graph_dict,
  265. "features": self.features_dict,
  266. "environment_variables": [var.model_dump(mode="json") for var in environment_variables],
  267. "conversation_variables": [var.model_dump(mode="json") for var in self.conversation_variables],
  268. }
  269. return result
  270. @property
  271. def conversation_variables(self) -> Sequence[Variable]:
  272. # TODO: find some way to init `self._conversation_variables` when instance created.
  273. if self._conversation_variables is None:
  274. self._conversation_variables = "{}"
  275. variables_dict: dict[str, Any] = json.loads(self._conversation_variables)
  276. results = [variable_factory.build_conversation_variable_from_mapping(v) for v in variables_dict.values()]
  277. return results
  278. @conversation_variables.setter
  279. def conversation_variables(self, value: Sequence[Variable]) -> None:
  280. self._conversation_variables = json.dumps(
  281. {var.name: var.model_dump() for var in value},
  282. ensure_ascii=False,
  283. )
  284. class WorkflowRunStatus(StrEnum):
  285. """
  286. Workflow Run Status Enum
  287. """
  288. RUNNING = "running"
  289. SUCCEEDED = "succeeded"
  290. FAILED = "failed"
  291. STOPPED = "stopped"
  292. PARTIAL_SUCCEEDED = "partial-succeeded"
  293. class WorkflowRun(Base):
  294. """
  295. Workflow Run
  296. Attributes:
  297. - id (uuid) Run ID
  298. - tenant_id (uuid) Workspace ID
  299. - app_id (uuid) App ID
  300. - sequence_number (int) Auto-increment sequence number, incremented within the App, starting from 1
  301. - workflow_id (uuid) Workflow ID
  302. - type (string) Workflow type
  303. - triggered_from (string) Trigger source
  304. `debugging` for canvas debugging
  305. `app-run` for (published) app execution
  306. - version (string) Version
  307. - graph (text) Workflow canvas configuration (JSON)
  308. - inputs (text) Input parameters
  309. - status (string) Execution status, `running` / `succeeded` / `failed` / `stopped`
  310. - outputs (text) `optional` Output content
  311. - error (string) `optional` Error reason
  312. - elapsed_time (float) `optional` Time consumption (s)
  313. - total_tokens (int) `optional` Total tokens used
  314. - total_steps (int) Total steps (redundant), default 0
  315. - created_by_role (string) Creator role
  316. - `account` Console account
  317. - `end_user` End user
  318. - created_by (uuid) Runner ID
  319. - created_at (timestamp) Run time
  320. - finished_at (timestamp) End time
  321. """
  322. __tablename__ = "workflow_runs"
  323. __table_args__ = (
  324. db.PrimaryKeyConstraint("id", name="workflow_run_pkey"),
  325. db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"),
  326. db.Index("workflow_run_tenant_app_sequence_idx", "tenant_id", "app_id", "sequence_number"),
  327. )
  328. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  329. tenant_id: Mapped[str] = mapped_column(StringUUID)
  330. app_id: Mapped[str] = mapped_column(StringUUID)
  331. sequence_number: Mapped[int] = mapped_column()
  332. workflow_id: Mapped[str] = mapped_column(StringUUID)
  333. type: Mapped[str] = mapped_column(db.String(255))
  334. triggered_from: Mapped[str] = mapped_column(db.String(255))
  335. version: Mapped[str] = mapped_column(db.String(255))
  336. graph: Mapped[Optional[str]] = mapped_column(db.Text)
  337. inputs: Mapped[Optional[str]] = mapped_column(db.Text)
  338. status: Mapped[str] = mapped_column(db.String(255)) # running, succeeded, failed, stopped, partial-succeeded
  339. outputs: Mapped[Optional[str]] = mapped_column(sa.Text, default="{}")
  340. error: Mapped[Optional[str]] = mapped_column(db.Text)
  341. elapsed_time = db.Column(db.Float, nullable=False, server_default=sa.text("0"))
  342. total_tokens: Mapped[int] = mapped_column(sa.BigInteger, server_default=sa.text("0"))
  343. total_steps = db.Column(db.Integer, server_default=db.text("0"))
  344. created_by_role: Mapped[str] = mapped_column(db.String(255)) # account, end_user
  345. created_by = db.Column(StringUUID, nullable=False)
  346. created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  347. finished_at = db.Column(db.DateTime)
  348. exceptions_count = db.Column(db.Integer, server_default=db.text("0"))
  349. @property
  350. def created_by_account(self):
  351. created_by_role = CreatedByRole(self.created_by_role)
  352. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  353. @property
  354. def created_by_end_user(self):
  355. from models.model import EndUser
  356. created_by_role = CreatedByRole(self.created_by_role)
  357. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  358. @property
  359. def graph_dict(self):
  360. return json.loads(self.graph) if self.graph else {}
  361. @property
  362. def inputs_dict(self) -> Mapping[str, Any]:
  363. return json.loads(self.inputs) if self.inputs else {}
  364. @property
  365. def outputs_dict(self) -> Mapping[str, Any]:
  366. return json.loads(self.outputs) if self.outputs else {}
  367. @property
  368. def message(self):
  369. from models.model import Message
  370. return (
  371. db.session.query(Message).filter(Message.app_id == self.app_id, Message.workflow_run_id == self.id).first()
  372. )
  373. @property
  374. def workflow(self):
  375. return db.session.query(Workflow).filter(Workflow.id == self.workflow_id).first()
  376. def to_dict(self):
  377. return {
  378. "id": self.id,
  379. "tenant_id": self.tenant_id,
  380. "app_id": self.app_id,
  381. "sequence_number": self.sequence_number,
  382. "workflow_id": self.workflow_id,
  383. "type": self.type,
  384. "triggered_from": self.triggered_from,
  385. "version": self.version,
  386. "graph": self.graph_dict,
  387. "inputs": self.inputs_dict,
  388. "status": self.status,
  389. "outputs": self.outputs_dict,
  390. "error": self.error,
  391. "elapsed_time": self.elapsed_time,
  392. "total_tokens": self.total_tokens,
  393. "total_steps": self.total_steps,
  394. "created_by_role": self.created_by_role,
  395. "created_by": self.created_by,
  396. "created_at": self.created_at,
  397. "finished_at": self.finished_at,
  398. "exceptions_count": self.exceptions_count,
  399. }
  400. @classmethod
  401. def from_dict(cls, data: dict) -> "WorkflowRun":
  402. return cls(
  403. id=data.get("id"),
  404. tenant_id=data.get("tenant_id"),
  405. app_id=data.get("app_id"),
  406. sequence_number=data.get("sequence_number"),
  407. workflow_id=data.get("workflow_id"),
  408. type=data.get("type"),
  409. triggered_from=data.get("triggered_from"),
  410. version=data.get("version"),
  411. graph=json.dumps(data.get("graph")),
  412. inputs=json.dumps(data.get("inputs")),
  413. status=data.get("status"),
  414. outputs=json.dumps(data.get("outputs")),
  415. error=data.get("error"),
  416. elapsed_time=data.get("elapsed_time"),
  417. total_tokens=data.get("total_tokens"),
  418. total_steps=data.get("total_steps"),
  419. created_by_role=data.get("created_by_role"),
  420. created_by=data.get("created_by"),
  421. created_at=data.get("created_at"),
  422. finished_at=data.get("finished_at"),
  423. exceptions_count=data.get("exceptions_count"),
  424. )
  425. class WorkflowNodeExecutionTriggeredFrom(StrEnum):
  426. """
  427. Workflow Node Execution Triggered From Enum
  428. """
  429. SINGLE_STEP = "single-step"
  430. WORKFLOW_RUN = "workflow-run"
  431. class WorkflowNodeExecutionStatus(StrEnum):
  432. """
  433. Workflow Node Execution Status Enum
  434. """
  435. RUNNING = "running"
  436. SUCCEEDED = "succeeded"
  437. FAILED = "failed"
  438. EXCEPTION = "exception"
  439. RETRY = "retry"
  440. class WorkflowNodeExecution(Base):
  441. """
  442. Workflow Node Execution
  443. - id (uuid) Execution ID
  444. - tenant_id (uuid) Workspace ID
  445. - app_id (uuid) App ID
  446. - workflow_id (uuid) Workflow ID
  447. - triggered_from (string) Trigger source
  448. `single-step` for single-step debugging
  449. `workflow-run` for workflow execution (debugging / user execution)
  450. - workflow_run_id (uuid) `optional` Workflow run ID
  451. Null for single-step debugging.
  452. - index (int) Execution sequence number, used for displaying Tracing Node order
  453. - predecessor_node_id (string) `optional` Predecessor node ID, used for displaying execution path
  454. - node_id (string) Node ID
  455. - node_type (string) Node type, such as `start`
  456. - title (string) Node title
  457. - inputs (json) All predecessor node variable content used in the node
  458. - process_data (json) Node process data
  459. - outputs (json) `optional` Node output variables
  460. - status (string) Execution status, `running` / `succeeded` / `failed`
  461. - error (string) `optional` Error reason
  462. - elapsed_time (float) `optional` Time consumption (s)
  463. - execution_metadata (text) Metadata
  464. - total_tokens (int) `optional` Total tokens used
  465. - total_price (decimal) `optional` Total cost
  466. - currency (string) `optional` Currency, such as USD / RMB
  467. - created_at (timestamp) Run time
  468. - created_by_role (string) Creator role
  469. - `account` Console account
  470. - `end_user` End user
  471. - created_by (uuid) Runner ID
  472. - finished_at (timestamp) End time
  473. """
  474. __tablename__ = "workflow_node_executions"
  475. __table_args__ = (
  476. db.PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"),
  477. db.Index(
  478. "workflow_node_execution_workflow_run_idx",
  479. "tenant_id",
  480. "app_id",
  481. "workflow_id",
  482. "triggered_from",
  483. "workflow_run_id",
  484. ),
  485. db.Index(
  486. "workflow_node_execution_node_run_idx", "tenant_id", "app_id", "workflow_id", "triggered_from", "node_id"
  487. ),
  488. db.Index(
  489. "workflow_node_execution_id_idx",
  490. "tenant_id",
  491. "app_id",
  492. "workflow_id",
  493. "triggered_from",
  494. "node_execution_id",
  495. ),
  496. )
  497. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  498. tenant_id: Mapped[str] = mapped_column(StringUUID)
  499. app_id: Mapped[str] = mapped_column(StringUUID)
  500. workflow_id: Mapped[str] = mapped_column(StringUUID)
  501. triggered_from: Mapped[str] = mapped_column(db.String(255))
  502. workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID)
  503. index: Mapped[int] = mapped_column(db.Integer)
  504. predecessor_node_id: Mapped[Optional[str]] = mapped_column(db.String(255))
  505. node_execution_id: Mapped[Optional[str]] = mapped_column(db.String(255))
  506. node_id: Mapped[str] = mapped_column(db.String(255))
  507. node_type: Mapped[str] = mapped_column(db.String(255))
  508. title: Mapped[str] = mapped_column(db.String(255))
  509. inputs: Mapped[Optional[str]] = mapped_column(db.Text)
  510. process_data: Mapped[Optional[str]] = mapped_column(db.Text)
  511. outputs: Mapped[Optional[str]] = mapped_column(db.Text)
  512. status: Mapped[str] = mapped_column(db.String(255))
  513. error: Mapped[Optional[str]] = mapped_column(db.Text)
  514. elapsed_time: Mapped[float] = mapped_column(db.Float, server_default=db.text("0"))
  515. execution_metadata: Mapped[Optional[str]] = mapped_column(db.Text)
  516. created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
  517. created_by_role: Mapped[str] = mapped_column(db.String(255))
  518. created_by: Mapped[str] = mapped_column(StringUUID)
  519. finished_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
  520. @property
  521. def created_by_account(self):
  522. created_by_role = CreatedByRole(self.created_by_role)
  523. # TODO(-LAN-): Avoid using db.session.get() here.
  524. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  525. @property
  526. def created_by_end_user(self):
  527. from models.model import EndUser
  528. created_by_role = CreatedByRole(self.created_by_role)
  529. # TODO(-LAN-): Avoid using db.session.get() here.
  530. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  531. @property
  532. def inputs_dict(self):
  533. return json.loads(self.inputs) if self.inputs else None
  534. @property
  535. def outputs_dict(self):
  536. return json.loads(self.outputs) if self.outputs else None
  537. @property
  538. def process_data_dict(self):
  539. return json.loads(self.process_data) if self.process_data else None
  540. @property
  541. def execution_metadata_dict(self):
  542. return json.loads(self.execution_metadata) if self.execution_metadata else None
  543. @property
  544. def extras(self):
  545. from core.tools.tool_manager import ToolManager
  546. extras = {}
  547. if self.execution_metadata_dict:
  548. from core.workflow.nodes import NodeType
  549. if self.node_type == NodeType.TOOL.value and "tool_info" in self.execution_metadata_dict:
  550. tool_info = self.execution_metadata_dict["tool_info"]
  551. extras["icon"] = ToolManager.get_tool_icon(
  552. tenant_id=self.tenant_id,
  553. provider_type=tool_info["provider_type"],
  554. provider_id=tool_info["provider_id"],
  555. )
  556. return extras
  557. class WorkflowAppLogCreatedFrom(Enum):
  558. """
  559. Workflow App Log Created From Enum
  560. """
  561. SERVICE_API = "service-api"
  562. WEB_APP = "web-app"
  563. INSTALLED_APP = "installed-app"
  564. @classmethod
  565. def value_of(cls, value: str) -> "WorkflowAppLogCreatedFrom":
  566. """
  567. Get value of given mode.
  568. :param value: mode value
  569. :return: mode
  570. """
  571. for mode in cls:
  572. if mode.value == value:
  573. return mode
  574. raise ValueError(f"invalid workflow app log created from value {value}")
  575. class WorkflowAppLog(Base):
  576. """
  577. Workflow App execution log, excluding workflow debugging records.
  578. Attributes:
  579. - id (uuid) run ID
  580. - tenant_id (uuid) Workspace ID
  581. - app_id (uuid) App ID
  582. - workflow_id (uuid) Associated Workflow ID
  583. - workflow_run_id (uuid) Associated Workflow Run ID
  584. - created_from (string) Creation source
  585. `service-api` App Execution OpenAPI
  586. `web-app` WebApp
  587. `installed-app` Installed App
  588. - created_by_role (string) Creator role
  589. - `account` Console account
  590. - `end_user` End user
  591. - created_by (uuid) Creator ID, depends on the user table according to created_by_role
  592. - created_at (timestamp) Creation time
  593. """
  594. __tablename__ = "workflow_app_logs"
  595. __table_args__ = (
  596. db.PrimaryKeyConstraint("id", name="workflow_app_log_pkey"),
  597. db.Index("workflow_app_log_app_idx", "tenant_id", "app_id", "created_at"),
  598. db.Index("workflow_app_log_workflow_run_idx", "workflow_run_id"),
  599. )
  600. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  601. tenant_id: Mapped[str] = mapped_column(StringUUID)
  602. app_id: Mapped[str] = mapped_column(StringUUID)
  603. workflow_id = db.Column(StringUUID, nullable=False)
  604. workflow_run_id: Mapped[str] = mapped_column(StringUUID)
  605. created_from = db.Column(db.String(255), nullable=False)
  606. created_by_role = db.Column(db.String(255), nullable=False)
  607. created_by = db.Column(StringUUID, nullable=False)
  608. created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  609. @property
  610. def workflow_run(self):
  611. return db.session.get(WorkflowRun, self.workflow_run_id)
  612. @property
  613. def created_by_account(self):
  614. created_by_role = CreatedByRole(self.created_by_role)
  615. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  616. @property
  617. def created_by_end_user(self):
  618. from models.model import EndUser
  619. created_by_role = CreatedByRole(self.created_by_role)
  620. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  621. class ConversationVariable(Base):
  622. __tablename__ = "workflow_conversation_variables"
  623. __table_args__ = (
  624. PrimaryKeyConstraint("id", "conversation_id", name="workflow_conversation_variables_pkey"),
  625. Index("workflow__conversation_variables_app_id_idx", "app_id"),
  626. Index("workflow__conversation_variables_created_at_idx", "created_at"),
  627. )
  628. id: Mapped[str] = mapped_column(StringUUID, primary_key=True)
  629. conversation_id: Mapped[str] = mapped_column(StringUUID, nullable=False, primary_key=True)
  630. app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  631. data = mapped_column(db.Text, nullable=False)
  632. created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  633. updated_at = mapped_column(
  634. db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
  635. )
  636. def __init__(self, *, id: str, app_id: str, conversation_id: str, data: str) -> None:
  637. self.id = id
  638. self.app_id = app_id
  639. self.conversation_id = conversation_id
  640. self.data = data
  641. @classmethod
  642. def from_variable(cls, *, app_id: str, conversation_id: str, variable: Variable) -> "ConversationVariable":
  643. obj = cls(
  644. id=variable.id,
  645. app_id=app_id,
  646. conversation_id=conversation_id,
  647. data=variable.model_dump_json(),
  648. )
  649. return obj
  650. def to_variable(self) -> Variable:
  651. mapping = json.loads(self.data)
  652. return variable_factory.build_conversation_variable_from_mapping(mapping)