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 29KB

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