dify client SDK hopes to add asynchronous functionality #15235

Closed
opened 2026-02-21 19:20:48 -05:00 by yindo · 6 comments
Owner

Originally created by @InfernalAzazel on GitHub (Jul 10, 2025).

Self Checks

  • I have searched for existing issues search for existing issues, including closed ones.
  • I confirm that I am using English to submit this report (我已阅读并同意 Language Policy).
  • [FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)
  • Please do not modify this template :) and fill in all the required fields.

1. Is this request related to a challenge you're experiencing? Tell me about your story.

Code

import json
import anyio
import httpx


def in_async_context() -> bool:
    try:
        anyio.get_current_task()
        return True
    except RuntimeError:
        return False

class DifyClient:
    def __init__(self, api_key, base_url = "https://api.dify.ai/v1"):
        self.api_key = api_key
        self.base_url = base_url

    async def _request_async(
            self, method: str,
            endpoint: str,
            json= None,
            params=None,
            stream: bool = False,
    ) -> httpx.Response:
        async with httpx.AsyncClient() as client:
            headers = {
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            }
            url = f"{self.base_url}{endpoint}"
            request = httpx.Request(method, url, json=json, params=params, headers=headers)
            return await client.send(request, stream=stream)

    def _send_request(self, method, endpoint, json=None, params=None, stream=False):
        coro = self._request_async(method, endpoint, json, params, stream)
        if in_async_context():
            return coro  # Return coroutine, external await
        else:
            return anyio.run(lambda: coro)  # Synchronize the environment and run automatically

    def _send_request_with_files(self, method, endpoint, data,files):
        headers = {"Authorization": f"Bearer {self.api_key}"}

        url = f"{self.base_url}{endpoint}"
        response = httpx.request(
            method, url, data=data, headers=headers, files=files
        )

        return response

    def message_feedback(self, message_id, rating, user):
        data = {"rating": rating, "user": user}
        return self._send_request("POST", f"/messages/{message_id}/feedbacks", data)

    def get_application_parameters(self, user):
        params = {"user": user}
        return self._send_request("GET", "/parameters", params=params)

    def file_upload(self, user, files):
        data = {"user": user}
        return self._send_request_with_files(
            "POST", "/files/upload", data=data, files=files
        )

    def text_to_audio(self, text: str, user: str, streaming: bool = False):
        data = {"text": text, "user": user, "streaming": streaming}
        return self._send_request("POST", "/text-to-audio", json=data)

    def get_meta(self, user):
        params = {"user": user}
        return self._send_request("GET", "/meta", params=params)


class CompletionClient(DifyClient):
    def create_completion_message(self, inputs, response_mode, user, files=None):
        data = {
            "inputs": inputs,
            "response_mode": response_mode,
            "user": user,
            "files": files,
        }
        return self._send_request(
            "POST",
            "/completion-messages",
            data,
            stream=True if response_mode == "streaming" else False,
        )


class ChatClient(DifyClient):
    def create_chat_message(
        self,
        inputs,
        query,
        user,
        response_mode="blocking",
        conversation_id=None,
        files=None,
    ):
        data = {
            "inputs": inputs,
            "query": query,
            "user": user,
            "response_mode": response_mode,
            "files": files,
        }
        if conversation_id:
            data["conversation_id"] = conversation_id

        return self._send_request(
            "POST",
            "/chat-messages",
            data,
            stream=True if response_mode == "streaming" else False,
        )

    def get_suggested(self, message_id, user: str):
        params = {"user": user}
        return self._send_request(
            "GET", f"/messages/{message_id}/suggested", params=params
        )

    def stop_message(self, task_id, user):
        data = {"user": user}
        return self._send_request("POST", f"/chat-messages/{task_id}/stop", data)

    def get_conversations(self, user, last_id=None, limit=None, pinned=None):
        params = {"user": user, "last_id": last_id, "limit": limit, "pinned": pinned}
        return self._send_request("GET", "/conversations", params=params)

    def get_conversation_messages(
        self, user, conversation_id=None, first_id=None, limit=None
    ):
        params = {"user": user}

        if conversation_id:
            params["conversation_id"] = conversation_id
        if first_id:
            params["first_id"] = first_id
        if limit:
            params["limit"] = limit

        return self._send_request("GET", "/messages", params=params)

    def rename_conversation(
        self, conversation_id, name, auto_generate: bool, user: str
    ):
        data = {"name": name, "auto_generate": auto_generate, "user": user}
        return self._send_request(
            "POST", f"/conversations/{conversation_id}/name", data
        )

    def delete_conversation(self, conversation_id, user):
        data = {"user": user}
        return self._send_request("DELETE", f"/conversations/{conversation_id}", data)

    def audio_to_text(self, audio_file, user):
        data = {"user": user}
        files = {"audio_file": audio_file}
        return self._send_request_with_files("POST", "/audio-to-text", data, files)


class WorkflowClient(DifyClient):
    def run(
        self, inputs: dict, response_mode: str = "streaming", user: str = "abc-123"
    ):
        data = {"inputs": inputs, "response_mode": response_mode, "user": user}
        return self._send_request("POST", "/workflows/run", data)

    def stop(self, task_id, user):
        data = {"user": user}
        return self._send_request("POST", f"/workflows/tasks/{task_id}/stop", data)

    def get_result(self, workflow_run_id):
        return self._send_request("GET", f"/workflows/run/{workflow_run_id}")


class KnowledgeBaseClient(DifyClient):
    def __init__(
        self,
        api_key,
        base_url: str = "https://api.dify.ai/v1",
        dataset_id: str | None = None,
    ):
        """
        Construct a KnowledgeBaseClient object.

        Args:
            api_key (str): API key of Dify.
            base_url (str, optional): Base URL of Dify API. Defaults to 'https://api.dify.ai/v1'.
            dataset_id (str, optional): ID of the dataset. Defaults to None. You don't need this if you just want to
                create a new dataset. or list datasets. otherwise you need to set this.
        """
        super().__init__(api_key=api_key, base_url=base_url)
        self.dataset_id = dataset_id

    def _get_dataset_id(self):
        if self.dataset_id is None:
            raise ValueError("dataset_id is not set")
        return self.dataset_id

    def create_dataset(self, name: str, **kwargs):
        return self._send_request("POST", "/datasets", {"name": name}, **kwargs)

    def list_datasets(self, page: int = 1, page_size: int = 20, **kwargs):
        return self._send_request(
            "GET", f"/datasets?page={page}&limit={page_size}", **kwargs
        )

    def create_document_by_text(
        self, name, text, extra_params: dict | None = None, **kwargs
    ):
        """
        Create a document by text.

        :param name: Name of the document
        :param text: Text content of the document
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        data = {
            "indexing_technique": "high_quality",
            "process_rule": {"mode": "automatic"},
            "name": name,
            "text": text,
        }
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = f"/datasets/{self._get_dataset_id()}/document/create_by_text"
        return self._send_request("POST", url, json=data, **kwargs)

    def update_document_by_text(
        self, document_id, name, text, extra_params: dict | None = None, **kwargs
    ):
        """
        Update a document by text.

        :param document_id: ID of the document
        :param name: Name of the document
        :param text: Text content of the document
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        data = {"name": name, "text": text}
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = (
            f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_text"
        )
        return self._send_request("POST", url, json=data, **kwargs)

    def create_document_by_file(
        self, file_path, original_document_id=None, extra_params: dict | None = None
    ):
        """
        Create a document by file.

        :param file_path: Path to the file
        :param original_document_id: pass this ID if you want to replace the original document (optional)
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        files = {"file": open(file_path, "rb")}
        data = {
            "process_rule": {"mode": "automatic"},
            "indexing_technique": "high_quality",
        }
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        if original_document_id is not None:
            data["original_document_id"] = original_document_id
        url = f"/datasets/{self._get_dataset_id()}/document/create_by_file"
        return self._send_request_with_files(
            "POST", url, {"data": json.dumps(data)}, files
        )

    def update_document_by_file(
        self, document_id, file_path, extra_params: dict | None = None
    ):
        """
        Update a document by file.

        :param document_id: ID of the document
        :param file_path: Path to the file
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return:
        """
        files = {"file": open(file_path, "rb")}
        data = {}
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = (
            f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_file"
        )
        return self._send_request_with_files(
            "POST", url, {"data": json.dumps(data)}, files
        )

    def batch_indexing_status(self, batch_id: str, **kwargs):
        """
        Get the status of the batch indexing.

        :param batch_id: ID of the batch uploading
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{batch_id}/indexing-status"
        return self._send_request("GET", url, **kwargs)

    def delete_dataset(self):
        """
        Delete this dataset.

        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}"
        return self._send_request("DELETE", url)

    def delete_document(self, document_id):
        """
        Delete a document.

        :param document_id: ID of the document
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}"
        return self._send_request("DELETE", url)

    def list_documents(
        self,
        page: int | None = None,
        page_size: int | None = None,
        keyword: str | None = None,
        **kwargs,
    ):
        """
        Get a list of documents in this dataset.

        :return: Response from the API
        """
        params = {}
        if page is not None:
            params["page"] = page
        if page_size is not None:
            params["limit"] = page_size
        if keyword is not None:
            params["keyword"] = keyword
        url = f"/datasets/{self._get_dataset_id()}/documents"
        return self._send_request("GET", url, params=params, **kwargs)

    def add_segments(self, document_id, segments, **kwargs):
        """
        Add segments to a document.

        :param document_id: ID of the document
        :param segments: List of segments to add, example: [{"content": "1", "answer": "1", "keyword": ["a"]}]
        :return: Response from the API
        """
        data = {"segments": segments}
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
        return self._send_request("POST", url, json=data, **kwargs)

    def query_segments(
        self,
        document_id,
        keyword: str | None = None,
        status: str | None = None,
        **kwargs,
    ):
        """
        Query segments in this document.

        :param document_id: ID of the document
        :param keyword: query keyword, optional
        :param status: status of the segment, optional, e.g. completed
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
        params = {}
        if keyword is not None:
            params["keyword"] = keyword
        if status is not None:
            params["status"] = status
        if "params" in kwargs:
            params.update(kwargs["params"])
        return self._send_request("GET", url, params=params, **kwargs)

    def delete_document_segment(self, document_id, segment_id):
        """
        Delete a segment from a document.

        :param document_id: ID of the document
        :param segment_id: ID of the segment
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
        return self._send_request("DELETE", url)

    def update_document_segment(self, document_id, segment_id, segment_data, **kwargs):
        """
        Update a segment in a document.

        :param document_id: ID of the document
        :param segment_id: ID of the segment
        :param segment_data: Data of the segment, example: {"content": "1", "answer": "1", "keyword": ["a"], "enabled": True}
        :return: Response from the API
        """
        data = {"segment": segment_data}
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
        return self._send_request("POST", url, json=data, **kwargs)



