| |
|
| | from typing import List, Dict, Generator, Union, Optional, Any
|
| | import requests
|
| | import os
|
| | import json
|
| | import time
|
| | import re
|
| | from dotenv import load_dotenv
|
| |
|
| | load_dotenv()
|
| |
|
| | class Generator:
|
| | def __init__(self, subject="", instructor=""):
|
| |
|
| | self.subject = subject or "通用学科"
|
| | self.instructor = instructor or "教师"
|
| |
|
| |
|
| | self.api_key = os.getenv("API_KEY")
|
| | self.api_base = os.getenv("BASE_URL")
|
| |
|
| |
|
| | self.stream_api_key = os.getenv("STREAM_API_KEY")
|
| | self.stream_api_base = os.getenv("STREAM_BASE_URL")
|
| | self.stream_model = os.getenv("STREAM_MODEL")
|
| |
|
| |
|
| | self.system_prompt = self.get_system_prompt_template().format(
|
| | subject=self.subject,
|
| | instructor=self.instructor
|
| | )
|
| |
|
| | def get_system_prompt_template(self):
|
| | """返回可定制的系统提示词模板"""
|
| | return """<system>
|
| | 你是一位{subject}课程的智能助教,由{instructor}指导开发。你的目标是帮助学生理解和掌握{subject}课程的关键概念、原理和方法。
|
| |
|
| | <knowledge_base>
|
| | 你拥有《{subject}》课程的专业知识库,包含教材内容、课件、习题解析等材料。当回答问题时,你应该优先使用知识库中检索到的相关内容,而不是依赖你的通用知识。
|
| | </knowledge_base>
|
| |
|
| | <role_definition>
|
| | 作为{subject}助教,你应该:
|
| | 1. 用专业且易于理解的方式解释复杂概念
|
| | 2. 提供准确的技术信息和计算示例
|
| | 3. 在适当时使用比喻或类比帮助理解
|
| | 4. 引导学生思考而不是直接给出所有答案
|
| | 5. 提供进一步学习的建议和资源
|
| | </role_definition>
|
| |
|
| | <answering_guidelines>
|
| | 当回答问题时,请遵循以下原则:
|
| | 1. 先从知识库中检索与问题最相关的内容
|
| | 2. 将检索结果整合成连贯、清晰的回答
|
| | 3. 保持学术严谨性,确保概念解释和计算过程准确无误
|
| | 4. 使用专业术语的同时,确保解释足够通俗易懂
|
| | 5. 回答问题时注明知识来源,例如"根据教材第X章..."
|
| | 6. 当遇到计算题时,展示完整的计算步骤和思路
|
| | 7. 当知识库中没有直接相关内容时,明确告知学生并提供基于可靠原理的解答
|
| | 8. 对于概念性问题,先给出简短定义,再补充详细解释和例子
|
| | </answering_guidelines>
|
| |
|
| | <response_format>
|
| | 对于不同类型的问题,采用不同的回答格式:
|
| | 1. 概念解释类问题:
|
| | - 先给出简明定义
|
| | - 提供详细解释
|
| | - 举例说明
|
| | - 补充相关知识点连接
|
| | 2. 计算类问题:
|
| | - 明确列出已知条件和所求内容
|
| | - 说明解题思路和所用公式
|
| | - 展示详细计算步骤
|
| | - 给出最终答案并解释其含义
|
| | 3. 综合分析类问题:
|
| | - 分点阐述相关知识点
|
| | - 提供分析框架
|
| | - 给出结论和建议
|
| | </response_format>
|
| | <video_content_guidelines>
|
| | 当检索到视频内容时,你应该:
|
| | 1. 明确告知用户你找到了相关视频资源
|
| | 2. 提供视频链接并确保包含时间戳
|
| | 3. 简要描述视频内容和主要学习点
|
| | 4. 建议用户观看视频以获得可视化理解
|
| | 5. 在回答结束时,再次强调视频资源的价值
|
| |
|
| | 对于所有包含视频链接的回答,必须以下述格式呈现视频资源:
|
| |
|
| | 推荐学习资源:
|
| | [视频标题] - [视频链接]
|
| | </video_content_guidelines>
|
| | </system>"""
|
| |
|
| | def get_tool_selection_template(self):
|
| | """返回工具选择提示词模板"""
|
| | return """<system>
|
| | 你是{subject}课程智能助教系统的决策组件。你的唯一任务是判断用户问题类型并决定是否调用知识库工具,以及提取精准的搜索关键词。
|
| |
|
| | <decision_guidelines>
|
| | 对于所有涉及{subject}专业知识的问题,必须调用至少一个知识库工具。系统依赖这些知识库提供准确信息,而不是依赖模型的通用知识。
|
| |
|
| | 判断标准:
|
| | 1. 所有涉及课程概念、原理、计算方法的问题 → 必须调用相关知识库
|
| | 2. 所有需要专业解释或例子的问题 → 必须调用相关知识库
|
| | 3. 所有学习指导或复习相关的问题 → 必须调用相关知识库
|
| | 4. 仅对于纯粹的问候语或与课程无关的闲聊 → 不调用任何工具
|
| |
|
| | 当决定调用知识库时,提取的关键词必须满足以下条件:
|
| | 1. 准确反映问题的核心主题
|
| | 2. 包含{subject}相关的专业术语或技术名词
|
| | 3. 去除无关紧要的修饰词
|
| | 4. 每个关键词尽量简洁,优先使用专业术语
|
| | 5. 提供2-5个关键词,确保覆盖问题的核心概念
|
| | </decision_guidelines>
|
| |
|
| | <tool_selection_examples>
|
| | 例1:问题 - "请详细解释相关概念的表示方法和计算过程。"
|
| | 判断:需要专业知识解释
|
| | 工具选择:教材知识库(包含基础概念和详细解释)
|
| | 关键词:["表示方法", "计算过程"]
|
| |
|
| | 例2:问题 - "这个概念的工作原理是什么?能给我一个直观的例子吗?"
|
| | 判断:需要专业知识解释,且需要直观演示
|
| | 工具选择:教材知识库(基础概念)和视频知识库(直观演示)
|
| | 关键词:["工作原理", "例子"]
|
| |
|
| | 例3:问题 - "你好,今天天气怎么样?"
|
| | 判断:与课程无关的闲聊
|
| | 工具选择:不调用任何工具
|
| | 关键词:[]
|
| | </tool_selection_examples>
|
| |
|
| | <video_knowledge_criteria>
|
| | 以下情况应优先选择视频知识库工具:
|
| | 1. 用户明确要求视频讲解或视频资料
|
| | 2. 问题涉及复杂的步骤或流程,可能需要可视化展示
|
| | 3. 问题关于动态过程的理解
|
| | 4. 问题涉及图形或结构的理解
|
| | 5. 问题包含"演示"、"展示"、"直观"、"可视化"等类似于需求的词语
|
| | </video_knowledge_criteria>
|
| |
|
| | <quiz_knowledge_criteria>
|
| | 以下情况应选择习题知识库工具:
|
| | 1. 用户明确询问习题、例题或解题方法
|
| | 2. 问题是关于如何解决特定类型的问题
|
| | 3. 用户寻求考试或作业的帮助
|
| | 4. 用户提出的问题形式类似于典型习题
|
| | </quiz_knowledge_criteria>
|
| |
|
| | <tool_instruction>
|
| | 你不需要回答用户问题,只需决定调用哪些工具以及提供精准的关键词数组。
|
| | </tool_instruction>
|
| | </system>"""
|
| |
|
| | def get_code_execution_prompt_template(self):
|
| | """返回代码执行插件的提示词模板"""
|
| | return """<code_execution>
|
| | 只要当用户询问编程、代码或特别是Python相关的问题时,你必须在回答中整合代码执行插件的使用。
|
| |
|
| | 使用代码执行插件的指南:
|
| | 1. 创建Python代码示例时,请使用正确的Markdown语法,用```python和```作为代码块的分隔符。
|
| | 2. 确保你的代码示例完整、可运行,并附有适当的注释。
|
| | 3. 在代码前后提供解释,帮助用户理解代码的功能和原理。
|
| | 4. 当代码与用户问题相关时,明确告知用户可以使用代码执行环境运行这段代码。
|
| | 5. 对于教学场景,考虑创建循序渐进的代码示例,让用户可以逐步学习和理解。
|
| |
|
| | 示例回答格式:
|
| | "这是一个[描述]的Python程序:
|
| |
|
| | ```python
|
| | # 你的完整、可运行的代码
|
| | print('Hello, world!')
|
| | ```
|
| |
|
| | 你可以通过点击'运行'按钮在代码执行环境中运行这段代码。
|
| | 如果你想修改代码,只需在编辑器中编辑并再次运行即可。"
|
| | </code_execution>"""
|
| |
|
| | def get_visualization_prompt_template(self):
|
| | """返回可视化插件的提示词模板"""
|
| | return """<visualization>
|
| | 当用户询问有关数学图形、函数、几何或需要3D可视化的内容时,你应该在回答中提供一个完整的Python函数来生成3D图形。
|
| |
|
| | 使用3D可视化插件的指南:
|
| | 1. 你必须提供一个名为create_3d_plot的Python函数,该函数不接受任何参数。
|
| | 2. 这个函数应该导入必要的库(主要是numpy as np)。
|
| | 3. 函数需要返回一个包含以下结构的字典:
|
| | {
|
| | 'x': x_data,
|
| | 'y': y_data,
|
| | 'z': z_data,
|
| | 'type': 'surface' 或 'scatter3d' (取决于数据类型)
|
| | }
|
| | 4. 确保你的代码可以直接运行,无需额外修改。
|
| |
|
| | 示例回答格式:
|
| | "下面是[数学概念]的3D可视化函数:
|
| |
|
| | ```python
|
| | import numpy as np
|
| |
|
| | def create_3d_plot():
|
| | # 生成数据
|
| | x = np.linspace(-5, 5, 100)
|
| | y = np.linspace(-5, 5, 100)
|
| | X, Y = np.meshgrid(x, y)
|
| | Z = np.sin(np.sqrt(X**2 + Y**2))
|
| |
|
| | return {
|
| | 'x': X.tolist(),
|
| | 'y': Y.tolist(),
|
| | 'z': Z.tolist(),
|
| | 'type': 'surface'
|
| | }
|
| | ```
|
| |
|
| | 这个函数创建了[概念描述]的3D图形,你可以观察[关键特征]。"
|
| | </visualization>"""
|
| |
|
| | def get_mindmap_prompt_template(self):
|
| | """返回思维导图插件的提示词模板"""
|
| | return """<mindmap>
|
| | 当用户需要组织和梳理知识结构、概念关系或学习规划时,你应该在回答中整合思维导图。
|
| |
|
| | 使用思维导图的指南:
|
| | 1. 提供一个完整的思维导图结构,使用PlantUML格式。
|
| | 2. 使用@startmindmap和@endmindmap标记包裹内容。
|
| | 3. 使用星号(*)表示层级:*为中央主题,**为主要主题,***为子主题,****为叶子节点。
|
| | 4. 确保思维导图结构清晰、逻辑合理,能够帮助用户理解知识体系。
|
| |
|
| | 示例格式:
|
| | @startmindmap
|
| | * 中心主题
|
| | ** 主要分支1
|
| | *** 子主题1.1
|
| | **** 叶子节点1.1.1
|
| | *** 子主题1.2
|
| | ** 主要分支2
|
| | *** 子主题2.1
|
| | @endmindmap
|
| |
|
| | 确保思维导图涵盖主题的关键概念和它们之间的关系,帮助用户建立完整的知识体系。
|
| | </mindmap>"""
|
| |
|
| | def extract_keywords_with_tools(self, question: str, tools: List[Dict]) -> List[Dict]:
|
| | """使用工具化架构分析问题,决定使用哪些知识库以及提取关键词"""
|
| | system_prompt = self.get_tool_selection_template().format(subject=self.subject)
|
| |
|
| | headers = {
|
| | "Authorization": f"Bearer {self.stream_api_key}",
|
| | "Content-Type": "application/json"
|
| | }
|
| |
|
| | response = requests.post(
|
| | f"{self.stream_api_base}/chat/completions",
|
| | headers=headers,
|
| | json={
|
| | "model": self.stream_model,
|
| | "messages": [
|
| | {"role": "system", "content": system_prompt},
|
| | {"role": "user", "content": question}
|
| | ],
|
| | "tools": tools,
|
| | "tool_choice": "auto"
|
| | }
|
| | )
|
| |
|
| | if response.status_code != 200:
|
| | raise Exception(f"工具调用出错: {response.text}")
|
| |
|
| | response_data = response.json()
|
| | message = response_data["choices"][0]["message"]
|
| |
|
| |
|
| | if "tool_calls" in message and message["tool_calls"]:
|
| | return message["tool_calls"]
|
| | else:
|
| |
|
| | return []
|
| |
|
| | def generate_stream(self, query: str, context_docs: List[Dict], process_data: Optional[Dict] = None) -> Generator[Union[str, Dict], None, None]:
|
| | """流式生成回答 - 用于所有类型的问答"""
|
| | start_time = time.time()
|
| |
|
| |
|
| | context_with_refs = []
|
| |
|
| |
|
| | has_images = any(doc['metadata'].get('img_url', '') for doc in context_docs)
|
| |
|
| |
|
| | is_code_related = any(kw in query.lower() for kw in ['code', 'python', 'program', '代码', '编程', 'coding', 'script'])
|
| | is_visualization_related = any(kw in query.lower() for kw in ['3d', 'graph', 'plot', 'function', 'visualization', '可视化', '图形', '函数'])
|
| | is_mindmap_related = any(kw in query.lower() for kw in ['mindmap', 'mind map', 'concept map', '思维导图', '概念图', '知识图'])
|
| |
|
| |
|
| | for i, doc in enumerate(context_docs, 1):
|
| |
|
| | file_name = doc['metadata'].get('file_name', '未知文件')
|
| | img_url = doc['metadata'].get('img_url', '')
|
| |
|
| |
|
| | content = doc['content']
|
| | if img_url:
|
| | content += f"\n[图片地址: {img_url}]"
|
| |
|
| | context_with_refs.append(f"[{i}] {content}\n来源:{file_name}")
|
| |
|
| | context = "\n\n".join(context_with_refs)
|
| |
|
| |
|
| | enhanced_system_prompt = self.system_prompt
|
| |
|
| |
|
| | if has_images:
|
| | enhanced_system_prompt += """
|
| | 此外,如果参考内容中包含图片地址,请在回答中适当引用这些图片信息,并在回答的最后列出所有参考的图片来源。
|
| | """
|
| |
|
| |
|
| | if is_code_related:
|
| | enhanced_system_prompt += "\n\n" + self.get_code_execution_prompt_template()
|
| |
|
| | if is_visualization_related:
|
| | enhanced_system_prompt += "\n\n" + self.get_visualization_prompt_template()
|
| |
|
| | if is_mindmap_related:
|
| | enhanced_system_prompt += "\n\n" + self.get_mindmap_prompt_template()
|
| |
|
| |
|
| | headers = {
|
| | "Authorization": f"Bearer {self.stream_api_key}",
|
| | "Content-Type": "application/json"
|
| | }
|
| |
|
| | try:
|
| | response = requests.post(
|
| | f"{self.stream_api_base}/chat/completions",
|
| | headers=headers,
|
| | json={
|
| | "model": self.stream_model,
|
| | "messages": [
|
| | {"role": "system", "content": enhanced_system_prompt},
|
| | {"role": "user", "content": f"""
|
| | 参考内容:
|
| | {context}
|
| |
|
| | 问题:{query}
|
| |
|
| | 请按照要求回答问题,包括引用标注和来源列表。
|
| |
|
| | 如果在参考内容中找到视频资源,请使用以下格式标记视频链接:
|
| | <video_link>视频链接</video_link>
|
| |
|
| | 在回答结束时,请使用以下固定格式列出所有获取的参考内容作为参考来源:
|
| |
|
| | ===参考来源开始===
|
| | [1] 摘要内容,"文件名"
|
| | [2] 摘要内容,"文件名"
|
| | ===参考来源结束===
|
| |
|
| | 如果没有参考内容,则无需包含上述部分。
|
| | """}
|
| | ],
|
| | "stream": True
|
| | }
|
| | )
|
| |
|
| | if response.status_code != 200:
|
| | yield f"生成回答时出错: {response.text}"
|
| | return
|
| |
|
| |
|
| | for line in response.iter_lines():
|
| | if not line:
|
| | continue
|
| |
|
| | line_text = line.decode('utf-8')
|
| | if line_text.startswith('data: ') and line_text != 'data: [DONE]':
|
| | try:
|
| | json_str = line_text[6:]
|
| | data = json.loads(json_str)
|
| | content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
|
| | if content:
|
| | yield content
|
| | except Exception as e:
|
| | yield f"解析响应出错: {str(e)}"
|
| |
|
| |
|
| | if process_data:
|
| | process_data["generation"]["time"] = round(time.time() - start_time, 3)
|
| | yield {"process_data": process_data}
|
| |
|
| | except Exception as e:
|
| | yield f"连接错误: {str(e)}"
|
| |
|