Skip to content

Commit

Permalink
Merge pull request #2 from etShaw-zh/AICO-dev
Browse files Browse the repository at this point in the history
feat: add more LLM supported.
  • Loading branch information
etShaw-zh authored Jan 28, 2025
1 parent 61fde9c commit f1bbdb1
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 164 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.AUTO_RELEASE }}
with:
tag_name: v1.0.3
tag_name: ${{ github.ref_name }}
draft: true
files: |
AICodingOfficer_windows.zip
AICodingOfficer_mac_x86.zip
AICodingOfficer_linux.zip
name: 🎉AICO
name: "AICO: A cutting-edge artificial intelligence text coding officer"
body: |
## Bug Fixes
- Fix a bug #1
47 changes: 47 additions & 0 deletions .github/workflows/pr-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: PR Test

on:
pull_request:
branches: [ main ]
paths-ignore:
- '**.md'
- 'docs/**'
- '.gitignore'

jobs:
test:
name: Build Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-latest, macos-latest, ubuntu-latest ]
python-version: ["3.10"]

steps:
- name: Check out
uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
- name: Build Test
run: pyinstaller build.spec

- name: Verify build artifacts
shell: bash
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
test -f "dist/AICodingOfficer.exe" || exit 1
elif [ "${{ runner.os }}" = "macOS" ]; then
test -d "dist/AICodingOfficer.app" || exit 1
elif [ "${{ runner.os }}" = "Linux" ]; then
test -f "dist/AICodingOfficer" || exit 1
fi
2 changes: 1 addition & 1 deletion build.spec
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ app = BUNDLE(
name='AICodingOfficer.app',
icon='image/icon.icns',
bundle_identifier=None,
version='1.0.3'
version='1.0.5'
)
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
project = 'AI Coding Officer'
copyright = '2024, Jianjun Xiao'
author = 'Jianjun Xiao'
release = 'v1.0.3'
release = 'v1.0.5'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
15 changes: 10 additions & 5 deletions src/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ def __init__(self):
self.myAutoCodingWindow = MyAutoCodingWindow()
self.stackWidget.addWidget(self.myAutoCodingWindow)

self.myMainWindow = MyMainWindow()
self.stackWidget.addWidget(self.myMainWindow)
# self.myMainWindow = MyMainWindow()
# self.stackWidget.addWidget(self.myMainWindow)

# initialize layout
self.initLayout()
Expand Down Expand Up @@ -570,9 +570,14 @@ def prepare_prompt(self):

