workflow.py 27 KB

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