model.py 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663
  1. import json
  2. import re
  3. import uuid
  4. from collections.abc import Mapping, Sequence
  5. from datetime import datetime
  6. from enum import Enum
  7. from typing import Any, Literal, Optional
  8. import sqlalchemy as sa
  9. from flask import request
  10. from flask_login import UserMixin
  11. from pydantic import BaseModel, Field
  12. from sqlalchemy import Float, func, text
  13. from sqlalchemy.orm import Mapped, mapped_column
  14. from configs import dify_config
  15. from core.file import FILE_MODEL_IDENTITY, File, FileExtraConfig, FileTransferMethod, FileType
  16. from core.file import helpers as file_helpers
  17. from core.file.tool_file_parser import ToolFileParser
  18. from extensions.ext_database import db
  19. from libs.helper import generate_string
  20. from models.enums import CreatedByRole
  21. from .account import Account, Tenant
  22. from .types import StringUUID
  23. class FileUploadConfig(BaseModel):
  24. enabled: bool = Field(default=False)
  25. allowed_file_types: Sequence[FileType] = Field(default_factory=list)
  26. allowed_extensions: Sequence[str] = Field(default_factory=list)
  27. allowed_upload_methods: Sequence[FileTransferMethod] = Field(default_factory=list)
  28. number_limits: int = Field(default=0, gt=0, le=10)
  29. class DifySetup(db.Model):
  30. __tablename__ = "dify_setups"
  31. __table_args__ = (db.PrimaryKeyConstraint("version", name="dify_setup_pkey"),)
  32. version = db.Column(db.String(255), nullable=False)
  33. setup_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  34. class AppMode(str, Enum):
  35. COMPLETION = "completion"
  36. WORKFLOW = "workflow"
  37. CHAT = "chat"
  38. ADVANCED_CHAT = "advanced-chat"
  39. AGENT_CHAT = "agent-chat"
  40. CHANNEL = "channel"
  41. @classmethod
  42. def value_of(cls, value: str) -> "AppMode":
  43. """
  44. Get value of given mode.
  45. :param value: mode value
  46. :return: mode
  47. """
  48. for mode in cls:
  49. if mode.value == value:
  50. return mode
  51. raise ValueError(f"invalid mode value {value}")
  52. class IconType(Enum):
  53. IMAGE = "image"
  54. EMOJI = "emoji"
  55. class App(db.Model):
  56. __tablename__ = "apps"
  57. __table_args__ = (db.PrimaryKeyConstraint("id", name="app_pkey"), db.Index("app_tenant_id_idx", "tenant_id"))
  58. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  59. tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False)
  60. name = db.Column(db.String(255), nullable=False)
  61. description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying"))
  62. mode = db.Column(db.String(255), nullable=False)
  63. icon_type = db.Column(db.String(255), nullable=True)
  64. icon = db.Column(db.String(255))
  65. icon_background = db.Column(db.String(255))
  66. app_model_config_id = db.Column(StringUUID, nullable=True)
  67. workflow_id = db.Column(StringUUID, nullable=True)
  68. status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying"))
  69. enable_site = db.Column(db.Boolean, nullable=False)
  70. enable_api = db.Column(db.Boolean, nullable=False)
  71. api_rpm = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  72. api_rph = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  73. is_demo = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  74. is_public = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  75. is_universal = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  76. tracing = db.Column(db.Text, nullable=True)
  77. max_active_requests = db.Column(db.Integer, nullable=True)
  78. created_by = db.Column(StringUUID, nullable=True)
  79. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  80. updated_by = db.Column(StringUUID, nullable=True)
  81. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  82. use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  83. @property
  84. def desc_or_prompt(self):
  85. if self.description:
  86. return self.description
  87. else:
  88. app_model_config = self.app_model_config
  89. if app_model_config:
  90. return app_model_config.pre_prompt
  91. else:
  92. return ""
  93. @property
  94. def site(self):
  95. site = db.session.query(Site).filter(Site.app_id == self.id).first()
  96. return site
  97. @property
  98. def app_model_config(self) -> Optional["AppModelConfig"]:
  99. if self.app_model_config_id:
  100. return db.session.query(AppModelConfig).filter(AppModelConfig.id == self.app_model_config_id).first()
  101. return None
  102. @property
  103. def workflow(self) -> Optional["Workflow"]:
  104. if self.workflow_id:
  105. from .workflow import Workflow
  106. return db.session.query(Workflow).filter(Workflow.id == self.workflow_id).first()
  107. return None
  108. @property
  109. def api_base_url(self):
  110. return (dify_config.SERVICE_API_URL or request.host_url.rstrip("/")) + "/v1"
  111. @property
  112. def tenant(self):
  113. tenant = db.session.query(Tenant).filter(Tenant.id == self.tenant_id).first()
  114. return tenant
  115. @property
  116. def is_agent(self) -> bool:
  117. app_model_config = self.app_model_config
  118. if not app_model_config:
  119. return False
  120. if not app_model_config.agent_mode:
  121. return False
  122. if self.app_model_config.agent_mode_dict.get("enabled", False) and self.app_model_config.agent_mode_dict.get(
  123. "strategy", ""
  124. ) in {"function_call", "react"}:
  125. self.mode = AppMode.AGENT_CHAT.value
  126. db.session.commit()
  127. return True
  128. return False
  129. @property
  130. def mode_compatible_with_agent(self) -> str:
  131. if self.mode == AppMode.CHAT.value and self.is_agent:
  132. return AppMode.AGENT_CHAT.value
  133. return self.mode
  134. @property
  135. def deleted_tools(self) -> list:
  136. # get agent mode tools
  137. app_model_config = self.app_model_config
  138. if not app_model_config:
  139. return []
  140. if not app_model_config.agent_mode:
  141. return []
  142. agent_mode = app_model_config.agent_mode_dict
  143. tools = agent_mode.get("tools", [])
  144. provider_ids = []
  145. for tool in tools:
  146. keys = list(tool.keys())
  147. if len(keys) >= 4:
  148. provider_type = tool.get("provider_type", "")
  149. provider_id = tool.get("provider_id", "")
  150. if provider_type == "api":
  151. # check if provider id is a uuid string, if not, skip
  152. try:
  153. uuid.UUID(provider_id)
  154. except Exception:
  155. continue
  156. provider_ids.append(provider_id)
  157. if not provider_ids:
  158. return []
  159. api_providers = db.session.execute(
  160. text("SELECT id FROM tool_api_providers WHERE id IN :provider_ids"), {"provider_ids": tuple(provider_ids)}
  161. ).fetchall()
  162. deleted_tools = []
  163. current_api_provider_ids = [str(api_provider.id) for api_provider in api_providers]
  164. for tool in tools:
  165. keys = list(tool.keys())
  166. if len(keys) >= 4:
  167. provider_type = tool.get("provider_type", "")
  168. provider_id = tool.get("provider_id", "")
  169. if provider_type == "api" and provider_id not in current_api_provider_ids:
  170. deleted_tools.append(tool["tool_name"])
  171. return deleted_tools
  172. @property
  173. def tags(self):
  174. tags = (
  175. db.session.query(Tag)
  176. .join(TagBinding, Tag.id == TagBinding.tag_id)
  177. .filter(
  178. TagBinding.target_id == self.id,
  179. TagBinding.tenant_id == self.tenant_id,
  180. Tag.tenant_id == self.tenant_id,
  181. Tag.type == "app",
  182. )
  183. .all()
  184. )
  185. return tags or []
  186. class AppModelConfig(db.Model):
  187. __tablename__ = "app_model_configs"
  188. __table_args__ = (db.PrimaryKeyConstraint("id", name="app_model_config_pkey"), db.Index("app_app_id_idx", "app_id"))
  189. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  190. app_id = db.Column(StringUUID, nullable=False)
  191. provider = db.Column(db.String(255), nullable=True)
  192. model_id = db.Column(db.String(255), nullable=True)
  193. configs = db.Column(db.JSON, nullable=True)
  194. created_by = db.Column(StringUUID, nullable=True)
  195. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  196. updated_by = db.Column(StringUUID, nullable=True)
  197. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  198. opening_statement = db.Column(db.Text)
  199. suggested_questions = db.Column(db.Text)
  200. suggested_questions_after_answer = db.Column(db.Text)
  201. speech_to_text = db.Column(db.Text)
  202. text_to_speech = db.Column(db.Text)
  203. more_like_this = db.Column(db.Text)
  204. model = db.Column(db.Text)
  205. user_input_form = db.Column(db.Text)
  206. dataset_query_variable = db.Column(db.String(255))
  207. pre_prompt = db.Column(db.Text)
  208. agent_mode = db.Column(db.Text)
  209. sensitive_word_avoidance = db.Column(db.Text)
  210. retriever_resource = db.Column(db.Text)
  211. prompt_type = db.Column(db.String(255), nullable=False, server_default=db.text("'simple'::character varying"))
  212. chat_prompt_config = db.Column(db.Text)
  213. completion_prompt_config = db.Column(db.Text)
  214. dataset_configs = db.Column(db.Text)
  215. external_data_tools = db.Column(db.Text)
  216. file_upload = db.Column(db.Text)
  217. @property
  218. def app(self):
  219. app = db.session.query(App).filter(App.id == self.app_id).first()
  220. return app
  221. @property
  222. def model_dict(self) -> dict:
  223. return json.loads(self.model) if self.model else None
  224. @property
  225. def suggested_questions_list(self) -> list:
  226. return json.loads(self.suggested_questions) if self.suggested_questions else []
  227. @property
  228. def suggested_questions_after_answer_dict(self) -> dict:
  229. return (
  230. json.loads(self.suggested_questions_after_answer)
  231. if self.suggested_questions_after_answer
  232. else {"enabled": False}
  233. )
  234. @property
  235. def speech_to_text_dict(self) -> dict:
  236. return json.loads(self.speech_to_text) if self.speech_to_text else {"enabled": False}
  237. @property
  238. def text_to_speech_dict(self) -> dict:
  239. return json.loads(self.text_to_speech) if self.text_to_speech else {"enabled": False}
  240. @property
  241. def retriever_resource_dict(self) -> dict:
  242. return json.loads(self.retriever_resource) if self.retriever_resource else {"enabled": True}
  243. @property
  244. def annotation_reply_dict(self) -> dict:
  245. annotation_setting = (
  246. db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == self.app_id).first()
  247. )
  248. if annotation_setting:
  249. collection_binding_detail = annotation_setting.collection_binding_detail
  250. return {
  251. "id": annotation_setting.id,
  252. "enabled": True,
  253. "score_threshold": annotation_setting.score_threshold,
  254. "embedding_model": {
  255. "embedding_provider_name": collection_binding_detail.provider_name,
  256. "embedding_model_name": collection_binding_detail.model_name,
  257. },
  258. }
  259. else:
  260. return {"enabled": False}
  261. @property
  262. def more_like_this_dict(self) -> dict:
  263. return json.loads(self.more_like_this) if self.more_like_this else {"enabled": False}
  264. @property
  265. def sensitive_word_avoidance_dict(self) -> dict:
  266. return (
  267. json.loads(self.sensitive_word_avoidance)
  268. if self.sensitive_word_avoidance
  269. else {"enabled": False, "type": "", "configs": []}
  270. )
  271. @property
  272. def external_data_tools_list(self) -> list[dict]:
  273. return json.loads(self.external_data_tools) if self.external_data_tools else []
  274. @property
  275. def user_input_form_list(self) -> dict:
  276. return json.loads(self.user_input_form) if self.user_input_form else []
  277. @property
  278. def agent_mode_dict(self) -> dict:
  279. return (
  280. json.loads(self.agent_mode)
  281. if self.agent_mode
  282. else {"enabled": False, "strategy": None, "tools": [], "prompt": None}
  283. )
  284. @property
  285. def chat_prompt_config_dict(self) -> dict:
  286. return json.loads(self.chat_prompt_config) if self.chat_prompt_config else {}
  287. @property
  288. def completion_prompt_config_dict(self) -> dict:
  289. return json.loads(self.completion_prompt_config) if self.completion_prompt_config else {}
  290. @property
  291. def dataset_configs_dict(self) -> dict:
  292. if self.dataset_configs:
  293. dataset_configs = json.loads(self.dataset_configs)
  294. if "retrieval_model" not in dataset_configs:
  295. return {"retrieval_model": "single"}
  296. else:
  297. return dataset_configs
  298. return {
  299. "retrieval_model": "multiple",
  300. }
  301. @property
  302. def file_upload_dict(self) -> dict:
  303. return (
  304. json.loads(self.file_upload)
  305. if self.file_upload
  306. else {
  307. "image": {
  308. "enabled": False,
  309. "number_limits": 3,
  310. "detail": "high",
  311. "transfer_methods": ["remote_url", "local_file"],
  312. }
  313. }
  314. )
  315. def to_dict(self) -> dict:
  316. return {
  317. "opening_statement": self.opening_statement,
  318. "suggested_questions": self.suggested_questions_list,
  319. "suggested_questions_after_answer": self.suggested_questions_after_answer_dict,
  320. "speech_to_text": self.speech_to_text_dict,
  321. "text_to_speech": self.text_to_speech_dict,
  322. "retriever_resource": self.retriever_resource_dict,
  323. "annotation_reply": self.annotation_reply_dict,
  324. "more_like_this": self.more_like_this_dict,
  325. "sensitive_word_avoidance": self.sensitive_word_avoidance_dict,
  326. "external_data_tools": self.external_data_tools_list,
  327. "model": self.model_dict,
  328. "user_input_form": self.user_input_form_list,
  329. "dataset_query_variable": self.dataset_query_variable,
  330. "pre_prompt": self.pre_prompt,
  331. "agent_mode": self.agent_mode_dict,
  332. "prompt_type": self.prompt_type,
  333. "chat_prompt_config": self.chat_prompt_config_dict,
  334. "completion_prompt_config": self.completion_prompt_config_dict,
  335. "dataset_configs": self.dataset_configs_dict,
  336. "file_upload": self.file_upload_dict,
  337. }
  338. def from_model_config_dict(self, model_config: Mapping[str, Any]):
  339. self.opening_statement = model_config.get("opening_statement")
  340. self.suggested_questions = (
  341. json.dumps(model_config["suggested_questions"]) if model_config.get("suggested_questions") else None
  342. )
  343. self.suggested_questions_after_answer = (
  344. json.dumps(model_config["suggested_questions_after_answer"])
  345. if model_config.get("suggested_questions_after_answer")
  346. else None
  347. )
  348. self.speech_to_text = json.dumps(model_config["speech_to_text"]) if model_config.get("speech_to_text") else None
  349. self.text_to_speech = json.dumps(model_config["text_to_speech"]) if model_config.get("text_to_speech") else None
  350. self.more_like_this = json.dumps(model_config["more_like_this"]) if model_config.get("more_like_this") else None
  351. self.sensitive_word_avoidance = (
  352. json.dumps(model_config["sensitive_word_avoidance"])
  353. if model_config.get("sensitive_word_avoidance")
  354. else None
  355. )
  356. self.external_data_tools = (
  357. json.dumps(model_config["external_data_tools"]) if model_config.get("external_data_tools") else None
  358. )
  359. self.model = json.dumps(model_config["model"]) if model_config.get("model") else None
  360. self.user_input_form = (
  361. json.dumps(model_config["user_input_form"]) if model_config.get("user_input_form") else None
  362. )
  363. self.dataset_query_variable = model_config.get("dataset_query_variable")
  364. self.pre_prompt = model_config["pre_prompt"]
  365. self.agent_mode = json.dumps(model_config["agent_mode"]) if model_config.get("agent_mode") else None
  366. self.retriever_resource = (
  367. json.dumps(model_config["retriever_resource"]) if model_config.get("retriever_resource") else None
  368. )
  369. self.prompt_type = model_config.get("prompt_type", "simple")
  370. self.chat_prompt_config = (
  371. json.dumps(model_config.get("chat_prompt_config")) if model_config.get("chat_prompt_config") else None
  372. )
  373. self.completion_prompt_config = (
  374. json.dumps(model_config.get("completion_prompt_config"))
  375. if model_config.get("completion_prompt_config")
  376. else None
  377. )
  378. self.dataset_configs = (
  379. json.dumps(model_config.get("dataset_configs")) if model_config.get("dataset_configs") else None
  380. )
  381. self.file_upload = json.dumps(model_config.get("file_upload")) if model_config.get("file_upload") else None
  382. return self
  383. def copy(self):
  384. new_app_model_config = AppModelConfig(
  385. id=self.id,
  386. app_id=self.app_id,
  387. opening_statement=self.opening_statement,
  388. suggested_questions=self.suggested_questions,
  389. suggested_questions_after_answer=self.suggested_questions_after_answer,
  390. speech_to_text=self.speech_to_text,
  391. text_to_speech=self.text_to_speech,
  392. more_like_this=self.more_like_this,
  393. sensitive_word_avoidance=self.sensitive_word_avoidance,
  394. external_data_tools=self.external_data_tools,
  395. model=self.model,
  396. user_input_form=self.user_input_form,
  397. dataset_query_variable=self.dataset_query_variable,
  398. pre_prompt=self.pre_prompt,
  399. agent_mode=self.agent_mode,
  400. retriever_resource=self.retriever_resource,
  401. prompt_type=self.prompt_type,
  402. chat_prompt_config=self.chat_prompt_config,
  403. completion_prompt_config=self.completion_prompt_config,
  404. dataset_configs=self.dataset_configs,
  405. file_upload=self.file_upload,
  406. )
  407. return new_app_model_config
  408. class RecommendedApp(db.Model):
  409. __tablename__ = "recommended_apps"
  410. __table_args__ = (
  411. db.PrimaryKeyConstraint("id", name="recommended_app_pkey"),
  412. db.Index("recommended_app_app_id_idx", "app_id"),
  413. db.Index("recommended_app_is_listed_idx", "is_listed", "language"),
  414. )
  415. id = db.Column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()"))
  416. app_id = db.Column(StringUUID, nullable=False)
  417. description = db.Column(db.JSON, nullable=False)
  418. copyright = db.Column(db.String(255), nullable=False)
  419. privacy_policy = db.Column(db.String(255), nullable=False)
  420. custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="")
  421. category = db.Column(db.String(255), nullable=False)
  422. position = db.Column(db.Integer, nullable=False, default=0)
  423. is_listed = db.Column(db.Boolean, nullable=False, default=True)
  424. install_count = db.Column(db.Integer, nullable=False, default=0)
  425. language = db.Column(db.String(255), nullable=False, server_default=db.text("'en-US'::character varying"))
  426. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  427. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  428. @property
  429. def app(self):
  430. app = db.session.query(App).filter(App.id == self.app_id).first()
  431. return app
  432. class InstalledApp(db.Model):
  433. __tablename__ = "installed_apps"
  434. __table_args__ = (
  435. db.PrimaryKeyConstraint("id", name="installed_app_pkey"),
  436. db.Index("installed_app_tenant_id_idx", "tenant_id"),
  437. db.Index("installed_app_app_id_idx", "app_id"),
  438. db.UniqueConstraint("tenant_id", "app_id", name="unique_tenant_app"),
  439. )
  440. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  441. tenant_id = db.Column(StringUUID, nullable=False)
  442. app_id = db.Column(StringUUID, nullable=False)
  443. app_owner_tenant_id = db.Column(StringUUID, nullable=False)
  444. position = db.Column(db.Integer, nullable=False, default=0)
  445. is_pinned = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  446. last_used_at = db.Column(db.DateTime, nullable=True)
  447. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  448. @property
  449. def app(self):
  450. app = db.session.query(App).filter(App.id == self.app_id).first()
  451. return app
  452. @property
  453. def tenant(self):
  454. tenant = db.session.query(Tenant).filter(Tenant.id == self.tenant_id).first()
  455. return tenant
  456. class Conversation(db.Model):
  457. __tablename__ = "conversations"
  458. __table_args__ = (
  459. db.PrimaryKeyConstraint("id", name="conversation_pkey"),
  460. db.Index("conversation_app_from_user_idx", "app_id", "from_source", "from_end_user_id"),
  461. )
  462. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  463. app_id = db.Column(StringUUID, nullable=False)
  464. app_model_config_id = db.Column(StringUUID, nullable=True)
  465. model_provider = db.Column(db.String(255), nullable=True)
  466. override_model_configs = db.Column(db.Text)
  467. model_id = db.Column(db.String(255), nullable=True)
  468. mode = db.Column(db.String(255), nullable=False)
  469. name = db.Column(db.String(255), nullable=False)
  470. summary = db.Column(db.Text)
  471. _inputs: Mapped[dict] = mapped_column("inputs", db.JSON)
  472. introduction = db.Column(db.Text)
  473. system_instruction = db.Column(db.Text)
  474. system_instruction_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  475. status = db.Column(db.String(255), nullable=False)
  476. invoke_from = db.Column(db.String(255), nullable=True)
  477. from_source = db.Column(db.String(255), nullable=False)
  478. from_end_user_id = db.Column(StringUUID)
  479. from_account_id = db.Column(StringUUID)
  480. read_at = db.Column(db.DateTime)
  481. read_account_id = db.Column(StringUUID)
  482. dialogue_count: Mapped[int] = mapped_column(default=0)
  483. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  484. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  485. messages = db.relationship("Message", backref="conversation", lazy="select", passive_deletes="all")
  486. message_annotations = db.relationship(
  487. "MessageAnnotation", backref="conversation", lazy="select", passive_deletes="all"
  488. )
  489. is_deleted = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  490. @property
  491. def inputs(self):
  492. inputs = self._inputs.copy()
  493. for key, value in inputs.items():
  494. if isinstance(value, dict) and value.get("dify_model_identity") == FILE_MODEL_IDENTITY:
  495. inputs[key] = File.model_validate(value)
  496. elif isinstance(value, list) and all(
  497. isinstance(item, dict) and item.get("dify_model_identity") == FILE_MODEL_IDENTITY for item in value
  498. ):
  499. inputs[key] = [File.model_validate(item) for item in value]
  500. return inputs
  501. @inputs.setter
  502. def inputs(self, value: Mapping[str, Any]):
  503. inputs = dict(value)
  504. for k, v in inputs.items():
  505. if isinstance(v, File):
  506. inputs[k] = v.model_dump()
  507. elif isinstance(v, list) and all(isinstance(item, File) for item in v):
  508. inputs[k] = [item.model_dump() for item in v]
  509. self._inputs = inputs
  510. @property
  511. def model_config(self):
  512. model_config = {}
  513. if self.mode == AppMode.ADVANCED_CHAT.value:
  514. if self.override_model_configs:
  515. override_model_configs = json.loads(self.override_model_configs)
  516. model_config = override_model_configs
  517. else:
  518. if self.override_model_configs:
  519. override_model_configs = json.loads(self.override_model_configs)
  520. if "model" in override_model_configs:
  521. app_model_config = AppModelConfig()
  522. app_model_config = app_model_config.from_model_config_dict(override_model_configs)
  523. model_config = app_model_config.to_dict()
  524. else:
  525. model_config["configs"] = override_model_configs
  526. else:
  527. app_model_config = (
  528. db.session.query(AppModelConfig).filter(AppModelConfig.id == self.app_model_config_id).first()
  529. )
  530. model_config = app_model_config.to_dict()
  531. model_config["model_id"] = self.model_id
  532. model_config["provider"] = self.model_provider
  533. return model_config
  534. @property
  535. def summary_or_query(self):
  536. if self.summary:
  537. return self.summary
  538. else:
  539. first_message = self.first_message
  540. if first_message:
  541. return first_message.query
  542. else:
  543. return ""
  544. @property
  545. def annotated(self):
  546. return db.session.query(MessageAnnotation).filter(MessageAnnotation.conversation_id == self.id).count() > 0
  547. @property
  548. def annotation(self):
  549. return db.session.query(MessageAnnotation).filter(MessageAnnotation.conversation_id == self.id).first()
  550. @property
  551. def message_count(self):
  552. return db.session.query(Message).filter(Message.conversation_id == self.id).count()
  553. @property
  554. def user_feedback_stats(self):
  555. like = (
  556. db.session.query(MessageFeedback)
  557. .filter(
  558. MessageFeedback.conversation_id == self.id,
  559. MessageFeedback.from_source == "user",
  560. MessageFeedback.rating == "like",
  561. )
  562. .count()
  563. )
  564. dislike = (
  565. db.session.query(MessageFeedback)
  566. .filter(
  567. MessageFeedback.conversation_id == self.id,
  568. MessageFeedback.from_source == "user",
  569. MessageFeedback.rating == "dislike",
  570. )
  571. .count()
  572. )
  573. return {"like": like, "dislike": dislike}
  574. @property
  575. def admin_feedback_stats(self):
  576. like = (
  577. db.session.query(MessageFeedback)
  578. .filter(
  579. MessageFeedback.conversation_id == self.id,
  580. MessageFeedback.from_source == "admin",
  581. MessageFeedback.rating == "like",
  582. )
  583. .count()
  584. )
  585. dislike = (
  586. db.session.query(MessageFeedback)
  587. .filter(
  588. MessageFeedback.conversation_id == self.id,
  589. MessageFeedback.from_source == "admin",
  590. MessageFeedback.rating == "dislike",
  591. )
  592. .count()
  593. )
  594. return {"like": like, "dislike": dislike}
  595. @property
  596. def first_message(self):
  597. return db.session.query(Message).filter(Message.conversation_id == self.id).first()
  598. @property
  599. def app(self):
  600. return db.session.query(App).filter(App.id == self.app_id).first()
  601. @property
  602. def from_end_user_session_id(self):
  603. if self.from_end_user_id:
  604. end_user = db.session.query(EndUser).filter(EndUser.id == self.from_end_user_id).first()
  605. if end_user:
  606. return end_user.session_id
  607. return None
  608. @property
  609. def from_account_name(self):
  610. if self.from_account_id:
  611. account = db.session.query(Account).filter(Account.id == self.from_account_id).first()
  612. if account:
  613. return account.name
  614. return None
  615. @property
  616. def in_debug_mode(self):
  617. return self.override_model_configs is not None
  618. class Message(db.Model):
  619. __tablename__ = "messages"
  620. __table_args__ = (
  621. db.PrimaryKeyConstraint("id", name="message_pkey"),
  622. db.Index("message_app_id_idx", "app_id", "created_at"),
  623. db.Index("message_conversation_id_idx", "conversation_id"),
  624. db.Index("message_end_user_idx", "app_id", "from_source", "from_end_user_id"),
  625. db.Index("message_account_idx", "app_id", "from_source", "from_account_id"),
  626. db.Index("message_workflow_run_id_idx", "conversation_id", "workflow_run_id"),
  627. )
  628. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  629. app_id = db.Column(StringUUID, nullable=False)
  630. model_provider = db.Column(db.String(255), nullable=True)
  631. model_id = db.Column(db.String(255), nullable=True)
  632. override_model_configs = db.Column(db.Text)
  633. conversation_id = db.Column(StringUUID, db.ForeignKey("conversations.id"), nullable=False)
  634. _inputs: Mapped[dict] = mapped_column("inputs", db.JSON)
  635. query: Mapped[str] = db.Column(db.Text, nullable=False)
  636. message = db.Column(db.JSON, nullable=False)
  637. message_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  638. message_unit_price = db.Column(db.Numeric(10, 4), nullable=False)
  639. message_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
  640. answer: Mapped[str] = db.Column(db.Text, nullable=False)
  641. answer_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  642. answer_unit_price = db.Column(db.Numeric(10, 4), nullable=False)
  643. answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
  644. parent_message_id = db.Column(StringUUID, nullable=True)
  645. provider_response_latency = db.Column(db.Float, nullable=False, server_default=db.text("0"))
  646. total_price = db.Column(db.Numeric(10, 7))
  647. currency = db.Column(db.String(255), nullable=False)
  648. status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying"))
  649. error = db.Column(db.Text)
  650. message_metadata = db.Column(db.Text)
  651. invoke_from: Mapped[Optional[str]] = db.Column(db.String(255), nullable=True)
  652. from_source = db.Column(db.String(255), nullable=False)
  653. from_end_user_id: Mapped[Optional[str]] = db.Column(StringUUID)
  654. from_account_id: Mapped[Optional[str]] = db.Column(StringUUID)
  655. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  656. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  657. agent_based = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  658. workflow_run_id = db.Column(StringUUID)
  659. @property
  660. def inputs(self):
  661. inputs = self._inputs.copy()
  662. for key, value in inputs.items():
  663. if isinstance(value, dict) and value.get("dify_model_identity") == FILE_MODEL_IDENTITY:
  664. inputs[key] = File.model_validate(value)
  665. elif isinstance(value, list) and all(
  666. isinstance(item, dict) and item.get("dify_model_identity") == FILE_MODEL_IDENTITY for item in value
  667. ):
  668. inputs[key] = [File.model_validate(item) for item in value]
  669. return inputs
  670. @inputs.setter
  671. def inputs(self, value: Mapping[str, Any]):
  672. inputs = dict(value)
  673. for k, v in inputs.items():
  674. if isinstance(v, File):
  675. inputs[k] = v.model_dump()
  676. elif isinstance(v, list) and all(isinstance(item, File) for item in v):
  677. inputs[k] = [item.model_dump() for item in v]
  678. self._inputs = inputs
  679. @property
  680. def re_sign_file_url_answer(self) -> str:
  681. if not self.answer:
  682. return self.answer
  683. pattern = r"\[!?.*?\]\((((http|https):\/\/.+)?\/files\/(tools\/)?[\w-]+.*?timestamp=.*&nonce=.*&sign=.*)\)"
  684. matches = re.findall(pattern, self.answer)
  685. if not matches:
  686. return self.answer
  687. urls = [match[0] for match in matches]
  688. # remove duplicate urls
  689. urls = list(set(urls))
  690. if not urls:
  691. return self.answer
  692. re_sign_file_url_answer = self.answer
  693. for url in urls:
  694. if "files/tools" in url:
  695. # get tool file id
  696. tool_file_id_pattern = r"\/files\/tools\/([\.\w-]+)?\?timestamp="
  697. result = re.search(tool_file_id_pattern, url)
  698. if not result:
  699. continue
  700. tool_file_id = result.group(1)
  701. # get extension
  702. if "." in tool_file_id:
  703. split_result = tool_file_id.split(".")
  704. extension = f".{split_result[-1]}"
  705. if len(extension) > 10:
  706. extension = ".bin"
  707. tool_file_id = split_result[0]
  708. else:
  709. extension = ".bin"
  710. if not tool_file_id:
  711. continue
  712. sign_url = ToolFileParser.get_tool_file_manager().sign_file(
  713. tool_file_id=tool_file_id, extension=extension
  714. )
  715. elif "file-preview" in url:
  716. # get upload file id
  717. upload_file_id_pattern = r"\/files\/([\w-]+)\/file-preview?\?timestamp="
  718. result = re.search(upload_file_id_pattern, url)
  719. if not result:
  720. continue
  721. upload_file_id = result.group(1)
  722. if not upload_file_id:
  723. continue
  724. sign_url = file_helpers.get_signed_file_url(upload_file_id)
  725. elif "image-preview" in url:
  726. # image-preview is deprecated, use file-preview instead
  727. upload_file_id_pattern = r"\/files\/([\w-]+)\/image-preview?\?timestamp="
  728. result = re.search(upload_file_id_pattern, url)
  729. if not result:
  730. continue
  731. upload_file_id = result.group(1)
  732. if not upload_file_id:
  733. continue
  734. sign_url = file_helpers.get_signed_file_url(upload_file_id)
  735. else:
  736. continue
  737. re_sign_file_url_answer = re_sign_file_url_answer.replace(url, sign_url)
  738. return re_sign_file_url_answer
  739. @property
  740. def user_feedback(self):
  741. feedback = (
  742. db.session.query(MessageFeedback)
  743. .filter(MessageFeedback.message_id == self.id, MessageFeedback.from_source == "user")
  744. .first()
  745. )
  746. return feedback
  747. @property
  748. def admin_feedback(self):
  749. feedback = (
  750. db.session.query(MessageFeedback)
  751. .filter(MessageFeedback.message_id == self.id, MessageFeedback.from_source == "admin")
  752. .first()
  753. )
  754. return feedback
  755. @property
  756. def feedbacks(self):
  757. feedbacks = db.session.query(MessageFeedback).filter(MessageFeedback.message_id == self.id).all()
  758. return feedbacks
  759. @property
  760. def annotation(self):
  761. annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.message_id == self.id).first()
  762. return annotation
  763. @property
  764. def annotation_hit_history(self):
  765. annotation_history = (
  766. db.session.query(AppAnnotationHitHistory).filter(AppAnnotationHitHistory.message_id == self.id).first()
  767. )
  768. if annotation_history:
  769. annotation = (
  770. db.session.query(MessageAnnotation)
  771. .filter(MessageAnnotation.id == annotation_history.annotation_id)
  772. .first()
  773. )
  774. return annotation
  775. return None
  776. @property
  777. def app_model_config(self):
  778. conversation = db.session.query(Conversation).filter(Conversation.id == self.conversation_id).first()
  779. if conversation:
  780. return (
  781. db.session.query(AppModelConfig).filter(AppModelConfig.id == conversation.app_model_config_id).first()
  782. )
  783. return None
  784. @property
  785. def in_debug_mode(self):
  786. return self.override_model_configs is not None
  787. @property
  788. def message_metadata_dict(self) -> dict:
  789. return json.loads(self.message_metadata) if self.message_metadata else {}
  790. @property
  791. def agent_thoughts(self):
  792. return (
  793. db.session.query(MessageAgentThought)
  794. .filter(MessageAgentThought.message_id == self.id)
  795. .order_by(MessageAgentThought.position.asc())
  796. .all()
  797. )
  798. @property
  799. def retriever_resources(self):
  800. return (
  801. db.session.query(DatasetRetrieverResource)
  802. .filter(DatasetRetrieverResource.message_id == self.id)
  803. .order_by(DatasetRetrieverResource.position.asc())
  804. .all()
  805. )
  806. @property
  807. def message_files(self):
  808. from factories import file_factory
  809. message_files = db.session.query(MessageFile).filter(MessageFile.message_id == self.id).all()
  810. current_app = db.session.query(App).filter(App.id == self.app_id).first()
  811. if not current_app:
  812. raise ValueError(f"App {self.app_id} not found")
  813. files: list[File] = []
  814. for message_file in message_files:
  815. if message_file.transfer_method == "local_file":
  816. if message_file.upload_file_id is None:
  817. raise ValueError(f"MessageFile {message_file.id} is a local file but has no upload_file_id")
  818. file = file_factory.build_from_mapping(
  819. mapping={
  820. "id": message_file.id,
  821. "upload_file_id": message_file.upload_file_id,
  822. "transfer_method": message_file.transfer_method,
  823. "type": message_file.type,
  824. },
  825. tenant_id=current_app.tenant_id,
  826. user_id=self.from_account_id or self.from_end_user_id or "",
  827. role=CreatedByRole(message_file.created_by_role),
  828. config=FileExtraConfig(),
  829. )
  830. elif message_file.transfer_method == "remote_url":
  831. if message_file.url is None:
  832. raise ValueError(f"MessageFile {message_file.id} is a remote url but has no url")
  833. file = file_factory.build_from_mapping(
  834. mapping={
  835. "id": message_file.id,
  836. "type": message_file.type,
  837. "transfer_method": message_file.transfer_method,
  838. "url": message_file.url,
  839. },
  840. tenant_id=current_app.tenant_id,
  841. user_id=self.from_account_id or self.from_end_user_id or "",
  842. role=CreatedByRole(message_file.created_by_role),
  843. config=FileExtraConfig(),
  844. )
  845. elif message_file.transfer_method == "tool_file":
  846. if message_file.upload_file_id is None:
  847. assert message_file.url is not None
  848. message_file.upload_file_id = message_file.url.split("/")[-1].split(".")[0]
  849. mapping = {
  850. "id": message_file.id,
  851. "type": message_file.type,
  852. "transfer_method": message_file.transfer_method,
  853. "tool_file_id": message_file.upload_file_id,
  854. }
  855. file = file_factory.build_from_mapping(
  856. mapping=mapping,
  857. tenant_id=current_app.tenant_id,
  858. user_id=self.from_account_id or self.from_end_user_id or "",
  859. role=CreatedByRole(message_file.created_by_role),
  860. config=FileExtraConfig(),
  861. )
  862. else:
  863. raise ValueError(
  864. f"MessageFile {message_file.id} has an invalid transfer_method {message_file.transfer_method}"
  865. )
  866. files.append(file)
  867. result = [
  868. {"belongs_to": message_file.belongs_to, **file.to_dict()}
  869. for (file, message_file) in zip(files, message_files)
  870. ]
  871. db.session.commit()
  872. return result
  873. @property
  874. def workflow_run(self):
  875. if self.workflow_run_id:
  876. from .workflow import WorkflowRun
  877. return db.session.query(WorkflowRun).filter(WorkflowRun.id == self.workflow_run_id).first()
  878. return None
  879. def to_dict(self) -> dict:
  880. return {
  881. "id": self.id,
  882. "app_id": self.app_id,
  883. "conversation_id": self.conversation_id,
  884. "inputs": self.inputs,
  885. "query": self.query,
  886. "message": self.message,
  887. "answer": self.answer,
  888. "status": self.status,
  889. "error": self.error,
  890. "message_metadata": self.message_metadata_dict,
  891. "from_source": self.from_source,
  892. "from_end_user_id": self.from_end_user_id,
  893. "from_account_id": self.from_account_id,
  894. "created_at": self.created_at.isoformat(),
  895. "updated_at": self.updated_at.isoformat(),
  896. "agent_based": self.agent_based,
  897. "workflow_run_id": self.workflow_run_id,
  898. }
  899. @classmethod
  900. def from_dict(cls, data: dict):
  901. return cls(
  902. id=data["id"],
  903. app_id=data["app_id"],
  904. conversation_id=data["conversation_id"],
  905. inputs=data["inputs"],
  906. query=data["query"],
  907. message=data["message"],
  908. answer=data["answer"],
  909. status=data["status"],
  910. error=data["error"],
  911. message_metadata=json.dumps(data["message_metadata"]),
  912. from_source=data["from_source"],
  913. from_end_user_id=data["from_end_user_id"],
  914. from_account_id=data["from_account_id"],
  915. created_at=data["created_at"],
  916. updated_at=data["updated_at"],
  917. agent_based=data["agent_based"],
  918. workflow_run_id=data["workflow_run_id"],
  919. )
  920. class MessageFeedback(db.Model):
  921. __tablename__ = "message_feedbacks"
  922. __table_args__ = (
  923. db.PrimaryKeyConstraint("id", name="message_feedback_pkey"),
  924. db.Index("message_feedback_app_idx", "app_id"),
  925. db.Index("message_feedback_message_idx", "message_id", "from_source"),
  926. db.Index("message_feedback_conversation_idx", "conversation_id", "from_source", "rating"),
  927. )
  928. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  929. app_id = db.Column(StringUUID, nullable=False)
  930. conversation_id = db.Column(StringUUID, nullable=False)
  931. message_id = db.Column(StringUUID, nullable=False)
  932. rating = db.Column(db.String(255), nullable=False)
  933. content = db.Column(db.Text)
  934. from_source = db.Column(db.String(255), nullable=False)
  935. from_end_user_id = db.Column(StringUUID)
  936. from_account_id = db.Column(StringUUID)
  937. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  938. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  939. @property
  940. def from_account(self):
  941. account = db.session.query(Account).filter(Account.id == self.from_account_id).first()
  942. return account
  943. class MessageFile(db.Model):
  944. __tablename__ = "message_files"
  945. __table_args__ = (
  946. db.PrimaryKeyConstraint("id", name="message_file_pkey"),
  947. db.Index("message_file_message_idx", "message_id"),
  948. db.Index("message_file_created_by_idx", "created_by"),
  949. )
  950. def __init__(
  951. self,
  952. *,
  953. message_id: str,
  954. type: FileType,
  955. transfer_method: FileTransferMethod,
  956. url: str | None = None,
  957. belongs_to: Literal["user", "assistant"] | None = None,
  958. upload_file_id: str | None = None,
  959. created_by_role: CreatedByRole,
  960. created_by: str,
  961. ):
  962. self.message_id = message_id
  963. self.type = type
  964. self.transfer_method = transfer_method
  965. self.url = url
  966. self.belongs_to = belongs_to
  967. self.upload_file_id = upload_file_id
  968. self.created_by_role = created_by_role.value
  969. self.created_by = created_by
  970. id: Mapped[str] = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  971. message_id: Mapped[str] = db.Column(StringUUID, nullable=False)
  972. type: Mapped[str] = db.Column(db.String(255), nullable=False)
  973. transfer_method: Mapped[str] = db.Column(db.String(255), nullable=False)
  974. url: Mapped[Optional[str]] = db.Column(db.Text, nullable=True)
  975. belongs_to: Mapped[Optional[str]] = db.Column(db.String(255), nullable=True)
  976. upload_file_id: Mapped[Optional[str]] = db.Column(StringUUID, nullable=True)
  977. created_by_role: Mapped[str] = db.Column(db.String(255), nullable=False)
  978. created_by: Mapped[str] = db.Column(StringUUID, nullable=False)
  979. created_at: Mapped[datetime] = db.Column(
  980. db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
  981. )
  982. class MessageAnnotation(db.Model):
  983. __tablename__ = "message_annotations"
  984. __table_args__ = (
  985. db.PrimaryKeyConstraint("id", name="message_annotation_pkey"),
  986. db.Index("message_annotation_app_idx", "app_id"),
  987. db.Index("message_annotation_conversation_idx", "conversation_id"),
  988. db.Index("message_annotation_message_idx", "message_id"),
  989. )
  990. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  991. app_id = db.Column(StringUUID, nullable=False)
  992. conversation_id = db.Column(StringUUID, db.ForeignKey("conversations.id"), nullable=True)
  993. message_id = db.Column(StringUUID, nullable=True)
  994. question = db.Column(db.Text, nullable=True)
  995. content = db.Column(db.Text, nullable=False)
  996. hit_count = db.Column(db.Integer, nullable=False, server_default=db.text("0"))
  997. account_id = db.Column(StringUUID, nullable=False)
  998. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  999. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1000. @property
  1001. def account(self):
  1002. account = db.session.query(Account).filter(Account.id == self.account_id).first()
  1003. return account
  1004. @property
  1005. def annotation_create_account(self):
  1006. account = db.session.query(Account).filter(Account.id == self.account_id).first()
  1007. return account
  1008. class AppAnnotationHitHistory(db.Model):
  1009. __tablename__ = "app_annotation_hit_histories"
  1010. __table_args__ = (
  1011. db.PrimaryKeyConstraint("id", name="app_annotation_hit_histories_pkey"),
  1012. db.Index("app_annotation_hit_histories_app_idx", "app_id"),
  1013. db.Index("app_annotation_hit_histories_account_idx", "account_id"),
  1014. db.Index("app_annotation_hit_histories_annotation_idx", "annotation_id"),
  1015. db.Index("app_annotation_hit_histories_message_idx", "message_id"),
  1016. )
  1017. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1018. app_id = db.Column(StringUUID, nullable=False)
  1019. annotation_id = db.Column(StringUUID, nullable=False)
  1020. source = db.Column(db.Text, nullable=False)
  1021. question = db.Column(db.Text, nullable=False)
  1022. account_id = db.Column(StringUUID, nullable=False)
  1023. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1024. score = db.Column(Float, nullable=False, server_default=db.text("0"))
  1025. message_id = db.Column(StringUUID, nullable=False)
  1026. annotation_question = db.Column(db.Text, nullable=False)
  1027. annotation_content = db.Column(db.Text, nullable=False)
  1028. @property
  1029. def account(self):
  1030. account = (
  1031. db.session.query(Account)
  1032. .join(MessageAnnotation, MessageAnnotation.account_id == Account.id)
  1033. .filter(MessageAnnotation.id == self.annotation_id)
  1034. .first()
  1035. )
  1036. return account
  1037. @property
  1038. def annotation_create_account(self):
  1039. account = db.session.query(Account).filter(Account.id == self.account_id).first()
  1040. return account
  1041. class AppAnnotationSetting(db.Model):
  1042. __tablename__ = "app_annotation_settings"
  1043. __table_args__ = (
  1044. db.PrimaryKeyConstraint("id", name="app_annotation_settings_pkey"),
  1045. db.Index("app_annotation_settings_app_idx", "app_id"),
  1046. )
  1047. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1048. app_id = db.Column(StringUUID, nullable=False)
  1049. score_threshold = db.Column(Float, nullable=False, server_default=db.text("0"))
  1050. collection_binding_id = db.Column(StringUUID, nullable=False)
  1051. created_user_id = db.Column(StringUUID, nullable=False)
  1052. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1053. updated_user_id = db.Column(StringUUID, nullable=False)
  1054. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1055. @property
  1056. def created_account(self):
  1057. account = (
  1058. db.session.query(Account)
  1059. .join(AppAnnotationSetting, AppAnnotationSetting.created_user_id == Account.id)
  1060. .filter(AppAnnotationSetting.id == self.annotation_id)
  1061. .first()
  1062. )
  1063. return account
  1064. @property
  1065. def updated_account(self):
  1066. account = (
  1067. db.session.query(Account)
  1068. .join(AppAnnotationSetting, AppAnnotationSetting.updated_user_id == Account.id)
  1069. .filter(AppAnnotationSetting.id == self.annotation_id)
  1070. .first()
  1071. )
  1072. return account
  1073. @property
  1074. def collection_binding_detail(self):
  1075. from .dataset import DatasetCollectionBinding
  1076. collection_binding_detail = (
  1077. db.session.query(DatasetCollectionBinding)
  1078. .filter(DatasetCollectionBinding.id == self.collection_binding_id)
  1079. .first()
  1080. )
  1081. return collection_binding_detail
  1082. class OperationLog(db.Model):
  1083. __tablename__ = "operation_logs"
  1084. __table_args__ = (
  1085. db.PrimaryKeyConstraint("id", name="operation_log_pkey"),
  1086. db.Index("operation_log_account_action_idx", "tenant_id", "account_id", "action"),
  1087. )
  1088. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1089. tenant_id = db.Column(StringUUID, nullable=False)
  1090. account_id = db.Column(StringUUID, nullable=False)
  1091. action = db.Column(db.String(255), nullable=False)
  1092. content = db.Column(db.JSON)
  1093. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1094. created_ip = db.Column(db.String(255), nullable=False)
  1095. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1096. class EndUser(UserMixin, db.Model):
  1097. __tablename__ = "end_users"
  1098. __table_args__ = (
  1099. db.PrimaryKeyConstraint("id", name="end_user_pkey"),
  1100. db.Index("end_user_session_id_idx", "session_id", "type"),
  1101. db.Index("end_user_tenant_session_id_idx", "tenant_id", "session_id", "type"),
  1102. )
  1103. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1104. tenant_id = db.Column(StringUUID, nullable=False)
  1105. app_id = db.Column(StringUUID, nullable=True)
  1106. type = db.Column(db.String(255), nullable=False)
  1107. external_user_id = db.Column(db.String(255), nullable=True)
  1108. name = db.Column(db.String(255))
  1109. is_anonymous = db.Column(db.Boolean, nullable=False, server_default=db.text("true"))
  1110. session_id = db.Column(db.String(255), nullable=False)
  1111. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1112. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1113. class Site(db.Model):
  1114. __tablename__ = "sites"
  1115. __table_args__ = (
  1116. db.PrimaryKeyConstraint("id", name="site_pkey"),
  1117. db.Index("site_app_id_idx", "app_id"),
  1118. db.Index("site_code_idx", "code", "status"),
  1119. )
  1120. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1121. app_id = db.Column(StringUUID, nullable=False)
  1122. title = db.Column(db.String(255), nullable=False)
  1123. icon_type = db.Column(db.String(255), nullable=True)
  1124. icon = db.Column(db.String(255))
  1125. icon_background = db.Column(db.String(255))
  1126. description = db.Column(db.Text)
  1127. default_language = db.Column(db.String(255), nullable=False)
  1128. chat_color_theme = db.Column(db.String(255))
  1129. chat_color_theme_inverted = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  1130. copyright = db.Column(db.String(255))
  1131. privacy_policy = db.Column(db.String(255))
  1132. show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text("true"))
  1133. use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  1134. custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="")
  1135. customize_domain = db.Column(db.String(255))
  1136. customize_token_strategy = db.Column(db.String(255), nullable=False)
  1137. prompt_public = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  1138. status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying"))
  1139. created_by = db.Column(StringUUID, nullable=True)
  1140. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1141. updated_by = db.Column(StringUUID, nullable=True)
  1142. updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1143. code = db.Column(db.String(255))
  1144. @staticmethod
  1145. def generate_code(n):
  1146. while True:
  1147. result = generate_string(n)
  1148. while db.session.query(Site).filter(Site.code == result).count() > 0:
  1149. result = generate_string(n)
  1150. return result
  1151. @property
  1152. def app_base_url(self):
  1153. return dify_config.APP_WEB_URL or request.url_root.rstrip("/")
  1154. class ApiToken(db.Model):
  1155. __tablename__ = "api_tokens"
  1156. __table_args__ = (
  1157. db.PrimaryKeyConstraint("id", name="api_token_pkey"),
  1158. db.Index("api_token_app_id_type_idx", "app_id", "type"),
  1159. db.Index("api_token_token_idx", "token", "type"),
  1160. db.Index("api_token_tenant_idx", "tenant_id", "type"),
  1161. )
  1162. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1163. app_id = db.Column(StringUUID, nullable=True)
  1164. tenant_id = db.Column(StringUUID, nullable=True)
  1165. type = db.Column(db.String(16), nullable=False)
  1166. token = db.Column(db.String(255), nullable=False)
  1167. last_used_at = db.Column(db.DateTime, nullable=True)
  1168. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1169. @staticmethod
  1170. def generate_api_key(prefix, n):
  1171. while True:
  1172. result = prefix + generate_string(n)
  1173. while db.session.query(ApiToken).filter(ApiToken.token == result).count() > 0:
  1174. result = prefix + generate_string(n)
  1175. return result
  1176. class UploadFile(db.Model):
  1177. __tablename__ = "upload_files"
  1178. __table_args__ = (
  1179. db.PrimaryKeyConstraint("id", name="upload_file_pkey"),
  1180. db.Index("upload_file_tenant_idx", "tenant_id"),
  1181. )
  1182. id: Mapped[str] = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1183. tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False)
  1184. storage_type: Mapped[str] = db.Column(db.String(255), nullable=False)
  1185. key: Mapped[str] = db.Column(db.String(255), nullable=False)
  1186. name: Mapped[str] = db.Column(db.String(255), nullable=False)
  1187. size: Mapped[int] = db.Column(db.Integer, nullable=False)
  1188. extension: Mapped[str] = db.Column(db.String(255), nullable=False)
  1189. mime_type: Mapped[str] = db.Column(db.String(255), nullable=True)
  1190. created_by_role: Mapped[str] = db.Column(
  1191. db.String(255), nullable=False, server_default=db.text("'account'::character varying")
  1192. )
  1193. created_by: Mapped[str] = db.Column(StringUUID, nullable=False)
  1194. created_at: Mapped[datetime] = db.Column(
  1195. db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
  1196. )
  1197. used: Mapped[bool] = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
  1198. used_by: Mapped[str | None] = db.Column(StringUUID, nullable=True)
  1199. used_at: Mapped[datetime | None] = db.Column(db.DateTime, nullable=True)
  1200. hash: Mapped[str | None] = db.Column(db.String(255), nullable=True)
  1201. source_url: Mapped[str] = mapped_column(sa.TEXT, default="")
  1202. def __init__(
  1203. self,
  1204. *,
  1205. tenant_id: str,
  1206. storage_type: str,
  1207. key: str,
  1208. name: str,
  1209. size: int,
  1210. extension: str,
  1211. mime_type: str,
  1212. created_by_role: CreatedByRole,
  1213. created_by: str,
  1214. created_at: datetime,
  1215. used: bool,
  1216. used_by: str | None = None,
  1217. used_at: datetime | None = None,
  1218. hash: str | None = None,
  1219. source_url: str = "",
  1220. ):
  1221. self.tenant_id = tenant_id
  1222. self.storage_type = storage_type
  1223. self.key = key
  1224. self.name = name
  1225. self.size = size
  1226. self.extension = extension
  1227. self.mime_type = mime_type
  1228. self.created_by_role = created_by_role.value
  1229. self.created_by = created_by
  1230. self.created_at = created_at
  1231. self.used = used
  1232. self.used_by = used_by
  1233. self.used_at = used_at
  1234. self.hash = hash
  1235. self.source_url = source_url
  1236. class ApiRequest(db.Model):
  1237. __tablename__ = "api_requests"
  1238. __table_args__ = (
  1239. db.PrimaryKeyConstraint("id", name="api_request_pkey"),
  1240. db.Index("api_request_token_idx", "tenant_id", "api_token_id"),
  1241. )
  1242. id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
  1243. tenant_id = db.Column(StringUUID, nullable=False)
  1244. api_token_id = db.Column(StringUUID, nullable=False)
  1245. path = db.Column(db.String(255), nullable=False)
  1246. request = db.Column(db.Text, nullable=True)
  1247. response = db.Column(db.Text, nullable=True)
  1248. ip = db.Column(db.String(255), nullable=False)
  1249. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1250. class MessageChain(db.Model):
  1251. __tablename__ = "message_chains"
  1252. __table_args__ = (
  1253. db.PrimaryKeyConstraint("id", name="message_chain_pkey"),
  1254. db.Index("message_chain_message_id_idx", "message_id"),
  1255. )
  1256. id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
  1257. message_id = db.Column(StringUUID, nullable=False)
  1258. type = db.Column(db.String(255), nullable=False)
  1259. input = db.Column(db.Text, nullable=True)
  1260. output = db.Column(db.Text, nullable=True)
  1261. created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
  1262. class MessageAgentThought(db.Model):
  1263. __tablename__ = "message_agent_thoughts"
  1264. __table_args__ = (
  1265. db.PrimaryKeyConstraint("id", name="message_agent_thought_pkey"),
  1266. db.Index("message_agent_thought_message_id_idx", "message_id"),
  1267. db.Index("message_agent_thought_message_chain_id_idx", "message_chain_id"),
  1268. )
  1269. id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
  1270. message_id = db.Column(StringUUID, nullable=False)
  1271. message_chain_id = db.Column(StringUUID, nullable=True)
  1272. position = db.Column(db.Integer, nullable=False)
  1273. thought = db.Column(db.Text, nullable=True)
  1274. tool = db.Column(db.Text, nullable=True)
  1275. tool_labels_str = db.Column(db.Text, nullable=False, server_default=db.text("'{}'::text"))
  1276. tool_meta_str = db.Column(db.Text, nullable=False, server_default=db.text("'{}'::text"))
  1277. tool_input = db.Column(db.Text, nullable=True)
  1278. observation = db.Column(db.Text, nullable=True)
  1279. # plugin_id = db.Column(StringUUID, nullable=True) ## for future design
  1280. tool_process_data = db.Column(db.Text, nullable=True)
  1281. message = db.Column(db.Text, nullable=True)
  1282. message_token = db.Column(db.Integer, nullable=True)
  1283. message_unit_price = db.Column(db.Numeric, nullable=True)
  1284. message_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
  1285. message_files = db.Column(db.Text, nullable=True)
  1286. answer = db.Column(db.Text, nullable=True)
  1287. answer_token = db.Column(db.Integer, nullable=True)
  1288. answer_unit_price = db.Column(db.Numeric, nullable=True)
  1289. answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001"))
  1290. tokens = db.Column(db.Integer, nullable=True)
  1291. total_price = db.Column(db.Numeric, nullable=True)
  1292. currency = db.Column(db.String, nullable=True)
  1293. latency = db.Column(db.Float, nullable=True)
  1294. created_by_role = db.Column(db.String, nullable=False)
  1295. created_by = db.Column(StringUUID, nullable=False)
  1296. created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
  1297. @property
  1298. def files(self) -> list:
  1299. if self.message_files:
  1300. return json.loads(self.message_files)
  1301. else:
  1302. return []
  1303. @property
  1304. def tools(self) -> list[str]:
  1305. return self.tool.split(";") if self.tool else []
  1306. @property
  1307. def tool_labels(self) -> dict:
  1308. try:
  1309. if self.tool_labels_str:
  1310. return json.loads(self.tool_labels_str)
  1311. else:
  1312. return {}
  1313. except Exception as e:
  1314. return {}
  1315. @property
  1316. def tool_meta(self) -> dict:
  1317. try:
  1318. if self.tool_meta_str:
  1319. return json.loads(self.tool_meta_str)
  1320. else:
  1321. return {}
  1322. except Exception as e:
  1323. return {}
  1324. @property
  1325. def tool_inputs_dict(self) -> dict:
  1326. tools = self.tools
  1327. try:
  1328. if self.tool_input:
  1329. data = json.loads(self.tool_input)
  1330. result = {}
  1331. for tool in tools:
  1332. if tool in data:
  1333. result[tool] = data[tool]
  1334. else:
  1335. if len(tools) == 1:
  1336. result[tool] = data
  1337. else:
  1338. result[tool] = {}
  1339. return result
  1340. else:
  1341. return {tool: {} for tool in tools}
  1342. except Exception as e:
  1343. return {}
  1344. @property
  1345. def tool_outputs_dict(self) -> dict:
  1346. tools = self.tools
  1347. try:
  1348. if self.observation:
  1349. data = json.loads(self.observation)
  1350. result = {}
  1351. for tool in tools:
  1352. if tool in data:
  1353. result[tool] = data[tool]
  1354. else:
  1355. if len(tools) == 1:
  1356. result[tool] = data
  1357. else:
  1358. result[tool] = {}
  1359. return result
  1360. else:
  1361. return {tool: {} for tool in tools}
  1362. except Exception as e:
  1363. if self.observation:
  1364. return dict.fromkeys(tools, self.observation)
  1365. class DatasetRetrieverResource(db.Model):
  1366. __tablename__ = "dataset_retriever_resources"
  1367. __table_args__ = (
  1368. db.PrimaryKeyConstraint("id", name="dataset_retriever_resource_pkey"),
  1369. db.Index("dataset_retriever_resource_message_id_idx", "message_id"),
  1370. )
  1371. id = db.Column(StringUUID, nullable=False, server_default=db.text("uuid_generate_v4()"))
  1372. message_id = db.Column(StringUUID, nullable=False)
  1373. position = db.Column(db.Integer, nullable=False)
  1374. dataset_id = db.Column(StringUUID, nullable=False)
  1375. dataset_name = db.Column(db.Text, nullable=False)
  1376. document_id = db.Column(StringUUID, nullable=True)
  1377. document_name = db.Column(db.Text, nullable=False)
  1378. data_source_type = db.Column(db.Text, nullable=True)
  1379. segment_id = db.Column(StringUUID, nullable=True)
  1380. score = db.Column(db.Float, nullable=True)
  1381. content = db.Column(db.Text, nullable=False)
  1382. hit_count = db.Column(db.Integer, nullable=True)
  1383. word_count = db.Column(db.Integer, nullable=True)
  1384. segment_position = db.Column(db.Integer, nullable=True)
  1385. index_node_hash = db.Column(db.Text, nullable=True)
  1386. retriever_from = db.Column(db.Text, nullable=False)
  1387. created_by = db.Column(StringUUID, nullable=False)
  1388. created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.current_timestamp())
  1389. class Tag(db.Model):
  1390. __tablename__ = "tags"
  1391. __table_args__ = (
  1392. db.PrimaryKeyConstraint("id", name="tag_pkey"),
  1393. db.Index("tag_type_idx", "type"),
  1394. db.Index("tag_name_idx", "name"),
  1395. )
  1396. TAG_TYPE_LIST = ["knowledge", "app"]
  1397. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1398. tenant_id = db.Column(StringUUID, nullable=True)
  1399. type = db.Column(db.String(16), nullable=False)
  1400. name = db.Column(db.String(255), nullable=False)
  1401. created_by = db.Column(StringUUID, nullable=False)
  1402. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1403. class TagBinding(db.Model):
  1404. __tablename__ = "tag_bindings"
  1405. __table_args__ = (
  1406. db.PrimaryKeyConstraint("id", name="tag_binding_pkey"),
  1407. db.Index("tag_bind_target_id_idx", "target_id"),
  1408. db.Index("tag_bind_tag_id_idx", "tag_id"),
  1409. )
  1410. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1411. tenant_id = db.Column(StringUUID, nullable=True)
  1412. tag_id = db.Column(StringUUID, nullable=True)
  1413. target_id = db.Column(StringUUID, nullable=True)
  1414. created_by = db.Column(StringUUID, nullable=False)
  1415. created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
  1416. class TraceAppConfig(db.Model):
  1417. __tablename__ = "trace_app_config"
  1418. __table_args__ = (
  1419. db.PrimaryKeyConstraint("id", name="tracing_app_config_pkey"),
  1420. db.Index("trace_app_config_app_id_idx", "app_id"),
  1421. )
  1422. id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
  1423. app_id = db.Column(StringUUID, nullable=False)
  1424. tracing_provider = db.Column(db.String(255), nullable=True)
  1425. tracing_config = db.Column(db.JSON, nullable=True)
  1426. created_at = db.Column(db.DateTime, nullable=False, server_default=func.now())
  1427. updated_at = db.Column(db.DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
  1428. is_active = db.Column(db.Boolean, nullable=False, server_default=db.text("true"))
  1429. @property
  1430. def tracing_config_dict(self):
  1431. return self.tracing_config or {}
  1432. @property
  1433. def tracing_config_str(self):
  1434. return json.dumps(self.tracing_config_dict)
  1435. def to_dict(self):
  1436. return {
  1437. "id": self.id,
  1438. "app_id": self.app_id,
  1439. "tracing_provider": self.tracing_provider,
  1440. "tracing_config": self.tracing_config_dict,
  1441. "is_active": self.is_active,
  1442. "created_at": str(self.created_at) if self.created_at else None,
  1443. "updated_at": str(self.updated_at) if self.updated_at else None,
  1444. }