unionllm 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cnlitellm/__init__.py +0 -0
- cnlitellm/api.py +40 -0
- cnlitellm/exceptions.py +11 -0
- cnlitellm/models.py +28 -0
- cnlitellm/providers/__init__.py +0 -0
- cnlitellm/providers/baichuan.py +128 -0
- cnlitellm/providers/base_provider.py +44 -0
- cnlitellm/providers/coze.py +230 -0
- cnlitellm/providers/dify.py +192 -0
- cnlitellm/providers/example_provider.py +19 -0
- cnlitellm/providers/fastgpt.py +171 -0
- cnlitellm/providers/minimax.py +122 -0
- cnlitellm/providers/moonshot.py +69 -0
- cnlitellm/providers/qwen.py +168 -0
- cnlitellm/providers/tiangong.py +145 -0
- cnlitellm/providers/wenxin.py +161 -0
- cnlitellm/providers/xunfei.py +245 -0
- cnlitellm/providers/zhipu.py +63 -0
- cnlitellm/utils.py +432 -0
- tests/__init__.py +0 -0
- tests/test_api.py +4 -0
- tests/test_baichuan.py +56 -0
- tests/test_coze.py +33 -0
- tests/test_fastgpt.py +44 -0
- tests/test_litellm.py +24 -0
- tests/test_minimax.py +51 -0
- tests/test_moonshot.py +78 -0
- tests/test_moonshot2.py +43 -0
- tests/test_qwen.py +53 -0
- tests/test_tiangong.py +54 -0
- tests/test_wenxin.py +54 -0
- tests/test_zhipu.py +47 -0
- tests/test_zhipu2.py +42 -0
- unionllm/__init__.py +1 -0
- unionllm/exceptions.py +11 -0
- unionllm/main.py +67 -0
- unionllm/models.py +28 -0
- unionllm/providers/__init__.py +0 -0
- unionllm/providers/baichuan.py +128 -0
- unionllm/providers/base_provider.py +51 -0
- unionllm/providers/coze.py +230 -0
- unionllm/providers/dify.py +192 -0
- unionllm/providers/fastgpt.py +171 -0
- unionllm/providers/litellm.py +63 -0
- unionllm/providers/minimax.py +122 -0
- unionllm/providers/moonshot.py +69 -0
- unionllm/providers/qwen.py +168 -0
- unionllm/providers/tiangong.py +145 -0
- unionllm/providers/wenxin.py +161 -0
- unionllm/providers/xunfei.py +245 -0
- unionllm/providers/zhipu.py +63 -0
- unionllm/utils.py +432 -0
- unionllm-0.1.0.dist-info/LICENSE +3 -0
- unionllm-0.1.0.dist-info/METADATA +17 -0
- unionllm-0.1.0.dist-info/RECORD +57 -0
- unionllm-0.1.0.dist-info/WHEEL +5 -0
- unionllm-0.1.0.dist-info/top_level.txt +2 -0
cnlitellm/__init__.py
ADDED
|
File without changes
|
cnlitellm/api.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, List
|
|
3
|
+
from .providers import base_provider
|
|
4
|
+
from .providers import zhipu, moonshot, minimax, qwen, tiangong, baichuan, wenxin, xunfei, dify, fastgpt, coze
|
|
5
|
+
from .exceptions import ProviderError
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class CNLiteLLM:
|
|
10
|
+
def __init__(self, provider: str, **kwargs):
|
|
11
|
+
self.provider = provider.lower()
|
|
12
|
+
if self.provider == "zhipuai":
|
|
13
|
+
self.provider_instance = zhipu.ZhipuAIProvider(**kwargs)
|
|
14
|
+
elif self.provider == "moonshot":
|
|
15
|
+
self.provider_instance = moonshot.MoonshotAIProvider(**kwargs)
|
|
16
|
+
elif self.provider == "minimax":
|
|
17
|
+
self.provider_instance = minimax.MinimaxAIProvider(**kwargs)
|
|
18
|
+
elif self.provider == "qwen":
|
|
19
|
+
self.provider_instance = qwen.QwenAIProvider(**kwargs)
|
|
20
|
+
elif self.provider == "tiangong":
|
|
21
|
+
self.provider_instance = tiangong.TianGongAIProvider(**kwargs)
|
|
22
|
+
elif self.provider == "baichuan":
|
|
23
|
+
self.provider_instance = baichuan.BaiChuanAIProvider(**kwargs)
|
|
24
|
+
elif self.provider == "wenxin":
|
|
25
|
+
self.provider_instance = wenxin.WenXinAIProvider(**kwargs)
|
|
26
|
+
elif self.provider == "xunfei":
|
|
27
|
+
self.provider_instance = xunfei.XunfeiAIProvider(**kwargs)
|
|
28
|
+
elif self.provider == "dify":
|
|
29
|
+
self.provider_instance = dify.DifyAIProvider(**kwargs)
|
|
30
|
+
elif self.provider == "fastgpt":
|
|
31
|
+
self.provider_instance = fastgpt.FastGPTProvider(**kwargs)
|
|
32
|
+
elif self.provider == "coze":
|
|
33
|
+
self.provider_instance = coze.CozeAIProvider(**kwargs)
|
|
34
|
+
else:
|
|
35
|
+
raise ProviderError(f"Provider '{self.provider}' is not supported.")
|
|
36
|
+
|
|
37
|
+
def completion(self, model: str, messages: List[str], **kwargs) -> Any:
|
|
38
|
+
if not self.provider_instance:
|
|
39
|
+
raise ProviderError(f"Provider '{self.provider}' is not initialized.")
|
|
40
|
+
return self.provider_instance.completion(model, messages, **kwargs)
|
cnlitellm/exceptions.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CNLiteLLMError(Exception):
|
|
2
|
+
"""Base exception class for CNLiteLLM."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class ProviderError(CNLiteLLMError):
|
|
6
|
+
"""Exception raised when there is an issue with the model provider."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class APICallError(CNLiteLLMError):
|
|
10
|
+
"""Exception raised when an API call fails."""
|
|
11
|
+
pass
|
cnlitellm/models.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
class ResponseModel:
|
|
4
|
+
def __init__(self, prompt_tokens: List[str], completion_tokens: List[str], total_tokens: int, total_attempts: int, raw_response: Optional[dict] = None, usage: Optional[dict] = None):
|
|
5
|
+
self.prompt_tokens = prompt_tokens
|
|
6
|
+
self.completion_tokens = completion_tokens
|
|
7
|
+
self.total_tokens = total_tokens
|
|
8
|
+
self.total_attempts = total_attempts
|
|
9
|
+
self.raw_response = raw_response
|
|
10
|
+
self.usage = usage
|
|
11
|
+
|
|
12
|
+
def to_dict(self) -> dict:
|
|
13
|
+
return {
|
|
14
|
+
"prompt_tokens": self.prompt_tokens,
|
|
15
|
+
"completion_tokens": self.completion_tokens,
|
|
16
|
+
"total_tokens": self.total_tokens,
|
|
17
|
+
"total_attempts": self.total_attempts,
|
|
18
|
+
"raw_response": self.raw_response,
|
|
19
|
+
"usage": self.usage
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def get_completions(self) -> list[str]:
|
|
23
|
+
completions = []
|
|
24
|
+
if self.raw_response and "choices" in self.raw_response:
|
|
25
|
+
for choice in self.raw_response["choices"]:
|
|
26
|
+
if "message" in choice and "content" in choice["message"]:
|
|
27
|
+
completions.append(choice["message"]["content"])
|
|
28
|
+
return completions
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from .base_provider import BaseProvider
|
|
3
|
+
from cnlitellm.utils import ModelResponse, Message, Choices, Usage, Delta, StreamingChoices
|
|
4
|
+
import requests
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
class BaiChuanOpenAIError(Exception):
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
status_code,
|
|
11
|
+
message,
|
|
12
|
+
):
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.message = message
|
|
15
|
+
super().__init__(self.message)
|
|
16
|
+
|
|
17
|
+
class BaiChuanAIProvider(BaseProvider):
|
|
18
|
+
def __init__(self, **model_kwargs):
|
|
19
|
+
self.api_key = model_kwargs.get("api_key")
|
|
20
|
+
self.endpoint_url = "https://api.baichuan-ai.com/v1/chat/completions"
|
|
21
|
+
|
|
22
|
+
def pre_processing(self, **kwargs):
|
|
23
|
+
supported_params = [
|
|
24
|
+
"model", "messages", "max_tokens", "temperature", "top_p", "n",
|
|
25
|
+
"logprobs", "stream", "stop", "presence_penalty", "frequency_penalty",
|
|
26
|
+
"best_of", "logit_bias"
|
|
27
|
+
]
|
|
28
|
+
for key in list(kwargs.keys()):
|
|
29
|
+
if key not in supported_params:
|
|
30
|
+
kwargs.pop(key)
|
|
31
|
+
return kwargs
|
|
32
|
+
|
|
33
|
+
def post_stream_processing_wrapper(self, model, messages, **new_kwargs):
|
|
34
|
+
payload = json.dumps({"model": model, "messages": messages, **new_kwargs})
|
|
35
|
+
headers = {
|
|
36
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
}
|
|
39
|
+
response = requests.post(self.endpoint_url, headers=headers, data=payload)
|
|
40
|
+
for line in response.iter_lines():
|
|
41
|
+
if line:
|
|
42
|
+
new_line = line.decode("utf-8").replace("data: ", "")
|
|
43
|
+
if new_line == "[DONE]":
|
|
44
|
+
break
|
|
45
|
+
data = json.loads(new_line)
|
|
46
|
+
chunk_choices = []
|
|
47
|
+
for choice in data["choices"]:
|
|
48
|
+
chunk_delta = Delta()
|
|
49
|
+
delta = choice.get("delta")
|
|
50
|
+
if delta:
|
|
51
|
+
if "role" in choice['delta']:
|
|
52
|
+
chunk_delta.role = choice['delta']["role"]
|
|
53
|
+
if "content" in choice['delta']:
|
|
54
|
+
chunk_delta.content = choice['delta']["content"]
|
|
55
|
+
chunk_choices.append(StreamingChoices(index=choice['index'], delta=chunk_delta))
|
|
56
|
+
|
|
57
|
+
if "usage" in data:
|
|
58
|
+
chunk_usage = Usage()
|
|
59
|
+
if "prompt_tokens" in data["usage"]:
|
|
60
|
+
chunk_usage.prompt_tokens = data["usage"]["prompt_tokens"]
|
|
61
|
+
if "completion_tokens" in data["usage"]:
|
|
62
|
+
chunk_usage.completion_tokens = data["usage"]["completion_tokens"]
|
|
63
|
+
if "total_tokens" in data["usage"]:
|
|
64
|
+
chunk_usage.total_tokens = data["usage"]["total_tokens"]
|
|
65
|
+
|
|
66
|
+
chunk_response = ModelResponse(
|
|
67
|
+
id=data["id"],
|
|
68
|
+
choices=chunk_choices,
|
|
69
|
+
created=data["created"],
|
|
70
|
+
model=model,
|
|
71
|
+
usage=chunk_usage if "usage" in data else None,
|
|
72
|
+
stream=True
|
|
73
|
+
)
|
|
74
|
+
yield chunk_response
|
|
75
|
+
|
|
76
|
+
def create_model_response_wrapper(self, result, model):
|
|
77
|
+
response_dict = result.json()
|
|
78
|
+
choices = []
|
|
79
|
+
for choice in response_dict["choices"]:
|
|
80
|
+
message = Message(
|
|
81
|
+
content=choice["message"]["content"], role=choice["message"]["role"]
|
|
82
|
+
)
|
|
83
|
+
choices.append(
|
|
84
|
+
Choices(
|
|
85
|
+
message=message,
|
|
86
|
+
index=choice["index"],
|
|
87
|
+
finish_reason=choice["finish_reason"],
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
usage = Usage(
|
|
91
|
+
prompt_tokens=response_dict["usage"]["prompt_tokens"],
|
|
92
|
+
completion_tokens=response_dict["usage"]["completion_tokens"],
|
|
93
|
+
total_tokens=response_dict["usage"]["total_tokens"],
|
|
94
|
+
)
|
|
95
|
+
response = ModelResponse(
|
|
96
|
+
id=response_dict["id"],
|
|
97
|
+
choices=choices,
|
|
98
|
+
created=response_dict["created"],
|
|
99
|
+
model=model,
|
|
100
|
+
usage=usage,
|
|
101
|
+
)
|
|
102
|
+
return response
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def completion(self, model: str, messages: list, **kwargs):
|
|
106
|
+
try:
|
|
107
|
+
if model is None or messages is None:
|
|
108
|
+
raise BaiChuanOpenAIError(
|
|
109
|
+
status_code=422, message=f"Missing model or messages"
|
|
110
|
+
)
|
|
111
|
+
new_kwargs = self.pre_processing(**kwargs)
|
|
112
|
+
stream = kwargs.get("stream", False)
|
|
113
|
+
|
|
114
|
+
if stream:
|
|
115
|
+
return self.post_stream_processing_wrapper(model, messages, **new_kwargs)
|
|
116
|
+
else:
|
|
117
|
+
payload = json.dumps({"model": model, "messages": messages, **new_kwargs})
|
|
118
|
+
headers = {
|
|
119
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
}
|
|
122
|
+
result = requests.post(self.endpoint_url, headers=headers, data=payload)
|
|
123
|
+
return self.create_model_response_wrapper(result, model=model)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
if hasattr(e, "status_code"):
|
|
126
|
+
raise BaiChuanOpenAIError(status_code=e.status_code, message=str(e))
|
|
127
|
+
else:
|
|
128
|
+
raise BaiChuanOpenAIError(status_code=500, message=str(e))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from ..models import ResponseModel
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, List
|
|
4
|
+
from cnlitellm.utils import Message, Choices, Usage, ModelResponse
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
else:
|
|
9
|
+
from pydantic.dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class BaseProvider(ABC):
|
|
13
|
+
cls: str
|
|
14
|
+
args: Optional[Dict[str, Any]] = None
|
|
15
|
+
key: Optional[str] = None
|
|
16
|
+
group: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def completion(self, model: str, messages: list) -> ResponseModel:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def create_model_response(self, openai_response: Any, model: str) -> ResponseModel:
|
|
23
|
+
print("openai_response: ", openai_response)
|
|
24
|
+
choices = []
|
|
25
|
+
|
|
26
|
+
for choice in openai_response.choices:
|
|
27
|
+
print("choice.message.content: ", choice.message.content)
|
|
28
|
+
message = Message(content=choice.message.content, role=choice.message.role)
|
|
29
|
+
print("message: ", message)
|
|
30
|
+
choices.append(Choices(message=message, index=choice.index, finish_reason=choice.finish_reason))
|
|
31
|
+
|
|
32
|
+
usage = Usage(
|
|
33
|
+
prompt_tokens=openai_response.usage.prompt_tokens,
|
|
34
|
+
completion_tokens=openai_response.usage.completion_tokens,
|
|
35
|
+
total_tokens=openai_response.usage.total_tokens
|
|
36
|
+
)
|
|
37
|
+
response = ModelResponse(
|
|
38
|
+
id=openai_response.id,
|
|
39
|
+
choices=choices,
|
|
40
|
+
created=openai_response.created,
|
|
41
|
+
model=model,
|
|
42
|
+
usage=usage
|
|
43
|
+
)
|
|
44
|
+
pass
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from .base_provider import BaseProvider
|
|
2
|
+
from cnlitellm.utils import ResponseModelInterface
|
|
3
|
+
from cnlitellm.utils import ModelResponse, Message, Choices, Usage, Context, generate_unique_uid, Delta, StreamingChoices
|
|
4
|
+
from openai import OpenAI
|
|
5
|
+
import logging, json, time, requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CozeAIError(Exception):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
status_code,
|
|
12
|
+
message,
|
|
13
|
+
):
|
|
14
|
+
self.status_code = status_code
|
|
15
|
+
self.message = message
|
|
16
|
+
super().__init__(self.message)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CozeAIProvider(BaseProvider):
|
|
20
|
+
def __init__(self, **model_kwargs):
|
|
21
|
+
self.api_key = model_kwargs.get("api_key")
|
|
22
|
+
self.base_url = "https://api.coze.com/open_api/v2"
|
|
23
|
+
self.endpoint_url = self.base_url + "/chat"
|
|
24
|
+
|
|
25
|
+
def pre_processing(self, **kwargs):
|
|
26
|
+
# 处理参数兼容性问题,不支持的参数全部舍弃
|
|
27
|
+
supported_params = [
|
|
28
|
+
"model", "messages", "max_tokens", "temperature", "top_p", "n",
|
|
29
|
+
"logprobs", "stream", "stop", "presence_penalty", "frequency_penalty",
|
|
30
|
+
"best_of", "logit_bias"
|
|
31
|
+
]
|
|
32
|
+
for key in list(kwargs.keys()):
|
|
33
|
+
if key not in supported_params:
|
|
34
|
+
kwargs.pop(key)
|
|
35
|
+
return kwargs
|
|
36
|
+
|
|
37
|
+
def to_formatted_prompt(self, messages):
|
|
38
|
+
message = messages[-1]
|
|
39
|
+
# if last message role is not user, return error
|
|
40
|
+
if message["role"] != "user":
|
|
41
|
+
raise CozeAIError(
|
|
42
|
+
status_code=422, message=f"Last message role should be user"
|
|
43
|
+
)
|
|
44
|
+
query = message["content"]
|
|
45
|
+
return messages, query
|
|
46
|
+
|
|
47
|
+
def post_stream_processing_wrapper(self, model, messages, **new_kwargs):
|
|
48
|
+
# 预处理消息内容并提取用户问题
|
|
49
|
+
messages, query = self.to_formatted_prompt(messages)
|
|
50
|
+
|
|
51
|
+
# 接收user_id作为用户唯一标识传入参数
|
|
52
|
+
if 'user_id' not in new_kwargs:
|
|
53
|
+
user_id = generate_unique_uid()
|
|
54
|
+
else:
|
|
55
|
+
user_id = new_kwargs['user_id']
|
|
56
|
+
payload = json.dumps({"query": query,"user": user_id,"bot_id": model,"chat_history":[], "stream": True})
|
|
57
|
+
headers = {
|
|
58
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
}
|
|
61
|
+
response = requests.post(self.endpoint_url, headers=headers, data=payload)
|
|
62
|
+
|
|
63
|
+
for line in response.iter_lines():
|
|
64
|
+
if line:
|
|
65
|
+
if line.startswith(b"data:"):
|
|
66
|
+
new_line = line.decode("utf-8").replace("data:", "")
|
|
67
|
+
if new_line == "[DONE]":
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
data = json.loads(new_line)
|
|
71
|
+
print("data is:",data)
|
|
72
|
+
chunk_line = {}
|
|
73
|
+
chunk_context = []
|
|
74
|
+
chunk_choices = []
|
|
75
|
+
index = 0
|
|
76
|
+
if "message" in data:
|
|
77
|
+
message = data["message"]
|
|
78
|
+
if message['role'] == 'assistant' and message['type'] == 'knowledge':
|
|
79
|
+
# 解析知识型内容
|
|
80
|
+
content = message['content']
|
|
81
|
+
conversation_id = data.get('conversation_id', 0)
|
|
82
|
+
# 假设knowledge类型内容是由"---\nrecall slice X:\n"分隔的
|
|
83
|
+
slices = content.split('---\n')
|
|
84
|
+
index = 0
|
|
85
|
+
for slice in slices:
|
|
86
|
+
if slice.strip().startswith('recall slice'):
|
|
87
|
+
# 去掉前缀找到JSON部分
|
|
88
|
+
json_part = slice.strip().split('\n', 1)[-1].strip()
|
|
89
|
+
# 如果不是以\"}结尾,则强制添加
|
|
90
|
+
if not json_part.endswith('\"}'):
|
|
91
|
+
json_part += '\"}'
|
|
92
|
+
try:
|
|
93
|
+
# 尝试解析JSON数据
|
|
94
|
+
recall_data = json.loads(json_part)
|
|
95
|
+
chunk_context.append({
|
|
96
|
+
"id": index,
|
|
97
|
+
"content": str(recall_data)
|
|
98
|
+
})
|
|
99
|
+
index += 1
|
|
100
|
+
except json.JSONDecodeError:
|
|
101
|
+
raise CozeAIError(status_code=500, message=f"Error decoding JSON from slice: {json_part}")
|
|
102
|
+
|
|
103
|
+
elif message['role'] == 'assistant' and message['type'] == 'answer':
|
|
104
|
+
chunk_choices = []
|
|
105
|
+
chunk_delta = Delta()
|
|
106
|
+
if "role" in message:
|
|
107
|
+
chunk_delta.role = message["role"]
|
|
108
|
+
if "content" in message:
|
|
109
|
+
chunk_delta.content = message["content"]
|
|
110
|
+
chunk_choices.append(StreamingChoices(index=str(index), delta=chunk_delta))
|
|
111
|
+
|
|
112
|
+
chunk_usage = Usage()
|
|
113
|
+
chunk_response = ModelResponse(
|
|
114
|
+
id=conversation_id,
|
|
115
|
+
choices=chunk_choices,
|
|
116
|
+
context=chunk_context,
|
|
117
|
+
created=int(time.time()),
|
|
118
|
+
model=model,
|
|
119
|
+
usage=chunk_usage if chunk_usage else None,
|
|
120
|
+
stream=True
|
|
121
|
+
)
|
|
122
|
+
index += 1
|
|
123
|
+
yield chunk_response
|
|
124
|
+
|
|
125
|
+
def create_model_response_wrapper(self, result, model):
|
|
126
|
+
choices = []
|
|
127
|
+
context = []
|
|
128
|
+
|
|
129
|
+
result_dict = result.json()
|
|
130
|
+
messages = result_dict.get('messages', [])
|
|
131
|
+
results = []
|
|
132
|
+
cotext_pre = []
|
|
133
|
+
|
|
134
|
+
print("messages is:",messages)
|
|
135
|
+
|
|
136
|
+
for message in messages:
|
|
137
|
+
# 根据消息类型处理数据
|
|
138
|
+
if message['role'] == 'assistant' and message['type'] == 'knowledge':
|
|
139
|
+
# 解析知识型内容
|
|
140
|
+
content = message['content']
|
|
141
|
+
print("content is:",content)
|
|
142
|
+
|
|
143
|
+
# 假设knowledge类型内容是由"---\nrecall slice X:\n"分隔的
|
|
144
|
+
slices = content.split('---\n')
|
|
145
|
+
for slice in slices:
|
|
146
|
+
if slice.strip().startswith('recall slice'):
|
|
147
|
+
# 去掉前缀找到JSON部分
|
|
148
|
+
json_part = slice.strip().split('\n', 1)[-1].strip()
|
|
149
|
+
# 如果不是以\"}结尾,则强制添加
|
|
150
|
+
if not json_part.endswith('\"}'):
|
|
151
|
+
json_part += '\"}'
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# 尝试解析JSON数据
|
|
155
|
+
recall_data = json.loads(json_part)
|
|
156
|
+
cotext_pre.append(str(recall_data))
|
|
157
|
+
except json.JSONDecodeError:
|
|
158
|
+
raise CozeAIError(status_code=500, message=f"Error decoding JSON from slice: {json_part}")
|
|
159
|
+
|
|
160
|
+
elif message['role'] == 'assistant' and message['type'] == 'answer':
|
|
161
|
+
# 直接添加回复内容
|
|
162
|
+
results.append(message['content'])
|
|
163
|
+
|
|
164
|
+
message = Message(
|
|
165
|
+
content=results[0],
|
|
166
|
+
role="assistant"
|
|
167
|
+
)
|
|
168
|
+
choices.append(
|
|
169
|
+
Choices(
|
|
170
|
+
message=message,
|
|
171
|
+
index=0,
|
|
172
|
+
finish_reason="stop",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
usage = Usage(
|
|
177
|
+
prompt_tokens=0,
|
|
178
|
+
completion_tokens=0,
|
|
179
|
+
total_tokens=0,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Assume response_dict contains a field 'responseData' with quoteList to extract context
|
|
183
|
+
if cotext_pre:
|
|
184
|
+
i = 0
|
|
185
|
+
for context_item in cotext_pre:
|
|
186
|
+
i += 1
|
|
187
|
+
context.append(Context(id=i, content=context_item))
|
|
188
|
+
|
|
189
|
+
response = ModelResponse(
|
|
190
|
+
id=result_dict.get('conversation_id'),
|
|
191
|
+
choices=choices,
|
|
192
|
+
context=context,
|
|
193
|
+
created=int(time.time()),
|
|
194
|
+
model='',
|
|
195
|
+
usage=usage,
|
|
196
|
+
)
|
|
197
|
+
return response
|
|
198
|
+
|
|
199
|
+
def completion(self, model: str, messages: list, **kwargs):
|
|
200
|
+
try:
|
|
201
|
+
if model is None or messages is None:
|
|
202
|
+
raise CozeAIError(
|
|
203
|
+
status_code=422, message="Missing model or messages"
|
|
204
|
+
)
|
|
205
|
+
new_kwargs = self.pre_processing(**kwargs)
|
|
206
|
+
stream = kwargs.get("stream", False)
|
|
207
|
+
|
|
208
|
+
if stream:
|
|
209
|
+
return self.post_stream_processing_wrapper(model, messages, **new_kwargs)
|
|
210
|
+
else:
|
|
211
|
+
messages, query = self.to_formatted_prompt(messages)
|
|
212
|
+
|
|
213
|
+
# 接受user_id作为用户唯一身份标识传入参数
|
|
214
|
+
if 'user_id' not in kwargs:
|
|
215
|
+
user_id = generate_unique_uid()
|
|
216
|
+
else:
|
|
217
|
+
user_id = kwargs['user_id']
|
|
218
|
+
payload = json.dumps({"query": query,"user": user_id,"bot_id": model,"chat_history":[],"steam":False})
|
|
219
|
+
|
|
220
|
+
headers = {
|
|
221
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
}
|
|
224
|
+
result = requests.post(self.endpoint_url, headers=headers, data=payload)
|
|
225
|
+
return self.create_model_response_wrapper(result, model=model)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
if hasattr(e, "status_code"):
|
|
228
|
+
raise CozeAIError(status_code=e.status_code, message=str(e))
|
|
229
|
+
else:
|
|
230
|
+
raise CozeAIError(status_code=500, message=str(e))
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from .base_provider import BaseProvider
|
|
3
|
+
from cnlitellm.utils import ModelResponse, Message, Choices, Usage, Context, Delta, StreamingChoices
|
|
4
|
+
import requests
|
|
5
|
+
import logging, json, time
|
|
6
|
+
|
|
7
|
+
class DifyOpenAIError(Exception):
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
status_code,
|
|
11
|
+
message,
|
|
12
|
+
):
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.message = message
|
|
15
|
+
super().__init__(self.message)
|
|
16
|
+
|
|
17
|
+
class DifyAIProvider(BaseProvider):
|
|
18
|
+
def __init__(self, **model_kwargs):
|
|
19
|
+
self.api_key = model_kwargs.get("api_key")
|
|
20
|
+
self.endpoint_url = "https://api.dify.ai/v1/chat-messages"
|
|
21
|
+
|
|
22
|
+
def pre_processing(self, **kwargs):
|
|
23
|
+
supported_params = [
|
|
24
|
+
"model", "messages", "max_tokens", "temperature", "top_p", "n",
|
|
25
|
+
"logprobs", "stream", "stop", "presence_penalty", "frequency_penalty",
|
|
26
|
+
"best_of", "logit_bias"
|
|
27
|
+
]
|
|
28
|
+
for key in list(kwargs.keys()):
|
|
29
|
+
if key not in supported_params:
|
|
30
|
+
kwargs.pop(key)
|
|
31
|
+
return kwargs
|
|
32
|
+
|
|
33
|
+
def to_formatted_prompt(self, messages):
|
|
34
|
+
message = messages[-1]
|
|
35
|
+
# if last message role is not user, return error
|
|
36
|
+
if message["role"] != "user":
|
|
37
|
+
raise DifyOpenAIError(
|
|
38
|
+
status_code=422, message=f"Last message role should be user"
|
|
39
|
+
)
|
|
40
|
+
query = message["content"]
|
|
41
|
+
return messages, query
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def post_stream_processing_wrapper(self, model, messages, **new_kwargs):
|
|
45
|
+
# 预处理对话内容并返回最近的用户问题
|
|
46
|
+
messages, query = self.to_formatted_prompt(messages)
|
|
47
|
+
mode = "streaming"
|
|
48
|
+
payload = json.dumps({"query": query, "response_mode": mode, "user": "abc-123","conversation_id": "","inputs":{}})
|
|
49
|
+
|
|
50
|
+
headers = {
|
|
51
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
}
|
|
54
|
+
response = requests.post(self.endpoint_url, headers=headers, data=payload)
|
|
55
|
+
|
|
56
|
+
chunk_usage = Usage()
|
|
57
|
+
index = 0
|
|
58
|
+
for line in response.iter_lines():
|
|
59
|
+
if line:
|
|
60
|
+
chunk_choices = []
|
|
61
|
+
chunk_context = []
|
|
62
|
+
chunk_delta = Delta()
|
|
63
|
+
# judge if the new_line begins with "data:"
|
|
64
|
+
if line.startswith(b"data:"):
|
|
65
|
+
new_line = line.decode("utf-8").replace("data: ", "")
|
|
66
|
+
if new_line == "[DONE]":
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
data = json.loads(new_line)
|
|
71
|
+
print("data is:",data)
|
|
72
|
+
event = data.get("event")
|
|
73
|
+
msg_id = data.get("message_id")
|
|
74
|
+
if event == "message":
|
|
75
|
+
if 'answer' in data:
|
|
76
|
+
chunk_message = data["answer"]
|
|
77
|
+
if chunk_message:
|
|
78
|
+
chunk_delta.role = "assistant"
|
|
79
|
+
chunk_delta.content = chunk_message
|
|
80
|
+
chunk_choices.append(StreamingChoices(index=str(index), delta=chunk_delta))
|
|
81
|
+
|
|
82
|
+
if event == "message_end" and "metadata" in data:
|
|
83
|
+
metadata = data["metadata"]
|
|
84
|
+
if "usage" in metadata:
|
|
85
|
+
usage_info = metadata["usage"]
|
|
86
|
+
if "prompt_tokens" in usage_info:
|
|
87
|
+
chunk_usage.prompt_tokens = usage_info["prompt_tokens"]
|
|
88
|
+
if "completion_tokens" in usage_info:
|
|
89
|
+
chunk_usage.completion_tokens = usage_info["completion_tokens"]
|
|
90
|
+
if "total_tokens" in usage_info:
|
|
91
|
+
chunk_usage.total_tokens = usage_info["total_tokens"]
|
|
92
|
+
|
|
93
|
+
if 'retriever_resources' in metadata:
|
|
94
|
+
for resource in metadata['retriever_resources']:
|
|
95
|
+
chunk_context.append({
|
|
96
|
+
"id": resource["position"],
|
|
97
|
+
"content": resource["content"],
|
|
98
|
+
"score": resource["score"],
|
|
99
|
+
})
|
|
100
|
+
chunk_response = ModelResponse(
|
|
101
|
+
id=msg_id,
|
|
102
|
+
choices=chunk_choices,
|
|
103
|
+
context=chunk_context,
|
|
104
|
+
created=int(time.time()),
|
|
105
|
+
model=model,
|
|
106
|
+
usage=chunk_usage,
|
|
107
|
+
stream=True
|
|
108
|
+
)
|
|
109
|
+
index += 1
|
|
110
|
+
yield chunk_response
|
|
111
|
+
|
|
112
|
+
def create_model_response_wrapper(self, result, model):
|
|
113
|
+
|
|
114
|
+
print("result of dify:", result.json())
|
|
115
|
+
|
|
116
|
+
response_dict = result.json()
|
|
117
|
+
choices = []
|
|
118
|
+
context = []
|
|
119
|
+
|
|
120
|
+
message = Message(
|
|
121
|
+
content=response_dict['answer'], role="assistant"
|
|
122
|
+
)
|
|
123
|
+
choices.append(
|
|
124
|
+
Choices(
|
|
125
|
+
message=message,
|
|
126
|
+
index=0,
|
|
127
|
+
finish_reason="stop",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
usage = Usage(
|
|
132
|
+
prompt_tokens=response_dict['metadata']["usage"]["prompt_tokens"],
|
|
133
|
+
completion_tokens=response_dict['metadata']["usage"]["completion_tokens"],
|
|
134
|
+
total_tokens=response_dict['metadata']["usage"]["total_tokens"],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if 'retriever_resources' in response_dict['metadata']:
|
|
138
|
+
retrieved_resources = response_dict['metadata']["retriever_resources"]
|
|
139
|
+
if retrieved_resources is not None:
|
|
140
|
+
for resource in retrieved_resources:
|
|
141
|
+
context.append(
|
|
142
|
+
Context(
|
|
143
|
+
id=resource["position"],
|
|
144
|
+
content=resource["content"],
|
|
145
|
+
score=resource["score"],
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
context = []
|
|
150
|
+
|
|
151
|
+
response = ModelResponse(
|
|
152
|
+
id=response_dict["id"],
|
|
153
|
+
choices=choices,
|
|
154
|
+
context=context,
|
|
155
|
+
created=int(time.time()),
|
|
156
|
+
model=model,
|
|
157
|
+
usage=usage,
|
|
158
|
+
)
|
|
159
|
+
return response
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def completion(self, model: str, messages: list, **kwargs):
|
|
163
|
+
try:
|
|
164
|
+
if model is None or messages is None:
|
|
165
|
+
raise DifyOpenAIError(
|
|
166
|
+
status_code=422, message=f"Missing model or messages"
|
|
167
|
+
)
|
|
168
|
+
new_kwargs = self.pre_processing(**kwargs)
|
|
169
|
+
stream = kwargs.get("stream", False)
|
|
170
|
+
|
|
171
|
+
print("stream is:", stream)
|
|
172
|
+
|
|
173
|
+
if stream:
|
|
174
|
+
return self.post_stream_processing_wrapper(model, messages, **new_kwargs)
|
|
175
|
+
else:
|
|
176
|
+
messages, query = self.to_formatted_prompt(messages)
|
|
177
|
+
mode = "blocking"
|
|
178
|
+
|
|
179
|
+
print("mode is:", mode)
|
|
180
|
+
|
|
181
|
+
payload = json.dumps({"query": query, "response_mode": mode, "user": "abc-123","conversation_id": "","inputs":{}})
|
|
182
|
+
headers = {
|
|
183
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
}
|
|
186
|
+
result = requests.post(self.endpoint_url, headers=headers, data=payload)
|
|
187
|
+
return self.create_model_response_wrapper(result, model=model)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
if hasattr(e, "status_code"):
|
|
190
|
+
raise DifyOpenAIError(status_code=e.status_code, message=str(e))
|
|
191
|
+
else:
|
|
192
|
+
raise DifyOpenAIError(status_code=500, message=str(e))
|