You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. import json
  2. from typing import Literal
  3. import requests
  4. class DifyClient:
  5. def __init__(self, api_key, base_url: str = "https://api.dify.ai/v1"):
  6. self.api_key = api_key
  7. self.base_url = base_url
  8. def _send_request(self, method: str, endpoint: str, json: dict | None = None, params: dict | None = None, stream: bool = False):
  9. headers = {
  10. "Authorization": f"Bearer {self.api_key}",
  11. "Content-Type": "application/json",
  12. }
  13. url = f"{self.base_url}{endpoint}"
  14. response = requests.request(
  15. method, url, json=json, params=params, headers=headers, stream=stream
  16. )
  17. return response
  18. def _send_request_with_files(self, method, endpoint, data, files):
  19. headers = {"Authorization": f"Bearer {self.api_key}"}
  20. url = f"{self.base_url}{endpoint}"
  21. response = requests.request(
  22. method, url, data=data, headers=headers, files=files
  23. )
  24. return response
  25. def message_feedback(self, message_id: str, rating: Literal["like", "dislike"], user: str):
  26. data = {"rating": rating, "user": user}
  27. return self._send_request("POST", f"/messages/{message_id}/feedbacks", data)
  28. def get_application_parameters(self, user: str):
  29. params = {"user": user}
  30. return self._send_request("GET", "/parameters", params=params)
  31. def file_upload(self, user: str, files: dict):
  32. data = {"user": user}
  33. return self._send_request_with_files(
  34. "POST", "/files/upload", data=data, files=files
  35. )
  36. def text_to_audio(self, text: str, user: str, streaming: bool = False):
  37. data = {"text": text, "user": user, "streaming": streaming}
  38. return self._send_request("POST", "/text-to-audio", json=data)
  39. def get_meta(self, user: str):
  40. params = {"user": user}
  41. return self._send_request("GET", "/meta", params=params)
  42. class CompletionClient(DifyClient):
  43. def create_completion_message(self, inputs: dict, response_mode: Literal["blocking", "streaming"], user: str, files: dict | None = None):
  44. data = {
  45. "inputs": inputs,
  46. "response_mode": response_mode,
  47. "user": user,
  48. "files": files,
  49. }
  50. return self._send_request(
  51. "POST",
  52. "/completion-messages",
  53. data,
  54. stream=True if response_mode == "streaming" else False,
  55. )
  56. class ChatClient(DifyClient):
  57. def create_chat_message(
  58. self,
  59. inputs: dict,
  60. query: str,
  61. user: str,
  62. response_mode: Literal["blocking", "streaming"] = "blocking",
  63. conversation_id: str | None = None,
  64. files: dict | None = None,
  65. ):
  66. data = {
  67. "inputs": inputs,
  68. "query": query,
  69. "user": user,
  70. "response_mode": response_mode,
  71. "files": files,
  72. }
  73. if conversation_id:
  74. data["conversation_id"] = conversation_id
  75. return self._send_request(
  76. "POST",
  77. "/chat-messages",
  78. data,
  79. stream=True if response_mode == "streaming" else False,
  80. )
  81. def get_suggested(self, message_id: str, user: str):
  82. params = {"user": user}
  83. return self._send_request(
  84. "GET", f"/messages/{message_id}/suggested", params=params
  85. )
  86. def stop_message(self, task_id: str, user: str):
  87. data = {"user": user}
  88. return self._send_request("POST", f"/chat-messages/{task_id}/stop", data)
  89. def get_conversations(
  90. self,
  91. user: str,
  92. last_id: str | None = None,
  93. limit: int | None = None,
  94. pinned: bool | None = None
  95. ):
  96. params = {"user": user, "last_id": last_id,
  97. "limit": limit, "pinned": pinned}
  98. return self._send_request("GET", "/conversations", params=params)
  99. def get_conversation_messages(
  100. self,
  101. user: str,
  102. conversation_id: str | None = None,
  103. first_id: str | None = None,
  104. limit: int | None = None
  105. ):
  106. params = {"user": user}
  107. if conversation_id:
  108. params["conversation_id"] = conversation_id
  109. if first_id:
  110. params["first_id"] = first_id
  111. if limit:
  112. params["limit"] = limit
  113. return self._send_request("GET", "/messages", params=params)
  114. def rename_conversation(
  115. self, conversation_id: str, name: str, auto_generate: bool, user: str
  116. ):
  117. data = {"name": name, "auto_generate": auto_generate, "user": user}
  118. return self._send_request(
  119. "POST", f"/conversations/{conversation_id}/name", data
  120. )
  121. def delete_conversation(self, conversation_id: str, user: str):
  122. data = {"user": user}
  123. return self._send_request("DELETE", f"/conversations/{conversation_id}", data)
  124. def audio_to_text(self, audio_file: dict, user: str):
  125. data = {"user": user}
  126. files = {"audio_file": audio_file}
  127. return self._send_request_with_files("POST", "/audio-to-text", data, files)
  128. class WorkflowClient(DifyClient):
  129. def run(
  130. self, inputs: dict, response_mode: Literal["blocking", "streaming"] = "streaming", user: str = "abc-123"
  131. ):
  132. data = {"inputs": inputs, "response_mode": response_mode, "user": user}
  133. return self._send_request("POST", "/workflows/run", data)
  134. def stop(self, task_id, user):
  135. data = {"user": user}
  136. return self._send_request("POST", f"/workflows/tasks/{task_id}/stop", data)
  137. def get_result(self, workflow_run_id):
  138. return self._send_request("GET", f"/workflows/run/{workflow_run_id}")
  139. class KnowledgeBaseClient(DifyClient):
  140. def __init__(
  141. self,
  142. api_key: str,
  143. base_url: str = "https://api.dify.ai/v1",
  144. dataset_id: str | None = None,
  145. ):
  146. """
  147. Construct a KnowledgeBaseClient object.
  148. Args:
  149. api_key (str): API key of Dify.
  150. base_url (str, optional): Base URL of Dify API. Defaults to 'https://api.dify.ai/v1'.
  151. dataset_id (str, optional): ID of the dataset. Defaults to None. You don't need this if you just want to
  152. create a new dataset. or list datasets. otherwise you need to set this.
  153. """
  154. super().__init__(api_key=api_key, base_url=base_url)
  155. self.dataset_id = dataset_id
  156. def _get_dataset_id(self):
  157. if self.dataset_id is None:
  158. raise ValueError("dataset_id is not set")
  159. return self.dataset_id
  160. def create_dataset(self, name: str, **kwargs):
  161. return self._send_request("POST", "/datasets", {"name": name}, **kwargs)
  162. def list_datasets(self, page: int = 1, page_size: int = 20, **kwargs):
  163. return self._send_request(
  164. "GET", f"/datasets?page={page}&limit={page_size}", **kwargs
  165. )
  166. def create_document_by_text(
  167. self, name, text, extra_params: dict | None = None, **kwargs
  168. ):
  169. """
  170. Create a document by text.
  171. :param name: Name of the document
  172. :param text: Text content of the document
  173. :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
  174. e.g.
  175. {
  176. 'indexing_technique': 'high_quality',
  177. 'process_rule': {
  178. 'rules': {
  179. 'pre_processing_rules': [
  180. {'id': 'remove_extra_spaces', 'enabled': True},
  181. {'id': 'remove_urls_emails', 'enabled': True}
  182. ],
  183. 'segmentation': {
  184. 'separator': '\n',
  185. 'max_tokens': 500
  186. }
  187. },
  188. 'mode': 'custom'
  189. }
  190. }
  191. :return: Response from the API
  192. """
  193. data = {
  194. "indexing_technique": "high_quality",
  195. "process_rule": {"mode": "automatic"},
  196. "name": name,
  197. "text": text,
  198. }
  199. if extra_params is not None and isinstance(extra_params, dict):
  200. data.update(extra_params)
  201. url = f"/datasets/{self._get_dataset_id()}/document/create_by_text"
  202. return self._send_request("POST", url, json=data, **kwargs)
  203. def update_document_by_text(
  204. self, document_id: str, name: str, text: str, extra_params: dict | None = None, **kwargs
  205. ):
  206. """
  207. Update a document by text.
  208. :param document_id: ID of the document
  209. :param name: Name of the document
  210. :param text: Text content of the document
  211. :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
  212. e.g.
  213. {
  214. 'indexing_technique': 'high_quality',
  215. 'process_rule': {
  216. 'rules': {
  217. 'pre_processing_rules': [
  218. {'id': 'remove_extra_spaces', 'enabled': True},
  219. {'id': 'remove_urls_emails', 'enabled': True}
  220. ],
  221. 'segmentation': {
  222. 'separator': '\n',
  223. 'max_tokens': 500
  224. }
  225. },
  226. 'mode': 'custom'
  227. }
  228. }
  229. :return: Response from the API
  230. """
  231. data = {"name": name, "text": text}
  232. if extra_params is not None and isinstance(extra_params, dict):
  233. data.update(extra_params)
  234. url = (
  235. f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_text"
  236. )
  237. return self._send_request("POST", url, json=data, **kwargs)
  238. def create_document_by_file(
  239. self, file_path: str, original_document_id: str | None = None, extra_params: dict | None = None
  240. ):
  241. """
  242. Create a document by file.
  243. :param file_path: Path to the file
  244. :param original_document_id: pass this ID if you want to replace the original document (optional)
  245. :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
  246. e.g.
  247. {
  248. 'indexing_technique': 'high_quality',
  249. 'process_rule': {
  250. 'rules': {
  251. 'pre_processing_rules': [
  252. {'id': 'remove_extra_spaces', 'enabled': True},
  253. {'id': 'remove_urls_emails', 'enabled': True}
  254. ],
  255. 'segmentation': {
  256. 'separator': '\n',
  257. 'max_tokens': 500
  258. }
  259. },
  260. 'mode': 'custom'
  261. }
  262. }
  263. :return: Response from the API
  264. """
  265. files = {"file": open(file_path, "rb")}
  266. data = {
  267. "process_rule": {"mode": "automatic"},
  268. "indexing_technique": "high_quality",
  269. }
  270. if extra_params is not None and isinstance(extra_params, dict):
  271. data.update(extra_params)
  272. if original_document_id is not None:
  273. data["original_document_id"] = original_document_id
  274. url = f"/datasets/{self._get_dataset_id()}/document/create_by_file"
  275. return self._send_request_with_files(
  276. "POST", url, {"data": json.dumps(data)}, files
  277. )
  278. def update_document_by_file(
  279. self, document_id: str, file_path: str, extra_params: dict | None = None
  280. ):
  281. """
  282. Update a document by file.
  283. :param document_id: ID of the document
  284. :param file_path: Path to the file
  285. :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
  286. e.g.
  287. {
  288. 'indexing_technique': 'high_quality',
  289. 'process_rule': {
  290. 'rules': {
  291. 'pre_processing_rules': [
  292. {'id': 'remove_extra_spaces', 'enabled': True},
  293. {'id': 'remove_urls_emails', 'enabled': True}
  294. ],
  295. 'segmentation': {
  296. 'separator': '\n',
  297. 'max_tokens': 500
  298. }
  299. },
  300. 'mode': 'custom'
  301. }
  302. }
  303. :return:
  304. """
  305. files = {"file": open(file_path, "rb")}
  306. data = {}
  307. if extra_params is not None and isinstance(extra_params, dict):
  308. data.update(extra_params)
  309. url = (
  310. f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_file"
  311. )
  312. return self._send_request_with_files(
  313. "POST", url, {"data": json.dumps(data)}, files
  314. )
  315. def batch_indexing_status(self, batch_id: str, **kwargs):
  316. """
  317. Get the status of the batch indexing.
  318. :param batch_id: ID of the batch uploading
  319. :return: Response from the API
  320. """
  321. url = f"/datasets/{self._get_dataset_id()}/documents/{batch_id}/indexing-status"
  322. return self._send_request("GET", url, **kwargs)
  323. def delete_dataset(self):
  324. """
  325. Delete this dataset.
  326. :return: Response from the API
  327. """
  328. url = f"/datasets/{self._get_dataset_id()}"
  329. return self._send_request("DELETE", url)
  330. def delete_document(self, document_id: str):
  331. """
  332. Delete a document.
  333. :param document_id: ID of the document
  334. :return: Response from the API
  335. """
  336. url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}"
  337. return self._send_request("DELETE", url)
  338. def list_documents(
  339. self,
  340. page: int | None = None,
  341. page_size: int | None = None,
  342. keyword: str | None = None,
  343. **kwargs,
  344. ):
  345. """
  346. Get a list of documents in this dataset.
  347. :return: Response from the API
  348. """
  349. params = {}
  350. if page is not None:
  351. params["page"] = page
  352. if page_size is not None:
  353. params["limit"] = page_size
  354. if keyword is not None:
  355. params["keyword"] = keyword
  356. url = f"/datasets/{self._get_dataset_id()}/documents"
  357. return self._send_request("GET", url, params=params, **kwargs)
  358. def add_segments(self, document_id: str, segments: list[dict], **kwargs):
  359. """
  360. Add segments to a document.
  361. :param document_id: ID of the document
  362. :param segments: List of segments to add, example: [{"content": "1", "answer": "1", "keyword": ["a"]}]
  363. :return: Response from the API
  364. """
  365. data = {"segments": segments}
  366. url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
  367. return self._send_request("POST", url, json=data, **kwargs)
  368. def query_segments(
  369. self,
  370. document_id: str,
  371. keyword: str | None = None,
  372. status: str | None = None,
  373. **kwargs,
  374. ):
  375. """
  376. Query segments in this document.
  377. :param document_id: ID of the document
  378. :param keyword: query keyword, optional
  379. :param status: status of the segment, optional, e.g. completed
  380. """
  381. url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
  382. params = {}
  383. if keyword is not None:
  384. params["keyword"] = keyword
  385. if status is not None:
  386. params["status"] = status
  387. if "params" in kwargs:
  388. params.update(kwargs["params"])
  389. return self._send_request("GET", url, params=params, **kwargs)
  390. def delete_document_segment(self, document_id: str, segment_id: str):
  391. """
  392. Delete a segment from a document.
  393. :param document_id: ID of the document
  394. :param segment_id: ID of the segment
  395. :return: Response from the API
  396. """
  397. url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
  398. return self._send_request("DELETE", url)
  399. def update_document_segment(self, document_id: str, segment_id: str, segment_data: dict, **kwargs):
  400. """
  401. Update a segment in a document.
  402. :param document_id: ID of the document
  403. :param segment_id: ID of the segment
  404. :param segment_data: Data of the segment, example: {"content": "1", "answer": "1", "keyword": ["a"], "enabled": True}
  405. :return: Response from the API
  406. """
  407. data = {"segment": segment_data}
  408. url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
  409. return self._send_request("POST", url, json=data, **kwargs)