Skip to content

Commit

Permalink
3.3
Browse files Browse the repository at this point in the history
1. Add more translations
2. Improve AI
  • Loading branch information
cdhigh committed Dec 25, 2024
1 parent 9fbfdd6 commit 68df041
Show file tree
Hide file tree
Showing 38 changed files with 19,390 additions and 440 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ tests/tools/*
tests/rss/*
tests/debug_mail/*
tests/cov_html/*
tests/pobackup/*
.idea/
datastore/
.coverage
Expand Down
6 changes: 3 additions & 3 deletions application/lib/ebook_summarizer/html_summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
from application.ke_utils import loc_exc_pos

def get_summarizer_engines():
return simple_ai_provider._PROV_AI_LIST
return simple_ai_provider.AI_LIST

class HtmlSummarizer:
def __init__(self, params: dict):
self.params = params
name = self.params.get('engine')
if name not in simple_ai_provider._PROV_AI_LIST:
if name not in simple_ai_provider.AI_LIST:
default_log.warning(f'Unsupported provider {name}, fallback to gemini')
name = 'gemini'
self.aiAgent = self.create_engine(name, params)

#创建一个AI封装实例
def create_engine(self, name, params):
return simple_ai_provider.SimpleAiProvider(name, params.get('api_key', ''),
model=params.get('model', ''), api_host=params.get('api_host', ''))
model=params.get('model', ''), apiHost=params.get('api_host', ''))

#给一段文字做摘要,记住不要太长
#返回 {'error': '', 'summary': ''}
Expand Down
220 changes: 112 additions & 108 deletions application/lib/simple_ai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,140 +3,161 @@
#简单封装几个流行的AI服务接口,对外提供一个统一的接口
#Author: cdhigh <https://github.com/cdhigh>
# 使用示例:
#provider = SimpleAiProvider("openai", api_key="xxxxxx")
#provider = SimpleAiProvider("openai", apiKey="xxxxxx")
#response = provider.chat("你好,请讲一个笑话")
#response = provider.chat([{"role": "system", "content": "你是一个专业的物理学家。"},{"role": "user", "content": "黑洞是怎么形成的?"}])
#import requests
from urllib.parse import urljoin
from urlopener import UrlOpener

#支持的AI服务商列表,models里面的第一项请设置为默认要使用的model
#rpm(requests per minute)是针对免费用户的,如果是付费用户,一般会高很多,可以自己修改
#大语言模型发展迅速,估计没多久这些数据会全部过时
#{'name': '', 'rpm': , 'context': },
_PROV_AI_LIST = {
'Gemini': [
AI_LIST = {
'google': {'host': 'https://generativelanguage.googleapis.com', 'models': [
{'name': 'gemini-1.5-flash', 'rpm': 15, 'context': 128000}, #其实支持100万
{'name': 'gemini-1.5-flash-8b', 'rpm': 15, 'context': 128000},
{'name': 'gemini-1.5-pro', 'rpm': 2, 'context': 128000},],
'Openai': [
{'name': 'gemini-1.5-pro', 'rpm': 2, 'context': 128000},],},
'openai': {'host': 'https://api.openai.com', 'models': [
{'name': 'gpt-4o-mini', 'rpm': 3, 'context': 128000},
{'name': 'gpt-4o', 'rpm': 3, 'context': 128000},
{'name': 'gpt-4-turbo', 'rpm': 3, 'context': 128000},
{'name': 'gpt-3.5-turbo', 'rpm': 3, 'context': 16000},
{'name': 'gpt-3.5-turbo-instruct', 'rpm': 3, 'context': 4000},],
'Anthropic': [
{'name': 'gpt-3.5-turbo-instruct', 'rpm': 3, 'context': 4000},],},
'anthropic': {'host': 'https://api.anthropic.com', 'models': [
{'name': 'claude-2', 'rpm': 5, 'context': 100000},
{'name': 'claude-3', 'rpm': 5, 'context': 200000},
{'name': 'claude-2.1', 'rpm': 5, 'context': 100000},],
'Grok': [
{'name': 'grok-beta', 'rpm': 60, 'context': 128000},],
'Mistral': [
{'name': 'claude-2.1', 'rpm': 5, 'context': 100000},],},
'xai': {'host': 'https://api.x.ai', 'models': [
{'name': 'grok-beta', 'rpm': 60, 'context': 128000},
{'name': 'grok-2', 'rpm': 60, 'context': 128000},],},
'mistral': {'host': 'https://api.mistral.ai', 'models': [
{'name': 'open-mistral-7b', 'rpm': 60, 'context': 32000},
{'name': 'mistral-small-latest', 'rpm': 60, 'context': 32000},
{'name': 'open-mixtral-8x7b', 'rpm': 60, 'context': 32000},
{'name': 'open-mixtral-8x22b', 'rpm': 60, 'context': 64000},
{'name': 'mistral-medium-latest', 'rpm': 60, 'context': 32000},
{'name': 'mistral-large-latest', 'rpm': 60, 'context': 128000},
{'name': 'pixtral-12b-2409', 'rpm': 60, 'context': 128000},],
'Groq': [
{'name': 'pixtral-12b-2409', 'rpm': 60, 'context': 128000},],},
'groq': {'host': 'https://api.groq.com', 'models': [
{'name': 'gemma2-9b-it', 'rpm': 30, 'context': 8000},
{'name': 'gemma-7b-it', 'rpm': 30, 'context': 8000},
{'name': 'llama-guard-3-8b', 'rpm': 30, 'context': 8000},
{'name': 'llama3-70b-8192', 'rpm': 30, 'context': 8000},
{'name': 'llama3-8b-8192', 'rpm': 30, 'context': 8000},
{'name': 'mixtral-8x7b-32768', 'rpm': 30, 'context': 32000},],
'Alibaba': [
{'name': 'mixtral-8x7b-32768', 'rpm': 30, 'context': 32000},],},
'perplexity': {'host': 'https://api.perplexity.ai', 'models': [
{'name': 'llama-3.1-sonar-small-128k-online', 'rpm': 60, 'context': 128000},
{'name': 'llama-3.1-sonar-large-128k-online', 'rpm': 60, 'context': 128000},
{'name': 'llama-3.1-sonar-huge-128k-online', 'rpm': 60, 'context': 128000},],},
'alibaba': {'host': 'https://dashscope.aliyuncs.com', 'models': [
{'name': 'qwen-turbo', 'rpm': 60, 'context': 128000}, #其实支持100万
{'name': 'qwen-plus', 'rpm': 60, 'context': 128000},
{'name': 'qwen-long', 'rpm': 60, 'context': 128000},
{'name': 'qwen-max', 'rpm': 60, 'context': 32000},],
{'name': 'qwen-max', 'rpm': 60, 'context': 32000},],},
}

class SimpleAiProvider:
#name: AI提供商的名字
def __init__(self, name, api_key, model=None, api_host=None):
if name not in _PROV_AI_LIST:
#apiHost: 支持自搭建的API转发服务器,传入以分号分割的地址列表字符串,则逐个使用
#singleTurn: 一些API转发服务不支持多轮对话模式,设置此标识,当前仅支持 openai
def __init__(self, name, apiKey, model=None, apiHost=None, singleTurn=False):
name = name.lower()
if name not in AI_LIST:
raise ValueError(f"Unsupported provider: {name}")
self.name = name
self.api_key = api_key
self.apiKey = apiKey
self.singleTurn = singleTurn
self.apiHosts = (apiHost or AI_LIST[name]['host']).split(';')
self.hostIdx = 0
self.host = '' #如果提供多个api host,这个变量保存当前使用的host
self._models = AI_LIST[name]['models']

index = 0
for idx, item in enumerate(_PROV_AI_LIST[name]):
if model == item['name']:
index = idx
break
#如果传入的model不在列表中,默认使用第一个
item = next((m for m in self._models if m['name'] == model), self._models[0])
self.model = item['name']
self.rpm = item['rpm']
self.context_size = item['context']
if self.rpm <= 0:
self.rpm = 2
if self.context_size < 1000:
self.context_size = 1000

item = _PROV_AI_LIST[name][index]
self._model = item['name']
self._rpm = item['rpm']
self._context = item['context']
if self._rpm <= 0:
self._rpm = 2
if self._context < 4000:
self._context = 4000
self.api_host = api_host
self.opener = UrlOpener()

@property
def model(self):
return self._model
@property
def rpm(self):
return self._rpm
@property
def request_interval(self):
return (60 / self._rpm) if (self._rpm > 0) else 30
@property
def context_size(self):
return self._context
return (60 / self.rpm) if (self.rpm >= 1) else 20

def __repr__(self):
return f'{self.name}({self._model})'
return f'{self.name}/{self.model}'

#返回支持的AI供应商列表,返回一个python字典
def ai_list(self):
return _PROV_AI_LIST
return AI_LIST

#返回需要使用的api host,自动轮换使用多host
@property
def apiHost(self):
self.host = self.apiHosts[self.hostIdx]
self.hostIdx += 1
if self.hostIdx >= len(self.apiHosts):
self.hostIdx = 0
return self.host

#外部调用此函数即可调用简单聊天功能
#message: 如果是文本,则使用各项默认参数
#传入 list/dict 可以定制 role 等参数
#返回 respTxt,如果要获取当前使用的主机,可以使用 host 属性
def chat(self, message):
name = self.name
if name == "Openai":
if name == "openai":
return self._openai_chat(message)
elif name == "Anthropic":
elif name == "anthropic":
return self._anthropic_chat(message)
elif name == "Gemini":
return self._gemini_chat(message)
elif name == "Grok":
return self._grok_chat(message)
elif name == "Mistral":
elif name == "google":
return self._google_chat(message)
elif name == "xai":
return self._xai_chat(message)
elif name == "mistral":
return self._mistral_chat(message)
elif name == 'Groq':
elif name == 'groq':
return self._groq_chat(message)
elif name == "Alibaba":
elif name == 'perplexity':
ret = self._perplexity_chat(message)
elif name == "alibaba":
return self._alibaba_chat(message)
else:
raise ValueError(f"Unsupported provider: {name}")

#openai的chat接口
def _openai_chat(self, message, defaultUrl='https://api.openai.com/v1/chat/completions'):
url = self.api_host if self.api_host else defaultUrl
headers = {'Authorization': f"Bearer {self.api_key}",
'Content-Type': 'application/json'}
payload = {
"model": self._model,
"messages": [{"role": "user", "content": message}] if isinstance(message, str) else message
}
response = self.opener.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
def _openai_chat(self, message, path='v1/chat/completions'):
url = urljoin(self.apiHost, path)
headers = {'Authorization': f'Bearer {self.apiKey}', 'Content-Type': 'application/json'}
if isinstance(message, str):
msg = [{"role": "user", "content": message}]
elif self.singleTurn and (len(message) > 1): #将多轮对话手动拼接为单一轮对话
msgArr = ['Previous conversions:\n']
roleMap = {'system': 'background', 'assistant': 'Your responsed'}
msgArr.extend([f'{roleMap.get(e["role"], "I asked")}:\n{e["content"]}\n' for e in message[:-1]])
msgArr.append(f'\nPlease continue this conversation based on the previous information:\n')
msgArr.append("I ask:")
msgArr.append(message[-1]['content'])
msgArr.append("You Response:\n")
msg = [{"role": "user", "content": '\n'.join(msgArr)}]
else:
msg = message
payload = {"model": self.model, "messages": msg}
resp = self.opener.post(url, headers=headers, json=payload)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]

#anthropic的chat接口
def _anthropic_chat(self, message):
url = self.api_host if self.api_host else 'https://api.anthropic.com/v1/complete'
def _anthropic_chat(self, message, path='v1/complete'):
url = urljoin(self.apiHost, path)
headers = {'Accept': 'application/json', 'Anthropic-Version': '2023-06-01',
'Content-Type': 'application/json', 'x-api-key': self.api_key}
'Content-Type': 'application/json', 'x-api-key': self.apiKey}

if isinstance(message, list): #将openai的payload格式转换为anthropic的格式
msg = []
Expand All @@ -152,17 +173,13 @@ def _anthropic_chat(self, message):
prompt = f"\n\nHuman: {message}\n\nAssistant:"
payload = {"prompt": prompt, "model": self.model, "max_tokens_to_sample": 256}

response = self.opener.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()["completion"]
resp = self.opener.post(url, headers=headers, json=payload)
resp.raise_for_status()
return resp.json()["completion"]

#gemini的chat接口
def _gemini_chat(self, message):
if self.api_host:
url = f'{self.api_host}?key={self.api_key}'
else:
url = f'https://generativelanguage.googleapis.com/v1beta/models/{self._model}:generateContent?key={self.api_key}'

#google的chat接口
def _google_chat(self, message):
url = urljoin(self.apiHost, f'v1beta/models/{self.model}:generateContent?key={self.apiKey}')
if isinstance(message, list): #将openai的payload格式转换为gemini的格式
msg = []
for item in message:
Expand All @@ -174,45 +191,32 @@ def _gemini_chat(self, message):
payload = message
else:
payload = {'contents': [{'role': 'user', 'parts': [{'text': message}]}]}
response = self.opener.post(url, json=payload)
response.raise_for_status()
contents = response.json()["candidates"][0]["content"]
resp = self.opener.post(url, json=payload)
resp.raise_for_status()
contents = resp.json()["candidates"][0]["content"]
return contents['parts'][0]['text']

#grok的chat接口
def _grok_chat(self, message):
#直接使用openai兼容接口
return self._openai_chat(message, defaultUrl='https://api.x.ai/v1/chat/completions')
#xai的chat接口
def _xai_chat(self, message):
return self._openai_chat(message, path='v1/chat/completions')

#mistral的chat接口
def _mistral_chat(self, message):
#直接使用openai兼容接口
return self._openai_chat(message, defaultUrl='https://api.mistral.ai/v1/chat/completions')

return self._openai_chat(message, path='v1/chat/completions')

#groq的chat接口
def _groq_chat(self, message):
#直接使用openai兼容接口
return self._openai_chat(message, defaultUrl='https://api.groq.com/openai/v1/chat/completions')
return self._openai_chat(message, path='openai/v1/chat/completions')

#perplexity的chat接口
def _perplexity_chat(self, message):
return self._openai_chat(message, path='chat/completions')

#通义千问
def _alibaba_chat(self, message):
#直接使用openai兼容接口
return self._openai_chat(message, defaultUrl='https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions')
return self._openai_chat(message, path='compatible-mode/v1/chat/completions')

def _baidu_chat(self, message):
url = self.api_host if self.api_host else 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/ernie-bot'
headers = {"Content-Type": "application/json"}
params = {"access_token": self.api_key}
payload = {
"messages": [{"role": "user", "content": message}] if isinstance(message, str) else message,
"model": self._model,
"max_tokens": 300
}
response = self.opener.post(url, headers=headers, params=params, json=payload)
response.raise_for_status()
return response.json()["result"]["content"]

if __name__ == '__main__':
provider = SimpleAiProvider("gemini", api_key="xxx")
response = provider.chat("你好,请讲一个笑话")
print(response)
client = SimpleAiProvider("gemini", apiKey="xxx")
resp = client.chat("你好,请讲一个笑话")
print(resp)
14 changes: 14 additions & 0 deletions application/static/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ button {
padding: 1em .5em;
}

/* 语言选择框 */
.home-menu .language-select {
border: none;
background: none;
outline: none;
padding: 5px;
font-size: 0.6em;
color: #333;
text-align: center;
}
.home-menu .language-select:focus {
outline: none;
}

.app-menu {
background-color: #0078e7;
}
Expand Down
12 changes: 11 additions & 1 deletion application/static/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ function MakeAjaxRequest(url, method, data, callback) {
});
}

///[start] base.html
//选择其他语种进行页面显示
function handleLanguageChange() {
const lang = $('#languageSelect').val();
MakeAjaxRequest(`/setlocale`, "POST", {'lang': lang}, function (resp) {
location.reload(); //刷新页面
});
}
///[end] base.html

///[start] my.html
var show_menu_box = false;

Expand Down Expand Up @@ -1620,7 +1630,7 @@ function PopulateSummarizerFields(currEngineName) {
function SummarizerEngineFieldChanged(model) {
let hasSelected = false;
var engineName = $('#summarizer_engine').val();
var models = g_ai_engines[engineName] || [];
var models = g_ai_engines[engineName].models || [];

let modelSel = $('#summarizer_model');
modelSel.empty();
Expand Down
Binary file removed application/static/cn.gif
Binary file not shown.
Binary file removed application/static/tr.gif
Binary file not shown.
Binary file removed application/static/us.gif
Binary file not shown.
Loading

0 comments on commit 68df041

Please sign in to comment.