workflow.py 26 KB

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