client.py 16 KB

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