Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. import json
  2. import logging
  3. from collections.abc import Mapping, Sequence
  4. from datetime import UTC, datetime
  5. from enum import Enum, StrEnum
  6. from typing import TYPE_CHECKING, Any, Optional, Self, Union
  7. from uuid import uuid4
  8. from core.variables import utils as variable_utils
  9. from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
  10. from factories.variable_factory import build_segment
  11. if TYPE_CHECKING:
  12. from models.model import AppMode
  13. import sqlalchemy as sa
  14. from sqlalchemy import UniqueConstraint, func
  15. from sqlalchemy.orm import Mapped, mapped_column
  16. import contexts
  17. from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE
  18. from core.helper import encrypter
  19. from core.variables import SecretVariable, Segment, SegmentType, Variable
  20. from factories import variable_factory
  21. from libs import helper
  22. from .account import Account
  23. from .base import Base
  24. from .engine import db
  25. from .enums import CreatorUserRole, DraftVariableType
  26. from .types import EnumText, StringUUID
  27. _logger = logging.getLogger(__name__)
  28. if TYPE_CHECKING:
  29. from models.model import AppMode
  30. class WorkflowType(Enum):
  31. """
  32. Workflow Type Enum
  33. """
  34. WORKFLOW = "workflow"
  35. CHAT = "chat"
  36. RAG_PIPELINE = "rag-pipeline"
  37. @classmethod
  38. def value_of(cls, value: str) -> "WorkflowType":
  39. """
  40. Get value of given mode.
  41. :param value: mode value
  42. :return: mode
  43. """
  44. for mode in cls:
  45. if mode.value == value:
  46. return mode
  47. raise ValueError(f"invalid workflow type value {value}")
  48. @classmethod
  49. def from_app_mode(cls, app_mode: Union[str, "AppMode"]) -> "WorkflowType":
  50. """
  51. Get workflow type from app mode.
  52. :param app_mode: app mode
  53. :return: workflow type
  54. """
  55. from models.model import AppMode
  56. app_mode = app_mode if isinstance(app_mode, AppMode) else AppMode.value_of(app_mode)
  57. return cls.WORKFLOW if app_mode == AppMode.WORKFLOW else cls.CHAT
  58. class Workflow(Base):
  59. """
  60. Workflow, for `Workflow App` and `Chat App workflow mode`.
  61. Attributes:
  62. - id (uuid) Workflow ID, pk
  63. - tenant_id (uuid) Workspace ID
  64. - app_id (uuid) App ID
  65. - type (string) Workflow type
  66. `workflow` for `Workflow App`
  67. `chat` for `Chat App workflow mode`
  68. - version (string) Version
  69. `draft` for draft version (only one for each app), other for version number (redundant)
  70. - graph (text) Workflow canvas configuration (JSON)
  71. The entire canvas configuration JSON, including Node, Edge, and other configurations
  72. - nodes (array[object]) Node list, see Node Schema
  73. - edges (array[object]) Edge list, see Edge Schema
  74. - created_by (uuid) Creator ID
  75. - created_at (timestamp) Creation time
  76. - updated_by (uuid) `optional` Last updater ID
  77. - updated_at (timestamp) `optional` Last update time
  78. """
  79. __tablename__ = "workflows"
  80. __table_args__ = (
  81. db.PrimaryKeyConstraint("id", name="workflow_pkey"),
  82. db.Index("workflow_version_idx", "tenant_id", "app_id", "version"),
  83. )
  84. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  85. tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  86. app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  87. type: Mapped[str] = mapped_column(db.String(255), nullable=False)
  88. version: Mapped[str] = mapped_column(db.String(255), nullable=False)
  89. marked_name: Mapped[str] = mapped_column(default="", server_default="")
  90. marked_comment: Mapped[str] = mapped_column(default="", server_default="")
  91. graph: Mapped[str] = mapped_column(sa.Text)
  92. _features: Mapped[str] = mapped_column("features", sa.TEXT)
  93. created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
  94. created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  95. updated_by: Mapped[Optional[str]] = mapped_column(StringUUID)
  96. updated_at: Mapped[datetime] = mapped_column(
  97. db.DateTime,
  98. nullable=False,
  99. default=datetime.now(UTC).replace(tzinfo=None),
  100. server_onupdate=func.current_timestamp(),
  101. )
  102. _environment_variables: Mapped[str] = mapped_column(
  103. "environment_variables", db.Text, nullable=False, server_default="{}"
  104. )
  105. _conversation_variables: Mapped[str] = mapped_column(
  106. "conversation_variables", db.Text, nullable=False, server_default="{}"
  107. )
  108. _rag_pipeline_variables: Mapped[str] = mapped_column(
  109. "rag_pipeline_variables", db.Text, nullable=False, server_default="{}"
  110. )
  111. @classmethod
  112. def new(
  113. cls,
  114. *,
  115. tenant_id: str,
  116. app_id: str,
  117. type: str,
  118. version: str,
  119. graph: str,
  120. features: str,
  121. created_by: str,
  122. environment_variables: Sequence[Variable],
  123. conversation_variables: Sequence[Variable],
  124. marked_name: str = "",
  125. marked_comment: str = "",
  126. ) -> Self:
  127. workflow = Workflow()
  128. workflow.id = str(uuid4())
  129. workflow.tenant_id = tenant_id
  130. workflow.app_id = app_id
  131. workflow.type = type
  132. workflow.version = version
  133. workflow.graph = graph
  134. workflow.features = features
  135. workflow.created_by = created_by
  136. workflow.environment_variables = environment_variables or []
  137. workflow.conversation_variables = conversation_variables or []
  138. workflow.marked_name = marked_name
  139. workflow.marked_comment = marked_comment
  140. workflow.created_at = datetime.now(UTC).replace(tzinfo=None)
  141. workflow.updated_at = workflow.created_at
  142. return workflow
  143. @property
  144. def created_by_account(self):
  145. return db.session.get(Account, self.created_by)
  146. @property
  147. def updated_by_account(self):
  148. return db.session.get(Account, self.updated_by) if self.updated_by else None
  149. @property
  150. def graph_dict(self) -> Mapping[str, Any]:
  151. return json.loads(self.graph) if self.graph else {}
  152. @property
  153. def features(self) -> str:
  154. """
  155. Convert old features structure to new features structure.
  156. """
  157. if not self._features:
  158. return self._features
  159. features = json.loads(self._features)
  160. if features.get("file_upload", {}).get("image", {}).get("enabled", False):
  161. image_enabled = True
  162. image_number_limits = int(features["file_upload"]["image"].get("number_limits", DEFAULT_FILE_NUMBER_LIMITS))
  163. image_transfer_methods = features["file_upload"]["image"].get(
  164. "transfer_methods", ["remote_url", "local_file"]
  165. )
  166. features["file_upload"]["enabled"] = image_enabled
  167. features["file_upload"]["number_limits"] = image_number_limits
  168. features["file_upload"]["allowed_file_upload_methods"] = image_transfer_methods
  169. features["file_upload"]["allowed_file_types"] = features["file_upload"].get("allowed_file_types", ["image"])
  170. features["file_upload"]["allowed_file_extensions"] = []
  171. del features["file_upload"]["image"]
  172. self._features = json.dumps(features)
  173. return self._features
  174. @features.setter
  175. def features(self, value: str) -> None:
  176. self._features = value
  177. @property
  178. def features_dict(self) -> dict[str, Any]:
  179. return json.loads(self.features) if self.features else {}
  180. def user_input_form(self, to_old_structure: bool = False) -> list:
  181. # get start node from graph
  182. if not self.graph:
  183. return []
  184. graph_dict = self.graph_dict
  185. if "nodes" not in graph_dict:
  186. return []
  187. start_node = next((node for node in graph_dict["nodes"] if node["data"]["type"] == "start"), None)
  188. if not start_node:
  189. return []
  190. # get user_input_form from start node
  191. variables: list[Any] = start_node.get("data", {}).get("variables", [])
  192. if to_old_structure:
  193. old_structure_variables = []
  194. for variable in variables:
  195. old_structure_variables.append({variable["type"]: variable})
  196. return old_structure_variables
  197. return variables
  198. @property
  199. def unique_hash(self) -> str:
  200. """
  201. Get hash of workflow.
  202. :return: hash
  203. """
  204. entity = {"graph": self.graph_dict, "features": self.features_dict}
  205. return helper.generate_text_hash(json.dumps(entity, sort_keys=True))
  206. @property
  207. def tool_published(self) -> bool:
  208. """
  209. DEPRECATED: This property is not accurate for determining if a workflow is published as a tool.
  210. It only checks if there's a WorkflowToolProvider for the app, not if this specific workflow version
  211. is the one being used by the tool.
  212. For accurate checking, use a direct query with tenant_id, app_id, and version.
  213. """
  214. from models.tools import WorkflowToolProvider
  215. return (
  216. db.session.query(WorkflowToolProvider)
  217. .filter(WorkflowToolProvider.tenant_id == self.tenant_id, WorkflowToolProvider.app_id == self.app_id)
  218. .count()
  219. > 0
  220. )
  221. @property
  222. def environment_variables(self) -> Sequence[Variable]:
  223. # TODO: find some way to init `self._environment_variables` when instance created.
  224. if self._environment_variables is None:
  225. self._environment_variables = "{}"
  226. tenant_id = contexts.tenant_id.get()
  227. environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables)
  228. results = [
  229. variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values()
  230. ]
  231. # decrypt secret variables value
  232. def decrypt_func(var):
  233. if isinstance(var, SecretVariable):
  234. return var.model_copy(update={"value": encrypter.decrypt_token(tenant_id=tenant_id, token=var.value)})
  235. else:
  236. return var
  237. results = list(map(decrypt_func, results))
  238. return results
  239. @environment_variables.setter
  240. def environment_variables(self, value: Sequence[Variable]):
  241. if not value:
  242. self._environment_variables = "{}"
  243. return
  244. tenant_id = contexts.tenant_id.get()
  245. value = list(value)
  246. if any(var for var in value if not var.id):
  247. raise ValueError("environment variable require a unique id")
  248. # Compare inputs and origin variables,
  249. # if the value is HIDDEN_VALUE, use the origin variable value (only update `name`).
  250. origin_variables_dictionary = {var.id: var for var in self.environment_variables}
  251. for i, variable in enumerate(value):
  252. if variable.id in origin_variables_dictionary and variable.value == HIDDEN_VALUE:
  253. value[i] = origin_variables_dictionary[variable.id].model_copy(update={"name": variable.name})
  254. # encrypt secret variables value
  255. def encrypt_func(var):
  256. if isinstance(var, SecretVariable):
  257. return var.model_copy(update={"value": encrypter.encrypt_token(tenant_id=tenant_id, token=var.value)})
  258. else:
  259. return var
  260. encrypted_vars = list(map(encrypt_func, value))
  261. environment_variables_json = json.dumps(
  262. {var.name: var.model_dump() for var in encrypted_vars},
  263. ensure_ascii=False,
  264. )
  265. self._environment_variables = environment_variables_json
  266. def to_dict(self, *, include_secret: bool = False) -> Mapping[str, Any]:
  267. environment_variables = list(self.environment_variables)
  268. environment_variables = [
  269. v if not isinstance(v, SecretVariable) or include_secret else v.model_copy(update={"value": ""})
  270. for v in environment_variables
  271. ]
  272. result = {
  273. "graph": self.graph_dict,
  274. "features": self.features_dict,
  275. "environment_variables": [var.model_dump(mode="json") for var in environment_variables],
  276. "conversation_variables": [var.model_dump(mode="json") for var in self.conversation_variables],
  277. "rag_pipeline_variables": [var.model_dump(mode="json") for var in self.rag_pipeline_variables],
  278. }
  279. return result
  280. @property
  281. def conversation_variables(self) -> Sequence[Variable]:
  282. # TODO: find some way to init `self._conversation_variables` when instance created.
  283. if self._conversation_variables is None:
  284. self._conversation_variables = "{}"
  285. variables_dict: dict[str, Any] = json.loads(self._conversation_variables)
  286. results = [variable_factory.build_conversation_variable_from_mapping(v) for v in variables_dict.values()]
  287. return results
  288. @conversation_variables.setter
  289. def conversation_variables(self, value: Sequence[Variable]) -> None:
  290. self._conversation_variables = json.dumps(
  291. {var.name: var.model_dump() for var in value},
  292. ensure_ascii=False,
  293. )
  294. @property
  295. def rag_pipeline_variables(self) -> Sequence[Variable]:
  296. # TODO: find some way to init `self._conversation_variables` when instance created.
  297. if self._rag_pipeline_variables is None:
  298. self._rag_pipeline_variables = "{}"
  299. variables_dict: dict[str, Any] = json.loads(self._rag_pipeline_variables)
  300. results = [v for v in variables_dict.values()]
  301. return results
  302. @rag_pipeline_variables.setter
  303. def rag_pipeline_variables(self, values: list[dict]) -> None:
  304. self._rag_pipeline_variables = json.dumps(
  305. {item["variable"]: item for item in values},
  306. ensure_ascii=False,
  307. )
  308. class WorkflowRunStatus(StrEnum):
  309. """
  310. Workflow Run Status Enum
  311. """
  312. RUNNING = "running"
  313. SUCCEEDED = "succeeded"
  314. FAILED = "failed"
  315. STOPPED = "stopped"
  316. PARTIAL_SUCCEEDED = "partial-succeeded"
  317. class WorkflowRun(Base):
  318. """
  319. Workflow Run
  320. Attributes:
  321. - id (uuid) Run ID
  322. - tenant_id (uuid) Workspace ID
  323. - app_id (uuid) App ID
  324. - sequence_number (int) Auto-increment sequence number, incremented within the App, starting from 1
  325. - workflow_id (uuid) Workflow ID
  326. - type (string) Workflow type
  327. - triggered_from (string) Trigger source
  328. `debugging` for canvas debugging
  329. `app-run` for (published) app execution
  330. - version (string) Version
  331. - graph (text) Workflow canvas configuration (JSON)
  332. - inputs (text) Input parameters
  333. - status (string) Execution status, `running` / `succeeded` / `failed` / `stopped`
  334. - outputs (text) `optional` Output content
  335. - error (string) `optional` Error reason
  336. - elapsed_time (float) `optional` Time consumption (s)
  337. - total_tokens (int) `optional` Total tokens used
  338. - total_steps (int) Total steps (redundant), default 0
  339. - created_by_role (string) Creator role
  340. - `account` Console account
  341. - `end_user` End user
  342. - created_by (uuid) Runner ID
  343. - created_at (timestamp) Run time
  344. - finished_at (timestamp) End time
  345. """
  346. __tablename__ = "workflow_runs"
  347. __table_args__ = (
  348. db.PrimaryKeyConstraint("id", name="workflow_run_pkey"),
  349. db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"),
  350. db.Index("workflow_run_tenant_app_sequence_idx", "tenant_id", "app_id", "sequence_number"),
  351. )
  352. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  353. tenant_id: Mapped[str] = mapped_column(StringUUID)
  354. app_id: Mapped[str] = mapped_column(StringUUID)
  355. sequence_number: Mapped[int] = mapped_column()
  356. workflow_id: Mapped[str] = mapped_column(StringUUID)
  357. type: Mapped[str] = mapped_column(db.String(255))
  358. triggered_from: Mapped[str] = mapped_column(db.String(255))
  359. version: Mapped[str] = mapped_column(db.String(255))
  360. graph: Mapped[Optional[str]] = mapped_column(db.Text)
  361. inputs: Mapped[Optional[str]] = mapped_column(db.Text)
  362. status: Mapped[str] = mapped_column(db.String(255)) # running, succeeded, failed, stopped, partial-succeeded
  363. outputs: Mapped[Optional[str]] = mapped_column(sa.Text, default="{}")
  364. error: Mapped[Optional[str]] = mapped_column(db.Text)
  365. elapsed_time = db.Column(db.Float, nullable=False, server_default=sa.text("0"))
  366. total_tokens: Mapped[int] = mapped_column(sa.BigInteger, server_default=sa.text("0"))
  367. total_steps = db.Column(db.Integer, server_default=db.text("0"))
  368. created_by_role: Mapped[str] = mapped_column(db.String(255)) # account, end_user
  369. created_by = db.Column(StringUUID, nullable=False)
  370. created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  371. finished_at = db.Column(db.DateTime)
  372. exceptions_count = db.Column(db.Integer, server_default=db.text("0"))
  373. @property
  374. def created_by_account(self):
  375. created_by_role = CreatorUserRole(self.created_by_role)
  376. return db.session.get(Account, self.created_by) if created_by_role == CreatorUserRole.ACCOUNT else None
  377. @property
  378. def created_by_end_user(self):
  379. from models.model import EndUser
  380. created_by_role = CreatorUserRole(self.created_by_role)
  381. return db.session.get(EndUser, self.created_by) if created_by_role == CreatorUserRole.END_USER else None
  382. @property
  383. def graph_dict(self):
  384. return json.loads(self.graph) if self.graph else {}
  385. @property
  386. def inputs_dict(self) -> Mapping[str, Any]:
  387. return json.loads(self.inputs) if self.inputs else {}
  388. @property
  389. def outputs_dict(self) -> Mapping[str, Any]:
  390. return json.loads(self.outputs) if self.outputs else {}
  391. @property
  392. def message(self):
  393. from models.model import Message
  394. return (
  395. db.session.query(Message).filter(Message.app_id == self.app_id, Message.workflow_run_id == self.id).first()
  396. )
  397. @property
  398. def workflow(self):
  399. return db.session.query(Workflow).filter(Workflow.id == self.workflow_id).first()
  400. def to_dict(self):
  401. return {
  402. "id": self.id,
  403. "tenant_id": self.tenant_id,
  404. "app_id": self.app_id,
  405. "sequence_number": self.sequence_number,
  406. "workflow_id": self.workflow_id,
  407. "type": self.type,
  408. "triggered_from": self.triggered_from,
  409. "version": self.version,
  410. "graph": self.graph_dict,
  411. "inputs": self.inputs_dict,
  412. "status": self.status,
  413. "outputs": self.outputs_dict,
  414. "error": self.error,
  415. "elapsed_time": self.elapsed_time,
  416. "total_tokens": self.total_tokens,
  417. "total_steps": self.total_steps,
  418. "created_by_role": self.created_by_role,
  419. "created_by": self.created_by,
  420. "created_at": self.created_at,
  421. "finished_at": self.finished_at,
  422. "exceptions_count": self.exceptions_count,
  423. }
  424. @classmethod
  425. def from_dict(cls, data: dict) -> "WorkflowRun":
  426. return cls(
  427. id=data.get("id"),
  428. tenant_id=data.get("tenant_id"),
  429. app_id=data.get("app_id"),
  430. sequence_number=data.get("sequence_number"),
  431. workflow_id=data.get("workflow_id"),
  432. type=data.get("type"),
  433. triggered_from=data.get("triggered_from"),
  434. version=data.get("version"),
  435. graph=json.dumps(data.get("graph")),
  436. inputs=json.dumps(data.get("inputs")),
  437. status=data.get("status"),
  438. outputs=json.dumps(data.get("outputs")),
  439. error=data.get("error"),
  440. elapsed_time=data.get("elapsed_time"),
  441. total_tokens=data.get("total_tokens"),
  442. total_steps=data.get("total_steps"),
  443. created_by_role=data.get("created_by_role"),
  444. created_by=data.get("created_by"),
  445. created_at=data.get("created_at"),
  446. finished_at=data.get("finished_at"),
  447. exceptions_count=data.get("exceptions_count"),
  448. )
  449. class WorkflowNodeExecutionTriggeredFrom(StrEnum):
  450. """
  451. Workflow Node Execution Triggered From Enum
  452. """
  453. SINGLE_STEP = "single-step"
  454. WORKFLOW_RUN = "workflow-run"
  455. class WorkflowNodeExecutionStatus(StrEnum):
  456. """
  457. Workflow Node Execution Status Enum
  458. """
  459. RUNNING = "running"
  460. SUCCEEDED = "succeeded"
  461. FAILED = "failed"
  462. EXCEPTION = "exception"
  463. RETRY = "retry"
  464. class WorkflowNodeExecution(Base):
  465. """
  466. Workflow Node Execution
  467. - id (uuid) Execution ID
  468. - tenant_id (uuid) Workspace ID
  469. - app_id (uuid) App ID
  470. - workflow_id (uuid) Workflow ID
  471. - triggered_from (string) Trigger source
  472. `single-step` for single-step debugging
  473. `workflow-run` for workflow execution (debugging / user execution)
  474. - workflow_run_id (uuid) `optional` Workflow run ID
  475. Null for single-step debugging.
  476. - index (int) Execution sequence number, used for displaying Tracing Node order
  477. - predecessor_node_id (string) `optional` Predecessor node ID, used for displaying execution path
  478. - node_id (string) Node ID
  479. - node_type (string) Node type, such as `start`
  480. - title (string) Node title
  481. - inputs (json) All predecessor node variable content used in the node
  482. - process_data (json) Node process data
  483. - outputs (json) `optional` Node output variables
  484. - status (string) Execution status, `running` / `succeeded` / `failed`
  485. - error (string) `optional` Error reason
  486. - elapsed_time (float) `optional` Time consumption (s)
  487. - execution_metadata (text) Metadata
  488. - total_tokens (int) `optional` Total tokens used
  489. - total_price (decimal) `optional` Total cost
  490. - currency (string) `optional` Currency, such as USD / RMB
  491. - created_at (timestamp) Run time
  492. - created_by_role (string) Creator role
  493. - `account` Console account
  494. - `end_user` End user
  495. - created_by (uuid) Runner ID
  496. - finished_at (timestamp) End time
  497. """
  498. __tablename__ = "workflow_node_executions"
  499. __table_args__ = (
  500. db.PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"),
  501. db.Index(
  502. "workflow_node_execution_workflow_run_idx",
  503. "tenant_id",
  504. "app_id",
  505. "workflow_id",
  506. "triggered_from",
  507. "workflow_run_id",
  508. ),
  509. db.Index(
  510. "workflow_node_execution_node_run_idx", "tenant_id", "app_id", "workflow_id", "triggered_from", "node_id"
  511. ),
  512. db.Index(
  513. "workflow_node_execution_id_idx",
  514. "tenant_id",
  515. "app_id",
  516. "workflow_id",
  517. "triggered_from",
  518. "node_execution_id",
  519. ),
  520. )
  521. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  522. tenant_id: Mapped[str] = mapped_column(StringUUID)
  523. app_id: Mapped[str] = mapped_column(StringUUID)
  524. workflow_id: Mapped[str] = mapped_column(StringUUID)
  525. triggered_from: Mapped[str] = mapped_column(db.String(255))
  526. workflow_run_id: Mapped[Optional[str]] = mapped_column(StringUUID)
  527. index: Mapped[int] = mapped_column(db.Integer)
  528. predecessor_node_id: Mapped[Optional[str]] = mapped_column(db.String(255))
  529. node_execution_id: Mapped[Optional[str]] = mapped_column(db.String(255))
  530. node_id: Mapped[str] = mapped_column(db.String(255))
  531. node_type: Mapped[str] = mapped_column(db.String(255))
  532. title: Mapped[str] = mapped_column(db.String(255))
  533. inputs: Mapped[Optional[str]] = mapped_column(db.Text)
  534. process_data: Mapped[Optional[str]] = mapped_column(db.Text)
  535. outputs: Mapped[Optional[str]] = mapped_column(db.Text)
  536. status: Mapped[str] = mapped_column(db.String(255))
  537. error: Mapped[Optional[str]] = mapped_column(db.Text)
  538. elapsed_time: Mapped[float] = mapped_column(db.Float, server_default=db.text("0"))
  539. execution_metadata: Mapped[Optional[str]] = mapped_column(db.Text)
  540. created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp())
  541. created_by_role: Mapped[str] = mapped_column(db.String(255))
  542. created_by: Mapped[str] = mapped_column(StringUUID)
  543. finished_at: Mapped[Optional[datetime]] = mapped_column(db.DateTime)
  544. @property
  545. def created_by_account(self):
  546. created_by_role = CreatorUserRole(self.created_by_role)
  547. # TODO(-LAN-): Avoid using db.session.get() here.
  548. return db.session.get(Account, self.created_by) if created_by_role == CreatorUserRole.ACCOUNT else None
  549. @property
  550. def created_by_end_user(self):
  551. from models.model import EndUser
  552. created_by_role = CreatorUserRole(self.created_by_role)
  553. # TODO(-LAN-): Avoid using db.session.get() here.
  554. return db.session.get(EndUser, self.created_by) if created_by_role == CreatorUserRole.END_USER else None
  555. @property
  556. def inputs_dict(self):
  557. return json.loads(self.inputs) if self.inputs else None
  558. @property
  559. def outputs_dict(self) -> dict[str, Any] | None:
  560. return json.loads(self.outputs) if self.outputs else None
  561. @property
  562. def process_data_dict(self):
  563. return json.loads(self.process_data) if self.process_data else None
  564. @property
  565. def execution_metadata_dict(self) -> dict[str, Any] | None:
  566. return json.loads(self.execution_metadata) if self.execution_metadata else None
  567. @property
  568. def extras(self):
  569. from core.tools.tool_manager import ToolManager
  570. extras = {}
  571. if self.execution_metadata_dict:
  572. from core.workflow.nodes import NodeType
  573. if self.node_type == NodeType.TOOL.value and "tool_info" in self.execution_metadata_dict:
  574. tool_info = self.execution_metadata_dict["tool_info"]
  575. extras["icon"] = ToolManager.get_tool_icon(
  576. tenant_id=self.tenant_id,
  577. provider_type=tool_info["provider_type"],
  578. provider_id=tool_info["provider_id"],
  579. )
  580. return extras
  581. class WorkflowAppLogCreatedFrom(Enum):
  582. """
  583. Workflow App Log Created From Enum
  584. """
  585. SERVICE_API = "service-api"
  586. WEB_APP = "web-app"
  587. INSTALLED_APP = "installed-app"
  588. @classmethod
  589. def value_of(cls, value: str) -> "WorkflowAppLogCreatedFrom":
  590. """
  591. Get value of given mode.
  592. :param value: mode value
  593. :return: mode
  594. """
  595. for mode in cls:
  596. if mode.value == value:
  597. return mode
  598. raise ValueError(f"invalid workflow app log created from value {value}")
  599. class WorkflowAppLog(Base):
  600. """
  601. Workflow App execution log, excluding workflow debugging records.
  602. Attributes:
  603. - id (uuid) run ID
  604. - tenant_id (uuid) Workspace ID
  605. - app_id (uuid) App ID
  606. - workflow_id (uuid) Associated Workflow ID
  607. - workflow_run_id (uuid) Associated Workflow Run ID
  608. - created_from (string) Creation source
  609. `service-api` App Execution OpenAPI
  610. `web-app` WebApp
  611. `installed-app` Installed App
  612. - created_by_role (string) Creator role
  613. - `account` Console account
  614. - `end_user` End user
  615. - created_by (uuid) Creator ID, depends on the user table according to created_by_role
  616. - created_at (timestamp) Creation time
  617. """
  618. __tablename__ = "workflow_app_logs"
  619. __table_args__ = (
  620. db.PrimaryKeyConstraint("id", name="workflow_app_log_pkey"),
  621. db.Index("workflow_app_log_app_idx", "tenant_id", "app_id"),
  622. )
  623. id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  624. tenant_id: Mapped[str] = mapped_column(StringUUID)
  625. app_id: Mapped[str] = mapped_column(StringUUID)
  626. workflow_id = db.Column(StringUUID, nullable=False)
  627. workflow_run_id: Mapped[str] = mapped_column(StringUUID)
  628. created_from = db.Column(db.String(255), nullable=False)
  629. created_by_role = db.Column(db.String(255), nullable=False)
  630. created_by = db.Column(StringUUID, nullable=False)
  631. created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp())
  632. @property
  633. def workflow_run(self):
  634. return db.session.get(WorkflowRun, self.workflow_run_id)
  635. @property
  636. def created_by_account(self):
  637. created_by_role = CreatorUserRole(self.created_by_role)
  638. return db.session.get(Account, self.created_by) if created_by_role == CreatorUserRole.ACCOUNT else None
  639. @property
  640. def created_by_end_user(self):
  641. from models.model import EndUser
  642. created_by_role = CreatorUserRole(self.created_by_role)
  643. return db.session.get(EndUser, self.created_by) if created_by_role == CreatorUserRole.END_USER else None
  644. class ConversationVariable(Base):
  645. __tablename__ = "workflow_conversation_variables"
  646. id: Mapped[str] = mapped_column(StringUUID, primary_key=True)
  647. conversation_id: Mapped[str] = mapped_column(StringUUID, nullable=False, primary_key=True, index=True)
  648. app_id: Mapped[str] = mapped_column(StringUUID, nullable=False, index=True)
  649. data = mapped_column(db.Text, nullable=False)
  650. created_at = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
  651. updated_at = mapped_column(
  652. db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
  653. )
  654. def __init__(self, *, id: str, app_id: str, conversation_id: str, data: str) -> None:
  655. self.id = id
  656. self.app_id = app_id
  657. self.conversation_id = conversation_id
  658. self.data = data
  659. @classmethod
  660. def from_variable(cls, *, app_id: str, conversation_id: str, variable: Variable) -> "ConversationVariable":
  661. obj = cls(
  662. id=variable.id,
  663. app_id=app_id,
  664. conversation_id=conversation_id,
  665. data=variable.model_dump_json(),
  666. )
  667. return obj
  668. def to_variable(self) -> Variable:
  669. mapping = json.loads(self.data)
  670. return variable_factory.build_conversation_variable_from_mapping(mapping)
  671. # Only `sys.query` and `sys.files` could be modified.
  672. _EDITABLE_SYSTEM_VARIABLE = frozenset(["query", "files"])
  673. def _naive_utc_datetime():
  674. return datetime.now(UTC).replace(tzinfo=None)
  675. class WorkflowDraftVariable(Base):
  676. @staticmethod
  677. def unique_columns() -> list[str]:
  678. return [
  679. "app_id",
  680. "node_id",
  681. "name",
  682. ]
  683. __tablename__ = "workflow_draft_variables"
  684. __table_args__ = (UniqueConstraint(*unique_columns()),)
  685. # id is the unique identifier of a draft variable.
  686. id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
  687. created_at = mapped_column(
  688. db.DateTime,
  689. nullable=False,
  690. default=_naive_utc_datetime,
  691. server_default=func.current_timestamp(),
  692. )
  693. updated_at = mapped_column(
  694. db.DateTime,
  695. nullable=False,
  696. default=_naive_utc_datetime,
  697. server_default=func.current_timestamp(),
  698. onupdate=func.current_timestamp(),
  699. )
  700. # "`app_id` maps to the `id` field in the `model.App` model."
  701. app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  702. # `last_edited_at` records when the value of a given draft variable
  703. # is edited.
  704. #
  705. # If it's not edited after creation, its value is `None`.
  706. last_edited_at: Mapped[datetime | None] = mapped_column(
  707. db.DateTime,
  708. nullable=True,
  709. default=None,
  710. )
  711. # The `node_id` field is special.
  712. #
  713. # If the variable is a conversation variable or a system variable, then the value of `node_id`
  714. # is `conversation` or `sys`, respective.
  715. #
  716. # Otherwise, if the variable is a variable belonging to a specific node, the value of `_node_id` is
  717. # the identity of correspond node in graph definition. An example of node id is `"1745769620734"`.
  718. #
  719. # However, there's one caveat. The id of the first "Answer" node in chatflow is "answer". (Other
  720. # "Answer" node conform the rules above.)
  721. node_id: Mapped[str] = mapped_column(sa.String(255), nullable=False, name="node_id")
  722. # From `VARIABLE_PATTERN`, we may conclude that the length of a top level variable is less than
  723. # 80 chars.
  724. #
  725. # ref: api/core/workflow/entities/variable_pool.py:18
  726. name: Mapped[str] = mapped_column(sa.String(255), nullable=False)
  727. description: Mapped[str] = mapped_column(
  728. sa.String(255),
  729. default="",
  730. nullable=False,
  731. )
  732. selector: Mapped[str] = mapped_column(sa.String(255), nullable=False, name="selector")
  733. value_type: Mapped[SegmentType] = mapped_column(EnumText(SegmentType, length=20))
  734. # JSON string
  735. value: Mapped[str] = mapped_column(sa.Text, nullable=False, name="value")
  736. # visible
  737. visible: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
  738. editable: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False)
  739. def get_selector(self) -> list[str]:
  740. selector = json.loads(self.selector)
  741. if not isinstance(selector, list):
  742. _logger.error(
  743. "invalid selector loaded from database, type=%s, value=%s",
  744. type(selector),
  745. self.selector,
  746. )
  747. raise ValueError("invalid selector.")
  748. return selector
  749. def _set_selector(self, value: list[str]):
  750. self.selector = json.dumps(value)
  751. def get_value(self) -> Segment | None:
  752. return build_segment(json.loads(self.value))
  753. def set_name(self, name: str):
  754. self.name = name
  755. self._set_selector([self.node_id, name])
  756. def set_value(self, value: Segment):
  757. self.value = json.dumps(value.value)
  758. self.value_type = value.value_type
  759. def get_node_id(self) -> str | None:
  760. if self.get_variable_type() == DraftVariableType.NODE:
  761. return self.node_id
  762. else:
  763. return None
  764. def get_variable_type(self) -> DraftVariableType:
  765. match self.node_id:
  766. case DraftVariableType.CONVERSATION:
  767. return DraftVariableType.CONVERSATION
  768. case DraftVariableType.SYS:
  769. return DraftVariableType.SYS
  770. case _:
  771. return DraftVariableType.NODE
  772. @classmethod
  773. def _new(
  774. cls,
  775. *,
  776. app_id: str,
  777. node_id: str,
  778. name: str,
  779. value: Segment,
  780. description: str = "",
  781. ) -> "WorkflowDraftVariable":
  782. variable = WorkflowDraftVariable()
  783. variable.created_at = _naive_utc_datetime()
  784. variable.updated_at = _naive_utc_datetime()
  785. variable.description = description
  786. variable.app_id = app_id
  787. variable.node_id = node_id
  788. variable.name = name
  789. variable.set_value(value)
  790. variable._set_selector(list(variable_utils.to_selector(node_id, name)))
  791. return variable
  792. @classmethod
  793. def new_conversation_variable(
  794. cls,
  795. *,
  796. app_id: str,
  797. name: str,
  798. value: Segment,
  799. ) -> "WorkflowDraftVariable":
  800. variable = cls._new(
  801. app_id=app_id,
  802. node_id=CONVERSATION_VARIABLE_NODE_ID,
  803. name=name,
  804. value=value,
  805. )
  806. return variable
  807. @classmethod
  808. def new_sys_variable(
  809. cls,
  810. *,
  811. app_id: str,
  812. name: str,
  813. value: Segment,
  814. editable: bool = False,
  815. ) -> "WorkflowDraftVariable":
  816. variable = cls._new(app_id=app_id, node_id=SYSTEM_VARIABLE_NODE_ID, name=name, value=value)
  817. variable.editable = editable
  818. return variable
  819. @classmethod
  820. def new_node_variable(
  821. cls,
  822. *,
  823. app_id: str,
  824. node_id: str,
  825. name: str,
  826. value: Segment,
  827. visible: bool = True,
  828. ) -> "WorkflowDraftVariable":
  829. variable = cls._new(app_id=app_id, node_id=node_id, name=name, value=value)
  830. variable.visible = visible
  831. variable.editable = True
  832. return variable
  833. @property
  834. def edited(self):
  835. return self.last_edited_at is not None
  836. def is_system_variable_editable(name: str) -> bool:
  837. return name in _EDITABLE_SYSTEM_VARIABLE