diff --git a/backend/app/engine/__init__.py b/backend/app/engine/__init__.py index 12a45fc..7b6a111 100644 --- a/backend/app/engine/__init__.py +++ b/backend/app/engine/__init__.py @@ -4,91 +4,25 @@ from llama_index.core import SQLDatabase, SummaryIndex, VectorStoreIndex from llama_index.core.indices.struct_store import SQLTableRetrieverQueryEngine from llama_index.core.objects import SQLTableNodeMapping, ObjectIndex from llama_index.core.settings import Settings -from llama_index.core.agent import AgentRunner, StructuredPlannerAgent, FunctionCallingAgentWorker +from llama_index.core.agent import AgentRunner, StructuredPlannerAgent, FunctionCallingAgentWorker, ReActChatFormatter from llama_index.core.tools.query_engine import QueryEngineTool from sqlalchemy import create_engine, Engine +from llama_index.core.response_synthesizers.type import ResponseMode from app.engine.loaders.db import makeDescriptionByEngine from app.engine.tools import ToolFactory from app.engine.index import get_index -from app.engine.retriever.CHBM25Retriever import CHBM25Retriever from app.settings import get_node_postprocessors -from llama_index.core.retrievers import BaseRetriever -from llama_index.core import QueryBundle -from llama_index.core.schema import NodeWithScore -from typing import List, Any, Optional,Dict -from llama_index.core.query_engine.retriever_query_engine import RetrieverQueryEngine +import nest_asyncio -class HybridRetriever(BaseRetriever): - def __init__( - self, - vector_index, - similarity_top_k: int = 2, - out_top_k: Optional[int] = None, - alpha: float = 0.5, - filters = None, - **kwargs: Any, - ) -> None: - super().__init__(**kwargs) - self._vector_index = vector_index - self._embed_model = vector_index._embed_model - self._out_top_k = out_top_k or similarity_top_k - self._vecRetriever = vector_index.as_retriever( - similarity_top_k=similarity_top_k,filters = filters - ) - - STORAGE_DIR = os.getenv("BM_RETRIEVER_PATH", "storage_bm") - if os.path.exists(STORAGE_DIR) and len(os.listdir(STORAGE_DIR)) > 0: - self._bm25Retriever = CHBM25Retriever.from_persist_dir(STORAGE_DIR) - else: - bmRetriver = CHBM25Retriever.from_defaults(similarity_top_k=similarity_top_k,nodes=self._vector_index.vector_store.get_nodes(None)) - bmRetriver.persist(STORAGE_DIR) - self._alpha = alpha - - def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: - vecNodes:List[NodeWithScore] = self._vecRetriever.retrieve(query_bundle.query_str) - bmNodes:List[NodeWithScore] = self._bm25Retriever.retrieve(query_bundle.query_str) - - bmDic:Dict[str,NodeWithScore] = {} - for node in bmNodes: - bmDic[node.node_id] = node - - result_tups = [] - for i in range(len(vecNodes)): - node = vecNodes[i] - bmScore = 0.0 - if node.node_id in bmDic: - bmScore = bmDic[node.node_id].score - bmDic.pop(node.node_id) - else: - bmScore = 0.0 - full_similarity = (self._alpha * node.score) + ( - (1 - self._alpha) * bmScore - ) - result_tups.append((full_similarity, node)) - - for _,node in bmDic.items(): - full_similarity = (1 - self._alpha) * node.score - result_tups.append((full_similarity, node)) - - result_tups = sorted(result_tups, key=lambda x: x[0], reverse=True) - for full_score, node in result_tups: - node.score = full_score - return [n for _, n in result_tups][:self._out_top_k] - -def get_Retriever(index,**kwargs): - bEnableHybrid = True if os.getenv("HYBRID_ENABLED",False).title() == 'True' else False - if bEnableHybrid: - alpha = float(os.getenv("HYBRID_ALPHA", "0.5")) - retriever = HybridRetriever(index,alpha = alpha,**kwargs) - else: - retriever = index.as_retriever(**kwargs) - return retriever +nest_asyncio.apply() sql_database = None sql_obj_index = None + + def get_chat_engine(filters=None, params=None): system_prompt = os.getenv("SYSTEM_PROMPT") top_k = int(os.getenv("TOP_K", "3")) @@ -128,12 +62,13 @@ def get_chat_engine(filters=None, params=None): # 创建向量检索查询工具 postprocess = get_node_postprocessors() - query_engine = RetrieverQueryEngine.from_args( - get_Retriever(index,similarity_top_k=top_k, - filters=filters), + query_engine = index.as_query_engine( + similarity_top_k=top_k, filters=filters, node_postprocessors=postprocess, + use_async=True, + streaming=True, +# response_mode=ResponseMode.TREE_SUMMARIZE, ) - query_engine_tool = QueryEngineTool.from_defaults(query_engine=query_engine, name="zj_query_tool", description="由博微公司编制的关于电力造价知识、电力造价编制软件知识和造价工程文件结构的知识库。适用于查询电力领域、电力造价领域、博微、博微电力、博微造价等业务等内容。如果本知识库没有直接答案但有解决思路的可以返回解决办法后建议使用“zjdata_query_tool”工具。如果你不知道答案,就说你不知道,不要编造答案。", ) @@ -145,12 +80,31 @@ def get_chat_engine(filters=None, params=None): # Add additional tools tools += ToolFactory.from_env() - return AgentRunner.from_llm( + # agentrunner = StructuredPlannerAgent.from_llm( + # llm=Settings.llm, + # tools=tools, + # system_prompt=system_prompt, + # verbose=True, + # ) + + # prompts = agentrunner.agent_worker._get_prompts() + # prompts["system_prompt"].template = """您的设计旨在帮助完成各种任务,从回答问题到提供其他类型分析的摘要。\n\n##工具\n\n你可以访问各种工具。你有责任按照你认为合适的顺序使用这些工具来完成当前的任务。\n这可能需要将任务分解为子任务,并使用不同的工具来完成每个子任务。\n\n你可以访问以下工具:\n{tool_desc}\n\n\n##输出格式\n\n请用与问题相同的语言回答,并使用以下格式:\n\n```\nThought: 用户当前的语言是:(user's language)。我需要使用工具来帮助我回答问题。\nAction: 如果使用工具,则为工具名称(one of {tool_names})。\nAction Input: 输入给工具的内容,使用JSON格式表示kwargs(例如{{\"input\": \"hello world\", \"num_beams\": 5}})\n```\n\n请始终以Thought开始。\n\n切勿用Markdown代码标记包围你的响应。如果需要,可以在响应中使用代码标记。\n\n请为Action Input使用有效的JSON格式。不要这样做{{\'input\': \'hello world\', \'num_beams\': 5}}。\n\n如果使用此格式,用户将以下面的格式进行回应:\n\n```\nObservation: 工具响应\n```\n\n你应该继续重复上述格式,直到你有足够的信息来回答问题而无需使用更多工具。此时,你必须使用以下两种格式之一进行回答:\n\n```\nThought: 我可以不用任何工具来回答。我将使用用户的语言来回答。\nAnswer: [你的答案(与用户问题相同的语言)]\n```\n\n```\nThought: 我无法使用提供的工具回答问题。\nAnswer: [你的答案(与用户问题相同的语言)]\n```\n\n## 当前对话\n\n以下是当前对话,由人类和助手的消息交替组成。\n""" + # agentrunner.agent_worker.update_prompts(prompts) + + prefix_messages = ("""您的设计旨在帮助完成各种任务,从回答问题到提供其他类型分析的摘要。\n\n##工具\n\n你可以访问各种工具。你有责任按照你认为合适的顺序使用这些工具来完成当前的任务。\n这可能需要将任务分解为子任务,并使用不同的工具来完成每个子任务。\n\n你可以访问以下工具:\n{tool_desc}\n\n\n##输出格式\n\n请用与问题相同的语言回答,并使用以下格式:\n\n```\nThought: 用户当前的语言是:(user's language)。我需要使用工具来帮助我回答问题。\nAction: 如果使用工具,则为工具名称(one of {tool_names})。\nAction Input: 输入给工具的内容,使用JSON格式表示kwargs(例如{{\"input\": \"hello world\", \"num_beams\": 5}})\n```\n\n请始终以Thought开始。\n\n切勿用Markdown代码标记包围你的响应。如果需要,可以在响应中使用代码标记。\n\n请为Action Input使用有效的JSON格式。不要这样做{{\'input\': \'hello world\', \'num_beams\': 5}}。\n\n如果使用此格式,用户将以下面的格式进行回应:\n\n```\nObservation: 工具响应\n```\n\n你应该继续重复上述格式,直到你有足够的信息来回答问题而无需使用更多工具。此时,你必须使用以下两种格式之一进行回答:\n\n```\nThought: 我可以不用任何工具来回答。我将使用用户的语言来回答。\nAnswer: [你的答案(与用户问题相同的语言)]\n```\n\n```\nThought: 我无法使用提供的工具回答问题。\nAnswer: [你的答案(与用户问题相同的语言)]\n```\n\n##如果从工具中得到的回应是Empty Response,那么只需要回答“我不知道”,不需要额外回答别的内容。## 当前对话\n\n以下是当前对话,由人类和助手的消息交替组成。\n""") + + react_chat_formatter = ReActChatFormatter.from_defaults(prefix_messages) + + agentrunner = AgentRunner.from_llm( llm=Settings.llm, tools=tools, + react_chat_formatter=react_chat_formatter, system_prompt=system_prompt, verbose=True, ) + return agentrunner + + # create the function calling worker for reasoning # worker = FunctionCallingAgentWorker.from_tools( # tools, verbose=True @@ -158,9 +112,3 @@ def get_chat_engine(filters=None, params=None): # # # wrap the worker in the top-level planner # return StructuredPlannerAgent(worker, tools) - - - - - - diff --git a/backend/main.py b/backend/main.py index 5f84c63..42844d1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,8 +16,90 @@ from app.observability import init_observability from fastapi.staticfiles import StaticFiles from phoenix.trace import using_project +from llama_index.core.prompts.default_prompts import ( + DEFAULT_REFINE_PROMPT, + DEFAULT_REFINE_TABLE_CONTEXT_PROMPT, + DEFAULT_TEXT_QA_PROMPT, + DEFAULT_TREE_SUMMARIZE_PROMPT, +) + logger = logging.getLogger("uvicorn") +DEFAULT_TEXT_QA_PROMPT.template = ( + "# 角色\n" + "你是一名博微造价工程数据查询助手,专精于电力工程文件中的信息。" + "你的职责是提供有关电力造价、造价编制软件、文件结构及相关数据的精准、客观的回答," + "如同直接从文件中提取的内容。\n" + + "## 技能\n" + "### 技能 1: 数据查询与提供\n" + "- 准确回答所有关于电力工程造价的相关问题。\n" + "- 提供具体数据,如成本估算、材料清单、劳动力需求等。\n" + "- 确保提供的信息严格基于工程文档中的记录。\n" + + "### 技能 2: 技术性解释\n" + "- 解释造价工程中的技术术语和概念。\n" + "- 为复杂的工程细节提供清晰易懂的说明。\n" + + "## 约束\n" + "- 仅回答与电力工程造价文件相关的具体问题。\n" + "- 不进行任何超出文件内容的猜测或假设。\n" + "- 所有回答均基于文件内容,采用客观和技术性的语言。\n" + "- 请基于这些信息回答问题。如果无法找到相关信息,请不要额外发散回答,不要回答多余的信息,只需要回答“我不知道这个问题的答案”。\n" + "上下文信息如下。\n" + "---------------------\n" + "{context_str}\n" + "---------------------\n" + "请仅依据上下文信息回答问题。\n" + "Query: {query_str}\n" + "Answer: " +) + + +DEFAULT_REFINE_PROMPT.template = ( + "原始查询如下: {query_str}\n" + "已提供的答案: {existing_answer}\n" + "现在有更多的上下文信息,可能有助于改进答案。\n" + "------------\n" + "{context_msg}\n" + "------------\n" + "请根据新的上下文改进原始答案,以更好地回答查询。" + "如果新的上下文信息无用,请保留原答案。\n" + "改进后的答案: " +) + + +DEFAULT_TREE_SUMMARIZE_PROMPT.template = ( + "# 角色\n" + "你是一名博微造价工程数据查询助手,专精于电力工程文件中的信息。" + "你的职责是提供有关电力造价、造价编制软件、文件结构及相关数据的精准、客观的回答," + "如同直接从文件中提取的内容。\n" + + "## 技能\n" + "### 技能 1: 数据查询与提供\n" + "- 准确回答所有关于电力工程造价的相关问题。\n" + "- 提供具体数据,如成本估算、材料清单、劳动力需求等。\n" + "- 确保提供的信息严格基于工程文档中的记录。\n" + + "### 技能 2: 技术性解释\n" + "- 解释造价工程中的技术术语和概念。\n" + "- 为复杂的工程细节提供清晰易懂的说明。\n" + + "## 约束\n" + "- 仅回答与电力工程造价文件相关的具体问题。\n" + "- 不进行任何超出文件内容的猜测或假设。\n" + "- 所有回答均基于文件内容,采用客观和技术性的语言。\n" + "- 请基于这些信息回答问题。如果无法找到相关信息,请不要额外发散回答,不要回答多余的信息,只需要回答“我不知道这个问题的答案”。\n" + "来自多个来源的上下文信息如下。\n" + "---------------------\n" + "{context_str}\n" + "---------------------\n" + "鉴于来自多个来源的信息而非先验知识, " + "回答查询。\n" + "Query: {query_str}\n" + "Answer: " +) + usPrj = using_project(os.getenv("PHOENIX_PROJECT_NAME")) usPrj.__enter__() @@ -52,7 +134,6 @@ mount_static_files("data_output", "/api/files/output") app.include_router(chat_router, prefix="/api/chat") app.include_router(file_upload_router, prefix="/api/chat/upload") -# Redirect to documentation page when accessing base URL @app.get("/") async def redirect_to_docs(): return RedirectResponse(url="/docs")