workflow.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. import json
  2. from collections.abc import Mapping, Sequence
  3. from datetime import 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, nullable=False, server_default=func.current_timestamp(), server_onupdate=func.current_timestamp()
  84. )
  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_file_upload_methods"] = image_transfer_methods
  139. features["file_upload"]["allowed_file_types"] = ["image"]
  140. features["file_upload"]["allowed_file_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)
  181. .filter(WorkflowToolProvider.tenant_id == self.tenant_id, WorkflowToolProvider.app_id == self.app_id)
  182. .count()
  183. > 0
  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. PARTIAL_SUCCESSED = "partial-succeeded"
  266. @classmethod
  267. def value_of(cls, value: str) -> "WorkflowRunStatus":
  268. """
  269. Get value of given mode.
  270. :param value: mode value
  271. :return: mode
  272. """
  273. for mode in cls:
  274. if mode.value == value:
  275. return mode
  276. raise ValueError(f"invalid workflow run status value {value}")
  277. class WorkflowRun(db.Model):
  278. """
  279. Workflow Run
  280. Attributes:
  281. - id (uuid) Run ID
  282. - tenant_id (uuid) Workspace ID
  283. - app_id (uuid) App ID
  284. - sequence_number (int) Auto-increment sequence number, incremented within the App, starting from 1
  285. - workflow_id (uuid) Workflow ID
  286. - type (string) Workflow type
  287. - triggered_from (string) Trigger source
  288. `debugging` for canvas debugging
  289. `app-run` for (published) app execution
  290. - version (string) Version
  291. - graph (text) Workflow canvas configuration (JSON)
  292. - inputs (text) Input parameters
  293. - status (string) Execution status, `running` / `succeeded` / `failed` / `stopped`
  294. - outputs (text) `optional` Output content
  295. - error (string) `optional` Error reason
  296. - elapsed_time (float) `optional` Time consumption (s)
  297. - total_tokens (int) `optional` Total tokens used
  298. - total_steps (int) Total steps (redundant), default 0
  299. - created_by_role (string) Creator role
  300. - `account` Console account
  301. - `end_user` End user
  302. - created_by (uuid) Runner ID
  303. - created_at (timestamp) Run time
  304. - finished_at (timestamp) End time
  305. """
  306. __tablename__ = "workflow_runs"
  307. __table_args__ = (
  308. db.PrimaryKeyConstraint("id", name="workflow_run_pkey"),
  309. db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"),
  310. db.Index("workflow_run_tenant_app_sequence_idx", "tenant_id", "app_id", "sequence_number"),
  311. )
  312. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  313. tenant_id = db.Column(StringUUID, nullable=False)
  314. app_id = db.Column(StringUUID, nullable=False)
  315. sequence_number = db.Column(db.Integer, nullable=False)
  316. workflow_id = db.Column(StringUUID, nullable=False)
  317. type = db.Column(db.String(255), nullable=False)
  318. triggered_from = db.Column(db.String(255), nullable=False)
  319. version = db.Column(db.String(255), nullable=False)
  320. graph = db.Column(db.Text)
  321. inputs = db.Column(db.Text)
  322. status = db.Column(db.String(255), nullable=False) # running, succeeded, failed, stopped, partial-succeeded
  323. outputs: Mapped[Optional[str]] = mapped_column(sa.Text, default="{}")
  324. error = db.Column(db.Text)
  325. elapsed_time = db.Column(db.Float, nullable=False, server_default=db.text("0"))
  326. total_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  327. total_steps = db.Column(db.Integer, server_default=db.text("0"))
  328. created_by_role = db.Column(db.String(255), nullable=False) # account, end_user
  329. created_by = db.Column(StringUUID, nullable=False)
  330. created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  331. finished_at = db.Column(db.DateTime)
  332. exceptions_count = db.Column(db.Integer, server_default=db.text("0"))
  333. @property
  334. def created_by_account(self):
  335. created_by_role = CreatedByRole(self.created_by_role)
  336. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  337. @property
  338. def created_by_end_user(self):
  339. from models.model import EndUser
  340. created_by_role = CreatedByRole(self.created_by_role)
  341. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  342. @property
  343. def graph_dict(self):
  344. return json.loads(self.graph) if self.graph else {}
  345. @property
  346. def inputs_dict(self) -> Mapping[str, Any]:
  347. return json.loads(self.inputs) if self.inputs else {}
  348. @property
  349. def outputs_dict(self) -> Mapping[str, Any]:
  350. return json.loads(self.outputs) if self.outputs else {}
  351. @property
  352. def message(self) -> Optional["Message"]:
  353. from models.model import Message
  354. return (
  355. db.session.query(Message).filter(Message.app_id == self.app_id, Message.workflow_run_id == self.id).first()
  356. )
  357. @property
  358. def workflow(self):
  359. return db.session.query(Workflow).filter(Workflow.id == self.workflow_id).first()
  360. def to_dict(self):
  361. return {
  362. "id": self.id,
  363. "tenant_id": self.tenant_id,
  364. "app_id": self.app_id,
  365. "sequence_number": self.sequence_number,
  366. "workflow_id": self.workflow_id,
  367. "type": self.type,
  368. "triggered_from": self.triggered_from,
  369. "version": self.version,
  370. "graph": self.graph_dict,
  371. "inputs": self.inputs_dict,
  372. "status": self.status,
  373. "outputs": self.outputs_dict,
  374. "error": self.error,
  375. "elapsed_time": self.elapsed_time,
  376. "total_tokens": self.total_tokens,
  377. "total_steps": self.total_steps,
  378. "created_by_role": self.created_by_role,
  379. "created_by": self.created_by,
  380. "created_at": self.created_at,
  381. "finished_at": self.finished_at,
  382. "exceptions_count": self.exceptions_count,
  383. }
  384. @classmethod
  385. def from_dict(cls, data: dict) -> "WorkflowRun":
  386. return cls(
  387. id=data.get("id"),
  388. tenant_id=data.get("tenant_id"),
  389. app_id=data.get("app_id"),
  390. sequence_number=data.get("sequence_number"),
  391. workflow_id=data.get("workflow_id"),
  392. type=data.get("type"),
  393. triggered_from=data.get("triggered_from"),
  394. version=data.get("version"),
  395. graph=json.dumps(data.get("graph")),
  396. inputs=json.dumps(data.get("inputs")),
  397. status=data.get("status"),
  398. outputs=json.dumps(data.get("outputs")),
  399. error=data.get("error"),
  400. elapsed_time=data.get("elapsed_time"),
  401. total_tokens=data.get("total_tokens"),
  402. total_steps=data.get("total_steps"),
  403. created_by_role=data.get("created_by_role"),
  404. created_by=data.get("created_by"),
  405. created_at=data.get("created_at"),
  406. finished_at=data.get("finished_at"),
  407. exceptions_count=data.get("exceptions_count"),
  408. )
  409. class WorkflowNodeExecutionTriggeredFrom(Enum):
  410. """
  411. Workflow Node Execution Triggered From Enum
  412. """
  413. SINGLE_STEP = "single-step"
  414. WORKFLOW_RUN = "workflow-run"
  415. @classmethod
  416. def value_of(cls, value: str) -> "WorkflowNodeExecutionTriggeredFrom":
  417. """
  418. Get value of given mode.
  419. :param value: mode value
  420. :return: mode
  421. """
  422. for mode in cls:
  423. if mode.value == value:
  424. return mode
  425. raise ValueError(f"invalid workflow node execution triggered from value {value}")
  426. class WorkflowNodeExecutionStatus(Enum):
  427. """
  428. Workflow Node Execution Status Enum
  429. """
  430. RUNNING = "running"
  431. SUCCEEDED = "succeeded"
  432. FAILED = "failed"
  433. EXCEPTION = "exception"
  434. RETRY = "retry"
  435. @classmethod
  436. def value_of(cls, value: str) -> "WorkflowNodeExecutionStatus":
  437. """
  438. Get value of given mode.
  439. :param value: mode value
  440. :return: mode
  441. """
  442. for mode in cls:
  443. if mode.value == value:
  444. return mode
  445. raise ValueError(f"invalid workflow node execution status value {value}")
  446. class WorkflowNodeExecution(db.Model):
  447. """
  448. Workflow Node Execution
  449. - id (uuid) Execution ID
  450. - tenant_id (uuid) Workspace ID
  451. - app_id (uuid) App ID
  452. - workflow_id (uuid) Workflow ID
  453. - triggered_from (string) Trigger source
  454. `single-step` for single-step debugging
  455. `workflow-run` for workflow execution (debugging / user execution)
  456. - workflow_run_id (uuid) `optional` Workflow run ID
  457. Null for single-step debugging.
  458. - index (int) Execution sequence number, used for displaying Tracing Node order
  459. - predecessor_node_id (string) `optional` Predecessor node ID, used for displaying execution path
  460. - node_id (string) Node ID
  461. - node_type (string) Node type, such as `start`
  462. - title (string) Node title
  463. - inputs (json) All predecessor node variable content used in the node
  464. - process_data (json) Node process data
  465. - outputs (json) `optional` Node output variables
  466. - status (string) Execution status, `running` / `succeeded` / `failed`
  467. - error (string) `optional` Error reason
  468. - elapsed_time (float) `optional` Time consumption (s)
  469. - execution_metadata (text) Metadata
  470. - total_tokens (int) `optional` Total tokens used
  471. - total_price (decimal) `optional` Total cost
  472. - currency (string) `optional` Currency, such as USD / RMB
  473. - created_at (timestamp) Run time
  474. - created_by_role (string) Creator role
  475. - `account` Console account
  476. - `end_user` End user
  477. - created_by (uuid) Runner ID
  478. - finished_at (timestamp) End time
  479. """
  480. __tablename__ = "workflow_node_executions"
  481. __table_args__ = (
  482. db.PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"),
  483. db.Index(
  484. "workflow_node_execution_workflow_run_idx",
  485. "tenant_id",
  486. "app_id",
  487. "workflow_id",
  488. "triggered_from",
  489. "workflow_run_id",
  490. ),
  491. db.Index(
  492. "workflow_node_execution_node_run_idx", "tenant_id", "app_id", "workflow_id", "triggered_from", "node_id"
  493. ),
  494. db.Index(
  495. "workflow_node_execution_id_idx",
  496. "tenant_id",
  497. "app_id",
  498. "workflow_id",
  499. "triggered_from",
  500. "node_execution_id",
  501. ),
  502. )
  503. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  504. tenant_id = db.Column(StringUUID, nullable=False)
  505. app_id = db.Column(StringUUID, nullable=False)
  506. workflow_id = db.Column(StringUUID, nullable=False)
  507. triggered_from = db.Column(db.String(255), nullable=False)
  508. workflow_run_id = db.Column(StringUUID)
  509. index = db.Column(db.Integer, nullable=False)
  510. predecessor_node_id = db.Column(db.String(255))
  511. node_execution_id = db.Column(db.String(255), nullable=True)
  512. node_id = db.Column(db.String(255), nullable=False)
  513. node_type = db.Column(db.String(255), nullable=False)
  514. title = db.Column(db.String(255), nullable=False)
  515. inputs = db.Column(db.Text)
  516. process_data = db.Column(db.Text)
  517. outputs = db.Column(db.Text)
  518. status = db.Column(db.String(255), nullable=False)
  519. error = db.Column(db.Text)
  520. elapsed_time = db.Column(db.Float, nullable=False, server_default=db.text("0"))
  521. execution_metadata = db.Column(db.Text)
  522. created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  523. created_by_role = db.Column(db.String(255), nullable=False)
  524. created_by = db.Column(StringUUID, nullable=False)
  525. finished_at = db.Column(db.DateTime)
  526. retry_index = db.Column(db.Integer, server_default=db.text("0"))
  527. @property
  528. def created_by_account(self):
  529. created_by_role = CreatedByRole(self.created_by_role)
  530. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  531. @property
  532. def created_by_end_user(self):
  533. from models.model import EndUser
  534. created_by_role = CreatedByRole(self.created_by_role)
  535. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  536. @property
  537. def inputs_dict(self):
  538. return json.loads(self.inputs) if self.inputs else None
  539. @property
  540. def outputs_dict(self):
  541. return json.loads(self.outputs) if self.outputs else None
  542. @property
  543. def process_data_dict(self):
  544. return json.loads(self.process_data) if self.process_data else None
  545. @property
  546. def execution_metadata_dict(self):
  547. return json.loads(self.execution_metadata) if self.execution_metadata else None
  548. @property
  549. def extras(self):
  550. from core.tools.tool_manager import ToolManager
  551. extras = {}
  552. if self.execution_metadata_dict:
  553. from core.workflow.nodes import NodeType
  554. if self.node_type == NodeType.TOOL.value and "tool_info" in self.execution_metadata_dict:
  555. tool_info = self.execution_metadata_dict["tool_info"]
  556. extras["icon"] = ToolManager.get_tool_icon(
  557. tenant_id=self.tenant_id,
  558. provider_type=tool_info["provider_type"],
  559. provider_id=tool_info["provider_id"],
  560. )
  561. return extras
  562. class WorkflowAppLogCreatedFrom(Enum):
  563. """
  564. Workflow App Log Created From Enum
  565. """
  566. SERVICE_API = "service-api"
  567. WEB_APP = "web-app"
  568. INSTALLED_APP = "installed-app"
  569. @classmethod
  570. def value_of(cls, value: str) -> "WorkflowAppLogCreatedFrom":
  571. """
  572. Get value of given mode.
  573. :param value: mode value
  574. :return: mode
  575. """
  576. for mode in cls:
  577. if mode.value == value:
  578. return mode
  579. raise ValueError(f"invalid workflow app log created from value {value}")
  580. class WorkflowAppLog(db.Model):
  581. """
  582. Workflow App execution log, excluding workflow debugging records.
  583. Attributes:
  584. - id (uuid) run ID
  585. - tenant_id (uuid) Workspace ID
  586. - app_id (uuid) App ID
  587. - workflow_id (uuid) Associated Workflow ID
  588. - workflow_run_id (uuid) Associated Workflow Run ID
  589. - created_from (string) Creation source
  590. `service-api` App Execution OpenAPI
  591. `web-app` WebApp
  592. `installed-app` Installed App
  593. - created_by_role (string) Creator role
  594. - `account` Console account
  595. - `end_user` End user
  596. - created_by (uuid) Creator ID, depends on the user table according to created_by_role
  597. - created_at (timestamp) Creation time
  598. """
  599. __tablename__ = "workflow_app_logs"
  600. __table_args__ = (
  601. db.PrimaryKeyConstraint("id", name="workflow_app_log_pkey"),
  602. db.Index("workflow_app_log_app_idx", "tenant_id", "app_id"),
  603. )
  604. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  605. tenant_id = db.Column(StringUUID, nullable=False)
  606. app_id = db.Column(StringUUID, nullable=False)
  607. workflow_id = db.Column(StringUUID, nullable=False)
  608. workflow_run_id = db.Column(StringUUID, nullable=False)
  609. created_from = db.Column(db.String(255), nullable=False)
  610. created_by_role = db.Column(db.String(255), nullable=False)
  611. created_by = db.Column(StringUUID, nullable=False)
  612. created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  613. @property
  614. def workflow_run(self):
  615. return db.session.get(WorkflowRun, self.workflow_run_id)
  616. @property
  617. def created_by_account(self):
  618. created_by_role = CreatedByRole(self.created_by_role)
  619. return db.session.get(Account, self.created_by) if created_by_role == CreatedByRole.ACCOUNT else None
  620. @property
  621. def created_by_end_user(self):
  622. from models.model import EndUser
  623. created_by_role = CreatedByRole(self.created_by_role)
  624. return db.session.get(EndUser, self.created_by) if created_by_role == CreatedByRole.END_USER else None
  625. class ConversationVariable(db.Model):
  626. __tablename__ = "workflow_conversation_variables"
  627. id: Mapped[str] = db.Column(StringUUID, primary_key=True)
  628. conversation_id: Mapped[str] = db.Column(StringUUID, nullable=False, primary_key=True)
  629. app_id: Mapped[str] = db.Column(StringUUID, nullable=False, index=True)
  630. data = db.Column(db.Text, nullable=False)
  631. created_at = db.Column(db.DateTime, nullable=False, index=True, server_default=func.current_timestamp())
  632. updated_at = db.Column(
  633. db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
  634. )
  635. def __init__(self, *, id: str, app_id: str, conversation_id: str, data: str) -> None:
  636. self.id = id
  637. self.app_id = app_id
  638. self.conversation_id = conversation_id
  639. self.data = data
  640. @classmethod
  641. def from_variable(cls, *, app_id: str, conversation_id: str, variable: Variable) -> "ConversationVariable":
  642. obj = cls(
  643. id=variable.id,
  644. app_id=app_id,
  645. conversation_id=conversation_id,
  646. data=variable.model_dump_json(),
  647. )
  648. return obj
  649. def to_variable(self) -> Variable:
  650. mapping = json.loads(self.data)
  651. return variable_factory.build_conversation_variable_from_mapping(mapping)