workflow.py 28 KB

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