if __name__ == "__main__":
    import asyncio
    async def main():
        client = ChatClient("key", 'http://127.0.0.1/v1')
        res = await client.get_conversations("test", limit=2)
        print(res.json())
        
    client = ChatClient("key", 'http://127.0.0.1/v1')
    res = client.get_conversations("test", limit=1)
    print(res.json())
    asyncio.run(main())

effect:

Image

2. Additional context or comments

No response

3. Can you help us with this feature?

  • I am interested in contributing to this feature.
Originally created by @InfernalAzazel on GitHub (Jul 10, 2025). ### Self Checks - [x] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. - [x] I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). - [x] [FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:) - [x] Please do not modify this template :) and fill in all the required fields. ### 1. Is this request related to a challenge you're experiencing? Tell me about your story. Code ```python import json import anyio import httpx def in_async_context() -> bool: try: anyio.get_current_task() return True except RuntimeError: return False class DifyClient: def __init__(self, api_key, base_url = "https://api.dify.ai/v1"): self.api_key = api_key self.base_url = base_url async def _request_async( self, method: str, endpoint: str, json= None, params=None, stream: bool = False, ) -> httpx.Response: async with httpx.AsyncClient() as client: headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } url = f"{self.base_url}{endpoint}" request = httpx.Request(method, url, json=json, params=params, headers=headers) return await client.send(request, stream=stream) def _send_request(self, method, endpoint, json=None, params=None, stream=False): coro = self._request_async(method, endpoint, json, params, stream) if in_async_context(): return coro # Return coroutine, external await else: return anyio.run(lambda: coro) # Synchronize the environment and run automatically def _send_request_with_files(self, method, endpoint, data,files): headers = {"Authorization": f"Bearer {self.api_key}"} url = f"{self.base_url}{endpoint}" response = httpx.request( method, url, data=data, headers=headers, files=files ) return response def message_feedback(self, message_id, rating, user): data = {"rating": rating, "user": user} return self._send_request("POST", f"/messages/{message_id}/feedbacks", data) def get_application_parameters(self, user): params = {"user": user} return self._send_request("GET", "/parameters", params=params) def file_upload(self, user, files): data = {"user": user} return self._send_request_with_files( "POST", "/files/upload", data=data, files=files ) def text_to_audio(self, text: str, user: str, streaming: bool = False): data = {"text": text, "user": user, "streaming": streaming} return self._send_request("POST", "/text-to-audio", json=data) def get_meta(self, user): params = {"user": user} return self._send_request("GET", "/meta", params=params) class CompletionClient(DifyClient): def create_completion_message(self, inputs, response_mode, user, files=None): data = { "inputs": inputs, "response_mode": response_mode, "user": user, "files": files, } return self._send_request( "POST", "/completion-messages", data, stream=True if response_mode == "streaming" else False, ) class ChatClient(DifyClient): def create_chat_message( self, inputs, query, user, response_mode="blocking", conversation_id=None, files=None, ): data = { "inputs": inputs, "query": query, "user": user, "response_mode": response_mode, "files": files, } if conversation_id: data["conversation_id"] = conversation_id return self._send_request( "POST", "/chat-messages", data, stream=True if response_mode == "streaming" else False, ) def get_suggested(self, message_id, user: str): params = {"user": user} return self._send_request( "GET", f"/messages/{message_id}/suggested", params=params ) def stop_message(self, task_id, user): data = {"user": user} return self._send_request("POST", f"/chat-messages/{task_id}/stop", data) def get_conversations(self, user, last_id=None, limit=None, pinned=None): params = {"user": user, "last_id": last_id, "limit": limit, "pinned": pinned} return self._send_request("GET", "/conversations", params=params) def get_conversation_messages( self, user, conversation_id=None, first_id=None, limit=None ): params = {"user": user} if conversation_id: params["conversation_id"] = conversation_id if first_id: params["first_id"] = first_id if limit: params["limit"] = limit return self._send_request("GET", "/messages", params=params) def rename_conversation( self, conversation_id, name, auto_generate: bool, user: str ): data = {"name": name, "auto_generate": auto_generate, "user": user} return self._send_request( "POST", f"/conversations/{conversation_id}/name", data ) def delete_conversation(self, conversation_id, user): data = {"user": user} return self._send_request("DELETE", f"/conversations/{conversation_id}", data) def audio_to_text(self, audio_file, user): data = {"user": user} files = {"audio_file": audio_file} return self._send_request_with_files("POST", "/audio-to-text", data, files) class WorkflowClient(DifyClient): def run( self, inputs: dict, response_mode: str = "streaming", user: str = "abc-123" ): data = {"inputs": inputs, "response_mode": response_mode, "user": user} return self._send_request("POST", "/workflows/run", data) def stop(self, task_id, user): data = {"user": user} return self._send_request("POST", f"/workflows/tasks/{task_id}/stop", data) def get_result(self, workflow_run_id): return self._send_request("GET", f"/workflows/run/{workflow_run_id}") class KnowledgeBaseClient(DifyClient): def __init__( self, api_key, base_url: str = "https://api.dify.ai/v1", dataset_id: str | None = None, ): """ Construct a KnowledgeBaseClient object. Args: api_key (str): API key of Dify. base_url (str, optional): Base URL of Dify API. Defaults to 'https://api.dify.ai/v1'. dataset_id (str, optional): ID of the dataset. Defaults to None. You don't need this if you just want to create a new dataset. or list datasets. otherwise you need to set this. """ super().__init__(api_key=api_key, base_url=base_url) self.dataset_id = dataset_id def _get_dataset_id(self): if self.dataset_id is None: raise ValueError("dataset_id is not set") return self.dataset_id def create_dataset(self, name: str, **kwargs): return self._send_request("POST", "/datasets", {"name": name}, **kwargs) def list_datasets(self, page: int = 1, page_size: int = 20, **kwargs): return self._send_request( "GET", f"/datasets?page={page}&limit={page_size}", **kwargs ) def create_document_by_text( self, name, text, extra_params: dict | None = None, **kwargs ): """ Create a document by text. :param name: Name of the document :param text: Text content of the document :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ data = { "indexing_technique": "high_quality", "process_rule": {"mode": "automatic"}, "name": name, "text": text, } if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = f"/datasets/{self._get_dataset_id()}/document/create_by_text" return self._send_request("POST", url, json=data, **kwargs) def update_document_by_text( self, document_id, name, text, extra_params: dict | None = None, **kwargs ): """ Update a document by text. :param document_id: ID of the document :param name: Name of the document :param text: Text content of the document :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ data = {"name": name, "text": text} if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = ( f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_text" ) return self._send_request("POST", url, json=data, **kwargs) def create_document_by_file( self, file_path, original_document_id=None, extra_params: dict | None = None ): """ Create a document by file. :param file_path: Path to the file :param original_document_id: pass this ID if you want to replace the original document (optional) :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ files = {"file": open(file_path, "rb")} data = { "process_rule": {"mode": "automatic"}, "indexing_technique": "high_quality", } if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) if original_document_id is not None: data["original_document_id"] = original_document_id url = f"/datasets/{self._get_dataset_id()}/document/create_by_file" return self._send_request_with_files( "POST", url, {"data": json.dumps(data)}, files ) def update_document_by_file( self, document_id, file_path, extra_params: dict | None = None ): """ Update a document by file. :param document_id: ID of the document :param file_path: Path to the file :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: """ files = {"file": open(file_path, "rb")} data = {} if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = ( f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_file" ) return self._send_request_with_files( "POST", url, {"data": json.dumps(data)}, files ) def batch_indexing_status(self, batch_id: str, **kwargs): """ Get the status of the batch indexing. :param batch_id: ID of the batch uploading :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{batch_id}/indexing-status" return self._send_request("GET", url, **kwargs) def delete_dataset(self): """ Delete this dataset. :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}" return self._send_request("DELETE", url) def delete_document(self, document_id): """ Delete a document. :param document_id: ID of the document :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}" return self._send_request("DELETE", url) def list_documents( self, page: int | None = None, page_size: int | None = None, keyword: str | None = None, **kwargs, ): """ Get a list of documents in this dataset. :return: Response from the API """ params = {} if page is not None: params["page"] = page if page_size is not None: params["limit"] = page_size if keyword is not None: params["keyword"] = keyword url = f"/datasets/{self._get_dataset_id()}/documents" return self._send_request("GET", url, params=params, **kwargs) def add_segments(self, document_id, segments, **kwargs): """ Add segments to a document. :param document_id: ID of the document :param segments: List of segments to add, example: [{"content": "1", "answer": "1", "keyword": ["a"]}] :return: Response from the API """ data = {"segments": segments} url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments" return self._send_request("POST", url, json=data, **kwargs) def query_segments( self, document_id, keyword: str | None = None, status: str | None = None, **kwargs, ): """ Query segments in this document. :param document_id: ID of the document :param keyword: query keyword, optional :param status: status of the segment, optional, e.g. completed """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments" params = {} if keyword is not None: params["keyword"] = keyword if status is not None: params["status"] = status if "params" in kwargs: params.update(kwargs["params"]) return self._send_request("GET", url, params=params, **kwargs) def delete_document_segment(self, document_id, segment_id): """ Delete a segment from a document. :param document_id: ID of the document :param segment_id: ID of the segment :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}" return self._send_request("DELETE", url) def update_document_segment(self, document_id, segment_id, segment_data, **kwargs): """ Update a segment in a document. :param document_id: ID of the document :param segment_id: ID of the segment :param segment_data: Data of the segment, example: {"content": "1", "answer": "1", "keyword": ["a"], "enabled": True} :return: Response from the API """ data = {"segment": segment_data} url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}" return self._send_request("POST", url, json=data, **kwargs) if __name__ == "__main__": import asyncio async def main(): client = ChatClient("key", 'http://127.0.0.1/v1') res = await client.get_conversations("test", limit=2) print(res.json()) client = ChatClient("key", 'http://127.0.0.1/v1') res = client.get_conversations("test", limit=1) print(res.json()) asyncio.run(main()) ``` effect: ![Image](https://github.com/user-attachments/assets/59fc6f1e-5acb-47d6-a0bd-5e9c1b680002) ### 2. Additional context or comments _No response_ ### 3. Can you help us with this feature? - [ ] I am interested in contributing to this feature.
yindo added the 💪 enhancement label 2026-02-21 19:20:48 -05:00
yindo closed this issue 2026-02-21 19:20:48 -05:00
Author
Owner

