gemini pro

This commit is contained in:
Zylan
2025-04-11 17:10:18 +08:00
parent 65d1123eb2
commit e662ff24b5
11 changed files with 467 additions and 94 deletions

15
app.py
View File

@@ -98,6 +98,8 @@ def create_model_instance(model_id, settings, is_reasoning=False):
api_key_id = "DeepseekApiKey"
elif "qvq" in model_id.lower() or "alibaba" in model_id.lower() or "qwen" in model_id.lower():
api_key_id = "AlibabaApiKey"
elif "gemini" in model_id.lower() or "google" in model_id.lower():
api_key_id = "GoogleApiKey"
# 首先尝试从本地配置获取API密钥
api_key = get_api_key(api_key_id)
@@ -758,12 +760,13 @@ def load_api_keys():
else:
# 如果文件不存在,创建默认配置
default_keys = {
"AnthropicApiKey": "",
"OpenaiApiKey": "",
"DeepseekApiKey": "",
"AlibabaApiKey": "",
"MathpixAppId": "",
"MathpixAppKey": ""
"AnthropicApiKey": "",
"OpenaiApiKey": "",
"DeepseekApiKey": "",
"AlibabaApiKey": "",
"MathpixAppId": "",
"MathpixAppKey": "",
"GoogleApiKey": ""
}
save_api_keys(default_keys)
return default_keys

View File

@@ -19,6 +19,11 @@
"name": "Alibaba",
"api_key_id": "AlibabaApiKey",
"class_name": "AlibabaModel"
},
"google": {
"name": "Google",
"api_key_id": "GoogleApiKey",
"class_name": "GoogleModel"
}
},
"models": {
@@ -77,6 +82,22 @@
"isReasoning": false,
"version": "latest",
"description": "阿里通义千问VL-MAX模型视觉理解能力最强支持图像理解和复杂任务"
},
"gemini-2.5-pro-preview-03-25": {
"name": "Gemini 2.5 Pro",
"provider": "google",
"supportsMultimodal": true,
"isReasoning": true,
"version": "preview-03-25",
"description": "Google最强大的Gemini 2.5 Pro模型支持图像理解"
},
"gemini-2.0-flash": {
"name": "Gemini 2.0 Flash",
"provider": "google",
"supportsMultimodal": true,
"isReasoning": false,
"version": "latest",
"description": "Google更快速的Gemini 2.0 Flash模型支持图像理解响应更迅速"
}
}
}

View File