for topic_id in topic_reply_tree_dict:
for reply_tree in topic_reply_tree_dict[topic_id]['reply_tree']:
prompt_content = r"""您将看到一组论坛中的话题和回帖,您的任务是优先根据下面的编码表中的含义解释对每个回帖提取一组标签“codes”(只有当编码表中没有合适的标签时才输出“NULL”),并以中文举例说明提取标签的理由,注意将理由翻译为中文列出。结果以JSON格式的数组输出:[{"reply_id":"1234","tags":[],"reason":[]},{"reply_id":"2345","tags":[],"reason":[]}],注意只输出JSON,不要包括其他内容!tags和reason中的内容一一对应,请根据实际情况填写,不要直接复制粘贴。
编码表:\n
"""
if self.language == 'Chinese':
prompt_content = r"""您将看到一组论坛中的话题和回帖,您的任务是优先根据下面的编码表中的含义解释对每个回帖提取一组标签“codes”(只有当编码表中没有合适的标签时才输出“NULL”),并以中文举例说明提取标签的理由,注意将理由翻译为中文列出。结果以JSON格式的数组输出:[{"reply_id":"1234","tags":[],"reason":[]},{"reply_id":"2345","tags":[],"reason":[]}],注意只输出JSON,不要包括其他内容!tags和reason中的内容一一对应,请根据实际情况填写,不要直接复制粘贴。
编码表:\n
"""
elif self.language == 'English':
prompt_content = r"""您将看到一组论坛中的话题和回帖,您的任务是优先根据下面的编码表中的含义解释对每个回帖提取一组标签“codes”(只有当编码表中没有合适的标签时才输出“NULL”),并以英文举例说明提取标签的理由,注意将理由翻译为英文列出。结果以JSON格式的数组输出:[{"reply_id":"1234","tags":[],"reason":[]},{"reply_id":"2345","tags":[],"reason":[]}],注意只输出JSON,不要包括其他内容!tags和reason中的内容一一对应,请根据实际情况填写,不要直接复制粘贴。
编码表:\n
"""
prompt_content += r"""
{encode_table_latex}
\n\n话题:\n
Expand Down
134 changes: 112 additions & 22 deletions src/gui/setting.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QFrame
from PySide6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QFrame, QMessageBox
from PySide6.QtGui import QIcon
from qfluentwidgets import LineEdit, PushButton, FluentIcon, PrimaryPushButton, EditableComboBox

from src.module.resource import getResource
from src.module.config import localDBFilePath, logFolder

from src.module.config import localDBFilePath, logFolder, readConfig, configFile
from src.module.aihubmix import AiHubMixAPI

class SettingWindow(object):
def setupUI(self, this_window):
Expand Down Expand Up @@ -33,15 +33,38 @@ def setupUI(self, this_window):

self.languageCard = self.settingCard(self.languageTitle, self.languageInfo, self.language, "full")

# 选择模型
# 设置模型API key
self.modelApiTitle = QLabel("API Key")
self.modelApiInfo = QLabel("Please enter your AiHubMix API key.")

self.modelApiKey = LineEdit(self)
self.modelApiKey.setFixedWidth(200)
self.modelApiKey.setClearButtonEnabled(True)
self.modelApiKey.setText(readConfig().get("APIkey", "api_key"))

# 添加验证按钮
self.validateApiButton = PushButton("Validate", self)
self.validateApiButton.setFixedWidth(80)
self.validateApiButton.clicked.connect(self.validateApiKey)

self.modelTypeTitle = QLabel("GPT model")
# 创建水平布局来放置API Key输入框和验证按钮
self.apiKeyLayout = QHBoxLayout()
self.apiKeyLayout.addWidget(self.modelApiKey)
self.apiKeyLayout.addWidget(self.validateApiButton)
self.apiKeyLayout.addStretch()

self.modelTypeInfo = QLabel("Select the GPT model to use, different models have different performance.")
self.apiKeyFrame = QFrame()
self.apiKeyFrame.setLayout(self.apiKeyLayout)

self.modelApiCard = self.settingCard(self.modelApiTitle, self.modelApiInfo, self.apiKeyFrame, "full")

# 选择模型
self.modelTypeTitle = QLabel("AI Model")
self.modelTypeInfo = QLabel("Select the AI model to use. The list shows models available with your API key.")
self.modelTypeInfo.setObjectName("cardInfoLabel")

self.modelTypeUrl = QLabel("<a href='https://platform.moonshot.cn/'"
"style='font-size:12px;color:#2E75B6;'>To read document.</a>")
self.modelTypeUrl = QLabel("<a href='https://doc.aihubmix.com/en'"
"style='font-size:12px;color:#2E75B6;'>View documentation</a>")
self.modelTypeUrl.setOpenExternalLinks(True)

self.modelInfoLayout = QHBoxLayout()
Expand All @@ -57,10 +80,25 @@ def setupUI(self, this_window):
self.modelType = EditableComboBox(self)
self.modelType.setMinimumWidth(200)
self.modelType.setMaximumWidth(200)
self.modelType.addItems(["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"])
self.modelType.setText("moonshot-v1-8k") # 设置默认值为 "Kimi"

# 初始化时加载已保存的模型
current_model = readConfig().get("AICO", "model")
self.modelType.setText(current_model)

# 添加刷新按钮
self.refreshModelsButton = PushButton("Refresh", self)
self.refreshModelsButton.setFixedWidth(80)
self.refreshModelsButton.clicked.connect(self.refreshAvailableModels)

self.modelTypeLayout = QHBoxLayout()
self.modelTypeLayout.addWidget(self.modelType)
self.modelTypeLayout.addWidget(self.refreshModelsButton)
self.modelTypeLayout.addStretch()

self.modelTypeCard = self.settingCard(self.modelTypeTitle, self.dateInfoFrame, self.modelType, "full")
self.modelTypeFrame = QFrame()
self.modelTypeFrame.setLayout(self.modelTypeLayout)

self.modelTypeCard = self.settingCard(self.modelTypeTitle, self.dateInfoFrame, self.modelTypeFrame, "full")

# 选择线程数
self.threadTitle = QLabel("Thread count")
Expand All @@ -76,17 +114,6 @@ def setupUI(self, this_window):

self.threadCard = self.settingCard(self.threadTitle, self.threadInfo, self.threadCount, "full")

# 设置模型API key

self.modelApiTitle = QLabel("GPT API Key")
self.modelApiInfo = QLabel("Please enter the API key of the Kimi open platform.")

self.modelApiKey = LineEdit(self)
self.modelApiKey.setFixedWidth(200)
self.modelApiKey.setClearButtonEnabled(True)

self.modelApiCard = self.settingCard(self.modelApiTitle, self.modelApiInfo, self.modelApiKey, "full")

# 本地数据库文件夹

self.localDBTitle = QLabel("Local database")
Expand Down Expand Up @@ -137,6 +164,8 @@ def setupUI(self, this_window):
layout.addSpacing(12)
layout.addLayout(self.buttonLayout)

self.applyButton.clicked.connect(self.saveSettings)

def settingCard(self, card_title, card_info, card_func, size):
card_title.setObjectName("cardTitleLabel")
card_info.setObjectName("cardInfoLabel")
Expand Down Expand Up @@ -182,3 +211,64 @@ def tutorialCard(self, card_token, card_explain):
self.card.setLayout(self.tutorialLayout)

return self.card

def validateApiKey(self):
"""验证API密钥"""
api_key = self.modelApiKey.text().strip()
if not api_key:
QMessageBox.warning(self, "Error", "Please enter your API key first!")
return

api = AiHubMixAPI()
if api.validate_api_key():
QMessageBox.information(self, "Success", "API Key is valid!")
self.refreshAvailableModels()
else:
QMessageBox.warning(self, "Error", "Invalid API key. Please check and try again.")

def refreshAvailableModels(self):
"""刷新可用模型列表"""
api = AiHubMixAPI()
models = api.get_available_models()

if not models:
QMessageBox.warning(self, "Error", "Failed to fetch available models. Please check your API key and try again.")
return

current_model = self.modelType.text()
self.modelType.clear()
self.modelType.addItems(models)

# 保持当前选择的模型(如果它仍然可用)
if current_model in models:
self.modelType.setText(current_model)
else:
self.modelType.setText(models[0])

def saveSettings(self):
"""保存设置"""
config = readConfig()

# 保存API密钥
api_key = self.modelApiKey.text().strip()
if api_key:
config.set("APIkey", "api_key", api_key)

# 保存选择的模型
selected_model = self.modelType.text()
if selected_model:
config.set("AICO", "model", selected_model)

# 保存语言设置
config.set("Language", "language", self.language.text())

# 保存线程数
thread_count = self.threadCount.text()
if thread_count.isdigit() and 1 <= int(thread_count) <= 8:
config.set("Thread", "thread_count", thread_count)

# 写入配置文件
with open(configFile(), "w", encoding="utf-8") as f:
config.write(f)

QMessageBox.information(self, "Success", "Settings saved successfully!")
108 changes: 108 additions & 0 deletions src/module/aihubmix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import requests
from src.module.config import readConfig

class APIError(Exception):
"""API错误的自定义异常类"""
def __init__(self, status_code, error_type, message):
self.status_code = status_code
self.error_type = error_type
self.message = message
super().__init__(self.message)

class AiHubMixAPI:
BASE_URL = "https://api.aihubmix.com/v1"

# HTTP状态码及其对应的错误描述
ERROR_MESSAGES = {
400: "请求格式错误,不能被服务器理解。通常意味着客户端错误。",
401: "API密钥验证未通过。你需要验证你的API密钥是否正确,其他原因",
403: "一般是权限不足。",
404: "请求的资源未找到。你可能正在尝试访问一个不存在的端点。",
413: "请求体太大。你可能需要减小你的请求体容量。",
429: "由于频繁的请求超过限制,你已经超过了你的速率限制。",
500: "服务器内部的错误。这可能是OpenAI服务器的问题,不是你的问题。",
503: "服务器暂时不可用。这可能是由于OpenAI正在进行维护或者服务器过载。"
}

def __init__(self):
self.api_key = readConfig().get("APIkey", "api_key")
self.headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {self.api_key}'
}

def _handle_error_response(self, response):
"""处理API错误响应"""
status_code = response.status_code
try:
error_data = response.json().get('error', {})
error_type = error_data.get('type', 'unknown_error')
error_message = error_data.get('message', '未知错误')
except ValueError:
error_type = 'parse_error'
error_message = '无法解析错误响应'

# 获取详细的错误描述
error_description = self.ERROR_MESSAGES.get(status_code, '未知错误')

# 组合完整的错误信息
full_error_message = f"HTTP {status_code}: {error_description}\n具体错误: {error_message}"

raise APIError(status_code, error_type, full_error_message)

def _make_request(self, method, endpoint, **kwargs):
"""统一的请求处理方法"""
try:
url = f"{self.BASE_URL}/{endpoint.lstrip('/')}"
response = requests.request(method, url, headers=self.headers, **kwargs)

if response.status_code != 200:
self._handle_error_response(response)

return response.json()
except requests.exceptions.RequestException as e:
raise APIError(0, 'network_error', f"网络请求错误: {str(e)}")
except Exception as e:
raise APIError(0, 'unknown_error', f"未知错误: {str(e)}")

def get_available_models(self):
"""获取当前API Key支持的所有模型列表"""
try:
response = self._make_request('GET', '/models')
models = response.get('data', [])
return [model['id'] for model in models if model.get('available', True)]
except APIError as e:
print(f"获取模型列表失败: {e.message}")
return []

def get_model_info(self, model_id):
"""获取特定模型的详细信息"""
try:
response = self._make_request('GET', f'/models/{model_id}')
return response.get('data', {})
except APIError as e:
print(f"获取模型信息失败: {e.message}")
return {}

def validate_api_key(self):
"""验证API Key是否有效"""
try:
self._make_request('GET', '/models')
return True
except APIError:
return False

def chat_completion(self, model, messages, temperature=0.7, max_tokens=None):
"""统一的聊天完成接口"""
try:
data = {
"model": model,
"messages": messages,
"temperature": temperature
}
if max_tokens is not None:
data["max_tokens"] = max_tokens

return self._make_request('POST', '/chat/completions', json=data)
except APIError as e:
return {"error": e.message}
Loading

0 comments on commit f1bbdb1

Please sign in to comment.