@crazywoola commented on GitHub (Jul 10, 2025):

Feel free to open a PR for this.

@crazywoola commented on GitHub (Jul 10, 2025): Feel free to open a PR for this.
Author
Owner

@InfernalAzazel commented on GitHub (Jul 10, 2025):

Feel free to open a PR for this.
Currently under research, there are some issues with the stream

    async def _request_async(
            self, method: str,
            endpoint: str,
            json= None,
            params=None,
            stream: bool = False,
    ) -> httpx.Response:
        async with httpx.AsyncClient() as client:
            headers = {
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            }
            url = f"{self.base_url}{endpoint}"
            request = httpx.Request(method, url, json=json, params=params, headers=headers)
            return await client.send(request, stream=stream)

test:

if __name__ == "__main__":
    import asyncio
    async def main():
        client = ChatClient("app-key", 'http://127.0.0.1/v1')
        res = await client.get_conversations("test", limit=2)
        print(res.json())
        response = await client.create_chat_message(
            inputs={},
            query='你好',
            user='test',
            response_mode='streaming',
        )
        async for line in response.aiter_lines():
            print("line:", line)
        # async for line in response.aiter_lines():
        #     if line:
        #         # Dify 的流式响应使用 SSE 格式,每行以 'data:' 开头
        #         if line.startswith("data:"):
        #             data = line.lstrip("data:").strip()
        #             if data:
        #                 try:
        #                     parsed = json.loads(data)
        #                     print(parsed.get("answer", ""))
        #                 except json.JSONDecodeError:
        #                     continue

    client = ChatClient("app-D5kYcBLTyv06Uft3e33ufLYT", 'http://127.0.0.1/v1')
    res = client.get_conversations("test", limit=1)
    print(res.json())
    asyncio.run(main())

error:

