mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-20 21:57:14 +08:00
Merge branch 'master' of github.com:zhayujie/chatgpt-on-wechat
This commit is contained in:
@@ -6,11 +6,14 @@ Google gemini bot
|
|||||||
"""
|
"""
|
||||||
# encoding:utf-8
|
# encoding:utf-8
|
||||||
|
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
from models.bot import Bot
|
from models.bot import Bot
|
||||||
import google.generativeai as genai
|
|
||||||
from models.session_manager import SessionManager
|
from models.session_manager import SessionManager
|
||||||
from bridge.context import ContextType, Context
|
from bridge.context import ContextType, Context
|
||||||
from bridge.reply import Reply, ReplyType
|
from bridge.reply import Reply, ReplyType
|
||||||
@@ -18,7 +21,6 @@ from common.log import logger
|
|||||||
from config import conf
|
from config import conf
|
||||||
from models.chatgpt.chat_gpt_session import ChatGPTSession
|
from models.chatgpt.chat_gpt_session import ChatGPTSession
|
||||||
from models.baidu.baidu_wenxin_session import BaiduWenxinSession
|
from models.baidu.baidu_wenxin_session import BaiduWenxinSession
|
||||||
from google.generativeai.types import HarmCategory, HarmBlockThreshold
|
|
||||||
|
|
||||||
|
|
||||||
# OpenAI对话模型API (可用)
|
# OpenAI对话模型API (可用)
|
||||||
@@ -43,6 +45,7 @@ class GoogleGeminiBot(Bot):
|
|||||||
self.api_base = "https://generativelanguage.googleapis.com"
|
self.api_base = "https://generativelanguage.googleapis.com"
|
||||||
|
|
||||||
def reply(self, query, context: Context = None) -> Reply:
|
def reply(self, query, context: Context = None) -> Reply:
|
||||||
|
session_id = None
|
||||||
try:
|
try:
|
||||||
if context.type != ContextType.TEXT:
|
if context.type != ContextType.TEXT:
|
||||||
logger.warn(f"[Gemini] Unsupported message type, type={context.type}")
|
logger.warn(f"[Gemini] Unsupported message type, type={context.type}")
|
||||||
@@ -50,35 +53,38 @@ class GoogleGeminiBot(Bot):
|
|||||||
logger.info(f"[Gemini] query={query}")
|
logger.info(f"[Gemini] query={query}")
|
||||||
session_id = context["session_id"]
|
session_id = context["session_id"]
|
||||||
session = self.sessions.session_query(query, session_id)
|
session = self.sessions.session_query(query, session_id)
|
||||||
gemini_messages = self._convert_to_gemini_messages(self.filter_messages(session.messages))
|
filtered_messages = self.filter_messages(session.messages)
|
||||||
logger.debug(f"[Gemini] messages={gemini_messages}")
|
logger.debug(f"[Gemini] messages={filtered_messages}")
|
||||||
genai.configure(api_key=self.api_key)
|
|
||||||
model = genai.GenerativeModel(self.model)
|
|
||||||
|
|
||||||
# 添加安全设置
|
response = self.call_with_tools(
|
||||||
safety_settings = {
|
messages=filtered_messages,
|
||||||
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
|
tools=None,
|
||||||
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
|
stream=False,
|
||||||
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
|
model=self.model
|
||||||
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 生成回复,包含安全设置
|
|
||||||
response = model.generate_content(
|
|
||||||
gemini_messages,
|
|
||||||
safety_settings=safety_settings
|
|
||||||
)
|
)
|
||||||
if response.candidates and response.candidates[0].content:
|
|
||||||
reply_text = response.candidates[0].content.parts[0].text
|
if isinstance(response, dict) and response.get("error"):
|
||||||
|
error_message = response.get("message", "Failed to invoke [Gemini] api!")
|
||||||
|
logger.error(f"[Gemini] API error: {error_message}")
|
||||||
|
self.sessions.session_reply(error_message, session_id)
|
||||||
|
return Reply(ReplyType.ERROR, error_message)
|
||||||
|
|
||||||
|
choices = response.get("choices", []) if isinstance(response, dict) else []
|
||||||
|
if choices and choices[0].get("message"):
|
||||||
|
reply_text = choices[0]["message"].get("content")
|
||||||
|
if reply_text:
|
||||||
logger.info(f"[Gemini] reply={reply_text}")
|
logger.info(f"[Gemini] reply={reply_text}")
|
||||||
self.sessions.session_reply(reply_text, session_id)
|
self.sessions.session_reply(reply_text, session_id)
|
||||||
return Reply(ReplyType.TEXT, reply_text)
|
return Reply(ReplyType.TEXT, reply_text)
|
||||||
else:
|
|
||||||
# 没有有效响应内容,可能内容被屏蔽,输出安全评分
|
|
||||||
logger.warning("[Gemini] No valid response generated. Checking safety ratings.")
|
logger.warning("[Gemini] No valid response generated. Checking safety ratings.")
|
||||||
if hasattr(response, 'candidates') and response.candidates:
|
safety_ratings = response.get("safety_ratings", []) if isinstance(response, dict) else []
|
||||||
for rating in response.candidates[0].safety_ratings:
|
if safety_ratings:
|
||||||
logger.warning(f"Safety rating: {rating.category} - {rating.probability}")
|
for rating in safety_ratings:
|
||||||
|
category = rating.get("category", "UNKNOWN")
|
||||||
|
probability = rating.get("probability", "UNKNOWN")
|
||||||
|
logger.warning(f"[Gemini] Safety rating: {category} - {probability}")
|
||||||
|
|
||||||
error_message = "No valid response generated due to safety constraints."
|
error_message = "No valid response generated due to safety constraints."
|
||||||
self.sessions.session_reply(error_message, session_id)
|
self.sessions.session_reply(error_message, session_id)
|
||||||
return Reply(ReplyType.ERROR, error_message)
|
return Reply(ReplyType.ERROR, error_message)
|
||||||
@@ -86,6 +92,7 @@ class GoogleGeminiBot(Bot):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[Gemini] Error generating response: {str(e)}", exc_info=True)
|
logger.error(f"[Gemini] Error generating response: {str(e)}", exc_info=True)
|
||||||
error_message = "Failed to invoke [Gemini] api!"
|
error_message = "Failed to invoke [Gemini] api!"
|
||||||
|
if session_id:
|
||||||
self.sessions.session_reply(error_message, session_id)
|
self.sessions.session_reply(error_message, session_id)
|
||||||
return Reply(ReplyType.ERROR, error_message)
|
return Reply(ReplyType.ERROR, error_message)
|
||||||
|
|
||||||
@@ -127,6 +134,93 @@ class GoogleGeminiBot(Bot):
|
|||||||
turn = "user"
|
turn = "user"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_image_paths_from_text(content: str):
|
||||||
|
if not isinstance(content, str):
|
||||||
|
return "", []
|
||||||
|
pattern = r"\[图片:\s*([^\]]+)\]"
|
||||||
|
image_paths = [m.strip().strip("'\"") for m in re.findall(pattern, content) if m.strip()]
|
||||||
|
cleaned_text = re.sub(pattern, "", content)
|
||||||
|
cleaned_text = re.sub(r"\n{3,}", "\n\n", cleaned_text).strip()
|
||||||
|
return cleaned_text, image_paths
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_image_inline_part(image_path: str):
|
||||||
|
if not image_path:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if image_path.startswith("file://"):
|
||||||
|
image_path = image_path[7:]
|
||||||
|
|
||||||
|
image_path = os.path.expanduser(image_path)
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
logger.warning(f"[Gemini] Image file not found: {image_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
|
||||||
|
mime_type = mimetypes.guess_type(image_path)[0] or "image/png"
|
||||||
|
if not mime_type.startswith("image/"):
|
||||||
|
mime_type = "image/png"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"inlineData": {
|
||||||
|
"mimeType": mime_type,
|
||||||
|
"data": base64.b64encode(image_bytes).decode("utf-8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[Gemini] Failed to build inline image part from path={image_path}, err={e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_inline_part_from_image_url(image_url):
|
||||||
|
if not image_url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(image_url, dict):
|
||||||
|
image_url = image_url.get("url")
|
||||||
|
if not image_url or not isinstance(image_url, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if image_url.startswith("data:"):
|
||||||
|
match = re.match(r"^data:([^;]+);base64,(.+)$", image_url, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
logger.warning("[Gemini] Invalid data URL for image block")
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"inlineData": {
|
||||||
|
"mimeType": match.group(1),
|
||||||
|
"data": match.group(2).strip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if image_url.startswith("file://") or os.path.exists(os.path.expanduser(image_url)):
|
||||||
|
return GoogleGeminiBot._build_image_inline_part(image_url)
|
||||||
|
|
||||||
|
if image_url.startswith("http://") or image_url.startswith("https://"):
|
||||||
|
try:
|
||||||
|
response = requests.get(image_url, timeout=20)
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.warning(f"[Gemini] Failed to fetch remote image: status={response.status_code}, url={image_url}")
|
||||||
|
return None
|
||||||
|
mime_type = response.headers.get("Content-Type", "image/png").split(";")[0].strip()
|
||||||
|
if not mime_type.startswith("image/"):
|
||||||
|
mime_type = "image/png"
|
||||||
|
return {
|
||||||
|
"inlineData": {
|
||||||
|
"mimeType": mime_type,
|
||||||
|
"data": base64.b64encode(response.content).decode("utf-8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[Gemini] Failed to download remote image: url={image_url}, err={e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.warning(f"[Gemini] Unsupported image URL format: {image_url[:120]}")
|
||||||
|
return None
|
||||||
|
|
||||||
def call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
def call_with_tools(self, messages, tools=None, stream=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Call Gemini API with tool support using REST API (following official docs)
|
Call Gemini API with tool support using REST API (following official docs)
|
||||||
@@ -145,6 +239,15 @@ class GoogleGeminiBot(Bot):
|
|||||||
|
|
||||||
# Build REST API payload
|
# Build REST API payload
|
||||||
payload = {"contents": []}
|
payload = {"contents": []}
|
||||||
|
inline_image_count = 0
|
||||||
|
|
||||||
|
# Keep legacy behavior: disable Gemini safety blocking like old SDK path.
|
||||||
|
payload["safetySettings"] = [
|
||||||
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
|
||||||
|
]
|
||||||
|
|
||||||
# Extract and set system instruction
|
# Extract and set system instruction
|
||||||
system_prompt = kwargs.get("system", "")
|
system_prompt = kwargs.get("system", "")
|
||||||
@@ -174,7 +277,18 @@ class GoogleGeminiBot(Bot):
|
|||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
# Simple text content
|
# Text with optional [图片: /path/to/file] markers
|
||||||
|
cleaned_text, image_paths = self._extract_image_paths_from_text(content)
|
||||||
|
if cleaned_text:
|
||||||
|
parts.append({"text": cleaned_text})
|
||||||
|
image_added = False
|
||||||
|
for image_path in image_paths:
|
||||||
|
image_part = self._build_image_inline_part(image_path)
|
||||||
|
if image_part:
|
||||||
|
parts.append(image_part)
|
||||||
|
image_added = True
|
||||||
|
inline_image_count += 1
|
||||||
|
if not cleaned_text and not image_added and content:
|
||||||
parts.append({"text": content})
|
parts.append({"text": content})
|
||||||
|
|
||||||
elif isinstance(content, list):
|
elif isinstance(content, list):
|
||||||
@@ -188,8 +302,39 @@ class GoogleGeminiBot(Bot):
|
|||||||
block_type = block.get("type")
|
block_type = block.get("type")
|
||||||
|
|
||||||
if block_type == "text":
|
if block_type == "text":
|
||||||
# Text block
|
# Text block with optional image markers
|
||||||
parts.append({"text": block.get("text", "")})
|
block_text = block.get("text", "")
|
||||||
|
cleaned_text, image_paths = self._extract_image_paths_from_text(block_text)
|
||||||
|
if cleaned_text:
|
||||||
|
parts.append({"text": cleaned_text})
|
||||||
|
for image_path in image_paths:
|
||||||
|
image_part = self._build_image_inline_part(image_path)
|
||||||
|
if image_part:
|
||||||
|
parts.append(image_part)
|
||||||
|
|
||||||
|
elif block_type in ["image", "image_url"]:
|
||||||
|
# OpenAI format: {"type":"image_url","image_url":{"url":"..."}}
|
||||||
|
# Claude format: {"type":"image","source":{"type":"base64","media_type":"...","data":"..."}}
|
||||||
|
image_part = None
|
||||||
|
if block_type == "image":
|
||||||
|
source = block.get("source", {})
|
||||||
|
if isinstance(source, dict) and source.get("type") == "base64" and source.get("data"):
|
||||||
|
image_part = {
|
||||||
|
"inlineData": {
|
||||||
|
"mimeType": source.get("media_type", "image/png"),
|
||||||
|
"data": source.get("data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif block.get("image_url"):
|
||||||
|
image_part = self._build_inline_part_from_image_url(block.get("image_url"))
|
||||||
|
else:
|
||||||
|
image_part = self._build_inline_part_from_image_url(block.get("image_url"))
|
||||||
|
|
||||||
|
if image_part:
|
||||||
|
parts.append(image_part)
|
||||||
|
inline_image_count += 1
|
||||||
|
else:
|
||||||
|
logger.warning(f"[Gemini] Skip invalid image block: {str(block)[:200]}")
|
||||||
|
|
||||||
elif block_type == "tool_result":
|
elif block_type == "tool_result":
|
||||||
# Convert Claude tool_result to Gemini functionResponse
|
# Convert Claude tool_result to Gemini functionResponse
|
||||||
@@ -238,6 +383,9 @@ class GoogleGeminiBot(Bot):
|
|||||||
"parts": parts
|
"parts": parts
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if inline_image_count > 0:
|
||||||
|
logger.info(f"[Gemini] Multimodal request includes {inline_image_count} image part(s)")
|
||||||
|
|
||||||
# Generation config
|
# Generation config
|
||||||
gen_config = {}
|
gen_config = {}
|
||||||
if kwargs.get("temperature") is not None:
|
if kwargs.get("temperature") is not None:
|
||||||
@@ -363,15 +511,18 @@ class GoogleGeminiBot(Bot):
|
|||||||
candidates = data.get("candidates", [])
|
candidates = data.get("candidates", [])
|
||||||
if not candidates:
|
if not candidates:
|
||||||
logger.warning("[Gemini] No candidates in response")
|
logger.warning("[Gemini] No candidates in response")
|
||||||
|
prompt_feedback = data.get("promptFeedback", {})
|
||||||
return {
|
return {
|
||||||
"error": True,
|
"error": True,
|
||||||
"message": "No candidates in response",
|
"message": "No candidates in response",
|
||||||
"status_code": 500
|
"status_code": 500,
|
||||||
|
"safety_ratings": prompt_feedback.get("safetyRatings", [])
|
||||||
}
|
}
|
||||||
|
|
||||||
candidate = candidates[0]
|
candidate = candidates[0]
|
||||||
content = candidate.get("content", {})
|
content = candidate.get("content", {})
|
||||||
parts = content.get("parts", [])
|
parts = content.get("parts", [])
|
||||||
|
safety_ratings = candidate.get("safetyRatings", [])
|
||||||
|
|
||||||
logger.debug(f"[Gemini] Candidate parts count: {len(parts)}")
|
logger.debug(f"[Gemini] Candidate parts count: {len(parts)}")
|
||||||
|
|
||||||
@@ -419,7 +570,8 @@ class GoogleGeminiBot(Bot):
|
|||||||
"message": message_dict,
|
"message": message_dict,
|
||||||
"finish_reason": "tool_calls" if tool_calls else "stop"
|
"finish_reason": "tool_calls" if tool_calls else "stop"
|
||||||
}],
|
}],
|
||||||
"usage": data.get("usageMetadata", {})
|
"usage": data.get("usageMetadata", {}),
|
||||||
|
"safety_ratings": safety_ratings
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user