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. 特兰克斯:来自未来的超级赛亚人,身披战甲,拥有强大战斗力,为拯救未来世界而寻求帮助。\n',
'metadata': {
'retriever_resources': [
{
'position': 1,
'dataset_id': '04adf974-e176-4d80-8d56-9720718b2b37',
'dataset_name': '时空之战_1',
'document_id': '时空之战.md',
'document_name': '时空之战.md',
'data_source_type': None,
'segment_id': None,
'retriever_from': 'workflow',
'score': 0.36003251935408387,
'hit_count': None,
'word_count': None,
'segment_position': None,
'index_node_hash': None,
'content': '# 哆啦 A 梦与超级赛亚人:时空之战\n\n在一个寻常的午后,大雄依旧坐在书桌前发呆,作业堆得像山,连第一页都没动。...',
'page': None,
'doc_metadata': None
},
...
],
'usage': {
'prompt_tokens': 22886,
'completion_tokens': 413,
'total_tokens': 23299,
...,
'latency': 4.649309985339642
}
},
'created_at': 1769582803
}