{'limit': 1, 'has_more': True, 'data': [{'id': '654067e2-f53f-4058-97e1-485fb088a9ca', 'name': '问候自己☺️', 'inputs': {}, 'status': 'normal', 'introduction': '', 'created_at': 1752140795, 'updated_at': 1752140795}]}
{'limit': 2, 'has_more': True, 'data': [{'id': '654067e2-f53f-4058-97e1-485fb088a9ca', 'name': '问候自己☺️', 'inputs': {}, 'status': 'normal', 'introduction': '', 'created_at': 1752140795, 'updated_at': 1752140795}, {'id': '4088c2de-f8a9-41e2-80f1-e00fa8038f04', 'name': '问候我☺️', 'inputs': {}, 'status': 'normal', 'introduction': '', 'created_at': 1752140773, 'updated_at': 1752140773}]}
line: data: {"event": "workflow_started", "conversation_id": "1db49b2a-4203-4117-ab82-de914b020e9f", "message_id": "21ea0deb-0b89-4b16-bcbd-c2192d873937", "created_at": 1752145439, "task_id": "41394676-e8ce-440e-a99d-409b566f65ed", "workflow_run_id": "fe3c83e5-b46c-4de6-a322-8b7b5dcb9fc2", "data": {"id": "fe3c83e5-b46c-4de6-a322-8b7b5dcb9fc2", "workflow_id": "5f0633e3-65c2-4b7a-bb6b-a87329eb03b7", "sequence_number": 66, "inputs": {"sys.query": "\u4f60\u597d", "sys.files": [], "sys.conversation_id": "1db49b2a-4203-4117-ab82-de914b020e9f", "sys.user_id": "test", "sys.dialogue_count": 0, "sys.app_id": "b71c94bc-8405-4a9e-884a-29e4996b4d04", "sys.workflow_id": "5f0633e3-65c2-4b7a-bb6b-a87329eb03b7", "sys.workflow_run_id": "fe3c83e5-b46c-4de6-a322-8b7b5dcb9fc2"}, "created_at": 1752145439}}
line: 
Traceback (most recent call last):
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
    yield
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 271, in __aiter__
    async for part in self._httpcore_stream:
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/connection_pool.py", line 407, in __aiter__
    raise exc from None
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/connection_pool.py", line 403, in __aiter__
    async for part in self._stream:
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 342, in __aiter__
    raise exc
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 334, in __aiter__
    async for chunk in self._connection._receive_response_body(**kwargs):
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 203, in _receive_response_body
    event = await self._receive_event(timeout=timeout)
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 217, in _receive_event
    data = await self._network_stream.read(
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_backends/anyio.py", line 32, in read
    with map_exceptions(exc_map):
  File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/contextlib.py", line 153, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions
    raise to_exc(exc) from exc
httpcore.ReadError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/kylin/work/code/github/test/dify_client.py", line 512, in <module>
    asyncio.run(main())
  File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
    return future.result()
  File "/Users/kylin/work/code/github/test/dify_client.py", line 495, in main
    async for line in response.aiter_lines():
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 1031, in aiter_lines
    async for text in self.aiter_text():
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 1018, in aiter_text
    async for byte_content in self.aiter_bytes():
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 997, in aiter_bytes
    async for raw_bytes in self.aiter_raw():
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 1055, in aiter_raw
    async for raw_stream_bytes in self.stream:
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_client.py", line 176, in __aiter__
    async for chunk in self._stream:
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 270, in __aiter__
    with map_httpcore_exceptions():
  File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/contextlib.py", line 153, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions
    raise mapped_exc(message) from exc
httpx.ReadError

I also encountered problems when integrating FastAPI

import json
from typing import Optional, Literal, Dict, List, Annotated
from fastapi import APIRouter, Depends, File
from fastapi_utils.cbv import cbv
from pydantic import BaseModel, Field
from starlette.responses import StreamingResponse
from dependencies.dify_client import ChatClient

router = APIRouter(prefix='/api/v1/dify')


class ChatMessageFile(BaseModel):
    type: Literal['document', 'image', 'audio', 'video', 'custom'] = Field(..., description='支持类型')
    transfer_method: Literal['remote_url', 'local_file'] = Field(..., description='支持类型')
    url: Optional[str] = Field(None, description='图片地址(仅当传递方式为 remote_url 时)')
    upload_file_id: Optional[str] = Field(None, description='上传文件 ID(仅当传递方式为 local_file 时)')


class ChatMessage(BaseModel):
    inputs: Optional[Dict] = None
    query: str
    user: str
    response_mode: Literal['blocking', 'streaming'] = "blocking"
    conversation_id: Optional[str] = None
    files: Optional[List[ChatMessageFile]] = None


@cbv(router)
class CBV:
    @router.post("/chat-message")
    async def chat_message(
            self,
            chat_message: ChatMessage,
            chat_client: Annotated[ChatClient, Depends()]
    ):
        """发送对话消息"""
        response = await chat_client.create_chat_message(
            inputs=chat_message.inputs,
            query=chat_message.query,
            user=chat_message.user,
            response_mode=chat_message.response_mode,
            conversation_id=chat_message.conversation_id,
            files=chat_message.files,
        )

        def event_generator():
            for line in response.iter_lines(decode_unicode=True):
                if line:
                    # Dify 的流式响应使用 SSE 格式,每行以 'data:' 开头
                    if line.startswith("data:"):
                        data = line.lstrip("data:").strip()
                        if data:
                            try:
                                parsed = json.loads(data)
                                yield parsed.get("answer", "")
                            except json.JSONDecodeError:
                                continue

        if chat_message.response_mode == "streaming":
            return StreamingResponse(event_generator(), media_type="text/plain")
        print(response.json())
        return response.json()

    @router.post("/chat-messages/{task_id}/stop")
    async def stop(self, task_id: str, user: str, chat_client: Annotated[ChatClient, Depends()]):
        """停止响应,仅支持流式模式"""
        return await chat_client.stop_message(task_id, user)

    # @router.post('/files/upload')
    # async def upload_file(self, user: str, file: File, chat_client: Annotated[ChatClient, Depends()]):
    #     """上传文件"""
    #     response = await chat_client.file_upload(user, file)
    #     return response.json()

    # @router.get('/messages')
    # async def messages(
    #         self,
    #         conversation_id: str,
    #         user: str,
    #         first_id: str,
    #         limit: int = 20,
    #         chat_client: Annotated[ChatClient, Depends()] = Depends()):
    #     """获取会话历史消息"""
    #     response = await chat_client.get_conversation_messages(
    #         user, conversation_id, first_id, limit
    #     )
    #     return response.json()

    # @router.post('/messages/{message_id}/feedbacks')
    # async def message_feedback(self,
    #               user: str,
    #               message_id: str,
    #               rating: Optional[Literal['like', 'dislike']] = None,
    #               chat_client: Annotated[ChatClient, Depends()] = Depends()):
    #     response = await chat_client.message_feedback(message_id, rating, user)
    #     return response.json()

    # @router.get("/conversations")
    # async def get_conversations(
    #         self,
    #         user: str,
    #         last_id: Optional[str] = None,
    #         limit: Optional[int] = 20,
    #         pinned: Optional[bool] = None,
    #         chat_client: Annotated[ChatClient, Depends()] = Depends()
    # ):
    #     response = await chat_client.get_conversations(
    #         user=user,
    #         last_id=last_id,
    #         limit=limit,
    #         pinned=pinned,
    #     )
    #     return response.json()

@InfernalAzazel commented on GitHub (Jul 10, 2025): > Feel free to open a PR for this. Currently under research, there are some issues with the stream ```python async def _request_async( self, method: str, endpoint: str, json= None, params=None, stream: bool = False, ) -> httpx.Response: async with httpx.AsyncClient() as client: headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } url = f"{self.base_url}{endpoint}" request = httpx.Request(method, url, json=json, params=params, headers=headers) return await client.send(request, stream=stream) ``` test: ```python if __name__ == "__main__": import asyncio async def main(): client = ChatClient("app-key", 'http://127.0.0.1/v1') res = await client.get_conversations("test", limit=2) print(res.json()) response = await client.create_chat_message( inputs={}, query='你好', user='test', response_mode='streaming', ) async for line in response.aiter_lines(): print("line:", line) # async for line in response.aiter_lines(): # if line: # # Dify 的流式响应使用 SSE 格式,每行以 'data:' 开头 # if line.startswith("data:"): # data = line.lstrip("data:").strip() # if data: # try: # parsed = json.loads(data) # print(parsed.get("answer", "")) # except json.JSONDecodeError: # continue client = ChatClient("app-D5kYcBLTyv06Uft3e33ufLYT", 'http://127.0.0.1/v1') res = client.get_conversations("test", limit=1) print(res.json()) asyncio.run(main()) ``` error: ```shell {'limit': 1, 'has_more': True, 'data': [{'id': '654067e2-f53f-4058-97e1-485fb088a9ca', 'name': '问候自己☺️', 'inputs': {}, 'status': 'normal', 'introduction': '', 'created_at': 1752140795, 'updated_at': 1752140795}]} {'limit': 2, 'has_more': True, 'data': [{'id': '654067e2-f53f-4058-97e1-485fb088a9ca', 'name': '问候自己☺️', 'inputs': {}, 'status': 'normal', 'introduction': '', 'created_at': 1752140795, 'updated_at': 1752140795}, {'id': '4088c2de-f8a9-41e2-80f1-e00fa8038f04', 'name': '问候我☺️', 'inputs': {}, 'status': 'normal', 'introduction': '', 'created_at': 1752140773, 'updated_at': 1752140773}]} line: data: {"event": "workflow_started", "conversation_id": "1db49b2a-4203-4117-ab82-de914b020e9f", "message_id": "21ea0deb-0b89-4b16-bcbd-c2192d873937", "created_at": 1752145439, "task_id": "41394676-e8ce-440e-a99d-409b566f65ed", "workflow_run_id": "fe3c83e5-b46c-4de6-a322-8b7b5dcb9fc2", "data": {"id": "fe3c83e5-b46c-4de6-a322-8b7b5dcb9fc2", "workflow_id": "5f0633e3-65c2-4b7a-bb6b-a87329eb03b7", "sequence_number": 66, "inputs": {"sys.query": "\u4f60\u597d", "sys.files": [], "sys.conversation_id": "1db49b2a-4203-4117-ab82-de914b020e9f", "sys.user_id": "test", "sys.dialogue_count": 0, "sys.app_id": "b71c94bc-8405-4a9e-884a-29e4996b4d04", "sys.workflow_id": "5f0633e3-65c2-4b7a-bb6b-a87329eb03b7", "sys.workflow_run_id": "fe3c83e5-b46c-4de6-a322-8b7b5dcb9fc2"}, "created_at": 1752145439}} line: Traceback (most recent call last): File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions yield File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 271, in __aiter__ async for part in self._httpcore_stream: File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/connection_pool.py", line 407, in __aiter__ raise exc from None File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/connection_pool.py", line 403, in __aiter__ async for part in self._stream: File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 342, in __aiter__ raise exc File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 334, in __aiter__ async for chunk in self._connection._receive_response_body(**kwargs): File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 203, in _receive_response_body event = await self._receive_event(timeout=timeout) File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_async/http11.py", line 217, in _receive_event data = await self._network_stream.read( File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_backends/anyio.py", line 32, in read with map_exceptions(exc_map): File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/contextlib.py", line 153, in __exit__ self.gen.throw(typ, value, traceback) File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions raise to_exc(exc) from exc httpcore.ReadError The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/Users/kylin/work/code/github/test/dify_client.py", line 512, in <module> asyncio.run(main()) File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/asyncio/runners.py", line 44, in run return loop.run_until_complete(main) File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete return future.result() File "/Users/kylin/work/code/github/test/dify_client.py", line 495, in main async for line in response.aiter_lines(): File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 1031, in aiter_lines async for text in self.aiter_text(): File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 1018, in aiter_text async for byte_content in self.aiter_bytes(): File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 997, in aiter_bytes async for raw_bytes in self.aiter_raw(): File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_models.py", line 1055, in aiter_raw async for raw_stream_bytes in self.stream: File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_client.py", line 176, in __aiter__ async for chunk in self._stream: File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 270, in __aiter__ with map_httpcore_exceptions(): File "/Users/kylin/.pyenv/versions/3.10.12/lib/python3.10/contextlib.py", line 153, in __exit__ self.gen.throw(typ, value, traceback) File "/Users/kylin/work/code/github/test/.venv/lib/python3.10/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions raise mapped_exc(message) from exc httpx.ReadError ``` I also encountered problems when integrating FastAPI ```python import json from typing import Optional, Literal, Dict, List, Annotated from fastapi import APIRouter, Depends, File from fastapi_utils.cbv import cbv from pydantic import BaseModel, Field from starlette.responses import StreamingResponse from dependencies.dify_client import ChatClient router = APIRouter(prefix='/api/v1/dify') class ChatMessageFile(BaseModel): type: Literal['document', 'image', 'audio', 'video', 'custom'] = Field(..., description='支持类型') transfer_method: Literal['remote_url', 'local_file'] = Field(..., description='支持类型') url: Optional[str] = Field(None, description='图片地址(仅当传递方式为 remote_url 时)') upload_file_id: Optional[str] = Field(None, description='上传文件 ID(仅当传递方式为 local_file 时)') class ChatMessage(BaseModel): inputs: Optional[Dict] = None query: str user: str response_mode: Literal['blocking', 'streaming'] = "blocking" conversation_id: Optional[str] = None files: Optional[List[ChatMessageFile]] = None @cbv(router) class CBV: @router.post("/chat-message") async def chat_message( self, chat_message: ChatMessage, chat_client: Annotated[ChatClient, Depends()] ): """发送对话消息""" response = await chat_client.create_chat_message( inputs=chat_message.inputs, query=chat_message.query, user=chat_message.user, response_mode=chat_message.response_mode, conversation_id=chat_message.conversation_id, files=chat_message.files, ) def event_generator(): for line in response.iter_lines(decode_unicode=True): if line: # Dify 的流式响应使用 SSE 格式,每行以 'data:' 开头 if line.startswith("data:"): data = line.lstrip("data:").strip() if data: try: parsed = json.loads(data) yield parsed.get("answer", "") except json.JSONDecodeError: continue if chat_message.response_mode == "streaming": return StreamingResponse(event_generator(), media_type="text/plain") print(response.json()) return response.json() @router.post("/chat-messages/{task_id}/stop") async def stop(self, task_id: str, user: str, chat_client: Annotated[ChatClient, Depends()]): """停止响应,仅支持流式模式""" return await chat_client.stop_message(task_id, user) # @router.post('/files/upload') # async def upload_file(self, user: str, file: File, chat_client: Annotated[ChatClient, Depends()]): # """上传文件""" # response = await chat_client.file_upload(user, file) # return response.json() # @router.get('/messages') # async def messages( # self, # conversation_id: str, # user: str, # first_id: str, # limit: int = 20, # chat_client: Annotated[ChatClient, Depends()] = Depends()): # """获取会话历史消息""" # response = await chat_client.get_conversation_messages( # user, conversation_id, first_id, limit # ) # return response.json() # @router.post('/messages/{message_id}/feedbacks') # async def message_feedback(self, # user: str, # message_id: str, # rating: Optional[Literal['like', 'dislike']] = None, # chat_client: Annotated[ChatClient, Depends()] = Depends()): # response = await chat_client.message_feedback(message_id, rating, user) # return response.json() # @router.get("/conversations") # async def get_conversations( # self, # user: str, # last_id: Optional[str] = None, # limit: Optional[int] = 20, # pinned: Optional[bool] = None, # chat_client: Annotated[ChatClient, Depends()] = Depends() # ): # response = await chat_client.get_conversations( # user=user, # last_id=last_id, # limit=limit, # pinned=pinned, # ) # return response.json() ```
Author
Owner

@InfernalAzazel commented on GitHub (Jul 10, 2025):

In addition, I also found that the dify ui docs and SDK function parameters are inconsistent, and I am not sure which one should prevail.

e.g:

Image Image
@InfernalAzazel commented on GitHub (Jul 10, 2025): In addition, I also found that the dify ui docs and SDK function parameters are inconsistent, and I am not sure which one should prevail. e.g: <img width="1007" height="751" alt="Image" src="https://github.com/user-attachments/assets/68bfc5ab-8069-4573-b844-5ac90497603a" /> <img width="1068" height="800" alt="Image" src="https://github.com/user-attachments/assets/b5e78064-3d92-4ba2-87da-b5c41bf9a591" />
Author
Owner

@InfernalAzazel commented on GitHub (Jul 10, 2025):

It should be a problem with the library https://github.com/encode/httpx/issues/3348

@InfernalAzazel commented on GitHub (Jul 10, 2025): It should be a problem with the library https://github.com/encode/httpx/issues/3348
Author
Owner

@InfernalAzazel commented on GitHub (Jul 11, 2025):

I didn't understand the materials being tested

Image

After research, only one code can be written synchronously and one code can be written asynchronously

dify_async_client.py

import asyncio
import json
import httpx


class AsyncDifyClient:
    def __init__(self, api_key, base_url: str = "https://api.dify.ai/v1", timeout=60):
        self.api_key = api_key
        self.base_url = base_url
        self.client = httpx.AsyncClient()
        self.timeout = timeout
        self.headers = {"Authorization": f"Bearer {self.api_key}"}

    async def _send_request(self, method, endpoint, json=None, params=None, stream=False):
        url = f"{self.base_url}{endpoint}"
        request = self.client.build_request(method, url, json=json, params=params, headers=self.headers,
                                            timeout=self.timeout)
        return await self.client.send(request, stream=stream)

    async def _send_request_with_files(self, method, endpoint, data, files):
        url = f"{self.base_url}{endpoint}"
        return await self.client.request(method, url, data=data, headers=self.headers, files=files)

    async def message_feedback(self, message_id, rating, user):
        data = {"rating": rating, "user": user}
        return await self._send_request("POST", f"/messages/{message_id}/feedbacks", data)

    async def get_application_parameters(self, user):
        params = {"user": user}
        return await self._send_request("GET", "/parameters", params=params)

    async def file_upload(self, user, files):
        data = {"user": user}
        return await self._send_request_with_files(
            "POST", "/files/upload", data=data, files=files
        )

    async def text_to_audio(self, text: str, user: str, streaming: bool = False):
        data = {"text": text, "user": user, "streaming": streaming}
        return await self._send_request("POST", "/text-to-audio", json=data)

    async def get_meta(self, user):
        params = {"user": user}
        return await self._send_request("GET", "/meta", params=params)


class AsyncCompletionClient(AsyncDifyClient):
    async def create_completion_message(self, inputs, response_mode, user, files=None):
        data = {
            "inputs": inputs,
            "response_mode": response_mode,
            "user": user,
            "files": files,
        }
        return await self._send_request(
            "POST",
            "/completion-messages",
            data,
            stream=(response_mode == "streaming"),
        )


class AsyncChatClient(AsyncDifyClient):
    async def create_chat_message(
            self,
            inputs,
            query,
            user,
            response_mode="blocking",
            conversation_id=None,
            files=None,
    ):
        data = {
            "inputs": inputs,
            "query": query,
            "user": user,
            "response_mode": response_mode,
            "files": files,
        }
        if conversation_id:
            data["conversation_id"] = conversation_id

        return await self._send_request(
            "POST",
            "/chat-messages",
            data,
            stream=True if response_mode == "streaming" else False,
        )

    async def get_suggested(self, message_id, user: str):
        params = {"user": user}
        return await self._send_request(
            "GET", f"/messages/{message_id}/suggested", params=params
        )

    async def stop_message(self, task_id, user):
        data = {"user": user}
        return await self._send_request("POST", f"/chat-messages/{task_id}/stop", data)

    async def get_conversations(self, user, last_id=None, limit=None, sort_by=None):
        params = {"user": user, "last_id": last_id, "limit": limit, "sort_by": sort_by}
        return await self._send_request("GET", "/conversations", params=params)

    async def get_conversation_messages(
            self, user, conversation_id=None, first_id=None, limit=None
    ):
        params = {"user": user}

        if conversation_id:
            params["conversation_id"] = conversation_id
        if first_id:
            params["first_id"] = first_id
        if limit:
            params["limit"] = limit

        return await self._send_request("GET", "/messages", params=params)

    async def rename_conversation(
            self, conversation_id: str, name: str, user: str, auto_generate: bool = False,
    ):
        data = {"name": name, "auto_generate": auto_generate, "user": user}
        return await self._send_request(
            "POST", f"/conversations/{conversation_id}/name", data
        )

    async def delete_conversation(self, conversation_id, user):
        data = {"user": user}
        return await self._send_request("DELETE", f"/conversations/{conversation_id}", data)

    async def audio_to_text(self, audio_file, user):
        data = {"user": user}
        files = {"audio_file": audio_file}
        return await self._send_request_with_files("POST", "/audio-to-text", data, files)


class AsyncWorkflowClient(AsyncDifyClient):
    async def run(
            self, inputs: dict, response_mode: str = "streaming", user: str = "abc-123"
    ):
        data = {"inputs": inputs, "response_mode": response_mode, "user": user}
        return await self._send_request("POST", "/workflows/run", data)

    async def stop(self, task_id, user):
        data = {"user": user}
        return await self._send_request("POST", f"/workflows/tasks/{task_id}/stop", data)

    async def get_result(self, workflow_run_id):
        return await self._send_request("GET", f"/workflows/run/{workflow_run_id}")


class AsyncKnowledgeBaseClient(AsyncDifyClient):
    def __init__(
            self,
            api_key,
            base_url: str = "https://api.dify.ai/v1",
            dataset_id: str | None = None,
    ):
        """
        Construct a KnowledgeBaseClient object.

        Args:
            api_key (str): API key of Dify.
            base_url (str, optional): Base URL of Dify API. Defaults to 'https://api.dify.ai/v1'.
            dataset_id (str, optional): ID of the dataset. Defaults to None. You don't need this if you just want to
                create a new dataset. or list datasets. otherwise you need to set this.
        """
        super().__init__(api_key=api_key, base_url=base_url)
        self.dataset_id = dataset_id

    def _get_dataset_id(self):
        if self.dataset_id is None:
            raise ValueError("dataset_id is not set")
        return self.dataset_id

    async def create_dataset(self, name: str, **kwargs):
        return await self._send_request("POST", "/datasets", {"name": name}, **kwargs)

    async def list_datasets(self, page: int = 1, page_size: int = 20, **kwargs):
        return await self._send_request(
            "GET", f"/datasets?page={page}&limit={page_size}", **kwargs
        )

    async def create_document_by_text(
            self, name, text, extra_params: dict | None = None, **kwargs
    ):
        """
        Create a document by text.

        :param name: Name of the document
        :param text: Text content of the document
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        data = {
            "indexing_technique": "high_quality",
            "process_rule": {"mode": "automatic"},
            "name": name,
            "text": text,
        }
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = f"/datasets/{self._get_dataset_id()}/document/create_by_text"
        return await self._send_request("POST", url, json=data, **kwargs)

    async def update_document_by_text(
            self, document_id, name, text, extra_params: dict | None = None, **kwargs
    ):
        """
        Update a document by text.

        :param document_id: ID of the document
        :param name: Name of the document
        :param text: Text content of the document
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        data = {"name": name, "text": text}
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = (
            f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_text"
        )
        return await self._send_request("POST", url, json=data, **kwargs)

    async def create_document_by_file(
            self, file_path, original_document_id=None, extra_params: dict | None = None
    ):
        """
        Create a document by file.

        :param file_path: Path to the file
        :param original_document_id: pass this ID if you want to replace the original document (optional)
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        files = {"file": open(file_path, "rb")}
        data = {
            "process_rule": {"mode": "automatic"},
            "indexing_technique": "high_quality",
        }
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        if original_document_id is not None:
            data["original_document_id"] = original_document_id
        url = f"/datasets/{self._get_dataset_id()}/document/create_by_file"
        return await self._send_request_with_files(
            "POST", url, {"data": json.dumps(data)}, files
        )

    async def update_document_by_file(
            self, document_id, file_path, extra_params: dict | None = None
    ):
        """
        Update a document by file.

        :param document_id: ID of the document
        :param file_path: Path to the file
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return:
        """
        files = {"file": open(file_path, "rb")}
        data = {}
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = (
            f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_file"
        )
        return await self._send_request_with_files(
            "POST", url, {"data": json.dumps(data)}, files
        )

    async def batch_indexing_status(self, batch_id: str, **kwargs):
        """
        Get the status of the batch indexing.

        :param batch_id: ID of the batch uploading
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{batch_id}/indexing-status"
        return await self._send_request("GET", url, **kwargs)

    async def delete_dataset(self):
        """
        Delete this dataset.

        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}"
        return await self._send_request("DELETE", url)

    async def delete_document(self, document_id):
        """
        Delete a document.

        :param document_id: ID of the document
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}"
        return await self._send_request("DELETE", url)

    async def list_documents(
            self,
            page: int | None = None,
            page_size: int | None = None,
            keyword: str | None = None,
            **kwargs,
    ):
        """
        Get a list of documents in this dataset.

        :return: Response from the API
        """
        params = {}
        if page is not None:
            params["page"] = page
        if page_size is not None:
            params["limit"] = page_size
        if keyword is not None:
            params["keyword"] = keyword
        url = f"/datasets/{self._get_dataset_id()}/documents"
        return await self._send_request("GET", url, params=params, **kwargs)

    async def add_segments(self, document_id, segments, **kwargs):
        """
        Add segments to a document.

        :param document_id: ID of the document
        :param segments: List of segments to add, example: [{"content": "1", "answer": "1", "keyword": ["a"]}]
        :return: Response from the API
        """
        data = {"segments": segments}
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
        return await self._send_request("POST", url, json=data, **kwargs)

    async def query_segments(
            self,
            document_id,
            keyword: str | None = None,
            status: str | None = None,
            **kwargs,
    ):
        """
        Query segments in this document.

        :param document_id: ID of the document
        :param keyword: query keyword, optional
        :param status: status of the segment, optional, e.g. completed
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
        params = {}
        if keyword is not None:
            params["keyword"] = keyword
        if status is not None:
            params["status"] = status
        if "params" in kwargs:
            params.update(kwargs["params"])
        return await self._send_request("GET", url, params=params, **kwargs)

    async def delete_document_segment(self, document_id, segment_id):
        """
        Delete a segment from a document.

        :param document_id: ID of the document
        :param segment_id: ID of the segment
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
        return await self._send_request("DELETE", url)

    async def update_document_segment(self, document_id, segment_id, segment_data, **kwargs):
        """
        Update a segment in a document.

        :param document_id: ID of the document
        :param segment_id: ID of the segment
        :param segment_data: Data of the segment, example: {"content": "1", "answer": "1", "keyword": ["a"], "enabled": True}
        :return: Response from the API
        """
        data = {"segment": segment_data}
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
        return await self._send_request("POST", url, json=data, **kwargs)