@@ -1,5 +1,5 @@
{
"default": {
"a_default": {
"name": "默认提示词",
"content": "您是一位专业的问题解决专家。请逐步分析问题,找出问题所在,并提供详细的解决方案。始终使用用户偏好的语言回答。",
"description": "通用问题解决提示词"
@@ -14,7 +14,7 @@
"content": "您是一位专业的多选题解析专家。当看到一个多选题时,请:\n1. 仔细阅读题目要求和所有选项\n2. 逐一分析每个选项的正确性\n3. 明确列出所有正确选项\n4. 详细解释每个正确选项的理由\n5. 说明错误选项的问题所在\n6. 归纳总结相关知识点",
"description": "专为多选题分析设计的提示词"
},
"acm_programming": {
"programming": {
"name": "ACM编程题提示词",
"content": "您是一位专业的ACM编程竞赛解题专家。当看到一个编程题时\n1. 分析题目要求、输入输出格式和约束条件\n2. 确定解题思路和算法策略\n3. 分析算法复杂度\n4. 提供完整、可运行的代码实现\n5. 解释代码中的关键部分\n6. 提供一些测试用例及其输出\n7. 讨论可能的优化方向",
"description": "专为ACM编程竞赛题设计的提示词"

View File

@@ -1,5 +1,5 @@
{
"version": "1.2.0",
"build_date": "2025-04-02",
"version": "1.3.0",
"build_date": "2025-04-11",
"github_repo": "Zippland/Snap-Solver"
}

View File

@@ -3,6 +3,7 @@ from .anthropic import AnthropicModel
from .openai import OpenAIModel
from .deepseek import DeepSeekModel
from .alibaba import AlibabaModel
from .google import GoogleModel
from .factory import ModelFactory
__all__ = [
@@ -11,5 +12,6 @@ __all__ = [
'OpenAIModel',
'DeepSeekModel',
'AlibabaModel',
'GoogleModel',
'ModelFactory'
]

View File

@@ -57,35 +57,18 @@ class ModelFactory:
@classmethod
def _initialize_defaults(cls):
"""初始化默认模型(当配置加载失败时)"""
print("使用默认模型配置")
# 导入所有模型类作为备份
from .anthropic import AnthropicModel
from .openai import OpenAIModel
from .deepseek import DeepSeekModel
print("配置加载失败,使用空模型列表")
cls._models = {
'claude-3-7-sonnet-20250219': {
'class': AnthropicModel,
'is_multimodal': True,
'is_reasoning': True,
'display_name': 'Claude 3.7 Sonnet',
'description': '强大的Claude 3.7 Sonnet模型支持图像理解和思考过程'
},
'gpt-4o-2024-11-20': {
'class': OpenAIModel,
'is_multimodal': True,
'is_reasoning': False,
'display_name': 'GPT-4o',
'description': 'OpenAI的GPT-4o模型支持图像理解'
},
'deepseek-reasoner': {
'class': DeepSeekModel,
'is_multimodal': False,
'is_reasoning': True,
'display_name': 'DeepSeek Reasoner',
'description': 'DeepSeek推理模型提供详细思考过程仅支持文本'
},
'mathpix': {
# 不再硬编码模型定义,而是使用空字典
cls._models = {}
# 只保留Mathpix作为基础工具
try:
# 导入MathpixModel类
from .mathpix import MathpixModel
# 添加Mathpix作为基础工具
cls._models['mathpix'] = {
'class': MathpixModel,
'is_multimodal': True,
'is_reasoning': False,
@@ -93,7 +76,8 @@ class ModelFactory:
'description': '文本提取工具,适用于数学公式和文本',
'is_ocr_only': True
}
}
except Exception as e:
print(f"无法加载基础Mathpix工具: {str(e)}")
@classmethod
def create_model(cls, model_name: str, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None) -> BaseModel:

236
models/google.py Normal file
View File

@@ -0,0 +1,236 @@
import json
import os
import base64
from typing import Generator, Dict, Any, Optional, List
import google.generativeai as genai
from .base import BaseModel
class GoogleModel(BaseModel):
"""
Google Gemini API模型实现类
支持Gemini 2.5 Pro等模型可处理文本和图像输入
"""
def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None, language: str = None, model_name: str = None):
"""
初始化Google模型
Args:
api_key: Google API密钥
temperature: 生成温度
system_prompt: 系统提示词
language: 首选语言
model_name: 指定具体模型名称,如不指定则使用默认值
"""
super().__init__(api_key, temperature, system_prompt, language)
self.model_name = model_name or self.get_model_identifier()
self.max_tokens = 8192 # 默认最大输出token数
# 配置Google API
genai.configure(api_key=api_key)
def get_default_system_prompt(self) -> str:
return """You are an expert at analyzing questions and providing detailed solutions. When presented with an image of a question:
1. First read and understand the question carefully
2. Break down the key components of the question
3. Provide a clear, step-by-step solution
4. If relevant, explain any concepts or theories involved
5. If there are multiple approaches, explain the most efficient one first"""
def get_model_identifier(self) -> str:
"""返回默认的模型标识符"""
return "gemini-2.5-pro-preview-03-25"
def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]:
"""流式生成文本响应"""
try:
yield {"status": "started"}
# 设置环境变量代理(如果提供)
original_proxies = None
if proxies:
original_proxies = {
'http_proxy': os.environ.get('http_proxy'),
'https_proxy': os.environ.get('https_proxy')
}
if 'http' in proxies:
os.environ['http_proxy'] = proxies['http']
if 'https' in proxies:
os.environ['https_proxy'] = proxies['https']
try:
# 初始化模型
model = genai.GenerativeModel(self.model_name)
# 获取最大输出Token设置
max_tokens = self.max_tokens if hasattr(self, 'max_tokens') else 8192
# 创建配置参数
generation_config = {
'temperature': self.temperature,
'max_output_tokens': max_tokens,
'top_p': 0.95,
'top_k': 64,
}
# 构建提示
prompt_parts = []
# 添加系统提示词
if self.system_prompt:
prompt_parts.append(self.system_prompt)
# 添加用户查询
if self.language and self.language != 'auto':
prompt_parts.append(f"请使用{self.language}回答以下问题: {text}")
else:
prompt_parts.append(text)
# 初始化响应缓冲区
response_buffer = ""
# 流式生成响应
response = model.generate_content(
prompt_parts,
generation_config=generation_config,
stream=True
)
for chunk in response:
if not chunk.text:
continue
# 累积响应文本
response_buffer += chunk.text
# 发送响应进度
if len(chunk.text) >= 10 or chunk.text.endswith(('.', '!', '?', '', '', '', '\n')):
yield {
"status": "streaming",
"content": response_buffer
}
# 确保发送完整的最终内容
yield {
"status": "completed",
"content": response_buffer
}
finally:
# 恢复原始代理设置
if original_proxies:
for key, value in original_proxies.items():
if value is None:
if key in os.environ:
del os.environ[key]
else:
os.environ[key] = value
except Exception as e:
yield {
"status": "error",
"error": f"Gemini API错误: {str(e)}"
}
def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]:
"""分析图像并流式生成响应"""
try:
yield {"status": "started"}
# 设置环境变量代理(如果提供)
original_proxies = None
if proxies:
original_proxies = {
'http_proxy': os.environ.get('http_proxy'),
'https_proxy': os.environ.get('https_proxy')
}
if 'http' in proxies:
os.environ['http_proxy'] = proxies['http']
if 'https' in proxies:
os.environ['https_proxy'] = proxies['https']
try:
# 初始化模型
model = genai.GenerativeModel(self.model_name)
# 获取最大输出Token设置
max_tokens = self.max_tokens if hasattr(self, 'max_tokens') else 8192
# 创建配置参数
generation_config = {
'temperature': self.temperature,
'max_output_tokens': max_tokens,
'top_p': 0.95,
'top_k': 64,
}
# 构建提示词
prompt_parts = []
# 添加系统提示词
if self.system_prompt:
prompt_parts.append(self.system_prompt)
# 添加默认图像分析指令
if self.language and self.language != 'auto':
prompt_parts.append(f"请使用{self.language}分析这张图片并提供详细解答。")
else:
prompt_parts.append("请分析这张图片并提供详细解答。")
# 处理图像数据
if image_data.startswith('data:image'):
# 如果是data URI提取base64部分
image_data = image_data.split(',', 1)[1]
# 使用genai的特定方法处理图像
image_part = {
"mime_type": "image/jpeg",
"data": base64.b64decode(image_data)
}
prompt_parts.append(image_part)
# 初始化响应缓冲区
response_buffer = ""
# 流式生成响应
response = model.generate_content(
prompt_parts,
generation_config=generation_config,
stream=True
)
for chunk in response:
if not chunk.text:
continue
# 累积响应文本
response_buffer += chunk.text
# 发送响应进度
if len(chunk.text) >= 10 or chunk.text.endswith(('.', '!', '?', '', '', '', '\n')):
yield {
"status": "streaming",
"content": response_buffer
}
# 确保发送完整的最终内容
yield {
"status": "completed",
"content": response_buffer
}
finally:
# 恢复原始代理设置
if original_proxies:
for key, value in original_proxies.items():
if value is None:
if key in os.environ:
del os.environ[key]
else:
os.environ[key] = value
except Exception as e:
yield {
"status": "error",
"error": f"Gemini图像分析错误: {str(e)}"
}

View File

@@ -5,4 +5,5 @@ flask-socketio==5.5.1
python-engineio==4.11.2
python-socketio==5.12.1
requests==2.32.3
openai==1.61.0
openai==1.61.0
google-generativeai==0.7.0

View File

@@ -697,28 +697,29 @@ class SettingsManager {
* @param {string} modelType 模型类型
*/
updateVisibleApiKey(modelType) {
// 获取所有API密钥状态元素
// 高亮显示对应的API密钥
const allApiKeys = document.querySelectorAll('.api-key-status');
// 首先隐藏所有API密钥
// 先清除所有高亮
allApiKeys.forEach(key => {
key.classList.remove('highlight');
});
// 根据当前选择的模型类型,突出显示对应的API密钥
// 根据模型类型高亮相应的API密钥
let apiKeyToHighlight = null;
if (modelType.startsWith('claude')) {
if (modelType && modelType.toLowerCase().includes('claude')) {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(1)'); // Anthropic
} else if (modelType.startsWith('gpt')) {
} else if (modelType && (modelType.toLowerCase().includes('gpt') || modelType.toLowerCase().includes('openai'))) {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(2)'); // OpenAI
} else if (modelType.startsWith('deepseek')) {
} else if (modelType && modelType.toLowerCase().includes('deepseek')) {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(3)'); // DeepSeek
} else if (modelType.startsWith('qwen')) {
} else if (modelType && (modelType.toLowerCase().includes('qwen') || modelType.toLowerCase().includes('qvq') || modelType.toLowerCase().includes('alibaba'))) {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(4)'); // Alibaba
} else if (modelType && (modelType.toLowerCase().includes('gemini') || modelType.toLowerCase().includes('google'))) {
apiKeyToHighlight = document.querySelector('.api-key-status:nth-child(5)'); // Google
}
// 高亮显示对应的API密钥
if (apiKeyToHighlight) {
apiKeyToHighlight.classList.add('highlight');
}
@@ -1541,61 +1542,43 @@ class SettingsManager {
*/
async loadPrompts() {
try {
// 从服务器获取提示词列表
// 从服务器获取提示词配置
const response = await fetch('/api/prompts');
if (response.ok) {
// 解析提示词列表
// 解析提示词数据
const prompts = await response.json();
// 保存到本地
// 存储提示词数据(保持原始顺序)
this.prompts = prompts;
// 添加提示词顺序属性,记录从服务器获取的原始顺序
this.promptsOrder = Object.keys(prompts);
// 更新提示词选择下拉框
if (this.promptSelect) {
this.updatePromptSelect();
this.updatePromptSelect();
}
// 确定要加载的提示词ID
let promptIdToLoad = null;
// 优先使用之前保存的currentPromptId
// 如果当前已有选中的提示词,尝试加载它
if (this.currentPromptId && this.prompts[this.currentPromptId]) {
promptIdToLoad = this.currentPromptId;
}
// 其次使用default提示词
else if (this.prompts.default) {
promptIdToLoad = 'default';
}
// 最后使用第一个可用的提示词
else if (Object.keys(this.prompts).length > 0) {
promptIdToLoad = Object.keys(this.prompts)[0];
this.loadPrompt(this.currentPromptId);
}
// 如果找到了要加载的提示词,加载它
if (promptIdToLoad) {
// 否则尝试加载默认提示词
else if (this.prompts.default) {
this.loadPrompt('default');
}
// 否则加载第一个提示词
else if (Object.keys(this.prompts).length > 0) {
const promptIdToLoad = Object.keys(this.prompts)[0];
this.loadPrompt(promptIdToLoad);
console.log('加载提示词:', promptIdToLoad);
} else {
// 如果没有提示词,显示默认描述
if (this.promptDescriptionElement) {
this.promptDescriptionElement.innerHTML = '<p>暂无提示词,请点击"+"创建新提示词</p>';
}
}
console.log('提示词加载成功:', this.prompts);
} else {
console.error('加载提示词失败:', response.status, response.statusText);
window.uiManager?.showToast('加载提示词失败', 'error');
// 显示默认描述
if (this.promptDescriptionElement) {
this.promptDescriptionElement.innerHTML = '<p>加载提示词失败,请检查网络连接</p>';
}
console.error('加载提示词失败:', response.statusText);
}
} catch (error) {
console.error('加载提示词错误:', error);
window.uiManager?.showToast('加载提示词错误: ' + error.message, 'error');
// 显示错误描述
if (this.promptDescriptionElement) {
@@ -1616,20 +1599,39 @@ class SettingsManager {
// 清空下拉框
this.promptSelect.innerHTML = '';
// 添加所有提示词选项
for (const promptId in this.prompts) {
const prompt = this.prompts[promptId];
const option = document.createElement('option');
option.value = promptId;
option.textContent = prompt.name;
this.promptSelect.appendChild(option);
// 按prompts.json中的原始顺序添加提示词选项
// 使用this.promptsOrder来保持原始顺序
if (this.promptsOrder && this.promptsOrder.length > 0) {
for (const promptId of this.promptsOrder) {
if (this.prompts[promptId]) {
const prompt = this.prompts[promptId];
const option = document.createElement('option');
option.value = promptId;
option.textContent = prompt.name;
this.promptSelect.appendChild(option);
}
}
} else {
// 如果没有保存顺序,则使用对象键的顺序(不推荐,但作为后备方案)
for (const promptId in this.prompts) {
const prompt = this.prompts[promptId];
const option = document.createElement('option');
option.value = promptId;
option.textContent = prompt.name;
this.promptSelect.appendChild(option);
}
}
// 恢复之前选中的提示词或选择第一个提示词
if (currentPromptId && this.prompts[currentPromptId]) {
this.promptSelect.value = currentPromptId;
} else if (this.promptsOrder && this.promptsOrder.length > 0) {
// 选择原始顺序的第一个提示词
this.promptSelect.value = this.promptsOrder[0];
// 更新当前提示词ID和描述显示
this.loadPrompt(this.promptSelect.value);
} else if (Object.keys(this.prompts).length > 0) {
// 如果之前选中的提示词不存在,选择第一个
// 如果没有原始顺序,选择第一个提示词
this.promptSelect.value = Object.keys(this.prompts)[0];
// 更新当前提示词ID和描述显示
this.loadPrompt(this.promptSelect.value);
@@ -1731,6 +1733,11 @@ class SettingsManager {
description: promptDescription
};
// 如果是新增提示词,将其添加到顺序数组末尾
if (isNew && this.promptsOrder && !this.promptsOrder.includes(promptId)) {
this.promptsOrder.push(promptId);
}
// 更新提示词选择下拉框
this.updatePromptSelect();
@@ -1779,6 +1786,11 @@ class SettingsManager {
if (response.ok) {
const result = await response.json();
if (result.success) {
// 从顺序数组中移除该提示词
if (this.promptsOrder && this.promptsOrder.includes(promptId)) {
this.promptsOrder = this.promptsOrder.filter(id => id !== promptId);
}
// 删除本地提示词
delete this.prompts[promptId];
@@ -1786,9 +1798,10 @@ class SettingsManager {
this.updatePromptSelect();
// 如果还有其他提示词,加载第一个
const promptIds = Object.keys(this.prompts);
if (promptIds.length > 0) {
this.loadPrompt(promptIds[0]);
if (this.promptsOrder && this.promptsOrder.length > 0) {
this.loadPrompt(this.promptsOrder[0]);
} else if (Object.keys(this.prompts).length > 0) {
this.loadPrompt(Object.keys(this.prompts)[0]);
} else {
// 如果没有提示词了,清空输入框和描述显示
this.systemPromptInput.value = '';
@@ -2093,6 +2106,7 @@ class SettingsManager {
'OpenaiApiKey': document.getElementById('OpenaiApiKey'),
'DeepseekApiKey': document.getElementById('DeepseekApiKey'),
'AlibabaApiKey': document.getElementById('AlibabaApiKey'),
'GoogleApiKey': document.getElementById('GoogleApiKey'),
'mathpixAppId': this.mathpixAppIdInput,
'mathpixAppKey': this.mathpixAppKeyInput
};
@@ -2133,6 +2147,7 @@ class SettingsManager {
'OpenaiApiKey': '',
'DeepseekApiKey': '',
'AlibabaApiKey': '',
'GoogleApiKey': '',
'MathpixAppId': '',
'MathpixAppKey': ''
};
@@ -2154,6 +2169,56 @@ class SettingsManager {
});
}
}
/**
* 打开提示词编辑对话框
* @param {string|null} promptId 要编辑的提示词ID为空则表示新建
*/
openPromptDialog(promptId = null) {
// 判断是否为新建提示词
const isNew = !promptId || !this.prompts[promptId];
if (isNew) {
// 新建提示词 - 清空输入框
this.promptIdInput.value = '';
this.promptNameInput.value = '';
this.promptContentInput.value = '';
this.promptDescriptionInput.value = '';
// 设置对话框标题
if (this.promptDialogTitle) {
this.promptDialogTitle.textContent = '新建提示词';
}
// 启用ID输入框
this.promptIdInput.disabled = false;
// 设置保存按钮动作
this.promptSaveBtn.onclick = () => this.savePrompt(true); // 明确传递isNew=true
} else {
// 编辑现有提示词 - 填充现有内容
this.promptIdInput.value = promptId;
this.promptNameInput.value = this.prompts[promptId].name;
this.promptContentInput.value = this.prompts[promptId].content;
this.promptDescriptionInput.value = this.prompts[promptId].description || '';
// 设置对话框标题
if (this.promptDialogTitle) {
this.promptDialogTitle.textContent = '编辑提示词';
}
// 禁用ID输入框不允许修改ID
this.promptIdInput.disabled = true;
// 设置保存按钮动作
this.promptSaveBtn.onclick = () => this.savePrompt(false); // 明确传递isNew=false
}
// 显示对话框
if (this.promptDialogMask) {
this.promptDialogMask.classList.remove('hidden');
}
}
}
// Export for use in other modules

View File

@@ -5284,3 +5284,42 @@ textarea,
.model-dropdown-panel::-webkit-scrollbar {
width: 6px;
}
/* 提示词预览样式 */
.prompt-preview {
position: relative;
cursor: pointer;
}
.prompt-preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.03);
opacity: 0;
transition: opacity 0.2s ease;
}
.prompt-preview:hover .prompt-preview-overlay {
opacity: 1;
}
.prompt-edit-hint {
position: absolute;
right: 10px;
bottom: 10px;
/* 不显示图标,因为上方已有编辑按钮 */
}
[data-theme="dark"] .prompt-preview-overlay {
background-color: rgba(255, 255, 255, 0.05);
}
@media (max-width: 768px) {
.prompt-preview-overlay {
display: none;
}
}

View File

@@ -300,7 +300,7 @@
</div>
<div class="prompt-preview-overlay">
<div class="prompt-edit-hint">
<i class="fas fa-edit"></i>
<!-- 移除重复的编辑图标 -->
</div>
</div>
</div>
@@ -403,6 +403,28 @@
</div>
</div>
</div>
<div class="api-key-status">
<span class="key-name">Google API:</span>
<div class="key-status-wrapper">
<!-- 显示状态 -->
<div class="key-display">
<span id="GoogleApiKeyStatus" class="key-status" data-key="GoogleApiKey">未设置</span>
<button class="btn-icon edit-api-key" data-key-type="GoogleApiKey" title="编辑此密钥">
<i class="fas fa-edit"></i>
</button>
</div>
<!-- 编辑状态 -->
<div class="key-edit hidden">
<input type="password" class="key-input" data-key-type="GoogleApiKey" placeholder="输入 Google API key">
<button class="btn-icon toggle-visibility">
<i class="fas fa-eye"></i>
</button>
<button class="btn-icon save-api-key" data-key-type="GoogleApiKey" title="保存密钥">
<i class="fas fa-save"></i>
</button>
</div>
</div>
</div>
<div class="api-key-status">
<span class="key-name">Mathpix App ID:</span>
<div class="key-status-wrapper">