workflow.py 29 KB

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