if __name__ == "__main__":
    async def main():
        chat_client = AsyncChatClient('app-sJPF4xwHdRid4Ch8gjDGBahe')
        response = await chat_client.create_chat_message({}, '"what is it?', 'test', 'blocking', files=[{
            "type": "image",
            "transfer_method": "remote_url",
            "url": "https://cloud.dify.ai/logo/logo-site.png"
        }])
        data = response.json()
        print('test1', data)

        response = await chat_client.create_chat_message({}, '"what is it?', 'test', 'streaming', files=[{
            "type": "image",
            "transfer_method": "remote_url",
            "url": "https://cloud.dify.ai/logo/logo-site.png"
        }])
        rdata =  await response.aread()
        print('test2', rdata.decode('utf-8'))

    asyncio.run(main())

The above issue has been resolved

Image

synchronous

import json
import httpx


class DifyClient:
    def __init__(self, api_key: str, base_url: str = "https://api.dify.ai/v1", timeout=60):
        self.api_key = api_key
        self.base_url = base_url
        self.timeout= timeout
        self.client = httpx.Client()
        self.headers = {"Authorization": f"Bearer {self.api_key}"}

    def _send_request(self, method: str, endpoint: str, json=None, params=None, stream=False):
       
        url = f"{self.base_url}{endpoint}"
        client = httpx.Client()
        request = client.build_request(method, url, json=json, params=params, headers=self.headers, timeout=self.timeout)
        response = client.send(request, stream=stream)

        return response

    def _send_request_with_files(self, method: str, endpoint: str, data, files):
        url = f"{self.base_url}{endpoint}"
        response = httpx.request(
            method, url, data=data, headers=self.headers, files=files
        )

        return response

    def message_feedback(self, message_id, rating, user):
        data = {"rating": rating, "user": user}
        return self._send_request("POST", f"/messages/{message_id}/feedbacks", data)

    def get_application_parameters(self, user):
        params = {"user": user}
        return self._send_request("GET", "/parameters", params=params)

    def file_upload(self, user, files):
        data = {"user": user}
        return self._send_request_with_files(
            "POST", "/files/upload", data=data, files=files
        )

    def text_to_audio(self, text: str, user: str, streaming: bool = False):
        data = {"text": text, "user": user, "streaming": streaming}
        return self._send_request("POST", "/text-to-audio", json=data)

    def get_meta(self, user):
        params = {"user": user}
        return self._send_request("GET", "/meta", params=params)


