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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946
  1. import json
  2. import re
  3. import uuid
  4. from collections.abc import Mapping
  5. from datetime import datetime
  6. from enum import StrEnum, auto
  7. from typing import TYPE_CHECKING, Any, Literal, Optional, cast
  8. import sqlalchemy as sa
  9. from flask import request
  10. from flask_login import UserMixin # type: ignore[import-untyped]
  11. from sqlalchemy import Float, Index, PrimaryKeyConstraint, String, exists, func, select, text
  12. from sqlalchemy.orm import Mapped, Session, mapped_column
  13. from configs import dify_config
  14. from constants import DEFAULT_FILE_NUMBER_LIMITS
  15. from core.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType
  16. from core.file import helpers as file_helpers
  17. from core.tools.signature import sign_tool_file
  18. from core.workflow.enums import WorkflowExecutionStatus
  19. from libs.helper import generate_string # type: ignore[import-not-found]
  20. from .account import Account, Tenant
  21. from .base import Base
  22. from .engine import db
  23. from .enums import CreatorUserRole
  24. from .provider_ids import GenericProviderID
  25. from .types import StringUUID
  26. if TYPE_CHECKING:
  27. from models.workflow import Workflow
  28. class DifySetup(Base):
  29. __tablename__ = "dify_setups"
  30. __table_args__ = (sa.PrimaryKeyConstraint("version", name="dify_setup_pkey"),)
  31. version: Mapped[str] = mapped_column(String(255), nullable=False)
  32. setup_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  33. class AppMode(StrEnum):
  34. COMPLETION = "completion"
  35. WORKFLOW = "workflow"
  36. CHAT = "chat"
  37. ADVANCED_CHAT = "advanced-chat"
  38. AGENT_CHAT = "agent-chat"
  39. CHANNEL = "channel"
  40. RAG_PIPELINE = "rag-pipeline"
  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(StrEnum):
  53. IMAGE = auto()
  54. EMOJI = auto()
  55. class App(Base):
  56. __tablename__ = "apps"
  57. __table_args__ = (sa.PrimaryKeyConstraint("id", name="app_pkey"), sa.Index("app_tenant_id_idx", "tenant_id"))
  58. id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  59. tenant_id: Mapped[str] = mapped_column(StringUUID)
  60. name: Mapped[str] = mapped_column(String(255))
  61. description: Mapped[str] = mapped_column(sa.Text, server_default=sa.text("''::character varying"))
  62. mode: Mapped[str] = mapped_column(String(255))
  63. icon_type: Mapped[str | None] = mapped_column(String(255)) # image, emoji
  64. icon = mapped_column(String(255))
  65. icon_background: Mapped[str | None] = mapped_column(String(255))
  66. app_model_config_id = mapped_column(StringUUID, nullable=True)
  67. workflow_id = mapped_column(StringUUID, nullable=True)
  68. status: Mapped[str] = mapped_column(String(255), server_default=sa.text("'normal'::character varying"))
  69. enable_site: Mapped[bool] = mapped_column(sa.Boolean)
  70. enable_api: Mapped[bool] = mapped_column(sa.Boolean)
  71. api_rpm: Mapped[int] = mapped_column(sa.Integer, server_default=sa.text("0"))
  72. api_rph: Mapped[int] = mapped_column(sa.Integer, server_default=sa.text("0"))
  73. is_demo: Mapped[bool] = mapped_column(sa.Boolean, server_default=sa.text("false"))
  74. is_public: Mapped[bool] = mapped_column(sa.Boolean, server_default=sa.text("false"))
  75. is_universal: Mapped[bool] = mapped_column(sa.Boolean, server_default=sa.text("false"))
  76. tracing = mapped_column(sa.Text, nullable=True)
  77. max_active_requests: Mapped[int | None]
  78. created_by = mapped_column(StringUUID, nullable=True)
  79. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  80. updated_by = mapped_column(StringUUID, nullable=True)
  81. updated_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  82. use_icon_as_answer_icon: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  83. @property
  84. def desc_or_prompt(self) -> str:
  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) -> Optional["Site"]:
  95. site = db.session.query(Site).where(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).where(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).where(Workflow.id == self.workflow_id).first()
  107. return None
  108. @property
  109. def api_base_url(self) -> str:
  110. return (dify_config.SERVICE_API_URL or request.host_url.rstrip("/")) + "/v1"
  111. @property
  112. def tenant(self) -> Tenant | None:
  113. tenant = db.session.query(Tenant).where(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 app_model_config.agent_mode_dict.get("enabled", False) and app_model_config.agent_mode_dict.get(
  123. "strategy", ""
  124. ) in {"function_call", "react"}:
  125. self.mode = AppMode.AGENT_CHAT
  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 and self.is_agent:
  132. return AppMode.AGENT_CHAT
  133. return str(self.mode)
  134. @property
  135. def deleted_tools(self) -> list[dict[str, str]]:
  136. from core.tools.tool_manager import ToolManager, ToolProviderType
  137. from services.plugin.plugin_service import PluginService
  138. # get agent mode tools
  139. app_model_config = self.app_model_config
  140. if not app_model_config:
  141. return []
  142. if not app_model_config.agent_mode:
  143. return []
  144. agent_mode = app_model_config.agent_mode_dict
  145. tools = agent_mode.get("tools", [])
  146. api_provider_ids: list[str] = []
  147. builtin_provider_ids: list[GenericProviderID] = []
  148. for tool in tools:
  149. keys = list(tool.keys())
  150. if len(keys) >= 4:
  151. provider_type = tool.get("provider_type", "")
  152. provider_id = tool.get("provider_id", "")
  153. if provider_type == ToolProviderType.API.value:
  154. try:
  155. uuid.UUID(provider_id)
  156. except Exception:
  157. continue
  158. api_provider_ids.append(provider_id)
  159. if provider_type == ToolProviderType.BUILT_IN.value:
  160. try:
  161. # check if it's hardcoded
  162. try:
  163. ToolManager.get_hardcoded_provider(provider_id)
  164. is_hardcoded = True
  165. except Exception:
  166. is_hardcoded = False
  167. provider_id = GenericProviderID(provider_id, is_hardcoded)
  168. except Exception:
  169. continue
  170. builtin_provider_ids.append(provider_id)
  171. if not api_provider_ids and not builtin_provider_ids:
  172. return []
  173. with Session(db.engine) as session:
  174. if api_provider_ids:
  175. existing_api_providers = [
  176. api_provider.id
  177. for api_provider in session.execute(
  178. text("SELECT id FROM tool_api_providers WHERE id IN :provider_ids"),
  179. {"provider_ids": tuple(api_provider_ids)},
  180. ).fetchall()
  181. ]
  182. else:
  183. existing_api_providers = []
  184. if builtin_provider_ids:
  185. # get the non-hardcoded builtin providers
  186. non_hardcoded_builtin_providers = [
  187. provider_id for provider_id in builtin_provider_ids if not provider_id.is_hardcoded
  188. ]
  189. if non_hardcoded_builtin_providers:
  190. existence = list(PluginService.check_tools_existence(self.tenant_id, non_hardcoded_builtin_providers))
  191. else:
  192. existence = []
  193. # add the hardcoded builtin providers
  194. existence.extend([True] * (len(builtin_provider_ids) - len(non_hardcoded_builtin_providers)))
  195. builtin_provider_ids = non_hardcoded_builtin_providers + [
  196. provider_id for provider_id in builtin_provider_ids if provider_id.is_hardcoded
  197. ]
  198. else:
  199. existence = []
  200. existing_builtin_providers = {
  201. provider_id.provider_name: existence[i] for i, provider_id in enumerate(builtin_provider_ids)
  202. }
  203. deleted_tools: list[dict[str, str]] = []
  204. for tool in tools:
  205. keys = list(tool.keys())
  206. if len(keys) >= 4:
  207. provider_type = tool.get("provider_type", "")
  208. provider_id = tool.get("provider_id", "")
  209. if provider_type == ToolProviderType.API.value:
  210. if uuid.UUID(provider_id) not in existing_api_providers:
  211. deleted_tools.append(
  212. {
  213. "type": ToolProviderType.API.value,
  214. "tool_name": tool["tool_name"],
  215. "provider_id": provider_id,
  216. }
  217. )
  218. if provider_type == ToolProviderType.BUILT_IN.value:
  219. generic_provider_id = GenericProviderID(provider_id)
  220. if not existing_builtin_providers[generic_provider_id.provider_name]:
  221. deleted_tools.append(
  222. {
  223. "type": ToolProviderType.BUILT_IN.value,
  224. "tool_name": tool["tool_name"],
  225. "provider_id": provider_id, # use the original one
  226. }
  227. )
  228. return deleted_tools
  229. @property
  230. def tags(self) -> list["Tag"]:
  231. tags = (
  232. db.session.query(Tag)
  233. .join(TagBinding, Tag.id == TagBinding.tag_id)
  234. .where(
  235. TagBinding.target_id == self.id,
  236. TagBinding.tenant_id == self.tenant_id,
  237. Tag.tenant_id == self.tenant_id,
  238. Tag.type == "app",
  239. )
  240. .all()
  241. )
  242. return tags or []
  243. @property
  244. def author_name(self) -> str | None:
  245. if self.created_by:
  246. account = db.session.query(Account).where(Account.id == self.created_by).first()
  247. if account:
  248. return account.name
  249. return None
  250. class AppModelConfig(Base):
  251. __tablename__ = "app_model_configs"
  252. __table_args__ = (sa.PrimaryKeyConstraint("id", name="app_model_config_pkey"), sa.Index("app_app_id_idx", "app_id"))
  253. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  254. app_id = mapped_column(StringUUID, nullable=False)
  255. provider = mapped_column(String(255), nullable=True)
  256. model_id = mapped_column(String(255), nullable=True)
  257. configs = mapped_column(sa.JSON, nullable=True)
  258. created_by = mapped_column(StringUUID, nullable=True)
  259. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  260. updated_by = mapped_column(StringUUID, nullable=True)
  261. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  262. opening_statement = mapped_column(sa.Text)
  263. suggested_questions = mapped_column(sa.Text)
  264. suggested_questions_after_answer = mapped_column(sa.Text)
  265. speech_to_text = mapped_column(sa.Text)
  266. text_to_speech = mapped_column(sa.Text)
  267. more_like_this = mapped_column(sa.Text)
  268. model = mapped_column(sa.Text)
  269. user_input_form = mapped_column(sa.Text)
  270. dataset_query_variable = mapped_column(String(255))
  271. pre_prompt = mapped_column(sa.Text)
  272. agent_mode = mapped_column(sa.Text)
  273. sensitive_word_avoidance = mapped_column(sa.Text)
  274. retriever_resource = mapped_column(sa.Text)
  275. prompt_type = mapped_column(String(255), nullable=False, server_default=sa.text("'simple'::character varying"))
  276. chat_prompt_config = mapped_column(sa.Text)
  277. completion_prompt_config = mapped_column(sa.Text)
  278. dataset_configs = mapped_column(sa.Text)
  279. external_data_tools = mapped_column(sa.Text)
  280. file_upload = mapped_column(sa.Text)
  281. @property
  282. def app(self) -> App | None:
  283. app = db.session.query(App).where(App.id == self.app_id).first()
  284. return app
  285. @property
  286. def model_dict(self) -> dict[str, Any]:
  287. return json.loads(self.model) if self.model else {}
  288. @property
  289. def suggested_questions_list(self) -> list[str]:
  290. return json.loads(self.suggested_questions) if self.suggested_questions else []
  291. @property
  292. def suggested_questions_after_answer_dict(self) -> dict[str, Any]:
  293. return (
  294. json.loads(self.suggested_questions_after_answer)
  295. if self.suggested_questions_after_answer
  296. else {"enabled": False}
  297. )
  298. @property
  299. def speech_to_text_dict(self) -> dict[str, Any]:
  300. return json.loads(self.speech_to_text) if self.speech_to_text else {"enabled": False}
  301. @property
  302. def text_to_speech_dict(self) -> dict[str, Any]:
  303. return json.loads(self.text_to_speech) if self.text_to_speech else {"enabled": False}
  304. @property
  305. def retriever_resource_dict(self) -> dict[str, Any]:
  306. return json.loads(self.retriever_resource) if self.retriever_resource else {"enabled": True}
  307. @property
  308. def annotation_reply_dict(self) -> dict[str, Any]:
  309. annotation_setting = (
  310. db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == self.app_id).first()
  311. )
  312. if annotation_setting:
  313. collection_binding_detail = annotation_setting.collection_binding_detail
  314. if not collection_binding_detail:
  315. raise ValueError("Collection binding detail not found")
  316. return {
  317. "id": annotation_setting.id,
  318. "enabled": True,
  319. "score_threshold": annotation_setting.score_threshold,
  320. "embedding_model": {
  321. "embedding_provider_name": collection_binding_detail.provider_name,
  322. "embedding_model_name": collection_binding_detail.model_name,
  323. },
  324. }
  325. else:
  326. return {"enabled": False}
  327. @property
  328. def more_like_this_dict(self) -> dict[str, Any]:
  329. return json.loads(self.more_like_this) if self.more_like_this else {"enabled": False}
  330. @property
  331. def sensitive_word_avoidance_dict(self) -> dict[str, Any]:
  332. return (
  333. json.loads(self.sensitive_word_avoidance)
  334. if self.sensitive_word_avoidance
  335. else {"enabled": False, "type": "", "configs": []}
  336. )
  337. @property
  338. def external_data_tools_list(self) -> list[dict[str, Any]]:
  339. return json.loads(self.external_data_tools) if self.external_data_tools else []
  340. @property
  341. def user_input_form_list(self) -> list[dict[str, Any]]:
  342. return json.loads(self.user_input_form) if self.user_input_form else []
  343. @property
  344. def agent_mode_dict(self) -> dict[str, Any]:
  345. return (
  346. json.loads(self.agent_mode)
  347. if self.agent_mode
  348. else {"enabled": False, "strategy": None, "tools": [], "prompt": None}
  349. )
  350. @property
  351. def chat_prompt_config_dict(self) -> dict[str, Any]:
  352. return json.loads(self.chat_prompt_config) if self.chat_prompt_config else {}
  353. @property
  354. def completion_prompt_config_dict(self) -> dict[str, Any]:
  355. return json.loads(self.completion_prompt_config) if self.completion_prompt_config else {}
  356. @property
  357. def dataset_configs_dict(self) -> dict[str, Any]:
  358. if self.dataset_configs:
  359. dataset_configs: dict[str, Any] = json.loads(self.dataset_configs)
  360. if "retrieval_model" not in dataset_configs:
  361. return {"retrieval_model": "single"}
  362. else:
  363. return dataset_configs
  364. return {
  365. "retrieval_model": "multiple",
  366. }
  367. @property
  368. def file_upload_dict(self) -> dict[str, Any]:
  369. return (
  370. json.loads(self.file_upload)
  371. if self.file_upload
  372. else {
  373. "image": {
  374. "enabled": False,
  375. "number_limits": DEFAULT_FILE_NUMBER_LIMITS,
  376. "detail": "high",
  377. "transfer_methods": ["remote_url", "local_file"],
  378. }
  379. }
  380. )
  381. def to_dict(self) -> dict[str, Any]:
  382. return {
  383. "opening_statement": self.opening_statement,
  384. "suggested_questions": self.suggested_questions_list,
  385. "suggested_questions_after_answer": self.suggested_questions_after_answer_dict,
  386. "speech_to_text": self.speech_to_text_dict,
  387. "text_to_speech": self.text_to_speech_dict,
  388. "retriever_resource": self.retriever_resource_dict,
  389. "annotation_reply": self.annotation_reply_dict,
  390. "more_like_this": self.more_like_this_dict,
  391. "sensitive_word_avoidance": self.sensitive_word_avoidance_dict,
  392. "external_data_tools": self.external_data_tools_list,
  393. "model": self.model_dict,
  394. "user_input_form": self.user_input_form_list,
  395. "dataset_query_variable": self.dataset_query_variable,
  396. "pre_prompt": self.pre_prompt,
  397. "agent_mode": self.agent_mode_dict,
  398. "prompt_type": self.prompt_type,
  399. "chat_prompt_config": self.chat_prompt_config_dict,
  400. "completion_prompt_config": self.completion_prompt_config_dict,
  401. "dataset_configs": self.dataset_configs_dict,
  402. "file_upload": self.file_upload_dict,
  403. }
  404. def from_model_config_dict(self, model_config: Mapping[str, Any]):
  405. self.opening_statement = model_config.get("opening_statement")
  406. self.suggested_questions = (
  407. json.dumps(model_config["suggested_questions"]) if model_config.get("suggested_questions") else None
  408. )
  409. self.suggested_questions_after_answer = (
  410. json.dumps(model_config["suggested_questions_after_answer"])
  411. if model_config.get("suggested_questions_after_answer")
  412. else None
  413. )
  414. self.speech_to_text = json.dumps(model_config["speech_to_text"]) if model_config.get("speech_to_text") else None
  415. self.text_to_speech = json.dumps(model_config["text_to_speech"]) if model_config.get("text_to_speech") else None
  416. self.more_like_this = json.dumps(model_config["more_like_this"]) if model_config.get("more_like_this") else None
  417. self.sensitive_word_avoidance = (
  418. json.dumps(model_config["sensitive_word_avoidance"])
  419. if model_config.get("sensitive_word_avoidance")
  420. else None
  421. )
  422. self.external_data_tools = (
  423. json.dumps(model_config["external_data_tools"]) if model_config.get("external_data_tools") else None
  424. )
  425. self.model = json.dumps(model_config["model"]) if model_config.get("model") else None
  426. self.user_input_form = (
  427. json.dumps(model_config["user_input_form"]) if model_config.get("user_input_form") else None
  428. )
  429. self.dataset_query_variable = model_config.get("dataset_query_variable")
  430. self.pre_prompt = model_config["pre_prompt"]
  431. self.agent_mode = json.dumps(model_config["agent_mode"]) if model_config.get("agent_mode") else None
  432. self.retriever_resource = (
  433. json.dumps(model_config["retriever_resource"]) if model_config.get("retriever_resource") else None
  434. )
  435. self.prompt_type = model_config.get("prompt_type", "simple")
  436. self.chat_prompt_config = (
  437. json.dumps(model_config.get("chat_prompt_config")) if model_config.get("chat_prompt_config") else None
  438. )
  439. self.completion_prompt_config = (
  440. json.dumps(model_config.get("completion_prompt_config"))
  441. if model_config.get("completion_prompt_config")
  442. else None
  443. )
  444. self.dataset_configs = (
  445. json.dumps(model_config.get("dataset_configs")) if model_config.get("dataset_configs") else None
  446. )
  447. self.file_upload = json.dumps(model_config.get("file_upload")) if model_config.get("file_upload") else None
  448. return self
  449. class RecommendedApp(Base):
  450. __tablename__ = "recommended_apps"
  451. __table_args__ = (
  452. sa.PrimaryKeyConstraint("id", name="recommended_app_pkey"),
  453. sa.Index("recommended_app_app_id_idx", "app_id"),
  454. sa.Index("recommended_app_is_listed_idx", "is_listed", "language"),
  455. )
  456. id = mapped_column(StringUUID, primary_key=True, server_default=sa.text("uuid_generate_v4()"))
  457. app_id = mapped_column(StringUUID, nullable=False)
  458. description = mapped_column(sa.JSON, nullable=False)
  459. copyright: Mapped[str] = mapped_column(String(255), nullable=False)
  460. privacy_policy: Mapped[str] = mapped_column(String(255), nullable=False)
  461. custom_disclaimer: Mapped[str] = mapped_column(sa.TEXT, default="")
  462. category: Mapped[str] = mapped_column(String(255), nullable=False)
  463. position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
  464. is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
  465. install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
  466. language = mapped_column(String(255), nullable=False, server_default=sa.text("'en-US'::character varying"))
  467. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  468. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  469. @property
  470. def app(self) -> App | None:
  471. app = db.session.query(App).where(App.id == self.app_id).first()
  472. return app
  473. class InstalledApp(Base):
  474. __tablename__ = "installed_apps"
  475. __table_args__ = (
  476. sa.PrimaryKeyConstraint("id", name="installed_app_pkey"),
  477. sa.Index("installed_app_tenant_id_idx", "tenant_id"),
  478. sa.Index("installed_app_app_id_idx", "app_id"),
  479. sa.UniqueConstraint("tenant_id", "app_id", name="unique_tenant_app"),
  480. )
  481. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  482. tenant_id = mapped_column(StringUUID, nullable=False)
  483. app_id = mapped_column(StringUUID, nullable=False)
  484. app_owner_tenant_id = mapped_column(StringUUID, nullable=False)
  485. position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
  486. is_pinned: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  487. last_used_at = mapped_column(sa.DateTime, nullable=True)
  488. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  489. @property
  490. def app(self) -> App | None:
  491. app = db.session.query(App).where(App.id == self.app_id).first()
  492. return app
  493. @property
  494. def tenant(self) -> Tenant | None:
  495. tenant = db.session.query(Tenant).where(Tenant.id == self.tenant_id).first()
  496. return tenant
  497. class OAuthProviderApp(Base):
  498. """
  499. Globally shared OAuth provider app information.
  500. Only for Dify Cloud.
  501. """
  502. __tablename__ = "oauth_provider_apps"
  503. __table_args__ = (
  504. sa.PrimaryKeyConstraint("id", name="oauth_provider_app_pkey"),
  505. sa.Index("oauth_provider_app_client_id_idx", "client_id"),
  506. )
  507. id = mapped_column(StringUUID, server_default=sa.text("uuidv7()"))
  508. app_icon = mapped_column(String(255), nullable=False)
  509. app_label = mapped_column(sa.JSON, nullable=False, server_default="{}")
  510. client_id = mapped_column(String(255), nullable=False)
  511. client_secret = mapped_column(String(255), nullable=False)
  512. redirect_uris = mapped_column(sa.JSON, nullable=False, server_default="[]")
  513. scope = mapped_column(
  514. String(255),
  515. nullable=False,
  516. server_default=sa.text("'read:name read:email read:avatar read:interface_language read:timezone'"),
  517. )
  518. created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)"))
  519. class Conversation(Base):
  520. __tablename__ = "conversations"
  521. __table_args__ = (
  522. sa.PrimaryKeyConstraint("id", name="conversation_pkey"),
  523. sa.Index("conversation_app_from_user_idx", "app_id", "from_source", "from_end_user_id"),
  524. )
  525. id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  526. app_id = mapped_column(StringUUID, nullable=False)
  527. app_model_config_id = mapped_column(StringUUID, nullable=True)
  528. model_provider = mapped_column(String(255), nullable=True)
  529. override_model_configs = mapped_column(sa.Text)
  530. model_id = mapped_column(String(255), nullable=True)
  531. mode: Mapped[str] = mapped_column(String(255))
  532. name: Mapped[str] = mapped_column(String(255), nullable=False)
  533. summary = mapped_column(sa.Text)
  534. _inputs: Mapped[dict[str, Any]] = mapped_column("inputs", sa.JSON)
  535. introduction = mapped_column(sa.Text)
  536. system_instruction = mapped_column(sa.Text)
  537. system_instruction_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"))
  538. status: Mapped[str] = mapped_column(String(255), nullable=False)
  539. # The `invoke_from` records how the conversation is created.
  540. #
  541. # Its value corresponds to the members of `InvokeFrom`.
  542. # (api/core/app/entities/app_invoke_entities.py)
  543. invoke_from = mapped_column(String(255), nullable=True)
  544. # ref: ConversationSource.
  545. from_source: Mapped[str] = mapped_column(String(255), nullable=False)
  546. from_end_user_id = mapped_column(StringUUID)
  547. from_account_id = mapped_column(StringUUID)
  548. read_at = mapped_column(sa.DateTime)
  549. read_account_id = mapped_column(StringUUID)
  550. dialogue_count: Mapped[int] = mapped_column(default=0)
  551. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  552. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  553. messages = db.relationship("Message", backref="conversation", lazy="select", passive_deletes="all")
  554. message_annotations = db.relationship(
  555. "MessageAnnotation", backref="conversation", lazy="select", passive_deletes="all"
  556. )
  557. is_deleted: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  558. @property
  559. def inputs(self) -> dict[str, Any]:
  560. inputs = self._inputs.copy()
  561. # Convert file mapping to File object
  562. for key, value in inputs.items():
  563. # NOTE: It's not the best way to implement this, but it's the only way to avoid circular import for now.
  564. from factories import file_factory
  565. if (
  566. isinstance(value, dict)
  567. and cast(dict[str, Any], value).get("dify_model_identity") == FILE_MODEL_IDENTITY
  568. ):
  569. value_dict = cast(dict[str, Any], value)
  570. if value_dict["transfer_method"] == FileTransferMethod.TOOL_FILE:
  571. value_dict["tool_file_id"] = value_dict["related_id"]
  572. elif value_dict["transfer_method"] in [FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL]:
  573. value_dict["upload_file_id"] = value_dict["related_id"]
  574. tenant_id = cast(str, value_dict.get("tenant_id", ""))
  575. inputs[key] = file_factory.build_from_mapping(mapping=value_dict, tenant_id=tenant_id)
  576. elif isinstance(value, list):
  577. value_list = cast(list[Any], value)
  578. if all(
  579. isinstance(item, dict)
  580. and cast(dict[str, Any], item).get("dify_model_identity") == FILE_MODEL_IDENTITY
  581. for item in value_list
  582. ):
  583. file_list: list[File] = []
  584. for item in value_list:
  585. if not isinstance(item, dict):
  586. continue
  587. item_dict = cast(dict[str, Any], item)
  588. if item_dict["transfer_method"] == FileTransferMethod.TOOL_FILE:
  589. item_dict["tool_file_id"] = item_dict["related_id"]
  590. elif item_dict["transfer_method"] in [
  591. FileTransferMethod.LOCAL_FILE,
  592. FileTransferMethod.REMOTE_URL,
  593. ]:
  594. item_dict["upload_file_id"] = item_dict["related_id"]
  595. tenant_id = cast(str, item_dict.get("tenant_id", ""))
  596. file_list.append(file_factory.build_from_mapping(mapping=item_dict, tenant_id=tenant_id))
  597. inputs[key] = file_list
  598. return inputs
  599. @inputs.setter
  600. def inputs(self, value: Mapping[str, Any]):
  601. inputs = dict(value)
  602. for k, v in inputs.items():
  603. if isinstance(v, File):
  604. inputs[k] = v.model_dump()
  605. elif isinstance(v, list):
  606. v_list = cast(list[Any], v)
  607. if all(isinstance(item, File) for item in v_list):
  608. inputs[k] = [item.model_dump() for item in v_list if isinstance(item, File)]
  609. self._inputs = inputs
  610. @property
  611. def model_config(self):
  612. model_config = {}
  613. app_model_config: AppModelConfig | None = None
  614. if self.mode == AppMode.ADVANCED_CHAT:
  615. if self.override_model_configs:
  616. override_model_configs = json.loads(self.override_model_configs)
  617. model_config = override_model_configs
  618. else:
  619. if self.override_model_configs:
  620. override_model_configs = json.loads(self.override_model_configs)
  621. if "model" in override_model_configs:
  622. app_model_config = AppModelConfig()
  623. app_model_config = app_model_config.from_model_config_dict(override_model_configs)
  624. model_config = app_model_config.to_dict()
  625. else:
  626. model_config["configs"] = override_model_configs
  627. else:
  628. app_model_config = (
  629. db.session.query(AppModelConfig).where(AppModelConfig.id == self.app_model_config_id).first()
  630. )
  631. if app_model_config:
  632. model_config = app_model_config.to_dict()
  633. model_config["model_id"] = self.model_id
  634. model_config["provider"] = self.model_provider
  635. return model_config
  636. @property
  637. def summary_or_query(self):
  638. if self.summary:
  639. return self.summary
  640. else:
  641. first_message = self.first_message
  642. if first_message:
  643. return first_message.query
  644. else:
  645. return ""
  646. @property
  647. def annotated(self):
  648. return db.session.query(MessageAnnotation).where(MessageAnnotation.conversation_id == self.id).count() > 0
  649. @property
  650. def annotation(self):
  651. return db.session.query(MessageAnnotation).where(MessageAnnotation.conversation_id == self.id).first()
  652. @property
  653. def message_count(self):
  654. return db.session.query(Message).where(Message.conversation_id == self.id).count()
  655. @property
  656. def user_feedback_stats(self):
  657. like = (
  658. db.session.query(MessageFeedback)
  659. .where(
  660. MessageFeedback.conversation_id == self.id,
  661. MessageFeedback.from_source == "user",
  662. MessageFeedback.rating == "like",
  663. )
  664. .count()
  665. )
  666. dislike = (
  667. db.session.query(MessageFeedback)
  668. .where(
  669. MessageFeedback.conversation_id == self.id,
  670. MessageFeedback.from_source == "user",
  671. MessageFeedback.rating == "dislike",
  672. )
  673. .count()
  674. )
  675. return {"like": like, "dislike": dislike}
  676. @property
  677. def admin_feedback_stats(self):
  678. like = (
  679. db.session.query(MessageFeedback)
  680. .where(
  681. MessageFeedback.conversation_id == self.id,
  682. MessageFeedback.from_source == "admin",
  683. MessageFeedback.rating == "like",
  684. )
  685. .count()
  686. )
  687. dislike = (
  688. db.session.query(MessageFeedback)
  689. .where(
  690. MessageFeedback.conversation_id == self.id,
  691. MessageFeedback.from_source == "admin",
  692. MessageFeedback.rating == "dislike",
  693. )
  694. .count()
  695. )
  696. return {"like": like, "dislike": dislike}
  697. @property
  698. def status_count(self):
  699. messages = db.session.scalars(select(Message).where(Message.conversation_id == self.id)).all()
  700. status_counts = {
  701. WorkflowExecutionStatus.RUNNING: 0,
  702. WorkflowExecutionStatus.SUCCEEDED: 0,
  703. WorkflowExecutionStatus.FAILED: 0,
  704. WorkflowExecutionStatus.STOPPED: 0,
  705. WorkflowExecutionStatus.PARTIAL_SUCCEEDED: 0,
  706. }
  707. for message in messages:
  708. if message.workflow_run:
  709. status_counts[WorkflowExecutionStatus(message.workflow_run.status)] += 1
  710. return (
  711. {
  712. "success": status_counts[WorkflowExecutionStatus.SUCCEEDED],
  713. "failed": status_counts[WorkflowExecutionStatus.FAILED],
  714. "partial_success": status_counts[WorkflowExecutionStatus.PARTIAL_SUCCEEDED],
  715. }
  716. if messages
  717. else None
  718. )
  719. @property
  720. def first_message(self):
  721. return (
  722. db.session.query(Message)
  723. .where(Message.conversation_id == self.id)
  724. .order_by(Message.created_at.asc())
  725. .first()
  726. )
  727. @property
  728. def app(self) -> App | None:
  729. with Session(db.engine, expire_on_commit=False) as session:
  730. return session.query(App).where(App.id == self.app_id).first()
  731. @property
  732. def from_end_user_session_id(self):
  733. if self.from_end_user_id:
  734. end_user = db.session.query(EndUser).where(EndUser.id == self.from_end_user_id).first()
  735. if end_user:
  736. return end_user.session_id
  737. return None
  738. @property
  739. def from_account_name(self) -> str | None:
  740. if self.from_account_id:
  741. account = db.session.query(Account).where(Account.id == self.from_account_id).first()
  742. if account:
  743. return account.name
  744. return None
  745. @property
  746. def in_debug_mode(self) -> bool:
  747. return self.override_model_configs is not None
  748. def to_dict(self) -> dict[str, Any]:
  749. return {
  750. "id": self.id,
  751. "app_id": self.app_id,
  752. "app_model_config_id": self.app_model_config_id,
  753. "model_provider": self.model_provider,
  754. "override_model_configs": self.override_model_configs,
  755. "model_id": self.model_id,
  756. "mode": self.mode,
  757. "name": self.name,
  758. "summary": self.summary,
  759. "inputs": self.inputs,
  760. "introduction": self.introduction,
  761. "system_instruction": self.system_instruction,
  762. "system_instruction_tokens": self.system_instruction_tokens,
  763. "status": self.status,
  764. "invoke_from": self.invoke_from,
  765. "from_source": self.from_source,
  766. "from_end_user_id": self.from_end_user_id,
  767. "from_account_id": self.from_account_id,
  768. "read_at": self.read_at,
  769. "read_account_id": self.read_account_id,
  770. "dialogue_count": self.dialogue_count,
  771. "created_at": self.created_at,
  772. "updated_at": self.updated_at,
  773. }
  774. class Message(Base):
  775. __tablename__ = "messages"
  776. __table_args__ = (
  777. PrimaryKeyConstraint("id", name="message_pkey"),
  778. Index("message_app_id_idx", "app_id", "created_at"),
  779. Index("message_conversation_id_idx", "conversation_id"),
  780. Index("message_end_user_idx", "app_id", "from_source", "from_end_user_id"),
  781. Index("message_account_idx", "app_id", "from_source", "from_account_id"),
  782. Index("message_workflow_run_id_idx", "conversation_id", "workflow_run_id"),
  783. Index("message_created_at_idx", "created_at"),
  784. )
  785. id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  786. app_id = mapped_column(StringUUID, nullable=False)
  787. model_provider = mapped_column(String(255), nullable=True)
  788. model_id = mapped_column(String(255), nullable=True)
  789. override_model_configs = mapped_column(sa.Text)
  790. conversation_id = mapped_column(StringUUID, sa.ForeignKey("conversations.id"), nullable=False)
  791. _inputs: Mapped[dict[str, Any]] = mapped_column("inputs", sa.JSON)
  792. query: Mapped[str] = mapped_column(sa.Text, nullable=False)
  793. message = mapped_column(sa.JSON, nullable=False)
  794. message_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"))
  795. message_unit_price = mapped_column(sa.Numeric(10, 4), nullable=False)
  796. message_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001"))
  797. answer: Mapped[str] = mapped_column(sa.Text, nullable=False)
  798. answer_tokens: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"))
  799. answer_unit_price = mapped_column(sa.Numeric(10, 4), nullable=False)
  800. answer_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001"))
  801. parent_message_id = mapped_column(StringUUID, nullable=True)
  802. provider_response_latency = mapped_column(sa.Float, nullable=False, server_default=sa.text("0"))
  803. total_price = mapped_column(sa.Numeric(10, 7))
  804. currency: Mapped[str] = mapped_column(String(255), nullable=False)
  805. status = mapped_column(String(255), nullable=False, server_default=sa.text("'normal'::character varying"))
  806. error = mapped_column(sa.Text)
  807. message_metadata = mapped_column(sa.Text)
  808. invoke_from: Mapped[str | None] = mapped_column(String(255), nullable=True)
  809. from_source: Mapped[str] = mapped_column(String(255), nullable=False)
  810. from_end_user_id: Mapped[str | None] = mapped_column(StringUUID)
  811. from_account_id: Mapped[str | None] = mapped_column(StringUUID)
  812. created_at: Mapped[datetime] = mapped_column(sa.DateTime, server_default=func.current_timestamp())
  813. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  814. agent_based: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  815. workflow_run_id: Mapped[str | None] = mapped_column(StringUUID)
  816. @property
  817. def inputs(self) -> dict[str, Any]:
  818. inputs = self._inputs.copy()
  819. for key, value in inputs.items():
  820. # NOTE: It's not the best way to implement this, but it's the only way to avoid circular import for now.
  821. from factories import file_factory
  822. if (
  823. isinstance(value, dict)
  824. and cast(dict[str, Any], value).get("dify_model_identity") == FILE_MODEL_IDENTITY
  825. ):
  826. value_dict = cast(dict[str, Any], value)
  827. if value_dict["transfer_method"] == FileTransferMethod.TOOL_FILE:
  828. value_dict["tool_file_id"] = value_dict["related_id"]
  829. elif value_dict["transfer_method"] in [FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL]:
  830. value_dict["upload_file_id"] = value_dict["related_id"]
  831. tenant_id = cast(str, value_dict.get("tenant_id", ""))
  832. inputs[key] = file_factory.build_from_mapping(mapping=value_dict, tenant_id=tenant_id)
  833. elif isinstance(value, list):
  834. value_list = cast(list[Any], value)
  835. if all(
  836. isinstance(item, dict)
  837. and cast(dict[str, Any], item).get("dify_model_identity") == FILE_MODEL_IDENTITY
  838. for item in value_list
  839. ):
  840. file_list: list[File] = []
  841. for item in value_list:
  842. if not isinstance(item, dict):
  843. continue
  844. item_dict = cast(dict[str, Any], item)
  845. if item_dict["transfer_method"] == FileTransferMethod.TOOL_FILE:
  846. item_dict["tool_file_id"] = item_dict["related_id"]
  847. elif item_dict["transfer_method"] in [
  848. FileTransferMethod.LOCAL_FILE,
  849. FileTransferMethod.REMOTE_URL,
  850. ]:
  851. item_dict["upload_file_id"] = item_dict["related_id"]
  852. tenant_id = cast(str, item_dict.get("tenant_id", ""))
  853. file_list.append(file_factory.build_from_mapping(mapping=item_dict, tenant_id=tenant_id))
  854. inputs[key] = file_list
  855. return inputs
  856. @inputs.setter
  857. def inputs(self, value: Mapping[str, Any]):
  858. inputs = dict(value)
  859. for k, v in inputs.items():
  860. if isinstance(v, File):
  861. inputs[k] = v.model_dump()
  862. elif isinstance(v, list):
  863. v_list = cast(list[Any], v)
  864. if all(isinstance(item, File) for item in v_list):
  865. inputs[k] = [item.model_dump() for item in v_list if isinstance(item, File)]
  866. self._inputs = inputs
  867. @property
  868. def re_sign_file_url_answer(self) -> str:
  869. if not self.answer:
  870. return self.answer
  871. pattern = r"\[!?.*?\]\((((http|https):\/\/.+)?\/files\/(tools\/)?[\w-]+.*?timestamp=.*&nonce=.*&sign=.*)\)"
  872. matches = re.findall(pattern, self.answer)
  873. if not matches:
  874. return self.answer
  875. urls = [match[0] for match in matches]
  876. # remove duplicate urls
  877. urls = list(set(urls))
  878. if not urls:
  879. return self.answer
  880. re_sign_file_url_answer = self.answer
  881. for url in urls:
  882. if "files/tools" in url:
  883. # get tool file id
  884. tool_file_id_pattern = r"\/files\/tools\/([\.\w-]+)?\?timestamp="
  885. result = re.search(tool_file_id_pattern, url)
  886. if not result:
  887. continue
  888. tool_file_id = result.group(1)
  889. # get extension
  890. if "." in tool_file_id:
  891. split_result = tool_file_id.split(".")
  892. extension = f".{split_result[-1]}"
  893. if len(extension) > 10:
  894. extension = ".bin"
  895. tool_file_id = split_result[0]
  896. else:
  897. extension = ".bin"
  898. if not tool_file_id:
  899. continue
  900. sign_url = sign_tool_file(tool_file_id=tool_file_id, extension=extension)
  901. elif "file-preview" in url:
  902. # get upload file id
  903. upload_file_id_pattern = r"\/files\/([\w-]+)\/file-preview\?timestamp="
  904. result = re.search(upload_file_id_pattern, url)
  905. if not result:
  906. continue
  907. upload_file_id = result.group(1)
  908. if not upload_file_id:
  909. continue
  910. sign_url = file_helpers.get_signed_file_url(upload_file_id)
  911. elif "image-preview" in url:
  912. # image-preview is deprecated, use file-preview instead
  913. upload_file_id_pattern = r"\/files\/([\w-]+)\/image-preview\?timestamp="
  914. result = re.search(upload_file_id_pattern, url)
  915. if not result:
  916. continue
  917. upload_file_id = result.group(1)
  918. if not upload_file_id:
  919. continue
  920. sign_url = file_helpers.get_signed_file_url(upload_file_id)
  921. else:
  922. continue
  923. # if as_attachment is in the url, add it to the sign_url.
  924. if "as_attachment" in url:
  925. sign_url += "&as_attachment=true"
  926. re_sign_file_url_answer = re_sign_file_url_answer.replace(url, sign_url)
  927. return re_sign_file_url_answer
  928. @property
  929. def user_feedback(self):
  930. feedback = (
  931. db.session.query(MessageFeedback)
  932. .where(MessageFeedback.message_id == self.id, MessageFeedback.from_source == "user")
  933. .first()
  934. )
  935. return feedback
  936. @property
  937. def admin_feedback(self):
  938. feedback = (
  939. db.session.query(MessageFeedback)
  940. .where(MessageFeedback.message_id == self.id, MessageFeedback.from_source == "admin")
  941. .first()
  942. )
  943. return feedback
  944. @property
  945. def feedbacks(self):
  946. feedbacks = db.session.scalars(select(MessageFeedback).where(MessageFeedback.message_id == self.id)).all()
  947. return feedbacks
  948. @property
  949. def annotation(self):
  950. annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.message_id == self.id).first()
  951. return annotation
  952. @property
  953. def annotation_hit_history(self):
  954. annotation_history = (
  955. db.session.query(AppAnnotationHitHistory).where(AppAnnotationHitHistory.message_id == self.id).first()
  956. )
  957. if annotation_history:
  958. annotation = (
  959. db.session.query(MessageAnnotation)
  960. .where(MessageAnnotation.id == annotation_history.annotation_id)
  961. .first()
  962. )
  963. return annotation
  964. return None
  965. @property
  966. def app_model_config(self):
  967. conversation = db.session.query(Conversation).where(Conversation.id == self.conversation_id).first()
  968. if conversation:
  969. return db.session.query(AppModelConfig).where(AppModelConfig.id == conversation.app_model_config_id).first()
  970. return None
  971. @property
  972. def in_debug_mode(self) -> bool:
  973. return self.override_model_configs is not None
  974. @property
  975. def message_metadata_dict(self) -> dict[str, Any]:
  976. return json.loads(self.message_metadata) if self.message_metadata else {}
  977. @property
  978. def agent_thoughts(self) -> list["MessageAgentThought"]:
  979. return (
  980. db.session.query(MessageAgentThought)
  981. .where(MessageAgentThought.message_id == self.id)
  982. .order_by(MessageAgentThought.position.asc())
  983. .all()
  984. )
  985. @property
  986. def retriever_resources(self) -> Any:
  987. return self.message_metadata_dict.get("retriever_resources") if self.message_metadata else []
  988. @property
  989. def message_files(self) -> list[dict[str, Any]]:
  990. from factories import file_factory
  991. message_files = db.session.scalars(select(MessageFile).where(MessageFile.message_id == self.id)).all()
  992. current_app = db.session.query(App).where(App.id == self.app_id).first()
  993. if not current_app:
  994. raise ValueError(f"App {self.app_id} not found")
  995. files: list[File] = []
  996. for message_file in message_files:
  997. if message_file.transfer_method == FileTransferMethod.LOCAL_FILE.value:
  998. if message_file.upload_file_id is None:
  999. raise ValueError(f"MessageFile {message_file.id} is a local file but has no upload_file_id")
  1000. file = file_factory.build_from_mapping(
  1001. mapping={
  1002. "id": message_file.id,
  1003. "type": message_file.type,
  1004. "transfer_method": message_file.transfer_method,
  1005. "upload_file_id": message_file.upload_file_id,
  1006. },
  1007. tenant_id=current_app.tenant_id,
  1008. )
  1009. elif message_file.transfer_method == FileTransferMethod.REMOTE_URL.value:
  1010. if message_file.url is None:
  1011. raise ValueError(f"MessageFile {message_file.id} is a remote url but has no url")
  1012. file = file_factory.build_from_mapping(
  1013. mapping={
  1014. "id": message_file.id,
  1015. "type": message_file.type,
  1016. "transfer_method": message_file.transfer_method,
  1017. "upload_file_id": message_file.upload_file_id,
  1018. "url": message_file.url,
  1019. },
  1020. tenant_id=current_app.tenant_id,
  1021. )
  1022. elif message_file.transfer_method == FileTransferMethod.TOOL_FILE.value:
  1023. if message_file.upload_file_id is None:
  1024. assert message_file.url is not None
  1025. message_file.upload_file_id = message_file.url.split("/")[-1].split(".")[0]
  1026. mapping = {
  1027. "id": message_file.id,
  1028. "type": message_file.type,
  1029. "transfer_method": message_file.transfer_method,
  1030. "tool_file_id": message_file.upload_file_id,
  1031. }
  1032. file = file_factory.build_from_mapping(
  1033. mapping=mapping,
  1034. tenant_id=current_app.tenant_id,
  1035. )
  1036. else:
  1037. raise ValueError(
  1038. f"MessageFile {message_file.id} has an invalid transfer_method {message_file.transfer_method}"
  1039. )
  1040. files.append(file)
  1041. result: list[dict[str, Any]] = [
  1042. {"belongs_to": message_file.belongs_to, "upload_file_id": message_file.upload_file_id, **file.to_dict()}
  1043. for (file, message_file) in zip(files, message_files)
  1044. ]
  1045. db.session.commit()
  1046. return result
  1047. @property
  1048. def workflow_run(self):
  1049. if self.workflow_run_id:
  1050. from .workflow import WorkflowRun
  1051. return db.session.query(WorkflowRun).where(WorkflowRun.id == self.workflow_run_id).first()
  1052. return None
  1053. def to_dict(self) -> dict[str, Any]:
  1054. return {
  1055. "id": self.id,
  1056. "app_id": self.app_id,
  1057. "conversation_id": self.conversation_id,
  1058. "model_id": self.model_id,
  1059. "inputs": self.inputs,
  1060. "query": self.query,
  1061. "total_price": self.total_price,
  1062. "message": self.message,
  1063. "answer": self.answer,
  1064. "status": self.status,
  1065. "error": self.error,
  1066. "message_metadata": self.message_metadata_dict,
  1067. "from_source": self.from_source,
  1068. "from_end_user_id": self.from_end_user_id,
  1069. "from_account_id": self.from_account_id,
  1070. "created_at": self.created_at.isoformat(),
  1071. "updated_at": self.updated_at.isoformat(),
  1072. "agent_based": self.agent_based,
  1073. "workflow_run_id": self.workflow_run_id,
  1074. }
  1075. @classmethod
  1076. def from_dict(cls, data: dict[str, Any]) -> "Message":
  1077. return cls(
  1078. id=data["id"],
  1079. app_id=data["app_id"],
  1080. conversation_id=data["conversation_id"],
  1081. model_id=data["model_id"],
  1082. inputs=data["inputs"],
  1083. total_price=data["total_price"],
  1084. query=data["query"],
  1085. message=data["message"],
  1086. answer=data["answer"],
  1087. status=data["status"],
  1088. error=data["error"],
  1089. message_metadata=json.dumps(data["message_metadata"]),
  1090. from_source=data["from_source"],
  1091. from_end_user_id=data["from_end_user_id"],
  1092. from_account_id=data["from_account_id"],
  1093. created_at=data["created_at"],
  1094. updated_at=data["updated_at"],
  1095. agent_based=data["agent_based"],
  1096. workflow_run_id=data["workflow_run_id"],
  1097. )
  1098. class MessageFeedback(Base):
  1099. __tablename__ = "message_feedbacks"
  1100. __table_args__ = (
  1101. sa.PrimaryKeyConstraint("id", name="message_feedback_pkey"),
  1102. sa.Index("message_feedback_app_idx", "app_id"),
  1103. sa.Index("message_feedback_message_idx", "message_id", "from_source"),
  1104. sa.Index("message_feedback_conversation_idx", "conversation_id", "from_source", "rating"),
  1105. )
  1106. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1107. app_id = mapped_column(StringUUID, nullable=False)
  1108. conversation_id = mapped_column(StringUUID, nullable=False)
  1109. message_id = mapped_column(StringUUID, nullable=False)
  1110. rating: Mapped[str] = mapped_column(String(255), nullable=False)
  1111. content = mapped_column(sa.Text)
  1112. from_source: Mapped[str] = mapped_column(String(255), nullable=False)
  1113. from_end_user_id = mapped_column(StringUUID)
  1114. from_account_id = mapped_column(StringUUID)
  1115. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1116. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1117. @property
  1118. def from_account(self):
  1119. account = db.session.query(Account).where(Account.id == self.from_account_id).first()
  1120. return account
  1121. def to_dict(self) -> dict[str, Any]:
  1122. return {
  1123. "id": str(self.id),
  1124. "app_id": str(self.app_id),
  1125. "conversation_id": str(self.conversation_id),
  1126. "message_id": str(self.message_id),
  1127. "rating": self.rating,
  1128. "content": self.content,
  1129. "from_source": self.from_source,
  1130. "from_end_user_id": str(self.from_end_user_id) if self.from_end_user_id else None,
  1131. "from_account_id": str(self.from_account_id) if self.from_account_id else None,
  1132. "created_at": self.created_at.isoformat(),
  1133. "updated_at": self.updated_at.isoformat(),
  1134. }
  1135. class MessageFile(Base):
  1136. __tablename__ = "message_files"
  1137. __table_args__ = (
  1138. sa.PrimaryKeyConstraint("id", name="message_file_pkey"),
  1139. sa.Index("message_file_message_idx", "message_id"),
  1140. sa.Index("message_file_created_by_idx", "created_by"),
  1141. )
  1142. def __init__(
  1143. self,
  1144. *,
  1145. message_id: str,
  1146. type: FileType,
  1147. transfer_method: FileTransferMethod,
  1148. url: str | None = None,
  1149. belongs_to: Literal["user", "assistant"] | None = None,
  1150. upload_file_id: str | None = None,
  1151. created_by_role: CreatorUserRole,
  1152. created_by: str,
  1153. ):
  1154. self.message_id = message_id
  1155. self.type = type
  1156. self.transfer_method = transfer_method
  1157. self.url = url
  1158. self.belongs_to = belongs_to
  1159. self.upload_file_id = upload_file_id
  1160. self.created_by_role = created_by_role.value
  1161. self.created_by = created_by
  1162. id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1163. message_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  1164. type: Mapped[str] = mapped_column(String(255), nullable=False)
  1165. transfer_method: Mapped[str] = mapped_column(String(255), nullable=False)
  1166. url: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
  1167. belongs_to: Mapped[str | None] = mapped_column(String(255), nullable=True)
  1168. upload_file_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
  1169. created_by_role: Mapped[str] = mapped_column(String(255), nullable=False)
  1170. created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
  1171. created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1172. class MessageAnnotation(Base):
  1173. __tablename__ = "message_annotations"
  1174. __table_args__ = (
  1175. sa.PrimaryKeyConstraint("id", name="message_annotation_pkey"),
  1176. sa.Index("message_annotation_app_idx", "app_id"),
  1177. sa.Index("message_annotation_conversation_idx", "conversation_id"),
  1178. sa.Index("message_annotation_message_idx", "message_id"),
  1179. )
  1180. id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1181. app_id: Mapped[str] = mapped_column(StringUUID)
  1182. conversation_id: Mapped[str | None] = mapped_column(StringUUID, sa.ForeignKey("conversations.id"))
  1183. message_id: Mapped[str | None] = mapped_column(StringUUID)
  1184. question = mapped_column(sa.Text, nullable=True)
  1185. content = mapped_column(sa.Text, nullable=False)
  1186. hit_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, server_default=sa.text("0"))
  1187. account_id = mapped_column(StringUUID, nullable=False)
  1188. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1189. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1190. @property
  1191. def account(self):
  1192. account = db.session.query(Account).where(Account.id == self.account_id).first()
  1193. return account
  1194. @property
  1195. def annotation_create_account(self):
  1196. account = db.session.query(Account).where(Account.id == self.account_id).first()
  1197. return account
  1198. class AppAnnotationHitHistory(Base):
  1199. __tablename__ = "app_annotation_hit_histories"
  1200. __table_args__ = (
  1201. sa.PrimaryKeyConstraint("id", name="app_annotation_hit_histories_pkey"),
  1202. sa.Index("app_annotation_hit_histories_app_idx", "app_id"),
  1203. sa.Index("app_annotation_hit_histories_account_idx", "account_id"),
  1204. sa.Index("app_annotation_hit_histories_annotation_idx", "annotation_id"),
  1205. sa.Index("app_annotation_hit_histories_message_idx", "message_id"),
  1206. )
  1207. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1208. app_id = mapped_column(StringUUID, nullable=False)
  1209. annotation_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  1210. source = mapped_column(sa.Text, nullable=False)
  1211. question = mapped_column(sa.Text, nullable=False)
  1212. account_id = mapped_column(StringUUID, nullable=False)
  1213. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1214. score = mapped_column(Float, nullable=False, server_default=sa.text("0"))
  1215. message_id = mapped_column(StringUUID, nullable=False)
  1216. annotation_question = mapped_column(sa.Text, nullable=False)
  1217. annotation_content = mapped_column(sa.Text, nullable=False)
  1218. @property
  1219. def account(self):
  1220. account = (
  1221. db.session.query(Account)
  1222. .join(MessageAnnotation, MessageAnnotation.account_id == Account.id)
  1223. .where(MessageAnnotation.id == self.annotation_id)
  1224. .first()
  1225. )
  1226. return account
  1227. @property
  1228. def annotation_create_account(self):
  1229. account = db.session.query(Account).where(Account.id == self.account_id).first()
  1230. return account
  1231. class AppAnnotationSetting(Base):
  1232. __tablename__ = "app_annotation_settings"
  1233. __table_args__ = (
  1234. sa.PrimaryKeyConstraint("id", name="app_annotation_settings_pkey"),
  1235. sa.Index("app_annotation_settings_app_idx", "app_id"),
  1236. )
  1237. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1238. app_id = mapped_column(StringUUID, nullable=False)
  1239. score_threshold = mapped_column(Float, nullable=False, server_default=sa.text("0"))
  1240. collection_binding_id = mapped_column(StringUUID, nullable=False)
  1241. created_user_id = mapped_column(StringUUID, nullable=False)
  1242. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1243. updated_user_id = mapped_column(StringUUID, nullable=False)
  1244. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1245. @property
  1246. def collection_binding_detail(self):
  1247. from .dataset import DatasetCollectionBinding
  1248. collection_binding_detail = (
  1249. db.session.query(DatasetCollectionBinding)
  1250. .where(DatasetCollectionBinding.id == self.collection_binding_id)
  1251. .first()
  1252. )
  1253. return collection_binding_detail
  1254. class OperationLog(Base):
  1255. __tablename__ = "operation_logs"
  1256. __table_args__ = (
  1257. sa.PrimaryKeyConstraint("id", name="operation_log_pkey"),
  1258. sa.Index("operation_log_account_action_idx", "tenant_id", "account_id", "action"),
  1259. )
  1260. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1261. tenant_id = mapped_column(StringUUID, nullable=False)
  1262. account_id = mapped_column(StringUUID, nullable=False)
  1263. action: Mapped[str] = mapped_column(String(255), nullable=False)
  1264. content = mapped_column(sa.JSON)
  1265. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1266. created_ip: Mapped[str] = mapped_column(String(255), nullable=False)
  1267. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1268. class DefaultEndUserSessionID(StrEnum):
  1269. """
  1270. End User Session ID enum.
  1271. """
  1272. DEFAULT_SESSION_ID = "DEFAULT-USER"
  1273. class EndUser(Base, UserMixin):
  1274. __tablename__ = "end_users"
  1275. __table_args__ = (
  1276. sa.PrimaryKeyConstraint("id", name="end_user_pkey"),
  1277. sa.Index("end_user_session_id_idx", "session_id", "type"),
  1278. sa.Index("end_user_tenant_session_id_idx", "tenant_id", "session_id", "type"),
  1279. )
  1280. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1281. tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  1282. app_id = mapped_column(StringUUID, nullable=True)
  1283. type: Mapped[str] = mapped_column(String(255), nullable=False)
  1284. external_user_id = mapped_column(String(255), nullable=True)
  1285. name = mapped_column(String(255))
  1286. _is_anonymous: Mapped[bool] = mapped_column(
  1287. "is_anonymous", sa.Boolean, nullable=False, server_default=sa.text("true")
  1288. )
  1289. @property
  1290. def is_anonymous(self) -> Literal[False]:
  1291. return False
  1292. @is_anonymous.setter
  1293. def is_anonymous(self, value: bool) -> None:
  1294. self._is_anonymous = value
  1295. session_id: Mapped[str] = mapped_column()
  1296. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1297. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1298. class AppMCPServer(Base):
  1299. __tablename__ = "app_mcp_servers"
  1300. __table_args__ = (
  1301. sa.PrimaryKeyConstraint("id", name="app_mcp_server_pkey"),
  1302. sa.UniqueConstraint("tenant_id", "app_id", name="unique_app_mcp_server_tenant_app_id"),
  1303. sa.UniqueConstraint("server_code", name="unique_app_mcp_server_server_code"),
  1304. )
  1305. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1306. tenant_id = mapped_column(StringUUID, nullable=False)
  1307. app_id = mapped_column(StringUUID, nullable=False)
  1308. name: Mapped[str] = mapped_column(String(255), nullable=False)
  1309. description: Mapped[str] = mapped_column(String(255), nullable=False)
  1310. server_code: Mapped[str] = mapped_column(String(255), nullable=False)
  1311. status = mapped_column(String(255), nullable=False, server_default=sa.text("'normal'::character varying"))
  1312. parameters = mapped_column(sa.Text, nullable=False)
  1313. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1314. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1315. @staticmethod
  1316. def generate_server_code(n: int) -> str:
  1317. while True:
  1318. result = generate_string(n)
  1319. while db.session.query(AppMCPServer).where(AppMCPServer.server_code == result).count() > 0:
  1320. result = generate_string(n)
  1321. return result
  1322. @property
  1323. def parameters_dict(self) -> dict[str, Any]:
  1324. return cast(dict[str, Any], json.loads(self.parameters))
  1325. class Site(Base):
  1326. __tablename__ = "sites"
  1327. __table_args__ = (
  1328. sa.PrimaryKeyConstraint("id", name="site_pkey"),
  1329. sa.Index("site_app_id_idx", "app_id"),
  1330. sa.Index("site_code_idx", "code", "status"),
  1331. )
  1332. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1333. app_id = mapped_column(StringUUID, nullable=False)
  1334. title: Mapped[str] = mapped_column(String(255), nullable=False)
  1335. icon_type = mapped_column(String(255), nullable=True)
  1336. icon = mapped_column(String(255))
  1337. icon_background = mapped_column(String(255))
  1338. description = mapped_column(sa.Text)
  1339. default_language: Mapped[str] = mapped_column(String(255), nullable=False)
  1340. chat_color_theme = mapped_column(String(255))
  1341. chat_color_theme_inverted: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  1342. copyright = mapped_column(String(255))
  1343. privacy_policy = mapped_column(String(255))
  1344. show_workflow_steps: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"))
  1345. use_icon_as_answer_icon: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  1346. _custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", sa.TEXT, default="")
  1347. customize_domain = mapped_column(String(255))
  1348. customize_token_strategy: Mapped[str] = mapped_column(String(255), nullable=False)
  1349. prompt_public: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  1350. status = mapped_column(String(255), nullable=False, server_default=sa.text("'normal'::character varying"))
  1351. created_by = mapped_column(StringUUID, nullable=True)
  1352. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1353. updated_by = mapped_column(StringUUID, nullable=True)
  1354. updated_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1355. code = mapped_column(String(255))
  1356. @property
  1357. def custom_disclaimer(self):
  1358. return self._custom_disclaimer
  1359. @custom_disclaimer.setter
  1360. def custom_disclaimer(self, value: str):
  1361. if len(value) > 512:
  1362. raise ValueError("Custom disclaimer cannot exceed 512 characters.")
  1363. self._custom_disclaimer = value
  1364. @staticmethod
  1365. def generate_code(n: int) -> str:
  1366. while True:
  1367. result = generate_string(n)
  1368. while db.session.query(Site).where(Site.code == result).count() > 0:
  1369. result = generate_string(n)
  1370. return result
  1371. @property
  1372. def app_base_url(self):
  1373. return dify_config.APP_WEB_URL or request.url_root.rstrip("/")
  1374. class ApiToken(Base):
  1375. __tablename__ = "api_tokens"
  1376. __table_args__ = (
  1377. sa.PrimaryKeyConstraint("id", name="api_token_pkey"),
  1378. sa.Index("api_token_app_id_type_idx", "app_id", "type"),
  1379. sa.Index("api_token_token_idx", "token", "type"),
  1380. sa.Index("api_token_tenant_idx", "tenant_id", "type"),
  1381. )
  1382. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1383. app_id = mapped_column(StringUUID, nullable=True)
  1384. tenant_id = mapped_column(StringUUID, nullable=True)
  1385. type = mapped_column(String(16), nullable=False)
  1386. token: Mapped[str] = mapped_column(String(255), nullable=False)
  1387. last_used_at = mapped_column(sa.DateTime, nullable=True)
  1388. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1389. @staticmethod
  1390. def generate_api_key(prefix: str, n: int) -> str:
  1391. while True:
  1392. result = prefix + generate_string(n)
  1393. if db.session.scalar(select(exists().where(ApiToken.token == result))):
  1394. continue
  1395. return result
  1396. class UploadFile(Base):
  1397. __tablename__ = "upload_files"
  1398. __table_args__ = (
  1399. sa.PrimaryKeyConstraint("id", name="upload_file_pkey"),
  1400. sa.Index("upload_file_tenant_idx", "tenant_id"),
  1401. )
  1402. # NOTE: The `id` field is generated within the application to minimize extra roundtrips
  1403. # (especially when generating `source_url`).
  1404. # The `server_default` serves as a fallback mechanism.
  1405. id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1406. tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
  1407. storage_type: Mapped[str] = mapped_column(String(255), nullable=False)
  1408. key: Mapped[str] = mapped_column(String(255), nullable=False)
  1409. name: Mapped[str] = mapped_column(String(255), nullable=False)
  1410. size: Mapped[int] = mapped_column(sa.Integer, nullable=False)
  1411. extension: Mapped[str] = mapped_column(String(255), nullable=False)
  1412. mime_type: Mapped[str] = mapped_column(String(255), nullable=True)
  1413. # The `created_by_role` field indicates whether the file was created by an `Account` or an `EndUser`.
  1414. # Its value is derived from the `CreatorUserRole` enumeration.
  1415. created_by_role: Mapped[str] = mapped_column(
  1416. String(255), nullable=False, server_default=sa.text("'account'::character varying")
  1417. )
  1418. # The `created_by` field stores the ID of the entity that created this upload file.
  1419. #
  1420. # If `created_by_role` is `ACCOUNT`, it corresponds to `Account.id`.
  1421. # Otherwise, it corresponds to `EndUser.id`.
  1422. created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
  1423. created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1424. # The fields `used` and `used_by` are not consistently maintained.
  1425. #
  1426. # When using this model in new code, ensure the following:
  1427. #
  1428. # 1. Set `used` to `true` when the file is utilized.
  1429. # 2. Assign `used_by` to the corresponding `Account.id` or `EndUser.id` based on the `created_by_role`.
  1430. # 3. Avoid relying on these fields for logic, as their values may not always be accurate.
  1431. #
  1432. # `used` may indicate whether the file has been utilized by another service.
  1433. used: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
  1434. # `used_by` may indicate the ID of the user who utilized this file.
  1435. used_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
  1436. used_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True)
  1437. hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
  1438. source_url: Mapped[str] = mapped_column(sa.TEXT, default="")
  1439. def __init__(
  1440. self,
  1441. *,
  1442. tenant_id: str,
  1443. storage_type: str,
  1444. key: str,
  1445. name: str,
  1446. size: int,
  1447. extension: str,
  1448. mime_type: str,
  1449. created_by_role: CreatorUserRole,
  1450. created_by: str,
  1451. created_at: datetime,
  1452. used: bool,
  1453. used_by: str | None = None,
  1454. used_at: datetime | None = None,
  1455. hash: str | None = None,
  1456. source_url: str = "",
  1457. ):
  1458. self.id = str(uuid.uuid4())
  1459. self.tenant_id = tenant_id
  1460. self.storage_type = storage_type
  1461. self.key = key
  1462. self.name = name
  1463. self.size = size
  1464. self.extension = extension
  1465. self.mime_type = mime_type
  1466. self.created_by_role = created_by_role.value
  1467. self.created_by = created_by
  1468. self.created_at = created_at
  1469. self.used = used
  1470. self.used_by = used_by
  1471. self.used_at = used_at
  1472. self.hash = hash
  1473. self.source_url = source_url
  1474. class ApiRequest(Base):
  1475. __tablename__ = "api_requests"
  1476. __table_args__ = (
  1477. sa.PrimaryKeyConstraint("id", name="api_request_pkey"),
  1478. sa.Index("api_request_token_idx", "tenant_id", "api_token_id"),
  1479. )
  1480. id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"))
  1481. tenant_id = mapped_column(StringUUID, nullable=False)
  1482. api_token_id = mapped_column(StringUUID, nullable=False)
  1483. path: Mapped[str] = mapped_column(String(255), nullable=False)
  1484. request = mapped_column(sa.Text, nullable=True)
  1485. response = mapped_column(sa.Text, nullable=True)
  1486. ip: Mapped[str] = mapped_column(String(255), nullable=False)
  1487. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1488. class MessageChain(Base):
  1489. __tablename__ = "message_chains"
  1490. __table_args__ = (
  1491. sa.PrimaryKeyConstraint("id", name="message_chain_pkey"),
  1492. sa.Index("message_chain_message_id_idx", "message_id"),
  1493. )
  1494. id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"))
  1495. message_id = mapped_column(StringUUID, nullable=False)
  1496. type: Mapped[str] = mapped_column(String(255), nullable=False)
  1497. input = mapped_column(sa.Text, nullable=True)
  1498. output = mapped_column(sa.Text, nullable=True)
  1499. created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.func.current_timestamp())
  1500. class MessageAgentThought(Base):
  1501. __tablename__ = "message_agent_thoughts"
  1502. __table_args__ = (
  1503. sa.PrimaryKeyConstraint("id", name="message_agent_thought_pkey"),
  1504. sa.Index("message_agent_thought_message_id_idx", "message_id"),
  1505. sa.Index("message_agent_thought_message_chain_id_idx", "message_chain_id"),
  1506. )
  1507. id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"))
  1508. message_id = mapped_column(StringUUID, nullable=False)
  1509. message_chain_id = mapped_column(StringUUID, nullable=True)
  1510. position: Mapped[int] = mapped_column(sa.Integer, nullable=False)
  1511. thought = mapped_column(sa.Text, nullable=True)
  1512. tool = mapped_column(sa.Text, nullable=True)
  1513. tool_labels_str = mapped_column(sa.Text, nullable=False, server_default=sa.text("'{}'::text"))
  1514. tool_meta_str = mapped_column(sa.Text, nullable=False, server_default=sa.text("'{}'::text"))
  1515. tool_input = mapped_column(sa.Text, nullable=True)
  1516. observation = mapped_column(sa.Text, nullable=True)
  1517. # plugin_id = mapped_column(StringUUID, nullable=True) ## for future design
  1518. tool_process_data = mapped_column(sa.Text, nullable=True)
  1519. message = mapped_column(sa.Text, nullable=True)
  1520. message_token: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
  1521. message_unit_price = mapped_column(sa.Numeric, nullable=True)
  1522. message_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001"))
  1523. message_files = mapped_column(sa.Text, nullable=True)
  1524. answer = mapped_column(sa.Text, nullable=True)
  1525. answer_token: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
  1526. answer_unit_price = mapped_column(sa.Numeric, nullable=True)
  1527. answer_price_unit = mapped_column(sa.Numeric(10, 7), nullable=False, server_default=sa.text("0.001"))
  1528. tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
  1529. total_price = mapped_column(sa.Numeric, nullable=True)
  1530. currency = mapped_column(String, nullable=True)
  1531. latency: Mapped[float | None] = mapped_column(sa.Float, nullable=True)
  1532. created_by_role = mapped_column(String, nullable=False)
  1533. created_by = mapped_column(StringUUID, nullable=False)
  1534. created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.func.current_timestamp())
  1535. @property
  1536. def files(self) -> list[Any]:
  1537. if self.message_files:
  1538. return cast(list[Any], json.loads(self.message_files))
  1539. else:
  1540. return []
  1541. @property
  1542. def tools(self) -> list[str]:
  1543. return self.tool.split(";") if self.tool else []
  1544. @property
  1545. def tool_labels(self) -> dict[str, Any]:
  1546. try:
  1547. if self.tool_labels_str:
  1548. return cast(dict[str, Any], json.loads(self.tool_labels_str))
  1549. else:
  1550. return {}
  1551. except Exception:
  1552. return {}
  1553. @property
  1554. def tool_meta(self) -> dict[str, Any]:
  1555. try:
  1556. if self.tool_meta_str:
  1557. return cast(dict[str, Any], json.loads(self.tool_meta_str))
  1558. else:
  1559. return {}
  1560. except Exception:
  1561. return {}
  1562. @property
  1563. def tool_inputs_dict(self) -> dict[str, Any]:
  1564. tools = self.tools
  1565. try:
  1566. if self.tool_input:
  1567. data = json.loads(self.tool_input)
  1568. result: dict[str, Any] = {}
  1569. for tool in tools:
  1570. if tool in data:
  1571. result[tool] = data[tool]
  1572. else:
  1573. if len(tools) == 1:
  1574. result[tool] = data
  1575. else:
  1576. result[tool] = {}
  1577. return result
  1578. else:
  1579. return {tool: {} for tool in tools}
  1580. except Exception:
  1581. return {}
  1582. @property
  1583. def tool_outputs_dict(self) -> dict[str, Any]:
  1584. tools = self.tools
  1585. try:
  1586. if self.observation:
  1587. data = json.loads(self.observation)
  1588. result: dict[str, Any] = {}
  1589. for tool in tools:
  1590. if tool in data:
  1591. result[tool] = data[tool]
  1592. else:
  1593. if len(tools) == 1:
  1594. result[tool] = data
  1595. else:
  1596. result[tool] = {}
  1597. return result
  1598. else:
  1599. return {tool: {} for tool in tools}
  1600. except Exception:
  1601. if self.observation:
  1602. return dict.fromkeys(tools, self.observation)
  1603. else:
  1604. return {}
  1605. class DatasetRetrieverResource(Base):
  1606. __tablename__ = "dataset_retriever_resources"
  1607. __table_args__ = (
  1608. sa.PrimaryKeyConstraint("id", name="dataset_retriever_resource_pkey"),
  1609. sa.Index("dataset_retriever_resource_message_id_idx", "message_id"),
  1610. )
  1611. id = mapped_column(StringUUID, nullable=False, server_default=sa.text("uuid_generate_v4()"))
  1612. message_id = mapped_column(StringUUID, nullable=False)
  1613. position: Mapped[int] = mapped_column(sa.Integer, nullable=False)
  1614. dataset_id = mapped_column(StringUUID, nullable=False)
  1615. dataset_name = mapped_column(sa.Text, nullable=False)
  1616. document_id = mapped_column(StringUUID, nullable=True)
  1617. document_name = mapped_column(sa.Text, nullable=False)
  1618. data_source_type = mapped_column(sa.Text, nullable=True)
  1619. segment_id = mapped_column(StringUUID, nullable=True)
  1620. score: Mapped[float | None] = mapped_column(sa.Float, nullable=True)
  1621. content = mapped_column(sa.Text, nullable=False)
  1622. hit_count: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
  1623. word_count: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
  1624. segment_position: Mapped[int | None] = mapped_column(sa.Integer, nullable=True)
  1625. index_node_hash = mapped_column(sa.Text, nullable=True)
  1626. retriever_from = mapped_column(sa.Text, nullable=False)
  1627. created_by = mapped_column(StringUUID, nullable=False)
  1628. created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.func.current_timestamp())
  1629. class Tag(Base):
  1630. __tablename__ = "tags"
  1631. __table_args__ = (
  1632. sa.PrimaryKeyConstraint("id", name="tag_pkey"),
  1633. sa.Index("tag_type_idx", "type"),
  1634. sa.Index("tag_name_idx", "name"),
  1635. )
  1636. TAG_TYPE_LIST = ["knowledge", "app"]
  1637. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1638. tenant_id = mapped_column(StringUUID, nullable=True)
  1639. type = mapped_column(String(16), nullable=False)
  1640. name: Mapped[str] = mapped_column(String(255), nullable=False)
  1641. created_by = mapped_column(StringUUID, nullable=False)
  1642. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1643. class TagBinding(Base):
  1644. __tablename__ = "tag_bindings"
  1645. __table_args__ = (
  1646. sa.PrimaryKeyConstraint("id", name="tag_binding_pkey"),
  1647. sa.Index("tag_bind_target_id_idx", "target_id"),
  1648. sa.Index("tag_bind_tag_id_idx", "tag_id"),
  1649. )
  1650. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1651. tenant_id = mapped_column(StringUUID, nullable=True)
  1652. tag_id = mapped_column(StringUUID, nullable=True)
  1653. target_id = mapped_column(StringUUID, nullable=True)
  1654. created_by = mapped_column(StringUUID, nullable=False)
  1655. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1656. class TraceAppConfig(Base):
  1657. __tablename__ = "trace_app_config"
  1658. __table_args__ = (
  1659. sa.PrimaryKeyConstraint("id", name="tracing_app_config_pkey"),
  1660. sa.Index("trace_app_config_app_id_idx", "app_id"),
  1661. )
  1662. id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
  1663. app_id = mapped_column(StringUUID, nullable=False)
  1664. tracing_provider = mapped_column(String(255), nullable=True)
  1665. tracing_config = mapped_column(sa.JSON, nullable=True)
  1666. created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
  1667. updated_at = mapped_column(
  1668. sa.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
  1669. )
  1670. is_active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"))
  1671. @property
  1672. def tracing_config_dict(self) -> dict[str, Any]:
  1673. return self.tracing_config or {}
  1674. @property
  1675. def tracing_config_str(self) -> str:
  1676. return json.dumps(self.tracing_config_dict)
  1677. def to_dict(self) -> dict[str, Any]:
  1678. return {
  1679. "id": self.id,
  1680. "app_id": self.app_id,
  1681. "tracing_provider": self.tracing_provider,
  1682. "tracing_config": self.tracing_config_dict,
  1683. "is_active": self.is_active,
  1684. "created_at": str(self.created_at) if self.created_at else None,
  1685. "updated_at": str(self.updated_at) if self.updated_at else None,
  1686. }