workflow.py 27 KB

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