class CompletionClient(DifyClient):
    def create_completion_message(self, inputs, response_mode, user, files=None):
        data = {
            "inputs": inputs,
            "response_mode": response_mode,
            "user": user,
            "files": files,
        }
        return self._send_request(
            "POST",
            "/completion-messages",
            data,
            stream=True if response_mode == "streaming" else False,
        )


class ChatClient(DifyClient):
    def create_chat_message(
        self,
        inputs,
        query,
        user,
        response_mode="blocking",
        conversation_id=None,
        files=None,
    ):
        data = {
            "inputs": inputs,
            "query": query,
            "user": user,
            "response_mode": response_mode,
            "files": files,
        }
        if conversation_id:
            data["conversation_id"] = conversation_id

        return self._send_request(
            "POST",
            "/chat-messages",
            data,
            stream=True if response_mode == "streaming" else False,
        )

    def get_suggested(self, message_id, user: str):
        params = {"user": user}
        return self._send_request(
            "GET", f"/messages/{message_id}/suggested", params=params
        )

    def stop_message(self, task_id, user):
        data = {"user": user}
        return self._send_request("POST", f"/chat-messages/{task_id}/stop", data)

    def get_conversations(self, user, last_id=None, limit=None, pinned=None):
        params = {"user": user, "last_id": last_id, "limit": limit, "pinned": pinned}
        return self._send_request("GET", "/conversations", params=params)

    def get_conversation_messages(
        self, user, conversation_id=None, first_id=None, limit=None
    ):
        params = {"user": user}

        if conversation_id:
            params["conversation_id"] = conversation_id
        if first_id:
            params["first_id"] = first_id
        if limit:
            params["limit"] = limit

        return self._send_request("GET", "/messages", params=params)

    def rename_conversation(
        self, conversation_id, name, auto_generate: bool, user: str
    ):
        data = {"name": name, "auto_generate": auto_generate, "user": user}
        return self._send_request(
            "POST", f"/conversations/{conversation_id}/name", data
        )

    def delete_conversation(self, conversation_id, user):
        data = {"user": user}
        return self._send_request("DELETE", f"/conversations/{conversation_id}", data)

    def audio_to_text(self, audio_file, user):
        data = {"user": user}
        files = {"audio_file": audio_file}
        return self._send_request_with_files("POST", "/audio-to-text", data, files)


class WorkflowClient(DifyClient):
    def run(
        self, inputs: dict, response_mode: str = "streaming", user: str = "abc-123"
    ):
        data = {"inputs": inputs, "response_mode": response_mode, "user": user}
        return self._send_request("POST", "/workflows/run", data)

    def stop(self, task_id, user):
        data = {"user": user}
        return self._send_request("POST", f"/workflows/tasks/{task_id}/stop", data)

    def get_result(self, workflow_run_id):
        return self._send_request("GET", f"/workflows/run/{workflow_run_id}")


class KnowledgeBaseClient(DifyClient):
    def __init__(
        self,
        api_key,
        base_url: str = "https://api.dify.ai/v1",
        dataset_id: str | None = None,
    ):
        """
        Construct a KnowledgeBaseClient object.

        Args:
            api_key (str): API key of Dify.
            base_url (str, optional): Base URL of Dify API. Defaults to 'https://api.dify.ai/v1'.
            dataset_id (str, optional): ID of the dataset. Defaults to None. You don't need this if you just want to
                create a new dataset. or list datasets. otherwise you need to set this.
        """
        super().__init__(api_key=api_key, base_url=base_url)
        self.dataset_id = dataset_id

    def _get_dataset_id(self):
        if self.dataset_id is None:
            raise ValueError("dataset_id is not set")
        return self.dataset_id

    def create_dataset(self, name: str, **kwargs):
        return self._send_request("POST", "/datasets", {"name": name}, **kwargs)

    def list_datasets(self, page: int = 1, page_size: int = 20, **kwargs):
        return self._send_request(
            "GET", f"/datasets?page={page}&limit={page_size}", **kwargs
        )

    def create_document_by_text(
        self, name, text, extra_params: dict | None = None, **kwargs
    ):
        """
        Create a document by text.

        :param name: Name of the document
        :param text: Text content of the document
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        data = {
            "indexing_technique": "high_quality",
            "process_rule": {"mode": "automatic"},
            "name": name,
            "text": text,
        }
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = f"/datasets/{self._get_dataset_id()}/document/create_by_text"
        return self._send_request("POST", url, json=data, **kwargs)

    def update_document_by_text(
        self, document_id, name, text, extra_params: dict | None = None, **kwargs
    ):
        """
        Update a document by text.

        :param document_id: ID of the document
        :param name: Name of the document
        :param text: Text content of the document
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        data = {"name": name, "text": text}
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = (
            f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_text"
        )
        return self._send_request("POST", url, json=data, **kwargs)

    def create_document_by_file(
        self, file_path, original_document_id=None, extra_params: dict | None = None
    ):
        """
        Create a document by file.

        :param file_path: Path to the file
        :param original_document_id: pass this ID if you want to replace the original document (optional)
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return: Response from the API
        """
        files = {"file": open(file_path, "rb")}
        data = {
            "process_rule": {"mode": "automatic"},
            "indexing_technique": "high_quality",
        }
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        if original_document_id is not None:
            data["original_document_id"] = original_document_id
        url = f"/datasets/{self._get_dataset_id()}/document/create_by_file"
        return self._send_request_with_files(
            "POST", url, {"data": json.dumps(data)}, files
        )

    def update_document_by_file(
        self, document_id, file_path, extra_params: dict | None = None
    ):
        """
        Update a document by file.

        :param document_id: ID of the document
        :param file_path: Path to the file
        :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional)
            e.g.
            {
            'indexing_technique': 'high_quality',
            'process_rule': {
                'rules': {
                    'pre_processing_rules': [
                        {'id': 'remove_extra_spaces', 'enabled': True},
                        {'id': 'remove_urls_emails', 'enabled': True}
                    ],
                    'segmentation': {
                        'separator': '\n',
                        'max_tokens': 500
                    }
                },
                'mode': 'custom'
            }
        }
        :return:
        """
        files = {"file": open(file_path, "rb")}
        data = {}
        if extra_params is not None and isinstance(extra_params, dict):
            data.update(extra_params)
        url = (
            f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_file"
        )
        return self._send_request_with_files(
            "POST", url, {"data": json.dumps(data)}, files
        )

    def batch_indexing_status(self, batch_id: str, **kwargs):
        """
        Get the status of the batch indexing.

        :param batch_id: ID of the batch uploading
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{batch_id}/indexing-status"
        return self._send_request("GET", url, **kwargs)

    def delete_dataset(self):
        """
        Delete this dataset.

        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}"
        return self._send_request("DELETE", url)

    def delete_document(self, document_id):
        """
        Delete a document.

        :param document_id: ID of the document
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}"
        return self._send_request("DELETE", url)

    def list_documents(
        self,
        page: int | None = None,
        page_size: int | None = None,
        keyword: str | None = None,
        **kwargs,
    ):
        """
        Get a list of documents in this dataset.

        :return: Response from the API
        """
        params = {}
        if page is not None:
            params["page"] = page
        if page_size is not None:
            params["limit"] = page_size
        if keyword is not None:
            params["keyword"] = keyword
        url = f"/datasets/{self._get_dataset_id()}/documents"
        return self._send_request("GET", url, params=params, **kwargs)

    def add_segments(self, document_id, segments, **kwargs):
        """
        Add segments to a document.

        :param document_id: ID of the document
        :param segments: List of segments to add, example: [{"content": "1", "answer": "1", "keyword": ["a"]}]
        :return: Response from the API
        """
        data = {"segments": segments}
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
        return self._send_request("POST", url, json=data, **kwargs)

    def query_segments(
        self,
        document_id,
        keyword: str | None = None,
        status: str | None = None,
        **kwargs,
    ):
        """
        Query segments in this document.

        :param document_id: ID of the document
        :param keyword: query keyword, optional
        :param status: status of the segment, optional, e.g. completed
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments"
        params = {}
        if keyword is not None:
            params["keyword"] = keyword
        if status is not None:
            params["status"] = status
        if "params" in kwargs:
            params.update(kwargs["params"])
        return self._send_request("GET", url, params=params, **kwargs)

    def delete_document_segment(self, document_id, segment_id):
        """
        Delete a segment from a document.

        :param document_id: ID of the document
        :param segment_id: ID of the segment
        :return: Response from the API
        """
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
        return self._send_request("DELETE", url)

    def update_document_segment(self, document_id, segment_id, segment_data, **kwargs):
        """
        Update a segment in a document.

        :param document_id: ID of the document
        :param segment_id: ID of the segment
        :param segment_data: Data of the segment, example: {"content": "1", "answer": "1", "keyword": ["a"], "enabled": True}
        :return: Response from the API
        """
        data = {"segment": segment_data}
        url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
        return self._send_request("POST", url, json=data, **kwargs)

I didn't understand the test cases and materials, and this part of the work needs someone who is more clear to complete.

