Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

workflow.py 30KB

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