From 5660bf99e993f5ab0b85fa96d0c00b65f7226afc Mon Sep 17 00:00:00 2001 From: 27942 Date: Tue, 16 Dec 2025 10:22:15 +0800 Subject: [PATCH] ggrg --- ai_main/chat_history.db | Bin 0 -> 53248 bytes ai_main/main.py | 492 ++++++++++++++++++++++++++++++++++++++++ ai_main/my_chats.db | Bin 0 -> 94208 bytes 3 files changed, 492 insertions(+) create mode 100644 ai_main/chat_history.db create mode 100644 ai_main/main.py create mode 100644 ai_main/my_chats.db diff --git a/ai_main/chat_history.db b/ai_main/chat_history.db new file mode 100644 index 0000000000000000000000000000000000000000..3049b9d333ecd864a08590b2ecc85c58828cdcd1 GIT binary patch literal 53248 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 literal 0 HcmV?d00001 diff --git a/ai_main/main.py b/ai_main/main.py new file mode 100644 index 000000000..9c9e9f4d9 --- /dev/null +++ b/ai_main/main.py @@ -0,0 +1,492 @@ +import json +import os +import uuid +from datetime import datetime +from typing import List, Dict, Optional, Any, Union + +from peewee import ( + Model, CharField, TextField, IntegerField, + DateTimeField, BooleanField, ForeignKeyField, + fn, SqliteDatabase +) +from playhouse.sqlite_ext import SqliteExtDatabase +from openai import OpenAI + + +# ==================== 数据库配置 ==================== +class DatabaseManager: + """数据库管理器""" + + def __init__(self, db_path: str = 'chat_sessions.db'): + self.db = SqliteExtDatabase( + db_path, + pragmas={ + 'journal_mode': 'wal', + 'cache_size': -1024 * 64, + 'foreign_keys': 1, + 'ignore_check_constraints': 0, + 'synchronous': 1 + } + ) + self.init_models() + + def init_models(self): + """初始化数据模型""" + + class BaseModel(Model): + class Meta: + database = self.db + + class Session(BaseModel): + """聊天会话模型""" + name = CharField(max_length=255, unique=True) # 会话名称唯一 + model = CharField(max_length=50, default='deepseek-chat') + system_prompt = TextField(default='你是一个乐于助人的助手。') + created_at = DateTimeField(default=datetime.now) + updated_at = DateTimeField(default=datetime.now) + is_active = BooleanField(default=True) + metadata_json = TextField(default='{}') + + @property + def metadata(self) -> Dict: + return json.loads(self.metadata_json) if self.metadata_json else {} + + @metadata.setter + def metadata(self, value: Dict): + self.metadata_json = json.dumps(value, ensure_ascii=False) + + def update_timestamp(self): + self.updated_at = datetime.now() + self.save() + + class Message(BaseModel): + """聊天消息模型""" + session = ForeignKeyField(Session, backref='messages', on_delete='CASCADE') + role = CharField(max_length=20, index=True) + content = TextField() + tokens = IntegerField(default=0) + timestamp = DateTimeField(default=datetime.now, index=True) + metadata_json = TextField(default='{}') + + @property + def metadata(self) -> Dict: + return json.loads(self.metadata_json) if self.metadata_json else {} + + @metadata.setter + def metadata(self, value: Dict): + self.metadata_json = json.dumps(value, ensure_ascii=False) + + class Meta: + indexes = ( + (('session', 'timestamp'), False), + ) + + # 保存模型类 + self.Session = Session + self.Message = Message + + # 创建表 + self.db.connect() + self.db.create_tables([Session, Message], safe=True) + + def close(self): + """关闭数据库连接""" + if not self.db.is_closed(): + self.db.close() + + +# ==================== 会话管理器 ==================== +class SessionManager: + """会话管理器""" + + def __init__(self, db_path: str = None): + self.db_manager = DatabaseManager(db_path or 'chat_sessions.db') + self.Session = self.db_manager.Session + self.Message = self.db_manager.Message + + def get_or_create_session(self, name: str, **kwargs) -> Any: + """获取或创建会话""" + # 尝试获取现有会话 + session = self.Session.get_or_none(self.Session.name == name) + + if session: + return session + else: + # 创建新会话 + defaults = { + 'model': kwargs.get('model', 'deepseek-chat'), + 'system_prompt': kwargs.get('system_prompt', '你是一个乐于助人的助手。'), + 'metadata': kwargs.get('metadata', {}) + } + + session = self.Session.create( + name=name, + **defaults + ) + + # 添加系统提示作为第一条消息 + if defaults['system_prompt']: + self.Message.create( + session=session, + role='system', + content=defaults['system_prompt'] + ) + + return session + + def add_message(self, session_name: str, role: str, content: str, + tokens: int = 0, metadata: Dict = None) -> str: + """添加消息到会话""" + session = self.get_or_create_session(session_name) + + self.Message.create( + session=session, + role=role, + content=content, + tokens=tokens, + timestamp=datetime.now(), + metadata=metadata or {} + ) + + # 更新会话时间戳 + session.update_timestamp() + + return content + + def get_session_history(self, session_name: str, limit: int = 20) -> List[Dict]: + """获取会话历史""" + session = self.Session.get_or_none(self.Session.name == session_name) + if not session: + return [] + + messages = (self.Message + .select() + .where(self.Message.session == session) + .order_by(self.Message.timestamp.asc()) + .limit(limit)) + + history = [] + for msg in messages: + history.append({ + 'role': msg.role, + 'content': msg.content + }) + + return history + + def get_session_info(self, session_name: str) -> Dict: + """获取会话信息""" + session = self.Session.get_or_none(self.Session.name == session_name) + if not session: + return {} + + messages = session.messages + message_count = messages.count() + user_messages = messages.where(self.Message.role == 'user').count() + assistant_messages = messages.where(self.Message.role == 'assistant').count() + + return { + 'name': session.name, + 'model': session.model, + 'system_prompt': session.system_prompt, + 'message_count': message_count, + 'user_messages': user_messages, + 'assistant_messages': assistant_messages, + 'created_at': session.created_at.isoformat(), + 'updated_at': session.updated_at.isoformat(), + 'metadata': session.metadata + } + + def list_sessions(self) -> List[Dict]: + """列出所有会话""" + sessions = self.Session.select().order_by(self.Session.updated_at.desc()) + + result = [] + for session in sessions: + message_count = session.messages.count() + result.append({ + 'name': session.name, + 'message_count': message_count, + 'updated_at': session.updated_at.isoformat() + }) + + return result + + def close(self): + """关闭连接""" + self.db_manager.close() + + +# ==================== 简化的客户端接口 ==================== +class DeepSeekChatClient: + """简化的DeepSeek聊天客户端""" + + def __init__(self, api_key: str, db_path: str = None): + """ + 初始化客户端 + + Args: + api_key: DeepSeek API密钥 + db_path: 数据库路径,默认为chat_sessions.db + """ + self.client = OpenAI(api_key=api_key, base_url='https://api.deepseek.com') + self.session_manager = SessionManager(db_path) + + # 缓存已加载的会话配置 + self._session_cache = {} + + def chat(self, user_input: str, name: str, model: str = None, + system_prompt: str = None, **kwargs) -> str: + """ + 发送聊天消息 + + Args: + user_input: 用户输入的问题 + name: 会话名称 + model: 模型名称(仅在创建新会话时使用) + system_prompt: 系统提示(仅在创建新会话时使用) + **kwargs: 其他OpenAI API参数 + + Returns: + AI的回复内容 + """ + # 获取或创建会话 + session = self.session_manager.get_or_create_session( + name=name, + model=model or 'deepseek-chat', + system_prompt=system_prompt + ) + + # 保存用户消息 + self.session_manager.add_message( + session_name=name, + role='user', + content=user_input, + tokens=len(user_input) * 0.8 + ) + + # 获取会话历史 + history = self.session_manager.get_session_history(name, limit=10) + + # 构建消息列表 + messages = [] + + # 添加系统提示(如果历史中没有) + if not any(msg['role'] == 'system' for msg in history): + messages.append({ + 'role': 'system', + 'content': session.system_prompt + }) + + # 添加上下文历史 + for msg in history: + messages.append({ + 'role': msg['role'], + 'content': msg['content'] + }) + + # 添加当前用户消息 + messages.append({ + 'role': 'user', + 'content': user_input + }) + + # 调用API + try: + response = self.client.chat.completions.create( + model=session.model, + messages=messages, + max_tokens=kwargs.get('max_tokens', 2000), + temperature=kwargs.get('temperature', 0.7), + stream=kwargs.get('stream', False) + ) + + # 处理响应 + ai_reply = response.choices[0].message.content + + # 保存AI回复 + self.session_manager.add_message( + session_name=name, + role='assistant', + content=ai_reply, + tokens=len(ai_reply) * 0.8 + ) + + return ai_reply + + except Exception as e: + error_msg = f"API调用错误: {str(e)}" + print(error_msg) + + # 保存错误信息 + self.session_manager.add_message( + session_name=name, + role='system', + content=error_msg, + metadata={'error': True, 'error_type': type(e).__name__} + ) + + return "" + + def get_session_info(self, name: str) -> Dict: + """获取会话信息""" + return self.session_manager.get_session_info(name) + + def list_sessions(self) -> List[Dict]: + """列出所有会话""" + return self.session_manager.list_sessions() + + def export_session(self, name: str) -> Dict: + """导出会话数据""" + info = self.get_session_info(name) + history = self.session_manager.get_session_history(name, limit=100) + + return { + 'session_info': info, + 'history': history, + 'exported_at': datetime.now().isoformat() + } + + def clear_session_history(self, name: str, keep_system: bool = True) -> bool: + """清空会话历史(保留系统提示)""" + session = self.session_manager.Session.get_or_none( + self.session_manager.Session.name == name + ) + + if not session: + return False + + # 删除消息 + query = session.messages + if keep_system: + query = query.where(self.session_manager.Message.role != 'system') + + query.delete().execute() + + # 重新添加系统提示(如果需要) + if keep_system and session.system_prompt: + messages = session.messages.where( + self.session_manager.Message.role == 'system' + ) + if not messages.exists(): + self.session_manager.add_message( + session_name=name, + role='system', + content=session.system_prompt + ) + + return True + + def close(self): + """关闭客户端""" + self.session_manager.close() + + +# ==================== 使用示例 ==================== +def main(): + # 初始化客户端(请使用环境变量管理API密钥) + API_KEY = "sk-bab97a2b9be042e18d945394f8feefa3" + + client = None + try: + # 创建客户端 + client = DeepSeekChatClient(api_key=API_KEY, db_path='my_chats.db') + print("✅ 客户端初始化成功!") + + # ========== 第一次调用:创建会话 ========== + print("\n=== 第一次调用:创建技术讨论会话 ===") + res1 = client.chat( + name='技术讨论', + model='deepseek-chat', + system_prompt='你是一个技术专家,请提供详细的解释和代码示例。', + user_input='请解释Python中的装饰器。' + ) + print("回答:", res1[:100] + "..." if len(res1) > 100 else res1) + + # ========== 第二次调用:使用现有会话 ========== + print("\n=== 第二次调用:使用现有技术讨论会话 ===") + res2 = client.chat( + name='技术讨论', # 使用相同名称 + user_input='请给我一个装饰器的实际应用例子。' # 只需要用户输入 + ) + print("回答:", res2[:100] + "..." if len(res2) > 100 else res2) + + # ========== 第三次调用:创建新类型会话 ========== + print("\n=== 第三次调用:创建创意写作会话 ===") + res3 = client.chat( + name='创意写作', + model='deepseek-chat', + system_prompt='你是一个创意作家,请提供富有想象力的回答。', + user_input='写一个关于人工智能的短故事开头' + ) + print("回答:", res3[:100] + "..." if len(res3) > 100 else res3) + + # ========== 查看会话信息 ========== + print("\n=== 查看技术讨论会话信息 ===") + session_info = client.get_session_info('技术讨论') + print(f"会话名称: {session_info['name']}") + print(f"模型: {session_info['model']}") + print(f"消息总数: {session_info['message_count']}") + print(f"用户消息: {session_info['user_messages']}") + print(f"助手消息: {session_info['assistant_messages']}") + + # ========== 列出所有会话 ========== + print("\n=== 所有会话列表 ===") + sessions = client.list_sessions() + for session in sessions: + print(f" {session['name']}: {session['message_count']} 条消息") + + # ========== 导出会话数据 ========== + print("\n=== 导出技术讨论会话 ===") + export_data = client.export_session('技术讨论') + print(f"导出了 {len(export_data['history'])} 条消息") + + print("\n🎉 测试完成!") + + except Exception as e: + print(f"❌ 程序运行出错: {e}") + import traceback + traceback.print_exc() + finally: + # 确保关闭连接 + if client: + client.close() + + +# ==================== 简洁的使用示例 ==================== +def simple_usage(): + """简洁的使用示例""" + API_KEY = "sk-bab97a2b9be042e18d945394f8feefa3" + + # 1. 创建客户端 + client = DeepSeekChatClient(api_key=API_KEY) + + # 2. 创建新会话(第一次调用) + response1 = client.chat( + name='数学辅导', + model='deepseek-chat', + system_prompt='你是一个数学老师,请用简单易懂的方式解释数学概念。', + user_input='什么是微积分?' + ) + print("第一次回答:", response1[:100]) + + # 3. 继续对话(只需要会话名称和用户输入) + response2 = client.chat( + name='数学辅导', + user_input='微积分有哪些实际应用?' + ) + print("第二次回答:", response2[:100]) + + # 4. 查看会话信息 + info = client.get_session_info('数学辅导') + print(f"会话 '{info['name']}' 有 {info['message_count']} 条消息") + + client.close() + + +if __name__ == '__main__': + # 运行完整示例 + main() + + # 或运行简洁示例 + # simple_usage() \ No newline at end of file diff --git a/ai_main/my_chats.db b/ai_main/my_chats.db new file mode 100644 index 0000000000000000000000000000000000000000..09f0cc924497bea4d3231af48b3c018a7a72b849 GIT binary patch literal 94208 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 literal 0 HcmV?d00001