@InfernalAzazel commented on GitHub (Jul 11, 2025): I didn't understand the materials being tested <img width="1400" height="949" alt="Image" src="https://github.com/user-attachments/assets/3c0bd6b8-0917-4f29-908c-7a3bfb39cf54" /> After research, only one code can be written synchronously and one code can be written asynchronously dify_async_client.py ```python import asyncio import json import httpx class AsyncDifyClient: def __init__(self, api_key, base_url: str = "https://api.dify.ai/v1", timeout=60): self.api_key = api_key self.base_url = base_url self.client = httpx.AsyncClient() self.timeout = timeout self.headers = {"Authorization": f"Bearer {self.api_key}"} async def _send_request(self, method, endpoint, json=None, params=None, stream=False): url = f"{self.base_url}{endpoint}" request = self.client.build_request(method, url, json=json, params=params, headers=self.headers, timeout=self.timeout) return await self.client.send(request, stream=stream) async def _send_request_with_files(self, method, endpoint, data, files): url = f"{self.base_url}{endpoint}" return await self.client.request(method, url, data=data, headers=self.headers, files=files) async def message_feedback(self, message_id, rating, user): data = {"rating": rating, "user": user} return await self._send_request("POST", f"/messages/{message_id}/feedbacks", data) async def get_application_parameters(self, user): params = {"user": user} return await self._send_request("GET", "/parameters", params=params) async def file_upload(self, user, files): data = {"user": user} return await self._send_request_with_files( "POST", "/files/upload", data=data, files=files ) async def text_to_audio(self, text: str, user: str, streaming: bool = False): data = {"text": text, "user": user, "streaming": streaming} return await self._send_request("POST", "/text-to-audio", json=data) async def get_meta(self, user): params = {"user": user} return await self._send_request("GET", "/meta", params=params) class AsyncCompletionClient(AsyncDifyClient): async def create_completion_message(self, inputs, response_mode, user, files=None): data = { "inputs": inputs, "response_mode": response_mode, "user": user, "files": files, } return await self._send_request( "POST", "/completion-messages", data, stream=(response_mode == "streaming"), ) class AsyncChatClient(AsyncDifyClient): async def create_chat_message( self, inputs, query, user, response_mode="blocking", conversation_id=None, files=None, ): data = { "inputs": inputs, "query": query, "user": user, "response_mode": response_mode, "files": files, } if conversation_id: data["conversation_id"] = conversation_id return await self._send_request( "POST", "/chat-messages", data, stream=True if response_mode == "streaming" else False, ) async def get_suggested(self, message_id, user: str): params = {"user": user} return await self._send_request( "GET", f"/messages/{message_id}/suggested", params=params ) async def stop_message(self, task_id, user): data = {"user": user} return await self._send_request("POST", f"/chat-messages/{task_id}/stop", data) async def get_conversations(self, user, last_id=None, limit=None, sort_by=None): params = {"user": user, "last_id": last_id, "limit": limit, "sort_by": sort_by} return await self._send_request("GET", "/conversations", params=params) async def get_conversation_messages( self, user, conversation_id=None, first_id=None, limit=None ): params = {"user": user} if conversation_id: params["conversation_id"] = conversation_id if first_id: params["first_id"] = first_id if limit: params["limit"] = limit return await self._send_request("GET", "/messages", params=params) async def rename_conversation( self, conversation_id: str, name: str, user: str, auto_generate: bool = False, ): data = {"name": name, "auto_generate": auto_generate, "user": user} return await self._send_request( "POST", f"/conversations/{conversation_id}/name", data ) async def delete_conversation(self, conversation_id, user): data = {"user": user} return await self._send_request("DELETE", f"/conversations/{conversation_id}", data) async def audio_to_text(self, audio_file, user): data = {"user": user} files = {"audio_file": audio_file} return await self._send_request_with_files("POST", "/audio-to-text", data, files) class AsyncWorkflowClient(AsyncDifyClient): async def run( self, inputs: dict, response_mode: str = "streaming", user: str = "abc-123" ): data = {"inputs": inputs, "response_mode": response_mode, "user": user} return await self._send_request("POST", "/workflows/run", data) async def stop(self, task_id, user): data = {"user": user} return await self._send_request("POST", f"/workflows/tasks/{task_id}/stop", data) async def get_result(self, workflow_run_id): return await self._send_request("GET", f"/workflows/run/{workflow_run_id}") class AsyncKnowledgeBaseClient(AsyncDifyClient): def __init__( self, api_key, base_url: str = "https://api.dify.ai/v1", dataset_id: str | None = None, ): """ Construct a KnowledgeBaseClient object. Args: api_key (str): API key of Dify. base_url (str, optional): Base URL of Dify API. Defaults to 'https://api.dify.ai/v1'. dataset_id (str, optional): ID of the dataset. Defaults to None. You don't need this if you just want to create a new dataset. or list datasets. otherwise you need to set this. """ super().__init__(api_key=api_key, base_url=base_url) self.dataset_id = dataset_id def _get_dataset_id(self): if self.dataset_id is None: raise ValueError("dataset_id is not set") return self.dataset_id async def create_dataset(self, name: str, **kwargs): return await self._send_request("POST", "/datasets", {"name": name}, **kwargs) async def list_datasets(self, page: int = 1, page_size: int = 20, **kwargs): return await self._send_request( "GET", f"/datasets?page={page}&limit={page_size}", **kwargs ) async def create_document_by_text( self, name, text, extra_params: dict | None = None, **kwargs ): """ Create a document by text. :param name: Name of the document :param text: Text content of the document :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ data = { "indexing_technique": "high_quality", "process_rule": {"mode": "automatic"}, "name": name, "text": text, } if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = f"/datasets/{self._get_dataset_id()}/document/create_by_text" return await self._send_request("POST", url, json=data, **kwargs) async def update_document_by_text( self, document_id, name, text, extra_params: dict | None = None, **kwargs ): """ Update a document by text. :param document_id: ID of the document :param name: Name of the document :param text: Text content of the document :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ data = {"name": name, "text": text} if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = ( f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_text" ) return await self._send_request("POST", url, json=data, **kwargs) async def create_document_by_file( self, file_path, original_document_id=None, extra_params: dict | None = None ): """ Create a document by file. :param file_path: Path to the file :param original_document_id: pass this ID if you want to replace the original document (optional) :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ files = {"file": open(file_path, "rb")} data = { "process_rule": {"mode": "automatic"}, "indexing_technique": "high_quality", } if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) if original_document_id is not None: data["original_document_id"] = original_document_id url = f"/datasets/{self._get_dataset_id()}/document/create_by_file" return await self._send_request_with_files( "POST", url, {"data": json.dumps(data)}, files ) async def update_document_by_file( self, document_id, file_path, extra_params: dict | None = None ): """ Update a document by file. :param document_id: ID of the document :param file_path: Path to the file :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: """ files = {"file": open(file_path, "rb")} data = {} if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = ( f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_file" ) return await self._send_request_with_files( "POST", url, {"data": json.dumps(data)}, files ) async def batch_indexing_status(self, batch_id: str, **kwargs): """ Get the status of the batch indexing. :param batch_id: ID of the batch uploading :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{batch_id}/indexing-status" return await self._send_request("GET", url, **kwargs) async def delete_dataset(self): """ Delete this dataset. :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}" return await self._send_request("DELETE", url) async def delete_document(self, document_id): """ Delete a document. :param document_id: ID of the document :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}" return await self._send_request("DELETE", url) async def list_documents( self, page: int | None = None, page_size: int | None = None, keyword: str | None = None, **kwargs, ): """ Get a list of documents in this dataset. :return: Response from the API """ params = {} if page is not None: params["page"] = page if page_size is not None: params["limit"] = page_size if keyword is not None: params["keyword"] = keyword url = f"/datasets/{self._get_dataset_id()}/documents" return await self._send_request("GET", url, params=params, **kwargs) async def add_segments(self, document_id, segments, **kwargs): """ Add segments to a document. :param document_id: ID of the document :param segments: List of segments to add, example: [{"content": "1", "answer": "1", "keyword": ["a"]}] :return: Response from the API """ data = {"segments": segments} url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments" return await self._send_request("POST", url, json=data, **kwargs) async def query_segments( self, document_id, keyword: str | None = None, status: str | None = None, **kwargs, ): """ Query segments in this document. :param document_id: ID of the document :param keyword: query keyword, optional :param status: status of the segment, optional, e.g. completed """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments" params = {} if keyword is not None: params["keyword"] = keyword if status is not None: params["status"] = status if "params" in kwargs: params.update(kwargs["params"]) return await self._send_request("GET", url, params=params, **kwargs) async def delete_document_segment(self, document_id, segment_id): """ Delete a segment from a document. :param document_id: ID of the document :param segment_id: ID of the segment :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}" return await self._send_request("DELETE", url) async def update_document_segment(self, document_id, segment_id, segment_data, **kwargs): """ Update a segment in a document. :param document_id: ID of the document :param segment_id: ID of the segment :param segment_data: Data of the segment, example: {"content": "1", "answer": "1", "keyword": ["a"], "enabled": True} :return: Response from the API """ data = {"segment": segment_data} url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}" return await self._send_request("POST", url, json=data, **kwargs) if __name__ == "__main__": async def main(): chat_client = AsyncChatClient('app-sJPF4xwHdRid4Ch8gjDGBahe') response = await chat_client.create_chat_message({}, '"what is it?', 'test', 'blocking', files=[{ "type": "image", "transfer_method": "remote_url", "url": "https://cloud.dify.ai/logo/logo-site.png" }]) data = response.json() print('test1', data) response = await chat_client.create_chat_message({}, '"what is it?', 'test', 'streaming', files=[{ "type": "image", "transfer_method": "remote_url", "url": "https://cloud.dify.ai/logo/logo-site.png" }]) rdata = await response.aread() print('test2', rdata.decode('utf-8')) asyncio.run(main()) ``` The above issue has been resolved <img width="1570" height="502" alt="Image" src="https://github.com/user-attachments/assets/4f0141c6-4e71-4a1a-9aad-8ae250c4f761" /> synchronous ```python import json import httpx class DifyClient: def __init__(self, api_key: str, base_url: str = "https://api.dify.ai/v1", timeout=60): self.api_key = api_key self.base_url = base_url self.timeout= timeout self.client = httpx.Client() self.headers = {"Authorization": f"Bearer {self.api_key}"} def _send_request(self, method: str, endpoint: str, json=None, params=None, stream=False): url = f"{self.base_url}{endpoint}" client = httpx.Client() request = client.build_request(method, url, json=json, params=params, headers=self.headers, timeout=self.timeout) response = client.send(request, stream=stream) return response def _send_request_with_files(self, method: str, endpoint: str, data, files): url = f"{self.base_url}{endpoint}" response = httpx.request( method, url, data=data, headers=self.headers, files=files ) return response def message_feedback(self, message_id, rating, user): data = {"rating": rating, "user": user} return self._send_request("POST", f"/messages/{message_id}/feedbacks", data) def get_application_parameters(self, user): params = {"user": user} return self._send_request("GET", "/parameters", params=params) def file_upload(self, user, files): data = {"user": user} return self._send_request_with_files( "POST", "/files/upload", data=data, files=files ) def text_to_audio(self, text: str, user: str, streaming: bool = False): data = {"text": text, "user": user, "streaming": streaming} return self._send_request("POST", "/text-to-audio", json=data) def get_meta(self, user): params = {"user": user} return self._send_request("GET", "/meta", params=params) class CompletionClient(DifyClient): def create_completion_message(self, inputs, response_mode, user, files=None): data = { "inputs": inputs, "response_mode": response_mode, "user": user, "files": files, } return self._send_request( "POST", "/completion-messages", data, stream=True if response_mode == "streaming" else False, ) class ChatClient(DifyClient): def create_chat_message( self, inputs, query, user, response_mode="blocking", conversation_id=None, files=None, ): data = { "inputs": inputs, "query": query, "user": user, "response_mode": response_mode, "files": files, } if conversation_id: data["conversation_id"] = conversation_id return self._send_request( "POST", "/chat-messages", data, stream=True if response_mode == "streaming" else False, ) def get_suggested(self, message_id, user: str): params = {"user": user} return self._send_request( "GET", f"/messages/{message_id}/suggested", params=params ) def stop_message(self, task_id, user): data = {"user": user} return self._send_request("POST", f"/chat-messages/{task_id}/stop", data) def get_conversations(self, user, last_id=None, limit=None, pinned=None): params = {"user": user, "last_id": last_id, "limit": limit, "pinned": pinned} return self._send_request("GET", "/conversations", params=params) def get_conversation_messages( self, user, conversation_id=None, first_id=None, limit=None ): params = {"user": user} if conversation_id: params["conversation_id"] = conversation_id if first_id: params["first_id"] = first_id if limit: params["limit"] = limit return self._send_request("GET", "/messages", params=params) def rename_conversation( self, conversation_id, name, auto_generate: bool, user: str ): data = {"name": name, "auto_generate": auto_generate, "user": user} return self._send_request( "POST", f"/conversations/{conversation_id}/name", data ) def delete_conversation(self, conversation_id, user): data = {"user": user} return self._send_request("DELETE", f"/conversations/{conversation_id}", data) def audio_to_text(self, audio_file, user): data = {"user": user} files = {"audio_file": audio_file} return self._send_request_with_files("POST", "/audio-to-text", data, files) class WorkflowClient(DifyClient): def run( self, inputs: dict, response_mode: str = "streaming", user: str = "abc-123" ): data = {"inputs": inputs, "response_mode": response_mode, "user": user} return self._send_request("POST", "/workflows/run", data) def stop(self, task_id, user): data = {"user": user} return self._send_request("POST", f"/workflows/tasks/{task_id}/stop", data) def get_result(self, workflow_run_id): return self._send_request("GET", f"/workflows/run/{workflow_run_id}") class KnowledgeBaseClient(DifyClient): def __init__( self, api_key, base_url: str = "https://api.dify.ai/v1", dataset_id: str | None = None, ): """ Construct a KnowledgeBaseClient object. Args: api_key (str): API key of Dify. base_url (str, optional): Base URL of Dify API. Defaults to 'https://api.dify.ai/v1'. dataset_id (str, optional): ID of the dataset. Defaults to None. You don't need this if you just want to create a new dataset. or list datasets. otherwise you need to set this. """ super().__init__(api_key=api_key, base_url=base_url) self.dataset_id = dataset_id def _get_dataset_id(self): if self.dataset_id is None: raise ValueError("dataset_id is not set") return self.dataset_id def create_dataset(self, name: str, **kwargs): return self._send_request("POST", "/datasets", {"name": name}, **kwargs) def list_datasets(self, page: int = 1, page_size: int = 20, **kwargs): return self._send_request( "GET", f"/datasets?page={page}&limit={page_size}", **kwargs ) def create_document_by_text( self, name, text, extra_params: dict | None = None, **kwargs ): """ Create a document by text. :param name: Name of the document :param text: Text content of the document :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ data = { "indexing_technique": "high_quality", "process_rule": {"mode": "automatic"}, "name": name, "text": text, } if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = f"/datasets/{self._get_dataset_id()}/document/create_by_text" return self._send_request("POST", url, json=data, **kwargs) def update_document_by_text( self, document_id, name, text, extra_params: dict | None = None, **kwargs ): """ Update a document by text. :param document_id: ID of the document :param name: Name of the document :param text: Text content of the document :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ data = {"name": name, "text": text} if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = ( f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_text" ) return self._send_request("POST", url, json=data, **kwargs) def create_document_by_file( self, file_path, original_document_id=None, extra_params: dict | None = None ): """ Create a document by file. :param file_path: Path to the file :param original_document_id: pass this ID if you want to replace the original document (optional) :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: Response from the API """ files = {"file": open(file_path, "rb")} data = { "process_rule": {"mode": "automatic"}, "indexing_technique": "high_quality", } if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) if original_document_id is not None: data["original_document_id"] = original_document_id url = f"/datasets/{self._get_dataset_id()}/document/create_by_file" return self._send_request_with_files( "POST", url, {"data": json.dumps(data)}, files ) def update_document_by_file( self, document_id, file_path, extra_params: dict | None = None ): """ Update a document by file. :param document_id: ID of the document :param file_path: Path to the file :param extra_params: extra parameters pass to the API, such as indexing_technique, process_rule. (optional) e.g. { 'indexing_technique': 'high_quality', 'process_rule': { 'rules': { 'pre_processing_rules': [ {'id': 'remove_extra_spaces', 'enabled': True}, {'id': 'remove_urls_emails', 'enabled': True} ], 'segmentation': { 'separator': '\n', 'max_tokens': 500 } }, 'mode': 'custom' } } :return: """ files = {"file": open(file_path, "rb")} data = {} if extra_params is not None and isinstance(extra_params, dict): data.update(extra_params) url = ( f"/datasets/{self._get_dataset_id()}/documents/{document_id}/update_by_file" ) return self._send_request_with_files( "POST", url, {"data": json.dumps(data)}, files ) def batch_indexing_status(self, batch_id: str, **kwargs): """ Get the status of the batch indexing. :param batch_id: ID of the batch uploading :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{batch_id}/indexing-status" return self._send_request("GET", url, **kwargs) def delete_dataset(self): """ Delete this dataset. :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}" return self._send_request("DELETE", url) def delete_document(self, document_id): """ Delete a document. :param document_id: ID of the document :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}" return self._send_request("DELETE", url) def list_documents( self, page: int | None = None, page_size: int | None = None, keyword: str | None = None, **kwargs, ): """ Get a list of documents in this dataset. :return: Response from the API """ params = {} if page is not None: params["page"] = page if page_size is not None: params["limit"] = page_size if keyword is not None: params["keyword"] = keyword url = f"/datasets/{self._get_dataset_id()}/documents" return self._send_request("GET", url, params=params, **kwargs) def add_segments(self, document_id, segments, **kwargs): """ Add segments to a document. :param document_id: ID of the document :param segments: List of segments to add, example: [{"content": "1", "answer": "1", "keyword": ["a"]}] :return: Response from the API """ data = {"segments": segments} url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments" return self._send_request("POST", url, json=data, **kwargs) def query_segments( self, document_id, keyword: str | None = None, status: str | None = None, **kwargs, ): """ Query segments in this document. :param document_id: ID of the document :param keyword: query keyword, optional :param status: status of the segment, optional, e.g. completed """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments" params = {} if keyword is not None: params["keyword"] = keyword if status is not None: params["status"] = status if "params" in kwargs: params.update(kwargs["params"]) return self._send_request("GET", url, params=params, **kwargs) def delete_document_segment(self, document_id, segment_id): """ Delete a segment from a document. :param document_id: ID of the document :param segment_id: ID of the segment :return: Response from the API """ url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}" return self._send_request("DELETE", url) def update_document_segment(self, document_id, segment_id, segment_data, **kwargs): """ Update a segment in a document. :param document_id: ID of the document :param segment_id: ID of the segment :param segment_data: Data of the segment, example: {"content": "1", "answer": "1", "keyword": ["a"], "enabled": True} :return: Response from the API """ data = {"segment": segment_data} url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}" return self._send_request("POST", url, json=data, **kwargs) ``` I didn't understand the test cases and materials, and this part of the work needs someone who is more clear to complete.
Author
Owner

