Spaces:
Sleeping
Sleeping
Upload 81 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +15 -0
- api.py +346 -0
- api_docs.md +154 -0
- dify_client_python/.DS_Store +0 -0
- dify_client_python/LICENSE +21 -0
- dify_client_python/MANIFEST.in +1 -0
- dify_client_python/README.md +155 -0
- dify_client_python/build.sh +9 -0
- dify_client_python/build/lib/dify_client/__init__.py +1 -0
- dify_client_python/build/lib/dify_client/_clientx.py +660 -0
- dify_client_python/build/lib/dify_client/errors.py +132 -0
- dify_client_python/build/lib/dify_client/models/__init__.py +7 -0
- dify_client_python/build/lib/dify_client/models/base.py +93 -0
- dify_client_python/build/lib/dify_client/models/chat.py +29 -0
- dify_client_python/build/lib/dify_client/models/completion.py +22 -0
- dify_client_python/build/lib/dify_client/models/feedback.py +21 -0
- dify_client_python/build/lib/dify_client/models/file.py +15 -0
- dify_client_python/build/lib/dify_client/models/stream.py +186 -0
- dify_client_python/build/lib/dify_client/models/workflow.py +91 -0
- dify_client_python/build/lib/dify_client/utils/__init__.py +1 -0
- dify_client_python/build/lib/dify_client/utils/_common.py +7 -0
- dify_client_python/dify_client/.DS_Store +0 -0
- dify_client_python/dify_client/__init__.py +1 -0
- dify_client_python/dify_client/__pycache__/__init__.cpython-310.pyc +0 -0
- dify_client_python/dify_client/__pycache__/__init__.cpython-312.pyc +0 -0
- dify_client_python/dify_client/__pycache__/_clientx.cpython-310.pyc +0 -0
- dify_client_python/dify_client/__pycache__/_clientx.cpython-312.pyc +0 -0
- dify_client_python/dify_client/__pycache__/errors.cpython-310.pyc +0 -0
- dify_client_python/dify_client/__pycache__/errors.cpython-312.pyc +0 -0
- dify_client_python/dify_client/_clientx.py +694 -0
- dify_client_python/dify_client/errors.py +134 -0
- dify_client_python/dify_client/models/__init__.py +7 -0
- dify_client_python/dify_client/models/__pycache__/__init__.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/__init__.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/base.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/base.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/chat.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/chat.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/completion.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/completion.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/feedback.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/feedback.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/file.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/file.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/stream.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/stream.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/workflow.cpython-310.pyc +0 -0
- dify_client_python/dify_client/models/__pycache__/workflow.cpython-312.pyc +0 -0
- dify_client_python/dify_client/models/base.py +93 -0
- dify_client_python/dify_client/models/chat.py +29 -0
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /code
|
| 4 |
+
|
| 5 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 6 |
+
COPY ./api.py /code/api.py
|
| 7 |
+
COPY ./json_parser.py /code/json_parser.py
|
| 8 |
+
COPY ./logger_config.py /code/logger_config.py
|
| 9 |
+
COPY ./response_formatter.py /code/response_formatter.py
|
| 10 |
+
|
| 11 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 12 |
+
|
| 13 |
+
EXPOSE 7860
|
| 14 |
+
|
| 15 |
+
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
|
api.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Request
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from typing import Dict, List, Optional, Union, Any
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import logging
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from dify_client_python.dify_client import models
|
| 11 |
+
from sse_starlette.sse import EventSourceResponse
|
| 12 |
+
import httpx
|
| 13 |
+
from json_parser import SSEParser
|
| 14 |
+
from logger_config import setup_logger
|
| 15 |
+
from fastapi.responses import StreamingResponse
|
| 16 |
+
from fastapi.responses import JSONResponse
|
| 17 |
+
from response_formatter import ResponseFormatter
|
| 18 |
+
import traceback
|
| 19 |
+
|
| 20 |
+
# Load environment variables
|
| 21 |
+
load_dotenv()
|
| 22 |
+
|
| 23 |
+
# Configure logging
|
| 24 |
+
logging.basicConfig(
|
| 25 |
+
level=logging.INFO,
|
| 26 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 27 |
+
)
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
class AgentOutput(BaseModel):
|
| 31 |
+
"""Structured output from agent processing"""
|
| 32 |
+
thought_content: str
|
| 33 |
+
observation: Optional[str]
|
| 34 |
+
tool_outputs: List[Dict]
|
| 35 |
+
citations: List[Dict]
|
| 36 |
+
metadata: Dict
|
| 37 |
+
raw_response: str
|
| 38 |
+
|
| 39 |
+
class AgentRequest(BaseModel):
|
| 40 |
+
"""Enhanced request model with additional parameters"""
|
| 41 |
+
query: str
|
| 42 |
+
conversation_id: Optional[str] = None
|
| 43 |
+
stream: bool = True
|
| 44 |
+
inputs: Dict = {}
|
| 45 |
+
files: List = []
|
| 46 |
+
user: str = "default_user"
|
| 47 |
+
response_mode: str = "streaming"
|
| 48 |
+
|
| 49 |
+
class AgentProcessor:
|
| 50 |
+
def __init__(self, api_key: str):
|
| 51 |
+
self.api_key = api_key
|
| 52 |
+
self.api_base = "https://rag-engine.go-yamamoto.com/v1"
|
| 53 |
+
self.formatter = ResponseFormatter()
|
| 54 |
+
self.client = httpx.AsyncClient(timeout=60.0)
|
| 55 |
+
self.logger = setup_logger("agent_processor")
|
| 56 |
+
|
| 57 |
+
async def log_request_details(
|
| 58 |
+
self,
|
| 59 |
+
request: AgentRequest,
|
| 60 |
+
start_time: datetime
|
| 61 |
+
) -> None:
|
| 62 |
+
"""Log detailed request information"""
|
| 63 |
+
self.logger.debug(
|
| 64 |
+
"Request details: \n"
|
| 65 |
+
f"Query: {request.query}\n"
|
| 66 |
+
f"User: {request.user}\n"
|
| 67 |
+
f"Conversation ID: {request.conversation_id}\n"
|
| 68 |
+
f"Stream mode: {request.stream}\n"
|
| 69 |
+
f"Start time: {start_time}\n"
|
| 70 |
+
f"Inputs: {request.inputs}\n"
|
| 71 |
+
f"Files: {len(request.files)} files attached"
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
async def log_error(
|
| 75 |
+
self,
|
| 76 |
+
error: Exception,
|
| 77 |
+
context: Optional[Dict] = None
|
| 78 |
+
) -> None:
|
| 79 |
+
"""Log detailed error information"""
|
| 80 |
+
error_msg = (
|
| 81 |
+
f"Error type: {type(error).__name__}\n"
|
| 82 |
+
f"Error message: {str(error)}\n"
|
| 83 |
+
f"Stack trace:\n{traceback.format_exc()}\n"
|
| 84 |
+
)
|
| 85 |
+
if context:
|
| 86 |
+
error_msg += f"Context:\n{json.dumps(context, indent=2)}"
|
| 87 |
+
|
| 88 |
+
self.logger.error(error_msg)
|
| 89 |
+
|
| 90 |
+
async def cleanup(self):
|
| 91 |
+
"""Cleanup method to properly close client"""
|
| 92 |
+
await self.client.aclose()
|
| 93 |
+
|
| 94 |
+
async def process_stream(self, request: AgentRequest):
|
| 95 |
+
start_time = datetime.now()
|
| 96 |
+
await self.log_request_details(request, start_time)
|
| 97 |
+
|
| 98 |
+
headers = {
|
| 99 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 100 |
+
"Content-Type": "application/json",
|
| 101 |
+
"Accept": "text/event-stream"
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
chat_request = {
|
| 105 |
+
"query": request.query,
|
| 106 |
+
"inputs": request.inputs,
|
| 107 |
+
"response_mode": "streaming" if request.stream else "blocking",
|
| 108 |
+
"user": request.user,
|
| 109 |
+
"conversation_id": request.conversation_id,
|
| 110 |
+
"files": request.files
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
async def event_generator():
|
| 114 |
+
parser = SSEParser()
|
| 115 |
+
citations = []
|
| 116 |
+
metadata = {}
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
async with self.client.stream(
|
| 120 |
+
"POST",
|
| 121 |
+
f"{self.api_base}/chat-messages",
|
| 122 |
+
headers=headers,
|
| 123 |
+
json=chat_request
|
| 124 |
+
) as response:
|
| 125 |
+
self.logger.debug(
|
| 126 |
+
f"Stream connection established\n"
|
| 127 |
+
f"Status: {response.status_code}\n"
|
| 128 |
+
f"Headers: {dict(response.headers)}"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
buffer = ""
|
| 132 |
+
async for line in response.aiter_lines():
|
| 133 |
+
if not line.strip():
|
| 134 |
+
continue
|
| 135 |
+
|
| 136 |
+
self.logger.debug(f"Raw SSE line: {line}")
|
| 137 |
+
|
| 138 |
+
if "data:" in line:
|
| 139 |
+
try:
|
| 140 |
+
data = line.split("data:", 1)[1].strip()
|
| 141 |
+
parsed = json.loads(data)
|
| 142 |
+
|
| 143 |
+
if parsed.get("event") == "message_end":
|
| 144 |
+
citations = parsed.get("retriever_resources", [])
|
| 145 |
+
metadata = parsed.get("metadata", {})
|
| 146 |
+
self.logger.debug(
|
| 147 |
+
f"Message end event:\n"
|
| 148 |
+
f"Citations: {citations}\n"
|
| 149 |
+
f"Metadata: {metadata}"
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
formatted = self.format_terminal_output(
|
| 153 |
+
parsed,
|
| 154 |
+
citations=citations,
|
| 155 |
+
metadata=metadata
|
| 156 |
+
)
|
| 157 |
+
if formatted:
|
| 158 |
+
self.logger.info(formatted)
|
| 159 |
+
except Exception as e:
|
| 160 |
+
await self.log_error(
|
| 161 |
+
e,
|
| 162 |
+
{"line": line, "event": "parse_data"}
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
buffer += line + "\n"
|
| 166 |
+
|
| 167 |
+
if line.startswith("data:") or buffer.strip().endswith("}"):
|
| 168 |
+
try:
|
| 169 |
+
processed_response = parser.parse_sse_event(buffer)
|
| 170 |
+
if processed_response and isinstance(processed_response, dict):
|
| 171 |
+
cleaned_response = self.clean_response(processed_response)
|
| 172 |
+
if cleaned_response:
|
| 173 |
+
xml_content = cleaned_response.get("content", "")
|
| 174 |
+
yield f"data: {xml_content}\n\n"
|
| 175 |
+
except Exception as parse_error:
|
| 176 |
+
await self.log_error(
|
| 177 |
+
parse_error,
|
| 178 |
+
{"buffer": buffer, "event": "process_buffer"}
|
| 179 |
+
)
|
| 180 |
+
error_xml = (
|
| 181 |
+
f"<agent_response>"
|
| 182 |
+
f"<error>{str(parse_error)}</error>"
|
| 183 |
+
f"</agent_response>"
|
| 184 |
+
)
|
| 185 |
+
yield f"data: {error_xml}\n\n"
|
| 186 |
+
finally:
|
| 187 |
+
buffer = ""
|
| 188 |
+
|
| 189 |
+
except httpx.ConnectError as e:
|
| 190 |
+
await self.log_error(e, {"event": "connection_error"})
|
| 191 |
+
error_xml = (
|
| 192 |
+
f"<agent_response>"
|
| 193 |
+
f"<error>Connection error: {str(e)}</error>"
|
| 194 |
+
f"</agent_response>"
|
| 195 |
+
)
|
| 196 |
+
yield f"data: {error_xml}\n\n"
|
| 197 |
+
except Exception as e:
|
| 198 |
+
await self.log_error(e, {"event": "stream_error"})
|
| 199 |
+
error_xml = (
|
| 200 |
+
f"<agent_response>"
|
| 201 |
+
f"<error>Streaming error: {str(e)}</error>"
|
| 202 |
+
f"</agent_response>"
|
| 203 |
+
)
|
| 204 |
+
yield f"data: {error_xml}\n\n"
|
| 205 |
+
finally:
|
| 206 |
+
end_time = datetime.now()
|
| 207 |
+
duration = (end_time - start_time).total_seconds()
|
| 208 |
+
self.logger.info(f"Request completed in {duration:.2f} seconds")
|
| 209 |
+
|
| 210 |
+
return StreamingResponse(
|
| 211 |
+
event_generator(),
|
| 212 |
+
media_type="text/event-stream",
|
| 213 |
+
headers={
|
| 214 |
+
"Cache-Control": "no-cache",
|
| 215 |
+
"Connection": "keep-alive",
|
| 216 |
+
"X-Accel-Buffering": "no",
|
| 217 |
+
"Access-Control-Allow-Origin": "*"
|
| 218 |
+
}
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
def format_terminal_output(
|
| 222 |
+
self,
|
| 223 |
+
response: Dict,
|
| 224 |
+
citations: List[Dict] = None,
|
| 225 |
+
metadata: Dict = None
|
| 226 |
+
) -> Optional[str]:
|
| 227 |
+
"""Format response for terminal output"""
|
| 228 |
+
event_type = response.get("event")
|
| 229 |
+
|
| 230 |
+
if event_type == "agent_thought":
|
| 231 |
+
thought = response.get("thought", "")
|
| 232 |
+
observation = response.get("observation", "")
|
| 233 |
+
terminal_output, _ = self.formatter.format_thought(
|
| 234 |
+
thought,
|
| 235 |
+
observation,
|
| 236 |
+
citations=citations,
|
| 237 |
+
metadata=metadata
|
| 238 |
+
)
|
| 239 |
+
return terminal_output
|
| 240 |
+
|
| 241 |
+
elif event_type == "agent_message":
|
| 242 |
+
message = response.get("answer", "")
|
| 243 |
+
terminal_output, _ = self.formatter.format_message(message)
|
| 244 |
+
return terminal_output
|
| 245 |
+
|
| 246 |
+
elif event_type == "error":
|
| 247 |
+
error = response.get("error", "Unknown error")
|
| 248 |
+
terminal_output, _ = self.formatter.format_error(error)
|
| 249 |
+
return terminal_output
|
| 250 |
+
|
| 251 |
+
return None
|
| 252 |
+
|
| 253 |
+
def clean_response(self, response: Dict) -> Optional[Dict]:
|
| 254 |
+
"""Clean and transform the response for frontend consumption"""
|
| 255 |
+
try:
|
| 256 |
+
event_type = response.get("event")
|
| 257 |
+
if not event_type:
|
| 258 |
+
return None
|
| 259 |
+
|
| 260 |
+
# Handle different event types
|
| 261 |
+
if event_type == "agent_thought":
|
| 262 |
+
thought = response.get("thought", "")
|
| 263 |
+
observation = response.get("observation", "")
|
| 264 |
+
_, xml_output = self.formatter.format_thought(thought, observation)
|
| 265 |
+
return {
|
| 266 |
+
"type": "thought",
|
| 267 |
+
"content": xml_output
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
elif event_type == "agent_message":
|
| 271 |
+
message = response.get("answer", "")
|
| 272 |
+
_, xml_output = self.formatter.format_message(message)
|
| 273 |
+
return {
|
| 274 |
+
"type": "message",
|
| 275 |
+
"content": xml_output
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
elif event_type == "error":
|
| 279 |
+
error = response.get("error", "Unknown error")
|
| 280 |
+
_, xml_output = self.formatter.format_error(error)
|
| 281 |
+
return {
|
| 282 |
+
"type": "error",
|
| 283 |
+
"content": xml_output
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
return None
|
| 287 |
+
except Exception as e:
|
| 288 |
+
logger.error(f"Error cleaning response: {str(e)}")
|
| 289 |
+
return None
|
| 290 |
+
|
| 291 |
+
# Initialize FastAPI app
|
| 292 |
+
app = FastAPI()
|
| 293 |
+
agent_processor = None
|
| 294 |
+
|
| 295 |
+
# Add CORS middleware
|
| 296 |
+
app.add_middleware(
|
| 297 |
+
CORSMiddleware,
|
| 298 |
+
allow_origins=["*"],
|
| 299 |
+
allow_credentials=True,
|
| 300 |
+
allow_methods=["*"],
|
| 301 |
+
allow_headers=["*"],
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
@app.on_event("startup")
|
| 305 |
+
async def startup_event():
|
| 306 |
+
global agent_processor
|
| 307 |
+
api_key = os.getenv("DIFY_API_KEY", "app-kVHTrZzEmFXEBfyXOi4rro7M")
|
| 308 |
+
agent_processor = AgentProcessor(api_key=api_key)
|
| 309 |
+
|
| 310 |
+
@app.on_event("shutdown")
|
| 311 |
+
async def shutdown_event():
|
| 312 |
+
global agent_processor
|
| 313 |
+
if agent_processor:
|
| 314 |
+
await agent_processor.cleanup()
|
| 315 |
+
|
| 316 |
+
@app.post("/v1/agent")
|
| 317 |
+
async def process_agent_request(request: AgentRequest):
|
| 318 |
+
try:
|
| 319 |
+
logger.info(f"Processing agent request: {request.query}")
|
| 320 |
+
return await agent_processor.process_stream(request)
|
| 321 |
+
|
| 322 |
+
except Exception as e:
|
| 323 |
+
logger.error(f"Error in agent request processing: {e}", exc_info=True)
|
| 324 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 325 |
+
|
| 326 |
+
@app.middleware("http")
|
| 327 |
+
async def error_handling_middleware(request: Request, call_next):
|
| 328 |
+
try:
|
| 329 |
+
response = await call_next(request)
|
| 330 |
+
return response
|
| 331 |
+
except Exception as e:
|
| 332 |
+
logger.error(f"Unhandled error: {str(e)}", exc_info=True)
|
| 333 |
+
return JSONResponse(
|
| 334 |
+
status_code=500,
|
| 335 |
+
content={"error": "Internal server error occurred"}
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Add host and port parameters to the launch
|
| 339 |
+
if __name__ == "__main__":
|
| 340 |
+
import uvicorn
|
| 341 |
+
uvicorn.run(
|
| 342 |
+
"api:app",
|
| 343 |
+
host="0.0.0.0",
|
| 344 |
+
port=8224,
|
| 345 |
+
reload=True
|
| 346 |
+
)
|
api_docs.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
POST
|
| 2 |
+
/chat-messages
|
| 3 |
+
Send Chat Message
|
| 4 |
+
Send a request to the chat application.
|
| 5 |
+
|
| 6 |
+
Request Body
|
| 7 |
+
Name
|
| 8 |
+
query
|
| 9 |
+
Type
|
| 10 |
+
string
|
| 11 |
+
Description
|
| 12 |
+
User Input/Question content
|
| 13 |
+
|
| 14 |
+
Name
|
| 15 |
+
inputs
|
| 16 |
+
Type
|
| 17 |
+
object
|
| 18 |
+
Description
|
| 19 |
+
Allows the entry of various variable values defined by the App. The inputs parameter contains multiple key/value pairs, with each key corresponding to a specific variable and each value being the specific value for that variable. Default {}
|
| 20 |
+
|
| 21 |
+
Name
|
| 22 |
+
response_mode
|
| 23 |
+
Type
|
| 24 |
+
string
|
| 25 |
+
Description
|
| 26 |
+
The mode of response return, supporting:
|
| 27 |
+
|
| 28 |
+
streaming Streaming mode (recommended), implements a typewriter-like output through SSE (Server-Sent Events).
|
| 29 |
+
blocking Blocking mode, returns result after execution is complete. (Requests may be interrupted if the process is long) Due to Cloudflare restrictions, the request will be interrupted without a return after 100 seconds. Note: blocking mode is not supported in Agent Assistant mode
|
| 30 |
+
Name
|
| 31 |
+
user
|
| 32 |
+
Type
|
| 33 |
+
string
|
| 34 |
+
Description
|
| 35 |
+
User identifier, used to define the identity of the end-user for retrieval and statistics. Should be uniquely defined by the developer within the application.
|
| 36 |
+
|
| 37 |
+
Name
|
| 38 |
+
conversation_id
|
| 39 |
+
Type
|
| 40 |
+
string
|
| 41 |
+
Description
|
| 42 |
+
Conversation ID, to continue the conversation based on previous chat records, it is necessary to pass the previous message's conversation_id.
|
| 43 |
+
|
| 44 |
+
Name
|
| 45 |
+
files
|
| 46 |
+
Type
|
| 47 |
+
array[object]
|
| 48 |
+
Description
|
| 49 |
+
File list, suitable for inputting files (images) combined with text understanding and answering questions, available only when the model supports Vision capability.
|
| 50 |
+
|
| 51 |
+
type (string) Supported type: image (currently only supports image type)
|
| 52 |
+
transfer_method (string) Transfer method, remote_url for image URL / local_file for file upload
|
| 53 |
+
url (string) Image URL (when the transfer method is remote_url)
|
| 54 |
+
upload_file_id (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is local_file)
|
| 55 |
+
Name
|
| 56 |
+
auto_generate_name
|
| 57 |
+
Type
|
| 58 |
+
bool
|
| 59 |
+
Description
|
| 60 |
+
Auto-generate title, default is true. If set to false, can achieve async title generation by calling the conversation rename API and setting auto_generate to true.
|
| 61 |
+
|
| 62 |
+
Response
|
| 63 |
+
When response_mode is blocking, return a CompletionResponse object. When response_mode is streaming, return a ChunkCompletionResponse stream.
|
| 64 |
+
|
| 65 |
+
ChatCompletionResponse
|
| 66 |
+
Returns the complete App result, Content-Type is application/json.
|
| 67 |
+
|
| 68 |
+
message_id (string) Unique message ID
|
| 69 |
+
conversation_id (string) Conversation ID
|
| 70 |
+
mode (string) App mode, fixed as chat
|
| 71 |
+
answer (string) Complete response content
|
| 72 |
+
metadata (object) Metadata
|
| 73 |
+
usage (Usage) Model usage information
|
| 74 |
+
retriever_resources (array[RetrieverResource]) Citation and Attribution List
|
| 75 |
+
created_at (int) Message creation timestamp, e.g., 1705395332
|
| 76 |
+
ChunkChatCompletionResponse
|
| 77 |
+
Returns the stream chunks outputted by the App, Content-Type is text/event-stream. Each streaming chunk starts with data:, separated by two newline characters \n\n, as shown below:
|
| 78 |
+
|
| 79 |
+
data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n
|
| 80 |
+
|
| 81 |
+
Copy
|
| 82 |
+
Copied!
|
| 83 |
+
The structure of the streaming chunks varies depending on the event:
|
| 84 |
+
|
| 85 |
+
event: message LLM returns text chunk event, i.e., the complete text is output in a chunked fashion.
|
| 86 |
+
task_id (string) Task ID, used for request tracking and the below Stop Generate API
|
| 87 |
+
message_id (string) Unique message ID
|
| 88 |
+
conversation_id (string) Conversation ID
|
| 89 |
+
answer (string) LLM returned text chunk content
|
| 90 |
+
created_at (int) Creation timestamp, e.g., 1705395332
|
| 91 |
+
event: agent_message LLM returns text chunk event, i.e., with Agent Assistant enabled, the complete text is output in a chunked fashion (Only supported in Agent mode)
|
| 92 |
+
task_id (string) Task ID, used for request tracking and the below Stop Generate API
|
| 93 |
+
message_id (string) Unique message ID
|
| 94 |
+
conversation_id (string) Conversation ID
|
| 95 |
+
answer (string) LLM returned text chunk content
|
| 96 |
+
created_at (int) Creation timestamp, e.g., 1705395332
|
| 97 |
+
event: tts_message TTS audio stream event, that is, speech synthesis output. The content is an audio block in Mp3 format, encoded as a base64 string. When playing, simply decode the base64 and feed it into the player. (This message is available only when auto-play is enabled)
|
| 98 |
+
task_id (string) Task ID, used for request tracking and the stop response interface below
|
| 99 |
+
message_id (string) Unique message ID
|
| 100 |
+
audio (string) The audio after speech synthesis, encoded in base64 text content, when playing, simply decode the base64 and feed it into the player
|
| 101 |
+
created_at (int) Creation timestamp, e.g.: 1705395332
|
| 102 |
+
event: tts_message_end TTS audio stream end event, receiving this event indicates the end of the audio stream.
|
| 103 |
+
task_id (string) Task ID, used for request tracking and the stop response interface below
|
| 104 |
+
message_id (string) Unique message ID
|
| 105 |
+
audio (string) The end event has no audio, so this is an empty string
|
| 106 |
+
created_at (int) Creation timestamp, e.g.: 1705395332
|
| 107 |
+
event: agent_thought thought of Agent, contains the thought of LLM, input and output of tool calls (Only supported in Agent mode)
|
| 108 |
+
id (string) Agent thought ID, every iteration has a unique agent thought ID
|
| 109 |
+
task_id (string) (string) Task ID, used for request tracking and the below Stop Generate API
|
| 110 |
+
message_id (string) Unique message ID
|
| 111 |
+
position (int) Position of current agent thought, each message may have multiple thoughts in order.
|
| 112 |
+
thought (string) What LLM is thinking about
|
| 113 |
+
observation (string) Response from tool calls
|
| 114 |
+
tool (string) A list of tools represents which tools are called,split by ;
|
| 115 |
+
tool_input (string) Input of tools in JSON format. Like: {"dalle3": {"prompt": "a cute cat"}}.
|
| 116 |
+
created_at (int) Creation timestamp, e.g., 1705395332
|
| 117 |
+
message_files (array[string]) Refer to message_file event
|
| 118 |
+
file_id (string) File ID
|
| 119 |
+
conversation_id (string) Conversation ID
|
| 120 |
+
event: message_file Message file event, a new file has created by tool
|
| 121 |
+
id (string) File unique ID
|
| 122 |
+
type (string) File type,only allow "image" currently
|
| 123 |
+
belongs_to (string) Belongs to, it will only be an 'assistant' here
|
| 124 |
+
url (string) Remote url of file
|
| 125 |
+
conversation_id (string) Conversation ID
|
| 126 |
+
event: message_end Message end event, receiving this event means streaming has ended.
|
| 127 |
+
task_id (string) Task ID, used for request tracking and the below Stop Generate API
|
| 128 |
+
message_id (string) Unique message ID
|
| 129 |
+
conversation_id (string) Conversation ID
|
| 130 |
+
metadata (object) Metadata
|
| 131 |
+
usage (Usage) Model usage information
|
| 132 |
+
retriever_resources (array[RetrieverResource]) Citation and Attribution List
|
| 133 |
+
event: message_replace Message content replacement event. When output content moderation is enabled, if the content is flagged, then the message content will be replaced with a preset reply through this event.
|
| 134 |
+
task_id (string) Task ID, used for request tracking and the below Stop Generate API
|
| 135 |
+
message_id (string) Unique message ID
|
| 136 |
+
conversation_id (string) Conversation ID
|
| 137 |
+
answer (string) Replacement content (directly replaces all LLM reply text)
|
| 138 |
+
created_at (int) Creation timestamp, e.g., 1705395332
|
| 139 |
+
event: error Exceptions that occur during the streaming process will be output in the form of stream events, and reception of an error event will end the stream.
|
| 140 |
+
task_id (string) Task ID, used for request tracking and the below Stop Generate API
|
| 141 |
+
message_id (string) Unique message ID
|
| 142 |
+
status (int) HTTP status code
|
| 143 |
+
code (string) Error code
|
| 144 |
+
message (string) Error message
|
| 145 |
+
event: ping Ping event every 10 seconds to keep the connection alive.
|
| 146 |
+
Errors
|
| 147 |
+
404, Conversation does not exists
|
| 148 |
+
400, invalid_param, abnormal parameter input
|
| 149 |
+
400, app_unavailable, App configuration unavailable
|
| 150 |
+
400, provider_not_initialize, no available model credential configuration
|
| 151 |
+
400, provider_quota_exceeded, model invocation quota insufficient
|
| 152 |
+
400, model_currently_not_support, current model unavailable
|
| 153 |
+
400, completion_request_error, text generation failed
|
| 154 |
+
500, internal server error
|
dify_client_python/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
dify_client_python/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 haoyuhu
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
dify_client_python/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
recursive-include dify_client *.py
|
dify_client_python/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dify-client-python
|
| 2 |
+
|
| 3 |
+
Welcome to the `dify-client-python` repository! This Python package provides a convenient and powerful interface to
|
| 4 |
+
interact with the Dify API, enabling developers to integrate a wide range of features into their applications with ease.
|
| 5 |
+
|
| 6 |
+
## Main Features
|
| 7 |
+
|
| 8 |
+
* **Synchronous and Asynchronous Support**: The client offers both synchronous and asynchronous methods, allowing for
|
| 9 |
+
flexible integration into various Python codebases and frameworks.
|
| 10 |
+
* **Stream and Non-stream Support**: Seamlessly work with both streaming and non-streaming endpoints of the Dify API for
|
| 11 |
+
real-time and batch processing use cases.
|
| 12 |
+
* **Comprehensive Endpoint Coverage**: Support completion, chat, workflows, feedback, file uploads, etc., the client
|
| 13 |
+
covers all available Dify API endpoints.
|
| 14 |
+
|
| 15 |
+
## Installation
|
| 16 |
+
|
| 17 |
+
Before using the `dify-client-python` client, you'll need to install it. You can easily install it using `pip`:
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install dify-client-python
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Quick Start
|
| 24 |
+
|
| 25 |
+
Here's a quick example of how you can use the Dify Client to send a chat message.
|
| 26 |
+
|
| 27 |
+
```python
|
| 28 |
+
import uuid
|
| 29 |
+
from dify_client import Client, models
|
| 30 |
+
|
| 31 |
+
# Initialize the client with your API key
|
| 32 |
+
client = Client(
|
| 33 |
+
api_key="your-api-key",
|
| 34 |
+
api_base="http://localhost/v1",
|
| 35 |
+
)
|
| 36 |
+
user = str(uuid.uuid4())
|
| 37 |
+
|
| 38 |
+
# Create a blocking chat request
|
| 39 |
+
blocking_chat_req = models.ChatRequest(
|
| 40 |
+
query="Hi, dify-client-python!",
|
| 41 |
+
inputs={"city": "Beijing"},
|
| 42 |
+
user=user,
|
| 43 |
+
response_mode=models.ResponseMode.BLOCKING,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# Send the chat message
|
| 47 |
+
chat_response = client.chat_messages(blocking_chat_req, timeout=60.)
|
| 48 |
+
print(chat_response)
|
| 49 |
+
|
| 50 |
+
# Create a streaming chat request
|
| 51 |
+
streaming_chat_req = models.ChatRequest(
|
| 52 |
+
query="Hi, dify-client-python!",
|
| 53 |
+
inputs={"city": "Beijing"},
|
| 54 |
+
user=user,
|
| 55 |
+
response_mode=models.ResponseMode.STREAMING,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Send the chat message
|
| 59 |
+
for chunk in client.chat_messages(streaming_chat_req, timeout=60.):
|
| 60 |
+
print(chunk)
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
For asynchronous operations, use the `AsyncClient` in a similar fashion:
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
import asyncio
|
| 67 |
+
import uuid
|
| 68 |
+
|
| 69 |
+
from dify_client import AsyncClient, models
|
| 70 |
+
|
| 71 |
+
# Initialize the async client with your API key
|
| 72 |
+
async_client = AsyncClient(
|
| 73 |
+
api_key="your-api-key",
|
| 74 |
+
api_base="http://localhost/v1",
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
# Define an asynchronous function to send a blocking chat message with BLOCKING ResponseMode
|
| 79 |
+
async def send_chat_message():
|
| 80 |
+
user = str(uuid.uuid4())
|
| 81 |
+
# Create a blocking chat request
|
| 82 |
+
blocking_chat_req = models.ChatRequest(
|
| 83 |
+
query="Hi, dify-client-python!",
|
| 84 |
+
inputs={"city": "Beijing"},
|
| 85 |
+
user=user,
|
| 86 |
+
response_mode=models.ResponseMode.BLOCKING,
|
| 87 |
+
)
|
| 88 |
+
chat_response = await async_client.achat_messages(blocking_chat_req, timeout=60.)
|
| 89 |
+
print(chat_response)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# Define an asynchronous function to send a chat message with STREAMING ResponseMode
|
| 93 |
+
async def send_chat_message_stream():
|
| 94 |
+
user = str(uuid.uuid4())
|
| 95 |
+
# Create a blocking chat request
|
| 96 |
+
streaming_chat_req = models.ChatRequest(
|
| 97 |
+
query="Hi, dify-client-python!",
|
| 98 |
+
inputs={"city": "Beijing"},
|
| 99 |
+
user=user,
|
| 100 |
+
response_mode=models.ResponseMode.STREAMING,
|
| 101 |
+
)
|
| 102 |
+
async for chunk in await async_client.achat_messages(streaming_chat_req, timeout=60.):
|
| 103 |
+
print(chunk)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# Run the asynchronous function
|
| 107 |
+
asyncio.gather(send_chat_message(), send_chat_message_stream())
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
## Documentation
|
| 111 |
+
|
| 112 |
+
For detailed information on all the functionalities and how to use each endpoint, please refer to the official Dify API
|
| 113 |
+
documentation. This will provide you with comprehensive guidance on request and response structures, error handling, and
|
| 114 |
+
other important details.
|
| 115 |
+
|
| 116 |
+
## Contributing
|
| 117 |
+
|
| 118 |
+
Contributions are welcome! If you would like to contribute to the `dify-client-python`, please feel free to make a pull
|
| 119 |
+
request or open an issue to discuss potential changes.
|
| 120 |
+
|
| 121 |
+
## License
|
| 122 |
+
|
| 123 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
| 124 |
+
|
| 125 |
+
```text
|
| 126 |
+
MIT License
|
| 127 |
+
|
| 128 |
+
Copyright (c) 2024 haoyuhu
|
| 129 |
+
|
| 130 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 131 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 132 |
+
in the Software without restriction, including without limitation the rights
|
| 133 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 134 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 135 |
+
furnished to do so, subject to the following conditions:
|
| 136 |
+
|
| 137 |
+
The above copyright notice and this permission notice shall be included in all
|
| 138 |
+
copies or substantial portions of the Software.
|
| 139 |
+
|
| 140 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 141 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 142 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 143 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 144 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 145 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 146 |
+
SOFTWARE.
|
| 147 |
+
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
## Support
|
| 151 |
+
|
| 152 |
+
If you encounter any issues or have questions regarding the usage of this client, please reach out to the Dify Client
|
| 153 |
+
support team.
|
| 154 |
+
|
| 155 |
+
Happy coding! 🚀
|
dify_client_python/build.sh
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
set -e
|
| 4 |
+
|
| 5 |
+
rm -rf build dist *.egg-info
|
| 6 |
+
|
| 7 |
+
pip install setuptools wheel twine
|
| 8 |
+
python setup.py sdist bdist_wheel
|
| 9 |
+
twine upload dist/*
|
dify_client_python/build/lib/dify_client/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from ._clientx import Client, AsyncClient
|
dify_client_python/build/lib/dify_client/_clientx.py
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, Any, Mapping, Iterator, AsyncIterator, Union, Dict
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
from enum import StrEnum
|
| 5 |
+
except ImportError:
|
| 6 |
+
from strenum import StrEnum
|
| 7 |
+
try:
|
| 8 |
+
from http import HTTPMethod
|
| 9 |
+
except ImportError:
|
| 10 |
+
class HTTPMethod(StrEnum):
|
| 11 |
+
GET = "GET"
|
| 12 |
+
POST = "POST"
|
| 13 |
+
PUT = "PUT"
|
| 14 |
+
DELETE = "DELETE"
|
| 15 |
+
|
| 16 |
+
import httpx
|
| 17 |
+
# noinspection PyProtectedMember
|
| 18 |
+
import httpx._types as types
|
| 19 |
+
from httpx_sse import connect_sse, ServerSentEvent, aconnect_sse
|
| 20 |
+
from pydantic import BaseModel
|
| 21 |
+
|
| 22 |
+
from dify_client import errors, models
|
| 23 |
+
|
| 24 |
+
_httpx_client = httpx.Client()
|
| 25 |
+
_async_httpx_client = httpx.AsyncClient()
|
| 26 |
+
|
| 27 |
+
IGNORED_STREAM_EVENTS = (models.StreamEvent.PING.value,)
|
| 28 |
+
|
| 29 |
+
# feedback
|
| 30 |
+
ENDPOINT_FEEDBACKS = "/messages/{message_id}/feedbacks"
|
| 31 |
+
# suggest
|
| 32 |
+
ENDPOINT_SUGGESTED = "/messages/{message_id}/suggested"
|
| 33 |
+
# files upload
|
| 34 |
+
ENDPOINT_FILES_UPLOAD = "/files/upload"
|
| 35 |
+
# completion
|
| 36 |
+
ENDPOINT_COMPLETION_MESSAGES = "/completion-messages"
|
| 37 |
+
ENDPOINT_STOP_COMPLETION_MESSAGES = "/completion-messages/{task_id}/stop"
|
| 38 |
+
# chat
|
| 39 |
+
ENDPOINT_CHAT_MESSAGES = "/chat-messages"
|
| 40 |
+
ENDPOINT_STOP_CHAT_MESSAGES = "/chat-messages/{task_id}/stop"
|
| 41 |
+
# workflow
|
| 42 |
+
ENDPOINT_RUN_WORKFLOWS = "/workflows/run"
|
| 43 |
+
ENDPOINT_STOP_WORKFLOWS = "/workflows/{task_id}/stop"
|
| 44 |
+
# audio <-> text
|
| 45 |
+
ENDPOINT_TEXT_TO_AUDIO = "/text-to-audio"
|
| 46 |
+
ENDPOINT_AUDIO_TO_TEXT = "/audio-to-text"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Client(BaseModel):
|
| 50 |
+
api_key: str
|
| 51 |
+
api_base: Optional[str] = "https://api.dify.ai/v1"
|
| 52 |
+
|
| 53 |
+
def request(self, endpoint: str, method: str,
|
| 54 |
+
content: Optional[types.RequestContent] = None,
|
| 55 |
+
data: Optional[types.RequestData] = None,
|
| 56 |
+
files: Optional[types.RequestFiles] = None,
|
| 57 |
+
json: Optional[Any] = None,
|
| 58 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 59 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 60 |
+
**kwargs: object,
|
| 61 |
+
) -> httpx.Response:
|
| 62 |
+
"""
|
| 63 |
+
Sends a synchronous HTTP request to the specified endpoint.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
endpoint: The API endpoint to send the request to.
|
| 67 |
+
method: The HTTP method to use (e.g., 'GET', 'POST').
|
| 68 |
+
content: Raw content to include in the request body.
|
| 69 |
+
data: Form data to include in the request body.
|
| 70 |
+
files: Files to include in the request body.
|
| 71 |
+
json: JSON data to include in the request body.
|
| 72 |
+
params: Query parameters to include in the request URL.
|
| 73 |
+
headers: Additional headers to include in the request.
|
| 74 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 75 |
+
|
| 76 |
+
Returns:
|
| 77 |
+
A `httpx.Response` object containing the HTTP response.
|
| 78 |
+
|
| 79 |
+
Raises:
|
| 80 |
+
Various DifyAPIError exceptions if the response contains an error.
|
| 81 |
+
"""
|
| 82 |
+
merged_headers = {}
|
| 83 |
+
if headers:
|
| 84 |
+
merged_headers.update(headers)
|
| 85 |
+
self._prepare_auth_headers(merged_headers)
|
| 86 |
+
|
| 87 |
+
response = _httpx_client.request(method, endpoint, content=content, data=data, files=files, json=json,
|
| 88 |
+
params=params, headers=merged_headers, **kwargs)
|
| 89 |
+
errors.raise_for_status(response)
|
| 90 |
+
return response
|
| 91 |
+
|
| 92 |
+
def request_stream(self, endpoint: str, method: str,
|
| 93 |
+
content: Optional[types.RequestContent] = None,
|
| 94 |
+
data: Optional[types.RequestData] = None,
|
| 95 |
+
files: Optional[types.RequestFiles] = None,
|
| 96 |
+
json: Optional[Any] = None,
|
| 97 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 98 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 99 |
+
**kwargs,
|
| 100 |
+
) -> Iterator[ServerSentEvent]:
|
| 101 |
+
"""
|
| 102 |
+
Opens a server-sent events (SSE) stream to the specified endpoint.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
endpoint: The API endpoint to send the request to.
|
| 106 |
+
method: The HTTP method to use (e.g., 'GET', 'POST').
|
| 107 |
+
content: Raw content to include in the request body.
|
| 108 |
+
data: Form data to include in the request body.
|
| 109 |
+
files: Files to include in the request body.
|
| 110 |
+
json: JSON data to include in the request body.
|
| 111 |
+
params: Query parameters to include in the request URL.
|
| 112 |
+
headers: Additional headers to include in the request.
|
| 113 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
An iterator of `ServerSentEvent` objects representing the stream of events.
|
| 117 |
+
|
| 118 |
+
Raises:
|
| 119 |
+
Various DifyAPIError exceptions if an error event is received in the stream.
|
| 120 |
+
"""
|
| 121 |
+
merged_headers = {}
|
| 122 |
+
if headers:
|
| 123 |
+
merged_headers.update(headers)
|
| 124 |
+
self._prepare_auth_headers(merged_headers)
|
| 125 |
+
|
| 126 |
+
with connect_sse(_httpx_client, method, endpoint, headers=merged_headers,
|
| 127 |
+
content=content, data=data, files=files, json=json, params=params, **kwargs) as event_source:
|
| 128 |
+
if not _check_stream_content_type(event_source.response):
|
| 129 |
+
event_source.response.read()
|
| 130 |
+
errors.raise_for_status(event_source.response)
|
| 131 |
+
for sse in event_source.iter_sse():
|
| 132 |
+
errors.raise_for_status(sse)
|
| 133 |
+
if sse.event in IGNORED_STREAM_EVENTS or sse.data in IGNORED_STREAM_EVENTS:
|
| 134 |
+
continue
|
| 135 |
+
yield sse
|
| 136 |
+
|
| 137 |
+
def feedback_messages(self, message_id: str, req: models.FeedbackRequest, **kwargs) -> models.FeedbackResponse:
|
| 138 |
+
"""
|
| 139 |
+
Submits feedback for a specific message.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
message_id: The identifier of the message to submit feedback for.
|
| 143 |
+
req: A `FeedbackRequest` object containing the feedback details, such as the rating.
|
| 144 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
A `FeedbackResponse` object containing the result of the feedback submission.
|
| 148 |
+
"""
|
| 149 |
+
response = self.request(
|
| 150 |
+
self._prepare_url(ENDPOINT_FEEDBACKS, message_id=message_id),
|
| 151 |
+
HTTPMethod.POST,
|
| 152 |
+
json=req.model_dump(),
|
| 153 |
+
**kwargs,
|
| 154 |
+
)
|
| 155 |
+
return models.FeedbackResponse(**response.json())
|
| 156 |
+
|
| 157 |
+
def suggest_messages(self, message_id: str, req: models.ChatSuggestRequest, **kwargs) -> models.ChatSuggestResponse:
|
| 158 |
+
"""
|
| 159 |
+
Retrieves suggested messages based on a specific message.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
message_id: The identifier of the message to get suggestions for.
|
| 163 |
+
req: A `ChatSuggestRequest` object containing the request details.
|
| 164 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
A `ChatSuggestResponse` object containing suggested messages.
|
| 168 |
+
"""
|
| 169 |
+
response = self.request(
|
| 170 |
+
self._prepare_url(ENDPOINT_SUGGESTED, message_id=message_id),
|
| 171 |
+
HTTPMethod.GET,
|
| 172 |
+
params=req.model_dump(),
|
| 173 |
+
**kwargs,
|
| 174 |
+
)
|
| 175 |
+
return models.ChatSuggestResponse(**response.json())
|
| 176 |
+
|
| 177 |
+
def upload_files(self, file: types.FileTypes, req: models.UploadFileRequest,
|
| 178 |
+
**kwargs) -> models.UploadFileResponse:
|
| 179 |
+
"""
|
| 180 |
+
Uploads a file to be used in subsequent requests.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
file: The file to upload. This can be a file-like object, or a tuple of
|
| 184 |
+
(`filename`, file-like object, mime_type).
|
| 185 |
+
req: An `UploadFileRequest` object containing the upload details, such as the user who is uploading.
|
| 186 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
An `UploadFileResponse` object containing details about the uploaded file, such as its identifier and URL.
|
| 190 |
+
"""
|
| 191 |
+
response = self.request(
|
| 192 |
+
self._prepare_url(ENDPOINT_FILES_UPLOAD),
|
| 193 |
+
HTTPMethod.POST,
|
| 194 |
+
data=req.model_dump(),
|
| 195 |
+
files=[("file", file)],
|
| 196 |
+
**kwargs,
|
| 197 |
+
)
|
| 198 |
+
return models.UploadFileResponse(**response.json())
|
| 199 |
+
|
| 200 |
+
def completion_messages(self, req: models.CompletionRequest, **kwargs) \
|
| 201 |
+
-> Union[models.CompletionResponse, Iterator[models.CompletionStreamResponse]]:
|
| 202 |
+
"""
|
| 203 |
+
Sends a request to generate a completion or a series of completions based on the provided input.
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
If the response mode is blocking, it returns a `CompletionResponse` object containing the generated message.
|
| 207 |
+
If the response mode is streaming, it returns an iterator of `CompletionStreamResponse` objects containing
|
| 208 |
+
the stream of generated events.
|
| 209 |
+
"""
|
| 210 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 211 |
+
return self._completion_messages(req, **kwargs)
|
| 212 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 213 |
+
return self._completion_messages_stream(req, **kwargs)
|
| 214 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 215 |
+
|
| 216 |
+
def _completion_messages(self, req: models.CompletionRequest, **kwargs) -> models.CompletionResponse:
|
| 217 |
+
response = self.request(
|
| 218 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 219 |
+
HTTPMethod.POST,
|
| 220 |
+
json=req.model_dump(),
|
| 221 |
+
**kwargs,
|
| 222 |
+
)
|
| 223 |
+
return models.CompletionResponse(**response.json())
|
| 224 |
+
|
| 225 |
+
def _completion_messages_stream(self, req: models.CompletionRequest, **kwargs) \
|
| 226 |
+
-> Iterator[models.CompletionStreamResponse]:
|
| 227 |
+
event_source = self.request_stream(
|
| 228 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 229 |
+
HTTPMethod.POST,
|
| 230 |
+
json=req.model_dump(),
|
| 231 |
+
**kwargs,
|
| 232 |
+
)
|
| 233 |
+
for sse in event_source:
|
| 234 |
+
yield models.build_completion_stream_response(sse.json())
|
| 235 |
+
|
| 236 |
+
def stop_completion_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 237 |
+
"""
|
| 238 |
+
Sends a request to stop a streaming completion task.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
A `StopResponse` object indicating the success of the operation.
|
| 242 |
+
"""
|
| 243 |
+
return self._stop_stream(self._prepare_url(ENDPOINT_STOP_COMPLETION_MESSAGES, task_id=task_id), req, **kwargs)
|
| 244 |
+
|
| 245 |
+
def chat_messages(self, req: models.ChatRequest, **kwargs) \
|
| 246 |
+
-> Union[models.ChatResponse, Iterator[models.ChatStreamResponse]]:
|
| 247 |
+
"""
|
| 248 |
+
Sends a request to generate a chat message or a series of chat messages based on the provided input.
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
If the response mode is blocking, it returns a `ChatResponse` object containing the generated chat message.
|
| 252 |
+
If the response mode is streaming, it returns an iterator of `ChatStreamResponse` objects containing the
|
| 253 |
+
stream of chat events.
|
| 254 |
+
"""
|
| 255 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 256 |
+
return self._chat_messages(req, **kwargs)
|
| 257 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 258 |
+
return self._chat_messages_stream(req, **kwargs)
|
| 259 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 260 |
+
|
| 261 |
+
def _chat_messages(self, req: models.ChatRequest, **kwargs) -> models.ChatResponse:
|
| 262 |
+
response = self.request(
|
| 263 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 264 |
+
HTTPMethod.POST,
|
| 265 |
+
json=req.model_dump(),
|
| 266 |
+
**kwargs,
|
| 267 |
+
)
|
| 268 |
+
return models.ChatResponse(**response.json())
|
| 269 |
+
|
| 270 |
+
def _chat_messages_stream(self, req: models.ChatRequest, **kwargs) -> Iterator[models.ChatStreamResponse]:
|
| 271 |
+
event_source = self.request_stream(
|
| 272 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 273 |
+
HTTPMethod.POST,
|
| 274 |
+
json=req.model_dump(),
|
| 275 |
+
**kwargs,
|
| 276 |
+
)
|
| 277 |
+
for sse in event_source:
|
| 278 |
+
yield models.build_chat_stream_response(sse.json())
|
| 279 |
+
|
| 280 |
+
def stop_chat_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 281 |
+
"""
|
| 282 |
+
Sends a request to stop a streaming chat task.
|
| 283 |
+
|
| 284 |
+
Returns:
|
| 285 |
+
A `StopResponse` object indicating the success of the operation.
|
| 286 |
+
"""
|
| 287 |
+
return self._stop_stream(self._prepare_url(ENDPOINT_STOP_CHAT_MESSAGES, task_id=task_id), req, **kwargs)
|
| 288 |
+
|
| 289 |
+
def run_workflows(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 290 |
+
-> Union[models.WorkflowsRunResponse, Iterator[models.WorkflowsRunStreamResponse]]:
|
| 291 |
+
"""
|
| 292 |
+
Initiates the execution of a workflow, which can consist of multiple steps and actions.
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
If the response mode is blocking, it returns a `WorkflowsRunResponse` object containing the results of the
|
| 296 |
+
completed workflow.
|
| 297 |
+
If the response mode is streaming, it returns an iterator of `WorkflowsRunStreamResponse` objects
|
| 298 |
+
containing the stream of workflow events.
|
| 299 |
+
"""
|
| 300 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 301 |
+
return self._run_workflows(req, **kwargs)
|
| 302 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 303 |
+
return self._run_workflows_stream(req, **kwargs)
|
| 304 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 305 |
+
|
| 306 |
+
def _run_workflows(self, req: models.WorkflowsRunRequest, **kwargs) -> models.WorkflowsRunResponse:
|
| 307 |
+
response = self.request(
|
| 308 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 309 |
+
HTTPMethod.POST,
|
| 310 |
+
json=req.model_dump(),
|
| 311 |
+
**kwargs,
|
| 312 |
+
)
|
| 313 |
+
return models.WorkflowsRunResponse(**response.json())
|
| 314 |
+
|
| 315 |
+
def _run_workflows_stream(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 316 |
+
-> Iterator[models.WorkflowsRunStreamResponse]:
|
| 317 |
+
event_source = self.request_stream(
|
| 318 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 319 |
+
HTTPMethod.POST,
|
| 320 |
+
json=req.model_dump(),
|
| 321 |
+
**kwargs,
|
| 322 |
+
)
|
| 323 |
+
for sse in event_source:
|
| 324 |
+
yield models.build_workflows_stream_response(sse.json())
|
| 325 |
+
|
| 326 |
+
def stop_workflows(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 327 |
+
"""
|
| 328 |
+
Sends a request to stop a streaming workflow task.
|
| 329 |
+
|
| 330 |
+
Returns:
|
| 331 |
+
A `StopResponse` object indicating the success of the operation.
|
| 332 |
+
"""
|
| 333 |
+
return self._stop_stream(self._prepare_url(ENDPOINT_STOP_WORKFLOWS, task_id=task_id), req, **kwargs)
|
| 334 |
+
|
| 335 |
+
def _stop_stream(self, endpoint: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 336 |
+
response = self.request(
|
| 337 |
+
endpoint,
|
| 338 |
+
HTTPMethod.POST,
|
| 339 |
+
json=req.model_dump(),
|
| 340 |
+
**kwargs,
|
| 341 |
+
)
|
| 342 |
+
return models.StopResponse(**response.json())
|
| 343 |
+
|
| 344 |
+
def _prepare_url(self, endpoint: str, **kwargs) -> str:
|
| 345 |
+
return self.api_base + endpoint.format(**kwargs)
|
| 346 |
+
|
| 347 |
+
def _prepare_auth_headers(self, headers: Dict[str, str]):
|
| 348 |
+
if "authorization" not in (key.lower() for key in headers.keys()):
|
| 349 |
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
class AsyncClient(BaseModel):
|
| 353 |
+
api_key: str
|
| 354 |
+
api_base: Optional[str] = "https://api.dify.ai/v1"
|
| 355 |
+
|
| 356 |
+
async def arequest(self, endpoint: str, method: str,
|
| 357 |
+
content: Optional[types.RequestContent] = None,
|
| 358 |
+
data: Optional[types.RequestData] = None,
|
| 359 |
+
files: Optional[types.RequestFiles] = None,
|
| 360 |
+
json: Optional[Any] = None,
|
| 361 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 362 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 363 |
+
**kwargs,
|
| 364 |
+
) -> httpx.Response:
|
| 365 |
+
"""
|
| 366 |
+
Asynchronously sends a request to the specified Dify API endpoint.
|
| 367 |
+
|
| 368 |
+
Args:
|
| 369 |
+
endpoint: The endpoint URL to which the request is sent.
|
| 370 |
+
method: The HTTP method to be used for the request (e.g., 'GET', 'POST').
|
| 371 |
+
content: Raw content to include in the request body, if any.
|
| 372 |
+
data: Form data to be sent in the request body.
|
| 373 |
+
files: Files to be uploaded with the request.
|
| 374 |
+
json: JSON data to be sent in the request body.
|
| 375 |
+
params: Query parameters to be included in the request URL.
|
| 376 |
+
headers: Additional headers to be sent with the request.
|
| 377 |
+
**kwargs: Extra keyword arguments to be passed to the underlying HTTPX request function.
|
| 378 |
+
|
| 379 |
+
Returns:
|
| 380 |
+
A httpx.Response object containing the server's response to the HTTP request.
|
| 381 |
+
|
| 382 |
+
Raises:
|
| 383 |
+
Various DifyAPIError exceptions if the response contains an error.
|
| 384 |
+
"""
|
| 385 |
+
merged_headers = {}
|
| 386 |
+
if headers:
|
| 387 |
+
merged_headers.update(headers)
|
| 388 |
+
self._prepare_auth_headers(merged_headers)
|
| 389 |
+
|
| 390 |
+
response = await _async_httpx_client.request(method, endpoint, content=content, data=data, files=files,
|
| 391 |
+
json=json, params=params, headers=merged_headers, **kwargs)
|
| 392 |
+
errors.raise_for_status(response)
|
| 393 |
+
return response
|
| 394 |
+
|
| 395 |
+
async def arequest_stream(self, endpoint: str, method: str,
|
| 396 |
+
content: Optional[types.RequestContent] = None,
|
| 397 |
+
data: Optional[types.RequestData] = None,
|
| 398 |
+
files: Optional[types.RequestFiles] = None,
|
| 399 |
+
json: Optional[Any] = None,
|
| 400 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 401 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 402 |
+
**kwargs,
|
| 403 |
+
) -> AsyncIterator[ServerSentEvent]:
|
| 404 |
+
"""
|
| 405 |
+
Asynchronously establishes a streaming connection to the specified Dify API endpoint.
|
| 406 |
+
|
| 407 |
+
Args:
|
| 408 |
+
endpoint: The endpoint URL to which the request is sent.
|
| 409 |
+
method: The HTTP method to be used for the request (e.g., 'GET', 'POST').
|
| 410 |
+
content: Raw content to include in the request body, if any.
|
| 411 |
+
data: Form data to be sent in the request body.
|
| 412 |
+
files: Files to be uploaded with the request.
|
| 413 |
+
json: JSON data to be sent in the request body.
|
| 414 |
+
params: Query parameters to be included in the request URL.
|
| 415 |
+
headers: Additional headers to be sent with the request.
|
| 416 |
+
**kwargs: Extra keyword arguments to be passed to the underlying HTTPX request function.
|
| 417 |
+
|
| 418 |
+
Yields:
|
| 419 |
+
ServerSentEvent objects representing the events received from the server.
|
| 420 |
+
|
| 421 |
+
Raises:
|
| 422 |
+
Various DifyAPIError exceptions if an error event is received in the stream.
|
| 423 |
+
"""
|
| 424 |
+
merged_headers = {}
|
| 425 |
+
if headers:
|
| 426 |
+
merged_headers.update(headers)
|
| 427 |
+
self._prepare_auth_headers(merged_headers)
|
| 428 |
+
|
| 429 |
+
async with aconnect_sse(_async_httpx_client, method, endpoint, headers=merged_headers,
|
| 430 |
+
content=content, data=data, files=files, json=json, params=params,
|
| 431 |
+
**kwargs) as event_source:
|
| 432 |
+
if not _check_stream_content_type(event_source.response):
|
| 433 |
+
await event_source.response.aread()
|
| 434 |
+
errors.raise_for_status(event_source.response)
|
| 435 |
+
async for sse in event_source.aiter_sse():
|
| 436 |
+
errors.raise_for_status(sse)
|
| 437 |
+
if sse.event in IGNORED_STREAM_EVENTS or sse.data in IGNORED_STREAM_EVENTS:
|
| 438 |
+
continue
|
| 439 |
+
yield sse
|
| 440 |
+
|
| 441 |
+
async def afeedback_messages(self, message_id: str, req: models.FeedbackRequest, **kwargs) \
|
| 442 |
+
-> models.FeedbackResponse:
|
| 443 |
+
"""
|
| 444 |
+
Submits feedback for a specific message.
|
| 445 |
+
|
| 446 |
+
Args:
|
| 447 |
+
message_id: The identifier of the message to submit feedback for.
|
| 448 |
+
req: A `FeedbackRequest` object containing the feedback details, such as the rating.
|
| 449 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 450 |
+
|
| 451 |
+
Returns:
|
| 452 |
+
A `FeedbackResponse` object containing the result of the feedback submission.
|
| 453 |
+
"""
|
| 454 |
+
response = await self.arequest(
|
| 455 |
+
self._prepare_url(ENDPOINT_FEEDBACKS, message_id=message_id),
|
| 456 |
+
HTTPMethod.POST,
|
| 457 |
+
json=req.model_dump(),
|
| 458 |
+
**kwargs,
|
| 459 |
+
)
|
| 460 |
+
return models.FeedbackResponse(**response.json())
|
| 461 |
+
|
| 462 |
+
async def asuggest_messages(self, message_id: str, req: models.ChatSuggestRequest, **kwargs) \
|
| 463 |
+
-> models.ChatSuggestResponse:
|
| 464 |
+
"""
|
| 465 |
+
Retrieves suggested messages based on a specific message.
|
| 466 |
+
|
| 467 |
+
Args:
|
| 468 |
+
message_id: The identifier of the message to get suggestions for.
|
| 469 |
+
req: A `ChatSuggestRequest` object containing the request details.
|
| 470 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 471 |
+
|
| 472 |
+
Returns:
|
| 473 |
+
A `ChatSuggestResponse` object containing suggested messages.
|
| 474 |
+
"""
|
| 475 |
+
response = await self.arequest(
|
| 476 |
+
self._prepare_url(ENDPOINT_SUGGESTED, message_id=message_id),
|
| 477 |
+
HTTPMethod.GET,
|
| 478 |
+
params=req.model_dump(),
|
| 479 |
+
**kwargs,
|
| 480 |
+
)
|
| 481 |
+
return models.ChatSuggestResponse(**response.json())
|
| 482 |
+
|
| 483 |
+
async def aupload_files(self, file: types.FileTypes, req: models.UploadFileRequest, **kwargs) \
|
| 484 |
+
-> models.UploadFileResponse:
|
| 485 |
+
"""
|
| 486 |
+
Uploads a file to be used in subsequent requests.
|
| 487 |
+
|
| 488 |
+
Args:
|
| 489 |
+
file: The file to upload. This can be a file-like object, or a tuple of
|
| 490 |
+
(`filename`, file-like object, mime_type).
|
| 491 |
+
req: An `UploadFileRequest` object containing the upload details, such as the user who is uploading.
|
| 492 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 493 |
+
|
| 494 |
+
Returns:
|
| 495 |
+
An `UploadFileResponse` object containing details about the uploaded file, such as its identifier and URL.
|
| 496 |
+
"""
|
| 497 |
+
response = await self.arequest(
|
| 498 |
+
self._prepare_url(ENDPOINT_FILES_UPLOAD),
|
| 499 |
+
HTTPMethod.POST,
|
| 500 |
+
data=req.model_dump(),
|
| 501 |
+
files=[("file", file)],
|
| 502 |
+
**kwargs,
|
| 503 |
+
)
|
| 504 |
+
return models.UploadFileResponse(**response.json())
|
| 505 |
+
|
| 506 |
+
async def acompletion_messages(self, req: models.CompletionRequest, **kwargs) \
|
| 507 |
+
-> Union[models.CompletionResponse, AsyncIterator[models.CompletionStreamResponse]]:
|
| 508 |
+
"""
|
| 509 |
+
Sends a request to generate a completion or a series of completions based on the provided input.
|
| 510 |
+
|
| 511 |
+
Returns:
|
| 512 |
+
If the response mode is blocking, it returns a `CompletionResponse` object containing the generated message.
|
| 513 |
+
If the response mode is streaming, it returns an iterator of `CompletionStreamResponse` objects containing
|
| 514 |
+
the stream of generated events.
|
| 515 |
+
"""
|
| 516 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 517 |
+
return await self._acompletion_messages(req, **kwargs)
|
| 518 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 519 |
+
return self._acompletion_messages_stream(req, **kwargs)
|
| 520 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 521 |
+
|
| 522 |
+
async def _acompletion_messages(self, req: models.CompletionRequest, **kwargs) -> models.CompletionResponse:
|
| 523 |
+
response = await self.arequest(
|
| 524 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 525 |
+
HTTPMethod.POST,
|
| 526 |
+
json=req.model_dump(),
|
| 527 |
+
**kwargs,
|
| 528 |
+
)
|
| 529 |
+
return models.CompletionResponse(**response.json())
|
| 530 |
+
|
| 531 |
+
async def _acompletion_messages_stream(self, req: models.CompletionRequest, **kwargs) \
|
| 532 |
+
-> AsyncIterator[models.CompletionStreamResponse]:
|
| 533 |
+
async for sse in self.arequest_stream(
|
| 534 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 535 |
+
HTTPMethod.POST,
|
| 536 |
+
json=req.model_dump(),
|
| 537 |
+
**kwargs):
|
| 538 |
+
yield models.build_completion_stream_response(sse.json())
|
| 539 |
+
|
| 540 |
+
async def astop_completion_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 541 |
+
"""
|
| 542 |
+
Sends a request to stop a streaming completion task.
|
| 543 |
+
|
| 544 |
+
Returns:
|
| 545 |
+
A `StopResponse` object indicating the success of the operation.
|
| 546 |
+
"""
|
| 547 |
+
return await self._astop_stream(
|
| 548 |
+
self._prepare_url(ENDPOINT_STOP_COMPLETION_MESSAGES, task_id=task_id), req, **kwargs)
|
| 549 |
+
|
| 550 |
+
async def achat_messages(self, req: models.ChatRequest, **kwargs) \
|
| 551 |
+
-> Union[models.ChatResponse, AsyncIterator[models.ChatStreamResponse]]:
|
| 552 |
+
"""
|
| 553 |
+
Sends a request to generate a chat message or a series of chat messages based on the provided input.
|
| 554 |
+
|
| 555 |
+
Returns:
|
| 556 |
+
If the response mode is blocking, it returns a `ChatResponse` object containing the generated chat message.
|
| 557 |
+
If the response mode is streaming, it returns an iterator of `ChatStreamResponse` objects containing the
|
| 558 |
+
stream of chat events.
|
| 559 |
+
"""
|
| 560 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 561 |
+
return await self._achat_messages(req, **kwargs)
|
| 562 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 563 |
+
return self._achat_messages_stream(req, **kwargs)
|
| 564 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 565 |
+
|
| 566 |
+
async def _achat_messages(self, req: models.ChatRequest, **kwargs) -> models.ChatResponse:
|
| 567 |
+
response = await self.arequest(
|
| 568 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 569 |
+
HTTPMethod.POST,
|
| 570 |
+
json=req.model_dump(),
|
| 571 |
+
**kwargs,
|
| 572 |
+
)
|
| 573 |
+
return models.ChatResponse(**response.json())
|
| 574 |
+
|
| 575 |
+
async def _achat_messages_stream(self, req: models.ChatRequest, **kwargs) \
|
| 576 |
+
-> AsyncIterator[models.ChatStreamResponse]:
|
| 577 |
+
async for sse in self.arequest_stream(
|
| 578 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 579 |
+
HTTPMethod.POST,
|
| 580 |
+
json=req.model_dump(),
|
| 581 |
+
**kwargs):
|
| 582 |
+
yield models.build_chat_stream_response(sse.json())
|
| 583 |
+
|
| 584 |
+
async def astop_chat_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 585 |
+
"""
|
| 586 |
+
Sends a request to stop a streaming chat task.
|
| 587 |
+
|
| 588 |
+
Returns:
|
| 589 |
+
A `StopResponse` object indicating the success of the operation.
|
| 590 |
+
"""
|
| 591 |
+
return await self._astop_stream(self._prepare_url(ENDPOINT_STOP_CHAT_MESSAGES, task_id=task_id), req, **kwargs)
|
| 592 |
+
|
| 593 |
+
async def arun_workflows(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 594 |
+
-> Union[models.WorkflowsRunResponse, AsyncIterator[models.WorkflowsStreamResponse]]:
|
| 595 |
+
"""
|
| 596 |
+
Initiates the execution of a workflow, which can consist of multiple steps and actions.
|
| 597 |
+
|
| 598 |
+
Returns:
|
| 599 |
+
If the response mode is blocking, it returns a `WorkflowsRunResponse` object containing the results of the
|
| 600 |
+
completed workflow.
|
| 601 |
+
If the response mode is streaming, it returns an iterator of `WorkflowsRunStreamResponse` objects
|
| 602 |
+
containing the stream of workflow events.
|
| 603 |
+
"""
|
| 604 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 605 |
+
return await self._arun_workflows(req, **kwargs)
|
| 606 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 607 |
+
return self._arun_workflows_stream(req, **kwargs)
|
| 608 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 609 |
+
|
| 610 |
+
async def _arun_workflows(self, req: models.WorkflowsRunRequest, **kwargs) -> models.WorkflowsRunResponse:
|
| 611 |
+
response = await self.arequest(
|
| 612 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 613 |
+
HTTPMethod.POST,
|
| 614 |
+
json=req.model_dump(),
|
| 615 |
+
**kwargs,
|
| 616 |
+
)
|
| 617 |
+
return models.WorkflowsRunResponse(**response.json())
|
| 618 |
+
|
| 619 |
+
async def _arun_workflows_stream(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 620 |
+
-> AsyncIterator[models.WorkflowsRunStreamResponse]:
|
| 621 |
+
async for sse in self.arequest_stream(
|
| 622 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 623 |
+
HTTPMethod.POST,
|
| 624 |
+
json=req.model_dump(),
|
| 625 |
+
**kwargs):
|
| 626 |
+
yield models.build_workflows_stream_response(sse.json())
|
| 627 |
+
|
| 628 |
+
async def astop_workflows(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 629 |
+
"""
|
| 630 |
+
Sends a request to stop a streaming workflow task.
|
| 631 |
+
|
| 632 |
+
Returns:
|
| 633 |
+
A `StopResponse` object indicating the success of the operation.
|
| 634 |
+
"""
|
| 635 |
+
return await self._astop_stream(self._prepare_url(ENDPOINT_STOP_WORKFLOWS, task_id=task_id), req, **kwargs)
|
| 636 |
+
|
| 637 |
+
async def _astop_stream(self, endpoint: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 638 |
+
response = await self.arequest(
|
| 639 |
+
endpoint,
|
| 640 |
+
HTTPMethod.POST,
|
| 641 |
+
json=req.model_dump(),
|
| 642 |
+
**kwargs,
|
| 643 |
+
)
|
| 644 |
+
return models.StopResponse(**response.json())
|
| 645 |
+
|
| 646 |
+
def _prepare_url(self, endpoint: str, **kwargs) -> str:
|
| 647 |
+
return self.api_base + endpoint.format(**kwargs)
|
| 648 |
+
|
| 649 |
+
def _prepare_auth_headers(self, headers: Dict[str, str]):
|
| 650 |
+
if "authorization" not in (key.lower() for key in headers.keys()):
|
| 651 |
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
def _get_content_type(headers: httpx.Headers) -> str:
|
| 655 |
+
return headers.get("content-type", "").partition(";")[0]
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def _check_stream_content_type(response: httpx.Response) -> bool:
|
| 659 |
+
content_type = _get_content_type(response.headers)
|
| 660 |
+
return response.is_success and "text/event-stream" in content_type
|
dify_client_python/build/lib/dify_client/errors.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from http import HTTPStatus
|
| 2 |
+
from typing import Union
|
| 3 |
+
|
| 4 |
+
import httpx
|
| 5 |
+
import httpx_sse
|
| 6 |
+
|
| 7 |
+
from dify_client import models
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DifyAPIError(Exception):
|
| 11 |
+
def __init__(self, status: int, code: str, message: str):
|
| 12 |
+
super().__init__(f"status_code={status}, code={code}, {message}")
|
| 13 |
+
self.status = status
|
| 14 |
+
self.code = code
|
| 15 |
+
self.message = message
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DifyInvalidParam(DifyAPIError):
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DifyNotChatApp(DifyAPIError):
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class DifyResourceNotFound(DifyAPIError):
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class DifyAppUnavailable(DifyAPIError):
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class DifyProviderNotInitialize(DifyAPIError):
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class DifyProviderQuotaExceeded(DifyAPIError):
|
| 39 |
+
pass
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class DifyModelCurrentlyNotSupport(DifyAPIError):
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class DifyCompletionRequestError(DifyAPIError):
|
| 47 |
+
pass
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class DifyInternalServerError(DifyAPIError):
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class DifyNoFileUploaded(DifyAPIError):
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class DifyTooManyFiles(DifyAPIError):
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class DifyUnsupportedPreview(DifyAPIError):
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class DifyUnsupportedEstimate(DifyAPIError):
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class DifyFileTooLarge(DifyAPIError):
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class DifyUnsupportedFileType(DifyAPIError):
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class DifyS3ConnectionFailed(DifyAPIError):
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class DifyS3PermissionDenied(DifyAPIError):
|
| 83 |
+
pass
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class DifyS3FileTooLarge(DifyAPIError):
|
| 87 |
+
pass
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
SPEC_CODE_ERRORS = {
|
| 91 |
+
# completion & chat & workflow
|
| 92 |
+
"invalid_param": DifyInvalidParam,
|
| 93 |
+
"not_chat_app": DifyNotChatApp,
|
| 94 |
+
"app_unavailable": DifyAppUnavailable,
|
| 95 |
+
"provider_not_initialize": DifyProviderNotInitialize,
|
| 96 |
+
"provider_quota_exceeded": DifyProviderQuotaExceeded,
|
| 97 |
+
"model_currently_not_support": DifyModelCurrentlyNotSupport,
|
| 98 |
+
"completion_request_error": DifyCompletionRequestError,
|
| 99 |
+
# files upload
|
| 100 |
+
"no_file_uploaded": DifyNoFileUploaded,
|
| 101 |
+
"too_many_files": DifyTooManyFiles,
|
| 102 |
+
"unsupported_preview": DifyUnsupportedPreview,
|
| 103 |
+
"unsupported_estimate": DifyUnsupportedEstimate,
|
| 104 |
+
"file_too_large": DifyFileTooLarge,
|
| 105 |
+
"unsupported_file_type": DifyUnsupportedFileType,
|
| 106 |
+
"s3_connection_failed": DifyS3ConnectionFailed,
|
| 107 |
+
"s3_permission_denied": DifyS3PermissionDenied,
|
| 108 |
+
"s3_file_too_large": DifyS3FileTooLarge,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def raise_for_status(response: Union[httpx.Response, httpx_sse.ServerSentEvent]):
|
| 113 |
+
if isinstance(response, httpx.Response):
|
| 114 |
+
if response.is_success:
|
| 115 |
+
return
|
| 116 |
+
json = response.json()
|
| 117 |
+
if "status" not in json:
|
| 118 |
+
json["status"] = response.status_code
|
| 119 |
+
details = models.ErrorResponse(**json)
|
| 120 |
+
elif isinstance(response, httpx_sse.ServerSentEvent):
|
| 121 |
+
if response.event != models.StreamEvent.ERROR.value:
|
| 122 |
+
return
|
| 123 |
+
details = models.ErrorStreamResponse(**response.json())
|
| 124 |
+
else:
|
| 125 |
+
raise ValueError(f"Invalid dify response type: {type(response)}")
|
| 126 |
+
|
| 127 |
+
if details.status == HTTPStatus.NOT_FOUND:
|
| 128 |
+
raise DifyResourceNotFound(details.status, details.code, details.message)
|
| 129 |
+
elif details.status == HTTPStatus.INTERNAL_SERVER_ERROR:
|
| 130 |
+
raise DifyInternalServerError(details.status, details.code, details.message)
|
| 131 |
+
else:
|
| 132 |
+
raise SPEC_CODE_ERRORS.get(details.code, DifyAPIError)(details.status, details.code, details.message)
|
dify_client_python/build/lib/dify_client/models/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .chat import *
|
| 2 |
+
from .completion import *
|
| 3 |
+
from .feedback import *
|
| 4 |
+
from .file import *
|
| 5 |
+
from .workflow import *
|
| 6 |
+
from .stream import *
|
| 7 |
+
from .base import StopRequest, StopResponse
|
dify_client_python/build/lib/dify_client/models/base.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from enum import StrEnum
|
| 3 |
+
except ImportError:
|
| 4 |
+
from strenum import StrEnum
|
| 5 |
+
from http import HTTPStatus
|
| 6 |
+
from typing import Optional, List
|
| 7 |
+
|
| 8 |
+
from pydantic import BaseModel, ConfigDict
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Mode(StrEnum):
|
| 12 |
+
CHAT = "chat"
|
| 13 |
+
COMPLETION = "completion"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ResponseMode(StrEnum):
|
| 17 |
+
STREAMING = 'streaming'
|
| 18 |
+
BLOCKING = 'blocking'
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class FileType(StrEnum):
|
| 22 |
+
IMAGE = "image"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class TransferMethod(StrEnum):
|
| 26 |
+
REMOTE_URL = "remote_url"
|
| 27 |
+
LOCAL_FILE = "local_file"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Allows the entry of various variable values defined by the App.
|
| 31 |
+
# The inputs parameter contains multiple key/value pairs, with each key corresponding to a specific variable and
|
| 32 |
+
# each value being the specific value for that variable.
|
| 33 |
+
# The text generation application requires at least one key/value pair to be inputted.
|
| 34 |
+
class CompletionInputs(BaseModel):
|
| 35 |
+
model_config = ConfigDict(extra='allow')
|
| 36 |
+
# Required The input text, the content to be processed.
|
| 37 |
+
query: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class File(BaseModel):
|
| 41 |
+
type: FileType
|
| 42 |
+
transfer_method: TransferMethod
|
| 43 |
+
url: Optional[str]
|
| 44 |
+
# Uploaded file ID, which must be obtained by uploading through the File Upload API in advance
|
| 45 |
+
# (when the transfer method is local_file)
|
| 46 |
+
upload_file_id: Optional[str]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Usage(BaseModel):
|
| 50 |
+
prompt_tokens: int
|
| 51 |
+
completion_tokens: int
|
| 52 |
+
total_tokens: int
|
| 53 |
+
|
| 54 |
+
prompt_unit_price: str
|
| 55 |
+
prompt_price_unit: str
|
| 56 |
+
prompt_price: str
|
| 57 |
+
completion_unit_price: str
|
| 58 |
+
completion_price_unit: str
|
| 59 |
+
completion_price: str
|
| 60 |
+
total_price: str
|
| 61 |
+
currency: str
|
| 62 |
+
|
| 63 |
+
latency: float
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class RetrieverResource(BaseModel):
|
| 67 |
+
position: int
|
| 68 |
+
dataset_id: str
|
| 69 |
+
dataset_name: str
|
| 70 |
+
document_id: str
|
| 71 |
+
document_name: str
|
| 72 |
+
segment_id: str
|
| 73 |
+
score: float
|
| 74 |
+
content: str
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class Metadata(BaseModel):
|
| 78 |
+
usage: Usage
|
| 79 |
+
retriever_resources: List[RetrieverResource] = []
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class StopRequest(BaseModel):
|
| 83 |
+
user: str
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class StopResponse(BaseModel):
|
| 87 |
+
result: str # success
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class ErrorResponse(BaseModel):
|
| 91 |
+
status: int = HTTPStatus.INTERNAL_SERVER_ERROR # HTTP status code
|
| 92 |
+
code: str = ""
|
| 93 |
+
message: str = ""
|
dify_client_python/build/lib/dify_client/models/chat.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Optional, Any
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
from dify_client.models.base import ResponseMode, File
|
| 6 |
+
from dify_client.models.completion import CompletionResponse
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ChatRequest(BaseModel):
|
| 10 |
+
query: str
|
| 11 |
+
inputs: Dict[str, Any] = Field(default_factory=dict)
|
| 12 |
+
response_mode: ResponseMode
|
| 13 |
+
user: str
|
| 14 |
+
conversation_id: Optional[str] = ""
|
| 15 |
+
files: List[File] = []
|
| 16 |
+
auto_generate_name: bool = True
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ChatResponse(CompletionResponse):
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ChatSuggestRequest(BaseModel):
|
| 24 |
+
user: str
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ChatSuggestResponse(BaseModel):
|
| 28 |
+
result: str
|
| 29 |
+
data: List[str] = []
|
dify_client_python/build/lib/dify_client/models/completion.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, List
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
|
| 5 |
+
from dify_client.models.base import CompletionInputs, ResponseMode, File, Metadata, Mode
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class CompletionRequest(BaseModel):
|
| 9 |
+
inputs: CompletionInputs
|
| 10 |
+
response_mode: ResponseMode
|
| 11 |
+
user: str
|
| 12 |
+
conversation_id: Optional[str] = ""
|
| 13 |
+
files: List[File] = []
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class CompletionResponse(BaseModel):
|
| 17 |
+
message_id: str
|
| 18 |
+
conversation_id: Optional[str] = ""
|
| 19 |
+
mode: Mode
|
| 20 |
+
answer: str
|
| 21 |
+
metadata: Metadata
|
| 22 |
+
created_at: int # unix timestamp seconds
|
dify_client_python/build/lib/dify_client/models/feedback.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from enum import StrEnum
|
| 3 |
+
except ImportError:
|
| 4 |
+
from strenum import StrEnum
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Rating(StrEnum):
|
| 11 |
+
LIKE = "like"
|
| 12 |
+
DISLIKE = "dislike"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class FeedbackRequest(BaseModel):
|
| 16 |
+
rating: Optional[Rating] = None
|
| 17 |
+
user: str
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class FeedbackResponse(BaseModel):
|
| 21 |
+
result: str # success
|
dify_client_python/build/lib/dify_client/models/file.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class UploadFileRequest(BaseModel):
|
| 5 |
+
user: str
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class UploadFileResponse(BaseModel):
|
| 9 |
+
id: str
|
| 10 |
+
name: str
|
| 11 |
+
size: int
|
| 12 |
+
extension: str
|
| 13 |
+
mime_type: str
|
| 14 |
+
created_by: str # created by user
|
| 15 |
+
created_at: int # unix timestamp seconds
|
dify_client_python/build/lib/dify_client/models/stream.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from enum import StrEnum
|
| 3 |
+
except ImportError:
|
| 4 |
+
from strenum import StrEnum
|
| 5 |
+
from typing import Union, Optional, List
|
| 6 |
+
|
| 7 |
+
from pydantic import BaseModel, ConfigDict, field_validator
|
| 8 |
+
|
| 9 |
+
from dify_client import utils
|
| 10 |
+
from dify_client.models.base import Metadata, ErrorResponse
|
| 11 |
+
from dify_client.models.workflow import WorkflowStartedData, WorkflowFinishedData, NodeStartedData, NodeFinishedData
|
| 12 |
+
|
| 13 |
+
STREAM_EVENT_KEY = "event"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class StreamEvent(StrEnum):
|
| 17 |
+
MESSAGE = "message"
|
| 18 |
+
AGENT_MESSAGE = "agent_message"
|
| 19 |
+
AGENT_THOUGHT = "agent_thought"
|
| 20 |
+
MESSAGE_FILE = "message_file" # need to show file
|
| 21 |
+
WORKFLOW_STARTED = "workflow_started"
|
| 22 |
+
NODE_STARTED = "node_started"
|
| 23 |
+
NODE_FINISHED = "node_finished"
|
| 24 |
+
WORKFLOW_FINISHED = "workflow_finished"
|
| 25 |
+
MESSAGE_END = "message_end"
|
| 26 |
+
MESSAGE_REPLACE = "message_replace"
|
| 27 |
+
ERROR = "error"
|
| 28 |
+
PING = "ping"
|
| 29 |
+
|
| 30 |
+
@classmethod
|
| 31 |
+
def new(cls, event: Union["StreamEvent", str]) -> "StreamEvent":
|
| 32 |
+
if isinstance(event, cls):
|
| 33 |
+
return event
|
| 34 |
+
return utils.str_to_enum(cls, event)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class StreamResponse(BaseModel):
|
| 38 |
+
model_config = ConfigDict(extra='allow')
|
| 39 |
+
|
| 40 |
+
event: StreamEvent
|
| 41 |
+
task_id: Optional[str] = ""
|
| 42 |
+
|
| 43 |
+
@field_validator("event", mode="before")
|
| 44 |
+
def transform_stream_event(cls, event: Union[StreamEvent, str]) -> StreamEvent:
|
| 45 |
+
return StreamEvent.new(event)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class PingResponse(StreamResponse):
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class ErrorStreamResponse(StreamResponse, ErrorResponse):
|
| 53 |
+
message_id: Optional[str] = ""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class MessageStreamResponse(StreamResponse):
|
| 57 |
+
message_id: str
|
| 58 |
+
conversation_id: Optional[str] = ""
|
| 59 |
+
answer: str
|
| 60 |
+
created_at: int # unix timestamp seconds
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class MessageEndStreamResponse(StreamResponse):
|
| 64 |
+
message_id: str
|
| 65 |
+
conversation_id: Optional[str] = ""
|
| 66 |
+
created_at: int # unix timestamp seconds
|
| 67 |
+
metadata: Optional[Metadata]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class MessageReplaceStreamResponse(MessageStreamResponse):
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class AgentMessageStreamResponse(MessageStreamResponse):
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class AgentThoughtStreamResponse(StreamResponse):
|
| 79 |
+
id: str # agent thought id
|
| 80 |
+
message_id: str
|
| 81 |
+
conversation_id: str
|
| 82 |
+
position: int # thought position, start from 1
|
| 83 |
+
thought: str
|
| 84 |
+
observation: str
|
| 85 |
+
tool: str
|
| 86 |
+
tool_input: str
|
| 87 |
+
message_files: List[str] = []
|
| 88 |
+
created_at: int # unix timestamp seconds
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class MessageFileStreamResponse(StreamResponse):
|
| 92 |
+
id: str # file id
|
| 93 |
+
conversation_id: str
|
| 94 |
+
type: str # only image
|
| 95 |
+
belongs_to: str # assistant
|
| 96 |
+
url: str
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class WorkflowsStreamResponse(StreamResponse):
|
| 100 |
+
workflow_run_id: str
|
| 101 |
+
data: Optional[Union[
|
| 102 |
+
WorkflowStartedData,
|
| 103 |
+
WorkflowFinishedData,
|
| 104 |
+
NodeStartedData,
|
| 105 |
+
NodeFinishedData]
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class ChatWorkflowsStreamResponse(WorkflowsStreamResponse):
|
| 110 |
+
message_id: str
|
| 111 |
+
conversation_id: str
|
| 112 |
+
created_at: int
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
_COMPLETION_EVENT_TO_STREAM_RESP_MAPPING = {
|
| 116 |
+
StreamEvent.PING: PingResponse,
|
| 117 |
+
StreamEvent.MESSAGE: MessageStreamResponse,
|
| 118 |
+
StreamEvent.MESSAGE_END: MessageEndStreamResponse,
|
| 119 |
+
StreamEvent.MESSAGE_REPLACE: MessageReplaceStreamResponse,
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
CompletionStreamResponse = Union[
|
| 123 |
+
PingResponse,
|
| 124 |
+
MessageStreamResponse,
|
| 125 |
+
MessageEndStreamResponse,
|
| 126 |
+
MessageReplaceStreamResponse,
|
| 127 |
+
]
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def build_completion_stream_response(data: dict) -> CompletionStreamResponse:
|
| 131 |
+
event = StreamEvent.new(data.get(STREAM_EVENT_KEY))
|
| 132 |
+
return _COMPLETION_EVENT_TO_STREAM_RESP_MAPPING.get(event, StreamResponse)(**data)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
_CHAT_EVENT_TO_STREAM_RESP_MAPPING = {
|
| 136 |
+
StreamEvent.PING: PingResponse,
|
| 137 |
+
# chat
|
| 138 |
+
StreamEvent.MESSAGE: MessageStreamResponse,
|
| 139 |
+
StreamEvent.MESSAGE_END: MessageEndStreamResponse,
|
| 140 |
+
StreamEvent.MESSAGE_REPLACE: MessageReplaceStreamResponse,
|
| 141 |
+
StreamEvent.MESSAGE_FILE: MessageFileStreamResponse,
|
| 142 |
+
# agent
|
| 143 |
+
StreamEvent.AGENT_MESSAGE: AgentMessageStreamResponse,
|
| 144 |
+
StreamEvent.AGENT_THOUGHT: AgentThoughtStreamResponse,
|
| 145 |
+
# workflow
|
| 146 |
+
StreamEvent.WORKFLOW_STARTED: WorkflowsStreamResponse,
|
| 147 |
+
StreamEvent.NODE_STARTED: WorkflowsStreamResponse,
|
| 148 |
+
StreamEvent.NODE_FINISHED: WorkflowsStreamResponse,
|
| 149 |
+
StreamEvent.WORKFLOW_FINISHED: WorkflowsStreamResponse,
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
ChatStreamResponse = Union[
|
| 153 |
+
PingResponse,
|
| 154 |
+
MessageStreamResponse,
|
| 155 |
+
MessageEndStreamResponse,
|
| 156 |
+
MessageReplaceStreamResponse,
|
| 157 |
+
MessageFileStreamResponse,
|
| 158 |
+
AgentMessageStreamResponse,
|
| 159 |
+
AgentThoughtStreamResponse,
|
| 160 |
+
WorkflowsStreamResponse,
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def build_chat_stream_response(data: dict) -> ChatStreamResponse:
|
| 165 |
+
event = StreamEvent.new(data.get(STREAM_EVENT_KEY))
|
| 166 |
+
return _CHAT_EVENT_TO_STREAM_RESP_MAPPING.get(event, StreamResponse)(**data)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
_WORKFLOW_EVENT_TO_STREAM_RESP_MAPPING = {
|
| 170 |
+
StreamEvent.PING: PingResponse,
|
| 171 |
+
# workflow
|
| 172 |
+
StreamEvent.WORKFLOW_STARTED: WorkflowsStreamResponse,
|
| 173 |
+
StreamEvent.NODE_STARTED: WorkflowsStreamResponse,
|
| 174 |
+
StreamEvent.NODE_FINISHED: WorkflowsStreamResponse,
|
| 175 |
+
StreamEvent.WORKFLOW_FINISHED: WorkflowsStreamResponse,
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
WorkflowsRunStreamResponse = Union[
|
| 179 |
+
PingResponse,
|
| 180 |
+
WorkflowsStreamResponse,
|
| 181 |
+
]
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def build_workflows_stream_response(data: dict) -> WorkflowsRunStreamResponse:
|
| 185 |
+
event = StreamEvent.new(data.get(STREAM_EVENT_KEY))
|
| 186 |
+
return _WORKFLOW_EVENT_TO_STREAM_RESP_MAPPING.get(event, StreamResponse)(**data)
|
dify_client_python/build/lib/dify_client/models/workflow.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from enum import StrEnum
|
| 3 |
+
except ImportError:
|
| 4 |
+
from strenum import StrEnum
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
|
| 9 |
+
from dify_client.models.base import ResponseMode, File
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class WorkflowStatus(StrEnum):
|
| 13 |
+
RUNNING = "running"
|
| 14 |
+
SUCCEEDED = "succeeded"
|
| 15 |
+
FAILED = "failed"
|
| 16 |
+
STOPPED = "stopped"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ExecutionMetadata(BaseModel):
|
| 20 |
+
total_tokens: Optional[int]
|
| 21 |
+
total_price: Optional[str]
|
| 22 |
+
currency: Optional[str]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class WorkflowStartedData(BaseModel):
|
| 26 |
+
id: str # workflow run id
|
| 27 |
+
workflow_id: str # workflow id
|
| 28 |
+
sequence_number: int
|
| 29 |
+
inputs: Optional[dict] = None
|
| 30 |
+
created_at: int # unix timestamp seconds
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class NodeStartedData(BaseModel):
|
| 34 |
+
id: str # workflow run id
|
| 35 |
+
node_id: str
|
| 36 |
+
node_type: str
|
| 37 |
+
title: str
|
| 38 |
+
index: int
|
| 39 |
+
predecessor_node_id: Optional[str] = None
|
| 40 |
+
inputs: Optional[dict] = None
|
| 41 |
+
created_at: int
|
| 42 |
+
extras: dict = {}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class NodeFinishedData(BaseModel):
|
| 46 |
+
id: str # workflow run id
|
| 47 |
+
node_id: str
|
| 48 |
+
node_type: str
|
| 49 |
+
title: str
|
| 50 |
+
index: int
|
| 51 |
+
predecessor_node_id: Optional[str] = None
|
| 52 |
+
inputs: Optional[dict] = None
|
| 53 |
+
process_data: Optional[dict] = None
|
| 54 |
+
outputs: Optional[dict] = {}
|
| 55 |
+
status: WorkflowStatus
|
| 56 |
+
error: Optional[str] = None
|
| 57 |
+
elapsed_time: Optional[float] # seconds
|
| 58 |
+
execution_metadata: Optional[ExecutionMetadata] = None
|
| 59 |
+
created_at: int
|
| 60 |
+
finished_at: int
|
| 61 |
+
files: List = []
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class WorkflowFinishedData(BaseModel):
|
| 65 |
+
id: str # workflow run id
|
| 66 |
+
workflow_id: str # workflow id
|
| 67 |
+
sequence_number: int
|
| 68 |
+
status: WorkflowStatus
|
| 69 |
+
outputs: Optional[dict]
|
| 70 |
+
error: Optional[str]
|
| 71 |
+
elapsed_time: Optional[float]
|
| 72 |
+
total_tokens: Optional[int]
|
| 73 |
+
total_steps: Optional[int] = 0
|
| 74 |
+
created_at: int
|
| 75 |
+
finished_at: int
|
| 76 |
+
created_by: dict = {}
|
| 77 |
+
files: List = []
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class WorkflowsRunRequest(BaseModel):
|
| 81 |
+
inputs: Dict = {}
|
| 82 |
+
response_mode: ResponseMode
|
| 83 |
+
user: str
|
| 84 |
+
conversation_id: Optional[str] = ""
|
| 85 |
+
files: List[File] = []
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class WorkflowsRunResponse(BaseModel):
|
| 89 |
+
log_id: str
|
| 90 |
+
task_id: str
|
| 91 |
+
data: WorkflowFinishedData
|
dify_client_python/build/lib/dify_client/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from ._common import *
|
dify_client_python/build/lib/dify_client/utils/_common.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def str_to_enum(str_enum_class, str_value: str, ignore_not_found: bool = False, enum_default=None):
|
| 2 |
+
for key, member in str_enum_class.__members__.items():
|
| 3 |
+
if str_value == member.value:
|
| 4 |
+
return member
|
| 5 |
+
if ignore_not_found:
|
| 6 |
+
return enum_default
|
| 7 |
+
raise ValueError(f"Invalid enum value: {str_value}")
|
dify_client_python/dify_client/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
dify_client_python/dify_client/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from ._clientx import Client, AsyncClient
|
dify_client_python/dify_client/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (231 Bytes). View file
|
|
|
dify_client_python/dify_client/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (228 Bytes). View file
|
|
|
dify_client_python/dify_client/__pycache__/_clientx.cpython-310.pyc
ADDED
|
Binary file (21.6 kB). View file
|
|
|
dify_client_python/dify_client/__pycache__/_clientx.cpython-312.pyc
ADDED
|
Binary file (37.3 kB). View file
|
|
|
dify_client_python/dify_client/__pycache__/errors.cpython-310.pyc
ADDED
|
Binary file (4.33 kB). View file
|
|
|
dify_client_python/dify_client/__pycache__/errors.cpython-312.pyc
ADDED
|
Binary file (6.12 kB). View file
|
|
|
dify_client_python/dify_client/_clientx.py
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, Any, Mapping, Iterator, AsyncIterator, Union, Dict, TypedDict
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
from enum import StrEnum
|
| 5 |
+
except ImportError:
|
| 6 |
+
from strenum import StrEnum
|
| 7 |
+
try:
|
| 8 |
+
from http import HTTPMethod
|
| 9 |
+
except ImportError:
|
| 10 |
+
class HTTPMethod(StrEnum):
|
| 11 |
+
GET = "GET"
|
| 12 |
+
POST = "POST"
|
| 13 |
+
PUT = "PUT"
|
| 14 |
+
DELETE = "DELETE"
|
| 15 |
+
|
| 16 |
+
import httpx
|
| 17 |
+
# noinspection PyProtectedMember
|
| 18 |
+
import httpx._types as types
|
| 19 |
+
from pydantic import BaseModel
|
| 20 |
+
|
| 21 |
+
from dify_client_python.dify_client import errors, models
|
| 22 |
+
|
| 23 |
+
_httpx_client = httpx.Client()
|
| 24 |
+
_async_httpx_client = httpx.AsyncClient()
|
| 25 |
+
|
| 26 |
+
IGNORED_STREAM_EVENTS = (models.StreamEvent.PING.value,)
|
| 27 |
+
|
| 28 |
+
# feedback
|
| 29 |
+
ENDPOINT_FEEDBACKS = "/messages/{message_id}/feedbacks"
|
| 30 |
+
# suggest
|
| 31 |
+
ENDPOINT_SUGGESTED = "/messages/{message_id}/suggested"
|
| 32 |
+
# files upload
|
| 33 |
+
ENDPOINT_FILES_UPLOAD = "/files/upload"
|
| 34 |
+
# completion
|
| 35 |
+
ENDPOINT_COMPLETION_MESSAGES = "/completion-messages"
|
| 36 |
+
ENDPOINT_STOP_COMPLETION_MESSAGES = "/completion-messages/{task_id}/stop"
|
| 37 |
+
# chat
|
| 38 |
+
ENDPOINT_CHAT_MESSAGES = "/chat-messages"
|
| 39 |
+
ENDPOINT_STOP_CHAT_MESSAGES = "/chat-messages/{task_id}/stop"
|
| 40 |
+
# workflow
|
| 41 |
+
ENDPOINT_RUN_WORKFLOWS = "/workflows/run"
|
| 42 |
+
ENDPOINT_STOP_WORKFLOWS = "/workflows/{task_id}/stop"
|
| 43 |
+
# audio <-> text
|
| 44 |
+
ENDPOINT_TEXT_TO_AUDIO = "/text-to-audio"
|
| 45 |
+
ENDPOINT_AUDIO_TO_TEXT = "/audio-to-text"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class ServerSentEvent(TypedDict):
|
| 49 |
+
event: Optional[str]
|
| 50 |
+
data: str
|
| 51 |
+
id: Optional[str]
|
| 52 |
+
retry: Optional[int]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class Client(BaseModel):
|
| 56 |
+
api_key: str
|
| 57 |
+
api_base: Optional[str] = "https://api.dify.ai/v1"
|
| 58 |
+
|
| 59 |
+
def request(self, endpoint: str, method: str,
|
| 60 |
+
content: Optional[types.RequestContent] = None,
|
| 61 |
+
data: Optional[types.RequestData] = None,
|
| 62 |
+
files: Optional[types.RequestFiles] = None,
|
| 63 |
+
json: Optional[Any] = None,
|
| 64 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 65 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 66 |
+
**kwargs: object,
|
| 67 |
+
) -> httpx.Response:
|
| 68 |
+
"""
|
| 69 |
+
Sends a synchronous HTTP request to the specified endpoint.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
endpoint: The API endpoint to send the request to.
|
| 73 |
+
method: The HTTP method to use (e.g., 'GET', 'POST').
|
| 74 |
+
content: Raw content to include in the request body.
|
| 75 |
+
data: Form data to include in the request body.
|
| 76 |
+
files: Files to include in the request body.
|
| 77 |
+
json: JSON data to include in the request body.
|
| 78 |
+
params: Query parameters to include in the request URL.
|
| 79 |
+
headers: Additional headers to include in the request.
|
| 80 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
A `httpx.Response` object containing the HTTP response.
|
| 84 |
+
|
| 85 |
+
Raises:
|
| 86 |
+
Various DifyAPIError exceptions if the response contains an error.
|
| 87 |
+
"""
|
| 88 |
+
merged_headers = {}
|
| 89 |
+
if headers:
|
| 90 |
+
merged_headers.update(headers)
|
| 91 |
+
self._prepare_auth_headers(merged_headers)
|
| 92 |
+
|
| 93 |
+
response = _httpx_client.request(method, endpoint, content=content, data=data, files=files, json=json,
|
| 94 |
+
params=params, headers=merged_headers, **kwargs)
|
| 95 |
+
errors.raise_for_status(response)
|
| 96 |
+
return response
|
| 97 |
+
|
| 98 |
+
def request_stream(self, endpoint: str, method: str,
|
| 99 |
+
content: Optional[types.RequestContent] = None,
|
| 100 |
+
data: Optional[types.RequestData] = None,
|
| 101 |
+
files: Optional[types.RequestFiles] = None,
|
| 102 |
+
json: Optional[Any] = None,
|
| 103 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 104 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 105 |
+
**kwargs,
|
| 106 |
+
) -> Iterator[ServerSentEvent]:
|
| 107 |
+
"""
|
| 108 |
+
Opens a server-sent events (SSE) stream to the specified endpoint.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
endpoint: The API endpoint to send the request to.
|
| 112 |
+
method: The HTTP method to use (e.g., 'GET', 'POST').
|
| 113 |
+
content: Raw content to include in the request body.
|
| 114 |
+
data: Form data to include in the request body.
|
| 115 |
+
files: Files to include in the request body.
|
| 116 |
+
json: JSON data to include in the request body.
|
| 117 |
+
params: Query parameters to include in the request URL.
|
| 118 |
+
headers: Additional headers to include in the request.
|
| 119 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
An iterator of `ServerSentEvent` objects representing the stream of events.
|
| 123 |
+
|
| 124 |
+
Raises:
|
| 125 |
+
Various DifyAPIError exceptions if an error event is received in the stream.
|
| 126 |
+
"""
|
| 127 |
+
merged_headers = {
|
| 128 |
+
'Accept': 'text/event-stream',
|
| 129 |
+
'Cache-Control': 'no-cache',
|
| 130 |
+
'Connection': 'keep-alive'
|
| 131 |
+
}
|
| 132 |
+
if headers:
|
| 133 |
+
merged_headers.update(headers)
|
| 134 |
+
self._prepare_auth_headers(merged_headers)
|
| 135 |
+
|
| 136 |
+
response = _httpx_client.stream(
|
| 137 |
+
method,
|
| 138 |
+
endpoint,
|
| 139 |
+
headers=merged_headers,
|
| 140 |
+
content=content,
|
| 141 |
+
data=data,
|
| 142 |
+
files=files,
|
| 143 |
+
json=json,
|
| 144 |
+
params=params,
|
| 145 |
+
**kwargs
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
with response as event_source:
|
| 149 |
+
if not _check_stream_content_type(event_source):
|
| 150 |
+
event_source.read()
|
| 151 |
+
errors.raise_for_status(event_source)
|
| 152 |
+
for line in event_source.iter_lines():
|
| 153 |
+
if not line:
|
| 154 |
+
continue
|
| 155 |
+
if line.startswith(b'data: '):
|
| 156 |
+
data = line[6:].decode('utf-8')
|
| 157 |
+
try:
|
| 158 |
+
json_data = json.loads(data)
|
| 159 |
+
event = {
|
| 160 |
+
'event': json_data.get('event'),
|
| 161 |
+
'data': data,
|
| 162 |
+
'id': None,
|
| 163 |
+
'retry': None
|
| 164 |
+
}
|
| 165 |
+
if event['event'] in IGNORED_STREAM_EVENTS:
|
| 166 |
+
continue
|
| 167 |
+
yield event
|
| 168 |
+
except json.JSONDecodeError:
|
| 169 |
+
continue
|
| 170 |
+
|
| 171 |
+
def feedback_messages(self, message_id: str, req: models.FeedbackRequest, **kwargs) -> models.FeedbackResponse:
|
| 172 |
+
"""
|
| 173 |
+
Submits feedback for a specific message.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
message_id: The identifier of the message to submit feedback for.
|
| 177 |
+
req: A `FeedbackRequest` object containing the feedback details, such as the rating.
|
| 178 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
A `FeedbackResponse` object containing the result of the feedback submission.
|
| 182 |
+
"""
|
| 183 |
+
response = self.request(
|
| 184 |
+
self._prepare_url(ENDPOINT_FEEDBACKS, message_id=message_id),
|
| 185 |
+
HTTPMethod.POST,
|
| 186 |
+
json=req.model_dump(),
|
| 187 |
+
**kwargs,
|
| 188 |
+
)
|
| 189 |
+
return models.FeedbackResponse(**response.json())
|
| 190 |
+
|
| 191 |
+
def suggest_messages(self, message_id: str, req: models.ChatSuggestRequest, **kwargs) -> models.ChatSuggestResponse:
|
| 192 |
+
"""
|
| 193 |
+
Retrieves suggested messages based on a specific message.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
message_id: The identifier of the message to get suggestions for.
|
| 197 |
+
req: A `ChatSuggestRequest` object containing the request details.
|
| 198 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 199 |
+
|
| 200 |
+
Returns:
|
| 201 |
+
A `ChatSuggestResponse` object containing suggested messages.
|
| 202 |
+
"""
|
| 203 |
+
response = self.request(
|
| 204 |
+
self._prepare_url(ENDPOINT_SUGGESTED, message_id=message_id),
|
| 205 |
+
HTTPMethod.GET,
|
| 206 |
+
params=req.model_dump(),
|
| 207 |
+
**kwargs,
|
| 208 |
+
)
|
| 209 |
+
return models.ChatSuggestResponse(**response.json())
|
| 210 |
+
|
| 211 |
+
def upload_files(self, file: types.FileTypes, req: models.UploadFileRequest,
|
| 212 |
+
**kwargs) -> models.UploadFileResponse:
|
| 213 |
+
"""
|
| 214 |
+
Uploads a file to be used in subsequent requests.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
file: The file to upload. This can be a file-like object, or a tuple of
|
| 218 |
+
(`filename`, file-like object, mime_type).
|
| 219 |
+
req: An `UploadFileRequest` object containing the upload details, such as the user who is uploading.
|
| 220 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 221 |
+
|
| 222 |
+
Returns:
|
| 223 |
+
An `UploadFileResponse` object containing details about the uploaded file, such as its identifier and URL.
|
| 224 |
+
"""
|
| 225 |
+
response = self.request(
|
| 226 |
+
self._prepare_url(ENDPOINT_FILES_UPLOAD),
|
| 227 |
+
HTTPMethod.POST,
|
| 228 |
+
data=req.model_dump(),
|
| 229 |
+
files=[("file", file)],
|
| 230 |
+
**kwargs,
|
| 231 |
+
)
|
| 232 |
+
return models.UploadFileResponse(**response.json())
|
| 233 |
+
|
| 234 |
+
def completion_messages(self, req: models.CompletionRequest, **kwargs) \
|
| 235 |
+
-> Union[models.CompletionResponse, Iterator[models.CompletionStreamResponse]]:
|
| 236 |
+
"""
|
| 237 |
+
Sends a request to generate a completion or a series of completions based on the provided input.
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
If the response mode is blocking, it returns a `CompletionResponse` object containing the generated message.
|
| 241 |
+
If the response mode is streaming, it returns an iterator of `CompletionStreamResponse` objects containing
|
| 242 |
+
the stream of generated events.
|
| 243 |
+
"""
|
| 244 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 245 |
+
return self._completion_messages(req, **kwargs)
|
| 246 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 247 |
+
return self._completion_messages_stream(req, **kwargs)
|
| 248 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 249 |
+
|
| 250 |
+
def _completion_messages(self, req: models.CompletionRequest, **kwargs) -> models.CompletionResponse:
|
| 251 |
+
response = self.request(
|
| 252 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 253 |
+
HTTPMethod.POST,
|
| 254 |
+
json=req.model_dump(),
|
| 255 |
+
**kwargs,
|
| 256 |
+
)
|
| 257 |
+
return models.CompletionResponse(**response.json())
|
| 258 |
+
|
| 259 |
+
def _completion_messages_stream(self, req: models.CompletionRequest, **kwargs) \
|
| 260 |
+
-> Iterator[models.CompletionStreamResponse]:
|
| 261 |
+
event_source = self.request_stream(
|
| 262 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 263 |
+
HTTPMethod.POST,
|
| 264 |
+
json=req.model_dump(),
|
| 265 |
+
**kwargs,
|
| 266 |
+
)
|
| 267 |
+
for sse in event_source:
|
| 268 |
+
yield models.build_completion_stream_response(sse.json())
|
| 269 |
+
|
| 270 |
+
def stop_completion_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 271 |
+
"""
|
| 272 |
+
Sends a request to stop a streaming completion task.
|
| 273 |
+
|
| 274 |
+
Returns:
|
| 275 |
+
A `StopResponse` object indicating the success of the operation.
|
| 276 |
+
"""
|
| 277 |
+
return self._stop_stream(self._prepare_url(ENDPOINT_STOP_COMPLETION_MESSAGES, task_id=task_id), req, **kwargs)
|
| 278 |
+
|
| 279 |
+
def chat_messages(self, req: models.ChatRequest, **kwargs) \
|
| 280 |
+
-> Union[models.ChatResponse, Iterator[models.ChatStreamResponse]]:
|
| 281 |
+
"""
|
| 282 |
+
Sends a request to generate a chat message or a series of chat messages based on the provided input.
|
| 283 |
+
|
| 284 |
+
Returns:
|
| 285 |
+
If the response mode is blocking, it returns a `ChatResponse` object containing the generated chat message.
|
| 286 |
+
If the response mode is streaming, it returns an iterator of `ChatStreamResponse` objects containing the
|
| 287 |
+
stream of chat events.
|
| 288 |
+
"""
|
| 289 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 290 |
+
return self._chat_messages(req, **kwargs)
|
| 291 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 292 |
+
return self._chat_messages_stream(req, **kwargs)
|
| 293 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 294 |
+
|
| 295 |
+
def _chat_messages(self, req: models.ChatRequest, **kwargs) -> models.ChatResponse:
|
| 296 |
+
response = self.request(
|
| 297 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 298 |
+
HTTPMethod.POST,
|
| 299 |
+
json=req.model_dump(),
|
| 300 |
+
**kwargs,
|
| 301 |
+
)
|
| 302 |
+
return models.ChatResponse(**response.json())
|
| 303 |
+
|
| 304 |
+
def _chat_messages_stream(self, req: models.ChatRequest, **kwargs) -> Iterator[models.ChatStreamResponse]:
|
| 305 |
+
event_source = self.request_stream(
|
| 306 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 307 |
+
HTTPMethod.POST,
|
| 308 |
+
json=req.model_dump(),
|
| 309 |
+
**kwargs,
|
| 310 |
+
)
|
| 311 |
+
for sse in event_source:
|
| 312 |
+
yield models.build_chat_stream_response(sse.json())
|
| 313 |
+
|
| 314 |
+
def stop_chat_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 315 |
+
"""
|
| 316 |
+
Sends a request to stop a streaming chat task.
|
| 317 |
+
|
| 318 |
+
Returns:
|
| 319 |
+
A `StopResponse` object indicating the success of the operation.
|
| 320 |
+
"""
|
| 321 |
+
return self._stop_stream(self._prepare_url(ENDPOINT_STOP_CHAT_MESSAGES, task_id=task_id), req, **kwargs)
|
| 322 |
+
|
| 323 |
+
def run_workflows(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 324 |
+
-> Union[models.WorkflowsRunResponse, Iterator[models.WorkflowsRunStreamResponse]]:
|
| 325 |
+
"""
|
| 326 |
+
Initiates the execution of a workflow, which can consist of multiple steps and actions.
|
| 327 |
+
|
| 328 |
+
Returns:
|
| 329 |
+
If the response mode is blocking, it returns a `WorkflowsRunResponse` object containing the results of the
|
| 330 |
+
completed workflow.
|
| 331 |
+
If the response mode is streaming, it returns an iterator of `WorkflowsRunStreamResponse` objects
|
| 332 |
+
containing the stream of workflow events.
|
| 333 |
+
"""
|
| 334 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 335 |
+
return self._run_workflows(req, **kwargs)
|
| 336 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 337 |
+
return self._run_workflows_stream(req, **kwargs)
|
| 338 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 339 |
+
|
| 340 |
+
def _run_workflows(self, req: models.WorkflowsRunRequest, **kwargs) -> models.WorkflowsRunResponse:
|
| 341 |
+
response = self.request(
|
| 342 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 343 |
+
HTTPMethod.POST,
|
| 344 |
+
json=req.model_dump(),
|
| 345 |
+
**kwargs,
|
| 346 |
+
)
|
| 347 |
+
return models.WorkflowsRunResponse(**response.json())
|
| 348 |
+
|
| 349 |
+
def _run_workflows_stream(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 350 |
+
-> Iterator[models.WorkflowsRunStreamResponse]:
|
| 351 |
+
event_source = self.request_stream(
|
| 352 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 353 |
+
HTTPMethod.POST,
|
| 354 |
+
json=req.model_dump(),
|
| 355 |
+
**kwargs,
|
| 356 |
+
)
|
| 357 |
+
for sse in event_source:
|
| 358 |
+
yield models.build_workflows_stream_response(sse.json())
|
| 359 |
+
|
| 360 |
+
def stop_workflows(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 361 |
+
"""
|
| 362 |
+
Sends a request to stop a streaming workflow task.
|
| 363 |
+
|
| 364 |
+
Returns:
|
| 365 |
+
A `StopResponse` object indicating the success of the operation.
|
| 366 |
+
"""
|
| 367 |
+
return self._stop_stream(self._prepare_url(ENDPOINT_STOP_WORKFLOWS, task_id=task_id), req, **kwargs)
|
| 368 |
+
|
| 369 |
+
def _stop_stream(self, endpoint: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 370 |
+
response = self.request(
|
| 371 |
+
endpoint,
|
| 372 |
+
HTTPMethod.POST,
|
| 373 |
+
json=req.model_dump(),
|
| 374 |
+
**kwargs,
|
| 375 |
+
)
|
| 376 |
+
return models.StopResponse(**response.json())
|
| 377 |
+
|
| 378 |
+
def _prepare_url(self, endpoint: str, **kwargs) -> str:
|
| 379 |
+
return self.api_base + endpoint.format(**kwargs)
|
| 380 |
+
|
| 381 |
+
def _prepare_auth_headers(self, headers: Dict[str, str]):
|
| 382 |
+
if "authorization" not in (key.lower() for key in headers.keys()):
|
| 383 |
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
class AsyncClient(BaseModel):
|
| 387 |
+
api_key: str
|
| 388 |
+
api_base: Optional[str] = "https://api.dify.ai/v1"
|
| 389 |
+
|
| 390 |
+
async def arequest(self, endpoint: str, method: str,
|
| 391 |
+
content: Optional[types.RequestContent] = None,
|
| 392 |
+
data: Optional[types.RequestData] = None,
|
| 393 |
+
files: Optional[types.RequestFiles] = None,
|
| 394 |
+
json: Optional[Any] = None,
|
| 395 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 396 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 397 |
+
**kwargs,
|
| 398 |
+
) -> httpx.Response:
|
| 399 |
+
"""
|
| 400 |
+
Asynchronously sends a request to the specified Dify API endpoint.
|
| 401 |
+
|
| 402 |
+
Args:
|
| 403 |
+
endpoint: The endpoint URL to which the request is sent.
|
| 404 |
+
method: The HTTP method to be used for the request (e.g., 'GET', 'POST').
|
| 405 |
+
content: Raw content to include in the request body, if any.
|
| 406 |
+
data: Form data to be sent in the request body.
|
| 407 |
+
files: Files to be uploaded with the request.
|
| 408 |
+
json: JSON data to be sent in the request body.
|
| 409 |
+
params: Query parameters to be included in the request URL.
|
| 410 |
+
headers: Additional headers to be sent with the request.
|
| 411 |
+
**kwargs: Extra keyword arguments to be passed to the underlying HTTPX request function.
|
| 412 |
+
|
| 413 |
+
Returns:
|
| 414 |
+
A httpx.Response object containing the server's response to the HTTP request.
|
| 415 |
+
|
| 416 |
+
Raises:
|
| 417 |
+
Various DifyAPIError exceptions if the response contains an error.
|
| 418 |
+
"""
|
| 419 |
+
merged_headers = {}
|
| 420 |
+
if headers:
|
| 421 |
+
merged_headers.update(headers)
|
| 422 |
+
self._prepare_auth_headers(merged_headers)
|
| 423 |
+
|
| 424 |
+
response = await _async_httpx_client.request(method, endpoint, content=content, data=data, files=files,
|
| 425 |
+
json=json, params=params, headers=merged_headers, **kwargs)
|
| 426 |
+
errors.raise_for_status(response)
|
| 427 |
+
return response
|
| 428 |
+
|
| 429 |
+
async def arequest_stream(self, endpoint: str, method: str,
|
| 430 |
+
content: Optional[types.RequestContent] = None,
|
| 431 |
+
data: Optional[types.RequestData] = None,
|
| 432 |
+
files: Optional[types.RequestFiles] = None,
|
| 433 |
+
json: Optional[Any] = None,
|
| 434 |
+
params: Optional[types.QueryParamTypes] = None,
|
| 435 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 436 |
+
**kwargs,
|
| 437 |
+
) -> AsyncIterator[ServerSentEvent]:
|
| 438 |
+
"""
|
| 439 |
+
Asynchronously establishes a streaming connection to the specified Dify API endpoint.
|
| 440 |
+
|
| 441 |
+
Args:
|
| 442 |
+
endpoint: The endpoint URL to which the request is sent.
|
| 443 |
+
method: The HTTP method to be used for the request (e.g., 'GET', 'POST').
|
| 444 |
+
content: Raw content to include in the request body, if any.
|
| 445 |
+
data: Form data to be sent in the request body.
|
| 446 |
+
files: Files to be uploaded with the request.
|
| 447 |
+
json: JSON data to be sent in the request body.
|
| 448 |
+
params: Query parameters to be included in the request URL.
|
| 449 |
+
headers: Additional headers to be sent with the request.
|
| 450 |
+
**kwargs: Extra keyword arguments to be passed to the underlying HTTPX request function.
|
| 451 |
+
|
| 452 |
+
Yields:
|
| 453 |
+
ServerSentEvent objects representing the events received from the server.
|
| 454 |
+
|
| 455 |
+
Raises:
|
| 456 |
+
Various DifyAPIError exceptions if an error event is received in the stream.
|
| 457 |
+
"""
|
| 458 |
+
merged_headers = {}
|
| 459 |
+
if headers:
|
| 460 |
+
merged_headers.update(headers)
|
| 461 |
+
self._prepare_auth_headers(merged_headers)
|
| 462 |
+
|
| 463 |
+
async with aconnect_sse(_async_httpx_client, method, endpoint, headers=merged_headers,
|
| 464 |
+
content=content, data=data, files=files, json=json, params=params,
|
| 465 |
+
**kwargs) as event_source:
|
| 466 |
+
if not _check_stream_content_type(event_source.response):
|
| 467 |
+
await event_source.response.aread()
|
| 468 |
+
errors.raise_for_status(event_source.response)
|
| 469 |
+
async for sse in event_source.aiter_sse():
|
| 470 |
+
errors.raise_for_status(sse)
|
| 471 |
+
if sse.event in IGNORED_STREAM_EVENTS or sse.data in IGNORED_STREAM_EVENTS:
|
| 472 |
+
continue
|
| 473 |
+
yield sse
|
| 474 |
+
|
| 475 |
+
async def afeedback_messages(self, message_id: str, req: models.FeedbackRequest, **kwargs) \
|
| 476 |
+
-> models.FeedbackResponse:
|
| 477 |
+
"""
|
| 478 |
+
Submits feedback for a specific message.
|
| 479 |
+
|
| 480 |
+
Args:
|
| 481 |
+
message_id: The identifier of the message to submit feedback for.
|
| 482 |
+
req: A `FeedbackRequest` object containing the feedback details, such as the rating.
|
| 483 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 484 |
+
|
| 485 |
+
Returns:
|
| 486 |
+
A `FeedbackResponse` object containing the result of the feedback submission.
|
| 487 |
+
"""
|
| 488 |
+
response = await self.arequest(
|
| 489 |
+
self._prepare_url(ENDPOINT_FEEDBACKS, message_id=message_id),
|
| 490 |
+
HTTPMethod.POST,
|
| 491 |
+
json=req.model_dump(),
|
| 492 |
+
**kwargs,
|
| 493 |
+
)
|
| 494 |
+
return models.FeedbackResponse(**response.json())
|
| 495 |
+
|
| 496 |
+
async def asuggest_messages(self, message_id: str, req: models.ChatSuggestRequest, **kwargs) \
|
| 497 |
+
-> models.ChatSuggestResponse:
|
| 498 |
+
"""
|
| 499 |
+
Retrieves suggested messages based on a specific message.
|
| 500 |
+
|
| 501 |
+
Args:
|
| 502 |
+
message_id: The identifier of the message to get suggestions for.
|
| 503 |
+
req: A `ChatSuggestRequest` object containing the request details.
|
| 504 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 505 |
+
|
| 506 |
+
Returns:
|
| 507 |
+
A `ChatSuggestResponse` object containing suggested messages.
|
| 508 |
+
"""
|
| 509 |
+
response = await self.arequest(
|
| 510 |
+
self._prepare_url(ENDPOINT_SUGGESTED, message_id=message_id),
|
| 511 |
+
HTTPMethod.GET,
|
| 512 |
+
params=req.model_dump(),
|
| 513 |
+
**kwargs,
|
| 514 |
+
)
|
| 515 |
+
return models.ChatSuggestResponse(**response.json())
|
| 516 |
+
|
| 517 |
+
async def aupload_files(self, file: types.FileTypes, req: models.UploadFileRequest, **kwargs) \
|
| 518 |
+
-> models.UploadFileResponse:
|
| 519 |
+
"""
|
| 520 |
+
Uploads a file to be used in subsequent requests.
|
| 521 |
+
|
| 522 |
+
Args:
|
| 523 |
+
file: The file to upload. This can be a file-like object, or a tuple of
|
| 524 |
+
(`filename`, file-like object, mime_type).
|
| 525 |
+
req: An `UploadFileRequest` object containing the upload details, such as the user who is uploading.
|
| 526 |
+
**kwargs: Extra keyword arguments to pass to the request function.
|
| 527 |
+
|
| 528 |
+
Returns:
|
| 529 |
+
An `UploadFileResponse` object containing details about the uploaded file, such as its identifier and URL.
|
| 530 |
+
"""
|
| 531 |
+
response = await self.arequest(
|
| 532 |
+
self._prepare_url(ENDPOINT_FILES_UPLOAD),
|
| 533 |
+
HTTPMethod.POST,
|
| 534 |
+
data=req.model_dump(),
|
| 535 |
+
files=[("file", file)],
|
| 536 |
+
**kwargs,
|
| 537 |
+
)
|
| 538 |
+
return models.UploadFileResponse(**response.json())
|
| 539 |
+
|
| 540 |
+
async def acompletion_messages(self, req: models.CompletionRequest, **kwargs) \
|
| 541 |
+
-> Union[models.CompletionResponse, AsyncIterator[models.CompletionStreamResponse]]:
|
| 542 |
+
"""
|
| 543 |
+
Sends a request to generate a completion or a series of completions based on the provided input.
|
| 544 |
+
|
| 545 |
+
Returns:
|
| 546 |
+
If the response mode is blocking, it returns a `CompletionResponse` object containing the generated message.
|
| 547 |
+
If the response mode is streaming, it returns an iterator of `CompletionStreamResponse` objects containing
|
| 548 |
+
the stream of generated events.
|
| 549 |
+
"""
|
| 550 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 551 |
+
return await self._acompletion_messages(req, **kwargs)
|
| 552 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 553 |
+
return self._acompletion_messages_stream(req, **kwargs)
|
| 554 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 555 |
+
|
| 556 |
+
async def _acompletion_messages(self, req: models.CompletionRequest, **kwargs) -> models.CompletionResponse:
|
| 557 |
+
response = await self.arequest(
|
| 558 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 559 |
+
HTTPMethod.POST,
|
| 560 |
+
json=req.model_dump(),
|
| 561 |
+
**kwargs,
|
| 562 |
+
)
|
| 563 |
+
return models.CompletionResponse(**response.json())
|
| 564 |
+
|
| 565 |
+
async def _acompletion_messages_stream(self, req: models.CompletionRequest, **kwargs) \
|
| 566 |
+
-> AsyncIterator[models.CompletionStreamResponse]:
|
| 567 |
+
async for sse in self.arequest_stream(
|
| 568 |
+
self._prepare_url(ENDPOINT_COMPLETION_MESSAGES),
|
| 569 |
+
HTTPMethod.POST,
|
| 570 |
+
json=req.model_dump(),
|
| 571 |
+
**kwargs):
|
| 572 |
+
yield models.build_completion_stream_response(sse.json())
|
| 573 |
+
|
| 574 |
+
async def astop_completion_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 575 |
+
"""
|
| 576 |
+
Sends a request to stop a streaming completion task.
|
| 577 |
+
|
| 578 |
+
Returns:
|
| 579 |
+
A `StopResponse` object indicating the success of the operation.
|
| 580 |
+
"""
|
| 581 |
+
return await self._astop_stream(
|
| 582 |
+
self._prepare_url(ENDPOINT_STOP_COMPLETION_MESSAGES, task_id=task_id), req, **kwargs)
|
| 583 |
+
|
| 584 |
+
async def achat_messages(self, req: models.ChatRequest, **kwargs) \
|
| 585 |
+
-> Union[models.ChatResponse, AsyncIterator[models.ChatStreamResponse]]:
|
| 586 |
+
"""
|
| 587 |
+
Sends a request to generate a chat message or a series of chat messages based on the provided input.
|
| 588 |
+
|
| 589 |
+
Returns:
|
| 590 |
+
If the response mode is blocking, it returns a `ChatResponse` object containing the generated chat message.
|
| 591 |
+
If the response mode is streaming, it returns an iterator of `ChatStreamResponse` objects containing the
|
| 592 |
+
stream of chat events.
|
| 593 |
+
"""
|
| 594 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 595 |
+
return await self._achat_messages(req, **kwargs)
|
| 596 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 597 |
+
return self._achat_messages_stream(req, **kwargs)
|
| 598 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 599 |
+
|
| 600 |
+
async def _achat_messages(self, req: models.ChatRequest, **kwargs) -> models.ChatResponse:
|
| 601 |
+
response = await self.arequest(
|
| 602 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 603 |
+
HTTPMethod.POST,
|
| 604 |
+
json=req.model_dump(),
|
| 605 |
+
**kwargs,
|
| 606 |
+
)
|
| 607 |
+
return models.ChatResponse(**response.json())
|
| 608 |
+
|
| 609 |
+
async def _achat_messages_stream(self, req: models.ChatRequest, **kwargs) \
|
| 610 |
+
-> AsyncIterator[models.ChatStreamResponse]:
|
| 611 |
+
async for sse in self.arequest_stream(
|
| 612 |
+
self._prepare_url(ENDPOINT_CHAT_MESSAGES),
|
| 613 |
+
HTTPMethod.POST,
|
| 614 |
+
json=req.model_dump(),
|
| 615 |
+
**kwargs):
|
| 616 |
+
yield models.build_chat_stream_response(sse.json())
|
| 617 |
+
|
| 618 |
+
async def astop_chat_messages(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 619 |
+
"""
|
| 620 |
+
Sends a request to stop a streaming chat task.
|
| 621 |
+
|
| 622 |
+
Returns:
|
| 623 |
+
A `StopResponse` object indicating the success of the operation.
|
| 624 |
+
"""
|
| 625 |
+
return await self._astop_stream(self._prepare_url(ENDPOINT_STOP_CHAT_MESSAGES, task_id=task_id), req, **kwargs)
|
| 626 |
+
|
| 627 |
+
async def arun_workflows(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 628 |
+
-> Union[models.WorkflowsRunResponse, AsyncIterator[models.WorkflowsStreamResponse]]:
|
| 629 |
+
"""
|
| 630 |
+
Initiates the execution of a workflow, which can consist of multiple steps and actions.
|
| 631 |
+
|
| 632 |
+
Returns:
|
| 633 |
+
If the response mode is blocking, it returns a `WorkflowsRunResponse` object containing the results of the
|
| 634 |
+
completed workflow.
|
| 635 |
+
If the response mode is streaming, it returns an iterator of `WorkflowsRunStreamResponse` objects
|
| 636 |
+
containing the stream of workflow events.
|
| 637 |
+
"""
|
| 638 |
+
if req.response_mode == models.ResponseMode.BLOCKING:
|
| 639 |
+
return await self._arun_workflows(req, **kwargs)
|
| 640 |
+
if req.response_mode == models.ResponseMode.STREAMING:
|
| 641 |
+
return self._arun_workflows_stream(req, **kwargs)
|
| 642 |
+
raise ValueError(f"Invalid request_mode: {req.response_mode}")
|
| 643 |
+
|
| 644 |
+
async def _arun_workflows(self, req: models.WorkflowsRunRequest, **kwargs) -> models.WorkflowsRunResponse:
|
| 645 |
+
response = await self.arequest(
|
| 646 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 647 |
+
HTTPMethod.POST,
|
| 648 |
+
json=req.model_dump(),
|
| 649 |
+
**kwargs,
|
| 650 |
+
)
|
| 651 |
+
return models.WorkflowsRunResponse(**response.json())
|
| 652 |
+
|
| 653 |
+
async def _arun_workflows_stream(self, req: models.WorkflowsRunRequest, **kwargs) \
|
| 654 |
+
-> AsyncIterator[models.WorkflowsRunStreamResponse]:
|
| 655 |
+
async for sse in self.arequest_stream(
|
| 656 |
+
self._prepare_url(ENDPOINT_RUN_WORKFLOWS),
|
| 657 |
+
HTTPMethod.POST,
|
| 658 |
+
json=req.model_dump(),
|
| 659 |
+
**kwargs):
|
| 660 |
+
yield models.build_workflows_stream_response(sse.json())
|
| 661 |
+
|
| 662 |
+
async def astop_workflows(self, task_id: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 663 |
+
"""
|
| 664 |
+
Sends a request to stop a streaming workflow task.
|
| 665 |
+
|
| 666 |
+
Returns:
|
| 667 |
+
A `StopResponse` object indicating the success of the operation.
|
| 668 |
+
"""
|
| 669 |
+
return await self._astop_stream(self._prepare_url(ENDPOINT_STOP_WORKFLOWS, task_id=task_id), req, **kwargs)
|
| 670 |
+
|
| 671 |
+
async def _astop_stream(self, endpoint: str, req: models.StopRequest, **kwargs) -> models.StopResponse:
|
| 672 |
+
response = await self.arequest(
|
| 673 |
+
endpoint,
|
| 674 |
+
HTTPMethod.POST,
|
| 675 |
+
json=req.model_dump(),
|
| 676 |
+
**kwargs,
|
| 677 |
+
)
|
| 678 |
+
return models.StopResponse(**response.json())
|
| 679 |
+
|
| 680 |
+
def _prepare_url(self, endpoint: str, **kwargs) -> str:
|
| 681 |
+
return self.api_base + endpoint.format(**kwargs)
|
| 682 |
+
|
| 683 |
+
def _prepare_auth_headers(self, headers: Dict[str, str]):
|
| 684 |
+
if "authorization" not in (key.lower() for key in headers.keys()):
|
| 685 |
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def _get_content_type(headers: httpx.Headers) -> str:
|
| 689 |
+
return headers.get("content-type", "").partition(";")[0]
|
| 690 |
+
|
| 691 |
+
|
| 692 |
+
def _check_stream_content_type(response: httpx.Response) -> bool:
|
| 693 |
+
content_type = _get_content_type(response.headers)
|
| 694 |
+
return response.is_success and "text/event-stream" in content_type
|
dify_client_python/dify_client/errors.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from http import HTTPStatus
|
| 2 |
+
from typing import Union
|
| 3 |
+
|
| 4 |
+
import httpx
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
from dify_client_python.dify_client import models
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DifyAPIError(Exception):
|
| 11 |
+
def __init__(self, status: int, code: str, message: str):
|
| 12 |
+
super().__init__(f"status_code={status}, code={code}, {message}")
|
| 13 |
+
self.status = status
|
| 14 |
+
self.code = code
|
| 15 |
+
self.message = message
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DifyInvalidParam(DifyAPIError):
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DifyNotChatApp(DifyAPIError):
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class DifyResourceNotFound(DifyAPIError):
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class DifyAppUnavailable(DifyAPIError):
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class DifyProviderNotInitialize(DifyAPIError):
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class DifyProviderQuotaExceeded(DifyAPIError):
|
| 39 |
+
pass
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class DifyModelCurrentlyNotSupport(DifyAPIError):
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class DifyCompletionRequestError(DifyAPIError):
|
| 47 |
+
pass
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class DifyInternalServerError(DifyAPIError):
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class DifyNoFileUploaded(DifyAPIError):
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class DifyTooManyFiles(DifyAPIError):
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class DifyUnsupportedPreview(DifyAPIError):
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class DifyUnsupportedEstimate(DifyAPIError):
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class DifyFileTooLarge(DifyAPIError):
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class DifyUnsupportedFileType(DifyAPIError):
|
| 75 |
+
pass
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class DifyS3ConnectionFailed(DifyAPIError):
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class DifyS3PermissionDenied(DifyAPIError):
|
| 83 |
+
pass
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class DifyS3FileTooLarge(DifyAPIError):
|
| 87 |
+
pass
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
SPEC_CODE_ERRORS = {
|
| 91 |
+
# completion & chat & workflow
|
| 92 |
+
"invalid_param": DifyInvalidParam,
|
| 93 |
+
"not_chat_app": DifyNotChatApp,
|
| 94 |
+
"app_unavailable": DifyAppUnavailable,
|
| 95 |
+
"provider_not_initialize": DifyProviderNotInitialize,
|
| 96 |
+
"provider_quota_exceeded": DifyProviderQuotaExceeded,
|
| 97 |
+
"model_currently_not_support": DifyModelCurrentlyNotSupport,
|
| 98 |
+
"completion_request_error": DifyCompletionRequestError,
|
| 99 |
+
# files upload
|
| 100 |
+
"no_file_uploaded": DifyNoFileUploaded,
|
| 101 |
+
"too_many_files": DifyTooManyFiles,
|
| 102 |
+
"unsupported_preview": DifyUnsupportedPreview,
|
| 103 |
+
"unsupported_estimate": DifyUnsupportedEstimate,
|
| 104 |
+
"file_too_large": DifyFileTooLarge,
|
| 105 |
+
"unsupported_file_type": DifyUnsupportedFileType,
|
| 106 |
+
"s3_connection_failed": DifyS3ConnectionFailed,
|
| 107 |
+
"s3_permission_denied": DifyS3PermissionDenied,
|
| 108 |
+
"s3_file_too_large": DifyS3FileTooLarge,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def raise_for_status(response: Union[httpx.Response, BaseModel]):
|
| 113 |
+
if isinstance(response, httpx.Response):
|
| 114 |
+
if response.is_success:
|
| 115 |
+
return
|
| 116 |
+
json = response.json()
|
| 117 |
+
if "status" not in json:
|
| 118 |
+
json["status"] = response.status_code
|
| 119 |
+
details = models.ErrorResponse(**json)
|
| 120 |
+
elif isinstance(response, BaseModel):
|
| 121 |
+
if not hasattr(response, 'event') or response.event != models.StreamEvent.ERROR.value:
|
| 122 |
+
return
|
| 123 |
+
details = models.ErrorStreamResponse(**response.dict())
|
| 124 |
+
else:
|
| 125 |
+
raise ValueError(f"Invalid dify response type: {type(response)}")
|
| 126 |
+
|
| 127 |
+
if details.status == HTTPStatus.NOT_FOUND:
|
| 128 |
+
raise DifyResourceNotFound(details.status, details.code, details.message)
|
| 129 |
+
elif details.status == HTTPStatus.INTERNAL_SERVER_ERROR:
|
| 130 |
+
raise DifyInternalServerError(details.status, details.code, details.message)
|
| 131 |
+
else:
|
| 132 |
+
raise SPEC_CODE_ERRORS.get(details.code, DifyAPIError)(
|
| 133 |
+
details.status, details.code, details.message
|
| 134 |
+
)
|
dify_client_python/dify_client/models/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .chat import *
|
| 2 |
+
from .completion import *
|
| 3 |
+
from .feedback import *
|
| 4 |
+
from .file import *
|
| 5 |
+
from .workflow import *
|
| 6 |
+
from .stream import *
|
| 7 |
+
from .base import StopRequest, StopResponse
|
dify_client_python/dify_client/models/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (357 Bytes). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (372 Bytes). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/base.cpython-310.pyc
ADDED
|
Binary file (3.17 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/base.cpython-312.pyc
ADDED
|
Binary file (3.77 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/chat.cpython-310.pyc
ADDED
|
Binary file (1.49 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/chat.cpython-312.pyc
ADDED
|
Binary file (1.67 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/completion.cpython-310.pyc
ADDED
|
Binary file (1.09 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/completion.cpython-312.pyc
ADDED
|
Binary file (1.21 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/feedback.cpython-310.pyc
ADDED
|
Binary file (955 Bytes). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/feedback.cpython-312.pyc
ADDED
|
Binary file (1.07 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/file.cpython-310.pyc
ADDED
|
Binary file (716 Bytes). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/file.cpython-312.pyc
ADDED
|
Binary file (784 Bytes). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/stream.cpython-310.pyc
ADDED
|
Binary file (5.4 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/stream.cpython-312.pyc
ADDED
|
Binary file (7.46 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/workflow.cpython-310.pyc
ADDED
|
Binary file (3.23 kB). View file
|
|
|
dify_client_python/dify_client/models/__pycache__/workflow.cpython-312.pyc
ADDED
|
Binary file (3.92 kB). View file
|
|
|
dify_client_python/dify_client/models/base.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from enum import StrEnum
|
| 3 |
+
except ImportError:
|
| 4 |
+
from strenum import StrEnum
|
| 5 |
+
from http import HTTPStatus
|
| 6 |
+
from typing import Optional, List
|
| 7 |
+
|
| 8 |
+
from pydantic import BaseModel, ConfigDict
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Mode(StrEnum):
|
| 12 |
+
CHAT = "chat"
|
| 13 |
+
COMPLETION = "completion"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ResponseMode(StrEnum):
|
| 17 |
+
STREAMING = 'streaming'
|
| 18 |
+
BLOCKING = 'blocking'
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class FileType(StrEnum):
|
| 22 |
+
IMAGE = "image"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class TransferMethod(StrEnum):
|
| 26 |
+
REMOTE_URL = "remote_url"
|
| 27 |
+
LOCAL_FILE = "local_file"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Allows the entry of various variable values defined by the App.
|
| 31 |
+
# The inputs parameter contains multiple key/value pairs, with each key corresponding to a specific variable and
|
| 32 |
+
# each value being the specific value for that variable.
|
| 33 |
+
# The text generation application requires at least one key/value pair to be inputted.
|
| 34 |
+
class CompletionInputs(BaseModel):
|
| 35 |
+
model_config = ConfigDict(extra='allow')
|
| 36 |
+
# Required The input text, the content to be processed.
|
| 37 |
+
query: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class File(BaseModel):
|
| 41 |
+
type: FileType
|
| 42 |
+
transfer_method: TransferMethod
|
| 43 |
+
url: Optional[str]
|
| 44 |
+
# Uploaded file ID, which must be obtained by uploading through the File Upload API in advance
|
| 45 |
+
# (when the transfer method is local_file)
|
| 46 |
+
upload_file_id: Optional[str]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Usage(BaseModel):
|
| 50 |
+
prompt_tokens: int
|
| 51 |
+
completion_tokens: int
|
| 52 |
+
total_tokens: int
|
| 53 |
+
|
| 54 |
+
prompt_unit_price: str
|
| 55 |
+
prompt_price_unit: str
|
| 56 |
+
prompt_price: str
|
| 57 |
+
completion_unit_price: str
|
| 58 |
+
completion_price_unit: str
|
| 59 |
+
completion_price: str
|
| 60 |
+
total_price: str
|
| 61 |
+
currency: str
|
| 62 |
+
|
| 63 |
+
latency: float
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class RetrieverResource(BaseModel):
|
| 67 |
+
position: int
|
| 68 |
+
dataset_id: str
|
| 69 |
+
dataset_name: str
|
| 70 |
+
document_id: str
|
| 71 |
+
document_name: str
|
| 72 |
+
segment_id: str
|
| 73 |
+
score: float
|
| 74 |
+
content: str
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class Metadata(BaseModel):
|
| 78 |
+
usage: Usage
|
| 79 |
+
retriever_resources: List[RetrieverResource] = []
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class StopRequest(BaseModel):
|
| 83 |
+
user: str
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class StopResponse(BaseModel):
|
| 87 |
+
result: str # success
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class ErrorResponse(BaseModel):
|
| 91 |
+
status: int = HTTPStatus.INTERNAL_SERVER_ERROR # HTTP status code
|
| 92 |
+
code: str = ""
|
| 93 |
+
message: str = ""
|
dify_client_python/dify_client/models/chat.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List, Optional, Any
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
from dify_client_python.dify_client.models.base import ResponseMode, File
|
| 6 |
+
from dify_client_python.dify_client.models.completion import CompletionResponse
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ChatRequest(BaseModel):
|
| 10 |
+
query: str
|
| 11 |
+
inputs: Dict[str, Any] = Field(default_factory=dict)
|
| 12 |
+
response_mode: ResponseMode
|
| 13 |
+
user: str
|
| 14 |
+
conversation_id: Optional[str] = ""
|
| 15 |
+
files: List[File] = []
|
| 16 |
+
auto_generate_name: bool = True
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class ChatResponse(CompletionResponse):
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ChatSuggestRequest(BaseModel):
|
| 24 |
+
user: str
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ChatSuggestResponse(BaseModel):
|
| 28 |
+
result: str
|
| 29 |
+
data: List[str] = []
|