workflow.py 27 KB

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