import os.path
import requests
class DifyChat:
"""
一个极简的 Dify HTTP API 调用封装(适合快速验证流程)。
你这里用到的核心接口:
- POST /v1/chat-messages 发起一次对话消息(可选携带图片)
- POST /v1/files/upload 上传文件,得到 upload_file_id(给 chat-messages 的 files 用)
- DELETE /v1/conversations/{id} 删除会话(有的服务端会返回空 body)
"""
def __init__(self, api_url, api_key, user="abc-123"):
"""
:param api_url: Dify 服务地址(到 /v1 这一层),例如 http://10.0.100.98/v1
:param api_key: Dify 应用 API Key(通常以 app- 开头)
:param user: 用于标识请求用户的字符串。Dify 多数接口需要这个字段用于配额/会话归属。
"""
self.api_url = api_url
self.api_key = api_key
self.user = user
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
@staticmethod
def _safe_parse_response(response):
""" 尽量把响应解析成 JSON。
- 像 DELETE 这类接口,经常返回 204 No Content(空 body)
- 或者返回纯文本/HTML 错误页(不是 JSON)
因此这里做'兜底',避免直接 response.json() 抛 JSONDecodeError。
"""
try:
if response.status_code in (204, 205) or not response.content:
return {
"status_code": response.status_code,
"ok": response.ok,
"text": ""
}
return response.json()
except Exception:
return {
"status_code": response.status_code,
"ok": response.ok,
"text": response.text
}
def create_chat(self, chat_name):
""" 创建 chat(是否需要取决于你的 Dify 应用/版本;很多情况下直接用 chat-messages 即可)。 """
url = f'{self.api_url}/chats/create'
data = {'chat_name': chat_name}
response = requests.post(url, json=data, headers=self.headers)
return self._safe_parse_response(response)
def send(self, message, file_id=None, conversation_id=""):
""" 发送一条消息(文本问答),可选带图片。
:param message: 用户问题/提示词(query)
:param file_id: upload_file 接口返回的 id(即 upload_file_id);不传则表示纯文本问答
:param conversation_id: 继续同一个会话时传入;留空表示开启新会话
"""
url = f'{self.api_url}/chat-messages'
data = {
"inputs": {},
"query": message,
"response_mode": "blocking",
"conversation_id": conversation_id or "",
"user": self.user
}
if file_id:
data["files"] = [{
"type": "image",
"transfer_method": "local_file",
"upload_file_id": file_id
}]
response = requests.post(url, json=data, headers=self.headers)
return self._safe_parse_response(response)
def delete(self, conver_id):
""" 删除会话。
注意:服务端可能返回 204(空响应体),所以不能直接 response.json()。
"""
url = f'{self.api_url}/conversations/{conver_id}'
data = {"user": self.user}
response = requests.delete(url, json=data, headers=self.headers)
return self._safe_parse_response(response)
def upload_file(self, file_path):
""" 上传本地文件到 Dify,返回值中通常包含:{"id": "..."}。
随后把这个 id 作为 send(..., file_id=...) 的 upload_file_id 来引用。
"""
url = f'{self.api_url}/files/upload'
data = {"user": self.user}
headers = {'Authorization': f'Bearer {self.api_key}'}
with open(file_path, 'rb') as f:
files = {'file': ("a.jpg", f, 'image/jpg')}
response = requests.post(url, files=files, data=data, headers=headers)
return self._safe_parse_response(response)
if __name__ == "__main__":
api_url = "http://127.0.0.1/v1"
api_key = "app-xxxxxxxxxxxxxxxxxxxx"
query = "时空之战的主要人物有哪些?"
image_path = ""
chat = DifyChat(api_url, api_key)
if not os.path.exists(image_path):
ret_send = chat.send(query)
else:
ret_upload = chat.upload_file(image_path)
file_id = ret_upload["id"]
ret_send = chat.send(query, file_id=file_id)
message_id = ret_send.get("message_id")
conver_id = ret_send.get("conversation_id")
print(ret_send)
if isinstance(ret_send, dict) and "answer" in ret_send:
print(ret_send["answer"])
if conver_id:
ret_delete = chat.delete(conver_id)
print(ret_delete)
{
"event": "message",
"task_id": "e50a0154-5523-4c1a-93de-ae45a2568d00",
"id": "c3f11257-cc16-4718-84f8-f5a550b38757",
"message_id": "c3f11257-cc16-4718-84f8-f5a550b38757",
"conversation_id": "3a1f9dd3-97a0-4d6e-bf74-bae87754ab02",
"mode": "advanced-chat",
"answer": "根据提供的知识内容,时空之战的主要人物包括:\n\n1. 大雄:故事的主角,原本性格懦弱、逃避责任,但在经历未来世界的战斗后实现内心成长。\n2. 哆啦 A 梦:大雄的好朋友,来自未来的机器猫,拥有众多神奇道具,是事件的关键推动者。\n3. 特兰克斯:来自未来的超级赛亚人,身披战甲,拥有强大战斗力,为拯救未来世界而寻求帮助。",
"metadata": {
"retriever_resources": [
{
"position": 1,
"dataset_id": "04adf974-e176-4d80-8d56-9720718b2b37",
"dataset_name": "时空之战_1",
"document_id": "时空之战.md",
"document_name": "时空之战.md",
"data_source_type": null,
"segment_id": null,
"retriever_from": "workflow",
"score": 0.36003251935408387,
"hit_count": null,
"word_count": null,
"segment_position": null,
"index_node_hash": null,
"content": "# 哆啦 A 梦与超级赛亚人:时空之战\n\n在一个寻常的午后,大雄依旧坐在书桌前发呆..."
},
{
"position": 2,
"dataset_id": "2030ad1d-3b1e-410a-8745-168a393a678f",
"dataset_name": "博士论文知识库",
"document_id": "配电网单相断线故障选线与定位方法的研究_张晓文.pdf",
"document_name": "配电网单相断线故障选线与定位方法的研究_张晓文.pdf",
"data_source_type": null,
"segment_id": null,
"retriever_from": "workflow",
"score": 0.34555257968591596,
"hit_count": null,
"word_count": null,
"segment_position": null,
"index_node_hash": null,
"content": "障定位方法,首先利用对称分量法..."
},
{
"position": 3,
"dataset_id": "2030ad1d-3b1e-410a-8745-168a393a678f",
"dataset_name": "单相博士论文知识库",
"document_id": "配电网单相断线故障选线与定位方法的研究_张晓文.pdf",
"document_name": "配电网单相断线故障选线与定位方法的研究_张晓文.pdf",
"data_source_type": null,
"segment_id": null,
"retriever_from": "workflow",
"score": 0.1545743801846572,
"hit_count": null,
"word_count": null,
"segment_position": null,
"index_node_hash": null,
"content": "第 3 章基于 Hausdorff 距离的单相断线故障选线方法在农村及偏远的乡镇..."
}
],
"usage": {
"prompt_tokens": 22886,
"prompt_unit_price": "0",
"prompt_price_unit": "0",
"prompt_price": "0",
"completion_tokens": 413,
"completion_unit_price": "0",
"completion_price_unit": "0",
"completion_price": "0",
"total_tokens": 23299,
"total_price": "0",
"currency": "USD",
"latency": 4.649309985339642
}
},
"created_at": 1769582803
}