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

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