Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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