workflow.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  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 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(
  85. sa.DateTime, nullable=False, default=datetime.now(tz=UTC), server_onupdate=func.current_timestamp()
  86. )
  87. _environment_variables: Mapped[str] = mapped_column(
  88. "environment_variables", db.Text, nullable=False, server_default="{}"
  89. )
  90. _conversation_variables: Mapped[str] = mapped_column(
  91. "conversation_variables", db.Text, nullable=False, server_default="{}"
  92. )
  93. def __init__(
  94. self,
  95. *,
  96. tenant_id: str,
  97. app_id: str,
  98. type: str,
  99. version: str,
  100. graph: str,
  101. features: str,
  102. created_by: str,
  103. environment_variables: Sequence[Variable],
  104. conversation_variables: Sequence[Variable],
  105. ):
  106. self.tenant_id = tenant_id
  107. self.app_id = app_id
  108. self.type = type
  109. self.version = version
  110. self.graph = graph
  111. self.features = features
  112. self.created_by = created_by
  113. self.environment_variables = environment_variables or []
  114. self.conversation_variables = conversation_variables or []
  115. @property
  116. def created_by_account(self):
  117. return db.session.get(Account, self.created_by)
  118. @property
  119. def updated_by_account(self):
  120. return db.session.get(Account, self.updated_by) if self.updated_by else None
  121. @property
  122. def graph_dict(self) -> Mapping[str, Any]:
  123. return json.loads(self.graph) if self.graph else {}
  124. @property
  125. def features(self) -> str:
  126. """
  127. Convert old features structure to new features structure.
  128. """
  129. if not self._features:
  130. return self._features
  131. features = json.loads(self._features)
  132. if features.get("file_upload", {}).get("image", {}).get("enabled", False):
  133. image_enabled = True
  134. image_number_limits = int(features["file_upload"]["image"].get("number_limits", 1))
  135. image_transfer_methods = features["file_upload"]["image"].get(
  136. "transfer_methods", ["remote_url", "local_file"]
  137. )
  138. features["file_upload"]["enabled"] = image_enabled
  139. features["file_upload"]["number_limits"] = image_number_limits
  140. features["file_upload"]["allowed_file_upload_methods"] = image_transfer_methods
  141. features["file_upload"]["allowed_file_types"] = ["image"]
  142. features["file_upload"]["allowed_file_extensions"] = []
  143. del features["file_upload"]["image"]
  144. self._features = json.dumps(features)
  145. return self._features
  146. @features.setter
  147. def features(self, value: str) -> None:
  148. self._features = value
  149. @property
  150. def features_dict(self) -> Mapping[str, Any]:
  151. return json.loads(self.features) if self.features else {}
  152. def user_input_form(self, to_old_structure: bool = False) -> list:
  153. # get start node from graph
  154. if not self.graph:
  155. return []
  156. graph_dict = self.graph_dict
  157. if "nodes" not in graph_dict:
  158. return []
  159. start_node = next((node for node in graph_dict["nodes"] if node["data"]["type"] == "start"), None)
  160. if not start_node:
  161. return []
  162. # get user_input_form from start node
  163. variables = start_node.get("data", {}).get("variables", [])
  164. if to_old_structure:
  165. old_structure_variables = []
  166. for variable in variables:
  167. old_structure_variables.append({variable["type"]: variable})
  168. return old_structure_variables
  169. return variables
  170. @property
  171. def unique_hash(self) -> str:
  172. """
  173. Get hash of workflow.
  174. :return: hash
  175. """
  176. entity = {"graph": self.graph_dict, "features": self.features_dict}
  177. return helper.generate_text_hash(json.dumps(entity, sort_keys=True))
  178. @property
  179. def tool_published(self) -> bool:
  180. from models.tools import WorkflowToolProvider
  181. return (
  182. db.session.query(WorkflowToolProvider).filter(WorkflowToolProvider.app_id == self.app_id).first()
  183. is not None
  184. )
  185. @property
  186. def environment_variables(self) -> Sequence[Variable]:
  187. # TODO: find some way to init `self._environment_variables` when instance created.
  188. if self._environment_variables is None:
  189. self._environment_variables = "{}"
  190. tenant_id = contexts.tenant_id.get()
  191. environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables)
  192. results = [
  193. variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values()
  194. ]
  195. # decrypt secret variables value
  196. decrypt_func = (
  197. lambda var: var.model_copy(update={"value": encrypter.decrypt_token(tenant_id=tenant_id, token=var.value)})
  198. if isinstance(var, SecretVariable)
  199. else var
  200. )
  201. results = list(map(decrypt_func, results))
  202. return results
  203. @environment_variables.setter
  204. def environment_variables(self, value: Sequence[Variable]):
  205. if not value:
  206. self._environment_variables = "{}"
  207. return
  208. tenant_id = contexts.tenant_id.get()
  209. value = list(value)
  210. if any(var for var in value if not var.id):
  211. raise ValueError("environment variable require a unique id")
  212. # Compare inputs and origin variables,
  213. # if the value is HIDDEN_VALUE, use the origin variable value (only update `name`).
  214. origin_variables_dictionary = {var.id: var for var in self.environment_variables}
  215. for i, variable in enumerate(value):
  216. if variable.id in origin_variables_dictionary and variable.value == HIDDEN_VALUE:
  217. value[i] = origin_variables_dictionary[variable.id].model_copy(update={"name": variable.name})
  218. # encrypt secret variables value
  219. encrypt_func = (
  220. lambda var: var.model_copy(update={"value": encrypter.encrypt_token(tenant_id=tenant_id, token=var.value)})
  221. if isinstance(var, SecretVariable)
  222. else var
  223. )
  224. encrypted_vars = list(map(encrypt_func, value))
  225. environment_variables_json = json.dumps(
  226. {var.name: var.model_dump() for var in encrypted_vars},
  227. ensure_ascii=False,
  228. )
  229. self._environment_variables = environment_variables_json
  230. def to_dict(self, *, include_secret: bool = False) -> Mapping[str, Any]:
  231. environment_variables = list(self.environment_variables)
  232. environment_variables = [
  233. v if not isinstance(v, SecretVariable) or include_secret else v.model_copy(update={"value": ""})
  234. for v in environment_variables
  235. ]
  236. result = {
  237. "graph": self.graph_dict,
  238. "features": self.features_dict,
  239. "environment_variables": [var.model_dump(mode="json") for var in environment_variables],
  240. "conversation_variables": [var.model_dump(mode="json") for var in self.conversation_variables],
  241. }
  242. return result
  243. @property
  244. def conversation_variables(self) -> Sequence[Variable]:
  245. # TODO: find some way to init `self._conversation_variables` when instance created.
  246. if self._conversation_variables is None:
  247. self._conversation_variables = "{}"
  248. variables_dict: dict[str, Any] = json.loads(self._conversation_variables)
  249. results = [variable_factory.build_conversation_variable_from_mapping(v) for v in variables_dict.values()]
  250. return results
  251. @conversation_variables.setter
  252. def conversation_variables(self, value: Sequence[Variable]) -> None:
  253. self._conversation_variables = json.dumps(
  254. {var.name: var.model_dump() for var in value},
  255. ensure_ascii=False,
  256. )
  257. class WorkflowRunStatus(StrEnum):
  258. """
  259. Workflow Run Status Enum
  260. """
  261. RUNNING = "running"
  262. SUCCEEDED = "succeeded"
  263. FAILED = "failed"
  264. STOPPED = "stopped"
  265. @classmethod
  266. def value_of(cls, value: str) -> "WorkflowRunStatus":
  267. """
  268. Get value of given mode.
  269. :param value: mode value
  270. :return: mode
  271. """
  272. for mode in cls:
  273. if mode.value == value:
  274. return mode
  275. raise ValueError(f"invalid workflow run status value {value}")
  276. class WorkflowRun(db.Model):
  277. """
  278. Workflow Run
  279. Attributes:
  280. - id (uuid) Run ID
  281. - tenant_id (uuid) Workspace ID
  282. - app_id (uuid) App ID
  283. - sequence_number (int) Auto-increment sequence number, incremented within the App, starting from 1
  284. - workflow_id (uuid) Workflow ID
  285. - type (string) Workflow type
  286. - triggered_from (string) Trigger source
  287. `debugging` for canvas debugging
  288. `app-run` for (published) app execution
  289. - version (string) Version
  290. - graph (text) Workflow canvas configuration (JSON)
  291. - inputs (text) Input parameters
  292. - status (string) Execution status, `running` / `succeeded` / `failed` / `stopped`
  293. - outputs (text) `optional` Output content
  294. - error (string) `optional` Error reason
  295. - elapsed_time (float) `optional` Time consumption (s)
  296. - total_tokens (int) `optional` Total tokens used
  297. - total_steps (int) Total steps (redundant), default 0
  298. - created_by_role (string) Creator role
  299. - `account` Console account
  300. - `end_user` End user
  301. - created_by (uuid) Runner ID
  302. - created_at (timestamp) Run time
  303. - finished_at (timestamp) End time
  304. """
  305. __tablename__ = "workflow_runs"
  306. __table_args__ = (
  307. db.PrimaryKeyConstraint("id", name="workflow_run_pkey"),
  308. db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"),
  309. db.Index("workflow_run_tenant_app_sequence_idx", "tenant_id", "app_id", "sequence_number"),
  310. )
  311. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  312. tenant_id = db.Column(StringUUID, nullable=False)
  313. app_id = db.Column(StringUUID, nullable=False)
  314. sequence_number = db.Column(db.Integer, nullable=False)
  315. workflow_id = db.Column(StringUUID, nullable=False)
  316. type = db.Column(db.String(255), nullable=False)
  317. triggered_from = db.Column(db.String(255), nullable=False)
  318. version = db.Column(db.String(255), nullable=False)
  319. graph = db.Column(db.Text)
  320. inputs = db.Column(db.Text)
  321. status = db.Column(db.String(255), nullable=False) # running, succeeded, failed, stopped
  322. outputs: Mapped[str] = mapped_column(sa.Text, default="{}")
  323. error = db.Column(db.Text)
  324. elapsed_time = db.Column(db.Float, nullable=False, server_default=db.text("0"))
  325. total_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  326. total_steps = db.Column(db.Integer, server_default=db.text("0"))
  327. created_by_role = db.Column(db.String(255), nullable=False) # account, end_user
  328. created_by = db.Column(StringUUID, nullable=False)
  329. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  330. finished_at = db.Column(db.DateTime)
  331. @property
  332. def created_by_account(self):
  333. created_by_role = CreatedByRole(self.created_by_role)
  334. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  335. @property
  336. def created_by_end_user(self):
  337. from models.model import EndUser
  338. created_by_role = CreatedByRole(self.created_by_role)
  339. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  340. @property
  341. def graph_dict(self):
  342. return json.loads(self.graph) if self.graph else {}
  343. @property
  344. def inputs_dict(self) -> Mapping[str, Any]:
  345. return json.loads(self.inputs) if self.inputs else {}
  346. @property
  347. def outputs_dict(self) -> Mapping[str, Any]:
  348. return json.loads(self.outputs) if self.outputs else {}
  349. @property
  350. def message(self) -> Optional["Message"]:
  351. from models.model import Message
  352. return (
  353. db.session.query(Message).filter(Message.app_id == self.app_id, Message.workflow_run_id == self.id).first()
  354. )
  355. @property
  356. def workflow(self):
  357. return db.session.query(Workflow).filter(Workflow.id == self.workflow_id).first()
  358. def to_dict(self):
  359. return {
  360. "id": self.id,
  361. "tenant_id": self.tenant_id,
  362. "app_id": self.app_id,
  363. "sequence_number": self.sequence_number,
  364. "workflow_id": self.workflow_id,
  365. "type": self.type,
  366. "triggered_from": self.triggered_from,
  367. "version": self.version,
  368. "graph": self.graph_dict,
  369. "inputs": self.inputs_dict,
  370. "status": self.status,
  371. "outputs": self.outputs_dict,
  372. "error": self.error,
  373. "elapsed_time": self.elapsed_time,
  374. "total_tokens": self.total_tokens,
  375. "total_steps": self.total_steps,
  376. "created_by_role": self.created_by_role,
  377. "created_by": self.created_by,
  378. "created_at": self.created_at,
  379. "finished_at": self.finished_at,
  380. }
  381. @classmethod
  382. def from_dict(cls, data: dict) -> "WorkflowRun":
  383. return cls(
  384. id=data.get("id"),
  385. tenant_id=data.get("tenant_id"),
  386. app_id=data.get("app_id"),
  387. sequence_number=data.get("sequence_number"),
  388. workflow_id=data.get("workflow_id"),
  389. type=data.get("type"),
  390. triggered_from=data.get("triggered_from"),
  391. version=data.get("version"),
  392. graph=json.dumps(data.get("graph")),
  393. inputs=json.dumps(data.get("inputs")),
  394. status=data.get("status"),
  395. outputs=json.dumps(data.get("outputs")),
  396. error=data.get("error"),
  397. elapsed_time=data.get("elapsed_time"),
  398. total_tokens=data.get("total_tokens"),
  399. total_steps=data.get("total_steps"),
  400. created_by_role=data.get("created_by_role"),
  401. created_by=data.get("created_by"),
  402. created_at=data.get("created_at"),
  403. finished_at=data.get("finished_at"),
  404. )
  405. class WorkflowNodeExecutionTriggeredFrom(Enum):
  406. """
  407. Workflow Node Execution Triggered From Enum
  408. """
  409. SINGLE_STEP = "single-step"
  410. WORKFLOW_RUN = "workflow-run"
  411. @classmethod
  412. def value_of(cls, value: str) -> "WorkflowNodeExecutionTriggeredFrom":
  413. """
  414. Get value of given mode.
  415. :param value: mode value
  416. :return: mode
  417. """
  418. for mode in cls:
  419. if mode.value == value:
  420. return mode
  421. raise ValueError(f"invalid workflow node execution triggered from value {value}")
  422. class WorkflowNodeExecutionStatus(Enum):
  423. """
  424. Workflow Node Execution Status Enum
  425. """
  426. RUNNING = "running"
  427. SUCCEEDED = "succeeded"
  428. FAILED = "failed"
  429. @classmethod
  430. def value_of(cls, value: str) -> "WorkflowNodeExecutionStatus":
  431. """
  432. Get value of given mode.
  433. :param value: mode value
  434. :return: mode
  435. """
  436. for mode in cls:
  437. if mode.value == value:
  438. return mode
  439. raise ValueError(f"invalid workflow node execution status value {value}")
  440. class WorkflowNodeExecution(db.Model):
  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 = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  498. tenant_id = db.Column(StringUUID, nullable=False)
  499. app_id = db.Column(StringUUID, nullable=False)
  500. workflow_id = db.Column(StringUUID, nullable=False)
  501. triggered_from = db.Column(db.String(255), nullable=False)
  502. workflow_run_id = db.Column(StringUUID)
  503. index = db.Column(db.Integer, nullable=False)
  504. predecessor_node_id = db.Column(db.String(255))
  505. node_execution_id = db.Column(db.String(255), nullable=True)
  506. node_id = db.Column(db.String(255), nullable=False)
  507. node_type = db.Column(db.String(255), nullable=False)
  508. title = db.Column(db.String(255), nullable=False)
  509. inputs = db.Column(db.Text)
  510. process_data = db.Column(db.Text)
  511. outputs = db.Column(db.Text)
  512. status = db.Column(db.String(255), nullable=False)
  513. error = db.Column(db.Text)
  514. elapsed_time = db.Column(db.Float, nullable=False, server_default=db.text("0"))
  515. execution_metadata = db.Column(db.Text)
  516. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  517. created_by_role = db.Column(db.String(255), nullable=False)
  518. created_by = db.Column(StringUUID, nullable=False)
  519. finished_at = db.Column(db.DateTime)
  520. @property
  521. def created_by_account(self):
  522. created_by_role = CreatedByRole(self.created_by_role)
  523. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  524. @property
  525. def created_by_end_user(self):
  526. from models.model import EndUser
  527. created_by_role = CreatedByRole(self.created_by_role)
  528. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  529. @property
  530. def inputs_dict(self):
  531. return json.loads(self.inputs) if self.inputs else None
  532. @property
  533. def outputs_dict(self):
  534. return json.loads(self.outputs) if self.outputs else None
  535. @property
  536. def process_data_dict(self):
  537. return json.loads(self.process_data) if self.process_data else None
  538. @property
  539. def execution_metadata_dict(self):
  540. return json.loads(self.execution_metadata) if self.execution_metadata else None
  541. @property
  542. def extras(self):
  543. from core.tools.tool_manager import ToolManager
  544. extras = {}
  545. if self.execution_metadata_dict:
  546. from core.workflow.nodes import NodeType
  547. if self.node_type == NodeType.TOOL.value and "tool_info" in self.execution_metadata_dict:
  548. tool_info = self.execution_metadata_dict["tool_info"]
  549. extras["icon"] = ToolManager.get_tool_icon(
  550. tenant_id=self.tenant_id,
  551. provider_type=tool_info["provider_type"],
  552. provider_id=tool_info["provider_id"],
  553. )
  554. return extras
  555. class WorkflowAppLogCreatedFrom(Enum):
  556. """
  557. Workflow App Log Created From Enum
  558. """
  559. SERVICE_API = "service-api"
  560. WEB_APP = "web-app"
  561. INSTALLED_APP = "installed-app"
  562. @classmethod
  563. def value_of(cls, value: str) -> "WorkflowAppLogCreatedFrom":
  564. """
  565. Get value of given mode.
  566. :param value: mode value
  567. :return: mode
  568. """
  569. for mode in cls:
  570. if mode.value == value:
  571. return mode
  572. raise ValueError(f"invalid workflow app log created from value {value}")
  573. class WorkflowAppLog(db.Model):
  574. """
  575. Workflow App execution log, excluding workflow debugging records.
  576. Attributes:
  577. - id (uuid) run ID
  578. - tenant_id (uuid) Workspace ID
  579. - app_id (uuid) App ID
  580. - workflow_id (uuid) Associated Workflow ID
  581. - workflow_run_id (uuid) Associated Workflow Run ID
  582. - created_from (string) Creation source
  583. `service-api` App Execution OpenAPI
  584. `web-app` WebApp
  585. `installed-app` Installed App
  586. - created_by_role (string) Creator role
  587. - `account` Console account
  588. - `end_user` End user
  589. - created_by (uuid) Creator ID, depends on the user table according to created_by_role
  590. - created_at (timestamp) Creation time
  591. """
  592. __tablename__ = "workflow_app_logs"
  593. __table_args__ = (
  594. db.PrimaryKeyConstraint("id", name="workflow_app_log_pkey"),
  595. db.Index("workflow_app_log_app_idx", "tenant_id", "app_id"),
  596. )
  597. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  598. tenant_id = db.Column(StringUUID, nullable=False)
  599. app_id = db.Column(StringUUID, nullable=False)
  600. workflow_id = db.Column(StringUUID, nullable=False)
  601. workflow_run_id = db.Column(StringUUID, nullable=False)
  602. created_from = db.Column(db.String(255), nullable=False)
  603. created_by_role = db.Column(db.String(255), nullable=False)
  604. created_by = db.Column(StringUUID, nullable=False)
  605. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  606. @property
  607. def workflow_run(self):
  608. return db.session.get(WorkflowRun, self.workflow_run_id)
  609. @property
  610. def created_by_account(self):
  611. created_by_role = CreatedByRole(self.created_by_role)
  612. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  613. @property
  614. def created_by_end_user(self):
  615. from models.model import EndUser
  616. created_by_role = CreatedByRole(self.created_by_role)
  617. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  618. class ConversationVariable(db.Model):
  619. __tablename__ = "workflow_conversation_variables"
  620. id: Mapped[str] = db.Column(StringUUID, primary_key=True)
  621. conversation_id: Mapped[str] = db.Column(StringUUID, nullable=False, primary_key=True)
  622. app_id: Mapped[str] = db.Column(StringUUID, nullable=False, index=True)
  623. data = db.Column(db.Text, nullable=False)
  624. created_at = db.Column(db.DateTime, nullable=False, index=True, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  625. updated_at = db.Column(
  626. db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
  627. )
  628. def __init__(self, *, id: str, app_id: str, conversation_id: str, data: str) -> None:
  629. self.id = id
  630. self.app_id = app_id
  631. self.conversation_id = conversation_id
  632. self.data = data
  633. @classmethod
  634. def from_variable(cls, *, app_id: str, conversation_id: str, variable: Variable) -> "ConversationVariable":
  635. obj = cls(
  636. id=variable.id,
  637. app_id=app_id,
  638. conversation_id=conversation_id,
  639. data=variable.model_dump_json(),
  640. )
  641. return obj
  642. def to_variable(self) -> Variable:
  643. mapping = json.loads(self.data)
  644. return variable_factory.build_conversation_variable_from_mapping(mapping)