@dosubot[bot] commented on GitHub (Aug 30, 2025):

Hi, @InfernalAzazel. I'm Dosu, and I'm helping the Dify team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You requested asynchronous support for the Dify client SDK and shared example code.
  • Encountered httpx ReadError issues during streaming responses.
  • Noted inconsistencies between Dify UI docs and SDK parameters.
  • Provided separate fully asynchronous and synchronous client implementations that resolved streaming errors.
  • The maintainer encouraged submitting a PR for the async feature.

Next Steps:

  • Please confirm if asynchronous support is still relevant to the latest version of Dify.
  • If so, you can keep the discussion open by commenting; otherwise, I will automatically close this issue in 15 days.

Thanks for your understanding and contribution!

@dosubot[bot] commented on GitHub (Aug 30, 2025): Hi, @InfernalAzazel. I'm [Dosu](https://dosu.dev), and I'm helping the Dify team manage their backlog and am marking this issue as stale. **Issue Summary:** - You requested asynchronous support for the Dify client SDK and shared example code. - Encountered httpx ReadError issues during streaming responses. - Noted inconsistencies between Dify UI docs and SDK parameters. - Provided separate fully asynchronous and synchronous client implementations that resolved streaming errors. - The maintainer encouraged submitting a PR for the async feature. **Next Steps:** - Please confirm if asynchronous support is still relevant to the latest version of Dify. - If so, you can keep the discussion open by commenting; otherwise, I will automatically close this issue in 15 days. Thanks for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify#15235