From f786e764caa96d3a82788ad484c06e49cf7df16f Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 17 Oct 2024 12:35:52 +0000 Subject: [PATCH 01/29] add langchain_integration folder;support lazy load model and chains --- .../common_logic/common_utils/constant.py | 12 +- .../common_logic/common_utils/prompt_utils.py | 4 +- source/lambda/online/lambda_agent/agent.py | 2 +- .../tool_calling_chain_claude_xml.py | 2 +- .../llm_generate_utils/llm_models.py | 1 + .../langchain_integration/chains/__init__.py | 194 ++++++++ .../chains/chat_chain.py | 338 +++++++++++++ .../chains/conversation_summary_chain.py | 215 +++++++++ .../chains/hyde_chain.py | 103 ++++ .../chains/intention_chain.py | 224 +++++++++ .../chains/llm_chain_base.py | 26 + .../chains/marketing_chains/__init__.py | 15 + .../mkt_conversation_summary.py | 120 +++++ .../chains/marketing_chains/mkt_rag_chain.py | 55 +++ .../chains/query_rewrite_chain.py | 143 ++++++ .../langchain_integration/chains/rag_chain.py | 161 +++++++ .../chains/retail_chains/__init__.py | 26 + .../retail_chains/auto_evaluation_chain.py | 99 ++++ .../retail_conversation_summary_chain.py | 208 ++++++++ .../retail_tool_calling_chain_claude_xml.py | 354 ++++++++++++++ .../retail_tool_calling_chain_json.py | 455 ++++++++++++++++++ .../chains/stepback_chain.py | 138 ++++++ .../chains/tool_calling_chain_claude_xml.py | 320 ++++++++++++ .../chains/translate_chain.py | 40 ++ .../chat_models/__init__.py | 97 ++++ .../chat_models/bedrock_models.py | 77 +++ .../chat_models/openai_models.py | 28 ++ 27 files changed, 3450 insertions(+), 7 deletions(-) create mode 100644 source/lambda/online/langchain_integration/chains/__init__.py create mode 100644 source/lambda/online/langchain_integration/chains/chat_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/conversation_summary_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/hyde_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/intention_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/llm_chain_base.py create mode 100644 source/lambda/online/langchain_integration/chains/marketing_chains/__init__.py create mode 100644 source/lambda/online/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py create mode 100644 source/lambda/online/langchain_integration/chains/marketing_chains/mkt_rag_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/query_rewrite_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/rag_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/retail_chains/__init__.py create mode 100644 source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py create mode 100644 source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py create mode 100644 source/lambda/online/langchain_integration/chains/stepback_chain.py create mode 100644 source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py create mode 100644 source/lambda/online/langchain_integration/chains/translate_chain.py create mode 100644 source/lambda/online/langchain_integration/chat_models/__init__.py create mode 100644 source/lambda/online/langchain_integration/chat_models/bedrock_models.py create mode 100644 source/lambda/online/langchain_integration/chat_models/openai_models.py diff --git a/source/lambda/online/common_logic/common_utils/constant.py b/source/lambda/online/common_logic/common_utils/constant.py index 518d35da..c14ee854 100644 --- a/source/lambda/online/common_logic/common_utils/constant.py +++ b/source/lambda/online/common_logic/common_utils/constant.py @@ -82,17 +82,19 @@ class LLMTaskType(ConstantBase): HYDE_TYPE = "hyde" CONVERSATION_SUMMARY_TYPE = "conversation_summary" RETAIL_CONVERSATION_SUMMARY_TYPE = "retail_conversation_summary" - MKT_CONVERSATION_SUMMARY_TYPE = "mkt_conversation_summary" MKT_QUERY_REWRITE_TYPE = "mkt_query_rewrite" STEPBACK_PROMPTING_TYPE = "stepback_prompting" - TOOL_CALLING = "tool_calling" + TOOL_CALLING_XML = "tool_calling_xml" + TOOL_CALLING_API = "tool_calling_api" RETAIL_TOOL_CALLING = "retail_tool_calling" RAG = "rag" + MTK_RAG = "mkt_rag" CHAT = 'chat' AUTO_EVALUATION = "auto_evaluation" + class MessageType(ConstantBase): HUMAN_MESSAGE_TYPE = 'human' AI_MESSAGE_TYPE = 'ai' @@ -133,12 +135,16 @@ class LLMModelType(ConstantBase): INTERNLM2_CHAT_7B = "internlm2-chat-7b" INTERNLM2_CHAT_20B = "internlm2-chat-20b" GLM_4_9B_CHAT = "glm-4-9b-chat" - CHATGPT_35_TURBO = "gpt-3.5-turbo-0125" + CHATGPT_35_TURBO_0125 = "gpt-3.5-turbo-0125" CHATGPT_4_TURBO = "gpt-4-turbo" CHATGPT_4O = "gpt-4o" QWEN2INSTRUCT7B = "qwen2-7B-instruct" QWEN2INSTRUCT72B = "qwen2-72B-instruct" QWEN15INSTRUCT32B = "qwen1_5-32B-instruct" + LLAMA3_1_70B_INSTRUCT = "meta.llama3-1-70b-instruct-v1:0" + MISTRAL_LARGE_2407 = "mistral.mistral-large-2407-v1:0" + COHERE_COMMAND_R_PLUS = "cohere.command-r-plus-v1:0" + class EmbeddingModelType(ConstantBase): diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index a97d3dc3..953f24b8 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -338,7 +338,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, ], - task_type=LLMTaskType.TOOL_CALLING, + task_type=LLMTaskType.TOOL_CALLING_XML, prompt_template=AGENT_USER_PROMPT, prompt_name="user_prompt" ) @@ -363,7 +363,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, ], - task_type=LLMTaskType.TOOL_CALLING, + task_type=LLMTaskType.TOOL_CALLING_XML, prompt_template=AGENT_GUIDELINES_PROMPT, prompt_name="guidelines_prompt" ) diff --git a/source/lambda/online/lambda_agent/agent.py b/source/lambda/online/lambda_agent/agent.py index da898a4b..495e2587 100644 --- a/source/lambda/online/lambda_agent/agent.py +++ b/source/lambda/online/lambda_agent/agent.py @@ -26,7 +26,7 @@ def tool_calling(state:dict): "fewshot_examples": state['intent_fewshot_examples'], } - agent_llm_type = state.get("agent_llm_type",None) or LLMTaskType.TOOL_CALLING + agent_llm_type = state.get("agent_llm_type",None) or LLMTaskType.TOOL_CALLING_XML group_name = state['chatbot_config']['group_name'] chatbot_id = state['chatbot_config']['chatbot_id'] diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py b/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py index b31ab0d6..3fc57da5 100644 --- a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py +++ b/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py @@ -168,7 +168,7 @@ def convert_openai_tool_to_anthropic(tools:list[dict])->str: class Claude2ToolCallingChain(LLMChain): model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.TOOL_CALLING + intent_type = LLMTaskType.TOOL_CALLING_XML default_model_kwargs = { "max_tokens": 2000, "temperature": 0.1, diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_models.py b/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_models.py index 1146cbbd..5ca6dc14 100644 --- a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_models.py +++ b/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_models.py @@ -2,6 +2,7 @@ import logging import os from datetime import datetime +from langchain_aws.chat_models import ChatBedrockConverse import boto3 diff --git a/source/lambda/online/langchain_integration/chains/__init__.py b/source/lambda/online/langchain_integration/chains/__init__.py new file mode 100644 index 00000000..0453a3ef --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/__init__.py @@ -0,0 +1,194 @@ +from typing import Any +from common_logic.common_utils.constant import LLMTaskType + + +class LLMChainMeta(type): + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + if name == "LLMChain": + return new_cls + new_cls.model_map[new_cls.get_chain_id()] = new_cls + return new_cls + + +class LLMChain(metaclass=LLMChainMeta): + model_map = {} + + @classmethod + def get_chain_id(cls): + return cls._get_chain_id(cls.model_id, cls.intent_type) + + @staticmethod + def _get_chain_id(model_id, intent_type): + return f"{model_id}__{intent_type}" + + @classmethod + def get_chain(cls, model_id, intent_type, model_kwargs=None, **kwargs): + # dynamic import + _load_module(intent_type) + return cls.model_map[cls._get_chain_id(model_id, intent_type)].create_chain( + model_kwargs=model_kwargs, **kwargs + ) + +def _import_chat_chain(): + from .chat_chain import ( + Claude2ChatChain, + Claude21ChatChain, + ClaudeInstanceChatChain, + Iternlm2Chat7BChatChain, + Iternlm2Chat20BChatChain, + Baichuan2Chat13B4BitsChatChain, + Claude3HaikuChatChain, + Claude3SonnetChatChain, +) + +def _import_conversation_summary_chain(): + from .conversation_summary_chain import ( + Iternlm2Chat7BConversationSummaryChain, + ClaudeInstanceConversationSummaryChain, + Claude21ConversationSummaryChain, + Claude3HaikuConversationSummaryChain, + Claude3SonnetConversationSummaryChain, + Iternlm2Chat20BConversationSummaryChain +) + +def _import_intention_chain(): + from .intention_chain import ( + Claude21IntentRecognitionChain, + Claude2IntentRecognitionChain, + ClaudeInstanceIntentRecognitionChain, + Claude3HaikuIntentRecognitionChain, + Claude3SonnetIntentRecognitionChain, + Iternlm2Chat7BIntentRecognitionChain, + Iternlm2Chat20BIntentRecognitionChain, + +) + + +def _import_rag_chain(): + from .rag_chain import ( + Claude21RagLLMChain, + Claude2RagLLMChain, + ClaudeInstanceRAGLLMChain, + Claude3HaikuRAGLLMChain, + Claude3SonnetRAGLLMChain, + Baichuan2Chat13B4BitsKnowledgeQaChain +) + + +def _import_translate_chain(): + from .translate_chain import ( + Iternlm2Chat7BTranslateChain, + Iternlm2Chat20BTranslateChain + ) + +def _import_mkt_conversation_summary_chains(): + from marketing_chains.mkt_conversation_summary import ( + Claude21MKTConversationSummaryChain, + ClaudeInstanceMKTConversationSummaryChain, + Claude2MKTConversationSummaryChain, + Claude3HaikuMKTConversationSummaryChain, + Claude3SonnetMKTConversationSummaryChain, + Iternlm2Chat7BMKTConversationSummaryChain, + Iternlm2Chat20BMKTConversationSummaryChain +) + +def _import_mkt_rag_chain(): + from marketing_chains.mkt_rag_chain import ( + Iternlm2Chat7BKnowledgeQaChain, + Iternlm2Chat20BKnowledgeQaChain +) + +def _import_stepback_chain(): + from .stepback_chain import ( + Claude21StepBackChain, + ClaudeInstanceStepBackChain, + Claude2StepBackChain, + Claude3HaikuStepBackChain, + Claude3SonnetStepBackChain, + Iternlm2Chat7BStepBackChain, + Iternlm2Chat20BStepBackChain +) + +def _import_hyde_chain(): + from .hyde_chain import ( + Claude21HydeChain, + Claude2HydeChain, + Claude3HaikuHydeChain, + Claude3SonnetHydeChain, + ClaudeInstanceHydeChain, + Iternlm2Chat20BHydeChain, + Iternlm2Chat7BHydeChain +) + +def _import_query_rewrite_chain(): + from .query_rewrite_chain import ( + Claude21QueryRewriteChain, + Claude2QueryRewriteChain, + ClaudeInstanceQueryRewriteChain, + Claude3HaikuQueryRewriteChain, + Claude3SonnetQueryRewriteChain, + Iternlm2Chat20BQueryRewriteChain, + Iternlm2Chat7BQueryRewriteChain +) + + +def _import_tool_calling_chain_claude_xml(): + from .tool_calling_chain_claude_xml import ( + Claude21ToolCallingChain, + Claude3HaikuToolCallingChain, + Claude2ToolCallingChain, + Claude3SonnetToolCallingChain, + ClaudeInstanceToolCallingChain +) + +def _import_retail_conversation_summary_chain(): + from .retail_chains.retail_conversation_summary_chain import ( + Claude2RetailConversationSummaryChain, + Claude21RetailConversationSummaryChain, + Claude3HaikuRetailConversationSummaryChain, + Claude3SonnetRetailConversationSummaryChain, + ClaudeInstanceRetailConversationSummaryChain +) + + +def _import_retail_tool_calling_chain_claude_xml(): + from .retail_chains.retail_tool_calling_chain_claude_xml import ( + Claude2RetailToolCallingChain, + Claude21RetailToolCallingChain, + ClaudeInstanceRetailToolCallingChain, + Claude3SonnetRetailToolCallingChain, + Claude3HaikuRetailToolCallingChain +) + + +def _import_auto_evaluation_chain(): + from .retail_chains.auto_evaluation_chain import ( + Claude3HaikuAutoEvaluationChain, + Claude21AutoEvaluationChain, + Claude2AutoEvaluationChain + +) + + +def _load_module(intent_type): + assert intent_type in CHAIN_MODULE_LOAD_FN_MAP,(intent_type,CHAIN_MODULE_LOAD_FN_MAP) + CHAIN_MODULE_LOAD_FN_MAP[intent_type]() + + +CHAIN_MODULE_LOAD_FN_MAP = { + LLMTaskType.CHAT:_import_chat_chain, + LLMTaskType.CONVERSATION_SUMMARY_TYPE:_import_conversation_summary_chain, + LLMTaskType.INTENT_RECOGNITION_TYPE: _import_intention_chain, + LLMTaskType.RAG: _import_rag_chain, + LLMTaskType.QUERY_TRANSLATE_TYPE: _import_translate_chain, + LLMTaskType.MKT_CONVERSATION_SUMMARY_TYPE: _import_mkt_conversation_summary_chains, + LLMTaskType.MTK_RAG: _import_mkt_rag_chain, + LLMTaskType.STEPBACK_PROMPTING_TYPE: _import_stepback_chain, + LLMTaskType.HYDE_TYPE: _import_hyde_chain, + LLMTaskType.QUERY_REWRITE_TYPE: _import_query_rewrite_chain, + LLMTaskType.TOOL_CALLING_XML: _import_tool_calling_chain_claude_xml, + LLMTaskType.RETAIL_CONVERSATION_SUMMARY_TYPE: _import_retail_conversation_summary_chain, + LLMTaskType.RETAIL_TOOL_CALLING: _import_retail_tool_calling_chain_claude_xml, + LLMTaskType.AUTO_EVALUATION: _import_auto_evaluation_chain +} diff --git a/source/lambda/online/langchain_integration/chains/chat_chain.py b/source/lambda/online/langchain_integration/chains/chat_chain.py new file mode 100644 index 00000000..730a8490 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/chat_chain.py @@ -0,0 +1,338 @@ +# chat llm chains + +from langchain.schema.runnable import RunnableLambda, RunnablePassthrough +from langchain_core.messages import AIMessage,SystemMessage +from langchain.prompts import ChatPromptTemplate,HumanMessagePromptTemplate +from langchain_core.messages import convert_to_messages + + +from ..llm_models import Model +from .llm_chain_base import LLMChain + +from common_logic.common_utils.constant import ( + MessageType, + LLMTaskType, + LLMModelType, +) +from common_logic.common_utils.time_utils import get_china_now +from common_logic.common_utils.prompt_utils import get_prompt_template + +AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE +HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE +QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE +SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE + + +class Claude2ChatChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.CHAT + + + @classmethod + def get_common_system_prompt(cls,system_prompt_template:str): + now = get_china_now() + date_str = now.strftime("%Y年%m月%d日") + weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] + weekday = weekdays[now.weekday()] + system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) + return system_prompt + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + stream = kwargs.get("stream", False) + system_prompt_template = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + system_prompt = kwargs.get('system_prompt',system_prompt_template) or "" + system_prompt = cls.get_common_system_prompt(system_prompt) + prefill = kwargs.get('prefill',None) + messages = [ + ("placeholder", "{chat_history}"), + HumanMessagePromptTemplate.from_template("{query}") + ] + if system_prompt: + messages.insert(0,SystemMessage(content=system_prompt)) + + if prefill is not None: + messages.append(AIMessage(content=prefill)) + + messages_template = ChatPromptTemplate.from_messages(messages) + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + chain = messages_template | RunnableLambda(lambda x: x.messages) + if stream: + chain = ( + chain | RunnableLambda(lambda messages: llm.stream(messages)) + | RunnableLambda(lambda x: (i.content for i in x)) + ) + else: + chain = chain | llm | RunnableLambda(lambda x: x.content) + + return chain + + +class Claude21ChatChain(Claude2ChatChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceChatChain(Claude2ChatChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetChatChain(Claude2ChatChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuChatChain(Claude2ChatChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude35SonnetChatChain(Claude2ChatChain): + model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" + + +class Mixtral8x7bChatChain(Claude2ChatChain): + model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT + default_model_kwargs = {"max_tokens": 4096, "temperature": 0.01} + + +class Baichuan2Chat13B4BitsChatChain(LLMChain): + model_id = LLMModelType.BAICHUAN2_13B_CHAT + intent_type = LLMTaskType.CHAT + default_model_kwargs = { + "max_new_tokens": 2048, + "temperature": 0.3, + "top_k": 5, + "top_p": 0.85, + "do_sample": True, + } + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + stream = kwargs.get("stream", False) + # chat_history = kwargs.pop('chat_history',[]) + model_kwargs = model_kwargs or {} + model_kwargs.update({"stream": stream}) + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + llm_chain = RunnableLambda(lambda x: llm.invoke(x, stream=stream)) + return llm_chain + + +class Iternlm2Chat7BChatChain(LLMChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + intent_type = LLMTaskType.CHAT + + default_model_kwargs = {"temperature": 0.5, "max_new_tokens": 1000} + + @staticmethod + def build_prompt( + query: str, + history=[], + meta_instruction="You are an AI assistant whose name is InternLM (书生·浦语).\n" + "- InternLM (书生·浦语) is a conversational language model that is developed by Shanghai AI Laboratory (上海人工智能实验室). It is designed to be helpful, honest, and harmless.\n" + "- InternLM (书生·浦语) can understand and communicate fluently in the language chosen by the user such as English and 中文.", + ): + prompt = "" + if meta_instruction: + prompt += f"""<|im_start|>system\n{meta_instruction}<|im_end|>\n""" + for record in history: + prompt += f"""<|im_start|>user\n{record[0]}<|im_end|>\n<|im_start|>assistant\n{record[1]}<|im_end|>\n""" + prompt += f"""<|im_start|>user\n{query}<|im_end|>\n<|im_start|>assistant\n""" + return prompt + + @classmethod + def create_history(cls, x): + chat_history = x.get("chat_history", []) + chat_history = convert_to_messages(chat_history) + + assert len(chat_history) % 2 == 0, chat_history + history = [] + for i in range(0, len(chat_history), 2): + user_message = chat_history[i] + ai_message = chat_history[i + 1] + assert ( + user_message.type == HUMAN_MESSAGE_TYPE + and ai_message.type == AI_MESSAGE_TYPE + ), chat_history + history.append((user_message.content, ai_message.content)) + return history + + @classmethod + def create_prompt(cls, x,system_prompt=None): + history = cls.create_history(x) + if system_prompt is None: + system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + prompt = cls.build_prompt( + query=x["query"], + history=history, + meta_instruction=system_prompt, + ) + return prompt + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + stream = kwargs.get("stream", False) + system_prompt = kwargs.get("system_prompt",None) + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + + prompt_template = RunnablePassthrough.assign( + prompt=RunnableLambda(lambda x: cls.create_prompt(x,system_prompt=system_prompt)) + ) + llm_chain = prompt_template | RunnableLambda( + lambda x: llm.invoke(x, stream=stream) + ) + return llm_chain + + +class Iternlm2Chat20BChatChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B + + +class GLM4Chat9BChatChain(LLMChain): + model_id = LLMModelType.GLM_4_9B_CHAT + intent_type = LLMTaskType.CHAT + default_model_kwargs = { + "max_new_tokens": 1024, + "timeout": 60, + "temperature": 0.1, + } + @classmethod + def create_chat_history(cls,x, system_prompt=None): + if system_prompt is None: + system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + chat_history = x['chat_history'] + + if system_prompt is not None: + chat_history = [{"role":"system","content": system_prompt}] + chat_history + chat_history = chat_history + [{"role":MessageType.HUMAN_MESSAGE_TYPE,"content":x['query']}] + + return chat_history + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + system_prompt = kwargs.get("system_prompt",None) + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + **kwargs + ) + + chain = RunnablePassthrough.assign( + chat_history = RunnableLambda(lambda x: cls.create_chat_history(x,system_prompt=system_prompt)) + ) | RunnableLambda(lambda x: llm.invoke(x)) + + return chain + + +class Qwen2Instruct7BChatChain(LLMChain): + model_id = LLMModelType.QWEN2INSTRUCT7B + intent_type = LLMTaskType.CHAT + default_model_kwargs = { + "max_tokens": 1024, + "temperature": 0.1, + } + + @classmethod + def create_chat_history(cls,x, system_prompt=None): + if system_prompt is None: + system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + chat_history = x['chat_history'] + + if system_prompt is not None: + chat_history = [{"role":"system", "content": system_prompt}] + chat_history + + chat_history = chat_history + [{"role": MessageType.HUMAN_MESSAGE_TYPE, "content":x['query']}] + return chat_history + + + @classmethod + def parse_function_calls_from_ai_message(cls,message:dict): + return message['text'] + + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + stream = kwargs.get("stream", False) + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + system_prompt = kwargs.get("system_prompt",None) + + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + **kwargs + ) + + chain = RunnablePassthrough.assign( + chat_history = RunnableLambda(lambda x: cls.create_chat_history(x,system_prompt=system_prompt)) + ) | RunnableLambda(lambda x: llm.invoke(x)) | RunnableLambda(lambda x: cls.parse_function_calls_from_ai_message(x)) + + return chain + +class Qwen2Instruct72BChatChain(Qwen2Instruct7BChatChain): + model_id = LLMModelType.QWEN2INSTRUCT72B + + +class Qwen2Instruct72BChatChain(Qwen2Instruct7BChatChain): + model_id = LLMModelType.QWEN15INSTRUCT32B + + +class ChatGPT35ChatChain(LLMChain): + model_id = LLMModelType.CHATGPT_35_TURBO + intent_type = LLMTaskType.CHAT + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + stream = kwargs.get("stream", False) + system_prompt = kwargs.get('system_prompt',None) + prefill = kwargs.get('prefill',None) + messages = [ + ("placeholder", "{chat_history}"), + HumanMessagePromptTemplate.from_template("{query}") + ] + if system_prompt is not None: + messages.insert(SystemMessage(content=system_prompt),0) + + if prefill is not None: + messages.append(AIMessage(content=prefill)) + + messages_template = ChatPromptTemplate.from_messages(messages) + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + chain = messages_template | RunnableLambda(lambda x: x.messages) + if stream: + chain = ( + chain | RunnableLambda(lambda messages: llm.stream(messages)) + | RunnableLambda(lambda x: (i.content for i in x)) + ) + else: + chain = chain | llm | RunnableLambda(lambda x: x.content) + + return chain + +class ChatGPT4ChatChain(ChatGPT35ChatChain): + model_id = LLMModelType.CHATGPT_4_TURBO + +class ChatGPT4oChatChain(ChatGPT35ChatChain): + model_id = LLMModelType.CHATGPT_4O diff --git a/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py new file mode 100644 index 00000000..c3f1aa1d --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py @@ -0,0 +1,215 @@ +# conversation summary chain +from typing import List +import json +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough, +) + + +from ..llm_models import Model +from .chat_chain import Iternlm2Chat7BChatChain +from .llm_chain_base import LLMChain +from common_logic.common_utils.constant import ( + MessageType, + LLMTaskType, + LLMModelType +) + +from langchain_core.messages import( + AIMessage, + BaseMessage, + HumanMessage, + SystemMessage, + convert_to_messages +) +from langchain.prompts import ( + HumanMessagePromptTemplate, + ChatPromptTemplate +) + +from common_logic.common_utils.prompt_utils import get_prompt_template +from common_logic.common_utils.logger_utils import get_logger,print_llm_messages + +logger = get_logger("conversation_summary") + +AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE +HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE +QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE +SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE + + +class Iternlm2Chat20BConversationSummaryChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B + default_model_kwargs = { + "max_new_tokens": 300, + "temperature": 0.1, + "stop_tokens": ["\n\n"], + } + + @classmethod + def create_prompt(cls, x,system_prompt=None): + chat_history = x["chat_history"] + conversational_contexts = [] + for his in chat_history: + role = his['role'] + assert role in [HUMAN_MESSAGE_TYPE, AI_MESSAGE_TYPE] + if role == HUMAN_MESSAGE_TYPE: + conversational_contexts.append(f"USER: {his['content']}") + else: + conversational_contexts.append(f"AI: {his['content']}") + if system_prompt is None: + system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + conversational_context = "\n".join(conversational_contexts) + prompt = cls.build_prompt( + system_prompt.format( + history=conversational_context, question=x["query"] + ) + ) + prompt = prompt + "Standalone Question: " + return prompt + +class Iternlm2Chat7BConversationSummaryChain(Iternlm2Chat20BConversationSummaryChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + + +class Claude2ConversationSummaryChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.CONVERSATION_SUMMARY_TYPE + + default_model_kwargs = {"max_tokens": 2000, "temperature": 0.1, "top_p": 0.9} + prefill = "From PersonU's point of view, here is the single standalone sentence:" + + @staticmethod + def create_conversational_context(chat_history:List[BaseMessage]): + conversational_contexts = [] + for his in chat_history: + assert isinstance(his,(AIMessage,HumanMessage)), his + content = his.content + if isinstance(his,HumanMessage): + conversational_contexts.append(f"USER: {content}") + else: + conversational_contexts.append(f"AI: {content}") + conversational_context = "\n".join(conversational_contexts) + return conversational_context + + @classmethod + def format_conversation(cls,conversation:list[BaseMessage]): + conversation_strs = [] + for message in conversation: + assert isinstance(message,(AIMessage,HumanMessage)), message + content = message.content + if isinstance(message, HumanMessage): + conversation_strs.append(f"PersonU: {content}") + elif isinstance(message, AIMessage): + conversation_strs.append(f"PersonA: {content}") + return "\n".join(conversation_strs) + + @classmethod + def create_messages_inputs(cls,x:dict,user_prompt,few_shots:list[dict]): + # create few_shots + few_shot_messages = [] + for few_shot in few_shots: + conversation=cls.format_conversation( + convert_to_messages(few_shot['conversation']) + ) + few_shot_messages.append(HumanMessage(content=user_prompt.format( + conversation=conversation, + current_query=few_shot['conversation'][-1]['content'] + ))) + few_shot_messages.append(AIMessage(content=f"{cls.prefill} {few_shot['rewrite_query']}")) + + # create current cocnversation + cur_messages = convert_to_messages( + x['chat_history'] + [{"role":MessageType.HUMAN_MESSAGE_TYPE,"content":x['query']}] + ) + + conversation = cls.format_conversation(cur_messages) + return { + "conversation":conversation, + "few_shots":few_shot_messages, + "current_query": x['query'] + } + + @classmethod + def create_messages_chain(cls,**kwargs): + system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + user_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="user_prompt" + ).prompt_template + + few_shots = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="few_shots" + ).prompt_template + + system_prompt = kwargs.get("system_prompt", system_prompt) + user_prompt = kwargs.get('user_prompt', user_prompt) + + cqr_template = ChatPromptTemplate.from_messages([ + SystemMessage(content=system_prompt), + ('placeholder','{few_shots}'), + HumanMessagePromptTemplate.from_template(user_prompt), + AIMessage(content=cls.prefill) + ]) + return RunnableLambda(lambda x: cls.create_messages_inputs(x,user_prompt=user_prompt,few_shots=json.loads(few_shots))) | cqr_template + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + ) + messages_chain = cls.create_messages_chain(**kwargs) + chain = messages_chain | RunnableLambda(lambda x: print_llm_messages(f"conversation summary messages: {x.messages}") or x.messages) \ + | llm | RunnableLambda(lambda x: x.content) + return chain + + +class Claude21ConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Qwen2Instruct72BConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.QWEN2INSTRUCT72B + + +class Qwen2Instruct72BConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.QWEN15INSTRUCT32B + + +class Qwen2Instruct7BConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.QWEN2INSTRUCT7B + + +class GLM4Chat9BConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.GLM_4_9B_CHAT + + diff --git a/source/lambda/online/langchain_integration/chains/hyde_chain.py b/source/lambda/online/langchain_integration/chains/hyde_chain.py new file mode 100644 index 00000000..de3b0f0d --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/hyde_chain.py @@ -0,0 +1,103 @@ +# hyde + +from langchain.prompts import ( + ChatPromptTemplate, + HumanMessagePromptTemplate, +) +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough, +) + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType +) + +from ..chains import LLMChain +from ..chat_models import Model as LLM_Model +from .chat_chain import Iternlm2Chat7BChatChain +from .llm_chain_base import LLMChain + +HYDE_TYPE = LLMTaskType.HYDE_TYPE + +WEB_SEARCH_TEMPLATE = """Please write a passage to answer the question +Question: {query} +Passage:""" +# hyde_web_search_template = PromptTemplate(template=WEB_SEARCH_TEMPLATE, input_variables=["query"]) + + +class Claude2HydeChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = HYDE_TYPE + + default_model_kwargs = { + "temperature": 0.5, + "max_tokens": 1000, + "stop_sequences": ["\n\nHuman:"], + } + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + # query_key = kwargs.pop("query_key", "query") + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + llm = LLM_Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + ) + prompt = ChatPromptTemplate.from_messages( + [HumanMessagePromptTemplate.from_template(WEB_SEARCH_TEMPLATE)] + ) + chain = RunnablePassthrough.assign( + hyde_doc=prompt | llm | RunnableLambda(lambda x: x.content) + ) + return chain + + +class Claude21HydeChain(Claude2HydeChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceHydeChain(Claude2HydeChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetHydeChain(Claude2HydeChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuHydeChain(Claude2HydeChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude35SonnetHydeChain(Claude2HydeChain): + model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" + + +internlm2_meta_instruction = "You are a helpful AI Assistant." + + +class Iternlm2Chat7BHydeChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + intent_type = HYDE_TYPE + + default_model_kwargs = {"temperature": 0.1, "max_new_tokens": 200} + + @classmethod + def create_prompt(cls, x): + query = f"""Please write a brief passage to answer the question. \nQuestion: {prompt}""" + prompt = ( + cls.build_prompt( + query=query, + meta_instruction=internlm2_meta_instruction, + ) + + "Passage: " + ) + return prompt + + +class Iternlm2Chat20BHydeChain(Iternlm2Chat7BHydeChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B + intent_type = HYDE_TYPE diff --git a/source/lambda/online/langchain_integration/chains/intention_chain.py b/source/lambda/online/langchain_integration/chains/intention_chain.py new file mode 100644 index 00000000..292023fd --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/intention_chain.py @@ -0,0 +1,224 @@ +import json +import os +from functools import lru_cache +from random import Random + +from langchain.prompts import ( + ChatPromptTemplate, + HumanMessagePromptTemplate, +) +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough, +) + +from common_logic.common_utils.constant import LLMTaskType,LLMModelType +from ..llm_models import Model +from .chat_chain import Iternlm2Chat7BChatChain +from .llm_chain_base import LLMChain + +abs_dir = os.path.dirname(__file__) + +intent_save_path = os.path.join( + os.path.dirname(os.path.dirname(abs_dir)), + "intent_utils", + "intent_examples", + "examples.json", +) + + +@lru_cache() +def load_intention_file(intent_save_path=intent_save_path, seed=42): + intent_few_shot_examples = json.load(open(intent_save_path)) + intent_indexs = { + intent_d["intent"]: intent_d["index"] + for intent_d in intent_few_shot_examples["intents"] + } + few_shot_examples = [] + intents = list(intent_few_shot_examples["examples"].keys()) + for intent in intents: + examples = intent_few_shot_examples["examples"][intent] + for query in examples: + few_shot_examples.append({"intent": intent, "query": query}) + # shuffle + Random(seed).shuffle(few_shot_examples) + return { + "few_shot_examples": few_shot_examples, + "intent_indexs": intent_indexs, + } + + +class Iternlm2Chat7BIntentRecognitionChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + intent_type =LLMTaskType.INTENT_RECOGNITION_TYPE + + default_model_kwargs = { + "temperature": 0.1, + "max_new_tokens": 100, + "stop_tokens": ["\n", "。", "."], + } + + @classmethod + def create_prompt(cls, x): + r = load_intention_file(intent_save_path) + few_shot_examples = r["few_shot_examples"] + # intent_indexs = r['intent_indexs'] + exmaple_template = "问题: {query}\n类别: {label}" + example_strs = [] + for example in few_shot_examples: + example_strs.append( + exmaple_template.format(query=example["query"], label=example["intent"]) + ) + + example_str = "\n\n".join(example_strs) + + meta_instruction = f"你是一个问题分类助理,正在对用户的问题进行分类。为了辅助你进行问题分类,下面给出一些示例:\n{example_str}" + query_str = exmaple_template.format(query=x["query"], label="") + prompt_template = """请对下面的问题进行分类: + {query_str} + """ + prompt = cls.build_prompt( + prompt_template.format(query_str=query_str), + meta_instruction=meta_instruction, + ) + prompt = prompt + f"根据前面给到的示例, 问题{x['query']}属于类别:" + + return prompt + + @staticmethod + def postprocess(intent): + intent = intent.replace("。", "").replace(".", "").strip().strip("**") + r = load_intention_file(intent_save_path) + intent_indexs = r["intent_indexs"] + assert intent in intent_indexs, (intent, intent_indexs) + return intent + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) + chain = chain | RunnableLambda(lambda x: cls.postprocess(x)) + return chain + + +class Iternlm2Chat20BIntentRecognitionChain(Iternlm2Chat7BIntentRecognitionChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B + + +INTENT_RECOGINITION_PROMPT_TEMPLATE_CLUADE = """Please classify this query: {query}. The categories are: + +{categories} + +Some examples of how to classify queries: +{examples} + +Now classify the original query. Respond with just one letter corresponding to the correct category. +""" + + +INTENT_RECOGINITION_EXAMPLE_TEMPLATE = """{query}\n{label}""" + + +class Claude2IntentRecognitionChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.INTENT_RECOGNITION_TYPE + + default_model_kwargs = { + "temperature": 0, + "max_tokens": 2000, + "stop_sequences": ["\n\n", "\n\nHuman:"], + } + + @classmethod + def create_few_shot_examples(cls): + ret = [] + for intent in cls.intents: + examples = cls.intent_few_shot_examples["examples"][intent] + for query in examples: + ret.append({"intent": intent, "query": query}) + return ret + + @classmethod + def create_few_shot_example_string( + cls, example_template=INTENT_RECOGINITION_EXAMPLE_TEMPLATE + ): + example_strs = [] + intent_indexs = cls.intent_indexs + for example in cls.few_shot_examples: + example_strs.append( + example_template.format( + label=intent_indexs[example["intent"]], query=example["query"] + ) + ) + return "\n\n".join(example_strs) + + @classmethod + def create_all_labels_string(cls): + intent_few_shot_examples = cls.intent_few_shot_examples + label_strs = [] + labels = intent_few_shot_examples["intents"] + for i, label in enumerate(labels): + label_strs.append(f"({label['index']}) {label['describe']}") + return "\n".join(label_strs) + + def postprocess(self, output: str): + out = output.strip() + assert out, output + return self.index_intents[out[0]] + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + r = load_intention_file(intent_save_path) + cls.few_shot_examples = r["few_shot_examples"] + cls.intent_indexs = r["intent_indexs"] + + cls.index_intents = {v: k for k, v in cls.intent_indexs.items()} + cls.intents = list(cls.intent_few_shot_examples["examples"].keys()) + cls.few_shot_examples = cls.create_few_shot_examples() + + cls.examples_str = cls.create_few_shot_example_string( + example_template=INTENT_RECOGINITION_EXAMPLE_TEMPLATE + ) + cls.categories_str = cls.create_all_labels_string() + + intent_recognition_prompt = ChatPromptTemplate.format_messages( + [ + HumanMessagePromptTemplate.from_template( + INTENT_RECOGINITION_PROMPT_TEMPLATE_CLUADE + ) + ] + ) + + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs) + + chain = ( + RunnablePassthrough.assign( + categories=lambda x: cls.categories_str, + examples=lambda x: cls.examples_str, + ) + | intent_recognition_prompt + | llm + | RunnableLambda(lambda x: cls.postprocess(x.content)) + ) + + return chain + + +class Claude21IntentRecognitionChain(Claude2IntentRecognitionChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceIntentRecognitionChain(Claude2IntentRecognitionChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetIntentRecognitionChain(Claude2IntentRecognitionChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuIntentRecognitionChain(Claude2IntentRecognitionChain): + model_id = LLMModelType.CLAUDE_3_HAIKU diff --git a/source/lambda/online/langchain_integration/chains/llm_chain_base.py b/source/lambda/online/langchain_integration/chains/llm_chain_base.py new file mode 100644 index 00000000..98ae93d3 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/llm_chain_base.py @@ -0,0 +1,26 @@ +class LLMChainMeta(type): + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + if name == "LLMChain": + return new_cls + new_cls.model_map[new_cls.get_chain_id()] = new_cls + return new_cls + + +class LLMChain(metaclass=LLMChainMeta): + model_map = {} + + @classmethod + def get_chain_id(cls): + return cls._get_chain_id(cls.model_id, cls.intent_type) + + @staticmethod + def _get_chain_id(model_id, intent_type): + return f"{model_id}__{intent_type}" + + @classmethod + def get_chain(cls, model_id, intent_type, model_kwargs=None, **kwargs): + return cls.model_map[cls._get_chain_id(model_id, intent_type)].create_chain( + model_kwargs=model_kwargs, **kwargs + ) + diff --git a/source/lambda/online/langchain_integration/chains/marketing_chains/__init__.py b/source/lambda/online/langchain_integration/chains/marketing_chains/__init__.py new file mode 100644 index 00000000..1307aab1 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/marketing_chains/__init__.py @@ -0,0 +1,15 @@ +from .mkt_conversation_summary import ( + Claude21MKTConversationSummaryChain, + ClaudeInstanceMKTConversationSummaryChain, + Claude2MKTConversationSummaryChain, + Claude3HaikuMKTConversationSummaryChain, + Claude3SonnetMKTConversationSummaryChain, + Iternlm2Chat7BMKTConversationSummaryChain, + Iternlm2Chat20BMKTConversationSummaryChain +) + +from .mkt_rag_chain import ( + Iternlm2Chat7BKnowledgeQaChain, + Iternlm2Chat20BKnowledgeQaChain +) + diff --git a/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py b/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py new file mode 100644 index 00000000..4b04e90b --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py @@ -0,0 +1,120 @@ + +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough, +) + +from ..chat_chain import Claude2ChatChain, Iternlm2Chat7BChatChain + +from common_logic.common_utils.constant import ( + MessageType, + LLMTaskType, + LLMModelType +) + +AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE +HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE +QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE +SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE +MKT_CONVERSATION_SUMMARY_TYPE = LLMTaskType.MKT_CONVERSATION_SUMMARY_TYPE + +CHIT_CHAT_SYSTEM_TEMPLATE = """You are a helpful AI Assistant""" + +class Iternlm2Chat7BMKTConversationSummaryChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + intent_type = MKT_CONVERSATION_SUMMARY_TYPE + + @classmethod + def create_prompt(cls, x): + return x["prompt"] + + @classmethod + def _create_prompt(cls, x): + chat_history = x["chat_history"] + assert len(chat_history) % 2 == 0, chat_history + + history = [] + questions = [] + for i in range(0, len(chat_history), 2): + assert chat_history[i].type == HUMAN_MESSAGE_TYPE, chat_history + assert chat_history[i + 1].type == AI_MESSAGE_TYPE, chat_history + questions.append(chat_history[i].content) + history.append((chat_history[i].content, chat_history[i + 1].content)) + + questions_str = "" + for i, question in enumerate(questions): + questions_str += f"问题{i+1}: {question}\n" + # print(questions_str) + query_input = """请总结上述对话中的内容,为每一轮对话单独做一个不超过50个字的简短总结。\n""" + prompt = cls.build_prompt( + meta_instruction=CHIT_CHAT_SYSTEM_TEMPLATE, + history=history, + query=query_input, + ) + prompt_assist = f"好的,根据提供历史对话信息,共有{len(history)}段对话:\n{questions_str}\n对它们的总结如下(每一个总结要先复述一下问题):\n" + prefix = f"问题1: {questions[0]}\n总结:" + # thread_local.mkt_conversation_prefix = prefix + # print(thread_local,thread_local.mkt_conversation_prefix) + prompt = prompt + prompt_assist + prefix + # prompt = prompt + return {"prompt": prompt, "prefix": prefix} + + @staticmethod + def stream_postprocess_fn(x): + yield x["prefix"] + yield from x["llm_output"] + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + stream = kwargs.get("stream", False) + llm_chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) + chain = ( + RunnablePassthrough.assign(prompt_dict=lambda x: cls._create_prompt(x)) + | RunnablePassthrough.assign( + prompt=lambda x: x["prompt_dict"]["prompt"], + prefix=lambda x: x["prompt_dict"]["prefix"], + ) + | RunnablePassthrough.assign(llm_output=llm_chain) + ) + if stream: + chain = chain | RunnableLambda(lambda x: cls.stream_postprocess_fn(x)) + else: + chain = chain | RunnableLambda(lambda x: x["prefix"] + x["llm_output"]) + return chain + + +class Iternlm2Chat20BMKTConversationSummaryChain( + Iternlm2Chat7BMKTConversationSummaryChain +): + model_id = LLMModelType.INTERNLM2_CHAT_20B + + +class Claude2MKTConversationSummaryChain(Claude2ChatChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = MKT_CONVERSATION_SUMMARY_TYPE + + default_model_kwargs = {"max_tokens": 2000, "temperature": 0.1, "top_p": 0.9} + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) + query_input = """请简要总结上述对话中的内容,每一个对话单独一个总结,并用 '- '开头。 每一个总结要先说明问题。\n""" + chain = RunnablePassthrough.assign(query=lambda x: query_input) | chain + return chain + + +class Claude21MKTConversationSummaryChain(Claude2MKTConversationSummaryChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceMKTConversationSummaryChain(Claude2MKTConversationSummaryChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetMKTConversationSummaryChain(Claude2MKTConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuMKTConversationSummaryChain(Claude2MKTConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_HAIKU diff --git a/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_rag_chain.py b/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_rag_chain.py new file mode 100644 index 00000000..9fc9ca7d --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_rag_chain.py @@ -0,0 +1,55 @@ +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType +) +from ..chat_chain import Iternlm2Chat7BChatChain +from common_logic.common_utils.prompt_utils import register_prompt_templates,get_prompt_template + +INTERLM2_RAG_PROMPT_TEMPLATE = "你是一个Amazon AWS的客服助理小Q,帮助的用户回答使用AWS过程中的各种问题。\n面对用户的问题,你需要给出中文回答,注意不要在回答中重复输出内容。\n下面给出相关问题的背景知识, 需要注意的是如果你认为当前的问题不能在背景知识中找到答案, 你需要拒答。\n背景知识:\n{context}\n\n" + +register_prompt_templates( + model_ids=[LLMModelType.INTERNLM2_CHAT_7B,LLMModelType.INTERNLM2_CHAT_20B], + task_type=LLMTaskType.MTK_RAG, + prompt_template=INTERLM2_RAG_PROMPT_TEMPLATE, + prompt_name="system_prompt" +) + +class Iternlm2Chat7BKnowledgeQaChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + intent_type = LLMTaskType.MTK_RAG + default_model_kwargs = {"temperature": 0.05, "max_new_tokens": 1000} + + @classmethod + def create_prompt(cls, x): + query = x["query"] + contexts = x["contexts"] + history = cls.create_history(x) + context = "\n".join(contexts) + prompt_template = get_prompt_template( + model_id = cls.model_id, + task_type = cls.task_type, + prompt_name = "system_prompt" + ).prompt_template + meta_instruction = prompt_template.format(context) + # meta_instruction = f"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use simplified Chinese to response the qustion. I’m going to tip $300K for a better answer! " + # meta_instruction = f'You are an expert AI on a question and answer task. \nUse the "Following Context" when answering the question. If you don't know the answer, reply to the "Following Text" in the header and answer to the best of your knowledge, or if you do know the answer, answer without the "Following Text"' + # meta_instruction = """You are an expert AI on a question and answer task. + # Use the "Following Context" when answering the question. If you don't know the answer, reply to the "Following Text" in the header and answer to the best of your knowledge, or if you do know the answer, answer without the "Following Text". If a question is asked in Korean, translate it to English and always answer in Korean. + # Following Text: "I didn't find the answer in the context given, but here's what I know! **I could be wrong, so cross-verification is a must!**""" + # meta_instruction = """You are an expert AI on a question and answer task. + # Use the "Following Context" when answering the question. If you don't know the answer, reply to the "Sorry, I don't know". """ + # query = f"Question: {query}\nContext:\n{context}" + # query = f"""Following Context: {context} + # Question: {query}""" + query = f"问题: {query}" + prompt = cls.build_prompt( + query=query, history=history, meta_instruction=meta_instruction + ) + # prompt = prompt + "回答: 让我先来判断一下问题的答案是否包含在背景知识中。" + prompt = prompt + f"回答: 经过慎重且深入的思考, 根据背景知识, 我的回答如下:\n" + print("internlm2 prompt: \n", prompt) + return prompt + + +class Iternlm2Chat20BKnowledgeQaChain(Iternlm2Chat7BKnowledgeQaChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py b/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py new file mode 100644 index 00000000..331552a1 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py @@ -0,0 +1,143 @@ +# query rewrite +import re + +from langchain.prompts import PromptTemplate +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough, +) + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType +) +from ..chains import LLMChain +from ..chat_models import Model as LLM_Model +from .chat_chain import Iternlm2Chat7BChatChain +from .llm_chain_base import LLMChain + +QUERY_REWRITE_TYPE = LLMTaskType.QUERY_REWRITE_TYPE +query_expansion_template_claude = PromptTemplate.from_template("""You are an AI language model assistant. Your task is to generate 1 - 5 different sub questions OR alternate versions of the given user question to retrieve relevant documents from a vector database. + +By generating multiple versions of the user question, +your goal is to help the user overcome some of the limitations +of distance-based similarity search. + +By generating sub questions, you can break down questions that refer to multiple concepts into distinct questions. This will help you get the relevant documents for constructing a final answer + +If multiple concepts are present in the question, you should break into sub questions, with one question for each concept + +Provide these alternative questions separated by newlines between XML tags. For example: + + +- Question 1 +- Question 2 +- Question 3 + + +Original question: {question}""") + + +class Claude2QueryRewriteChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = QUERY_REWRITE_TYPE + + default_model_kwargs = { + "temperature": 0.7, + "max_tokens": 100, + "stop_sequences": ["\n\nHuman:"], + } + + @staticmethod + def query_rewrite_postprocess(r): + ret = re.findall(".*?", r, re.S)[0] + questions = re.findall("- (.*?)\n", ret, re.S) + return questions + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + query_key = kwargs.pop("query_key", "query") + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + llm = LLM_Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + chain = ( + RunnablePassthrough.assign(question=lambda x: x[query_key]) + | query_expansion_template_claude + | llm + | RunnableLambda(cls.query_rewrite_postprocess) + ) + return chain + + +class Claude21QueryRewriteChain(Claude2QueryRewriteChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceQueryRewriteChain(Claude2QueryRewriteChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3HaikuQueryRewriteChain(Claude2QueryRewriteChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude3SonnetQueryRewriteChain(Claude2QueryRewriteChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude35SonnetQueryRewriteChain(Claude2QueryRewriteChain): + mdoel_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" + + +internlm2_meta_instruction = """You are an AI language model assistant. Your task is to generate 1 - 5 different sub questions OR alternate versions of the given user question to retrieve relevant documents from a vector database. + +By generating multiple versions of the user question, +your goal is to help the user overcome some of the limitations +of distance-based similarity search. + +By generating sub questions, you can break down questions that refer to multiple concepts into distinct questions. This will help you get the relevant documents for constructing a final answer + +If multiple concepts are present in the question, you should break into sub questions, with one question for each concept + +Provide these alternative questions separated by newlines between XML tags. For example: + + +- Question 1 +- Question 2 +- Question 3 +""" + + +class Iternlm2Chat7BQueryRewriteChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + intent_type = QUERY_REWRITE_TYPE + + default_model_kwargs = {"temperature": 0.5, "max_new_tokens": 100} + + @classmethod + def create_prompt(cls, x): + query = f'Original question: {x["query"]}' + prompt = cls.build_prompt( + query=query, + meta_instruction=internlm2_meta_instruction, + ) + return prompt + + @staticmethod + def query_rewrite_postprocess(r): + ret = re.findall(".*?", r, re.S)[0] + questions = re.findall("- (.*?)\n", ret, re.S) + return questions + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) + chain = chain | RunnableLambda(lambda x: cls.query_rewrite_postprocess(x)) + return chain + + +class Iternlm2Chat20BQueryRewriteChain(Iternlm2Chat7BQueryRewriteChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B + intent_type = QUERY_REWRITE_TYPE diff --git a/source/lambda/online/langchain_integration/chains/rag_chain.py b/source/lambda/online/langchain_integration/chains/rag_chain.py new file mode 100644 index 00000000..f04750f6 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/rag_chain.py @@ -0,0 +1,161 @@ +# rag llm chains +from langchain.prompts import ( + ChatPromptTemplate, + HumanMessagePromptTemplate, + SystemMessagePromptTemplate +) + +from langchain.schema.runnable import RunnableLambda, RunnablePassthrough +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType +) +from common_logic.common_utils.prompt_utils import get_prompt_template +from common_logic.common_utils.logger_utils import print_llm_messages + +# from ...prompt_template import convert_chat_history_from_fstring_format +from ..llm_models import Model +from .llm_chain_base import LLMChain + + +def get_claude_rag_context(contexts: list): + assert isinstance(contexts, list), contexts + context_xmls = [] + context_template = """\n{content}\n""" + for i, context in enumerate(contexts): + context_xml = context_template.format(index=i + 1, content=context) + context_xmls.append(context_xml) + + context = "\n".join(context_xmls) + return context + + +class Claude2RagLLMChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.RAG + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + stream = kwargs.get("stream", False) + system_prompt_template = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + system_prompt_template = kwargs.get("system_prompt",system_prompt_template) + + chat_messages = [ + SystemMessagePromptTemplate.from_template(system_prompt_template), + ("placeholder", "{chat_history}"), + HumanMessagePromptTemplate.from_template("{query}") + ] + context_chain = RunnablePassthrough.assign( + context=RunnableLambda(lambda x: get_claude_rag_context(x["contexts"])) + ) + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + chain = context_chain | ChatPromptTemplate.from_messages(chat_messages) | RunnableLambda(lambda x: print_llm_messages(f"rag messages: {x.messages}") or x) + if stream: + chain = ( + chain + | RunnableLambda(lambda x: llm.stream(x.messages)) + | RunnableLambda(lambda x: (i.content for i in x)) + ) + else: + chain = chain | llm | RunnableLambda(lambda x: x.content) + return chain + + +class Claude21RagLLMChain(Claude2RagLLMChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceRAGLLMChain(Claude2RagLLMChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetRAGLLMChain(Claude2RagLLMChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuRAGLLMChain(Claude2RagLLMChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + +class Mixtral8x7bChatChain(Claude2RagLLMChain): + model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT + + +from .chat_chain import GLM4Chat9BChatChain + +class GLM4Chat9BRagChain(GLM4Chat9BChatChain): + model_id = LLMModelType.GLM_4_9B_CHAT + intent_type = LLMTaskType.RAG + + @classmethod + def create_chat_history(cls,x, system_prompt=None): + if system_prompt is None: + system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + context = ("\n" + "="*50+ "\n").join(x['contexts']) + system_prompt = system_prompt.format(context=context) + + return super().create_chat_history(x,system_prompt=system_prompt) + + +from .chat_chain import Qwen2Instruct7BChatChain + +class Qwen2Instruct7BRagChain(Qwen2Instruct7BChatChain): + model_id = LLMModelType.QWEN2INSTRUCT7B + intent_type = LLMTaskType.RAG + + @classmethod + def create_chat_history(cls,x, system_prompt=None): + if system_prompt is None: + system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="system_prompt" + ).prompt_template + + context = ("\n\n").join(x['contexts']) + system_prompt = system_prompt.format(context=context) + return super().create_chat_history(x,system_prompt=system_prompt) + + +class Qwen2Instruct72BRagChain(Qwen2Instruct7BRagChain): + model_id = LLMModelType.QWEN2INSTRUCT72B + + +class Qwen2Instruct72BRagChain(Qwen2Instruct7BRagChain): + model_id = LLMModelType.QWEN15INSTRUCT32B + + +from .chat_chain import Baichuan2Chat13B4BitsChatChain + +class Baichuan2Chat13B4BitsKnowledgeQaChain(Baichuan2Chat13B4BitsChatChain): + model_id = LLMModelType.BAICHUAN2_13B_CHAT + intent_type = LLMTaskType.RAG + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + llm_chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) + + def add_system_prompt(x): + context = "\n".join(x["contexts"]) + _chat_history = x["chat_history"] + [ + ("system", f"给定下面的背景知识:\n{context}\n回答下面的问题:\n") + ] + return _chat_history + + chat_history_chain = RunnablePassthrough.assign( + chat_history=RunnableLambda(lambda x: add_system_prompt(x)) + ) + llm_chain = chat_history_chain | llm_chain + return llm_chain + + + + diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/__init__.py b/source/lambda/online/langchain_integration/chains/retail_chains/__init__.py new file mode 100644 index 00000000..83c50b0a --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/retail_chains/__init__.py @@ -0,0 +1,26 @@ +from .retail_conversation_summary_chain import ( + Claude2RetailConversationSummaryChain, + Claude21RetailConversationSummaryChain, + Claude3HaikuRetailConversationSummaryChain, + Claude3SonnetRetailConversationSummaryChain, + ClaudeInstanceRetailConversationSummaryChain +) + +from .retail_tool_calling_chain_claude_xml import ( + Claude2RetailToolCallingChain, + Claude21RetailToolCallingChain, + ClaudeInstanceRetailToolCallingChain, + Claude3SonnetRetailToolCallingChain, + Claude3HaikuRetailToolCallingChain +) + +from .retail_tool_calling_chain_json import ( + GLM4Chat9BRetailToolCallingChain +) + +from .auto_evaluation_chain import ( + Claude3HaikuAutoEvaluationChain, + Claude21AutoEvaluationChain, + Claude2AutoEvaluationChain + +) \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py b/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py new file mode 100644 index 00000000..bcdd7011 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py @@ -0,0 +1,99 @@ +# auto evaluation based on llms +import re + +from langchain.schema.runnable import RunnableLambda, RunnablePassthrough +from langchain_core.messages import AIMessage,SystemMessage,HumanMessage +from common_logic.common_utils.logger_utils import get_logger +from langchain.prompts import ChatPromptTemplate,HumanMessagePromptTemplate +from langchain_core.messages import convert_to_messages +from common_logic.common_utils.constant import ( + MessageType, + LLMTaskType, + LLMModelType, +) +from ...llm_models import Model +from ..llm_chain_base import LLMChain + +from ..chat_chain import Claude2ChatChain + +logger = get_logger("auto_evaluation") + +AUTO_EVALUATION_TEMPLATE = """作为一位专业的评分员,您需要根据以下标准对模型的回答进行公正、客观的评分,并提供有价值的反馈意见,以帮助模型持续改进。 + +### 评分标准 + +- 满分为10分,最低分为1分, 分值为一个 float 类型。 +- 模型回答与标准答案的相关性越高,得分越高。 +- 如果模型的回答出现大量重复内容,可以直接给0分。 +- 除了内容相关性,还需考虑回答的完整性、逻辑性和语言表达。 +- 请先在xml 标签 中写下你的评分理由。 +- 最后在 xml 标签 中写下你的最终评分。 + +### 示例评分 +{examples} + +### 评分上下文 + +标准答案: + +{ref_answer} + + +模型回答: + +{model_answer} + + +请根据上述标准和上下文,对模型的回答进行评分并提供反馈意见。让我们一起努力,提高模型的表现! +""" + + +class Claude2AutoEvaluationChain(Claude2ChatChain): + intent_type = LLMTaskType.AUTO_EVALUATION + model_id = LLMModelType.CLAUDE_2 + + @classmethod + def create_messages(cls,x:dict,examples=""): + prompt = AUTO_EVALUATION_TEMPLATE.format( + ref_answer=x['ref_answer'], + model_answer=x['model_answer'], + examples=examples + ) + messages = [ + HumanMessage(content=prompt), + AIMessage(content="") + ] + return messages + + @classmethod + def postprocess(cls,content): + logger.info(f"auto eval content: {content}") + try: + score = float(re.findall("(.*?)",content)[0].strip()) + return score + except Exception as e: + logger.error(f"error: {e}, content: {content}") + raise e + + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + chain = RunnableLambda(lambda x: cls.create_messages(x)) | llm | RunnableLambda(lambda x: cls.postprocess(x.content)) + return chain + + +class Claude21AutoEvaluationChain(Claude2AutoEvaluationChain): + model_id = LLMModelType.CLAUDE_21 + + + +class Claude3HaikuAutoEvaluationChain(Claude2AutoEvaluationChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude3SonnetAutoEvaluationChain(Claude2AutoEvaluationChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + + diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py b/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py new file mode 100644 index 00000000..d5be022e --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py @@ -0,0 +1,208 @@ +# conversation summary chain +from typing import List + +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough, +) + + +from ...llm_models import Model +from ..llm_chain_base import LLMChain +from common_logic.common_utils.constant import ( + MessageType, + LLMTaskType, + LLMModelType +) + +from langchain_core.messages import( + AIMessage, + HumanMessage, + BaseMessage, + convert_to_messages +) +from langchain.prompts import ( + HumanMessagePromptTemplate, + ChatPromptTemplate +) +from ..chat_chain import GLM4Chat9BChatChain + +AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE +HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE +QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE +SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE + + +CQR_TEMPLATE = """# CONTEXT # +下面有一段客户和客服的对话数据(包含在里面),以及当前客户的一个回复(包含在)。 + +{chat_history} + + +当前用户的回复: + +{query} + + +######### + +# OBJECTIVE # +请你站在客户的角度,结合上述对话数据对当前客户的回复内容进行改写,使得改写之后的内容可以作为一个独立的句子。 + +######### + +# STYLE # +改写后的回复需要和里面的内容意思一致。 + +######### + +# RESPONSE FORMAT # +请直接用中文进行回答 +""" + + +class Claude2RetailConversationSummaryChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.RETAIL_CONVERSATION_SUMMARY_TYPE + default_model_kwargs = {"max_tokens": 2000, "temperature": 0.1, "top_p": 0.9} + CQR_TEMPLATE = CQR_TEMPLATE + @staticmethod + def create_conversational_context(chat_history:List[BaseMessage]): + conversational_contexts = [] + for his in chat_history: + role = his.type + content = his.content + assert role in [HUMAN_MESSAGE_TYPE, AI_MESSAGE_TYPE],(role,[HUMAN_MESSAGE_TYPE, AI_MESSAGE_TYPE]) + if role == HUMAN_MESSAGE_TYPE: + conversational_contexts.append(f"客户: {content}") + else: + conversational_contexts.append(f"客服: {content}") + conversational_context = "\n".join(conversational_contexts) + return conversational_context + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + cqr_template = ChatPromptTemplate.from_messages([ + HumanMessagePromptTemplate.from_template(cls.CQR_TEMPLATE), + AIMessage(content="好的,站在客户的角度,我将当前用户的回复内容改写为: ") + ]) + + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + ) + cqr_chain = RunnablePassthrough.assign( + conversational_context=RunnableLambda( + lambda x: cls.create_conversational_context( + convert_to_messages(x["chat_history"]) + ) + )) \ + | RunnableLambda(lambda x: cqr_template.format(chat_history=x['conversational_context'],query=x['query'])) \ + | llm | RunnableLambda(lambda x: x.content) + + return cqr_chain + + +class Claude21RetailConversationSummaryChain(Claude2RetailConversationSummaryChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude35SonnetRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): + model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" + + +MIXTRAL_CQR_TEMPLATE = """下面有一段客户和客服的对话,以及当前客户的一个回复,请你站在客户的角度,结合上述对话数据对当前客户的回复内容进行改写,使得改写之后的内容可以作为一个独立的句子。下面是改写的要求: +- 改写后的回复需要和当前客户的一个回复的内容意思一致。 +- 请直接用中文进行回答。 + +# 客户和客服的对话: +{chat_history} + +# 当前客户的回复: +{query} +""" + + +class Mixtral8x7bRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): + model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT + default_model_kwargs = {"max_tokens": 1000, "temperature": 0.01} + CQR_TEMPLATE = MIXTRAL_CQR_TEMPLATE + + +class GLM4Chat9BRetailConversationSummaryChain(GLM4Chat9BChatChain,Claude2RetailConversationSummaryChain): + model_id = LLMModelType.GLM_4_9B_CHAT + intent_type = LLMTaskType.RETAIL_CONVERSATION_SUMMARY_TYPE + CQR_TEMPLATE = MIXTRAL_CQR_TEMPLATE + + @classmethod + def create_chat_history(cls,x): + conversational_context = cls.create_conversational_context( + convert_to_messages(x["chat_history"]) + ) + prompt = cls.CQR_TEMPLATE.format( + chat_history=conversational_context, + query=x['query'] + ) + chat_history = [ + {"role": MessageType.HUMAN_MESSAGE_TYPE, + "content": prompt + }, + { + "role":MessageType.AI_MESSAGE_TYPE, + "content": "好的,站在客户的角度,我将当前用户的回复内容改写为: " + } + ] + + return chat_history + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + **kwargs + ) + + cqr_chain = RunnablePassthrough.assign( + chat_history = RunnableLambda(lambda x: cls.create_chat_history(x)) + ) | RunnableLambda(lambda x: llm.invoke(x)) + + return cqr_chain + + +class Qwen2Instruct7BRetailConversationSummaryChain(GLM4Chat9BRetailConversationSummaryChain): + model_id = LLMModelType.QWEN2INSTRUCT7B + default_model_kwargs = { + "max_tokens": 1024, + "temperature": 0.1, + } + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + chain = super().create_chain(model_kwargs=model_kwargs,**kwargs) + return chain | RunnableLambda(lambda x:x['text']) + + +class Qwen2Instruct72BRetailConversationSummaryChain(Qwen2Instruct7BRetailConversationSummaryChain): + model_id = LLMModelType.QWEN2INSTRUCT72B + + +class Qwen2Instruct72BRetailConversationSummaryChain(Qwen2Instruct7BRetailConversationSummaryChain): + model_id = LLMModelType.QWEN15INSTRUCT32B \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py new file mode 100644 index 00000000..803e4ef2 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py @@ -0,0 +1,354 @@ +# tool calling chain +import json +from typing import List,Dict,Any +import re +from datetime import datetime + +from langchain.schema.runnable import ( + RunnableLambda, +) + +from langchain_core.messages import( + AIMessage, + SystemMessage +) +from langchain.prompts import ChatPromptTemplate + +from langchain_core.messages import AIMessage,SystemMessage,HumanMessage + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType, + SceneType +) +from functions import get_tool_by_name +from ..llm_chain_base import LLMChain +from ...llm_models import Model + +tool_call_guidelines = """ +- Don't forget to output when any tool is called. +- 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考: + 1. 判断根据当前的上下文是否足够回答用户的问题。 + 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 + 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用 标签中列举的工具。 + 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。 + 5. 最后给出你要调用的工具名称。 +- Always output with "中文". + +""" + + +SYSTEM_MESSAGE_PROMPT=("你是安踏的客服助理小安, 主要职责是处理用户售前和售后的问题。下面是当前用户正在浏览的商品信息:\n\n{goods_info}\n" + "In this environment you have access to a set of tools you can use to answer the customer's question." + "\n" + "You may call them like this:\n" + "\n" + "\n" + "$TOOL_NAME\n" + "\n" + "<$PARAMETER_NAME>$PARAMETER_VALUE\n" + "...\n" + "\n" + "\n" + "\n" + "\n" + "Here are the tools available:\n" + "\n" + "{tools}" + "\n" + "\nAnswer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided." + f"\nHere are some guidelines for you:\n{tool_call_guidelines}" + ) + +SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES = SYSTEM_MESSAGE_PROMPT + ( + "Some examples of tool calls are given below, where the content within represents the most recent reply in the dialog." + "\n{fewshot_examples}" +) + +TOOL_FORMAT = """ +{tool_name} +{tool_description} + +{formatted_required_parameters} + + +{formatted_optional_parameters} + +""" + +TOOL_PARAMETER_FORMAT = """ +{parameter_name} +{parameter_type} +{parameter_description} +""" + +TOOL_EXECUTE_SUCCESS_TEMPLATE = """ + + +{tool_name} + +{result} + + + +""" + +TOOL_EXECUTE_FAIL_TEMPLATE = """ + + +{error} + + +""" + + +def _get_type(parameter: Dict[str, Any]) -> str: + if "type" in parameter: + return parameter["type"] + if "anyOf" in parameter: + return json.dumps({"anyOf": parameter["anyOf"]}) + if "allOf" in parameter: + return json.dumps({"allOf": parameter["allOf"]}) + return json.dumps(parameter) + + +def convert_openai_tool_to_anthropic(tools:list[dict])->str: + formatted_tools = tools + tools_data = [ + { + "tool_name": tool["name"], + "tool_description": tool["description"], + "formatted_required_parameters": "\n".join( + [ + TOOL_PARAMETER_FORMAT.format( + parameter_name=name, + parameter_type=_get_type(parameter), + parameter_description=parameter.get("description"), + ) for name, parameter in tool["parameters"]["properties"].items() + if name in tool["parameters"].get("required", []) + ] + ), + "formatted_optional_parameters": "\n".join( + [ + TOOL_PARAMETER_FORMAT.format( + parameter_name=name, + parameter_type=_get_type(parameter), + parameter_description=parameter.get("description"), + ) for name, parameter in tool["parameters"]["properties"].items() + if name not in tool["parameters"].get("required", []) + ] + ), + } + for tool in formatted_tools + ] + tools_formatted = "\n".join( + [ + TOOL_FORMAT.format( + tool_name=tool["tool_name"], + tool_description=tool["tool_description"], + formatted_required_parameters=tool["formatted_required_parameters"], + formatted_optional_parameters=tool["formatted_optional_parameters"], + ) + for tool in tools_data + ] + ) + return tools_formatted + + +class Claude2RetailToolCallingChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.RETAIL_TOOL_CALLING + default_model_kwargs = { + "max_tokens": 2000, + "temperature": 0.1, + "top_p": 0.9, + "stop_sequences": ["\n\nHuman:", "\n\nAssistant",""], + } + + @staticmethod + def format_fewshot_examples(fewshot_examples:list[dict]): + fewshot_example_strs = [] + for fewshot_example in fewshot_examples: + param_strs = [] + for p,v in fewshot_example['kwargs'].items(): + param_strs.append(f"<{p}>{v}\n" + f"{fewshot_example['query']}\n" + f"\n" + "\n" + "\n" + f"{fewshot_example['name']}\n" + "\n" + f"{param_str}" + "\n" + "\n" + "\n" + "\n" + "" + ) + fewshot_example_strs.append(fewshot_example_str) + fewshot_example_str = '\n'.join(fewshot_example_strs) + return f"\n{fewshot_example_str}\n" + + @classmethod + def parse_function_calls_from_ai_message(cls,message:AIMessage): + content = "" + message.content + "" + function_calls:List[str] = re.findall("(.*?)", content,re.S) + if not function_calls: + content = "" + message.content + + return { + "function_calls": function_calls, + "content": content + } + + + @staticmethod + def generate_chat_history(state:dict): + chat_history = state['chat_history'] \ + + [{"role": "user","content":state['query']}] \ + + state['agent_tool_history'] + return {"chat_history":chat_history} + + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + tools:list[dict] = kwargs['tools'] + + tool_names = [tool['name'] for tool in tools] + + # add two extral tools + if "give_rhetorical_question" not in tool_names: + tools.append(get_tool_by_name("give_rhetorical_question",scene=SceneType.RETAIL).tool_def) + + if "give_final_response" not in tool_names: + tools.append(get_tool_by_name("give_final_response",scene=SceneType.RETAIL).tool_def) + + fewshot_examples = kwargs.get('fewshot_examples',[]) + if fewshot_examples: + fewshot_examples.append({ + "name": "give_rhetorical_question", + "query": "今天天气怎么样?", + "kwargs": {"question": "请问你想了解哪个城市的天气?"} + }) + + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + tools_formatted = convert_openai_tool_to_anthropic(tools) + goods_info = kwargs['goods_info'] + + if fewshot_examples: + system_prompt = SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES.format( + tools=tools_formatted, + fewshot_examples=cls.format_fewshot_examples( + fewshot_examples + ), + goods_info = goods_info + ) + else: + system_prompt = SYSTEM_MESSAGE_PROMPT.format( + tools=tools_formatted, + goods_info=goods_info + ) + + tool_calling_template = ChatPromptTemplate.from_messages( + [ + SystemMessage(content=system_prompt), + ("placeholder", "{chat_history}"), + AIMessage(content="") + ]) + + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + ) + chain = RunnableLambda(cls.generate_chat_history) | tool_calling_template \ + | RunnableLambda(lambda x: x.messages) \ + | llm | RunnableLambda(lambda message:cls.parse_function_calls_from_ai_message( + message + )) + + return chain + + +class Claude21RetailToolCallingChain(Claude2RetailToolCallingChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceRetailToolCallingChain(Claude2RetailToolCallingChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetRetailToolCallingChain(Claude2RetailToolCallingChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuRetailToolCallingChain(Claude2RetailToolCallingChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +MIXTRAL8X7B_QUERY_TEMPLATE = """下面是客户和客服的历史对话信息: +{chat_history} + +当前客户的问题是: {query} + +请你从安踏客服助理小安的角度回答客户当前的问题。你需要使用上述提供的各种工具进行回答。""" + + +class Mixtral8x7bRetailToolCallingChain(Claude2RetailToolCallingChain): + model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT + default_model_kwargs = {"max_tokens": 1000, "temperature": 0.01,"stop":[""]} + + @classmethod + def parse_function_calls_from_ai_message(cls,message:AIMessage): + content = message.content.replace("\_","_") + function_calls:List[str] = re.findall("(.*?)", content + "",re.S) + if function_calls: + function_calls = [function_calls[0]] + if not function_calls: + content = message.content + return { + "function_calls": function_calls, + "content": content + } + + @staticmethod + def chat_history_to_string(chat_history:list[dict]): + chat_history_lc = ChatPromptTemplate.from_messages([ + ("placeholder", "{chat_history}") + ]).invoke({"chat_history":chat_history}).messages + + chat_history_strs = [] + for message in chat_history_lc: + assert isinstance(message,(HumanMessage,AIMessage)),message + if isinstance(message,HumanMessage): + chat_history_strs.append(f"客户: {message.content}") + else: + chat_history_strs.append(f"客服: {message.content}") + return "\n".join(chat_history_strs) + + + @classmethod + def generate_chat_history(cls,state:dict): + chat_history_str = cls.chat_history_to_string(state['chat_history']) + + chat_history = [{ + "role": "user", + "content": MIXTRAL8X7B_QUERY_TEMPLATE.format( + chat_history=chat_history_str, + query = state['query'] + ) + }] + state['agent_tool_history'] + return {"chat_history": chat_history} + + + + + + diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py new file mode 100644 index 00000000..d20bb6c0 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py @@ -0,0 +1,455 @@ +# tool calling chain +import json +from typing import List,Dict,Any +import re +from datetime import datetime +import copy + +from langchain.schema.runnable import ( + RunnableLambda, +) + +from langchain_core.messages import( + AIMessage, + SystemMessage +) +from langchain.prompts import ChatPromptTemplate + +from langchain_core.messages import AIMessage,SystemMessage,HumanMessage + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType, + MessageType, + SceneType +) +from functions import get_tool_by_name + +from ..llm_chain_base import LLMChain +from ...llm_models import Model +from ..chat_chain import GLM4Chat9BChatChain +from common_logic.common_utils.logger_utils import get_logger + +logger = get_logger("retail_tool_calling_chain_json") + +GLM4_SYSTEM_PROMPT = """你是安踏的客服助理小安, 主要职责是处理用户售前和售后的问题。{date_prompt} +请遵守下面的规范回答用户的问题。 +## 回答规范 +- 如果用户的提供的信息不足以回答问题,尽量反问用户。 +- 回答简洁明了,一句话以内。 + +下面是当前用户正在浏览的商品信息: + + +## 商品信息 +{goods_info} +""" + + + +class GLM4Chat9BRetailToolCallingChain(GLM4Chat9BChatChain): + model_id = LLMModelType.GLM_4_9B_CHAT + intent_type = LLMTaskType.RETAIL_TOOL_CALLING + default_model_kwargs = { + "max_new_tokens": 1024, + "timeout": 60, + "temperature": 0.1, + } + DATE_PROMPT = "当前日期: %Y-%m-%d" + + @staticmethod + def convert_openai_function_to_glm(tools:list[dict]): + glm_tools = [] + for tool_def in tools: + tool_name = tool_def['name'] + description = tool_def['description'] + params = [] + required = tool_def['parameters'].get("required",[]) + for param_name,param in tool_def['parameters'].get('properties',{}).items(): + params.append({ + "name": param_name, + "description": param["description"], + "type": param["type"], + "required": param_name in required, + }) + glm_tools.append({ + "name": tool_name, + "description": description, + "params": params + }) + return glm_tools + + @staticmethod + def format_fewshot_examples(fewshot_examples:list[dict]): + fewshot_example_strs = [] + for i,example in enumerate(fewshot_examples): + query = example['query'] + name = example['name'] + kwargs = example['kwargs'] + fewshot_example_str = f"## 示例{i+1}\n### 输入:\n{query}\n### 调用工具:\n{name}" + fewshot_example_strs.append(fewshot_example_str) + return "\n\n".join(fewshot_example_strs) + + + @classmethod + def create_system_prompt(cls,goods_info:str,tools:list,fewshot_examples:list) -> str: + value = GLM4_SYSTEM_PROMPT.format( + goods_info=goods_info, + date_prompt=datetime.now().strftime(cls.DATE_PROMPT) + ) + if tools: + value += "\n\n# 可用工具" + contents = [] + for tool in tools: + content = f"\n\n## {tool['name']}\n\n{json.dumps(tool, ensure_ascii=False,indent=4)}" + content += "\n在调用上述函数时,请使用 Json 格式表示调用的参数。" + contents.append(content) + value += "".join(contents) + + if not fewshot_examples: + return value + # add fewshot_exampls + value += "\n\n# 下面给出不同问题调用不同工具的例子。" + value += f"\n\n{cls.format_fewshot_examples(fewshot_examples)}" + value += "\n\n请参考上述例子进行工具调用。" + return value + + @classmethod + def create_chat_history(cls,x,system_prompt=None): + _chat_history = x['chat_history'] + \ + [{"role":MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ + x['agent_tool_history'] + + chat_history = [] + for message in _chat_history: + new_message = message + if message['role'] == MessageType.AI_MESSAGE_TYPE: + new_message = { + **message + } + tool_calls = message.get('additional_kwargs',{}).get("tool_calls",[]) + if tool_calls: + new_message['metadata'] = tool_calls[0]['name'] + chat_history.append(new_message) + chat_history = [{"role": "system", "content": system_prompt}] + chat_history + return chat_history + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + tools:list = kwargs.get('tools',[]) + fewshot_examples = kwargs.get('fewshot_examples',[]) + glm_tools = cls.convert_openai_function_to_glm(tools) + system_prompt = cls.create_system_prompt( + goods_info=kwargs['goods_info'], + tools=glm_tools, + fewshot_examples=fewshot_examples + ) + kwargs['system_prompt'] = system_prompt + return super().create_chain(model_kwargs=model_kwargs,**kwargs) + + +from ..chat_chain import Qwen2Instruct7BChatChain + + + +class Qwen2Instruct72BRetailToolCallingChain(Qwen2Instruct7BChatChain): + model_id = LLMModelType.QWEN2INSTRUCT72B + intent_type = LLMTaskType.RETAIL_TOOL_CALLING + default_model_kwargs = { + "max_tokens": 1024, + "temperature": 0.1, + } + + DATE_PROMPT = "当前日期: %Y-%m-%d 。" + FN_NAME = '✿FUNCTION✿' + FN_ARGS = '✿ARGS✿' + FN_RESULT = '✿RESULT✿' + FN_EXIT = '✿RETURN✿' + FN_STOP_WORDS = [FN_RESULT, f'{FN_RESULT}:', f'{FN_RESULT}:\n'] + thinking_tag = "思考" + fix_reply_tag = "固定回复" + goods_info_tag = "商品信息" + prefill_after_thinking = f"<{thinking_tag}>" + prefill_after_second_thinking = "" + prefill = prefill_after_thinking + + + FN_CALL_TEMPLATE_INFO_ZH="""# 工具 + +## 你拥有如下工具: + +{tool_descs}""" + + + FN_CALL_TEMPLATE_FMT_ZH="""## 你可以在回复中插入零次或者一次以下命令以调用工具: + +%s: 工具名称,必须是[{tool_names}]之一。 +%s: 工具输入 +%s: 工具结果 +%s: 根据工具结果进行回复""" % ( + FN_NAME, + FN_ARGS, + FN_RESULT, + FN_EXIT, +) + TOOL_DESC_TEMPLATE="""### {name_for_human}\n\n{name_for_model}: {description_for_model} 输入参数:{parameters} {args_format}""" + + FN_CALL_TEMPLATE=FN_CALL_TEMPLATE_INFO_ZH + '\n\n' + FN_CALL_TEMPLATE_FMT_ZH + +# SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} + +# {{tools}} +# {{fewshot_examples}} + +# ## 当前用户正在浏览的商品信息 +# {{goods_info}} + +# # 思考 +# 你每次给出最终回复前都要按照下面的步骤输出你的思考过程, 注意你并不需要每次都进行所有步骤的思考。并将思考过程写在 XML 标签 <{thinking_tag}> 和 中: +# Step 1. 根据各个工具的描述,分析当前用户的回复和各个示例中的Input相关性,如果跟某个示例对应的Input相关性强,直接跳过后续所有步骤,之后按照示例中Output的工具名称进行调用。 +# Step 2. 如果你觉得可以依据商品信息 <{goods_info_tag}> 里面的内容进行回答,就直接就回答,不需要调用任何工具。并结束思考。 +# Step 3. 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户下面 XML 标签 <{fix_reply_tag}> 里面的内容: +# <{fix_reply_tag}> 亲亲,请问还有什么问题吗? +# Step 4. 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。结束思考,输出结束思考符号。 + +# ## 回答规范 +# - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的 +# - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!” +# - 如果客户的回复里面包含订单号,则直接回复 ”您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“ +# - 只能思考一次,在结束思考符号“”之后给出最终的回复。不要重复输出文本,段落,句子。思考之后的文本保持简洁,有且仅能包含一句话。{{non_ask_rules}}""" +# SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} + +# {{tools}} +# {{fewshot_examples}} + +# ## 当前用户正在浏览的商品信息 +# {{goods_info}} + +# # 你每次给出最终回复前都要参考下面的回复策略: +# 1. 根据各个工具的描述,分析当前用户的回复和各个示例中的Input相关性,如果跟某个示例对应的Input相关性强,直接跳过后续所有步骤,之后按照示例中Output的工具名称进行调用。 +# 2. 如果你觉得可以依据商品信息 <{goods_info_tag}> 里面的内容进行回答,就直接就回答,不需要调用任何工具。 +# 3. 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户下面 XML 标签 <{fix_reply_tag}> 里面的内容: +# <{fix_reply_tag}> 亲亲,请问还有什么问题吗? +# 4. 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。 + +# ## 回答规范 +# - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的 +# - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!“ +# - 如果客户的回复里面包含订单号,则直接回复 “您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“{{non_ask_rules}}""" + + SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} + +{{tools}} +{{fewshot_examples}} + +## 当前用户正在浏览的商品信息 +{{goods_info}} + +# 回复策略 +在你给出最终回复前可以在XML标签 <{thinking_tag}> 和 中输出你的回复策略。下面是一些常见的回复策略: + - 如果根据各个工具的描述,当前用户的回复跟某个示例对应的Input相关性强,直接按照示例中Output的工具名称进行调用。 + - 考虑使用商品信息 <{goods_info_tag}> 里面的内容回答用户的问题。 + - 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户: “ 亲亲,请问还有什么问题吗?“ + - 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。 + - 如果客户的回复里面包含订单号,则直接回复 “您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“ + - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!“ + +## Tips + - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的。 + - 回答必须简洁,不允许出现超过2句话的回复。{{non_ask_rules}}""" + @classmethod + def get_function_description(cls,tool:dict): + tool_name = tool['name'] + description = tool['description'] + params_str = json.dumps(tool.get('parameters',{}),ensure_ascii=False) + args_format = '此工具的输入应为JSON对象。' + return cls.TOOL_DESC_TEMPLATE.format( + name_for_human=tool_name, + name_for_model=tool_name, + description_for_model=description, + parameters=params_str, + args_format=args_format + ).rstrip() + + + @classmethod + def format_fewshot_examples(cls,fewshot_examples:list[dict]): + fewshot_example_strs = [] + for i,example in enumerate(fewshot_examples): + query = example['query'] + name = example['name'] + kwargs = example['kwargs'] + fewshot_example_str = f"""## 工具调用例子{i+1}\nInput:\n{query}\nOutput:\n{cls.FN_NAME}: {name}\n{cls.FN_ARGS}: {json.dumps(kwargs,ensure_ascii=False)}\n{cls.FN_RESULT}""" + fewshot_example_strs.append(fewshot_example_str) + return "\n\n".join(fewshot_example_strs) + + + @classmethod + def create_system_prompt(cls,goods_info:str,tools:list[dict],fewshot_examples:list,create_time=None) -> str: + tool_descs = '\n\n'.join(cls.get_function_description(tool) for tool in tools) + tool_names = ','.join(tool['name'] for tool in tools) + tool_system = cls.FN_CALL_TEMPLATE.format( + tool_descs=tool_descs, + tool_names=tool_names + ) + fewshot_examples_str = "" + if fewshot_examples: + fewshot_examples_str = "\n\n# 下面给出不同客户回复下调用不同工具的例子。" + fewshot_examples_str += f"\n\n{cls.format_fewshot_examples(fewshot_examples)}" + fewshot_examples_str += "\n\n请参考上述例子进行工具调用。" + + non_ask_tool_list = [] + # for tool in tools: + # should_ask_parameter = get_tool_by_name(tool['name']).should_ask_parameter + # if should_ask_parameter != "True": + # format_string = tool['name']+"工具"+should_ask_parameter + # non_ask_tool_list.append(format_string) + if len(non_ask_tool_list) == 0: + non_ask_rules = "" + else: + non_ask_rules = "\n - " + ','.join(non_ask_tool_list) + + if create_time: + datetime_object = datetime.strptime(create_time, '%Y-%m-%d %H:%M:%S.%f') + else: + datetime_object = datetime.now() + logger.info(f"create_time: {create_time} is not valid, use current time instead.") + return cls.SYSTEM_PROMPT.format( + goods_info=goods_info, + tools=tool_system, + fewshot_examples=fewshot_examples_str, + non_ask_rules=non_ask_rules, + date_prompt=datetime_object.strftime(cls.DATE_PROMPT) + ) + + @classmethod + def create_chat_history(cls,x,system_prompt=None): + # deal with function + _chat_history = x['chat_history'] + \ + [{"role": MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ + x['agent_tool_history'] + + # print(f'chat_history_before create: {_chat_history}') + # merge chat_history + chat_history = [] + if system_prompt is not None: + chat_history.append({ + "role": MessageType.SYSTEM_MESSAGE_TYPE, + "content":system_prompt + }) + + # move tool call results to assistant + for i,message in enumerate(copy.deepcopy(_chat_history)): + role = message['role'] + if i==0: + assert role == MessageType.HUMAN_MESSAGE_TYPE, f"The first message should comes from human role" + + if role == MessageType.TOOL_MESSAGE_TYPE: + assert chat_history[-1]['role'] == MessageType.AI_MESSAGE_TYPE,_chat_history + chat_history[-1]['content'] += message['content'] + continue + elif role == MessageType.AI_MESSAGE_TYPE: + # continue ai message + if chat_history[-1]['role'] == MessageType.AI_MESSAGE_TYPE: + chat_history[-1]['content'] += message['content'] + continue + + chat_history.append(message) + + # move the last tool call message to user + if chat_history[-1]['role'] == MessageType.AI_MESSAGE_TYPE: + assert chat_history[-2]['role'] == MessageType.HUMAN_MESSAGE_TYPE,chat_history + tool_calls = chat_history[-1].get("additional_kwargs",{}).get("tool_calls",[]) + if tool_calls: + chat_history[-2]['content'] += ("\n\n" + chat_history[-1]['content']) + chat_history = chat_history[:-1] + + return chat_history + + + @classmethod + def parse_function_calls_from_ai_message(cls,message:dict): + stop_reason = message['stop_reason'] + content = f"{cls.prefill}" + message['text'] + content = content.strip() + stop_reason = stop_reason or "" + + + function_calls = re.findall(f"{cls.FN_NAME}.*?{cls.FN_RESULT}", content + stop_reason,re.S) + return { + "function_calls":function_calls, + "content":content + } + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + tools:list = kwargs.get('tools',[]) + # add extral tools + if "give_rhetorical_question" not in tools: + tools.append(get_tool_by_name("give_rhetorical_question",scene=SceneType.RETAIL).tool_def) + fewshot_examples = kwargs.get('fewshot_examples',[]) + system_prompt = cls.create_system_prompt( + goods_info=kwargs['goods_info'], + create_time=kwargs.get('create_time',None), + tools=tools, + fewshot_examples=fewshot_examples + ) + + agent_current_call_number = kwargs['agent_current_call_number'] + + # give different prefill + if agent_current_call_number == 0: + cls.prefill = cls.prefill_after_thinking + else: + cls.prefill = cls.prefill_after_second_thinking + + # cls.prefill = '' + + model_kwargs = model_kwargs or {} + kwargs['system_prompt'] = system_prompt + model_kwargs = {**model_kwargs} + # model_kwargs["stop"] = model_kwargs.get("stop",[]) + ['✿RESULT✿', '✿RESULT✿:', '✿RESULT✿:\n','✿RETURN✿',f'<{cls.thinking_tag}>',f'<{cls.thinking_tag}/>'] + model_kwargs["stop"] = model_kwargs.get("stop",[]) + ['✿RESULT✿', '✿RESULT✿:', '✿RESULT✿:\n','✿RETURN✿',f'<{cls.thinking_tag}/>'] + # model_kwargs["prefill"] = "我先看看调用哪个工具,下面是我的思考过程:\n\nstep 1." + if "prefill" not in model_kwargs: + model_kwargs["prefill"] = f'{cls.prefill}' + return super().create_chain(model_kwargs=model_kwargs,**kwargs) + + +class Qwen2Instruct7BRetailToolCallingChain(Qwen2Instruct72BRetailToolCallingChain): + model_id = LLMModelType.QWEN2INSTRUCT7B + goods_info_tag = "商品信息" + SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} + +{{tools}} +{{fewshot_examples}} + +## 当前用户正在浏览的商品信息 +{{goods_info}} + +# 回复策略 +下面是一些常见的回复策略: + - 如果根据各个工具的描述,当前用户的回复跟某个示例对应的Input相关性强,直接按照示例中Output的工具名称进行调用。 + - 考虑使用商品信息 <{goods_info_tag}> 里面的内容回答用户的问题。 + - 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户: “ 亲亲,请问还有什么问题吗?“ + - 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。 + - 如果客户的回复里面包含订单号,则直接回复 “您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“ + - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!“ + +## Tips + - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的。 + - 回答必须简洁,不允许出现超过2句话的回复。{{non_ask_rules}}""" + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs["prefill"] = "" + res = super().create_chain(model_kwargs=model_kwargs,**kwargs) + cls.prefill = "" + return res + +class Qwen15Instruct32BRetailToolCallingChain(Qwen2Instruct7BRetailToolCallingChain): + model_id = LLMModelType.QWEN15INSTRUCT32B + + + + diff --git a/source/lambda/online/langchain_integration/chains/stepback_chain.py b/source/lambda/online/langchain_integration/chains/stepback_chain.py new file mode 100644 index 00000000..4a14db1d --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/stepback_chain.py @@ -0,0 +1,138 @@ +from langchain.prompts import ( + ChatPromptTemplate, + FewShotChatMessagePromptTemplate, +) +from langchain.schema.runnable import RunnableLambda + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType +) +from ..chains.chat_chain import Iternlm2Chat7BChatChain +from ..chains.llm_chain_base import LLMChain +from ..chat_models import Model + +STEPBACK_PROMPTING_TYPE = LLMTaskType.STEPBACK_PROMPTING_TYPE + +class Iternlm2Chat7BStepBackChain(Iternlm2Chat7BChatChain): + model_id = LLMModelType.INTERNLM2_CHAT_7B + intent_type = STEPBACK_PROMPTING_TYPE + + default_model_kwargs = {"temperature": 0.1, "max_new_tokens": 200} + + @classmethod + def create_prompt(cls, x): + meta_instruction_template = "You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples: {few_examples}" + # meta_instruction_template = "你是一个拥有世界知识的专家. 你的任务是将问题转述为更通用的问题,这样更容易回答。更通用指的是将问题进行抽象表达,省略问题中的各种细节,包括具体时间,地点等。 下面有一些例子: {few_examples}" + + few_examples = [ + { + "input": "阿尔伯特-爱因斯坦的出生地是哪里?", + "output": "阿尔伯特-爱因斯坦的个人经历是怎样的?", + }, + { + "input": "特斯拉在中国上海有多少门店", + "output": "特斯拉在中国的门店分布情况", + }, + ] + + few_examples_template = """origin question: {origin_question} + step-back question: {step_back_question} + """ + few_examples_strs = [] + for few_example in few_examples: + few_examples_strs.append( + few_examples_template.format( + origin_question=few_example["input"], + step_back_question=few_example["output"], + ) + ) + meta_instruction = meta_instruction_template.format( + few_examples="\n\n".join(few_examples_strs) + ) + prompt = ( + cls.build_prompt( + query=f"origin question: {x['query']}", + history=[], + meta_instruction=meta_instruction, + ) + + "step-back question: " + ) + return prompt + + +class Iternlm2Chat20BStepBackChain(Iternlm2Chat7BStepBackChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B + intent_type = STEPBACK_PROMPTING_TYPE + + +class Claude2StepBackChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = STEPBACK_PROMPTING_TYPE + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + stream = kwargs.get("stream", False) + examples = [ + { + "input": "Could the members of The Police perform lawful arrests?", + "output": "what can the members of The Police do?", + }, + { + "input": "Jan Sindel’s was born in what country?", + "output": "what is Jan Sindel’s personal history?", + }, + ] + # We now transform these to example messages + example_prompt = ChatPromptTemplate.from_messages( + [ + ("human", "{input}"), + ("ai", "{output}"), + ] + ) + few_shot_prompt = FewShotChatMessagePromptTemplate( + example_prompt=example_prompt, + examples=examples, + ) + + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + """You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:""", + ), + # Few shot examples + few_shot_prompt, + # New question + ("user", "{query}"), + ] + ) + + llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) + chain = prompt | llm + if stream: + chain = ( + prompt + | RunnableLambda(lambda x: llm.stream(x.messages)) + | RunnableLambda(lambda x: (i.content for i in x)) + ) + + else: + chain = prompt | llm | RunnableLambda(lambda x: x.content) + return chain + + +class Claude21StepBackChain(Claude2StepBackChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceStepBackChain(Claude2StepBackChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetStepBackChain(Claude2StepBackChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuStepBackChain(Claude2StepBackChain): + model_id = LLMModelType.CLAUDE_3_HAIKU diff --git a/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py b/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py new file mode 100644 index 00000000..9d6b84b3 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py @@ -0,0 +1,320 @@ +# tool calling chain +import json +from typing import List,Dict,Any +import re + +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough +) +from common_logic.common_utils.prompt_utils import get_prompt_template +from common_logic.common_utils.logger_utils import print_llm_messages +from langchain_core.messages import( + AIMessage, + SystemMessage +) +from langchain.prompts import ChatPromptTemplate + +from langchain_core.messages import AIMessage,SystemMessage + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType, + MessageType +) +from common_logic.common_utils.time_utils import get_china_now + +from .llm_chain_base import LLMChain +from ..llm_models import Model + +incorrect_tool_call_example = """Here is an example of an incorrectly formatted tool call, which you should avoid. + + + +tool_name + + +question +string +value + + + + + + +In this incorrect tool calling example, the parameter `name` should form a XLM tag. +""" + + +SYSTEM_MESSAGE_PROMPT =(f"In this environment you have access to a set of tools you can use to answer the user's question.\n" + "\n" + "You may call them like this:\n" + "\n" + "\n" + "$TOOL_NAME\n" + "\n" + "<$PARAMETER_NAME>$PARAMETER_VALUE\n" + "...\n" + "\n" + "\n" + "\n" + "\n" + "Here are the tools available:\n" + "\n" + "{tools}" + "\n" + "\nAnswer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided." + "\nHere are some guidelines for you:\n{tool_call_guidelines}." + f"\n{incorrect_tool_call_example}" + ) + +SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES = SYSTEM_MESSAGE_PROMPT + ( + "Some examples of tool calls are given below, where the content within represents the most recent reply in the dialog." + "\n{fewshot_examples}" +) + +TOOL_FORMAT = """ +{tool_name} +{tool_description} + +{formatted_required_parameters} + + +{formatted_optional_parameters} + +""" + +TOOL_PARAMETER_FORMAT = """ +{parameter_name} +{parameter_type} +{parameter_description} +""" + +TOOL_EXECUTE_SUCCESS_TEMPLATE = """ + + +{tool_name} + +{result} + + + +""" + +TOOL_EXECUTE_FAIL_TEMPLATE = """ + + +{error} + + +""" + +AGENT_SYSTEM_PROMPT = "你是一个亚马逊云科技的AI助理,你的名字是亚麻小Q。今天是{date_str},{weekday}. " + + +def _get_type(parameter: Dict[str, Any]) -> str: + if "type" in parameter: + return parameter["type"] + if "anyOf" in parameter: + return json.dumps({"anyOf": parameter["anyOf"]}) + if "allOf" in parameter: + return json.dumps({"allOf": parameter["allOf"]}) + return json.dumps(parameter) + + +def convert_openai_tool_to_anthropic(tools:list[dict])->str: + formatted_tools = tools + tools_data = [ + { + "tool_name": tool["name"], + "tool_description": tool["description"], + "formatted_required_parameters": "\n".join( + [ + TOOL_PARAMETER_FORMAT.format( + parameter_name=name, + parameter_type=_get_type(parameter), + parameter_description=parameter.get("description"), + ) for name, parameter in tool["parameters"]["properties"].items() + if name in tool["parameters"].get("required", []) + ] + ), + "formatted_optional_parameters": "\n".join( + [ + TOOL_PARAMETER_FORMAT.format( + parameter_name=name, + parameter_type=_get_type(parameter), + parameter_description=parameter.get("description"), + ) for name, parameter in tool["parameters"]["properties"].items() + if name not in tool["parameters"].get("required", []) + ] + ), + } + for tool in formatted_tools + ] + tools_formatted = "\n".join( + [ + TOOL_FORMAT.format( + tool_name=tool["tool_name"], + tool_description=tool["tool_description"], + formatted_required_parameters=tool["formatted_required_parameters"], + formatted_optional_parameters=tool["formatted_optional_parameters"], + ) + for tool in tools_data + ] + ) + return tools_formatted + + +class Claude2ToolCallingChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.TOOL_CALLING_XML + default_model_kwargs = { + "max_tokens": 2000, + "temperature": 0.1, + "top_p": 0.9, + "stop_sequences": ["\n\nHuman:", "\n\nAssistant",""], + } + + @staticmethod + def format_fewshot_examples(fewshot_examples:list[dict]): + fewshot_example_strs = [] + for fewshot_example in fewshot_examples: + param_strs = [] + for p,v in fewshot_example['kwargs'].items(): + param_strs.append(f"<{p}>{v}\n" + f"{fewshot_example['query']}\n" + f"\n" + "\n" + "\n" + f"{fewshot_example['name']}\n" + "\n" + f"{param_str}" + "\n" + "\n" + "\n" + "\n" + "" + ) + fewshot_example_strs.append(fewshot_example_str) + fewshot_example_str = '\n'.join(fewshot_example_strs) + return f"\n{fewshot_example_str}\n" + + @classmethod + def parse_function_calls_from_ai_message(cls,message:AIMessage): + content = "" + message.content + "" + function_calls:List[str] = re.findall("(.*?)", content,re.S) + if not function_calls: + content = "" + message.content + + return { + "function_calls": function_calls, + "content": content + } + + @classmethod + def create_chat_history(cls,x): + chat_history = x['chat_history'] + \ + [{"role": MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ + x['agent_tool_history'] + return chat_history + + @classmethod + def get_common_system_prompt(cls,system_prompt_template:str): + now = get_china_now() + date_str = now.strftime("%Y年%m月%d日") + weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] + weekday = weekdays[now.weekday()] + system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) + return system_prompt + + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + tools:list = kwargs['tools'] + fewshot_examples = kwargs.get('fewshot_examples',[]) + if fewshot_examples: + fewshot_examples.append({ + "name": "give_rhetorical_question", + "query": "今天天气怎么样?", + "kwargs": {"question": "请问你想了解哪个城市的天气?"} + }) + user_system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="user_prompt" + ).prompt_template + + user_system_prompt = kwargs.get("user_prompt",None) or user_system_prompt + + user_system_prompt = cls.get_common_system_prompt( + user_system_prompt + ) + guidelines_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="guidelines_prompt" + ).prompt_template + + guidelines_prompt = kwargs.get("guidelines_prompt",None) or guidelines_prompt + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + tools_formatted = convert_openai_tool_to_anthropic(tools) + + if fewshot_examples: + system_prompt = SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES.format( + tools=tools_formatted, + fewshot_examples=cls.format_fewshot_examples(fewshot_examples), + tool_call_guidelines=guidelines_prompt + ) + else: + system_prompt = SYSTEM_MESSAGE_PROMPT.format( + tools=tools_formatted, + tool_call_guidelines=guidelines_prompt + ) + + system_prompt = user_system_prompt + system_prompt + tool_calling_template = ChatPromptTemplate.from_messages( + [ + SystemMessage(content=system_prompt), + ("placeholder", "{chat_history}"), + AIMessage(content="") + ]) + + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + ) + chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | tool_calling_template \ + | RunnableLambda(lambda x: print_llm_messages(f"Agent messages: {x.messages}") or x.messages ) \ + | llm | RunnableLambda(lambda message:cls.parse_function_calls_from_ai_message( + message + )) + return chain + + +class Claude21ToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude35SonnetToolCallingChain(Claude2ToolCallingChain): + model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" diff --git a/source/lambda/online/langchain_integration/chains/translate_chain.py b/source/lambda/online/langchain_integration/chains/translate_chain.py new file mode 100644 index 00000000..07b92b3b --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/translate_chain.py @@ -0,0 +1,40 @@ +# translate chain +from langchain.schema.runnable import RunnableLambda + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType +) +from .chat_chain import Iternlm2Chat7BChatChain + +QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE + + +class Iternlm2Chat7BTranslateChain(Iternlm2Chat7BChatChain): + intent_type = QUERY_TRANSLATE_TYPE + default_model_kwargs = {"temperature": 0.1, "max_new_tokens": 200} + + @classmethod + def create_prompt(cls, x): + query = x["query"] + target_lang = x["target_lang"] + history = cls.create_history(x) + meta_instruction = f"你是一个有经验的翻译助理, 正在将用户的问题翻译成{target_lang},不要试图去回答用户的问题,仅仅做翻译。" + query = f'将文本:\n "{query}" \n 翻译成{target_lang}。\n直接翻译文本,不要输出多余的文本。' + + prompt = cls.build_prompt( + query=query, history=history, meta_instruction=meta_instruction + ) + return prompt + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + llm_chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) + llm_chain = llm_chain | RunnableLambda(lambda x: x.strip('"')) # postprocess + return llm_chain + + +class Iternlm2Chat20BTranslateChain(Iternlm2Chat7BTranslateChain): + model_id = LLMModelType.INTERNLM2_CHAT_20B diff --git a/source/lambda/online/langchain_integration/chat_models/__init__.py b/source/lambda/online/langchain_integration/chat_models/__init__.py new file mode 100644 index 00000000..8b9092ec --- /dev/null +++ b/source/lambda/online/langchain_integration/chat_models/__init__.py @@ -0,0 +1,97 @@ +""" +chat models build in command pattern +""" +from common_logic.common_utils.constant import LLMModelType + + +class ModeMixins: + @staticmethod + def convert_messages_role(messages:list[dict],role_map:dict): + """ + Args: + messages (list[dict]): + role_map (dict): {"current_role":"targe_role"} + + Returns: + _type_: as messages + """ + valid_roles = list(role_map.keys()) + new_messages = [] + for message in messages: + message = {**message} + role = message['role'] + assert role in valid_roles,(role,valid_roles,messages) + message['role'] = role_map[role] + new_messages.append(message) + return new_messages + + +class ModelMeta(type): + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + if name == "Model" or new_cls.model_id is None: + return new_cls + new_cls.model_map[new_cls.model_id] = new_cls + return new_cls + + +class Model(ModeMixins,metaclass=ModelMeta): + model_id = None + model_map = {} + + @classmethod + def create_model(cls, model_kwargs=None, **kwargs): + raise NotImplementedError + + @classmethod + def get_model(cls, model_id, model_kwargs=None, **kwargs): + # dynamic load module + _load_module(model_id) + return cls.model_map[model_id].create_model(model_kwargs=model_kwargs, **kwargs) + +def _import_bedrock_models(): + from .bedrock_models import ( + Claude2, + ClaudeInstance, + Claude21, + Claude3Sonnet, + Claude3Haiku, + Claude35Sonnet, + MistralLarge2407, + Llama3d1Instruct70B, + CohereCommandRPlus + ) + +def _import_openai_models(): + from .openai_models import ( + ChatGPT35, + ChatGPT4Turbo, + ChatGPT4o + ) + + +def _load_module(model_id): + assert model_id in MODEL_MODULE_LOAD_FN_MAP,(model_id,MODEL_MODULE_LOAD_FN_MAP) + MODEL_MODULE_LOAD_FN_MAP[model_id]() + + +MODEL_MODULE_LOAD_FN_MAP = { + LLMModelType.CHATGPT_35_TURBO_0125:_import_openai_models, + LLMModelType.CHATGPT_4_TURBO:_import_openai_models, + LLMModelType.CHATGPT_4O:_import_openai_models, + LLMModelType.CLAUDE_2:_import_bedrock_models, + LLMModelType.CLAUDE_INSTANCE:_import_bedrock_models, + LLMModelType.CLAUDE_21:_import_bedrock_models, + LLMModelType.CLAUDE_3_SONNET:_import_bedrock_models, + LLMModelType.CLAUDE_3_HAIKU:_import_bedrock_models, + LLMModelType.CLAUDE_3_5_SONNET:_import_bedrock_models, + LLMModelType.LLAMA3_1_70B_INSTRUCT:_import_bedrock_models, + LLMModelType.MISTRAL_LARGE_2407:_import_bedrock_models, + LLMModelType.COHERE_COMMAND_R_PLUS:_import_bedrock_models, +} + + + + + + diff --git a/source/lambda/online/langchain_integration/chat_models/bedrock_models.py b/source/lambda/online/langchain_integration/chat_models/bedrock_models.py new file mode 100644 index 00000000..3239e012 --- /dev/null +++ b/source/lambda/online/langchain_integration/chat_models/bedrock_models.py @@ -0,0 +1,77 @@ +import os +from langchain_aws.chat_models import ChatBedrockConverse +from common_logic.common_utils.constant import ( + MessageType, + LLMModelType +) +from common_logic.common_utils.logger_utils import get_logger +from . import Model + + +# AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE +# HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE +# SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE + +logger = get_logger("bedrock_model") + +# Bedrock model type +class Claude2(Model): + model_id = LLMModelType.CLAUDE_2 + default_model_kwargs = {"max_tokens": 2000, "temperature": 0.7, "top_p": 0.9} + + @classmethod + def create_model(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + credentials_profile_name = ( + kwargs.get("credentials_profile_name", None) + or os.environ.get("AWS_PROFILE", None) + or None + ) + region_name = ( + kwargs.get("region_name", None) + or os.environ.get("BEDROCK_REGION", None) + or None + ) + llm = ChatBedrockConverse( + credentials_profile_name=credentials_profile_name, + region_name=region_name, + model=cls.model_id, + **model_kwargs, + ) + return llm + + +class ClaudeInstance(Claude2): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude21(Claude2): + model_id = LLMModelType.CLAUDE_21 + + +class Claude3Sonnet(Claude2): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3Haiku(Claude2): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude35Sonnet(Claude2): + model_id = LLMModelType.CLAUDE_3_5_SONNET + + +class MistralLarge2407(Claude2): + model_id = LLMModelType.MISTRAL_LARGE_2407 + + +class Llama3d1Instruct70B(Claude2): + model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT + +class CohereCommandRPlus(Claude2): + model_id = LLMModelType.COHERE_COMMAND_R_PLUS + + + diff --git a/source/lambda/online/langchain_integration/chat_models/openai_models.py b/source/lambda/online/langchain_integration/chat_models/openai_models.py new file mode 100644 index 00000000..fdddeb45 --- /dev/null +++ b/source/lambda/online/langchain_integration/chat_models/openai_models.py @@ -0,0 +1,28 @@ +from langchain_openai import ChatOpenAI +from common_logic.common_utils.constant import LLMModelType +from common_logic.common_utils.logger_utils import get_logger +from . import Model + +logger = get_logger("openai_model") + +class ChatGPT35(Model): + model_id = LLMModelType.CHATGPT_35_TURBO_0125 + default_model_kwargs = {"max_tokens": 2000, "temperature": 0.7, "top_p": 0.9} + + @classmethod + def create_model(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + llm = ChatOpenAI( + model=cls.model_id, + **model_kwargs, + ) + return llm + + +class ChatGPT4Turbo(ChatGPT35): + model_id = LLMModelType.CHATGPT_4_TURBO + + +class ChatGPT4o(ChatGPT35): + model_id = LLMModelType.CHATGPT_4O \ No newline at end of file From 0e9cc927c82a3d8375aeb45e0f96696035041593 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Fri, 18 Oct 2024 05:54:41 +0000 Subject: [PATCH 02/29] modify --- source/lambda/online/functions/_tool_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/lambda/online/functions/_tool_base.py b/source/lambda/online/functions/_tool_base.py index 63fca04d..c4084d2c 100644 --- a/source/lambda/online/functions/_tool_base.py +++ b/source/lambda/online/functions/_tool_base.py @@ -3,6 +3,7 @@ from enum import Enum from common_logic.common_utils.constant import SceneType,ToolRuningMode + class ToolDefType(Enum): openai = "openai" @@ -19,6 +20,7 @@ class Tool(BaseModel): scene: str = Field(description="tool use scene",default=SceneType.COMMON) # should_ask_parameter: bool = Field(description="tool use scene") + class ToolManager: def __init__(self) -> None: self.tools = {} From 7d34b82b557d22483d49c30bc1159108b8139897 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Fri, 18 Oct 2024 05:55:06 +0000 Subject: [PATCH 03/29] modify --- .../langchain_integration/chat_models/bedrock_models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/source/lambda/online/langchain_integration/chat_models/bedrock_models.py b/source/lambda/online/langchain_integration/chat_models/bedrock_models.py index 3239e012..162ab998 100644 --- a/source/lambda/online/langchain_integration/chat_models/bedrock_models.py +++ b/source/lambda/online/langchain_integration/chat_models/bedrock_models.py @@ -8,10 +8,6 @@ from . import Model -# AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE -# HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE -# SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE - logger = get_logger("bedrock_model") # Bedrock model type From 52cdcc8377c56180a19681f10c013af3da29d79e Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 24 Oct 2024 03:22:24 +0000 Subject: [PATCH 04/29] refactor: unified tool to adapt to langchian's tool --- .../langchain_integration/tools/__init__.py | 328 ++++++++++++++++++ .../tools/common_tools/__init__.py | 121 +++++++ .../tools/common_tools/chat.py | 5 + .../tools/common_tools/comparison_rag.py | 51 +++ .../tools/common_tools/get_weather.py | 34 ++ .../tools/common_tools/give_final_response.py | 4 + .../common_tools/give_rhetorical_question.py | 4 + .../tools/common_tools/rag.py | 65 ++++ .../tools/common_tools/step_back_rag.py | 50 +++ 9 files changed, 662 insertions(+) create mode 100644 source/lambda/online/langchain_integration/tools/__init__.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/__init__.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/chat.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/comparison_rag.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/get_weather.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/give_final_response.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/give_rhetorical_question.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/rag.py create mode 100644 source/lambda/online/langchain_integration/tools/common_tools/step_back_rag.py diff --git a/source/lambda/online/langchain_integration/tools/__init__.py b/source/lambda/online/langchain_integration/tools/__init__.py new file mode 100644 index 00000000..91e937b2 --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/__init__.py @@ -0,0 +1,328 @@ +# from langchain.tools.base import StructuredTool,BaseTool,tool +# StructuredTool.from_function +# from langchain_experimental.tools import PythonREPLTool +# from langchain_core.utils.function_calling import convert_to_openai_function +# from llama_index.core.tools import FunctionTool +# from langchain.tools import BaseTool +# from pydantic import create_model + +# from langchain_community.tools import WikipediaQueryRun + + +# builder = StateGraph(State) + + +# # Define nodes: these do the work +# builder.add_node("assistant", Assistant(part_1_assistant_runnable)) +# builder.add_node("tools", create_tool_node_with_fallback(part_1_tools)) +# # Define edges: these determine how the control flow moves +# builder.add_edge(START, "assistant") +# builder.add_conditional_edges( +# "assistant", +# tools_condition, +# ) +# builder.add_edge("tools", "assistant") + +# # The checkpointer lets the graph persist its state +# # this is a complete memory for the entire graph. +# memory = MemorySaver() +# part_1_graph = builder.compile(checkpointer=memory) + +from typing import Optional,Union +from pydantic import BaseModel +import platform +import json +import inspect +from functools import wraps +import types + +from datamodel_code_generator import DataModelType, PythonVersion +from datamodel_code_generator.model import get_data_model_types +from datamodel_code_generator.parser.jsonschema import JsonSchemaParser +from langchain.tools.base import StructuredTool,BaseTool + +from common_logic.common_utils.constant import SceneType +from common_logic.common_utils.lambda_invoke_utils import invoke_with_lambda + + +class ToolIdentifier(BaseModel): + scene: SceneType + name: str + + @property + def tool_id(self): + return f"{self.scene}__{self.name}" + + +class ToolManager: + tool_map = {} + + @staticmethod + def convert_tool_def_to_pydantic(tool_id,tool_def:Union[dict,BaseModel]): + if not isinstance(tool_def,dict): + return tool_def + # convert tool definition to pydantic model + current_python_version = ".".join(platform.python_version().split(".")[:-1]) + data_model_types = get_data_model_types( + DataModelType.PydanticBaseModel, + target_python_version=PythonVersion(current_python_version) + ) + parser = JsonSchemaParser( + json.dumps(tool_def,ensure_ascii=False,indent=2), + data_model_type=data_model_types.data_model, + data_model_root_type=data_model_types.root_model, + data_model_field_type=data_model_types.field_model, + data_type_manager_type=data_model_types.data_type_manager, + dump_resolve_reference_action=data_model_types.dump_resolve_reference_action, + use_schema_description=True + ) + result = parser.parse() + new_tool_module = types.ModuleType(tool_id) + exec(result, new_tool_module.__dict__) + return new_tool_module.Model + + + @staticmethod + def get_tool_identifier(scene=None,name=None,tool_identifier=None): + if tool_identifier is None: + tool_identifier = ToolIdentifier(scene=scene,name=name) + return tool_identifier + + + @classmethod + def register_lc_tool( + cls, + tool:BaseTool, + scene=None, + name=None, + tool_identifier=None, + ): + tool_identifier = cls.get_tool_identifier( + scene=scene, + name=name, + tool_identifier=None + ) + assert isinstance(tool,BaseTool),(tool,type(tool)) + cls.tool_map[tool_identifier.tool_id] = tool + + + @classmethod + def register_func_as_tool( + cls, + func:callable, + tool_def:dict, + return_direct:False, + scene=None, + name=None, + tool_identifier=None, + ): + tool_identifier = cls.get_tool_identifier( + scene=scene, + name=name, + tool_identifier=tool_identifier + ) + tool = StructuredTool.from_function( + func=func, + name=tool_identifier.name, + args_schema=ToolManager.convert_tool_def_to_pydantic( + tool_id=tool_identifier.tool_id, + tool_def=tool_def + ), + return_direct=return_direct + ) + # register tool + ToolManager.register_lc_tool( + tool_identifier=tool_identifier, + tool=tool + ) + + + @classmethod + def register_aws_lambda_as_tool( + cls, + lambda_name:str, + tool_def:dict, + scene=None, + name=None, + tool_identifier=None, + return_direct=False + ): + + def _func(**kargs): + return invoke_with_lambda(lambda_name,kargs) + + tool_identifier = cls.get_tool_identifier( + scene=scene, + name=name, + tool_identifier=tool_identifier + ) + tool = StructuredTool.from_function( + func=_func, + name=tool_identifier.name, + args_schema=ToolManager.convert_tool_def_to_pydantic( + tool_id=tool_identifier.tool_id, + tool_def=tool_def + ), + return_direct=return_direct + ) + ToolManager.register_lc_tool( + tool_identifier=tool_identifier, + tool=tool + ) + + + + @classmethod + def get_tool(cls, scene, name,**kwargs): + # dynamic import + tool_identifier = ToolIdentifier(scene=scene, name=name) + tool_id = tool_identifier.tool_id + if tool_id not in cls.tool_map: + TOOL_MOFULE_LOAD_FN_MAP[tool_id](**kwargs) + return cls.tool_map[tool_id] + + +TOOL_MOFULE_LOAD_FN_MAP = {} + + +def lazy_tool_load_decorator(scene:SceneType,name): + def decorator(func): + tool_identifier = ToolIdentifier(scene=scene, name=name) + @wraps(func) + def wrapper(*args, **kwargs): + if "tool_identifier" in inspect.signature(func).parameters: + kwargs = {**kwargs,"tool_identifier":tool_identifier} + return func(*args, **kwargs) + TOOL_MOFULE_LOAD_FN_MAP[tool_identifier.tool_id] = func + return wrapper + return decorator + + +############################# tool load func ###################### + + +@lazy_tool_load_decorator(SceneType.COMMON,"get_weather") +def _load_common_weather_tool(tool_identifier:ToolIdentifier): + from .common_tools import get_weather + tool_def = { + "description": "Get the current weather for `city_name`", + "properties": { + "city_name": { + "description": "The name of the city to be queried", + "type": "string" + }, + }, + "required": ["city_name"] + } + ToolManager.register_func_as_tool( + tool_identifier.scene, + tool_identifier.name, + get_weather.get_weather, + tool_def, + return_direct=False + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"give_rhetorical_question") +def _load_common_rhetorical_tool(tool_identifier:ToolIdentifier): + from .common_tools import give_rhetorical_question + tool_def = { + "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", + "properties": { + "question": { + "description": "The rhetorical question to user", + "type": "string" + }, + } + } + ToolManager.register_func_as_tool( + tool_identifier.scene, + tool_identifier.name, + give_rhetorical_question.give_rhetorical_question, + tool_def, + return_direct=True + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"give_final_response") +def _load_common_final_response_tool(tool_identifier:ToolIdentifier): + from .common_tools import give_final_response + + tool_def = { + "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", + "properties": { + "response": { + "description": "Response to user", + "type": "string" + } + }, + "required": ["response"] + } + ToolManager.register_func_as_tool( + tool_identifier.scene, + tool_identifier.name, + give_final_response.give_final_response, + tool_def, + return_direct=True + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"chat") +def _load_common_chat_tool(tool_identifier:ToolIdentifier): + from .common_tools import chat + tool_def = { + "description": "casual talk with AI", + "properties": { + "response": { + "description": "response to users", + "type": "string" + } + }, + "required": ["response"] + } + + ToolManager.register_func_as_tool( + tool_identifier.scene, + tool_identifier.name, + chat.chat, + tool_def, + return_direct=True + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"rag_tool") +def _load_common_rag_tool(tool_identifier:ToolIdentifier): + from .common_tools import rag + tool_def = { + "description": "private knowledge", + "properties": { + "query": { + "description": "query for retrieve", + "type": "string" + } + }, + "required": ["query"] + } + ToolManager.register_func_as_tool( + tool_identifier.scene, + tool_identifier.name, + rag.rag, + tool_def, + return_direct=True + ) + + + + + + + + + + + + + + + + diff --git a/source/lambda/online/langchain_integration/tools/common_tools/__init__.py b/source/lambda/online/langchain_integration/tools/common_tools/__init__.py new file mode 100644 index 00000000..c5706989 --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/__init__.py @@ -0,0 +1,121 @@ +from common_logic.common_utils.constant import SceneType, ToolRuningMode +from .._tool_base import tool_manager +from . import ( + get_weather, + give_rhetorical_question, + give_final_response, + chat, + rag +) + + +SCENE = SceneType.COMMON +LAMBDA_NAME = "lambda_common_tools" + +tool_manager.register_tool({ + "name": "get_weather", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": get_weather.lambda_handler, + "tool_def": { + "name": "get_weather", + "description": "Get the current weather for `city_name`", + "parameters": { + "type": "object", + "properties": { + "city_name": { + "description": "The name of the city to be queried", + "type": "string" + }, + }, + "required": ["city_name"] + } + }, + "running_mode": ToolRuningMode.LOOP +}) + + +tool_manager.register_tool( + { + "name": "give_rhetorical_question", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": give_rhetorical_question.lambda_handler, + "tool_def": { + "name": "give_rhetorical_question", + "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", + "parameters": { + "type": "object", + "properties": { + "question": { + "description": "The rhetorical question to user", + "type": "string" + }, + }, + "required": ["question"], + }, + }, + "running_mode": ToolRuningMode.ONCE + } +) + + +tool_manager.register_tool( + { + "name": "give_final_response", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": give_final_response.lambda_handler, + "tool_def": { + "name": "give_final_response", + "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", + "parameters": { + "type": "object", + "properties": { + "response": { + "description": "Response to user", + "type": "string" + } + }, + "required": ["response"] + }, + }, + "running_mode": ToolRuningMode.ONCE + } +) + + +tool_manager.register_tool({ + "name": "chat", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": chat.lambda_handler, + "tool_def": { + "name": "chat", + "description": "casual talk with AI", + "parameters": { + "type": "object", + "properties": { + "response": { + "description": "response to users", + "type": "string" + }}, + "required": ["response"] + }, + }, + "running_mode": ToolRuningMode.ONCE +}) + + +tool_manager.register_tool({ + "name": "rag_tool", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": rag.lambda_handler, + "tool_def": { + "name": "rag_tool", + "description": "private knowledge", + "parameters": {} + }, + "running_mode": ToolRuningMode.ONCE +}) diff --git a/source/lambda/online/langchain_integration/tools/common_tools/chat.py b/source/lambda/online/langchain_integration/tools/common_tools/chat.py new file mode 100644 index 00000000..c007c353 --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/chat.py @@ -0,0 +1,5 @@ +# give chat response + +def chat(response:str): + return response + \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/tools/common_tools/comparison_rag.py b/source/lambda/online/langchain_integration/tools/common_tools/comparison_rag.py new file mode 100644 index 00000000..3bf57396 --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/comparison_rag.py @@ -0,0 +1,51 @@ +# knowledge base retrieve +from common_logic.common_utils.lambda_invoke_utils import invoke_lambda +from common_logic.common_utils.constant import ( + LLMTaskType +) + +def knowledge_base_retrieve(retriever_params, context=None): + output: str = invoke_lambda( + event_body=retriever_params, + lambda_name="Online_Functions", + lambda_module_path="functions.functions_utils.retriever.retriever", + handler_name="lambda_handler", + ) + contexts = [doc["page_content"] for doc in output["result"]["docs"]] + return contexts + +def lambda_handler(event_body, context=None): + state = event_body['state'] + retriever_params = state["chatbot_config"]["comparison_rag_config"]["retriever_config"] + contexts = [] + retriever_params["query"] = event_body['kwargs']['query_a'] + contexts.extend(knowledge_base_retrieve(retriever_params, context=context)) + retriever_params["query"] = event_body['kwargs']['query_b'] + contexts.extend(knowledge_base_retrieve(retriever_params, context=context)) + context = "\n\n".join(contexts) + + # llm generate + system_prompt = (f"请根据context内的信息回答问题:\n" + "\n" + " - 回复内容需要展现出礼貌。回答内容为一句话,言简意赅。\n" + " - 使用中文回答。\n" + "\n" + "\n" + f"{context}\n" + "" + ) + + output:str = invoke_lambda( + lambda_name='Online_LLM_Generate', + lambda_module_path="lambda_llm_generate.llm_generate", + handler_name='lambda_handler', + event_body={ + "llm_config": { + **state['chatbot_config']['rag_daily_reception_config']['llm_config'], + "system_prompt": system_prompt, + "intent_type": LLMTaskType.CHAT}, + "llm_input": {"query": state['query'], "chat_history": state['chat_history']} + } + ) + + return {"code":0, "result":output} \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/tools/common_tools/get_weather.py b/source/lambda/online/langchain_integration/tools/common_tools/get_weather.py new file mode 100644 index 00000000..ccecb204 --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/get_weather.py @@ -0,0 +1,34 @@ +# get weather tool +import requests + +def get_weather(city_name:str): + if not isinstance(city_name, str): + raise TypeError("City name must be a string") + + key_selection = { + "current_condition": [ + "temp_C", + "FeelsLikeC", + "humidity", + "weatherDesc", + "observation_time", + ], + } + + try: + resp = requests.get(f"https://wttr.in/{city_name}?format=j1") + resp.raise_for_status() + resp = resp.json() + ret = {k: {_v: resp[k][0][_v] for _v in v} for k, v in key_selection.items()} + except: + import traceback + + ret = ("Error encountered while fetching weather data!\n" + traceback.format_exc() + ) + + return str(ret) + + +def lambda_handler(event_body,context=None): + result = get_weather(**event_body['kwargs']) + return {"code":0, "result": result} \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/tools/common_tools/give_final_response.py b/source/lambda/online/langchain_integration/tools/common_tools/give_final_response.py new file mode 100644 index 00000000..82146d9b --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/give_final_response.py @@ -0,0 +1,4 @@ +# give final response tool + +def give_final_response(response:str): + return response \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/tools/common_tools/give_rhetorical_question.py b/source/lambda/online/langchain_integration/tools/common_tools/give_rhetorical_question.py new file mode 100644 index 00000000..ac78268a --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/give_rhetorical_question.py @@ -0,0 +1,4 @@ +# give rhetorical question + +def give_rhetorical_question(question:str): + return question \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/langchain_integration/tools/common_tools/rag.py new file mode 100644 index 00000000..2537bb5c --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/rag.py @@ -0,0 +1,65 @@ +from common_logic.common_utils.lambda_invoke_utils import invoke_lambda +from common_logic.common_utils.prompt_utils import get_prompt_templates_from_ddb +from common_logic.common_utils.constant import ( + LLMTaskType +) +from common_logic.common_utils.lambda_invoke_utils import send_trace + + +def rag(query,state): + # state = event_body['state'] + context_list = [] + # add qq match results + context_list.extend(state['qq_match_results']) + figure_list = [] + retriever_params = state["chatbot_config"]["private_knowledge_config"] + retriever_params["query"] = state[retriever_params.get("retriever_config",{}).get("query_key","query")] + output: str = invoke_lambda( + event_body=retriever_params, + lambda_name="Online_Functions", + lambda_module_path="functions.functions_utils.retriever.retriever", + handler_name="lambda_handler", + ) + + for doc in output["result"]["docs"]: + context_list.append(doc["page_content"]) + figure_list = figure_list + doc.get("figure",[]) + + # Remove duplicate figures + unique_set = {tuple(d.items()) for d in figure_list} + unique_figure_list = [dict(t) for t in unique_set] + state['extra_response']['figures'] = unique_figure_list + + send_trace(f"\n\n**rag-contexts:** {context_list}", enable_trace=state["enable_trace"]) + + group_name = state['chatbot_config']['group_name'] + llm_config = state["chatbot_config"]["private_knowledge_config"]['llm_config'] + chatbot_id = state["chatbot_config"]["chatbot_id"] + task_type = LLMTaskType.RAG + prompt_templates_from_ddb = get_prompt_templates_from_ddb( + group_name, + model_id=llm_config['model_id'], + task_type=task_type, + chatbot_id=chatbot_id + ) + + output: str = invoke_lambda( + lambda_name="Online_LLM_Generate", + lambda_module_path="lambda_llm_generate.llm_generate", + handler_name="lambda_handler", + event_body={ + "llm_config": { + **prompt_templates_from_ddb, + **llm_config, + "stream": state["stream"], + "intent_type": task_type, + }, + "llm_input": { + "contexts": context_list, + "query": state["query"], + "chat_history": state["chat_history"], + }, + }, + ) + return output + diff --git a/source/lambda/online/langchain_integration/tools/common_tools/step_back_rag.py b/source/lambda/online/langchain_integration/tools/common_tools/step_back_rag.py new file mode 100644 index 00000000..cbc09a57 --- /dev/null +++ b/source/lambda/online/langchain_integration/tools/common_tools/step_back_rag.py @@ -0,0 +1,50 @@ +# knowledge base retrieve +from common_logic.common_utils.lambda_invoke_utils import invoke_lambda +from common_logic.common_utils.constant import ( + LLMTaskType +) + +def knowledge_base_retrieve(retriever_params, context=None): + output: str = invoke_lambda( + event_body=retriever_params, + lambda_name="Online_Functions", + lambda_module_path="functions.functions_utils.retriever.retriever", + handler_name="lambda_handler", + ) + contexts = [doc["page_content"] for doc in output["result"]["docs"]] + return contexts + +def lambda_handler(event_body, context=None): + state = event_body['state'] + retriever_params = state["chatbot_config"]["step_back_rag_config"]["retriever_config"] + contexts = [] + retriever_params["query"] = event_body['kwargs']['step_back_query'] + contexts.extend(knowledge_base_retrieve(retriever_params, context=context)) + context = "\n\n".join(contexts) + + # llm generate + system_prompt = (f"请根据context内的信息回答问题:\n" + "\n" + " - 回复内容需要展现出礼貌。回答内容为一句话,言简意赅。\n" + " - 每次回答总是先进行思考,并将思考过程写在标签中。\n" + " - 使用中文回答。\n" + "\n" + "\n" + f"{context}\n" + "" + ) + + output:str = invoke_lambda( + lambda_name='Online_LLM_Generate', + lambda_module_path="lambda_llm_generate.llm_generate", + handler_name='lambda_handler', + event_body={ + "llm_config": { + **state['chatbot_config']['rag_daily_reception_config']['llm_config'], + "system_prompt": system_prompt, + "intent_type": LLMTaskType.CHAT}, + "llm_input": {"query": state['query'], "chat_history": state['chat_history']} + } + ) + + return {"code":0, "result":output} \ No newline at end of file From d62f3a9a54f2d054cf139ca99d82d17f5fa1caa5 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 24 Oct 2024 03:24:55 +0000 Subject: [PATCH 05/29] refactor: modify module import --- .../chains/chat_chain.py | 4 +-- .../chains/conversation_summary_chain.py | 4 +-- .../chains/hyde_chain.py | 2 +- .../chains/intention_chain.py | 4 +-- .../chains/llm_chain_base.py | 26 ------------------- .../chains/query_rewrite_chain.py | 2 +- .../langchain_integration/chains/rag_chain.py | 21 +++++++++++++-- .../retail_chains/auto_evaluation_chain.py | 2 +- .../retail_conversation_summary_chain.py | 2 +- .../retail_tool_calling_chain_claude_xml.py | 2 +- .../retail_tool_calling_chain_json.py | 2 +- .../chains/stepback_chain.py | 4 +-- .../chains/tool_calling_chain_claude_xml.py | 4 +-- 13 files changed, 35 insertions(+), 44 deletions(-) delete mode 100644 source/lambda/online/langchain_integration/chains/llm_chain_base.py diff --git a/source/lambda/online/langchain_integration/chains/chat_chain.py b/source/lambda/online/langchain_integration/chains/chat_chain.py index 730a8490..35bdb41c 100644 --- a/source/lambda/online/langchain_integration/chains/chat_chain.py +++ b/source/lambda/online/langchain_integration/chains/chat_chain.py @@ -6,8 +6,8 @@ from langchain_core.messages import convert_to_messages -from ..llm_models import Model -from .llm_chain_base import LLMChain +from ..chat_models import Model +from . import LLMChain from common_logic.common_utils.constant import ( MessageType, diff --git a/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py index c3f1aa1d..c7f0631f 100644 --- a/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py +++ b/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py @@ -7,9 +7,9 @@ ) -from ..llm_models import Model +from ..chat_models import Model from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain +from . import LLMChain from common_logic.common_utils.constant import ( MessageType, LLMTaskType, diff --git a/source/lambda/online/langchain_integration/chains/hyde_chain.py b/source/lambda/online/langchain_integration/chains/hyde_chain.py index de3b0f0d..8fda3f2c 100644 --- a/source/lambda/online/langchain_integration/chains/hyde_chain.py +++ b/source/lambda/online/langchain_integration/chains/hyde_chain.py @@ -17,7 +17,7 @@ from ..chains import LLMChain from ..chat_models import Model as LLM_Model from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain +from . import LLMChain HYDE_TYPE = LLMTaskType.HYDE_TYPE diff --git a/source/lambda/online/langchain_integration/chains/intention_chain.py b/source/lambda/online/langchain_integration/chains/intention_chain.py index 292023fd..4c2d3d20 100644 --- a/source/lambda/online/langchain_integration/chains/intention_chain.py +++ b/source/lambda/online/langchain_integration/chains/intention_chain.py @@ -13,9 +13,9 @@ ) from common_logic.common_utils.constant import LLMTaskType,LLMModelType -from ..llm_models import Model +from ..chat_models import Model from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain +from . import LLMChain abs_dir = os.path.dirname(__file__) diff --git a/source/lambda/online/langchain_integration/chains/llm_chain_base.py b/source/lambda/online/langchain_integration/chains/llm_chain_base.py deleted file mode 100644 index 98ae93d3..00000000 --- a/source/lambda/online/langchain_integration/chains/llm_chain_base.py +++ /dev/null @@ -1,26 +0,0 @@ -class LLMChainMeta(type): - def __new__(cls, name, bases, attrs): - new_cls = type.__new__(cls, name, bases, attrs) - if name == "LLMChain": - return new_cls - new_cls.model_map[new_cls.get_chain_id()] = new_cls - return new_cls - - -class LLMChain(metaclass=LLMChainMeta): - model_map = {} - - @classmethod - def get_chain_id(cls): - return cls._get_chain_id(cls.model_id, cls.intent_type) - - @staticmethod - def _get_chain_id(model_id, intent_type): - return f"{model_id}__{intent_type}" - - @classmethod - def get_chain(cls, model_id, intent_type, model_kwargs=None, **kwargs): - return cls.model_map[cls._get_chain_id(model_id, intent_type)].create_chain( - model_kwargs=model_kwargs, **kwargs - ) - diff --git a/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py b/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py index 331552a1..9379b84e 100644 --- a/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py +++ b/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py @@ -14,7 +14,7 @@ from ..chains import LLMChain from ..chat_models import Model as LLM_Model from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain +from . import LLMChain QUERY_REWRITE_TYPE = LLMTaskType.QUERY_REWRITE_TYPE query_expansion_template_claude = PromptTemplate.from_template("""You are an AI language model assistant. Your task is to generate 1 - 5 different sub questions OR alternate versions of the given user question to retrieve relevant documents from a vector database. diff --git a/source/lambda/online/langchain_integration/chains/rag_chain.py b/source/lambda/online/langchain_integration/chains/rag_chain.py index f04750f6..be9d42ef 100644 --- a/source/lambda/online/langchain_integration/chains/rag_chain.py +++ b/source/lambda/online/langchain_integration/chains/rag_chain.py @@ -14,8 +14,8 @@ from common_logic.common_utils.logger_utils import print_llm_messages # from ...prompt_template import convert_chat_history_from_fstring_format -from ..llm_models import Model -from .llm_chain_base import LLMChain +from ..chat_models import Model +from . import LLMChain def get_claude_rag_context(contexts: list): @@ -81,10 +81,27 @@ class Claude3SonnetRAGLLMChain(Claude2RagLLMChain): class Claude3HaikuRAGLLMChain(Claude2RagLLMChain): model_id = LLMModelType.CLAUDE_3_HAIKU +class Claude35SonnetRAGLLMChain(Claude2RagLLMChain): + model_id = LLMModelType.CLAUDE_3_5_SONNET + + +class Llama31Instruct70B(Claude2RagLLMChain): + model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT + + +class MistraLlarge2407(Claude2RagLLMChain): + model_id = LLMModelType.MISTRAL_LARGE_2407 + + +class CohereCommandRPlus(Claude2RagLLMChain): + model_id = LLMModelType.COHERE_COMMAND_R_PLUS + + class Mixtral8x7bChatChain(Claude2RagLLMChain): model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT + from .chat_chain import GLM4Chat9BChatChain class GLM4Chat9BRagChain(GLM4Chat9BChatChain): diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py b/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py index bcdd7011..28d4b22c 100644 --- a/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py +++ b/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py @@ -12,7 +12,7 @@ LLMModelType, ) from ...llm_models import Model -from ..llm_chain_base import LLMChain +from ..__llm_chain_base import LLMChain from ..chat_chain import Claude2ChatChain diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py b/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py index d5be022e..eae0716d 100644 --- a/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py +++ b/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py @@ -8,7 +8,7 @@ from ...llm_models import Model -from ..llm_chain_base import LLMChain +from ..__llm_chain_base import LLMChain from common_logic.common_utils.constant import ( MessageType, LLMTaskType, diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py index 803e4ef2..71a953c5 100644 --- a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py +++ b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py @@ -22,7 +22,7 @@ SceneType ) from functions import get_tool_by_name -from ..llm_chain_base import LLMChain +from ..__llm_chain_base import LLMChain from ...llm_models import Model tool_call_guidelines = """ diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py index d20bb6c0..f1bc5d8b 100644 --- a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py +++ b/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py @@ -25,7 +25,7 @@ ) from functions import get_tool_by_name -from ..llm_chain_base import LLMChain +from ..__llm_chain_base import LLMChain from ...llm_models import Model from ..chat_chain import GLM4Chat9BChatChain from common_logic.common_utils.logger_utils import get_logger diff --git a/source/lambda/online/langchain_integration/chains/stepback_chain.py b/source/lambda/online/langchain_integration/chains/stepback_chain.py index 4a14db1d..01e21bb4 100644 --- a/source/lambda/online/langchain_integration/chains/stepback_chain.py +++ b/source/lambda/online/langchain_integration/chains/stepback_chain.py @@ -8,8 +8,8 @@ LLMTaskType, LLMModelType ) -from ..chains.chat_chain import Iternlm2Chat7BChatChain -from ..chains.llm_chain_base import LLMChain +from .chat_chain import Iternlm2Chat7BChatChain +from . import LLMChain from ..chat_models import Model STEPBACK_PROMPTING_TYPE = LLMTaskType.STEPBACK_PROMPTING_TYPE diff --git a/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py b/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py index 9d6b84b3..114139f8 100644 --- a/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py +++ b/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py @@ -24,8 +24,8 @@ ) from common_logic.common_utils.time_utils import get_china_now -from .llm_chain_base import LLMChain -from ..llm_models import Model +from . import LLMChain +from ..chat_models import Model incorrect_tool_call_example = """Here is an example of an incorrectly formatted tool call, which you should avoid. From e3f063ee33542730396a3c3e30c7a6b7c8449a2a Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 24 Oct 2024 03:26:49 +0000 Subject: [PATCH 06/29] refactor: add llm chain tool_calling_api and it's prompt template --- .../common_logic/common_utils/prompt_utils.py | 108 ++++-- .../chains/tool_calling_chain_claude_api.py | 320 ++++++++++++++++++ 2 files changed, 392 insertions(+), 36 deletions(-) create mode 100644 source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index 7d146a69..f2a9c3f5 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -17,7 +17,9 @@ EXPORT_MODEL_IDS = [ LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, - # LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS ] EXPORT_SCENES = [ @@ -142,7 +144,10 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, LLMModelType.CLAUDE_INSTANCE, - LLMModelType.MIXTRAL_8X7B_INSTRUCT + LLMModelType.MIXTRAL_8X7B_INSTRUCT, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.RAG, prompt_template=CLAUDE_RAG_SYSTEM_PROMPT, @@ -171,37 +176,9 @@ def prompt_template_render(self, prompt_template: dict): ) -# CHIT_CHAT_SYSTEM_TEMPLATE = "你是一个AI助理。今天是{date},{weekday}. " - -# register_prompt_templates( -# model_ids=[ -# LLMModelType.CLAUDE_2, -# LLMModelType.CLAUDE_21, -# LLMModelType.CLAUDE_3_HAIKU, -# LLMModelType.CLAUDE_3_SONNET, -# LLMModelType.CLAUDE_3_5_SONNET, -# LLMModelType.CLAUDE_INSTANCE, -# LLMModelType.MIXTRAL_8X7B_INSTRUCT, -# LLMModelType.GLM_4_9B_CHAT, -# LLMModelType.QWEN2INSTRUCT72B, -# LLMModelType.QWEN2INSTRUCT7B -# ], -# task_type=LLMTaskType.CHAT, -# prompt_template=CHIT_CHAT_SYSTEM_TEMPLATE, -# prompt_name="system_prompt" -# ) - - -# CQR_TEMPLATE = """Given the following conversation between `USER` and `AI`, and a follow up `USER` reply, Put yourself in the shoes of `USER`, rephrase the follow up \ -# `USER` reply to be a standalone reply. - -# Chat History: -# {history} - -# The USER's follow up reply: {question}""" - - +################ # query rewrite prompt template from paper https://arxiv.org/pdf/2401.10225 +################### CQR_SYSTEM_PROMPT = """You are a helpful, pattern-following assistant.""" CQR_USER_PROMPT_TEMPLATE = """Given the following conversation between PersonU and PersonA: @@ -284,7 +261,11 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.QWEN2INSTRUCT72B, LLMModelType.QWEN2INSTRUCT7B, - LLMModelType.GLM_4_9B_CHAT + LLMModelType.GLM_4_9B_CHAT, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS, + ], task_type=LLMTaskType.CONVERSATION_SUMMARY_TYPE, prompt_template=CQR_SYSTEM_PROMPT, @@ -302,7 +283,10 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.QWEN2INSTRUCT72B, LLMModelType.QWEN2INSTRUCT7B, - LLMModelType.GLM_4_9B_CHAT + LLMModelType.GLM_4_9B_CHAT, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.CONVERSATION_SUMMARY_TYPE, prompt_template=CQR_USER_PROMPT_TEMPLATE, @@ -321,14 +305,19 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.QWEN2INSTRUCT72B, LLMModelType.QWEN2INSTRUCT7B, - LLMModelType.GLM_4_9B_CHAT + LLMModelType.GLM_4_9B_CHAT, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.CONVERSATION_SUMMARY_TYPE, prompt_template=json.dumps(CQR_FEW_SHOTS, ensure_ascii=False, indent=2), prompt_name="few_shots" ) -# agent prompt + + +############## xml agent prompt ############# AGENT_USER_PROMPT = "你是一个AI助理。今天是{date},{weekday}. " register_prompt_templates( model_ids=[ @@ -362,12 +351,59 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.TOOL_CALLING_XML, prompt_template=AGENT_GUIDELINES_PROMPT, prompt_name="guidelines_prompt" ) +################# api agent prompt ##################### +AGENT_USER_PROMPT = "你是一个AI助理。今天是{date},{weekday}. " +register_prompt_templates( + model_ids=[ + LLMModelType.CLAUDE_3_HAIKU, + LLMModelType.CLAUDE_3_SONNET, + LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS, + ], + task_type=LLMTaskType.TOOL_CALLING_API, + prompt_template=AGENT_USER_PROMPT, + prompt_name="user_prompt" +) + +AGENT_GUIDELINES_PROMPT = """ +- 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考: + 1. 判断根据当前的上下文是否足够回答用户的问题。 + 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 + 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用提供的工具。 + 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。如果调用工具不需要参数,则不需要调用反问工具。 + 5. 最后给出你要调用的工具名称。 +- Always output with the same language as user's query. If the content is english, use englisth to output. If the content is Chinese, use Chinese to output. + +""" + +register_prompt_templates( + model_ids=[ + LLMModelType.CLAUDE_2, + LLMModelType.CLAUDE_21, + LLMModelType.CLAUDE_3_HAIKU, + LLMModelType.CLAUDE_3_SONNET, + LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407, + LLMModelType.COHERE_COMMAND_R_PLUS, + ], + task_type=LLMTaskType.TOOL_CALLING_API, + prompt_template=AGENT_GUIDELINES_PROMPT, + prompt_name="guidelines_prompt" +) + + if __name__ == "__main__": print(get_all_templates()) diff --git a/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py b/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py new file mode 100644 index 00000000..55d88f95 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py @@ -0,0 +1,320 @@ +# tool calling chain +import json +from typing import List,Dict,Any +import re + +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough +) +from common_logic.common_utils.prompt_utils import get_prompt_template +from common_logic.common_utils.logger_utils import print_llm_messages +from langchain_core.messages import( + AIMessage, + SystemMessage +) +from langchain.prompts import ChatPromptTemplate +from langchain_core.messages import AIMessage,SystemMessage +from langchain.tools.base import BaseTool + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType, + MessageType +) +from common_logic.common_utils.time_utils import get_china_now + +from . import LLMChain +from ..chat_models import Model + +# incorrect_tool_call_example = """Here is an example of an incorrectly formatted tool call, which you should avoid. +# +# +# +# tool_name +# +# +# question +# string +# value +# +# +# +# +# + +# In this incorrect tool calling example, the parameter `name` should form a XLM tag. +# """ + + +# SYSTEM_MESSAGE_PROMPT =(f"In this environment you have access to a set of tools you can use to answer the user's question.\n" +# "\n" +# "You may call them like this:\n" +# "\n" +# "\n" +# "$TOOL_NAME\n" +# "\n" +# "<$PARAMETER_NAME>$PARAMETER_VALUE\n" +# "...\n" +# "\n" +# "\n" +# "\n" +# "\n" +# "Here are the tools available:\n" +# "\n" +# "{tools}" +# "\n" +# "\nAnswer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided." +# "\nHere are some guidelines for you:\n{tool_call_guidelines}." +# f"\n{incorrect_tool_call_example}" +# ) + +# SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES = SYSTEM_MESSAGE_PROMPT + ( +# "Some examples of tool calls are given below, where the content within represents the most recent reply in the dialog." +# "\n{fewshot_examples}" +# ) + +# TOOL_FORMAT = """ +# {tool_name} +# {tool_description} +# +# {formatted_required_parameters} +# +# +# {formatted_optional_parameters} +# +# """ + +# TOOL_PARAMETER_FORMAT = """ +# {parameter_name} +# {parameter_type} +# {parameter_description} +# """ + +# TOOL_EXECUTE_SUCCESS_TEMPLATE = """ +# +# +# {tool_name} +# +# {result} +# +# +# +# """ + +# TOOL_EXECUTE_FAIL_TEMPLATE = """ +# +# +# {error} +# +# +# """ + +# AGENT_SYSTEM_PROMPT = "你是一个亚马逊云科技的AI助理,你的名字是亚麻小Q。今天是{date_str},{weekday}. " + + +# def _get_type(parameter: Dict[str, Any]) -> str: +# if "type" in parameter: +# return parameter["type"] +# if "anyOf" in parameter: +# return json.dumps({"anyOf": parameter["anyOf"]}) +# if "allOf" in parameter: +# return json.dumps({"allOf": parameter["allOf"]}) +# return json.dumps(parameter) + + +# def convert_openai_tool_to_anthropic(tools:list[dict])->str: +# formatted_tools = tools +# tools_data = [ +# { +# "tool_name": tool["name"], +# "tool_description": tool["description"], +# "formatted_required_parameters": "\n".join( +# [ +# TOOL_PARAMETER_FORMAT.format( +# parameter_name=name, +# parameter_type=_get_type(parameter), +# parameter_description=parameter.get("description"), +# ) for name, parameter in tool["parameters"]["properties"].items() +# if name in tool["parameters"].get("required", []) +# ] +# ), +# "formatted_optional_parameters": "\n".join( +# [ +# TOOL_PARAMETER_FORMAT.format( +# parameter_name=name, +# parameter_type=_get_type(parameter), +# parameter_description=parameter.get("description"), +# ) for name, parameter in tool["parameters"]["properties"].items() +# if name not in tool["parameters"].get("required", []) +# ] +# ), +# } +# for tool in formatted_tools +# ] +# tools_formatted = "\n".join( +# [ +# TOOL_FORMAT.format( +# tool_name=tool["tool_name"], +# tool_description=tool["tool_description"], +# formatted_required_parameters=tool["formatted_required_parameters"], +# formatted_optional_parameters=tool["formatted_optional_parameters"], +# ) +# for tool in tools_data +# ] +# ) +# return tools_formatted + + +class Claude2ToolCallingChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.TOOL_CALLING_API + default_model_kwargs = { + "max_tokens": 2000, + "temperature": 0.1, + "top_p": 0.9 + } + + @staticmethod + def format_fewshot_examples(fewshot_examples:list[dict]): + fewshot_example_strs = [] + for fewshot_example in fewshot_examples: + param_strs = [] + for p,v in fewshot_example['kwargs'].items(): + param_strs.append(f"<{p}>{v}\n" + f"{fewshot_example['query']}\n" + f"\n" + "\n" + "\n" + f"{fewshot_example['name']}\n" + "\n" + f"{param_str}" + "\n" + "\n" + "\n" + "\n" + "" + ) + fewshot_example_strs.append(fewshot_example_str) + fewshot_example_str = '\n'.join(fewshot_example_strs) + return f"\n{fewshot_example_str}\n" + + @classmethod + def parse_function_calls_from_ai_message(cls,message:AIMessage): + content = "" + message.content + "" + function_calls:List[str] = re.findall("(.*?)", content,re.S) + if not function_calls: + content = "" + message.content + + return { + "function_calls": function_calls, + "content": content + } + + @classmethod + def create_chat_history(cls,x): + chat_history = x['chat_history'] + \ + [{"role": MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ + x['agent_tool_history'] + return chat_history + + @classmethod + def get_common_system_prompt(cls,system_prompt_template:str): + now = get_china_now() + date_str = now.strftime("%Y年%m月%d日") + weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] + weekday = weekdays[now.weekday()] + system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) + return system_prompt + + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + tools:list = kwargs['tools'] + assert all(isinstance(tool,BaseTool) for tool in tools),tools + fewshot_examples = kwargs.get('fewshot_examples',[]) + if fewshot_examples: + fewshot_examples.append({ + "name": "give_rhetorical_question", + "query": "今天天气怎么样?", + "kwargs": {"question": "请问你想了解哪个城市的天气?"} + }) + user_system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="user_prompt" + ).prompt_template + + user_system_prompt = kwargs.get("user_prompt",None) or user_system_prompt + + user_system_prompt = cls.get_common_system_prompt( + user_system_prompt + ) + guidelines_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="guidelines_prompt" + ).prompt_template + + guidelines_prompt = kwargs.get("guidelines_prompt",None) or guidelines_prompt + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + # tools_formatted = convert_openai_tool_to_anthropic(tools) + + if fewshot_examples: + system_prompt = SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES.format( + tools=tools_formatted, + fewshot_examples=cls.format_fewshot_examples(fewshot_examples), + tool_call_guidelines=guidelines_prompt + ) + else: + system_prompt = SYSTEM_MESSAGE_PROMPT.format( + tools=tools_formatted, + tool_call_guidelines=guidelines_prompt + ) + + system_prompt = user_system_prompt + system_prompt + tool_calling_template = ChatPromptTemplate.from_messages( + [ + SystemMessage(content=system_prompt), + ("placeholder", "{chat_history}"), + AIMessage(content="") + ]) + + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + ) + chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | tool_calling_template \ + | RunnableLambda(lambda x: print_llm_messages(f"Agent messages: {x.messages}") or x.messages ) \ + | llm | RunnableLambda(lambda message:cls.parse_function_calls_from_ai_message( + message + )) + return chain + + +class Claude21ToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_21 + + +class ClaudeInstanceToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_INSTANCE + + +class Claude3SonnetToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude35SonnetToolCallingChain(Claude2ToolCallingChain): + model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" From a4c99df4ecbb43755f1d75ed833cea6edbbb399c Mon Sep 17 00:00:00 2001 From: zhouxss Date: Tue, 29 Oct 2024 14:26:45 +0000 Subject: [PATCH 07/29] complete the tool refactor, next to test --- .../common_logic/common_utils/prompt_utils.py | 84 ++- .../functions_utils/retriever/retriever.py | 1 - .../online_entries/common_entry_v2.py | 678 ++++++++++++++++++ .../online/langchain_integration/__init__.py | 0 .../chains/chat_chain.py | 13 + .../chains/conversation_summary_chain.py | 20 + .../chains/tool_calling_chain_api.py | 158 ++++ .../chains/tool_calling_chain_claude_api.py | 320 --------- .../langchain_integration/tools/__init__.py | 63 +- .../tools/common_tools/__init__.py | 121 ---- .../tools/common_tools/rag.py | 11 +- 11 files changed, 1003 insertions(+), 466 deletions(-) create mode 100644 source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py create mode 100644 source/lambda/online/langchain_integration/__init__.py create mode 100644 source/lambda/online/langchain_integration/chains/tool_calling_chain_api.py delete mode 100644 source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py delete mode 100644 source/lambda/online/langchain_integration/tools/common_tools/__init__.py diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index f2a9c3f5..644411a6 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -361,7 +361,19 @@ def prompt_template_render(self, prompt_template: dict): ) ################# api agent prompt ##################### -AGENT_USER_PROMPT = "你是一个AI助理。今天是{date},{weekday}. " +AGENT_SYSTEM_PROMPT = """\ +You are a helpful AI assistant. Today is {date},{weekday}. +Here are some guidelines for you: + +- Always start each answer with a reflection and write the reflection process in the tag. Please follow the steps below to think about it: + 1. Determine whether the current context is sufficient to answer the user's question. + 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. + 3. If the current context is not sufficient to answer the user's question, you can consider calling the provided tools. + 4. If the parameters of the tool you call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. If the tool does not require parameters, do not call the `give_rhetorical_question` tool. + 5. Finally, output the name of the tool you want to call. +- Always output with the same language as the content within . If the content is english, use english to output. If the content is chinese, use chinese to output. +""" + register_prompt_templates( model_ids=[ LLMModelType.CLAUDE_3_HAIKU, @@ -372,20 +384,62 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.TOOL_CALLING_API, - prompt_template=AGENT_USER_PROMPT, - prompt_name="user_prompt" + prompt_template=AGENT_SYSTEM_PROMPT, + prompt_name="agent_system_prompt" ) -AGENT_GUIDELINES_PROMPT = """ -- 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考: - 1. 判断根据当前的上下文是否足够回答用户的问题。 - 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 - 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用提供的工具。 - 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。如果调用工具不需要参数,则不需要调用反问工具。 - 5. 最后给出你要调用的工具名称。 -- Always output with the same language as user's query. If the content is english, use englisth to output. If the content is Chinese, use Chinese to output. - -""" +# AGENT_GUIDELINES_PROMPT = """ +# - 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考:。 +# 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 +# 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用提供的工具。 +# 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。如果调用工具不需要参数,则不需要调用反问工具。 +# 5. 最后给出你要调用的工具名称。 +# - Always output with the same language as user's query. If the content is english, use englisth to output. If the content is Chinese, use Chinese to output. +# """ +# register_prompt_templates( +# model_ids=[ +# LLMModelType.CLAUDE_3_HAIKU, +# LLMModelType.CLAUDE_3_SONNET, +# LLMModelType.CLAUDE_3_5_SONNET, +# LLMModelType.LLAMA3_1_70B_INSTRUCT, +# LLMModelType.MISTRAL_LARGE_2407, +# LLMModelType.COHERE_COMMAND_R_PLUS, +# ], +# task_type=LLMTaskType.TOOL_CALLING_API, +# prompt_template=AGENT_USER_PROMPT, +# prompt_name="agent_prompt" +# ) + +# AGENT_GUIDELINES_PROMPT = """ +# - 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考: +# 1. 判断根据当前的上下文是否足够回答用户的问题。 +# 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 +# 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用提供的工具。 +# 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。如果调用工具不需要参数,则不需要调用反问工具。 +# 5. 最后给出你要调用的工具名称。 +# - Always output with the same language as user's query. If the content is english, use englisth to output. If the content is Chinese, use Chinese to output. +# +# """ + +# register_prompt_templates( +# model_ids=[ +# LLMModelType.CLAUDE_2, +# LLMModelType.CLAUDE_21, +# LLMModelType.CLAUDE_3_HAIKU, +# LLMModelType.CLAUDE_3_SONNET, +# LLMModelType.CLAUDE_3_5_SONNET, +# LLMModelType.LLAMA3_1_70B_INSTRUCT, +# LLMModelType.MISTRAL_LARGE_2407, +# LLMModelType.COHERE_COMMAND_R_PLUS, +# ], +# task_type=LLMTaskType.TOOL_CALLING_API, +# prompt_template=AGENT_GUIDELINES_PROMPT, +# prompt_name="guidelines_prompt" +# ) + +TOOL_FEWSHOT_PROMPT = """\ +Input: {query} +Args: {args}""" register_prompt_templates( model_ids=[ @@ -399,8 +453,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.TOOL_CALLING_API, - prompt_template=AGENT_GUIDELINES_PROMPT, - prompt_name="guidelines_prompt" + prompt_template=TOOL_FEWSHOT_PROMPT, + prompt_name="tool_fewshot_prompt" ) diff --git a/source/lambda/online/functions/functions_utils/retriever/retriever.py b/source/lambda/online/functions/functions_utils/retriever/retriever.py index 086006e0..694f8fbd 100644 --- a/source/lambda/online/functions/functions_utils/retriever/retriever.py +++ b/source/lambda/online/functions/functions_utils/retriever/retriever.py @@ -1,6 +1,5 @@ import json import os - os.environ["PYTHONUNBUFFERED"] = "1" import logging import sys diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py new file mode 100644 index 00000000..e05aedcb --- /dev/null +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py @@ -0,0 +1,678 @@ +import json +from typing import Annotated, Any, TypedDict, List +import copy + +from common_logic.common_utils.chatbot_utils import ChatbotManager +from common_logic.common_utils.constant import ( + ChatbotMode, + IndexType, + LLMTaskType, + SceneType, + ToolRuningMode, +) +from common_logic.common_utils.lambda_invoke_utils import ( + invoke_lambda, + is_running_local, + node_monitor_wrapper, + send_trace, +) +from langchain_core.messages import ToolMessage,AIMessage +from common_logic.common_utils.logger_utils import get_logger +from common_logic.common_utils.prompt_utils import get_prompt_templates_from_ddb +from common_logic.common_utils.python_utils import add_messages, update_nest_dict +from common_logic.common_utils.response_utils import process_response +from common_logic.common_utils.serialization_utils import JSONEncoder +from langchain_integration.tools import ToolManager +from langchain_core.tools import BaseTool +from langchain_core.messages.tool import ToolCall +from langgraph.prebuilt import ToolNode +from langchain_integration.chains import LLMChain + + +# from lambda_main.main_utils.online_entries.agent_base import ( +# build_agent_graph, +# tool_execution, +# ) +from lambda_main.main_utils.parse_config import CommonConfigParser +from langgraph.graph import END, StateGraph +from langchain_integration.langgraph_integration import set_currrent_app + +logger = get_logger("common_entry") + + +class ChatbotState(TypedDict): + ########### input/output states ########### + # inputs + # origin event body + event_body: dict + # origianl input question + query: str + # chat history between human and agent + chat_history: Annotated[list[dict], add_messages] + # complete chatbot config, consumed by all the nodes + chatbot_config: dict + # websocket connection id for the agent + ws_connection_id: str + # whether to enbale stream output via ws_connection_id + stream: bool + # message id related to original input question + message_id: str = None + # record running states of different nodes + trace_infos: Annotated[list[str], add_messages] + # whether to enbale trace info update via streaming ouput + enable_trace: bool + # outputs + # final answer generated by whole app graph + answer: Any + # information needed return to user, e.g. intention, context, figure and so on, anything you can get during execution + extra_response: Annotated[dict, update_nest_dict] + # addition kwargs which need to save into ddb + ddb_additional_kwargs: dict + # response of entire app + app_response: Any + + ########### query rewrite states ########### + # query rewrite results + query_rewrite: str = None + + ########### intention detection states ########### + # intention type of retrieved intention samples in search engine, e.g. OpenSearch + intent_type: str = None + # retrieved intention samples in search engine, e.g. OpenSearch + intent_fewshot_examples: list + # tools of retrieved intention samples in search engine, e.g. OpenSearch + intent_fewshot_tools: list + + ########### retriever states ########### + # contexts information retrieved in search engine, e.g. OpenSearch + qq_match_results: list = [] + contexts: str = None + figure: list = None + + ########### agent states ########### + # current output of agent + # agent_current_output: dict + # # record messages during agent tool choose and calling, including agent message, tool ouput and error messages + agent_tool_history: Annotated[List[AIMessage | ToolMessage], add_messages] + # # the maximum number that agent node can be called + # agent_repeated_call_limit: int + # # the current call time of agent + # agent_current_call_number: int # + # # whehter the current call time is less than maximum number of agent call + # agent_repeated_call_validation: bool + # # function calling + # # whether the output of agent can be parsed as the valid tool calling + # function_calling_parse_ok: bool + # # whether the current parsed tool calling is run once + tool_calling_is_run_once: bool + # # current tool calls + # function_calling_parsed_tool_calls: list + # current_agent_tools_def: list + last_tool_messages: List[ToolMessage] + tools: List[BaseTool] + # the global rag tool use all knowledge + all_knowledge_rag_tool: BaseTool + + +def is_null_or_empty(value): + if value is None: + return True + elif isinstance(value, (dict, list, str)) and not value: + return True + return False + + +def format_intention_output(data): + if is_null_or_empty(data): + return "" + + markdown_table = "| Query | Score | Name | Intent | Additional Info |\n" + markdown_table += "|----------------------|-------|------------|-------------|----------------------|\n" + for item in data: + query = item.get("query", "") + score = item.get("score", "") + name = item.get("name", "") + intent = item.get("intent", "") + kwargs = ', '.join([f'{k}: {v}' for k, v in item.get('kwargs', {}).items()]) + markdown_table += f"| {query} | {score} | {name} | {intent} | {kwargs} |\n" + logger.info(markdown_table) + + return markdown_table + +#################### +# nodes in graph # +#################### + + +@node_monitor_wrapper +def query_preprocess(state: ChatbotState): + output: str = invoke_lambda( + event_body=state, + lambda_name="Online_Query_Preprocess", + lambda_module_path="lambda_query_preprocess.query_preprocess", + handler_name="lambda_handler", + ) + + send_trace(f"\n**query rewrite:** {output}\n**origin query:** {state['query']}") + return {"query_rewrite": output} + + +@node_monitor_wrapper +def intention_detection(state: ChatbotState): + # if state['chatbot_config']['agent_config']['only_use_rag_tool']: + # return { + # "intent_type": "intention detected" + # } + retriever_params = state["chatbot_config"]["qq_match_config"] + retriever_params["query"] = state[ + retriever_params.get("retriever_config", {}).get("query_key", "query") + ] + output: str = invoke_lambda( + event_body=retriever_params, + lambda_name="Online_Functions", + lambda_module_path="functions.functions_utils.retriever.retriever", + handler_name="lambda_handler", + ) + context_list = [] + qq_match_threshold = retriever_params["threshold"] + for doc in output["result"]["docs"]: + if doc["retrieval_score"] > qq_match_threshold: + send_trace( + f"\n\n**similar query found**\n{doc}", + state["stream"], + state["ws_connection_id"], + state["enable_trace"], + ) + query_content = doc["answer"] + # query_content = doc['answer']['jsonlAnswer'] + return { + "answer": query_content, + "intent_type": "similar query found", + } + question = doc["question"] + answer = doc["answer"] + context_list.append(f"问题: {question}, \n答案:{answer}") + + if state["chatbot_config"]["agent_config"]["only_use_rag_tool"]: + return {"qq_match_results": context_list, "intent_type": "intention detected"} + + intent_fewshot_examples = invoke_lambda( + lambda_module_path="lambda_intention_detection.intention", + lambda_name="Online_Intention_Detection", + handler_name="lambda_handler", + event_body=state, + ) + + intent_fewshot_tools: list[str] = list( + set([e["intent"] for e in intent_fewshot_examples]) + ) + + markdown_table = format_intention_output(intent_fewshot_examples) + send_trace( + f"**intention retrieved:**\n\n {markdown_table}", + state["stream"], + state["ws_connection_id"], + state["enable_trace"], + ) + return { + "intent_fewshot_examples": intent_fewshot_examples, + "intent_fewshot_tools": intent_fewshot_tools, + "qq_match_results": context_list, + "intent_type": "intention detected", + } + + +@node_monitor_wrapper +def agent(state: ChatbotState): + # two cases to invoke rag function + # 1. when valid intention fewshot found + # 2. for the first time, agent decides to give final results + + # deal with once tool calling + last_tool_messages = state["last_tool_messages"] + if last_tool_messages and len(last_tool_messages) == 1: + last_tool_message = last_tool_messages[0] + tool:BaseTool = ToolManager.get_tool( + scene=SceneType.COMMON, + name=last_tool_message.name + ) + if tool.return_direct: + send_trace("once tool", enable_trace=state["enable_trace"]) + return {"answer": last_tool_message.content, "tool_calling_is_run_once": True} + + # tool_execute_res = last_tool_calls_results[-1].additional_kwargs[ + # "raw_tool_call_results" + # ][0] + # tool_name = tool_execute_res["name"] + # output = tool_execute_res["output"] + # tool = get_tool_by_name(tool_name, scene=SceneType.COMMON) + # if tool.running_mode == ToolRuningMode.ONCE: + # send_trace("once tool", enable_trace=state["enable_trace"]) + # return {"answer": output["result"], "tool_calling_is_run_once": True} + + + + # if state["agent_tool_history"] and state["agent_tool_history"][-1].type=="tool_call": + # tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ + # "raw_tool_call_results" + # ][0] + # tool_name = tool_execute_res["name"] + # output = tool_execute_res["output"] + # tool = get_tool_by_name(tool_name, scene=SceneType.COMMON) + # if tool.running_mode == ToolRuningMode.ONCE: + # send_trace("once tool", enable_trace=state["enable_trace"]) + # return {"answer": output["result"], "tool_calling_is_run_once": True} + + no_intention_condition = not state["intent_fewshot_examples"] + # first_tool_final_response = False + # if ( + # (state["agent_current_call_number"] == 1) + # and state["function_calling_parse_ok"] + # and state["agent_tool_history"] + # ): + # tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ + # "raw_tool_call_results" + # ][0] + # tool_name = tool_execute_res["name"] + # if tool_name == "give_final_response": + # first_tool_final_response = True + + if ( + no_intention_condition + # or first_tool_final_response + or state["chatbot_config"]["agent_config"]["only_use_rag_tool"] + ): + if state["chatbot_config"]["agent_config"]["only_use_rag_tool"]: + send_trace("agent only use rag tool", enable_trace=state["enable_trace"]) + elif no_intention_condition: + send_trace( + "no_intention_condition, switch to rag tool", + enable_trace=state["enable_trace"], + ) + # elif first_tool_final_response: + # send_trace( + # "first tool is final response, switch to rag tool", + # enable_trace=state["enable_trace"], + # ) + + all_knowledge_rag_tool = state['all_knowledge_rag_tool'] + return AIMessage(content="",tool_calls=[ + ToolCall( + name=all_knowledge_rag_tool.name, + args={} + ) + ]) + + # response = app_agent.invoke(state) + + # normal call + agent_config = state["chatbot_config"]['agent_config'] + tools_name = state['intent_fewshot_tools'] + agent_config['tools'] + # get tools from tool names + tools = [ + ToolManager.get_tool( + scene=SceneType.COMMON, + name=name + ) + for name in tools_name + ] + llm_config = { + **agent_config['llm_config'], + "tools": tools, + "fewshot_examples": state['intent_fewshot_examples'], + } + group_name = state['chatbot_config']['group_name'] + chatbot_id = state['chatbot_config']['chatbot_id'] + prompt_templates_from_ddb = get_prompt_templates_from_ddb( + group_name, + model_id = llm_config['model_id'], + task_type=LLMTaskType.TOOL_CALLING_API, + chatbot_id=chatbot_id + ) + llm_config.update(**prompt_templates_from_ddb) + + tool_calling_chain = LLMChain.get_chain( + intent_type=LLMTaskType.TOOL_CALLING_API, + scene=SceneType.COMMON, + **llm_config + ) + agent_message:AIMessage = tool_calling_chain.invoke(**state) + send_trace( + # f"\n\n**agent_current_output:** \n{agent_message}\n\n **agent_current_call_number:** {agent_current_call_number}", + f"\n\n**agent_current_output:** \n{agent_message}\n\n", + state["stream"], + state["ws_connection_id"] + ) + + return {"agent_tool_history":[agent_message],"tools":tools} + + +@node_monitor_wrapper +def llm_direct_results_generation(state: ChatbotState): + group_name = state["chatbot_config"]["group_name"] + llm_config = state["chatbot_config"]["chat_config"] + task_type = LLMTaskType.CHAT + + prompt_templates_from_ddb = get_prompt_templates_from_ddb( + group_name, model_id=llm_config["model_id"], task_type=task_type + ) + logger.info(prompt_templates_from_ddb) + + answer: dict = invoke_lambda( + event_body={ + "llm_config": { + **llm_config, + "stream": state["stream"], + "intent_type": task_type, + **prompt_templates_from_ddb, + }, + "llm_input": { + "query": state["query"], + "chat_history": state["chat_history"], + }, + }, + lambda_name="Online_LLM_Generate", + lambda_module_path="lambda_llm_generate.llm_generate", + handler_name="lambda_handler", + ) + return {"answer": answer} + + +@node_monitor_wrapper +def tool_execution(state): + """executor lambda + Args: + state (NestUpdateState): _description_ + + Returns: + _type_: _description_ + """ + tools:List[BaseTool] = state['tools'] + tool_node = ToolNode(tools) + last_agent_message:AIMessage = state["agent_tool_history"][-1] + + # pass state to tools if needed + tools_map = {tool.name:tool for tool in tools} + tool_calls:List[ToolCall] = copy.deepcopy(last_agent_message.tool_calls) + + for tool_call in tool_calls: + tool = tools_map[tool_call.name] + if tool.pass_state: + tool_call.args.update({tool.pass_state_name:state}) + + tool_messages:List[ToolMessage] = tool_node.invoke( + [AIMessage(content="",tool_calls=tool_calls)] + ) + + # tool_calls = state['function_calling_parsed_tool_calls'] + # assert len(tool_calls) == 1, tool_calls + # tool_call_results = [] + # for tool_call in tool_calls: + # tool_name = tool_call["name"] + # tool_kwargs = tool_call['kwargs'] + # # call tool + # output = invoke_lambda( + # event_body = { + # "tool_name":tool_name, + # "state":state, + # "kwargs":tool_kwargs + # }, + # lambda_name="Online_Tool_Execute", + # lambda_module_path="functions.lambda_tool", + # handler_name="lambda_handler" + # ) + # tool_call_results.append({ + # "name": tool_name, + # "output": output, + # "kwargs": tool_call['kwargs'], + # "model_id": tool_call['model_id'] + # }) + + # output = format_tool_call_results(tool_call['model_id'],tool_call_results) + send_trace(f'**tool_execute_res:** \n{tool_messages}', enable_trace=state["enable_trace"]) + return { + "agent_tool_history": tool_messages, + "last_tool_messages": tool_messages + } + + +def final_results_preparation(state: ChatbotState): + app_response = process_response(state["event_body"], state) + return {"app_response": app_response} + + +def matched_query_return(state: ChatbotState): + return {"answer": state["answer"]} + + +################ +# define edges # +################ + + +def query_route(state: dict): + return f"{state['chatbot_config']['chatbot_mode']} mode" + + +def intent_route(state: dict): + return state["intent_type"] + + +def agent_route(state: dict): + if state.get("tool_calling_is_run_once", False): + return "no need tool calling" + + # state["agent_repeated_call_validation"] = ( + # state["agent_current_call_number"] < state["agent_repeated_call_limit"] + # ) + + if state["agent_repeated_call_validation"]: + return "valid tool calling" + else: + # TODO give final strategy + raise RuntimeError + + +############################# +# define online top-level graph for app # +############################# + + +def build_graph(chatbot_state_cls): + workflow = StateGraph(chatbot_state_cls) + + # add node for all chat/rag/agent mode + workflow.add_node("query_preprocess", query_preprocess) + # chat mode + workflow.add_node("llm_direct_results_generation", llm_direct_results_generation) + # rag mode + # workflow.add_node("knowledge_retrieve", knowledge_retrieve) + # workflow.add_node("llm_rag_results_generation", llm_rag_results_generation) + # agent mode + workflow.add_node("intention_detection", intention_detection) + workflow.add_node("matched_query_return", matched_query_return) + # agent sub graph + workflow.add_node("agent", agent) + workflow.add_node("tools_execution", tool_execution) + workflow.add_node("final_results_preparation", final_results_preparation) + + # add all edges + workflow.set_entry_point("query_preprocess") + # chat mode + workflow.add_edge("llm_direct_results_generation", "final_results_preparation") + # rag mode + # workflow.add_edge("knowledge_retrieve", "llm_rag_results_generation") + # workflow.add_edge("llm_rag_results_generation", END) + # agent mode + workflow.add_edge("tools_execution", "agent") + workflow.add_edge("matched_query_return", "final_results_preparation") + workflow.add_edge("final_results_preparation", END) + + # add conditional edges + # choose running mode based on user selection: + # 1. chat mode: let llm generate results directly + # 2. rag mode: retrive all knowledge and let llm generate results + # 3. agent mode: let llm generate results based on intention detection, tool calling and retrieved knowledge + workflow.add_conditional_edges( + "query_preprocess", + query_route, + { + "chat mode": "llm_direct_results_generation", + "agent mode": "intention_detection", + }, + ) + + # three running branch will be chosen based on intention detection results: + # 1. similar query found: if very similar queries were found in knowledge base, these queries will be given as results + # 2. intention detected: if intention detected, the agent logic will be invoked + workflow.add_conditional_edges( + "intention_detection", + intent_route, + { + "similar query found": "matched_query_return", + "intention detected": "agent", + }, + ) + + # the results of agent planning will be evaluated and decide next step: + # 1. valid tool calling: the agent chooses the valid tools, and the tools will be executed + # 2. no need tool calling: the agent thinks no tool needs to be called, the final results can be generated + workflow.add_conditional_edges( + "agent", + agent_route, + { + "valid tool calling": "tools_execution", + "no need tool calling": "final_results_preparation", + }, + ) + + app = workflow.compile() + return app + + +##################################### +# define online sub-graph for agent # +##################################### +# app_agent = None +app = None + + +# def register_rag_tool( +# name: str, +# description: str, +# scene=SceneType.COMMON, +# lambda_name: str = "lambda_common_tools", +# ): +# tool_manager.register_tool( +# { +# "name": name, +# "scene": scene, +# "lambda_name": lambda_name, +# "lambda_module_path": rag.lambda_handler, +# "tool_def": { +# "name": name, +# "description": description, +# }, +# "running_mode": ToolRuningMode.ONCE, +# } +# ) + +def register_rag_tool_from_config(event_body: dict): + group_name = event_body.get("chatbot_config").get("group_name", "Admin") + chatbot_id = event_body.get("chatbot_config").get("chatbot_id", "admin") + chatbot_manager = ChatbotManager.from_environ() + chatbot = chatbot_manager.get_chatbot(group_name, chatbot_id) + logger.info(chatbot) + for index_type, item_dict in chatbot.index_ids.items(): + if index_type != IndexType.INTENTION: + for index_content in item_dict["value"].values(): + if "indexId" in index_content and "description" in index_content: + # TODO give specific retriever config + ToolManager.register_common_rag_tool( + retriever_config=event_body["chatbot_config"]["private_knowledge_config"], + name=index_content["indexId"], + scene=SceneType.COMMON, + description=index_content["description"], + pass_state=True, + pass_state_name='state' + ) + + +def common_entry(event_body): + """ + Entry point for the Lambda function. + :param event_body: The event body for lambda function. + return: answer(str) + """ + global app, app_agent + if app is None: + app = build_graph(ChatbotState) + + # if app_agent is None: + # app_agent = build_agent_graph(ChatbotState) + + # debuging + if is_running_local(): + with open("common_entry_workflow.png", "wb") as f: + f.write(app.get_graph().draw_mermaid_png()) + + # with open("common_entry_agent_workflow.png", "wb") as f: + # f.write(app_agent.get_graph().draw_mermaid_png()) + + ################################################################################ + # prepare inputs and invoke graph + event_body["chatbot_config"] = CommonConfigParser.from_chatbot_config( + event_body["chatbot_config"] + ) + logger.info(event_body) + chatbot_config = event_body["chatbot_config"] + query = event_body["query"] + use_history = chatbot_config["use_history"] + chat_history = event_body["chat_history"] if use_history else [] + stream = event_body["stream"] + message_id = event_body["custom_message_id"] + ws_connection_id = event_body["ws_connection_id"] + enable_trace = chatbot_config["enable_trace"] + + # register as rag tool for each aos index + register_rag_tool_from_config(event_body) + + # define all knowledge rag tool + all_knowledge_rag_tool = ToolManager.register_common_rag_tool( + retriever_config=event_body["chatbot_config"]["private_knowledge_config"], + name="all_knowledge_rag_tool", + scene=SceneType.COMMON, + description="all knowledge rag tool", + pass_state=True, + pass_state_name='state' + ) + + # invoke graph and get results + response = app.invoke( + { + "event_body": event_body, + "stream": stream, + "chatbot_config": chatbot_config, + "query": query, + "enable_trace": enable_trace, + "trace_infos": [], + "message_id": message_id, + "chat_history": chat_history, + "agent_tool_history": [], + "ws_connection_id": ws_connection_id, + "debug_infos": {}, + "extra_response": {}, + "qq_match_results": [], + "last_tool_calls_results":None, + "all_knowledge_rag_tool":all_knowledge_rag_tool, + "tools":None, + # "agent_repeated_call_limit": chatbot_config["agent_repeated_call_limit"], + # "agent_current_call_number": 0, + "ddb_additional_kwargs": {}, + + } + ) + return response["app_response"] + + +main_chain_entry = common_entry diff --git a/source/lambda/online/langchain_integration/__init__.py b/source/lambda/online/langchain_integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/source/lambda/online/langchain_integration/chains/chat_chain.py b/source/lambda/online/langchain_integration/chains/chat_chain.py index 35bdb41c..b51e342d 100644 --- a/source/lambda/online/langchain_integration/chains/chat_chain.py +++ b/source/lambda/online/langchain_integration/chains/chat_chain.py @@ -98,6 +98,19 @@ class Mixtral8x7bChatChain(Claude2ChatChain): default_model_kwargs = {"max_tokens": 4096, "temperature": 0.01} +class Llama31Instruct70BChatChain(Claude2ChatChain): + model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT + + +class MistraLlargeChat2407ChatChain(Claude2ChatChain): + model_id = LLMModelType.MISTRAL_LARGE_2407 + + +class CohereCommandRPlusChatChain(Claude2ChatChain): + model_id = LLMModelType.COHERE_COMMAND_R_PLUS + + + class Baichuan2Chat13B4BitsChatChain(LLMChain): model_id = LLMModelType.BAICHUAN2_13B_CHAT intent_type = LLMTaskType.CHAT diff --git a/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py index c7f0631f..61b67598 100644 --- a/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py +++ b/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py @@ -197,6 +197,26 @@ class Claude3HaikuConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.CLAUDE_3_HAIKU +class Mixtral8x7bConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT + default_model_kwargs = {"max_tokens": 4096, "temperature": 0.01} + + +class Llama31Instruct70BConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT + + +class MistraLlargeChat2407ConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.MISTRAL_LARGE_2407 + + +class CohereCommandRPlusConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.COHERE_COMMAND_R_PLUS + + + + + class Qwen2Instruct72BConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.QWEN2INSTRUCT72B diff --git a/source/lambda/online/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/langchain_integration/chains/tool_calling_chain_api.py new file mode 100644 index 00000000..be918e36 --- /dev/null +++ b/source/lambda/online/langchain_integration/chains/tool_calling_chain_api.py @@ -0,0 +1,158 @@ +# tool calling chain +import json +from typing import List,Dict,Any +import re +from collections import defaultdict + +from langchain.schema.runnable import ( + RunnableLambda, + RunnablePassthrough +) +from common_logic.common_utils.prompt_utils import get_prompt_template +from common_logic.common_utils.logger_utils import print_llm_messages +from langchain_core.messages import( + AIMessage, + SystemMessage +) +from langchain.prompts import ChatPromptTemplate +from langchain_core.messages import AIMessage,SystemMessage +from langchain.tools.base import BaseTool +from langchain_core.language_models import BaseChatModel + +from common_logic.common_utils.constant import ( + LLMTaskType, + LLMModelType, + MessageType +) +from common_logic.common_utils.time_utils import get_china_now + +from . import LLMChain +from ..chat_models import Model + + +class Claude2ToolCallingChain(LLMChain): + model_id = LLMModelType.CLAUDE_2 + intent_type = LLMTaskType.TOOL_CALLING_API + default_model_kwargs = { + "max_tokens": 2000, + "temperature": 0.1, + "top_p": 0.9 + } + + @classmethod + def create_chat_history(cls,x): + chat_history = x['chat_history'] + \ + [{"role": MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ + x['agent_tool_history'] + return chat_history + + @classmethod + def get_common_system_prompt(cls,system_prompt_template:str): + now = get_china_now() + date_str = now.strftime("%Y年%m月%d日") + weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] + weekday = weekdays[now.weekday()] + system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) + return system_prompt + + + @classmethod + def bind_tools(cls,llm:BaseChatModel,tools:List[BaseTool], fewshot_examples=None, fewshot_template=None,tool_choice='any'): + tools = [tool.model_copy() for tool in tools] + if not fewshot_examples: + return llm.bind_tools(tools,tool_choice=tool_choice) + + # add fewshot examples to tool description + tools_map = {tool.name:tool for tool in tools} + + # group fewshot examples + fewshot_examples_grouped = defaultdict(list) + for example in fewshot_examples: + fewshot_examples_grouped[example['name']].append(example) + + for tool_name,examples in fewshot_examples_grouped.items(): + tool = tools_map[tool_name] + tool.description += "\n\nHere are some examples where this tool are called:\n" + examples_strs = [] + for example in examples: + params_str = json.dumps(example['kwargs'],ensure_ascii=False) + examples_strs.append( + fewshot_template.format( + query=example['query'], + args=params_str + ) + ) + + tool.description += "\n\n".join(examples_strs) + return llm.bind_tools(tools,tool_choice=tool_choice) + + + @classmethod + def create_chain(cls, model_kwargs=None, **kwargs): + model_kwargs = model_kwargs or {} + tools:list = kwargs['tools'] + assert all(isinstance(tool,BaseTool) for tool in tools),tools + fewshot_examples = kwargs.get('fewshot_examples',[]) + if fewshot_examples: + fewshot_examples.append({ + "name": "give_rhetorical_question", + "query": "今天天气怎么样?", + "kwargs": {"question": "请问你想了解哪个城市的天气?"} + }) + agent_system_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="agent_system_prompt" + ).prompt_template + + agent_system_prompt = kwargs.get("agent_system_prompt",None) or agent_system_prompt + + agent_system_prompt = cls.get_common_system_prompt( + agent_system_prompt + ) + + # tool fewshot prompt + tool_fewshot_prompt = get_prompt_template( + model_id=cls.model_id, + task_type=cls.intent_type, + prompt_name="tool_fewshot_prompt" + ).prompt_template + tool_fewshot_prompt = kwargs.get('tool_fewshot_prompt',None) or tool_fewshot_prompt + + model_kwargs = {**cls.default_model_kwargs, **model_kwargs} + + llm = Model.get_model( + model_id=cls.model_id, + model_kwargs=model_kwargs, + ) + llm = cls.bind_tools(llm,tools,fewshot_examples,fewshot_template=tool_fewshot_prompt) + chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | llm + return chain + + +class Claude21ToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_21 + + +class Claude3SonnetToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_SONNET + + +class Claude3HaikuToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_HAIKU + + +class Claude35SonnetToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_5_SONNET + + +class Llama31Instruct70BToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT + + +class MistraLlarge2407ToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.MISTRAL_LARGE_2407 + + +class CohereCommandRPlusToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.COHERE_COMMAND_R_PLUS diff --git a/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py b/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py deleted file mode 100644 index 55d88f95..00000000 --- a/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_api.py +++ /dev/null @@ -1,320 +0,0 @@ -# tool calling chain -import json -from typing import List,Dict,Any -import re - -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough -) -from common_logic.common_utils.prompt_utils import get_prompt_template -from common_logic.common_utils.logger_utils import print_llm_messages -from langchain_core.messages import( - AIMessage, - SystemMessage -) -from langchain.prompts import ChatPromptTemplate -from langchain_core.messages import AIMessage,SystemMessage -from langchain.tools.base import BaseTool - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType, - MessageType -) -from common_logic.common_utils.time_utils import get_china_now - -from . import LLMChain -from ..chat_models import Model - -# incorrect_tool_call_example = """Here is an example of an incorrectly formatted tool call, which you should avoid. -# -# -# -# tool_name -# -# -# question -# string -# value -# -# -# -# -# - -# In this incorrect tool calling example, the parameter `name` should form a XLM tag. -# """ - - -# SYSTEM_MESSAGE_PROMPT =(f"In this environment you have access to a set of tools you can use to answer the user's question.\n" -# "\n" -# "You may call them like this:\n" -# "\n" -# "\n" -# "$TOOL_NAME\n" -# "\n" -# "<$PARAMETER_NAME>$PARAMETER_VALUE\n" -# "...\n" -# "\n" -# "\n" -# "\n" -# "\n" -# "Here are the tools available:\n" -# "\n" -# "{tools}" -# "\n" -# "\nAnswer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided." -# "\nHere are some guidelines for you:\n{tool_call_guidelines}." -# f"\n{incorrect_tool_call_example}" -# ) - -# SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES = SYSTEM_MESSAGE_PROMPT + ( -# "Some examples of tool calls are given below, where the content within represents the most recent reply in the dialog." -# "\n{fewshot_examples}" -# ) - -# TOOL_FORMAT = """ -# {tool_name} -# {tool_description} -# -# {formatted_required_parameters} -# -# -# {formatted_optional_parameters} -# -# """ - -# TOOL_PARAMETER_FORMAT = """ -# {parameter_name} -# {parameter_type} -# {parameter_description} -# """ - -# TOOL_EXECUTE_SUCCESS_TEMPLATE = """ -# -# -# {tool_name} -# -# {result} -# -# -# -# """ - -# TOOL_EXECUTE_FAIL_TEMPLATE = """ -# -# -# {error} -# -# -# """ - -# AGENT_SYSTEM_PROMPT = "你是一个亚马逊云科技的AI助理,你的名字是亚麻小Q。今天是{date_str},{weekday}. " - - -# def _get_type(parameter: Dict[str, Any]) -> str: -# if "type" in parameter: -# return parameter["type"] -# if "anyOf" in parameter: -# return json.dumps({"anyOf": parameter["anyOf"]}) -# if "allOf" in parameter: -# return json.dumps({"allOf": parameter["allOf"]}) -# return json.dumps(parameter) - - -# def convert_openai_tool_to_anthropic(tools:list[dict])->str: -# formatted_tools = tools -# tools_data = [ -# { -# "tool_name": tool["name"], -# "tool_description": tool["description"], -# "formatted_required_parameters": "\n".join( -# [ -# TOOL_PARAMETER_FORMAT.format( -# parameter_name=name, -# parameter_type=_get_type(parameter), -# parameter_description=parameter.get("description"), -# ) for name, parameter in tool["parameters"]["properties"].items() -# if name in tool["parameters"].get("required", []) -# ] -# ), -# "formatted_optional_parameters": "\n".join( -# [ -# TOOL_PARAMETER_FORMAT.format( -# parameter_name=name, -# parameter_type=_get_type(parameter), -# parameter_description=parameter.get("description"), -# ) for name, parameter in tool["parameters"]["properties"].items() -# if name not in tool["parameters"].get("required", []) -# ] -# ), -# } -# for tool in formatted_tools -# ] -# tools_formatted = "\n".join( -# [ -# TOOL_FORMAT.format( -# tool_name=tool["tool_name"], -# tool_description=tool["tool_description"], -# formatted_required_parameters=tool["formatted_required_parameters"], -# formatted_optional_parameters=tool["formatted_optional_parameters"], -# ) -# for tool in tools_data -# ] -# ) -# return tools_formatted - - -class Claude2ToolCallingChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.TOOL_CALLING_API - default_model_kwargs = { - "max_tokens": 2000, - "temperature": 0.1, - "top_p": 0.9 - } - - @staticmethod - def format_fewshot_examples(fewshot_examples:list[dict]): - fewshot_example_strs = [] - for fewshot_example in fewshot_examples: - param_strs = [] - for p,v in fewshot_example['kwargs'].items(): - param_strs.append(f"<{p}>{v}\n" - f"{fewshot_example['query']}\n" - f"\n" - "\n" - "\n" - f"{fewshot_example['name']}\n" - "\n" - f"{param_str}" - "\n" - "\n" - "\n" - "\n" - "" - ) - fewshot_example_strs.append(fewshot_example_str) - fewshot_example_str = '\n'.join(fewshot_example_strs) - return f"\n{fewshot_example_str}\n" - - @classmethod - def parse_function_calls_from_ai_message(cls,message:AIMessage): - content = "" + message.content + "" - function_calls:List[str] = re.findall("(.*?)", content,re.S) - if not function_calls: - content = "" + message.content - - return { - "function_calls": function_calls, - "content": content - } - - @classmethod - def create_chat_history(cls,x): - chat_history = x['chat_history'] + \ - [{"role": MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ - x['agent_tool_history'] - return chat_history - - @classmethod - def get_common_system_prompt(cls,system_prompt_template:str): - now = get_china_now() - date_str = now.strftime("%Y年%m月%d日") - weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] - weekday = weekdays[now.weekday()] - system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) - return system_prompt - - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - tools:list = kwargs['tools'] - assert all(isinstance(tool,BaseTool) for tool in tools),tools - fewshot_examples = kwargs.get('fewshot_examples',[]) - if fewshot_examples: - fewshot_examples.append({ - "name": "give_rhetorical_question", - "query": "今天天气怎么样?", - "kwargs": {"question": "请问你想了解哪个城市的天气?"} - }) - user_system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="user_prompt" - ).prompt_template - - user_system_prompt = kwargs.get("user_prompt",None) or user_system_prompt - - user_system_prompt = cls.get_common_system_prompt( - user_system_prompt - ) - guidelines_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="guidelines_prompt" - ).prompt_template - - guidelines_prompt = kwargs.get("guidelines_prompt",None) or guidelines_prompt - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - # tools_formatted = convert_openai_tool_to_anthropic(tools) - - if fewshot_examples: - system_prompt = SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES.format( - tools=tools_formatted, - fewshot_examples=cls.format_fewshot_examples(fewshot_examples), - tool_call_guidelines=guidelines_prompt - ) - else: - system_prompt = SYSTEM_MESSAGE_PROMPT.format( - tools=tools_formatted, - tool_call_guidelines=guidelines_prompt - ) - - system_prompt = user_system_prompt + system_prompt - tool_calling_template = ChatPromptTemplate.from_messages( - [ - SystemMessage(content=system_prompt), - ("placeholder", "{chat_history}"), - AIMessage(content="") - ]) - - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - ) - chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | tool_calling_template \ - | RunnableLambda(lambda x: print_llm_messages(f"Agent messages: {x.messages}") or x.messages ) \ - | llm | RunnableLambda(lambda message:cls.parse_function_calls_from_ai_message( - message - )) - return chain - - -class Claude21ToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35SonnetToolCallingChain(Claude2ToolCallingChain): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" diff --git a/source/lambda/online/langchain_integration/tools/__init__.py b/source/lambda/online/langchain_integration/tools/__init__.py index 91e937b2..0e81f84b 100644 --- a/source/lambda/online/langchain_integration/tools/__init__.py +++ b/source/lambda/online/langchain_integration/tools/__init__.py @@ -39,10 +39,18 @@ from datamodel_code_generator import DataModelType, PythonVersion from datamodel_code_generator.model import get_data_model_types from datamodel_code_generator.parser.jsonschema import JsonSchemaParser -from langchain.tools.base import StructuredTool,BaseTool - +from langchain.tools.base import StructuredTool as _StructuredTool ,BaseTool +from langchain_core.pydantic_v1 import create_model,BaseModel from common_logic.common_utils.constant import SceneType from common_logic.common_utils.lambda_invoke_utils import invoke_with_lambda +from functools import partial + + + +class StructuredTool(_StructuredTool): + pass_state:bool = False # if pass state into tool invoke + pass_state_name:str = "state" # pass state name + class ToolIdentifier(BaseModel): @@ -104,6 +112,7 @@ def register_lc_tool( ) assert isinstance(tool,BaseTool),(tool,type(tool)) cls.tool_map[tool_identifier.tool_id] = tool + return tool @classmethod @@ -131,7 +140,7 @@ def register_func_as_tool( return_direct=return_direct ) # register tool - ToolManager.register_lc_tool( + return ToolManager.register_lc_tool( tool_identifier=tool_identifier, tool=tool ) @@ -165,11 +174,55 @@ def _func(**kargs): ), return_direct=return_direct ) - ToolManager.register_lc_tool( + return ToolManager.register_lc_tool( tool_identifier=tool_identifier, tool=tool ) + @classmethod + def register_common_rag_tool( + cls, + retriever_config:dict, + description:str, + scene=None, + name=None, + tool_identifier=None, + return_direct=False, + pass_state=True, + pass_state_name='state' + ): + assert scene == SceneType.COMMON, scene + from .common_tools.rag import rag_tool + + tool_identifier = cls.get_tool_identifier( + scene=scene, + name=name, + tool_identifier=tool_identifier + ) + + class RagModel(BaseModel): + class Config: + schema_extra = {"description": description} + + tool = StructuredTool.from_function( + func=partial(rag_tool, + retriever_config=retriever_config + ), + name=tool_identifier.name, + args_schema=ToolManager.convert_tool_def_to_pydantic( + tool_id=tool_identifier.tool_id, + tool_def=RagModel + ), + description=description, + return_direct=return_direct, + pass_state=pass_state, + pass_state_name=pass_state_name + ) + + return ToolManager.register_lc_tool( + tool_identifier=tool_identifier, + tool=tool + ) @classmethod @@ -306,7 +359,7 @@ def _load_common_rag_tool(tool_identifier:ToolIdentifier): ToolManager.register_func_as_tool( tool_identifier.scene, tool_identifier.name, - rag.rag, + rag.rag_tool, tool_def, return_direct=True ) diff --git a/source/lambda/online/langchain_integration/tools/common_tools/__init__.py b/source/lambda/online/langchain_integration/tools/common_tools/__init__.py deleted file mode 100644 index c5706989..00000000 --- a/source/lambda/online/langchain_integration/tools/common_tools/__init__.py +++ /dev/null @@ -1,121 +0,0 @@ -from common_logic.common_utils.constant import SceneType, ToolRuningMode -from .._tool_base import tool_manager -from . import ( - get_weather, - give_rhetorical_question, - give_final_response, - chat, - rag -) - - -SCENE = SceneType.COMMON -LAMBDA_NAME = "lambda_common_tools" - -tool_manager.register_tool({ - "name": "get_weather", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": get_weather.lambda_handler, - "tool_def": { - "name": "get_weather", - "description": "Get the current weather for `city_name`", - "parameters": { - "type": "object", - "properties": { - "city_name": { - "description": "The name of the city to be queried", - "type": "string" - }, - }, - "required": ["city_name"] - } - }, - "running_mode": ToolRuningMode.LOOP -}) - - -tool_manager.register_tool( - { - "name": "give_rhetorical_question", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_rhetorical_question.lambda_handler, - "tool_def": { - "name": "give_rhetorical_question", - "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", - "parameters": { - "type": "object", - "properties": { - "question": { - "description": "The rhetorical question to user", - "type": "string" - }, - }, - "required": ["question"], - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool( - { - "name": "give_final_response", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_final_response.lambda_handler, - "tool_def": { - "name": "give_final_response", - "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", - "parameters": { - "type": "object", - "properties": { - "response": { - "description": "Response to user", - "type": "string" - } - }, - "required": ["response"] - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool({ - "name": "chat", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": chat.lambda_handler, - "tool_def": { - "name": "chat", - "description": "casual talk with AI", - "parameters": { - "type": "object", - "properties": { - "response": { - "description": "response to users", - "type": "string" - }}, - "required": ["response"] - }, - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name": "rag_tool", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": rag.lambda_handler, - "tool_def": { - "name": "rag_tool", - "description": "private knowledge", - "parameters": {} - }, - "running_mode": ToolRuningMode.ONCE -}) diff --git a/source/lambda/online/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/langchain_integration/tools/common_tools/rag.py index 2537bb5c..8c0b6d73 100644 --- a/source/lambda/online/langchain_integration/tools/common_tools/rag.py +++ b/source/lambda/online/langchain_integration/tools/common_tools/rag.py @@ -4,16 +4,19 @@ LLMTaskType ) from common_logic.common_utils.lambda_invoke_utils import send_trace +from langchain_integration.langgraph_integration import get_current_app - -def rag(query,state): +def rag_tool(retriever_config:dict,state): + # state = event_body['state'] context_list = [] # add qq match results context_list.extend(state['qq_match_results']) figure_list = [] - retriever_params = state["chatbot_config"]["private_knowledge_config"] - retriever_params["query"] = state[retriever_params.get("retriever_config",{}).get("query_key","query")] + retriever_params = retriever_config + # retriever_params = state["chatbot_config"]["private_knowledge_config"] + retriever_params["query"] = state[retriever_config.get("query_key","query")] + # retriever_params["query"] = query output: str = invoke_lambda( event_body=retriever_params, lambda_name="Online_Functions", From 541c3e7eb130e83b41f10b9de051ae8da17ad288 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 31 Oct 2024 06:58:00 +0000 Subject: [PATCH 08/29] fix: one rag tool should respect to one index; refactor: add registerd rag tools to agent config --- .../online_entries/common_entry_v2.py | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py index e05aedcb..5e4814de 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py @@ -583,19 +583,36 @@ def register_rag_tool_from_config(event_body: dict): chatbot_manager = ChatbotManager.from_environ() chatbot = chatbot_manager.get_chatbot(group_name, chatbot_id) logger.info(chatbot) + registered_tool_names = [] for index_type, item_dict in chatbot.index_ids.items(): if index_type != IndexType.INTENTION: for index_content in item_dict["value"].values(): if "indexId" in index_content and "description" in index_content: + # Find retriever contain index_id + retrievers = event_body["chatbot_config"]["private_knowledge_config"]['retrievers'] + retriever = None + for retriever in retrievers: + if retriever["index_name"] == index_content["indexId"]: + break + assert retriever is not None,retrievers + reranks = event_body["chatbot_config"]["private_knowledge_config"]['reranks'] + index_name = index_content["indexId"] # TODO give specific retriever config ToolManager.register_common_rag_tool( - retriever_config=event_body["chatbot_config"]["private_knowledge_config"], - name=index_content["indexId"], + retriever_config={ + "retrievers":[retriever], + "reranks":[reranks[0]], + "llm_config": event_body["chatbot_config"]["private_knowledge_config"]['llm_config'] + }, + # event_body["chatbot_config"]["private_knowledge_config"], + name=index_name, scene=SceneType.COMMON, description=index_content["description"], pass_state=True, pass_state_name='state' ) + registered_tool_names.append(index_name) + return registered_tool_names def common_entry(event_body): @@ -604,7 +621,7 @@ def common_entry(event_body): :param event_body: The event body for lambda function. return: answer(str) """ - global app, app_agent + global app if app is None: app = build_graph(ChatbotState) @@ -633,10 +650,15 @@ def common_entry(event_body): message_id = event_body["custom_message_id"] ws_connection_id = event_body["ws_connection_id"] enable_trace = chatbot_config["enable_trace"] + agent_config = event_body["chatbot_config"]["agent_config"] # register as rag tool for each aos index - register_rag_tool_from_config(event_body) - + registered_tool_names = register_rag_tool_from_config(event_body) + # update private knowledge tool to agent config + for registered_tool_name in registered_tool_names: + if registered_tool_name not in agent_config['tools']: + agent_config['tools'].append(registered_tool_name) + # define all knowledge rag tool all_knowledge_rag_tool = ToolManager.register_common_rag_tool( retriever_config=event_body["chatbot_config"]["private_knowledge_config"], From ffb1f43cecffe1af079e2deee16c1809377a3884 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Sat, 2 Nov 2024 02:49:49 +0000 Subject: [PATCH 09/29] move langchain_integration into common_logic --- .../langchain_integration/__init__.py | 0 .../langchain_integration/chains/__init__.py | 13 ++ .../chains/__llm_chain_base.py | 26 ++++ .../chains/chat_chain.py | 0 .../chains/conversation_summary_chain.py | 0 .../chains/hyde_chain.py | 0 .../chains/intention_chain.py | 0 .../chains/marketing_chains/__init__.py | 0 .../mkt_conversation_summary.py | 0 .../chains/marketing_chains/mkt_rag_chain.py | 0 .../chains/query_rewrite_chain.py | 0 .../langchain_integration/chains/rag_chain.py | 1 - .../chains/retail_chains/__init__.py | 0 .../retail_chains/auto_evaluation_chain.py | 0 .../retail_conversation_summary_chain.py | 0 .../retail_tool_calling_chain_claude_xml.py | 0 .../retail_tool_calling_chain_json.py | 0 .../chains/stepback_chain.py | 0 .../chains/tool_calling_chain_api.py | 12 +- .../chains/tool_calling_chain_claude_xml.py | 0 .../chains/translate_chain.py | 0 .../chat_models/__init__.py | 0 .../chat_models/bedrock_models.py | 1 + .../chat_models/openai_models.py | 0 .../langgraph_integration.py | 12 ++ .../langchain_integration/tools/__init__.py | 55 ++++---- .../tools/common_tools/__init__retire.py | 121 ++++++++++++++++++ .../tools/common_tools/chat.py | 0 .../tools/common_tools/comparison_rag.py | 0 .../tools/common_tools/get_weather.py | 0 .../tools/common_tools/give_final_response.py | 0 .../common_tools/give_rhetorical_question.py | 0 .../tools/common_tools/rag.py | 2 +- .../tools/common_tools/step_back_rag.py | 0 34 files changed, 214 insertions(+), 29 deletions(-) rename source/lambda/online/{ => common_logic}/langchain_integration/__init__.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/__init__.py (92%) create mode 100644 source/lambda/online/common_logic/langchain_integration/chains/__llm_chain_base.py rename source/lambda/online/{ => common_logic}/langchain_integration/chains/chat_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/conversation_summary_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/hyde_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/intention_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/marketing_chains/__init__.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/marketing_chains/mkt_rag_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/query_rewrite_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/rag_chain.py (99%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/retail_chains/__init__.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/retail_chains/auto_evaluation_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/stepback_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/tool_calling_chain_api.py (93%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/tool_calling_chain_claude_xml.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chains/translate_chain.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chat_models/__init__.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/chat_models/bedrock_models.py (99%) rename source/lambda/online/{ => common_logic}/langchain_integration/chat_models/openai_models.py (100%) create mode 100644 source/lambda/online/common_logic/langchain_integration/langgraph_integration.py rename source/lambda/online/{ => common_logic}/langchain_integration/tools/__init__.py (90%) create mode 100644 source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py rename source/lambda/online/{ => common_logic}/langchain_integration/tools/common_tools/chat.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/tools/common_tools/comparison_rag.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/tools/common_tools/get_weather.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/tools/common_tools/give_final_response.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/tools/common_tools/give_rhetorical_question.py (100%) rename source/lambda/online/{ => common_logic}/langchain_integration/tools/common_tools/rag.py (96%) rename source/lambda/online/{ => common_logic}/langchain_integration/tools/common_tools/step_back_rag.py (100%) diff --git a/source/lambda/online/langchain_integration/__init__.py b/source/lambda/online/common_logic/langchain_integration/__init__.py similarity index 100% rename from source/lambda/online/langchain_integration/__init__.py rename to source/lambda/online/common_logic/langchain_integration/__init__.py diff --git a/source/lambda/online/langchain_integration/chains/__init__.py b/source/lambda/online/common_logic/langchain_integration/chains/__init__.py similarity index 92% rename from source/lambda/online/langchain_integration/chains/__init__.py rename to source/lambda/online/common_logic/langchain_integration/chains/__init__.py index 0453a3ef..9ba61fa1 100644 --- a/source/lambda/online/langchain_integration/chains/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/__init__.py @@ -161,6 +161,18 @@ def _import_retail_tool_calling_chain_claude_xml(): Claude3HaikuRetailToolCallingChain ) +def _import_tool_calling_chain_api(): + from .tool_calling_chain_api import ( + Claude21ToolCallingChain, + Claude2ToolCallingChain, + Claude35SonnetToolCallingChain, + Claude3HaikuToolCallingChain, + Claude3SonnetToolCallingChain, + Llama31Instruct70BToolCallingChain, + CohereCommandRPlusToolCallingChain, + MistraLlarge2407ToolCallingChain + ) + def _import_auto_evaluation_chain(): from .retail_chains.auto_evaluation_chain import ( @@ -188,6 +200,7 @@ def _load_module(intent_type): LLMTaskType.HYDE_TYPE: _import_hyde_chain, LLMTaskType.QUERY_REWRITE_TYPE: _import_query_rewrite_chain, LLMTaskType.TOOL_CALLING_XML: _import_tool_calling_chain_claude_xml, + LLMTaskType.TOOL_CALLING_API:_import_tool_calling_chain_api, LLMTaskType.RETAIL_CONVERSATION_SUMMARY_TYPE: _import_retail_conversation_summary_chain, LLMTaskType.RETAIL_TOOL_CALLING: _import_retail_tool_calling_chain_claude_xml, LLMTaskType.AUTO_EVALUATION: _import_auto_evaluation_chain diff --git a/source/lambda/online/common_logic/langchain_integration/chains/__llm_chain_base.py b/source/lambda/online/common_logic/langchain_integration/chains/__llm_chain_base.py new file mode 100644 index 00000000..98ae93d3 --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/chains/__llm_chain_base.py @@ -0,0 +1,26 @@ +class LLMChainMeta(type): + def __new__(cls, name, bases, attrs): + new_cls = type.__new__(cls, name, bases, attrs) + if name == "LLMChain": + return new_cls + new_cls.model_map[new_cls.get_chain_id()] = new_cls + return new_cls + + +class LLMChain(metaclass=LLMChainMeta): + model_map = {} + + @classmethod + def get_chain_id(cls): + return cls._get_chain_id(cls.model_id, cls.intent_type) + + @staticmethod + def _get_chain_id(model_id, intent_type): + return f"{model_id}__{intent_type}" + + @classmethod + def get_chain(cls, model_id, intent_type, model_kwargs=None, **kwargs): + return cls.model_map[cls._get_chain_id(model_id, intent_type)].create_chain( + model_kwargs=model_kwargs, **kwargs + ) + diff --git a/source/lambda/online/langchain_integration/chains/chat_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/chat_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py diff --git a/source/lambda/online/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/conversation_summary_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py diff --git a/source/lambda/online/langchain_integration/chains/hyde_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/hyde_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/hyde_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/hyde_chain.py diff --git a/source/lambda/online/langchain_integration/chains/intention_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/intention_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/intention_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/intention_chain.py diff --git a/source/lambda/online/langchain_integration/chains/marketing_chains/__init__.py b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/__init__.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/marketing_chains/__init__.py rename to source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/__init__.py diff --git a/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py rename to source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py diff --git a/source/lambda/online/langchain_integration/chains/marketing_chains/mkt_rag_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_rag_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/marketing_chains/mkt_rag_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_rag_chain.py diff --git a/source/lambda/online/langchain_integration/chains/query_rewrite_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/query_rewrite_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py diff --git a/source/lambda/online/langchain_integration/chains/rag_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py similarity index 99% rename from source/lambda/online/langchain_integration/chains/rag_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py index be9d42ef..60c6b33b 100644 --- a/source/lambda/online/langchain_integration/chains/rag_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py @@ -101,7 +101,6 @@ class Mixtral8x7bChatChain(Claude2RagLLMChain): model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - from .chat_chain import GLM4Chat9BChatChain class GLM4Chat9BRagChain(GLM4Chat9BChatChain): diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/__init__.py b/source/lambda/online/common_logic/langchain_integration/chains/retail_chains/__init__.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/retail_chains/__init__.py rename to source/lambda/online/common_logic/langchain_integration/chains/retail_chains/__init__.py diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/retail_chains/auto_evaluation_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/retail_chains/auto_evaluation_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/retail_chains/auto_evaluation_chain.py diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/retail_chains/retail_conversation_summary_chain.py diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py b/source/lambda/online/common_logic/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py rename to source/lambda/online/common_logic/langchain_integration/chains/retail_chains/retail_tool_calling_chain_claude_xml.py diff --git a/source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py b/source/lambda/online/common_logic/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py rename to source/lambda/online/common_logic/langchain_integration/chains/retail_chains/retail_tool_calling_chain_json.py diff --git a/source/lambda/online/langchain_integration/chains/stepback_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/stepback_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/stepback_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/stepback_chain.py diff --git a/source/lambda/online/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py similarity index 93% rename from source/lambda/online/langchain_integration/chains/tool_calling_chain_api.py rename to source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py index be918e36..a5674de5 100644 --- a/source/lambda/online/langchain_integration/chains/tool_calling_chain_api.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py @@ -125,8 +125,18 @@ def create_chain(cls, model_kwargs=None, **kwargs): model_id=cls.model_id, model_kwargs=model_kwargs, ) + llm = cls.bind_tools(llm,tools,fewshot_examples,fewshot_template=tool_fewshot_prompt) - chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | llm + + tool_calling_template = ChatPromptTemplate.from_messages( + [ + SystemMessage(content=agent_system_prompt), + ("placeholder", "{chat_history}"), + ("human", "{query}") + ] + ) + # chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | llm + chain = tool_calling_template | llm return chain diff --git a/source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_claude_xml.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/tool_calling_chain_claude_xml.py rename to source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_claude_xml.py diff --git a/source/lambda/online/langchain_integration/chains/translate_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/translate_chain.py similarity index 100% rename from source/lambda/online/langchain_integration/chains/translate_chain.py rename to source/lambda/online/common_logic/langchain_integration/chains/translate_chain.py diff --git a/source/lambda/online/langchain_integration/chat_models/__init__.py b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py similarity index 100% rename from source/lambda/online/langchain_integration/chat_models/__init__.py rename to source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py diff --git a/source/lambda/online/langchain_integration/chat_models/bedrock_models.py b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py similarity index 99% rename from source/lambda/online/langchain_integration/chat_models/bedrock_models.py rename to source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py index 162ab998..835313dd 100644 --- a/source/lambda/online/langchain_integration/chat_models/bedrock_models.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py @@ -66,6 +66,7 @@ class MistralLarge2407(Claude2): class Llama3d1Instruct70B(Claude2): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT + class CohereCommandRPlus(Claude2): model_id = LLMModelType.COHERE_COMMAND_R_PLUS diff --git a/source/lambda/online/langchain_integration/chat_models/openai_models.py b/source/lambda/online/common_logic/langchain_integration/chat_models/openai_models.py similarity index 100% rename from source/lambda/online/langchain_integration/chat_models/openai_models.py rename to source/lambda/online/common_logic/langchain_integration/chat_models/openai_models.py diff --git a/source/lambda/online/common_logic/langchain_integration/langgraph_integration.py b/source/lambda/online/common_logic/langchain_integration/langgraph_integration.py new file mode 100644 index 00000000..61b264e0 --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/langgraph_integration.py @@ -0,0 +1,12 @@ + +# set global langgraph app + +current_app = None + +def set_currrent_app(app): + global current_app + current_app = app + +def get_current_app(): + assert current_app is not None + return current_app \ No newline at end of file diff --git a/source/lambda/online/langchain_integration/tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py similarity index 90% rename from source/lambda/online/langchain_integration/tools/__init__.py rename to source/lambda/online/common_logic/langchain_integration/tools/__init__.py index 0e81f84b..5d1afe16 100644 --- a/source/lambda/online/langchain_integration/tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py @@ -85,9 +85,12 @@ def convert_tool_def_to_pydantic(tool_id,tool_def:Union[dict,BaseModel]): use_schema_description=True ) result = parser.parse() + result = result.replace("from __future__ import annotations","") new_tool_module = types.ModuleType(tool_id) exec(result, new_tool_module.__dict__) - return new_tool_module.Model + model_cls = new_tool_module.Model + # model_cls.model_rebuild() + return model_cls @staticmethod @@ -108,7 +111,7 @@ def register_lc_tool( tool_identifier = cls.get_tool_identifier( scene=scene, name=name, - tool_identifier=None + tool_identifier=tool_identifier ) assert isinstance(tool,BaseTool),(tool,type(tool)) cls.tool_map[tool_identifier.tool_id] = tool @@ -246,14 +249,13 @@ def wrapper(*args, **kwargs): if "tool_identifier" in inspect.signature(func).parameters: kwargs = {**kwargs,"tool_identifier":tool_identifier} return func(*args, **kwargs) - TOOL_MOFULE_LOAD_FN_MAP[tool_identifier.tool_id] = func + TOOL_MOFULE_LOAD_FN_MAP[tool_identifier.tool_id] = wrapper return wrapper return decorator ############################# tool load func ###################### - @lazy_tool_load_decorator(SceneType.COMMON,"get_weather") def _load_common_weather_tool(tool_identifier:ToolIdentifier): from .common_tools import get_weather @@ -268,10 +270,10 @@ def _load_common_weather_tool(tool_identifier:ToolIdentifier): "required": ["city_name"] } ToolManager.register_func_as_tool( - tool_identifier.scene, - tool_identifier.name, - get_weather.get_weather, - tool_def, + func=get_weather.get_weather, + tool_def=tool_def, + scene=tool_identifier.scene, + name=tool_identifier.name, return_direct=False ) @@ -286,13 +288,14 @@ def _load_common_rhetorical_tool(tool_identifier:ToolIdentifier): "description": "The rhetorical question to user", "type": "string" }, - } + }, + "required": [] #["question"] } ToolManager.register_func_as_tool( - tool_identifier.scene, - tool_identifier.name, - give_rhetorical_question.give_rhetorical_question, - tool_def, + scene=tool_identifier.scene, + name=tool_identifier.name, + func=give_rhetorical_question.give_rhetorical_question, + tool_def=tool_def, return_direct=True ) @@ -312,10 +315,10 @@ def _load_common_final_response_tool(tool_identifier:ToolIdentifier): "required": ["response"] } ToolManager.register_func_as_tool( - tool_identifier.scene, - tool_identifier.name, - give_final_response.give_final_response, - tool_def, + scene=tool_identifier.scene, + name=tool_identifier.name, + func=give_final_response.give_final_response, + tool_def=tool_def, return_direct=True ) @@ -335,10 +338,10 @@ def _load_common_chat_tool(tool_identifier:ToolIdentifier): } ToolManager.register_func_as_tool( - tool_identifier.scene, - tool_identifier.name, - chat.chat, - tool_def, + scene=tool_identifier.scene, + name=tool_identifier.name, + func=chat.chat, + tool_def=tool_def, return_direct=True ) @@ -354,13 +357,13 @@ def _load_common_rag_tool(tool_identifier:ToolIdentifier): "type": "string" } }, - "required": ["query"] + # "required": ["query"] } ToolManager.register_func_as_tool( - tool_identifier.scene, - tool_identifier.name, - rag.rag_tool, - tool_def, + scene=tool_identifier.scene, + name=tool_identifier.name, + func=rag.rag_tool, + tool_def=tool_def, return_direct=True ) diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py new file mode 100644 index 00000000..c5706989 --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py @@ -0,0 +1,121 @@ +from common_logic.common_utils.constant import SceneType, ToolRuningMode +from .._tool_base import tool_manager +from . import ( + get_weather, + give_rhetorical_question, + give_final_response, + chat, + rag +) + + +SCENE = SceneType.COMMON +LAMBDA_NAME = "lambda_common_tools" + +tool_manager.register_tool({ + "name": "get_weather", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": get_weather.lambda_handler, + "tool_def": { + "name": "get_weather", + "description": "Get the current weather for `city_name`", + "parameters": { + "type": "object", + "properties": { + "city_name": { + "description": "The name of the city to be queried", + "type": "string" + }, + }, + "required": ["city_name"] + } + }, + "running_mode": ToolRuningMode.LOOP +}) + + +tool_manager.register_tool( + { + "name": "give_rhetorical_question", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": give_rhetorical_question.lambda_handler, + "tool_def": { + "name": "give_rhetorical_question", + "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", + "parameters": { + "type": "object", + "properties": { + "question": { + "description": "The rhetorical question to user", + "type": "string" + }, + }, + "required": ["question"], + }, + }, + "running_mode": ToolRuningMode.ONCE + } +) + + +tool_manager.register_tool( + { + "name": "give_final_response", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": give_final_response.lambda_handler, + "tool_def": { + "name": "give_final_response", + "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", + "parameters": { + "type": "object", + "properties": { + "response": { + "description": "Response to user", + "type": "string" + } + }, + "required": ["response"] + }, + }, + "running_mode": ToolRuningMode.ONCE + } +) + + +tool_manager.register_tool({ + "name": "chat", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": chat.lambda_handler, + "tool_def": { + "name": "chat", + "description": "casual talk with AI", + "parameters": { + "type": "object", + "properties": { + "response": { + "description": "response to users", + "type": "string" + }}, + "required": ["response"] + }, + }, + "running_mode": ToolRuningMode.ONCE +}) + + +tool_manager.register_tool({ + "name": "rag_tool", + "scene": SCENE, + "lambda_name": LAMBDA_NAME, + "lambda_module_path": rag.lambda_handler, + "tool_def": { + "name": "rag_tool", + "description": "private knowledge", + "parameters": {} + }, + "running_mode": ToolRuningMode.ONCE +}) diff --git a/source/lambda/online/langchain_integration/tools/common_tools/chat.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/chat.py similarity index 100% rename from source/lambda/online/langchain_integration/tools/common_tools/chat.py rename to source/lambda/online/common_logic/langchain_integration/tools/common_tools/chat.py diff --git a/source/lambda/online/langchain_integration/tools/common_tools/comparison_rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/comparison_rag.py similarity index 100% rename from source/lambda/online/langchain_integration/tools/common_tools/comparison_rag.py rename to source/lambda/online/common_logic/langchain_integration/tools/common_tools/comparison_rag.py diff --git a/source/lambda/online/langchain_integration/tools/common_tools/get_weather.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/get_weather.py similarity index 100% rename from source/lambda/online/langchain_integration/tools/common_tools/get_weather.py rename to source/lambda/online/common_logic/langchain_integration/tools/common_tools/get_weather.py diff --git a/source/lambda/online/langchain_integration/tools/common_tools/give_final_response.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/give_final_response.py similarity index 100% rename from source/lambda/online/langchain_integration/tools/common_tools/give_final_response.py rename to source/lambda/online/common_logic/langchain_integration/tools/common_tools/give_final_response.py diff --git a/source/lambda/online/langchain_integration/tools/common_tools/give_rhetorical_question.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/give_rhetorical_question.py similarity index 100% rename from source/lambda/online/langchain_integration/tools/common_tools/give_rhetorical_question.py rename to source/lambda/online/common_logic/langchain_integration/tools/common_tools/give_rhetorical_question.py diff --git a/source/lambda/online/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py similarity index 96% rename from source/lambda/online/langchain_integration/tools/common_tools/rag.py rename to source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py index 8c0b6d73..e6a878fb 100644 --- a/source/lambda/online/langchain_integration/tools/common_tools/rag.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py @@ -4,7 +4,7 @@ LLMTaskType ) from common_logic.common_utils.lambda_invoke_utils import send_trace -from langchain_integration.langgraph_integration import get_current_app +from common_logic.langchain_integration.langgraph_integration import get_current_app def rag_tool(retriever_config:dict,state): diff --git a/source/lambda/online/langchain_integration/tools/common_tools/step_back_rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/step_back_rag.py similarity index 100% rename from source/lambda/online/langchain_integration/tools/common_tools/step_back_rag.py rename to source/lambda/online/common_logic/langchain_integration/tools/common_tools/step_back_rag.py From 4401f386d0985ec77ed6b7275d9943a2f29e7a5a Mon Sep 17 00:00:00 2001 From: zhouxss Date: Sat, 2 Nov 2024 08:12:41 +0000 Subject: [PATCH 10/29] modify agent prompt; add python_repl tool; adapt to pydantic v2 --- source/lambda/job/dep/llm_bot_dep/sm_utils.py | 15 +- .../common_utils/lambda_invoke_utils.py | 49 +- .../common_logic/common_utils/logger_utils.py | 13 +- .../common_logic/common_utils/prompt_utils.py | 27 +- .../common_utils/pydantic_models.py | 1 + .../chains/tool_calling_chain_api.py | 13 +- .../chat_models/__init__.py | 3 +- .../chat_models/bedrock_models.py | 16 +- .../langchain_integration/tools/__init__.py | 161 +--- .../tools/common_tools/__init__.py | 135 ++++ .../tools/common_tools/__init__retire.py | 121 --- .../tools/common_tools/rag.py | 10 +- source/lambda/online/functions/__init__.py | 14 +- .../functions_utils/retriever/retriever.py | 2 +- .../retriever/utils/websearch_retrievers.py | 2 +- .../main_utils/online_entries/__init__.py | 6 +- .../main_utils/online_entries/common_entry.py | 414 ++++++++--- .../online_entries/common_entry_v2.py | 700 ------------------ .../lambda_main/test/local_test_base.py | 2 +- .../test/main_local_test_common.py | 38 +- 20 files changed, 603 insertions(+), 1139 deletions(-) create mode 100644 source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py delete mode 100644 source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py delete mode 100644 source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py diff --git a/source/lambda/job/dep/llm_bot_dep/sm_utils.py b/source/lambda/job/dep/llm_bot_dep/sm_utils.py index f9a06326..7520ec9c 100644 --- a/source/lambda/job/dep/llm_bot_dep/sm_utils.py +++ b/source/lambda/job/dep/llm_bot_dep/sm_utils.py @@ -1,11 +1,12 @@ import json import io from typing import Any, Dict, Iterator, List, Mapping, Optional -from langchain.llms.sagemaker_endpoint import LLMContentHandler, SagemakerEndpoint -from langchain.embeddings import SagemakerEndpointEmbeddings, BedrockEmbeddings -from langchain.embeddings.sagemaker_endpoint import EmbeddingsContentHandler +from langchain_community.llms import SagemakerEndpoint +from langchain_community.llms.sagemaker_endpoint import LLMContentHandler +from langchain_community.embeddings import SagemakerEndpointEmbeddings,BedrockEmbeddings +from langchain_community.embeddings.sagemaker_endpoint import EmbeddingsContentHandler from langchain.callbacks.manager import CallbackManagerForLLMRun -from langchain.llms.utils import enforce_stop_tokens +from langchain_community.llms.utils import enforce_stop_tokens from typing import Dict, List, Optional, Any,Iterator from langchain_core.outputs import GenerationChunk import boto3 @@ -234,12 +235,12 @@ def transform_output(self, output: bytes) -> str: function. See `boto3`_. docs for more info. .. _boto3: """ - content_type = "application/json" - accepts = "application/json" + content_type: str = "application/json" + accepts: str = "application/json" class Config: """Configuration for this pydantic object.""" - extra = Extra.forbid + extra = Extra.forbid.value @root_validator() def validate_environment(cls, values: Dict) -> Dict: diff --git a/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py b/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py index f87ef838..e5197c89 100644 --- a/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py +++ b/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py @@ -3,18 +3,21 @@ import importlib import json import time +import os from typing import Any, Dict, Optional, Callable, Union +import threading import requests from common_logic.common_utils.constant import StreamMessageType from common_logic.common_utils.logger_utils import get_logger from common_logic.common_utils.websocket_utils import is_websocket_request, send_to_ws_client -from langchain.pydantic_v1 import BaseModel, Field, root_validator +from pydantic import BaseModel, Field, model_validator + from .exceptions import LambdaInvokeError logger = get_logger("lambda_invoke_utils") - +thread_local = threading.local() __FUNC_NAME_MAP = { "query_preprocess": "Preprocess for multi-round conversation", @@ -25,6 +28,33 @@ "tool_execution": "Final tool result" } + +class StateContext: + + def __init__(self,state): + self.state=state + + @classmethod + def get_current_state(cls): + state = getattr(thread_local,'state',None) + assert state is not None,"There is not a valid state in current context" + return state + + @classmethod + def set_current_state(cls, state): + setattr(thread_local, 'state', state) + + @classmethod + def clear_state(cls): + setattr(thread_local, 'state', None) + + def __enter__(self): + self.set_current_state(self.state) + + def __exit__(self, exc_type, exc_val, exc_tb): + self.clear_state() + + class LAMBDA_INVOKE_MODE(enum.Enum): LAMBDA = "lambda" LOCAL = "local" @@ -54,26 +84,24 @@ class LambdaInvoker(BaseModel): region_name: str = None credentials_profile_name: Optional[str] = Field(default=None, exclude=True) - @root_validator() + @model_validator(mode="before") def validate_environment(cls, values: Dict): if values.get("client") is not None: return values try: import boto3 - try: - if values["credentials_profile_name"] is not None: + if values.get("credentials_profile_name") is not None: session = boto3.Session( profile_name=values["credentials_profile_name"] ) else: # use default credentials session = boto3.Session() - values["client"] = session.client( - "lambda", region_name=values["region_name"] + "lambda", + region_name=values.get("region_name",os.environ['AWS_REGION']) ) - except Exception as e: raise ValueError( "Could not load credentials to authenticate with AWS client. " @@ -284,7 +312,10 @@ def wrapper(state: Dict[str, Any]) -> Dict[str, Any]: current_stream_use, ws_connection_id, enable_trace) state['trace_infos'].append( f"Enter: {func.__name__}, time: {time.time()}") - output = func(state) + + with StateContext(state): + output = func(state) + current_monitor_infos = output.get(monitor_key, None) if current_monitor_infos is not None: send_trace(f"\n\n {current_monitor_infos}", diff --git a/source/lambda/online/common_logic/common_utils/logger_utils.py b/source/lambda/online/common_logic/common_utils/logger_utils.py index 22ba7032..5216c8ef 100644 --- a/source/lambda/online/common_logic/common_utils/logger_utils.py +++ b/source/lambda/online/common_logic/common_utils/logger_utils.py @@ -1,4 +1,3 @@ - import logging import threading import os @@ -72,3 +71,15 @@ def print_llm_messages(msg, logger=logger): "ENABLE_PRINT_MESSAGES", 'True').lower() in ('true', '1', 't') if enable_print_messages: logger.info(msg) + + +def llm_messages_print_decorator(fn): + @wraps(fn) + def _inner(*args, **kwargs): + if args: + print_llm_messages(args) + if kwargs: + print_llm_messages(kwargs) + return fn(*args, **kwargs) + return _inner + diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index 644411a6..0ff72f40 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -365,13 +365,13 @@ def prompt_template_render(self, prompt_template: dict): You are a helpful AI assistant. Today is {date},{weekday}. Here are some guidelines for you: -- Always start each answer with a reflection and write the reflection process in the tag. Please follow the steps below to think about it: +- Here are some tips for tool use: 1. Determine whether the current context is sufficient to answer the user's question. 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. 3. If the current context is not sufficient to answer the user's question, you can consider calling the provided tools. 4. If the parameters of the tool you call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. If the tool does not require parameters, do not call the `give_rhetorical_question` tool. 5. Finally, output the name of the tool you want to call. -- Always output with the same language as the content within . If the content is english, use english to output. If the content is chinese, use chinese to output. +- Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. """ register_prompt_templates( @@ -388,6 +388,29 @@ def prompt_template_render(self, prompt_template: dict): prompt_name="agent_system_prompt" ) + +# AGENT_SYSTEM_PROMPT_LLAMA = """\ +# You are a helpful AI assistant. Today is {date},{weekday}. +# Here are some guidelines for you: +# +# - Always referece each answer with a reflection and write the reflection process in the tag. Please follow the steps below to think about it: +# 1. Determine whether the current context is sufficient to answer the user's question. +# 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. +# 3. If the current context is not sufficient to answer the user's question, you can consider calling the provided tools. +# 4. If the parameters of the tool you call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. If the tool does not require parameters, do not call the `give_rhetorical_question` tool. +# 5. Finally, output the name of the tool you want to call. +# - Always output with the same language as the content within . If the content is english, use english to output. If the content is chinese, use chinese to output. +# """ + +# register_prompt_templates( +# model_ids=[ +# LLMModelType.LLAMA3_1_70B_INSTRUCT, +# ], +# task_type=LLMTaskType.TOOL_CALLING_API, +# prompt_template=AGENT_SYSTEM_PROMPT, +# prompt_name="agent_system_prompt" +# ) + # AGENT_GUIDELINES_PROMPT = """ # - 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考:。 # 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 diff --git a/source/lambda/online/common_logic/common_utils/pydantic_models.py b/source/lambda/online/common_logic/common_utils/pydantic_models.py index 2ee51e36..1296b372 100644 --- a/source/lambda/online/common_logic/common_utils/pydantic_models.py +++ b/source/lambda/online/common_logic/common_utils/pydantic_models.py @@ -28,6 +28,7 @@ class ForbidBaseModel(BaseModel): class AllowBaseModel(BaseModel): class Config: extra = "allow" + use_enum_values = True class LLMConfig(AllowBaseModel): diff --git a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py index a5674de5..35c7c0fa 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py @@ -60,7 +60,9 @@ def get_common_system_prompt(cls,system_prompt_template:str): def bind_tools(cls,llm:BaseChatModel,tools:List[BaseTool], fewshot_examples=None, fewshot_template=None,tool_choice='any'): tools = [tool.model_copy() for tool in tools] if not fewshot_examples: - return llm.bind_tools(tools,tool_choice=tool_choice) + if getattr(llm,"enable_auto_tool_choice",True): + return llm.bind_tools(tools,tool_choice=tool_choice) + return llm.bind_tools(tools) # add fewshot examples to tool description tools_map = {tool.name:tool for tool in tools} @@ -84,7 +86,10 @@ def bind_tools(cls,llm:BaseChatModel,tools:List[BaseTool], fewshot_examples=None ) tool.description += "\n\n".join(examples_strs) - return llm.bind_tools(tools,tool_choice=tool_choice) + + if getattr(llm,"enable_auto_tool_choice",True): + return llm.bind_tools(tools,tool_choice=tool_choice) + return llm.bind_tools(tools) @classmethod @@ -132,7 +137,9 @@ def create_chain(cls, model_kwargs=None, **kwargs): [ SystemMessage(content=agent_system_prompt), ("placeholder", "{chat_history}"), - ("human", "{query}") + ("human", "{query}"), + ("placeholder", "{agent_tool_history}"), + ] ) # chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | llm diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py index 8b9092ec..56ee8dd5 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py @@ -36,7 +36,8 @@ def __new__(cls, name, bases, attrs): class Model(ModeMixins,metaclass=ModelMeta): - model_id = None + model_id: str = None + enable_auto_tool_choice: bool = True model_map = {} @classmethod diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py index 835313dd..4c82373f 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py @@ -1,19 +1,24 @@ import os -from langchain_aws.chat_models import ChatBedrockConverse +from langchain_aws.chat_models import ChatBedrockConverse as _ChatBedrockConverse from common_logic.common_utils.constant import ( MessageType, LLMModelType ) -from common_logic.common_utils.logger_utils import get_logger +from common_logic.common_utils.logger_utils import get_logger,llm_messages_print_decorator from . import Model - logger = get_logger("bedrock_model") + +class ChatBedrockConverse(_ChatBedrockConverse): + enable_auto_tool_choice: bool = True + + # Bedrock model type class Claude2(Model): model_id = LLMModelType.CLAUDE_2 default_model_kwargs = {"max_tokens": 2000, "temperature": 0.7, "top_p": 0.9} + enable_auto_tool_choice = True @classmethod def create_model(cls, model_kwargs=None, **kwargs): @@ -34,8 +39,11 @@ def create_model(cls, model_kwargs=None, **kwargs): credentials_profile_name=credentials_profile_name, region_name=region_name, model=cls.model_id, + enable_auto_tool_choice=cls.enable_auto_tool_choice, **model_kwargs, ) + llm.client.converse_stream = llm_messages_print_decorator(llm.client.converse_stream) + llm.client.converse = llm_messages_print_decorator(llm.client.converse) return llm @@ -65,10 +73,12 @@ class MistralLarge2407(Claude2): class Llama3d1Instruct70B(Claude2): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT + enable_auto_tool_choice = False class CohereCommandRPlus(Claude2): model_id = LLMModelType.COHERE_COMMAND_R_PLUS + enable_auto_tool_choice = False diff --git a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py index 5d1afe16..f25fd76e 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py @@ -1,33 +1,3 @@ -# from langchain.tools.base import StructuredTool,BaseTool,tool -# StructuredTool.from_function -# from langchain_experimental.tools import PythonREPLTool -# from langchain_core.utils.function_calling import convert_to_openai_function -# from llama_index.core.tools import FunctionTool -# from langchain.tools import BaseTool -# from pydantic import create_model - -# from langchain_community.tools import WikipediaQueryRun - - -# builder = StateGraph(State) - - -# # Define nodes: these do the work -# builder.add_node("assistant", Assistant(part_1_assistant_runnable)) -# builder.add_node("tools", create_tool_node_with_fallback(part_1_tools)) -# # Define edges: these determine how the control flow moves -# builder.add_edge(START, "assistant") -# builder.add_conditional_edges( -# "assistant", -# tools_condition, -# ) -# builder.add_edge("tools", "assistant") - -# # The checkpointer lets the graph persist its state -# # this is a complete memory for the entire graph. -# memory = MemorySaver() -# part_1_graph = builder.compile(checkpointer=memory) - from typing import Optional,Union from pydantic import BaseModel import platform @@ -40,7 +10,7 @@ from datamodel_code_generator.model import get_data_model_types from datamodel_code_generator.parser.jsonschema import JsonSchemaParser from langchain.tools.base import StructuredTool as _StructuredTool ,BaseTool -from langchain_core.pydantic_v1 import create_model,BaseModel +# from langchain_core.pydantic_v1 import BaseModel from common_logic.common_utils.constant import SceneType from common_logic.common_utils.lambda_invoke_utils import invoke_with_lambda from functools import partial @@ -48,10 +18,9 @@ class StructuredTool(_StructuredTool): - pass_state:bool = False # if pass state into tool invoke - pass_state_name:str = "state" # pass state name - - + pass + # pass_state:bool = False # if pass state into tool invoke + # pass_state_name:str = "state" # pass state name class ToolIdentifier(BaseModel): scene: SceneType @@ -89,7 +58,6 @@ def convert_tool_def_to_pydantic(tool_id,tool_def:Union[dict,BaseModel]): new_tool_module = types.ModuleType(tool_id) exec(result, new_tool_module.__dict__) model_cls = new_tool_module.Model - # model_cls.model_rebuild() return model_cls @@ -191,8 +159,8 @@ def register_common_rag_tool( name=None, tool_identifier=None, return_direct=False, - pass_state=True, - pass_state_name='state' + # pass_state=True, + # pass_state_name='state' ): assert scene == SceneType.COMMON, scene from .common_tools.rag import rag_tool @@ -218,8 +186,8 @@ class Config: ), description=description, return_direct=return_direct, - pass_state=pass_state, - pass_state_name=pass_state_name + # pass_state=pass_state, + # pass_state_name=pass_state_name ) return ToolManager.register_lc_tool( @@ -254,118 +222,7 @@ def wrapper(*args, **kwargs): return decorator -############################# tool load func ###################### - -@lazy_tool_load_decorator(SceneType.COMMON,"get_weather") -def _load_common_weather_tool(tool_identifier:ToolIdentifier): - from .common_tools import get_weather - tool_def = { - "description": "Get the current weather for `city_name`", - "properties": { - "city_name": { - "description": "The name of the city to be queried", - "type": "string" - }, - }, - "required": ["city_name"] - } - ToolManager.register_func_as_tool( - func=get_weather.get_weather, - tool_def=tool_def, - scene=tool_identifier.scene, - name=tool_identifier.name, - return_direct=False - ) - - -@lazy_tool_load_decorator(SceneType.COMMON,"give_rhetorical_question") -def _load_common_rhetorical_tool(tool_identifier:ToolIdentifier): - from .common_tools import give_rhetorical_question - tool_def = { - "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", - "properties": { - "question": { - "description": "The rhetorical question to user", - "type": "string" - }, - }, - "required": [] #["question"] - } - ToolManager.register_func_as_tool( - scene=tool_identifier.scene, - name=tool_identifier.name, - func=give_rhetorical_question.give_rhetorical_question, - tool_def=tool_def, - return_direct=True - ) - - -@lazy_tool_load_decorator(SceneType.COMMON,"give_final_response") -def _load_common_final_response_tool(tool_identifier:ToolIdentifier): - from .common_tools import give_final_response - - tool_def = { - "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", - "properties": { - "response": { - "description": "Response to user", - "type": "string" - } - }, - "required": ["response"] - } - ToolManager.register_func_as_tool( - scene=tool_identifier.scene, - name=tool_identifier.name, - func=give_final_response.give_final_response, - tool_def=tool_def, - return_direct=True - ) - - -@lazy_tool_load_decorator(SceneType.COMMON,"chat") -def _load_common_chat_tool(tool_identifier:ToolIdentifier): - from .common_tools import chat - tool_def = { - "description": "casual talk with AI", - "properties": { - "response": { - "description": "response to users", - "type": "string" - } - }, - "required": ["response"] - } - - ToolManager.register_func_as_tool( - scene=tool_identifier.scene, - name=tool_identifier.name, - func=chat.chat, - tool_def=tool_def, - return_direct=True - ) - - -@lazy_tool_load_decorator(SceneType.COMMON,"rag_tool") -def _load_common_rag_tool(tool_identifier:ToolIdentifier): - from .common_tools import rag - tool_def = { - "description": "private knowledge", - "properties": { - "query": { - "description": "query for retrieve", - "type": "string" - } - }, - # "required": ["query"] - } - ToolManager.register_func_as_tool( - scene=tool_identifier.scene, - name=tool_identifier.name, - func=rag.rag_tool, - tool_def=tool_def, - return_direct=True - ) +from . import common_tools diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py new file mode 100644 index 00000000..170daa44 --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py @@ -0,0 +1,135 @@ +from .. import lazy_tool_load_decorator,ToolIdentifier,ToolManager +from common_logic.common_utils.constant import SceneType + + +@lazy_tool_load_decorator(SceneType.COMMON,"get_weather") +def _load_weather_tool(tool_identifier:ToolIdentifier): + from . import get_weather + tool_def = { + "description": "Get the current weather for `city_name`", + "properties": { + "city_name": { + "description": "The name of the city to be queried", + "type": "string" + }, + }, + "required": ["city_name"] + } + ToolManager.register_func_as_tool( + func=get_weather.get_weather, + tool_def=tool_def, + scene=tool_identifier.scene, + name=tool_identifier.name, + return_direct=False + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"give_rhetorical_question") +def _load_rhetorical_tool(tool_identifier:ToolIdentifier): + from . import give_rhetorical_question + tool_def = { + "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", + "properties": { + "question": { + "description": "The rhetorical question to user", + "type": "string" + }, + }, + "required": ["question"] + } + ToolManager.register_func_as_tool( + scene=tool_identifier.scene, + name=tool_identifier.name, + func=give_rhetorical_question.give_rhetorical_question, + tool_def=tool_def, + return_direct=True + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"give_final_response") +def _load_final_response_tool(tool_identifier:ToolIdentifier): + from . import give_final_response + + tool_def = { + "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", + "properties": { + "response": { + "description": "Response to user", + "type": "string" + } + }, + "required": ["response"] + } + ToolManager.register_func_as_tool( + scene=tool_identifier.scene, + name=tool_identifier.name, + func=give_final_response.give_final_response, + tool_def=tool_def, + return_direct=True + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"chat") +def _load_chat_tool(tool_identifier:ToolIdentifier): + from . import chat + tool_def = { + "description": "casual talk with AI", + "properties": { + "response": { + "description": "response to users", + "type": "string" + } + }, + "required": ["response"] + } + + ToolManager.register_func_as_tool( + scene=tool_identifier.scene, + name=tool_identifier.name, + func=chat.chat, + tool_def=tool_def, + return_direct=True + ) + + +@lazy_tool_load_decorator(SceneType.COMMON,"rag_tool") +def _load_rag_tool(tool_identifier:ToolIdentifier): + from . import rag + tool_def = { + "description": "private knowledge", + "properties": { + "query": { + "description": "query for retrieve", + "type": "string" + } + }, + # "required": ["query"] + } + ToolManager.register_func_as_tool( + scene=tool_identifier.scene, + name=tool_identifier.name, + func=rag.rag_tool, + tool_def=tool_def, + return_direct=True + ) + + + +################### langchain tools ####################### + +@lazy_tool_load_decorator(SceneType.COMMON,"python_repl") +def _loadd_python_repl_tool(tool_identifier:ToolIdentifier): + from langchain_core.tools import Tool + from langchain_experimental.utilities import PythonREPL + python_repl = PythonREPL() + repl_tool = Tool( + name="python_repl", + description="A Python shell. Use this to execute python commands. Input should be a valid python command. If you want to see the output of a value, you SHOULD print it out with `print(...)`.", + func=python_repl.run + ) + ToolManager.register_lc_tool( + scene=tool_identifier.scene, + name=tool_identifier.name, + tool=repl_tool + ) + diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py deleted file mode 100644 index c5706989..00000000 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__retire.py +++ /dev/null @@ -1,121 +0,0 @@ -from common_logic.common_utils.constant import SceneType, ToolRuningMode -from .._tool_base import tool_manager -from . import ( - get_weather, - give_rhetorical_question, - give_final_response, - chat, - rag -) - - -SCENE = SceneType.COMMON -LAMBDA_NAME = "lambda_common_tools" - -tool_manager.register_tool({ - "name": "get_weather", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": get_weather.lambda_handler, - "tool_def": { - "name": "get_weather", - "description": "Get the current weather for `city_name`", - "parameters": { - "type": "object", - "properties": { - "city_name": { - "description": "The name of the city to be queried", - "type": "string" - }, - }, - "required": ["city_name"] - } - }, - "running_mode": ToolRuningMode.LOOP -}) - - -tool_manager.register_tool( - { - "name": "give_rhetorical_question", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_rhetorical_question.lambda_handler, - "tool_def": { - "name": "give_rhetorical_question", - "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", - "parameters": { - "type": "object", - "properties": { - "question": { - "description": "The rhetorical question to user", - "type": "string" - }, - }, - "required": ["question"], - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool( - { - "name": "give_final_response", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_final_response.lambda_handler, - "tool_def": { - "name": "give_final_response", - "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", - "parameters": { - "type": "object", - "properties": { - "response": { - "description": "Response to user", - "type": "string" - } - }, - "required": ["response"] - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool({ - "name": "chat", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": chat.lambda_handler, - "tool_def": { - "name": "chat", - "description": "casual talk with AI", - "parameters": { - "type": "object", - "properties": { - "response": { - "description": "response to users", - "type": "string" - }}, - "required": ["response"] - }, - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name": "rag_tool", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": rag.lambda_handler, - "tool_def": { - "name": "rag_tool", - "description": "private knowledge", - "parameters": {} - }, - "running_mode": ToolRuningMode.ONCE -}) diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py index e6a878fb..8d6ce7d3 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py @@ -1,13 +1,13 @@ -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda +from common_logic.common_utils.lambda_invoke_utils import invoke_lambda,StateContext from common_logic.common_utils.prompt_utils import get_prompt_templates_from_ddb from common_logic.common_utils.constant import ( LLMTaskType ) from common_logic.common_utils.lambda_invoke_utils import send_trace -from common_logic.langchain_integration.langgraph_integration import get_current_app -def rag_tool(retriever_config:dict,state): - + +def rag_tool(retriever_config:dict,query=None): + state = StateContext.get_current_state() # state = event_body['state'] context_list = [] # add qq match results @@ -15,7 +15,7 @@ def rag_tool(retriever_config:dict,state): figure_list = [] retriever_params = retriever_config # retriever_params = state["chatbot_config"]["private_knowledge_config"] - retriever_params["query"] = state[retriever_config.get("query_key","query")] + retriever_params["query"] = query or state[retriever_config.get("query_key","query")] # retriever_params["query"] = query output: str = invoke_lambda( event_body=retriever_params, diff --git a/source/lambda/online/functions/__init__.py b/source/lambda/online/functions/__init__.py index 2497bbe9..12aa317a 100644 --- a/source/lambda/online/functions/__init__.py +++ b/source/lambda/online/functions/__init__.py @@ -1,11 +1,11 @@ # tool -from ._tool_base import get_tool_by_name,Tool,tool_manager +# from ._tool_base import get_tool_by_name,Tool,tool_manager -def init_common_tools(): - from . import lambda_common_tools +# def init_common_tools(): +# from . import lambda_common_tools -def init_aws_qa_tools(): - from . import lambda_aws_qa_tools +# def init_aws_qa_tools(): +# from . import lambda_aws_qa_tools -def init_retail_tools(): - from . import lambda_retail_tools \ No newline at end of file +# def init_retail_tools(): +# from . import lambda_retail_tools \ No newline at end of file diff --git a/source/lambda/online/functions/functions_utils/retriever/retriever.py b/source/lambda/online/functions/functions_utils/retriever/retriever.py index 694f8fbd..977855e2 100644 --- a/source/lambda/online/functions/functions_utils/retriever/retriever.py +++ b/source/lambda/online/functions/functions_utils/retriever/retriever.py @@ -23,9 +23,9 @@ GoogleRetriever, ) from langchain.retrievers import ( - AmazonKnowledgeBasesRetriever, ContextualCompressionRetriever, ) +from langchain_community.retrievers import AmazonKnowledgeBasesRetriever from langchain.retrievers.merger_retriever import MergerRetriever from langchain.schema.runnable import RunnableLambda, RunnablePassthrough from langchain_community.retrievers import AmazonKnowledgeBasesRetriever diff --git a/source/lambda/online/functions/functions_utils/retriever/utils/websearch_retrievers.py b/source/lambda/online/functions/functions_utils/retriever/utils/websearch_retrievers.py index f9b67d60..babdeb9b 100644 --- a/source/lambda/online/functions/functions_utils/retriever/utils/websearch_retrievers.py +++ b/source/lambda/online/functions/functions_utils/retriever/utils/websearch_retrievers.py @@ -9,7 +9,7 @@ logger = logging.getLogger() logger.setLevel(logging.INFO) -from langchain.utilities import GoogleSearchAPIWrapper +from langchain_community.utilities import GoogleSearchAPIWrapper from langchain.callbacks.manager import CallbackManagerForRetrieverRun from langchain.docstore.document import Document from langchain.schema.retriever import BaseRetriever diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/__init__.py b/source/lambda/online/lambda_main/main_utils/online_entries/__init__.py index 5f4c315b..bca58edd 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/__init__.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/__init__.py @@ -1,14 +1,14 @@ from common_logic.common_utils.constant import EntryType -from functions import get_tool_by_name,init_common_tools,init_retail_tools +# from functions import get_tool_by_name,init_common_tools,init_retail_tools def get_common_entry(): from .common_entry import main_chain_entry - init_common_tools() + # init_common_tools() return main_chain_entry def get_retail_entry(): from .retail_entry import main_chain_entry - init_retail_tools() + # init_retail_tools() return main_chain_entry entry_map = { diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index 8a59c437..21445ad1 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -1,5 +1,7 @@ import json -from typing import Annotated, Any, TypedDict +import traceback +from typing import Annotated, Any, TypedDict, List,Union +import copy from common_logic.common_utils.chatbot_utils import ChatbotManager from common_logic.common_utils.constant import ( @@ -7,7 +9,6 @@ IndexType, LLMTaskType, SceneType, - ToolRuningMode, ) from common_logic.common_utils.lambda_invoke_utils import ( invoke_lambda, @@ -15,20 +16,26 @@ node_monitor_wrapper, send_trace, ) +from langchain_core.messages import ToolMessage,AIMessage from common_logic.common_utils.logger_utils import get_logger from common_logic.common_utils.prompt_utils import get_prompt_templates_from_ddb from common_logic.common_utils.python_utils import add_messages, update_nest_dict from common_logic.common_utils.response_utils import process_response from common_logic.common_utils.serialization_utils import JSONEncoder -from functions import get_tool_by_name -from functions._tool_base import tool_manager -from functions.lambda_common_tools import rag -from lambda_main.main_utils.online_entries.agent_base import ( - build_agent_graph, - tool_execution, -) +from common_logic.langchain_integration.tools import ToolManager +from langchain_core.tools import BaseTool +from langchain_core.messages.tool import ToolCall +from langgraph.prebuilt.tool_node import ToolNode,TOOL_CALL_ERROR_TEMPLATE +from common_logic.langchain_integration.chains import LLMChain + + +# from lambda_main.main_utils.online_entries.agent_base import ( +# build_agent_graph, +# tool_execution, +# ) from lambda_main.main_utils.parse_config import CommonConfigParser from langgraph.graph import END, StateGraph +from common_logic.langchain_integration.langgraph_integration import set_currrent_app logger = get_logger("common_entry") @@ -84,23 +91,27 @@ class ChatbotState(TypedDict): ########### agent states ########### # current output of agent - agent_current_output: dict - # record messages during agent tool choose and calling, including agent message, tool ouput and error messages - agent_tool_history: Annotated[list[dict], add_messages] - # the maximum number that agent node can be called - agent_repeated_call_limit: int - # the current call time of agent - agent_current_call_number: int # - # whehter the current call time is less than maximum number of agent call - agent_repeated_call_validation: bool - # function calling - # whether the output of agent can be parsed as the valid tool calling - function_calling_parse_ok: bool - # whether the current parsed tool calling is run once - function_calling_is_run_once: bool - # current tool calls - function_calling_parsed_tool_calls: list - current_agent_tools_def: list + # agent_current_output: dict + # # record messages during agent tool choose and calling, including agent message, tool ouput and error messages + agent_tool_history: Annotated[List[Union[AIMessage,ToolMessage]], add_messages] + # # the maximum number that agent node can be called + # agent_repeated_call_limit: int + # # the current call time of agent + # agent_current_call_number: int # + # # whehter the current call time is less than maximum number of agent call + # agent_repeated_call_validation: bool + # # function calling + # # whether the output of agent can be parsed as the valid tool calling + # function_calling_parse_ok: bool + # # whether the current parsed tool calling is run once + exit_tool_calling: bool + # # current tool calls + # function_calling_parsed_tool_calls: list + # current_agent_tools_def: list + last_tool_messages: List[ToolMessage] + tools: List[BaseTool] + # the global rag tool use all knowledge + all_knowledge_rag_tool: BaseTool def is_null_or_empty(value): @@ -218,38 +229,57 @@ def agent(state: ChatbotState): # 2. for the first time, agent decides to give final results # deal with once tool calling - if ( - state["agent_repeated_call_validation"] - and state["function_calling_parse_ok"] - and state["agent_tool_history"] - ): - tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ - "raw_tool_call_results" - ][0] - tool_name = tool_execute_res["name"] - output = tool_execute_res["output"] - tool = get_tool_by_name(tool_name, scene=SceneType.COMMON) - if tool.running_mode == ToolRuningMode.ONCE: + last_tool_messages = state["last_tool_messages"] + if last_tool_messages and len(last_tool_messages) == 1: + last_tool_message = last_tool_messages[0] + tool:BaseTool = ToolManager.get_tool( + scene=SceneType.COMMON, + name=last_tool_message.name + ) + if tool.return_direct: send_trace("once tool", enable_trace=state["enable_trace"]) - return {"answer": output["result"], "function_calling_is_run_once": True} + return {"answer": last_tool_message.content, "exit_tool_calling": True} + + # tool_execute_res = last_tool_calls_results[-1].additional_kwargs[ + # "raw_tool_call_results" + # ][0] + # tool_name = tool_execute_res["name"] + # output = tool_execute_res["output"] + # tool = get_tool_by_name(tool_name, scene=SceneType.COMMON) + # if tool.running_mode == ToolRuningMode.ONCE: + # send_trace("once tool", enable_trace=state["enable_trace"]) + # return {"answer": output["result"], "exit_tool_calling": True} + + + + # if state["agent_tool_history"] and state["agent_tool_history"][-1].type=="tool_call": + # tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ + # "raw_tool_call_results" + # ][0] + # tool_name = tool_execute_res["name"] + # output = tool_execute_res["output"] + # tool = get_tool_by_name(tool_name, scene=SceneType.COMMON) + # if tool.running_mode == ToolRuningMode.ONCE: + # send_trace("once tool", enable_trace=state["enable_trace"]) + # return {"answer": output["result"], "exit_tool_calling": True} no_intention_condition = not state["intent_fewshot_examples"] - first_tool_final_response = False - if ( - (state["agent_current_call_number"] == 1) - and state["function_calling_parse_ok"] - and state["agent_tool_history"] - ): - tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ - "raw_tool_call_results" - ][0] - tool_name = tool_execute_res["name"] - if tool_name == "give_final_response": - first_tool_final_response = True + # first_tool_final_response = False + # if ( + # (state["agent_current_call_number"] == 1) + # and state["function_calling_parse_ok"] + # and state["agent_tool_history"] + # ): + # tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ + # "raw_tool_call_results" + # ][0] + # tool_name = tool_execute_res["name"] + # if tool_name == "give_final_response": + # first_tool_final_response = True if ( no_intention_condition - or first_tool_final_response + # or first_tool_final_response or state["chatbot_config"]["agent_config"]["only_use_rag_tool"] ): if state["chatbot_config"]["agent_config"]["only_use_rag_tool"]: @@ -259,28 +289,67 @@ def agent(state: ChatbotState): "no_intention_condition, switch to rag tool", enable_trace=state["enable_trace"], ) - elif first_tool_final_response: - send_trace( - "first tool is final response, switch to rag tool", - enable_trace=state["enable_trace"], + + all_knowledge_rag_tool = state['all_knowledge_rag_tool'] + return AIMessage(content="",tool_calls=[ + ToolCall( + name=all_knowledge_rag_tool.name, + args={} ) + ]) + + # normal call + agent_config = state["chatbot_config"]['agent_config'] + + tools_name = list(set(state['intent_fewshot_tools'] + agent_config['tools'])) + # get tools from tool names + tools = [ + ToolManager.get_tool( + scene=SceneType.COMMON, + name=name + ) + for name in tools_name + ] + llm_config = { + **agent_config['llm_config'], + "tools": tools, + "fewshot_examples": state['intent_fewshot_examples'], + } + group_name = state['chatbot_config']['group_name'] + chatbot_id = state['chatbot_config']['chatbot_id'] + prompt_templates_from_ddb = get_prompt_templates_from_ddb( + group_name, + model_id = llm_config['model_id'], + task_type=LLMTaskType.TOOL_CALLING_API, + chatbot_id=chatbot_id + ) + llm_config.update(**prompt_templates_from_ddb) - return { - "function_calling_parse_ok": True, - "agent_repeated_call_validation": True, - "function_calling_parsed_tool_calls": [ - { - "name": "rag_tool", - "kwargs": {}, - "model_id": state["chatbot_config"]["agent_config"]["llm_config"][ - "model_id" - ], - } - ], - } - response = app_agent.invoke(state) + tool_calling_chain = LLMChain.get_chain( + intent_type=LLMTaskType.TOOL_CALLING_API, + scene=SceneType.COMMON, + **llm_config + ) + + + # print(state['chat_history'] + state['agent_tool_history']) + agent_message:AIMessage = tool_calling_chain.invoke({ + "query":state['query'], + "chat_history":state['chat_history'], + "agent_tool_history":state['agent_tool_history'] + }) + + + send_trace( + # f"\n\n**agent_current_output:** \n{agent_message}\n\n **agent_current_call_number:** {agent_current_call_number}", + f"\n\n**agent_current_output:** \n{agent_message}\n\n", + state["stream"], + state["ws_connection_id"] + ) + if not agent_message.tool_calls: + return {"answer": agent_message.content, "exit_tool_calling": True} - return response + return {"agent_tool_history":[agent_message],"tools":tools} @node_monitor_wrapper @@ -314,6 +383,78 @@ def llm_direct_results_generation(state: ChatbotState): return {"answer": answer} +@node_monitor_wrapper +def tool_execution(state): + """executor lambda + Args: + state (NestUpdateState): _description_ + + Returns: + _type_: _description_ + """ + tools:List[BaseTool] = state['tools'] + + + def handle_tool_errors(e): + content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) + logger.error(f"Tool execution error:\n{traceback.format_exc()}") + return content + + tool_node = ToolNode( + tools, + handle_tool_errors=handle_tool_errors + ) + last_agent_message:AIMessage = state["agent_tool_history"][-1] + + # print(last_agent_message) + # pass state to tools if needed + # tools_map = {tool.name:tool for tool in tools} + tool_calls = last_agent_message.tool_calls + # tool_calls:List[ToolCall] = copy.deepcopy(last_agent_message.tool_calls) + + # for tool_call in tool_calls: + # tool = tools_map[tool_call['name']] + # if tool.pass_state: + # tool_call['args'].update({tool.pass_state_name:state}) + + tool_messages:List[ToolMessage] = tool_node.invoke( + [AIMessage(content="",tool_calls=tool_calls)] + ) + + print("tool result",tool_messages[0].content) + + # tool_calls = state['function_calling_parsed_tool_calls'] + # assert len(tool_calls) == 1, tool_calls + # tool_call_results = [] + # for tool_call in tool_calls: + # tool_name = tool_call["name"] + # tool_kwargs = tool_call['kwargs'] + # # call tool + # output = invoke_lambda( + # event_body = { + # "tool_name":tool_name, + # "state":state, + # "kwargs":tool_kwargs + # }, + # lambda_name="Online_Tool_Execute", + # lambda_module_path="functions.lambda_tool", + # handler_name="lambda_handler" + # ) + # tool_call_results.append({ + # "name": tool_name, + # "output": output, + # "kwargs": tool_call['kwargs'], + # "model_id": tool_call['model_id'] + # }) + + # output = format_tool_call_results(tool_call['model_id'],tool_call_results) + send_trace(f'**tool_execute_res:** \n{tool_messages}', enable_trace=state["enable_trace"]) + return { + "agent_tool_history": tool_messages, + "last_tool_messages": tool_messages + } + + def final_results_preparation(state: ChatbotState): app_response = process_response(state["event_body"], state) return {"app_response": app_response} @@ -337,18 +478,17 @@ def intent_route(state: dict): def agent_route(state: dict): - if state.get("function_calling_is_run_once", False): + if state.get("exit_tool_calling", False): return "no need tool calling" + # state["agent_repeated_call_validation"] = ( + # state["agent_current_call_number"] < state["agent_repeated_call_limit"] + # ) + # if state["agent_repeated_call_validation"]: - state["agent_repeated_call_validation"] = ( - state["agent_current_call_number"] < state["agent_repeated_call_limit"] - ) - - if state["agent_repeated_call_validation"]: - return "valid tool calling" - else: - # TODO give final strategy - raise RuntimeError + return "valid tool calling" + # else: + # # TODO give final strategy + # raise RuntimeError ############################# @@ -358,6 +498,7 @@ def agent_route(state: dict): def build_graph(chatbot_state_cls): workflow = StateGraph(chatbot_state_cls) + # add node for all chat/rag/agent mode workflow.add_node("query_preprocess", query_preprocess) # chat mode @@ -430,30 +571,29 @@ def build_graph(chatbot_state_cls): ##################################### # define online sub-graph for agent # ##################################### -app_agent = None +# app_agent = None app = None -def register_rag_tool( - name: str, - description: str, - scene=SceneType.COMMON, - lambda_name: str = "lambda_common_tools", -): - tool_manager.register_tool( - { - "name": name, - "scene": scene, - "lambda_name": lambda_name, - "lambda_module_path": rag.lambda_handler, - "tool_def": { - "name": name, - "description": description, - }, - "running_mode": ToolRuningMode.ONCE, - } - ) - +# def register_rag_tool( +# name: str, +# description: str, +# scene=SceneType.COMMON, +# lambda_name: str = "lambda_common_tools", +# ): +# tool_manager.register_tool( +# { +# "name": name, +# "scene": scene, +# "lambda_name": lambda_name, +# "lambda_module_path": rag.lambda_handler, +# "tool_def": { +# "name": name, +# "description": description, +# }, +# "running_mode": ToolRuningMode.ONCE, +# } +# ) def register_rag_tool_from_config(event_body: dict): group_name = event_body.get("chatbot_config").get("group_name", "Admin") @@ -461,13 +601,36 @@ def register_rag_tool_from_config(event_body: dict): chatbot_manager = ChatbotManager.from_environ() chatbot = chatbot_manager.get_chatbot(group_name, chatbot_id) logger.info(chatbot) + registered_tool_names = [] for index_type, item_dict in chatbot.index_ids.items(): if index_type != IndexType.INTENTION: for index_content in item_dict["value"].values(): if "indexId" in index_content and "description" in index_content: - register_rag_tool( - index_content["indexId"], index_content["description"] + # Find retriever contain index_id + retrievers = event_body["chatbot_config"]["private_knowledge_config"]['retrievers'] + retriever = None + for retriever in retrievers: + if retriever["index_name"] == index_content["indexId"]: + break + assert retriever is not None,retrievers + reranks = event_body["chatbot_config"]["private_knowledge_config"]['reranks'] + index_name = index_content["indexId"] + # TODO give specific retriever config + ToolManager.register_common_rag_tool( + retriever_config={ + "retrievers":[retriever], + "reranks":[reranks[0]], + "llm_config": event_body["chatbot_config"]["private_knowledge_config"]['llm_config'] + }, + # event_body["chatbot_config"]["private_knowledge_config"], + name=index_name, + scene=SceneType.COMMON, + description=index_content["description"], + # pass_state=True, + # pass_state_name='state' ) + registered_tool_names.append(index_name) + return registered_tool_names def common_entry(event_body): @@ -476,20 +639,20 @@ def common_entry(event_body): :param event_body: The event body for lambda function. return: answer(str) """ - global app, app_agent + global app if app is None: app = build_graph(ChatbotState) - if app_agent is None: - app_agent = build_agent_graph(ChatbotState) + # if app_agent is None: + # app_agent = build_agent_graph(ChatbotState) # debuging if is_running_local(): with open("common_entry_workflow.png", "wb") as f: f.write(app.get_graph().draw_mermaid_png()) - with open("common_entry_agent_workflow.png", "wb") as f: - f.write(app_agent.get_graph().draw_mermaid_png()) + # with open("common_entry_agent_workflow.png", "wb") as f: + # f.write(app_agent.get_graph().draw_mermaid_png()) ################################################################################ # prepare inputs and invoke graph @@ -505,7 +668,26 @@ def common_entry(event_body): message_id = event_body["custom_message_id"] ws_connection_id = event_body["ws_connection_id"] enable_trace = chatbot_config["enable_trace"] - register_rag_tool_from_config(event_body) + agent_config = event_body["chatbot_config"]["agent_config"] + + # register as rag tool for each aos index + registered_tool_names = register_rag_tool_from_config(event_body) + # update private knowledge tool to agent config + for registered_tool_name in registered_tool_names: + if registered_tool_name not in agent_config['tools']: + agent_config['tools'].append(registered_tool_name) + + # define all knowledge rag tool + print('private_knowledge_config',event_body["chatbot_config"]["private_knowledge_config"]) + + all_knowledge_rag_tool = ToolManager.register_common_rag_tool( + retriever_config=event_body["chatbot_config"]["private_knowledge_config"], + name="all_knowledge_rag_tool", + scene=SceneType.COMMON, + description="all knowledge rag tool", + # pass_state=True, + # pass_state_name='state' + ) # invoke graph and get results response = app.invoke( @@ -523,10 +705,14 @@ def common_entry(event_body): "debug_infos": {}, "extra_response": {}, "qq_match_results": [], - "agent_repeated_call_limit": chatbot_config["agent_repeated_call_limit"], - "agent_current_call_number": 0, - "ddb_additional_kwargs": {}, - } + "last_tool_messages":None, + "all_knowledge_rag_tool":all_knowledge_rag_tool, + "tools":None, + # "agent_repeated_call_limit": chatbot_config["agent_repeated_call_limit"], + # "agent_current_call_number": 0, + "ddb_additional_kwargs": {} + }, + config={"recursion_limit": 10} ) return response["app_response"] diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py deleted file mode 100644 index 5e4814de..00000000 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry_v2.py +++ /dev/null @@ -1,700 +0,0 @@ -import json -from typing import Annotated, Any, TypedDict, List -import copy - -from common_logic.common_utils.chatbot_utils import ChatbotManager -from common_logic.common_utils.constant import ( - ChatbotMode, - IndexType, - LLMTaskType, - SceneType, - ToolRuningMode, -) -from common_logic.common_utils.lambda_invoke_utils import ( - invoke_lambda, - is_running_local, - node_monitor_wrapper, - send_trace, -) -from langchain_core.messages import ToolMessage,AIMessage -from common_logic.common_utils.logger_utils import get_logger -from common_logic.common_utils.prompt_utils import get_prompt_templates_from_ddb -from common_logic.common_utils.python_utils import add_messages, update_nest_dict -from common_logic.common_utils.response_utils import process_response -from common_logic.common_utils.serialization_utils import JSONEncoder -from langchain_integration.tools import ToolManager -from langchain_core.tools import BaseTool -from langchain_core.messages.tool import ToolCall -from langgraph.prebuilt import ToolNode -from langchain_integration.chains import LLMChain - - -# from lambda_main.main_utils.online_entries.agent_base import ( -# build_agent_graph, -# tool_execution, -# ) -from lambda_main.main_utils.parse_config import CommonConfigParser -from langgraph.graph import END, StateGraph -from langchain_integration.langgraph_integration import set_currrent_app - -logger = get_logger("common_entry") - - -class ChatbotState(TypedDict): - ########### input/output states ########### - # inputs - # origin event body - event_body: dict - # origianl input question - query: str - # chat history between human and agent - chat_history: Annotated[list[dict], add_messages] - # complete chatbot config, consumed by all the nodes - chatbot_config: dict - # websocket connection id for the agent - ws_connection_id: str - # whether to enbale stream output via ws_connection_id - stream: bool - # message id related to original input question - message_id: str = None - # record running states of different nodes - trace_infos: Annotated[list[str], add_messages] - # whether to enbale trace info update via streaming ouput - enable_trace: bool - # outputs - # final answer generated by whole app graph - answer: Any - # information needed return to user, e.g. intention, context, figure and so on, anything you can get during execution - extra_response: Annotated[dict, update_nest_dict] - # addition kwargs which need to save into ddb - ddb_additional_kwargs: dict - # response of entire app - app_response: Any - - ########### query rewrite states ########### - # query rewrite results - query_rewrite: str = None - - ########### intention detection states ########### - # intention type of retrieved intention samples in search engine, e.g. OpenSearch - intent_type: str = None - # retrieved intention samples in search engine, e.g. OpenSearch - intent_fewshot_examples: list - # tools of retrieved intention samples in search engine, e.g. OpenSearch - intent_fewshot_tools: list - - ########### retriever states ########### - # contexts information retrieved in search engine, e.g. OpenSearch - qq_match_results: list = [] - contexts: str = None - figure: list = None - - ########### agent states ########### - # current output of agent - # agent_current_output: dict - # # record messages during agent tool choose and calling, including agent message, tool ouput and error messages - agent_tool_history: Annotated[List[AIMessage | ToolMessage], add_messages] - # # the maximum number that agent node can be called - # agent_repeated_call_limit: int - # # the current call time of agent - # agent_current_call_number: int # - # # whehter the current call time is less than maximum number of agent call - # agent_repeated_call_validation: bool - # # function calling - # # whether the output of agent can be parsed as the valid tool calling - # function_calling_parse_ok: bool - # # whether the current parsed tool calling is run once - tool_calling_is_run_once: bool - # # current tool calls - # function_calling_parsed_tool_calls: list - # current_agent_tools_def: list - last_tool_messages: List[ToolMessage] - tools: List[BaseTool] - # the global rag tool use all knowledge - all_knowledge_rag_tool: BaseTool - - -def is_null_or_empty(value): - if value is None: - return True - elif isinstance(value, (dict, list, str)) and not value: - return True - return False - - -def format_intention_output(data): - if is_null_or_empty(data): - return "" - - markdown_table = "| Query | Score | Name | Intent | Additional Info |\n" - markdown_table += "|----------------------|-------|------------|-------------|----------------------|\n" - for item in data: - query = item.get("query", "") - score = item.get("score", "") - name = item.get("name", "") - intent = item.get("intent", "") - kwargs = ', '.join([f'{k}: {v}' for k, v in item.get('kwargs', {}).items()]) - markdown_table += f"| {query} | {score} | {name} | {intent} | {kwargs} |\n" - logger.info(markdown_table) - - return markdown_table - -#################### -# nodes in graph # -#################### - - -@node_monitor_wrapper -def query_preprocess(state: ChatbotState): - output: str = invoke_lambda( - event_body=state, - lambda_name="Online_Query_Preprocess", - lambda_module_path="lambda_query_preprocess.query_preprocess", - handler_name="lambda_handler", - ) - - send_trace(f"\n**query rewrite:** {output}\n**origin query:** {state['query']}") - return {"query_rewrite": output} - - -@node_monitor_wrapper -def intention_detection(state: ChatbotState): - # if state['chatbot_config']['agent_config']['only_use_rag_tool']: - # return { - # "intent_type": "intention detected" - # } - retriever_params = state["chatbot_config"]["qq_match_config"] - retriever_params["query"] = state[ - retriever_params.get("retriever_config", {}).get("query_key", "query") - ] - output: str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler", - ) - context_list = [] - qq_match_threshold = retriever_params["threshold"] - for doc in output["result"]["docs"]: - if doc["retrieval_score"] > qq_match_threshold: - send_trace( - f"\n\n**similar query found**\n{doc}", - state["stream"], - state["ws_connection_id"], - state["enable_trace"], - ) - query_content = doc["answer"] - # query_content = doc['answer']['jsonlAnswer'] - return { - "answer": query_content, - "intent_type": "similar query found", - } - question = doc["question"] - answer = doc["answer"] - context_list.append(f"问题: {question}, \n答案:{answer}") - - if state["chatbot_config"]["agent_config"]["only_use_rag_tool"]: - return {"qq_match_results": context_list, "intent_type": "intention detected"} - - intent_fewshot_examples = invoke_lambda( - lambda_module_path="lambda_intention_detection.intention", - lambda_name="Online_Intention_Detection", - handler_name="lambda_handler", - event_body=state, - ) - - intent_fewshot_tools: list[str] = list( - set([e["intent"] for e in intent_fewshot_examples]) - ) - - markdown_table = format_intention_output(intent_fewshot_examples) - send_trace( - f"**intention retrieved:**\n\n {markdown_table}", - state["stream"], - state["ws_connection_id"], - state["enable_trace"], - ) - return { - "intent_fewshot_examples": intent_fewshot_examples, - "intent_fewshot_tools": intent_fewshot_tools, - "qq_match_results": context_list, - "intent_type": "intention detected", - } - - -@node_monitor_wrapper -def agent(state: ChatbotState): - # two cases to invoke rag function - # 1. when valid intention fewshot found - # 2. for the first time, agent decides to give final results - - # deal with once tool calling - last_tool_messages = state["last_tool_messages"] - if last_tool_messages and len(last_tool_messages) == 1: - last_tool_message = last_tool_messages[0] - tool:BaseTool = ToolManager.get_tool( - scene=SceneType.COMMON, - name=last_tool_message.name - ) - if tool.return_direct: - send_trace("once tool", enable_trace=state["enable_trace"]) - return {"answer": last_tool_message.content, "tool_calling_is_run_once": True} - - # tool_execute_res = last_tool_calls_results[-1].additional_kwargs[ - # "raw_tool_call_results" - # ][0] - # tool_name = tool_execute_res["name"] - # output = tool_execute_res["output"] - # tool = get_tool_by_name(tool_name, scene=SceneType.COMMON) - # if tool.running_mode == ToolRuningMode.ONCE: - # send_trace("once tool", enable_trace=state["enable_trace"]) - # return {"answer": output["result"], "tool_calling_is_run_once": True} - - - - # if state["agent_tool_history"] and state["agent_tool_history"][-1].type=="tool_call": - # tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ - # "raw_tool_call_results" - # ][0] - # tool_name = tool_execute_res["name"] - # output = tool_execute_res["output"] - # tool = get_tool_by_name(tool_name, scene=SceneType.COMMON) - # if tool.running_mode == ToolRuningMode.ONCE: - # send_trace("once tool", enable_trace=state["enable_trace"]) - # return {"answer": output["result"], "tool_calling_is_run_once": True} - - no_intention_condition = not state["intent_fewshot_examples"] - # first_tool_final_response = False - # if ( - # (state["agent_current_call_number"] == 1) - # and state["function_calling_parse_ok"] - # and state["agent_tool_history"] - # ): - # tool_execute_res = state["agent_tool_history"][-1]["additional_kwargs"][ - # "raw_tool_call_results" - # ][0] - # tool_name = tool_execute_res["name"] - # if tool_name == "give_final_response": - # first_tool_final_response = True - - if ( - no_intention_condition - # or first_tool_final_response - or state["chatbot_config"]["agent_config"]["only_use_rag_tool"] - ): - if state["chatbot_config"]["agent_config"]["only_use_rag_tool"]: - send_trace("agent only use rag tool", enable_trace=state["enable_trace"]) - elif no_intention_condition: - send_trace( - "no_intention_condition, switch to rag tool", - enable_trace=state["enable_trace"], - ) - # elif first_tool_final_response: - # send_trace( - # "first tool is final response, switch to rag tool", - # enable_trace=state["enable_trace"], - # ) - - all_knowledge_rag_tool = state['all_knowledge_rag_tool'] - return AIMessage(content="",tool_calls=[ - ToolCall( - name=all_knowledge_rag_tool.name, - args={} - ) - ]) - - # response = app_agent.invoke(state) - - # normal call - agent_config = state["chatbot_config"]['agent_config'] - tools_name = state['intent_fewshot_tools'] + agent_config['tools'] - # get tools from tool names - tools = [ - ToolManager.get_tool( - scene=SceneType.COMMON, - name=name - ) - for name in tools_name - ] - llm_config = { - **agent_config['llm_config'], - "tools": tools, - "fewshot_examples": state['intent_fewshot_examples'], - } - group_name = state['chatbot_config']['group_name'] - chatbot_id = state['chatbot_config']['chatbot_id'] - prompt_templates_from_ddb = get_prompt_templates_from_ddb( - group_name, - model_id = llm_config['model_id'], - task_type=LLMTaskType.TOOL_CALLING_API, - chatbot_id=chatbot_id - ) - llm_config.update(**prompt_templates_from_ddb) - - tool_calling_chain = LLMChain.get_chain( - intent_type=LLMTaskType.TOOL_CALLING_API, - scene=SceneType.COMMON, - **llm_config - ) - agent_message:AIMessage = tool_calling_chain.invoke(**state) - send_trace( - # f"\n\n**agent_current_output:** \n{agent_message}\n\n **agent_current_call_number:** {agent_current_call_number}", - f"\n\n**agent_current_output:** \n{agent_message}\n\n", - state["stream"], - state["ws_connection_id"] - ) - - return {"agent_tool_history":[agent_message],"tools":tools} - - -@node_monitor_wrapper -def llm_direct_results_generation(state: ChatbotState): - group_name = state["chatbot_config"]["group_name"] - llm_config = state["chatbot_config"]["chat_config"] - task_type = LLMTaskType.CHAT - - prompt_templates_from_ddb = get_prompt_templates_from_ddb( - group_name, model_id=llm_config["model_id"], task_type=task_type - ) - logger.info(prompt_templates_from_ddb) - - answer: dict = invoke_lambda( - event_body={ - "llm_config": { - **llm_config, - "stream": state["stream"], - "intent_type": task_type, - **prompt_templates_from_ddb, - }, - "llm_input": { - "query": state["query"], - "chat_history": state["chat_history"], - }, - }, - lambda_name="Online_LLM_Generate", - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name="lambda_handler", - ) - return {"answer": answer} - - -@node_monitor_wrapper -def tool_execution(state): - """executor lambda - Args: - state (NestUpdateState): _description_ - - Returns: - _type_: _description_ - """ - tools:List[BaseTool] = state['tools'] - tool_node = ToolNode(tools) - last_agent_message:AIMessage = state["agent_tool_history"][-1] - - # pass state to tools if needed - tools_map = {tool.name:tool for tool in tools} - tool_calls:List[ToolCall] = copy.deepcopy(last_agent_message.tool_calls) - - for tool_call in tool_calls: - tool = tools_map[tool_call.name] - if tool.pass_state: - tool_call.args.update({tool.pass_state_name:state}) - - tool_messages:List[ToolMessage] = tool_node.invoke( - [AIMessage(content="",tool_calls=tool_calls)] - ) - - # tool_calls = state['function_calling_parsed_tool_calls'] - # assert len(tool_calls) == 1, tool_calls - # tool_call_results = [] - # for tool_call in tool_calls: - # tool_name = tool_call["name"] - # tool_kwargs = tool_call['kwargs'] - # # call tool - # output = invoke_lambda( - # event_body = { - # "tool_name":tool_name, - # "state":state, - # "kwargs":tool_kwargs - # }, - # lambda_name="Online_Tool_Execute", - # lambda_module_path="functions.lambda_tool", - # handler_name="lambda_handler" - # ) - # tool_call_results.append({ - # "name": tool_name, - # "output": output, - # "kwargs": tool_call['kwargs'], - # "model_id": tool_call['model_id'] - # }) - - # output = format_tool_call_results(tool_call['model_id'],tool_call_results) - send_trace(f'**tool_execute_res:** \n{tool_messages}', enable_trace=state["enable_trace"]) - return { - "agent_tool_history": tool_messages, - "last_tool_messages": tool_messages - } - - -def final_results_preparation(state: ChatbotState): - app_response = process_response(state["event_body"], state) - return {"app_response": app_response} - - -def matched_query_return(state: ChatbotState): - return {"answer": state["answer"]} - - -################ -# define edges # -################ - - -def query_route(state: dict): - return f"{state['chatbot_config']['chatbot_mode']} mode" - - -def intent_route(state: dict): - return state["intent_type"] - - -def agent_route(state: dict): - if state.get("tool_calling_is_run_once", False): - return "no need tool calling" - - # state["agent_repeated_call_validation"] = ( - # state["agent_current_call_number"] < state["agent_repeated_call_limit"] - # ) - - if state["agent_repeated_call_validation"]: - return "valid tool calling" - else: - # TODO give final strategy - raise RuntimeError - - -############################# -# define online top-level graph for app # -############################# - - -def build_graph(chatbot_state_cls): - workflow = StateGraph(chatbot_state_cls) - - # add node for all chat/rag/agent mode - workflow.add_node("query_preprocess", query_preprocess) - # chat mode - workflow.add_node("llm_direct_results_generation", llm_direct_results_generation) - # rag mode - # workflow.add_node("knowledge_retrieve", knowledge_retrieve) - # workflow.add_node("llm_rag_results_generation", llm_rag_results_generation) - # agent mode - workflow.add_node("intention_detection", intention_detection) - workflow.add_node("matched_query_return", matched_query_return) - # agent sub graph - workflow.add_node("agent", agent) - workflow.add_node("tools_execution", tool_execution) - workflow.add_node("final_results_preparation", final_results_preparation) - - # add all edges - workflow.set_entry_point("query_preprocess") - # chat mode - workflow.add_edge("llm_direct_results_generation", "final_results_preparation") - # rag mode - # workflow.add_edge("knowledge_retrieve", "llm_rag_results_generation") - # workflow.add_edge("llm_rag_results_generation", END) - # agent mode - workflow.add_edge("tools_execution", "agent") - workflow.add_edge("matched_query_return", "final_results_preparation") - workflow.add_edge("final_results_preparation", END) - - # add conditional edges - # choose running mode based on user selection: - # 1. chat mode: let llm generate results directly - # 2. rag mode: retrive all knowledge and let llm generate results - # 3. agent mode: let llm generate results based on intention detection, tool calling and retrieved knowledge - workflow.add_conditional_edges( - "query_preprocess", - query_route, - { - "chat mode": "llm_direct_results_generation", - "agent mode": "intention_detection", - }, - ) - - # three running branch will be chosen based on intention detection results: - # 1. similar query found: if very similar queries were found in knowledge base, these queries will be given as results - # 2. intention detected: if intention detected, the agent logic will be invoked - workflow.add_conditional_edges( - "intention_detection", - intent_route, - { - "similar query found": "matched_query_return", - "intention detected": "agent", - }, - ) - - # the results of agent planning will be evaluated and decide next step: - # 1. valid tool calling: the agent chooses the valid tools, and the tools will be executed - # 2. no need tool calling: the agent thinks no tool needs to be called, the final results can be generated - workflow.add_conditional_edges( - "agent", - agent_route, - { - "valid tool calling": "tools_execution", - "no need tool calling": "final_results_preparation", - }, - ) - - app = workflow.compile() - return app - - -##################################### -# define online sub-graph for agent # -##################################### -# app_agent = None -app = None - - -# def register_rag_tool( -# name: str, -# description: str, -# scene=SceneType.COMMON, -# lambda_name: str = "lambda_common_tools", -# ): -# tool_manager.register_tool( -# { -# "name": name, -# "scene": scene, -# "lambda_name": lambda_name, -# "lambda_module_path": rag.lambda_handler, -# "tool_def": { -# "name": name, -# "description": description, -# }, -# "running_mode": ToolRuningMode.ONCE, -# } -# ) - -def register_rag_tool_from_config(event_body: dict): - group_name = event_body.get("chatbot_config").get("group_name", "Admin") - chatbot_id = event_body.get("chatbot_config").get("chatbot_id", "admin") - chatbot_manager = ChatbotManager.from_environ() - chatbot = chatbot_manager.get_chatbot(group_name, chatbot_id) - logger.info(chatbot) - registered_tool_names = [] - for index_type, item_dict in chatbot.index_ids.items(): - if index_type != IndexType.INTENTION: - for index_content in item_dict["value"].values(): - if "indexId" in index_content and "description" in index_content: - # Find retriever contain index_id - retrievers = event_body["chatbot_config"]["private_knowledge_config"]['retrievers'] - retriever = None - for retriever in retrievers: - if retriever["index_name"] == index_content["indexId"]: - break - assert retriever is not None,retrievers - reranks = event_body["chatbot_config"]["private_knowledge_config"]['reranks'] - index_name = index_content["indexId"] - # TODO give specific retriever config - ToolManager.register_common_rag_tool( - retriever_config={ - "retrievers":[retriever], - "reranks":[reranks[0]], - "llm_config": event_body["chatbot_config"]["private_knowledge_config"]['llm_config'] - }, - # event_body["chatbot_config"]["private_knowledge_config"], - name=index_name, - scene=SceneType.COMMON, - description=index_content["description"], - pass_state=True, - pass_state_name='state' - ) - registered_tool_names.append(index_name) - return registered_tool_names - - -def common_entry(event_body): - """ - Entry point for the Lambda function. - :param event_body: The event body for lambda function. - return: answer(str) - """ - global app - if app is None: - app = build_graph(ChatbotState) - - # if app_agent is None: - # app_agent = build_agent_graph(ChatbotState) - - # debuging - if is_running_local(): - with open("common_entry_workflow.png", "wb") as f: - f.write(app.get_graph().draw_mermaid_png()) - - # with open("common_entry_agent_workflow.png", "wb") as f: - # f.write(app_agent.get_graph().draw_mermaid_png()) - - ################################################################################ - # prepare inputs and invoke graph - event_body["chatbot_config"] = CommonConfigParser.from_chatbot_config( - event_body["chatbot_config"] - ) - logger.info(event_body) - chatbot_config = event_body["chatbot_config"] - query = event_body["query"] - use_history = chatbot_config["use_history"] - chat_history = event_body["chat_history"] if use_history else [] - stream = event_body["stream"] - message_id = event_body["custom_message_id"] - ws_connection_id = event_body["ws_connection_id"] - enable_trace = chatbot_config["enable_trace"] - agent_config = event_body["chatbot_config"]["agent_config"] - - # register as rag tool for each aos index - registered_tool_names = register_rag_tool_from_config(event_body) - # update private knowledge tool to agent config - for registered_tool_name in registered_tool_names: - if registered_tool_name not in agent_config['tools']: - agent_config['tools'].append(registered_tool_name) - - # define all knowledge rag tool - all_knowledge_rag_tool = ToolManager.register_common_rag_tool( - retriever_config=event_body["chatbot_config"]["private_knowledge_config"], - name="all_knowledge_rag_tool", - scene=SceneType.COMMON, - description="all knowledge rag tool", - pass_state=True, - pass_state_name='state' - ) - - # invoke graph and get results - response = app.invoke( - { - "event_body": event_body, - "stream": stream, - "chatbot_config": chatbot_config, - "query": query, - "enable_trace": enable_trace, - "trace_infos": [], - "message_id": message_id, - "chat_history": chat_history, - "agent_tool_history": [], - "ws_connection_id": ws_connection_id, - "debug_infos": {}, - "extra_response": {}, - "qq_match_results": [], - "last_tool_calls_results":None, - "all_knowledge_rag_tool":all_knowledge_rag_tool, - "tools":None, - # "agent_repeated_call_limit": chatbot_config["agent_repeated_call_limit"], - # "agent_current_call_number": 0, - "ddb_additional_kwargs": {}, - - } - ) - return response["app_response"] - - -main_chain_entry = common_entry diff --git a/source/lambda/online/lambda_main/test/local_test_base.py b/source/lambda/online/lambda_main/test/local_test_base.py index 25e351cc..8feef1eb 100644 --- a/source/lambda/online/lambda_main/test/local_test_base.py +++ b/source/lambda/online/lambda_main/test/local_test_base.py @@ -13,7 +13,7 @@ from typing import Any import common_logic.common_utils.websocket_utils as websocket_utils from common_logic.common_utils.constant import LLMTaskType -from langchain_core.pydantic_v1 import BaseModel, Field, validator +from pydantic import BaseModel, Field class DummyWebSocket: def post_to_connection(self,ConnectionId,Data): diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index f058da3f..67546ca1 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -144,11 +144,33 @@ def test_multi_turns_agent_pr(): # "qq_match": [], # "private_knowledge": ['pr_test-qd-sso_poc'] # } + # user_queries = [{ + # "query": "今天天气怎么样", + # "use_history": True, + # "enable_trace": False + # }] + user_queries = [{ + # "query": "199乘以98等于多少", + "query": "1234乘以89878等于多少?", + "use_history": True, + "enable_trace": True + }] + default_index_names = { "intention":[], "qq_match": [], "private_knowledge": [] } + default_llm_config = { + # 'model_id': 'anthropic.claude-3-sonnet-20240229-v1:0', + # 'model_id': "meta.llama3-1-70b-instruct-v1:0", + # 'model_id':"mistral.mistral-large-2407-v1:0", + 'model_id':"cohere.command-r-plus-v1:0", + 'model_kwargs': { + 'temperature': 0.1, + 'max_tokens': 4096 + } + } for query in user_queries: print("==" * 50) @@ -158,12 +180,14 @@ def test_multi_turns_agent_pr(): session_id=session_id, query=query['query'], use_history=query['use_history'], - chatbot_id="pr_test", - group_name='pr_test', + chatbot_id="admin", + group_name='admin', only_use_rag_tool=False, default_index_names=default_index_names, - enable_trace = query.get('enable_trace',True) - ) + enable_trace = query.get('enable_trace',True), + agent_config={"tools":["python_repl"]}, + default_llm_config=default_llm_config + ) print() @@ -200,8 +224,6 @@ def test_qq_case_from_hanxu(): - - def complete_test_pr(): print("start test in agent mode") test_multi_turns_agent_pr() @@ -409,10 +431,10 @@ def anta_test(): if __name__ == "__main__": # complete_test_pr() # test_multi_turns_rag_pr() - # test_multi_turns_agent_pr() + test_multi_turns_agent_pr() # test_qq_case_from_hanxu() # test_multi_turns_chat_pr() # bigo_test() # sso_batch_test() # anta_test() - bigo_test() + # bigo_test() From 6577e2664d0b7405ff03ebd9969ccf341b4d6be5 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Sun, 3 Nov 2024 05:17:43 +0000 Subject: [PATCH 11/29] move retrievers to common_logic --- .../common_logic/common_utils/prompt_utils.py | 2 +- .../retrievers/retriever.py | 181 ++++ .../retrievers/utils/aos_retrievers.py | 840 ++++++++++++++++++ .../retrievers/utils/aos_utils.py | 217 +++++ .../retrievers/utils/context_utils.py | 78 ++ .../retrievers/utils/reranker.py | 217 +++++ .../retrievers/utils/test.py | 176 ++++ .../retrievers/utils/websearch_retrievers.py | 124 +++ .../tools/common_tools/rag.py | 15 +- .../functions/lambda_common_tools/rag.py | 2 + .../lambda_intention_detection/intention.py | 20 +- 11 files changed, 1855 insertions(+), 17 deletions(-) create mode 100644 source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py create mode 100644 source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_retrievers.py create mode 100644 source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_utils.py create mode 100644 source/lambda/online/common_logic/langchain_integration/retrievers/utils/context_utils.py create mode 100644 source/lambda/online/common_logic/langchain_integration/retrievers/utils/reranker.py create mode 100644 source/lambda/online/common_logic/langchain_integration/retrievers/utils/test.py create mode 100644 source/lambda/online/common_logic/langchain_integration/retrievers/utils/websearch_retrievers.py diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index 0ff72f40..03bbed15 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -2,7 +2,7 @@ import os import json -from langchain.pydantic_v1 import BaseModel, Field +from pydantic import BaseModel, Field from collections import defaultdict from common_logic.common_utils.constant import LLMModelType, LLMTaskType import copy diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py b/source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py new file mode 100644 index 00000000..ba411211 --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py @@ -0,0 +1,181 @@ +import json +import os +os.environ["PYTHONUNBUFFERED"] = "1" +import logging +import sys + +import boto3 +from common_logic.common_utils.chatbot_utils import ChatbotManager +from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper +from functions.functions_utils.retriever.utils.aos_retrievers import ( + QueryDocumentBM25Retriever, + QueryDocumentKNNRetriever, + QueryQuestionRetriever, +) +from functions.functions_utils.retriever.utils.context_utils import ( + retriever_results_format, +) +from functions.functions_utils.retriever.utils.reranker import ( + BGEReranker, + MergeReranker, +) +from functions.functions_utils.retriever.utils.websearch_retrievers import ( + GoogleRetriever, +) +from langchain.retrievers import ( + ContextualCompressionRetriever, +) +from langchain_community.retrievers import AmazonKnowledgeBasesRetriever +from langchain.retrievers.merger_retriever import MergerRetriever +from langchain.schema.runnable import RunnableLambda, RunnablePassthrough +from langchain_community.retrievers import AmazonKnowledgeBasesRetriever + +logger = logging.getLogger("retriever") +logger.setLevel(logging.INFO) + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.dirname(SCRIPT_DIR)) + +kb_enabled = os.environ["KNOWLEDGE_BASE_ENABLED"].lower() == "true" +kb_type = json.loads(os.environ["KNOWLEDGE_BASE_TYPE"]) +chatbot_table_name = os.environ.get("CHATBOT_TABLE", "") +model_table_name = os.environ.get("MODEL_TABLE", "") +index_table_name = os.environ.get("INDEX_TABLE", "") +dynamodb = boto3.resource("dynamodb") +chatbot_table = dynamodb.Table(chatbot_table_name) +model_table = dynamodb.Table(model_table_name) +index_table = dynamodb.Table(index_table_name) +chatbot_manager = ChatbotManager(chatbot_table, index_table, model_table) + +region = boto3.Session().region_name + +knowledgebase_client = boto3.client("bedrock-agent-runtime", region) +sm_client = boto3.client("sagemaker-runtime") + + +def get_bedrock_kb_retrievers(knowledge_base_id_list, top_k: int): + retriever_list = [ + AmazonKnowledgeBasesRetriever( + knowledge_base_id=knowledge_base_id, + retrieval_config={"vectorSearchConfiguration": {"numberOfResults": top_k}}, + ) + for knowledge_base_id in knowledge_base_id_list + ] + return retriever_list + + +def get_websearch_retrievers(top_k: int): + retriever_list = [GoogleRetriever(top_k)] + return retriever_list + + +def get_custom_qd_retrievers(config: dict, using_bm25=False): + qd_retriever = QueryDocumentKNNRetriever(**config) + + if using_bm25: + bm25_retrievert = QueryDocumentBM25Retriever( + **{ + "index_name": config["index_name"], + "using_whole_doc": config.get("using_whole_doc", False), + "context_num": config["context_num"], + "enable_debug": config.get("enable_debug", False), + } + ) + return [qd_retriever, bm25_retrievert] + return [qd_retriever] + + +def get_custom_qq_retrievers(config: dict): + qq_retriever = QueryQuestionRetriever(**config) + return [qq_retriever] + + +def get_whole_chain(retriever_list, reranker_config): + lotr = MergerRetriever(retrievers=retriever_list) + if len(reranker_config): + default_reranker_config = { + "enable_debug": False, + "target_model": "bge_reranker_model.tar.gz", + "top_k": 10, + } + reranker_config = {**default_reranker_config, **reranker_config} + compressor = BGEReranker(**reranker_config) + else: + compressor = MergeReranker() + + compression_retriever = ContextualCompressionRetriever( + base_compressor=compressor, base_retriever=lotr + ) + whole_chain = RunnablePassthrough.assign( + docs=compression_retriever | RunnableLambda(retriever_results_format) + ) + return whole_chain + + +retriever_dict = { + "qq": get_custom_qq_retrievers, + "intention": get_custom_qq_retrievers, + "qd": get_custom_qd_retrievers, + "websearch": get_websearch_retrievers, + "bedrock_kb": get_bedrock_kb_retrievers, +} + + +def get_custom_retrievers(retriever): + return retriever_dict[retriever["index_type"]](retriever) + + + + +def lambda_handler(event, context=None): + logger.info(f"Retrieval event: {event}") + event_body = event + retriever_list = [] + for retriever in event_body["retrievers"]: + if not kb_enabled: + retriever["vector_field"] = "sentence_vector" + retriever["source_field"] = "source" + retriever["text_field"] = "paragraph" + retriever_list.extend(get_custom_retrievers(retriever)) + rerankers = event_body.get("rerankers", None) + if rerankers: + reranker_config = rerankers[0]["config"] + else: + reranker_config = {} + + if len(retriever_list) > 0: + whole_chain = get_whole_chain(retriever_list, reranker_config) + else: + whole_chain = RunnablePassthrough.assign(docs=lambda x: []) + docs = whole_chain.invoke({"query": event_body["query"], "debug_info": {}}) + return {"code": 0, "result": docs} + + +if __name__ == "__main__": + query = """test""" + event = { + "retrievers": [ + { + "index_type": "qd", + "top_k": 5, + "context_num": 1, + "using_whole_doc": False, + "query_key": "query", + "index_name": "admin-qd-default", + "kb_type": "aos", + "target_model": "amazon.titan-embed-text-v1", + "embedding_model_endpoint": "amazon.titan-embed-text-v1", + "model_type": "bedrock", + "group_name": "Admin", + } + ], + "rerankers": [], + "llm_config": { + "model_id": "anthropic.claude-3-sonnet-20240229-v1:0", + "model_kwargs": {"temperature": 0.01, "max_tokens": 1000}, + "endpoint_name": "", + }, + "query": "亚马逊云计算服务可以通过超文本传输协议(HTTP)访问吗?", + } + response = lambda_handler(event, None) + print(response) diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_retrievers.py b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_retrievers.py new file mode 100644 index 00000000..5fb9ff4d --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_retrievers.py @@ -0,0 +1,840 @@ +import asyncio +import json +import logging +import os +import traceback +from typing import Any, Dict, List, Union + +import boto3 +from common_logic.common_utils.time_utils import timeit +from langchain.callbacks.manager import CallbackManagerForRetrieverRun +from langchain.docstore.document import Document +from langchain.schema.retriever import BaseRetriever +from langchain_community.embeddings import BedrockEmbeddings +from sm_utils import SagemakerEndpointVectorOrCross + +from .aos_utils import LLMBotOpenSearchClient + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +# region = os.environ["AWS_REGION"] +kb_enabled = os.environ["KNOWLEDGE_BASE_ENABLED"].lower() == "true" +kb_type = json.loads(os.environ["KNOWLEDGE_BASE_TYPE"]) +intelli_agent_kb_enabled = kb_type.get("intelliAgentKb", {}).get("enabled", False) +aos_endpoint = os.environ.get("AOS_ENDPOINT", "") +aos_domain_name = os.environ.get("AOS_DOMAIN_NAME", "smartsearch") +aos_secret = os.environ.get("AOS_SECRET_NAME", "opensearch-master-user") +sm_client = boto3.client("secretsmanager") +bedrock_region = os.environ.get("BEDROCK_REGION", "us-east-1") +try: + master_user = sm_client.get_secret_value(SecretId=aos_secret)[ + "SecretString" + ] + if not aos_endpoint: + opensearch_client = boto3.client("opensearch") + response = opensearch_client.describe_domain( + DomainName=aos_domain_name + ) + aos_endpoint = response["DomainStatus"]["Endpoint"] + cred = json.loads(master_user) + username = cred.get("username") + password = cred.get("password") + auth = (username, password) + aos_client = LLMBotOpenSearchClient(aos_endpoint, auth) +except sm_client.exceptions.ResourceNotFoundException: + logger.info(f"Secret '{aos_secret}' not found in Secrets Manager") + aos_client = LLMBotOpenSearchClient(aos_endpoint) +except Exception as e: + logger.error(f"Error retrieving secret '{aos_secret}': {str(e)}") + raise + +DEFAULT_TEXT_FIELD_NAME = "text" +DEFAULT_VECTOR_FIELD_NAME = "vector_field" +DEFAULT_SOURCE_FIELD_NAME = "source" + + +def remove_redundancy_debug_info(results): + # filtered_results = copy.deepcopy(results) + filtered_results = results + for result in filtered_results: + for field in list(result["detail"].keys()): + if field.endswith("embedding") or field.startswith("vector"): + del result["detail"][field] + return filtered_results + + +@timeit +def get_similarity_embedding( + query: str, + embedding_model_endpoint: str, + target_model: str, + model_type: str = "vector", +) -> List[List[float]]: + if model_type.lower() == "bedrock": + embeddings = BedrockEmbeddings(model_id=embedding_model_endpoint, region_name=bedrock_region) + response = embeddings.embed_query(query) + else: + query_similarity_embedding_prompt = query + response = SagemakerEndpointVectorOrCross( + prompt=query_similarity_embedding_prompt, + endpoint_name=embedding_model_endpoint, + model_type=model_type, + stop=None, + region_name=None, + target_model=target_model, + ) + return response + + +@timeit +def get_relevance_embedding( + query: str, + query_lang: str, + embedding_model_endpoint: str, + target_model: str, + model_type: str = "vector", +): + if model_type == "bedrock": + embeddings = BedrockEmbeddings(model_id=embedding_model_endpoint, region_name=bedrock_region) + response = embeddings.embed_query(query) + else: + if model_type == "vector": + if query_lang == "zh": + query_relevance_embedding_prompt = ( + "为这个句子生成表示以用于检索相关文章:" + query + ) + elif query_lang == "en": + query_relevance_embedding_prompt = ( + "Represent this sentence for searching relevant passages: " + query + ) + else: + query_relevance_embedding_prompt = query + elif model_type == "m3" or model_type == "bce": + query_relevance_embedding_prompt = query + else: + raise ValueError(f"invalid embedding model type: {model_type}") + response = SagemakerEndpointVectorOrCross( + prompt=query_relevance_embedding_prompt, + endpoint_name=embedding_model_endpoint, + model_type=model_type, + region_name=None, + stop=None, + target_model=target_model, + ) + + return response + + +def get_filter_list(parsed_query: dict): + filter_list = [] + if "is_api_query" in parsed_query and parsed_query["is_api_query"]: + filter_list.append({"term": {"metadata.is_api": True}}) + return filter_list + + +def get_faq_answer(source, index_name, source_field): + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=source, + field=f"metadata.{source_field}", + ) + for r in opensearch_query_response["hits"]["hits"]: + if ( + "field" in r["_source"]["metadata"] + and "answer" == r["_source"]["metadata"]["field"] + ): + return r["_source"]["content"] + elif "jsonlAnswer" in r["_source"]["metadata"]: + return r["_source"]["metadata"]["jsonlAnswer"]["answer"] + return "" + + +def get_faq_content(source, index_name): + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=source, + field="metadata.source", + ) + for r in opensearch_query_response["hits"]["hits"]: + if r["_source"]["metadata"]["field"] == "all_text": + return r["_source"]["content"] + return "" + + +def get_doc(file_path, index_name): + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=file_path, + field="metadata.file_path", + size=100, + ) + chunk_list = [] + chunk_id_set = set() + for r in opensearch_query_response["hits"]["hits"]: + try: + if "chunk_id" not in r["_source"]["metadata"] or not r["_source"][ + "metadata" + ]["chunk_id"].startswith("$"): + continue + chunk_id = r["_source"]["metadata"]["chunk_id"] + content_type = r["_source"]["metadata"]["content_type"] + chunk_group_id = int(chunk_id.split("-")[0].strip("$")) + chunk_section_id = int(chunk_id.split("-")[-1]) + if (chunk_id, content_type) in chunk_id_set: + continue + except Exception as e: + logger.error(traceback.format_exc()) + continue + chunk_id_set.add((chunk_id, content_type)) + chunk_list.append( + ( + chunk_id, + chunk_group_id, + content_type, + chunk_section_id, + r["_source"]["text"], + ) + ) + sorted_chunk_list = sorted(chunk_list, key=lambda x: (x[1], x[2], x[3])) + chunk_text_list = [x[4] for x in sorted_chunk_list] + return "\n".join(chunk_text_list) + + +def get_child_context(chunk_id, index_name, window_size): + next_content_list = [] + previous_content_list = [] + previous_pos = 0 + next_pos = 0 + chunk_id_prefix = "-".join(chunk_id.split("-")[:-1]) + section_id = int(chunk_id.split("-")[-1]) + previous_section_id = section_id + next_section_id = section_id + while previous_pos < window_size: + previous_section_id -= 1 + if previous_section_id < 1: + break + previous_chunk_id = f"{chunk_id_prefix}-{previous_section_id}" + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=previous_chunk_id, + field="metadata.chunk_id", + size=1, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + previous_content_list.insert(0, r["_source"]["text"]) + previous_pos += 1 + else: + break + while next_pos < window_size: + next_section_id += 1 + next_chunk_id = f"{chunk_id_prefix}-{next_section_id}" + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=next_chunk_id, + field="metadata.chunk_id", + size=1, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + next_content_list.insert(0, r["_source"]["text"]) + next_pos += 1 + else: + break + return [previous_content_list, next_content_list] + + +def get_sibling_context(chunk_id, index_name, window_size): + next_content_list = [] + previous_content_list = [] + previous_pos = 0 + next_pos = 0 + chunk_id_prefix = "-".join(chunk_id.split("-")[:-1]) + section_id = int(chunk_id.split("-")[-1]) + previous_section_id = section_id + next_section_id = section_id + while previous_pos < window_size: + previous_section_id -= 1 + if previous_section_id < 1: + break + previous_chunk_id = f"{chunk_id_prefix}-{previous_section_id}" + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=previous_chunk_id, + field="metadata.chunk_id", + size=1, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + previous_content_list.insert(0, r["_source"]["text"]) + previous_pos += 1 + else: + break + while next_pos < window_size: + next_section_id += 1 + next_chunk_id = f"{chunk_id_prefix}-{next_section_id}" + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=next_chunk_id, + field="metadata.chunk_id", + size=1, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + next_content_list.insert(0, r["_source"]["text"]) + next_pos += 1 + else: + break + return [previous_content_list, next_content_list] + + +def get_context(aos_hit, index_name, window_size): + previous_content_list = [] + next_content_list = [] + if "chunk_id" not in aos_hit["_source"]["metadata"]: + return previous_content_list, next_content_list + chunk_id = aos_hit["_source"]["metadata"]["chunk_id"] + inner_previous_content_list, inner_next_content_list = get_sibling_context( + chunk_id, index_name, window_size + ) + if ( + len(inner_previous_content_list) == window_size + and len(inner_next_content_list) == window_size + ): + return inner_previous_content_list, inner_next_content_list + + if "heading_hierarchy" not in aos_hit["_source"]["metadata"]: + return [previous_content_list, next_content_list] + if "previous" in aos_hit["_source"]["metadata"]["heading_hierarchy"]: + previous_chunk_id = aos_hit["_source"]["metadata"]["heading_hierarchy"][ + "previous" + ] + previous_pos = 0 + while ( + previous_chunk_id + and previous_chunk_id.startswith("$") + and previous_pos < window_size + ): + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=previous_chunk_id, + field="metadata.chunk_id", + size=1, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + previous_chunk_id = r["_source"]["metadata"]["heading_hierarchy"][ + "previous" + ] + previous_content_list.insert(0, r["_source"]["text"]) + previous_pos += 1 + else: + break + if "next" in aos_hit["_source"]["metadata"]["heading_hierarchy"]: + next_chunk_id = aos_hit["_source"]["metadata"]["heading_hierarchy"]["next"] + next_pos = 0 + while ( + next_chunk_id and next_chunk_id.startswith("$") and next_pos < window_size + ): + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=next_chunk_id, + field="metadata.chunk_id", + size=1, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + next_chunk_id = r["_source"]["metadata"]["heading_hierarchy"]["next"] + next_content_list.append(r["_source"]["text"]) + next_pos += 1 + else: + break + return [previous_content_list, next_content_list] + + +def get_parent_content(previous_chunk_id, next_chunk_id, index_name): + previous_content_list = [] + while previous_chunk_id.startswith("$"): + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=previous_chunk_id, + field="metadata.chunk_id", + size=10, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + previous_chunk_id = r["_source"]["metadata"]["chunk_id"] + previous_content_list.append(r["_source"]["text"]) + else: + break + next_content_list = [] + while next_chunk_id.startswith("$"): + opensearch_query_response = aos_client.search( + index_name=index_name, + query_type="basic", + query_term=next_chunk_id, + field="metadata.chunk_id", + size=10, + ) + if len(opensearch_query_response["hits"]["hits"]) > 0: + r = opensearch_query_response["hits"]["hits"][0] + next_chunk_id = r["_source"]["metadata"]["chunk_id"] + next_content_list.append(r["_source"]["text"]) + else: + break + return [previous_content_list, next_content_list] + + +def organize_faq_results( + response, index_name, source_field="file_path", text_field="text" +): + """ + Organize results from aos response + + :param query_type: query type + :param response: aos response json + """ + results = [] + if not response: + return results + aos_hits = response["hits"]["hits"] + for aos_hit in aos_hits: + result = {} + try: + result["score"] = aos_hit["_score"] + data = aos_hit["_source"] + metadata = data["metadata"] + if "field" in metadata: + result["answer"] = get_faq_answer( + result["source"], index_name, source_field + ) + result["content"] = aos_hit["_source"]["content"] + result["question"] = aos_hit["_source"]["content"] + result[source_field] = aos_hit["_source"]["metadata"][source_field] + elif "answer" in metadata: + # Intentions + result["answer"] = metadata["answer"] + result["question"] = data["text"] + result["content"] = data["text"] + result["source"] = metadata[source_field] + result["kwargs"] = metadata.get("kwargs", {}) + elif "jsonlAnswer" in aos_hit["_source"]["metadata"] and "answer" in aos_hit["_source"]["metadata"]["jsonlAnswer"]: + # Intention + result["answer"] = aos_hit["_source"]["metadata"]["jsonlAnswer"]["answer"] + result["question"] = aos_hit["_source"]["metadata"]["jsonlAnswer"]["question"] + result["content"] = aos_hit["_source"]["text"] + if source_field in aos_hit["_source"]["metadata"]["jsonlAnswer"].keys(): + result[source_field] = aos_hit["_source"]["metadata"]["jsonlAnswer"][source_field] + else: + result[source_field] = aos_hit["_source"]["metadata"]["file_path"] + elif "jsonlAnswer" in aos_hit["_source"]["metadata"] and "answer" not in aos_hit["_source"]["metadata"]["jsonlAnswer"]: + # QQ match + result["answer"] = aos_hit["_source"]["metadata"]["jsonlAnswer"] + result["question"] = aos_hit["_source"]["text"] + result["content"] = aos_hit["_source"]["text"] + result[source_field] = aos_hit["_source"]["metadata"]["file_path"] + else: + result["answer"] = aos_hit["_source"]["metadata"] + result["content"] = aos_hit["_source"][text_field] + result["question"] = aos_hit["_source"][text_field] + result[source_field] = aos_hit["_source"]["metadata"][source_field] + except Exception as e: + logger.error(e) + logger.error(traceback.format_exc()) + logger.error(aos_hit) + continue + results.append(result) + return results + + +class QueryQuestionRetriever(BaseRetriever): + index_name: str + vector_field: str = "vector_field" + source_field: str = "source" + top_k: int = 10 + embedding_model_endpoint: str + target_model: str + model_type: str = "vector" + enable_debug: bool = False + + @timeit + def _get_relevant_documents( + self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun + ) -> List[Document]: + query = question["query"] + debug_info = question["debug_info"] + opensearch_knn_results = [] + query_repr = get_similarity_embedding( + query, self.embedding_model_endpoint, self.target_model, self.model_type + ) + opensearch_knn_response = aos_client.search( + index_name=self.index_name, + query_type="knn", + query_term=query_repr, + field=self.vector_field, + size=self.top_k, + ) + opensearch_knn_results.extend( + organize_faq_results( + opensearch_knn_response, self.index_name, self.source_field + ) + ) + docs = [] + for result in opensearch_knn_results: + docs.append( + Document( + page_content=result["content"], + metadata={ + "source": result[self.source_field], + "score": result["score"], + "retrieval_score": result["score"], + "retrieval_content": result["content"], + "answer": result["answer"], + "question": result["question"], + }, + ) + ) + if self.enable_debug: + debug_info[f"qq-knn-recall-{self.index_name}"] = ( + remove_redundancy_debug_info(opensearch_knn_results) + ) + return docs + + +class QueryDocumentKNNRetriever(BaseRetriever): + index_name: str + vector_field: str = "vector_field" + source_field: str = "file_path" + text_field: str = "text" + using_whole_doc: bool = False + context_num: int = 2 + top_k: int = 10 + # lang: Any + model_type: str = "vector" + embedding_model_endpoint: Any + target_model: Any + enable_debug: bool = False + lang: str = "zh" + + async def __ainvoke_get_context(self, aos_hit, window_size, loop): + return await loop.run_in_executor( + None, get_context, aos_hit, self.index_name, window_size + ) + + async def __spawn_task(self, aos_hits, context_size): + loop = asyncio.get_event_loop() + task_list = [] + for aos_hit in aos_hits: + if context_size: + task = asyncio.create_task( + self.__ainvoke_get_context(aos_hit, context_size, loop) + ) + task_list.append(task) + return await asyncio.gather(*task_list) + + @timeit + def organize_results( + self, + response, + aos_index=None, + source_field="file_path", + text_field="text", + using_whole_doc=True, + context_size=0, + ): + """ + Organize results from aos response + + :param query_type: query type + :param response: aos response json + """ + results = [] + if not response: + return results + aos_hits = response["hits"]["hits"] + if len(aos_hits) == 0: + return results + for aos_hit in aos_hits: + result = {"data": {}} + source = aos_hit["_source"]["metadata"][source_field] + result["source"] = source + result["score"] = aos_hit["_score"] + result["detail"] = aos_hit["_source"] + result["content"] = aos_hit["_source"][text_field] + result["doc"] = result["content"] + results.append(result) + if kb_enabled: + if using_whole_doc: + for result in results: + doc = get_doc(result["source"], aos_index) + if doc: + result["doc"] = doc + else: + response_list = asyncio.run(self.__spawn_task(aos_hits, context_size)) + for context, result in zip(response_list, results): + result["doc"] = "\n".join(context[0] + [result["content"]] + context[1]) + return results + + @timeit + def __get_knn_results(self, query_term, filter): + opensearch_knn_response = aos_client.search( + index_name=self.index_name, + query_type="knn", + query_term=query_term, + field=self.vector_field, + size=self.top_k, + filter=filter, + ) + opensearch_knn_results = self.organize_results( + opensearch_knn_response, + self.index_name, + self.source_field, + self.text_field, + self.using_whole_doc, + self.context_num, + )[: self.top_k] + return opensearch_knn_results + + @timeit + def _get_relevant_documents( + self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun + ) -> List[Document]: + query = question["query"] + # if "query_lang" in question and question["query_lang"] != self.lang and "translated_text" in question: + # query = question["translated_text"] + debug_info = question["debug_info"] + query_repr = get_relevance_embedding( + query, + self.lang, + self.embedding_model_endpoint, + self.target_model, + self.model_type, + ) + # question["colbert"] = query_repr["colbert_vecs"][0] + filter = get_filter_list(question) + # Get AOS KNN results. + opensearch_knn_results = self.__get_knn_results(query_repr, filter) + final_results = opensearch_knn_results + doc_list = [] + content_set = set() + for result in final_results: + if result["doc"] in content_set: + continue + content_set.add(result["content"]) + # TODO: add jsonlans + result_metadata = { + "source": result["source"], + "retrieval_content": result["content"], + "retrieval_data": result["data"], + "retrieval_score": result["score"], + # Set common score for llm. + "score": result["score"], + } + if "figure" in result["detail"]["metadata"]: + result_metadata["figure"] = result["detail"]["metadata"]["figure"] + if "content_type" in result["detail"]["metadata"]: + result_metadata["content_type"] = result["detail"]["metadata"][ + "content_type" + ] + doc_list.append( + Document(page_content=result["doc"], metadata=result_metadata) + ) + if self.enable_debug: + debug_info[f"qd-knn-recall-{self.index_name}"] = ( + remove_redundancy_debug_info(opensearch_knn_results) + ) + + return doc_list + + +class QueryDocumentBM25Retriever(BaseRetriever): + index_name: str + vector_field: str = "vector_field" + source_field: str = "source" + text_field: str = "text" + using_whole_doc: bool = False + context_num: Any + top_k: int = 5 + enable_debug: Any + config: Dict = {"run_name": "BM25"} + + async def __ainvoke_get_context(self, aos_hit, window_size, loop): + return await loop.run_in_executor( + None, get_context, aos_hit, self.index_name, window_size + ) + + async def __spawn_task(self, aos_hits, context_size): + loop = asyncio.get_event_loop() + task_list = [] + for aos_hit in aos_hits: + if context_size: + task = asyncio.create_task( + self.__ainvoke_get_context(aos_hit, context_size, loop) + ) + task_list.append(task) + return await asyncio.gather(*task_list) + + @timeit + def organize_results( + self, + response, + aos_index=None, + source_field="file_path", + text_field="text", + using_whole_doc=True, + context_size=0, + ): + """ + Organize results from aos response + + :param query_type: query type + :param response: aos response json + """ + results = [] + if not response: + return results + aos_hits = response["hits"]["hits"] + if len(aos_hits) == 0: + return results + for aos_hit in aos_hits: + result = {"data": {}} + source = aos_hit["_source"]["metadata"][source_field] + source = ( + source.replace( + "s3://aws-chatbot-knowledge-base/aws-acts-knowledge/qd/zh_CN/", + "https://www.amazonaws.cn/", + ) + .replace( + "s3://aws-chatbot-knowledge-base/aws-acts-knowledge/qd/en_US/", + "https://www.amazonaws.cn/en/", + ) + .replace( + "s3://aws-chatbot-knowledge-base/aws-global-site-cn-knowledge/", + "https://aws.amazon.com/", + ) + ) + result["source"] = source + result["score"] = aos_hit["_score"] + result["detail"] = aos_hit["_source"] + # result["content"] = aos_hit['_source'][text_field] + result["content"] = aos_hit["_source"][text_field] + result["doc"] = result["content"] + # if 'additional_vecs' in aos_hit['_source']['metadata'] and \ + # 'colbert_vecs' in aos_hit['_source']['metadata']['additional_vecs']: + # result["data"]["colbert"] = aos_hit['_source']['metadata']['additional_vecs']['colbert_vecs'] + if "jsonlAnswer" in aos_hit["_source"]["metadata"]: + result["jsonlAnswer"] = aos_hit["_source"]["metadata"]["jsonlAnswer"] + results.append(result) + if using_whole_doc: + for result in results: + doc = get_doc(result["source"], aos_index) + if doc: + result["doc"] = doc + else: + response_list = asyncio.run(self.__spawn_task(aos_hits, context_size)) + for context, result in zip(response_list, results): + result["doc"] = "\n".join(context[0] + [result["doc"]] + context[1]) + # context = get_context(aos_hit['_source']["metadata"]["heading_hierarchy"]["previous"], + # aos_hit['_source']["metadata"]["heading_hierarchy"]["next"], + # aos_index, + # context_size) + # if context: + # result["doc"] = "\n".join(context[0] + [result["doc"]] + context[1]) + return results + + @timeit + def __get_bm25_results(self, query_term, filter): + opensearch_bm25_response = aos_client.search( + index_name=self.index_name, + query_type="fuzzy", + query_term=query_term, + field=self.text_field, + size=self.top_k, + filter=filter, + ) + opensearch_bm25_results = self.organize_results( + opensearch_bm25_response, + self.index_name, + self.source_field, + self.text_field, + self.using_whole_doc, + self.context_num, + )[: self.top_k] + return opensearch_bm25_results + + @timeit + def _get_relevant_documents( + self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun + ) -> List[Document]: + query = question["query"] + # if "query_lang" in question and question["query_lang"] != self.lang and "translated_text" in question: + # query = question["translated_text"] + debug_info = question["debug_info"] + # query_repr = get_relevance_embedding(query, self.lang, self.embedding_model_endpoint, self.model_type) + # question["colbert"] = query_repr["colbert_vecs"][0] + filter = get_filter_list(question) + opensearch_bm25_results = self.__get_bm25_results(query, filter) + final_results = opensearch_bm25_results + doc_list = [] + content_set = set() + for result in final_results: + if result["doc"] in content_set: + continue + content_set.add(result["content"]) + result_metadata = { + "source": result["source"], + "retrieval_content": result["content"], + "retrieval_data": result["data"], + "retrieval_score": result["score"], + # Set common score for llm. + "score": result["score"], + } + if "figure" in result["detail"]["metadata"]: + result_metadata["figure"] = result["detail"]["metadata"]["figure"] + if "content_type" in result["detail"]["metadata"]: + result_metadata["content_type"] = result["detail"]["metadata"][ + "content_type" + ] + doc_list.append( + Document(page_content=result["doc"], metadata=result_metadata) + ) + if self.enable_debug: + debug_info[f"qd-bm25-recall-{self.index_name}"] = ( + remove_redundancy_debug_info(opensearch_bm25_results) + ) + return doc_list + + +def index_results_format(docs: list, threshold=-1): + results = [] + for doc in docs: + if doc.metadata["score"] < threshold: + continue + results.append( + { + "score": doc.metadata["score"], + "source": doc.metadata["source"], + "answer": doc.metadata["answer"], + "question": doc.metadata["question"], + } + ) + # output = {"answer": json.dumps(results, ensure_ascii=False), "sources": [], "contexts": []} + output = { + "answer": results, + "sources": [], + "contexts": [], + "context_docs": [], + "context_sources": [], + } + return output diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_utils.py b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_utils.py new file mode 100644 index 00000000..be14d765 --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/aos_utils.py @@ -0,0 +1,217 @@ +import os +import threading + +import boto3 +from opensearchpy import OpenSearch, RequestsHttpConnection +from requests_aws4auth import AWS4Auth + +open_search_client_lock = threading.Lock() + +credentials = boto3.Session().get_credentials() + +region = boto3.Session().region_name +awsauth = AWS4Auth( + credentials.access_key, + credentials.secret_key, + region, + "es", + session_token=credentials.token, +) + +IMPORT_OPENSEARCH_PY_ERROR = ( + "Could not import OpenSearch. Please install it with `pip install opensearch-py`." +) + + +def _import_not_found_error(): + """Import not found error if available, otherwise raise error.""" + try: + from opensearchpy.exceptions import NotFoundError + except ImportError: + raise ImportError(IMPORT_OPENSEARCH_PY_ERROR) + return NotFoundError + + +class LLMBotOpenSearchClient: + instance = None + + def __new__(cls, host, auth=None): + with open_search_client_lock: + if cls.instance is not None and cls.instance.host == host: + return cls.instance + obj = object.__new__(cls) + cls.instance = obj + return obj + + def __init__(self, host, auth=None): + """ + Initialize OpenSearch client using OpenSearch Endpoint + """ + self.host = host + self.client = OpenSearch( + hosts=[ + { + "host": host.replace("https://", ""), + "port": int(os.environ.get("AOS_PORT", 443)), + } + ], + http_auth=auth if auth is not None else awsauth, + use_ssl=True, + verify_certs=True, + connection_class=RequestsHttpConnection, + ) + self.query_match = { + "knn": self._build_knn_search_query, + "exact": self._build_exactly_match_query, + "fuzzy": self._build_fuzzy_search_query, + "basic": self._build_basic_search_query, + } + + def _build_basic_search_query( + self, index_name, query_term, field, size, filter=None + ): + """ + Build basic search query + + :param index_name: Target Index Name + :param query_term: query term + :param field: search field + :param size: number of results to return from aos + + :return: aos response json + """ + query = { + "size": size, + "query": { + "bool": { + "should": [{"match_phrase": {field: query_term}}], + } + }, + "sort": [{"_score": {"order": "desc"}}], + } + if filter: + query["query"]["bool"]["filter"] = filter + + return query + + def _build_fuzzy_search_query( + self, index_name, query_term, field, size, filter=None + ): + """ + Build basic search query + + :param index_name: Target Index Name + :param query_term: query term + :param field: search field + :param size: number of results to return from aos + + :return: aos response json + """ + query = { + "size": size, + "query": {"match": {"text": query_term}}, + "_source": {"excludes": ["*.additional_vecs", "vector_field"]}, + } + if filter: + query["query"]["bool"]["filter"] = filter + + return query + + def _build_knn_search_query(self, index_name, query_term, field, size, filter=None): + """ + Build knn search query + + :param index_name: Target Index Name + :param query_term: query term + :param field: search field + :param size: number of results to return from aos + + :return: aos response json + """ + if filter: + query = { + "size": size, + "query": { + "bool": { + "filter": {"bool": {"must": filter}}, + "must": [{"knn": {field: {"vector": query_term, "k": size}}}], + } + }, + "_source": {"excludes": ["*.additional_vecs", "vector_field"]}, + } + else: + query = { + "size": size, + "query": {"knn": {field: {"vector": query_term, "k": size}}}, + "_source": {"excludes": ["*.additional_vecs", "vector_field"]}, + } + return query + + def _build_exactly_match_query(self, index_name, query_term, field, size): + """ + Build exactly match query + + :param index_name: Target Index Name + :param query_term: query term + :param field: search field + :param size: number of results to return from aos + + :return: aos response json + """ + query = {"query": {"match_phrase": {field: query_term}}} + return query + + def organize_results(self, query_type, response, field): + """ + Organize results from aos response + + :param query_type: query type + :param response: aos response json + """ + results = [] + aos_hits = response["hits"]["hits"] + if query_type == "exact": + for aos_hit in aos_hits: + doc = aos_hit["_source"][field] + source = aos_hit["_source"]["metadata"]["source"] + score = aos_hit["_score"] + results.append({"doc": doc, "score": score, "source": source}) + else: + for aos_hit in aos_hits: + doc = f"{aos_hit['_source'][field]}" + source = aos_hit["_source"]["metadata"]["source"] + score = aos_hit["_score"] + results.append({"doc": doc, "score": score, "source": source}) + return results + + def search( + self, + index_name, + query_type, + query_term, + field: str = "text", + size: int = 10, + filter=None, + ): + """ + Perform search on aos + + :param index_name: Target Index Name + :param query_type: query type + :param query_term: query term + :param field: search field + :param size: number of results to return from aos + :param filter: filter query + + :return: aos response json + """ + not_found_error = _import_not_found_error() + try: + self.client.indices.get(index=index_name) + except not_found_error: + return [] + query = self.query_match[query_type]( + index_name, query_term, field, size, filter + ) + response = self.client.search(body=query, index=index_name) + return response diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/utils/context_utils.py b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/context_utils.py new file mode 100644 index 00000000..cada844c --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/context_utils.py @@ -0,0 +1,78 @@ +import logging +import os + +from langchain.docstore.document import Document + +from common_logic.common_utils.time_utils import timeit + +logger = logging.getLogger("context_utils") +logger.setLevel(logging.INFO) + + +def contexts_trunc(docs: list[dict], context_num=2): + docs = [doc for doc in docs[:context_num]] + # the most related doc will be placed last + docs.sort(key=lambda x: x["score"]) + # filter same docs + s = set() + context_strs = [] + context_docs = [] + context_sources = [] + for doc in docs: + content = doc["page_content"] + if content not in s: + context_strs.append(content) + s.add(content) + context_docs.append( + {"doc": content, "source": doc["source"], "score": doc["score"]} + ) + context_sources.append(doc["source"]) + return { + "contexts": context_strs, + "context_docs": context_docs, + "context_sources": context_sources, + } + + +@timeit +def retriever_results_format( + docs: list[Document], + print_source=True, + print_content=os.environ.get("print_content", False), +): + doc_dicts = [] + + for doc in docs: + doc_dicts.append( + { + "page_content": doc.page_content, + "retrieval_score": doc.metadata["retrieval_score"], + "rerank_score": doc.metadata["score"], + "score": doc.metadata["score"], + "source": doc.metadata["source"], + "answer": doc.metadata.get("answer", ""), + "question": doc.metadata.get("question", ""), + "figure": doc.metadata.get("figure", []), + } + ) + if print_source: + source_strs = [] + for doc_dict in doc_dicts: + content = "" + if print_content: + content = f', content: {doc_dict["page_content"]}' + source_strs.append( + f'source: {doc_dict["source"]}, score: {doc_dict["score"]}{content}, retrieval score: {doc_dict["retrieval_score"]}' + ) + logger.info("retrieved sources:\n" + "\n".join(source_strs)) + return doc_dicts + + +def documents_list_filter(doc_dicts: list[dict], filter_key="score", threshold=-1): + results = [] + for doc_dict in doc_dicts: + if doc_dict[filter_key] < threshold: + continue + results.append(doc_dict) + + return results diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/utils/reranker.py b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/reranker.py new file mode 100644 index 00000000..7405d59d --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/reranker.py @@ -0,0 +1,217 @@ +import json +import os +import time +import logging +import asyncio +import numpy as np +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +from typing import Dict, Optional, Sequence, Any + +from langchain.callbacks.manager import Callbacks +from langchain.schema import Document +from langchain.retrievers.document_compressors.base import BaseDocumentCompressor + +from sm_utils import SagemakerEndpointVectorOrCross + +rerank_model_endpoint = os.environ.get("RERANK_ENDPOINT", "") + +"""Document compressor that uses BGE reranker model.""" +class BGEM3Reranker(BaseDocumentCompressor): + + """Number of documents to return.""" + def _colbert_score_np(self, q_reps, p_reps): + token_scores = np.einsum('nik,njk->nij', q_reps, p_reps) + scores = token_scores.max(-1) + scores = np.sum(scores) / q_reps.shape[0] + return scores + + async def __ainvoke_rerank_model(self, query_batch, doc_batch, loop): + return await loop.run_in_executor(None, + self._colbert_score_np, + np.asarray(query_batch), + np.asarray(doc_batch)) + + async def __spawn_task(self, query_colbert_list, doc_colbert_list): + batch_size = 1 + task_list = [] + loop = asyncio.get_event_loop() + for batch_start in range(0, len(query_colbert_list), batch_size): + task = asyncio.create_task(self.__ainvoke_rerank_model( + query_colbert_list[batch_start:batch_start + batch_size], + doc_colbert_list[batch_start:batch_start + batch_size], loop)) + task_list.append(task) + return await asyncio.gather(*task_list) + + def compress_documents( + self, + documents: Sequence[Document], + query: dict, + callbacks: Optional[Callbacks] = None, + ) -> Sequence[Document]: + """ + Compress documents using BGE M3 Colbert Score. + + Args: + documents: A sequence of documents to compress. + query: The query to use for compressing the documents. + callbacks: Callbacks to run during the compression process. + + Returns: + A sequence of compressed documents. + """ + start = time.time() + if len(documents) == 0: # to avoid empty api call + return [] + doc_list = list(documents) + _docs = [d.metadata["retrieval_data"]['colbert'] for d in doc_list] + + rerank_text_length = 1024 * 10 + query_colbert_list = [] + doc_colbert_list = [] + for doc in _docs: + query_colbert_list.append(query["colbert"][:rerank_text_length]) + doc_colbert_list.append(doc[:rerank_text_length]) + score_list = [] + logger.info(f'rerank pair num {len(query_colbert_list)}, m3 method: colbert score') + score_list = asyncio.run(self.__spawn_task(query_colbert_list, doc_colbert_list)) + final_results = [] + debug_info = query["debug_info"] + debug_info["knowledge_qa_rerank"] = [] + for doc, score in zip(doc_list, score_list): + doc.metadata["rerank_score"] = score + # set common score for llm. + doc.metadata["score"] = doc.metadata["rerank_score"] + final_results.append(doc) + debug_info["knowledge_qa_rerank"].append((doc.page_content, doc.metadata["retrieval_content"], doc.metadata["source"], score)) + final_results.sort(key=lambda x: x.metadata["rerank_score"], reverse=True) + debug_info["knowledge_qa_rerank"].sort(key=lambda x: x[-1], reverse=True) + recall_end_time = time.time() + elpase_time = recall_end_time - start + logger.info(f"runing time of rerank: {elpase_time}s seconds") + return final_results + +"""Document compressor that uses BGE reranker model.""" +class BGEReranker(BaseDocumentCompressor): + + """Number of documents to return.""" + config: Dict={"run_name": "BGEReranker"} + enable_debug: Any + target_model: Any + rerank_model_endpoint: str=rerank_model_endpoint + top_k: int=10 + + def __init__(self,enable_debug=False, rerank_model_endpoint=rerank_model_endpoint, target_model=None, top_k=10): + super().__init__() + self.enable_debug = enable_debug + self.rerank_model_endpoint = rerank_model_endpoint + self.target_model = target_model + self.top_k = top_k + + async def __ainvoke_rerank_model(self, batch, loop): + logging.info("invoke endpoint") + return await loop.run_in_executor(None, + SagemakerEndpointVectorOrCross, + json.dumps(batch), + self.rerank_model_endpoint, + None, + "rerank", + None, + self.target_model) + + async def __spawn_task(self, rerank_pair): + batch_size = 128 + task_list = [] + loop = asyncio.get_event_loop() + for batch_start in range(0, len(rerank_pair), batch_size): + task = asyncio.create_task(self.__ainvoke_rerank_model(rerank_pair[batch_start:batch_start + batch_size], loop)) + task_list.append(task) + return await asyncio.gather(*task_list) + + def compress_documents( + self, + documents: Sequence[Document], + query: str, + callbacks: Optional[Callbacks] = None, + ) -> Sequence[Document]: + """ + Compress documents using BGE rerank model. + + Args: + documents: A sequence of documents to compress. + query: The query to use for compressing the documents. + callbacks: Callbacks to run during the compression process. + + Returns: + A sequence of compressed documents. + """ + start = time.time() + if len(documents) == 0: # to avoid empty api call + return [] + doc_list = list(documents) + _docs = [d.metadata["retrieval_content"] for d in doc_list] + + rerank_pair = [] + rerank_text_length = 1024 * 10 + for doc in _docs: + rerank_pair.append([query["query"], doc[:rerank_text_length]]) + score_list = [] + logger.info(f'rerank pair num {len(rerank_pair)}, endpoint_name: {self.rerank_model_endpoint}') + response_list = asyncio.run(self.__spawn_task(rerank_pair)) + for response in response_list: + score_list.extend(json.loads(response)) + final_results = [] + debug_info = query["debug_info"] + debug_info["knowledge_qa_rerank"] = [] + for doc, score in zip(doc_list, score_list): + doc.metadata["rerank_score"] = score + # set common score for llm. + doc.metadata["retrieval_score"] = doc.metadata["retrieval_score"] + doc.metadata["score"] = doc.metadata["rerank_score"] + final_results.append(doc) + if self.enable_debug: + debug_info["knowledge_qa_rerank"].append((doc.page_content, doc.metadata["retrieval_content"], doc.metadata["source"], score)) + final_results.sort(key=lambda x: x.metadata["rerank_score"], reverse=True) + debug_info["knowledge_qa_rerank"].sort(key=lambda x: x[-1], reverse=True) + recall_end_time = time.time() + elpase_time = recall_end_time - start + logger.info(f"runing time of rerank: {elpase_time}s seconds") + return final_results[:self.top_k] + +"""Document compressor that uses retriever score.""" +class MergeReranker(BaseDocumentCompressor): + + """Number of documents to return.""" + + def compress_documents( + self, + documents: Sequence[Document], + query: str, + callbacks: Optional[Callbacks] = None, + ) -> Sequence[Document]: + """ + Compress documents using BGE rerank model. + + Args: + documents: A sequence of documents to compress. + query: The query to use for compressing the documents. + callbacks: Callbacks to run during the compression process. + + Returns: + A sequence of compressed documents. + """ + start = time.time() + if len(documents) == 0: # to avoid empty api call + return [] + final_results = [] + debug_info = query["debug_info"] + debug_info["knowledge_qa_rerank"] = [] + final_results = list(documents) + final_results.sort(key=lambda x: x.metadata["score"], reverse=True) + debug_info["knowledge_qa_rerank"].append([(doc.page_content, doc.metadata["retrieval_content"], + doc.metadata["source"], doc.metadata["score"]) for doc in final_results]) + recall_end_time = time.time() + elpase_time = recall_end_time - start + logger.info(f"runing time of rerank: {elpase_time}s seconds") + return final_results \ No newline at end of file diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/utils/test.py b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/test.py new file mode 100644 index 00000000..2c7daa75 --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/test.py @@ -0,0 +1,176 @@ +import json +import os + +os.environ["PYTHONUNBUFFERED"] = "1" +import logging +import sys + +import boto3 +from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper +from lambda_retriever.utils.aos_retrievers import ( + QueryDocumentBM25Retriever, + QueryDocumentKNNRetriever, + QueryQuestionRetriever, +) +from lambda_retriever.utils.context_utils import retriever_results_format +from lambda_retriever.utils.reranker import MergeReranker +from langchain.retrievers import ( + AmazonKnowledgeBasesRetriever, + ContextualCompressionRetriever, +) +from langchain.retrievers.merger_retriever import MergerRetriever +from langchain.schema.runnable import RunnableLambda, RunnablePassthrough +from langchain_community.retrievers import AmazonKnowledgeBasesRetriever + +logger = logging.getLogger("retriever") +logger.setLevel(logging.INFO) + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.dirname(SCRIPT_DIR)) + +region = boto3.Session().region_name + +knowledgebase_client = boto3.client("bedrock-agent-runtime", region) +sm_client = boto3.client("sagemaker-runtime") + + +def get_bedrock_kb_retrievers(knowledge_base_id_list, top_k: int): + retriever_list = [ + AmazonKnowledgeBasesRetriever( + knowledge_base_id=knowledge_base_id, + retrieval_config={"vectorSearchConfiguration": {"numberOfResults": top_k}}, + ) + for knowledge_base_id in knowledge_base_id_list + ] + return retriever_list + + +def get_custom_qd_retrievers(retriever_config, using_bm25=False): + default_qd_config = { + "using_whole_doc": False, + "context_num": 1, + "top_k": 10, + "query_key": "query", + } + # qd_config = {**default_qd_config, **qd_config} + retriever_list = [QueryDocumentKNNRetriever(retriever_config)] + if using_bm25: + retriever_list += [QueryDocumentBM25Retriever(retriever_config)] + return retriever_list + + +def get_custom_qq_retrievers(retriever_config): + default_qq_config = {"top_k": 10, "query_key": "query"} + + return [ + QueryQuestionRetriever( + retriever_config, + # **qq_config + ) + ] + + +def get_whole_chain(retriever_list, reranker_config): + lotr = MergerRetriever(retrievers=retriever_list) + # if len(reranker_config): + # default_reranker_config = { + # "enable_debug": False, + # "target_model": "bge_reranker_model.tar.gz", + # "query_key": "query", + # "top_k": 10 + # } + # reranker_config = {**default_reranker_config, **reranker_config} + # compressor = BGEReranker(**reranker_config) + # else: + compressor = MergeReranker() + + compression_retriever = ContextualCompressionRetriever( + base_compressor=compressor, base_retriever=lotr + ) + whole_chain = RunnablePassthrough.assign( + docs=compression_retriever | RunnableLambda(retriever_results_format) + ) + return whole_chain + + +def get_custom_retrievers(retriever_config, retriever_type="qd"): + retriever_dict = { + "qq": get_custom_qq_retrievers, + "qd": get_custom_qd_retrievers, + "bedrock_kb": get_bedrock_kb_retrievers, + } + # retriever_type = retriever_config["type"] + return retriever_dict[retriever_type](retriever_config) + + +@chatbot_lambda_call_wrapper +def lambda_handler(event, context=None): + event_body = event + retriever_list = [] + retriever_type = event_body["type"] + for retriever_config in event_body["retrievers"]: + # retriever_type = retriever_config["type"] + retriever_list.extend(get_custom_retrievers(retriever_config, retriever_type)) + + # Re-rank not used. + # rerankers = event_body.get("rerankers", None) + # if rerankers: + # reranker_config = rerankers[0]["config"] + # else: + # reranker_config = {} + reranker_config = {} + if len(retriever_list) > 0: + whole_chain = get_whole_chain(retriever_list, reranker_config) + else: + whole_chain = RunnablePassthrough.assign(docs=lambda x: []) + docs = whole_chain.invoke({"query": event_body["query"], "debug_info": {}}) + return {"code": 0, "result": docs} + + +if __name__ == "__main__": + query = """test""" + event = { + "body": json.dumps( + { + "retrievers": [ + { + "index": "test-intent", + "config": {"top_k": "3"}, + "embedding": { + "type": "Bedrock", + "model_id": "cohere.embed-multilingual-v3", + }, + } + ], + "query": query, + "type": "qq", + } + ) + } + + event2 = { + "body": json.dumps( + { + "retrievers": [ + { + "index": "test-qa", + "config": { + "top_k": "3", + "vector_field_name": "sentence_vector", + "text_field_name": "paragraph", + "source_field_name": "source", + }, + "embedding": { + "type": "Bedrock", + "model_id": "amazon.titan-embed-text-v2:0", + }, + } + ], + "query": query, + "type": "qd", + } + ) + } + + response = lambda_handler(event2, None) + print(response) diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/utils/websearch_retrievers.py b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/websearch_retrievers.py new file mode 100644 index 00000000..babdeb9b --- /dev/null +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/utils/websearch_retrievers.py @@ -0,0 +1,124 @@ +import asyncio +import aiohttp +import time +import re +from bs4 import BeautifulSoup +import os +from typing import Any, Dict, List +import logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +from langchain_community.utilities import GoogleSearchAPIWrapper +from langchain.callbacks.manager import CallbackManagerForRetrieverRun +from langchain.docstore.document import Document +from langchain.schema.retriever import BaseRetriever +from langchain.agents import Tool + +GOOGLE_API_KEY=os.environ.get('GOOGLE_API_KEY',None) +GOOGLE_CSE_ID=os.environ.get('GOOGLE_CSE_ID',None) + + +class GoogleSearchTool(): + tool:Tool + topk:int = 5 + + def __init__(self,top_k=5): + self.topk = top_k + search = GoogleSearchAPIWrapper() + def top_results(query): + return search.results(query, self.topk) + self.tool = Tool( + name="Google Search Snippets", + description="Search Google for recent results.", + func=top_results, + ) + + def run(self,query): + return self.tool.run(query) + +def remove_html_tags(text): + soup = BeautifulSoup(text, 'html.parser') + text = soup.get_text() + text = re.sub(r'\r{1,}',"\n\n",text) + text = re.sub(r'\t{1,}',"\t",text) + text = re.sub(r'\n{2,}',"\n\n",text) + return text + +async def fetch(session, url, timeout): + try: + async with session.get(url) as response: + return await asyncio.wait_for(response.text(), timeout=timeout) + except asyncio.TimeoutError: + print(f"timeout:{url}") + return '' + except Exception as e: + print(f"ClientError:{url}", str(e)) + return '' + + +async def fetch_all(urls, timeout): + async with aiohttp.ClientSession() as session: + tasks = [] + for url in urls: + task = asyncio.create_task(fetch(session, url, timeout)) + tasks.append(task) + + results = await asyncio.gather(*tasks) + return results + +def web_search(**args): + if not GOOGLE_API_KEY or not GOOGLE_CSE_ID: + logger.info('Missing google API key') + return [] + tool = GoogleSearchTool(args['top_k']) + result = tool.run(args['query']) + return [item for item in result if 'title' in item and 'link' in item and 'snippet' in item] + + +def add_webpage_content(snippet_results): + t1 = time.time() + urls = [item['doc_author'] for item in snippet_results] + loop = asyncio.get_event_loop() + fetch_results = loop.run_until_complete(fetch_all(urls,5)) + t2= time.time() + logger.info(f'deep web search time:{t2-t1:1f}s') + final_results = [] + for i, result in enumerate(fetch_results): + if not result: + continue + page_content = remove_html_tags(result) + final_results.append({**snippet_results[i], + 'doc':snippet_results[i]['doc']+'\n'+page_content[:10000] + }) + return final_results + +class GoogleRetriever(BaseRetriever): + search: Any + result_num: Any + + def __init__(self, result_num): + super().__init__() + self.result_num = result_num + + def _get_relevant_documents( + self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun + ) -> List[Document]: + query = question[self.query_key] + result_list = web_search(query=query, top_k=self.result_num) + doc_list = [] + for result in result_list: + doc_list.append( + Document( + page_content=result["doc"], + metadata={ + "source": result["link"], + "retrieval_content": result["title"] + '\n' + result["snippet"], + "retrieval_data": result["title"] + } + ) + ) + return doc_list + + def get_whole_doc(self, results) -> Dict: + return add_webpage_content(self._get_relevant_documents(results)) \ No newline at end of file diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py index 8d6ce7d3..1e727de1 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py @@ -4,7 +4,7 @@ LLMTaskType ) from common_logic.common_utils.lambda_invoke_utils import send_trace - +from common_logic.langchain_integration.retrievers.retriever import lambda_handler as retrieve_fn def rag_tool(retriever_config:dict,query=None): state = StateContext.get_current_state() @@ -17,12 +17,13 @@ def rag_tool(retriever_config:dict,query=None): # retriever_params = state["chatbot_config"]["private_knowledge_config"] retriever_params["query"] = query or state[retriever_config.get("query_key","query")] # retriever_params["query"] = query - output: str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler", - ) + # output: str = invoke_lambda( + # event_body=retriever_params, + # lambda_name="Online_Functions", + # lambda_module_path="functions.functions_utils.retriever.retriever", + # handler_name="lambda_handler", + # ) + output = retrieve_fn(retriever_params) for doc in output["result"]["docs"]: context_list.append(doc["page_content"]) diff --git a/source/lambda/online/functions/lambda_common_tools/rag.py b/source/lambda/online/functions/lambda_common_tools/rag.py index 01170fab..e6e9e60b 100644 --- a/source/lambda/online/functions/lambda_common_tools/rag.py +++ b/source/lambda/online/functions/lambda_common_tools/rag.py @@ -17,12 +17,14 @@ def lambda_handler(event_body, context=None): retriever_params = state["chatbot_config"]["private_knowledge_config"] retriever_params["query"] = state[retriever_params.get( "retriever_config", {}).get("query_key", "query")] + output: str = invoke_lambda( event_body=retriever_params, lambda_name="Online_Functions", lambda_module_path="functions.functions_utils.retriever.retriever", handler_name="lambda_handler", ) + print("RAG debug") print(output) diff --git a/source/lambda/online/lambda_intention_detection/intention.py b/source/lambda/online/lambda_intention_detection/intention.py index f0bbc456..93f019d5 100644 --- a/source/lambda/online/lambda_intention_detection/intention.py +++ b/source/lambda/online/lambda_intention_detection/intention.py @@ -1,9 +1,11 @@ -from common_logic.common_utils.logger_utils import get_logger -from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper,invoke_lambda import json import pathlib import os +from common_logic.common_utils.logger_utils import get_logger +from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper,invoke_lambda +from common_logic.langchain_integration.retrievers.retriever import lambda_handler as retrieve_fn + logger = get_logger("intention") kb_enabled = os.environ["KNOWLEDGE_BASE_ENABLED"].lower() == "true" kb_type = json.loads(os.environ["KNOWLEDGE_BASE_TYPE"]) @@ -26,12 +28,13 @@ def get_intention_results(query:str, intention_config:dict): **intention_config } # call retriver - res:list[dict] = invoke_lambda( - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler", - event_body=event_body - ) + # res:list[dict] = invoke_lambda( + # lambda_name="Online_Functions", + # lambda_module_path="functions.functions_utils.retriever.retriever", + # handler_name="lambda_handler", + # event_body=event_body + # ) + res = retrieve_fn(event_body) if not res["result"]["docs"]: # add default intention @@ -91,6 +94,5 @@ def lambda_handler(state:dict, context=None): **intention_config, } ) - return output From d9b6851fc311b76c17f99c3c8d577f5eaf3e6fe3 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Tue, 5 Nov 2024 05:46:20 +0000 Subject: [PATCH 12/29] move functions to __functions --- source/lambda/online/{functions => __functions}/__init__.py | 0 source/lambda/online/{functions => __functions}/_tool_base.py | 0 .../functions_utils/retriever/retriever.py | 0 .../functions_utils/retriever/utils/aos_retrievers.py | 0 .../functions_utils/retriever/utils/aos_utils.py | 0 .../functions_utils/retriever/utils/context_utils.py | 0 .../functions_utils/retriever/utils/reranker.py | 0 .../functions_utils/retriever/utils/test.py | 0 .../functions_utils/retriever/utils/websearch_retrievers.py | 0 .../{functions => __functions}/lambda_aws_qa_tools/__init__.py | 0 .../lambda_aws_qa_tools/aws_ec2_price.py | 0 .../lambda_aws_qa_tools/check_service_availability.py | 0 .../{functions => __functions}/lambda_aws_qa_tools/comfort.py | 0 .../lambda_aws_qa_tools/explain_abbr.py | 0 .../{functions => __functions}/lambda_aws_qa_tools/service_org.py | 0 .../{functions => __functions}/lambda_aws_qa_tools/transfer.py | 0 .../{functions => __functions}/lambda_common_tools/__init__.py | 0 .../online/{functions => __functions}/lambda_common_tools/chat.py | 0 .../lambda_common_tools/comparison_rag.py | 0 .../{functions => __functions}/lambda_common_tools/get_weather.py | 0 .../lambda_common_tools/give_final_response.py | 0 .../lambda_common_tools/give_rhetorical_question.py | 0 .../online/{functions => __functions}/lambda_common_tools/rag.py | 0 .../lambda_common_tools/step_back_rag.py | 0 .../{functions => __functions}/lambda_retail_tools/__init__.py | 0 .../lambda_retail_tools/customer_complain.py | 0 .../lambda_retail_tools/daily_reception.py | 0 .../lambda_retail_tools/goods_exchange.py | 0 .../{functions => __functions}/lambda_retail_tools/order_info.py | 0 .../lambda_retail_tools/product_aftersales.py | 0 .../lambda_retail_tools/product_information_search.py | 0 .../{functions => __functions}/lambda_retail_tools/promotion.py | 0 .../lambda_retail_tools/rule_response.py | 0 .../{functions => __functions}/lambda_retail_tools/size_guide.py | 0 .../{functions => __functions}/lambda_retail_tools/transfer.py | 0 source/lambda/online/{functions => __functions}/lambda_tool.py | 0 .../online/{functions => __functions}/tool_calling_parse.py | 0 .../{functions => __functions}/tool_execute_result_format.py | 0 38 files changed, 0 insertions(+), 0 deletions(-) rename source/lambda/online/{functions => __functions}/__init__.py (100%) rename source/lambda/online/{functions => __functions}/_tool_base.py (100%) rename source/lambda/online/{functions => __functions}/functions_utils/retriever/retriever.py (100%) rename source/lambda/online/{functions => __functions}/functions_utils/retriever/utils/aos_retrievers.py (100%) rename source/lambda/online/{functions => __functions}/functions_utils/retriever/utils/aos_utils.py (100%) rename source/lambda/online/{functions => __functions}/functions_utils/retriever/utils/context_utils.py (100%) rename source/lambda/online/{functions => __functions}/functions_utils/retriever/utils/reranker.py (100%) rename source/lambda/online/{functions => __functions}/functions_utils/retriever/utils/test.py (100%) rename source/lambda/online/{functions => __functions}/functions_utils/retriever/utils/websearch_retrievers.py (100%) rename source/lambda/online/{functions => __functions}/lambda_aws_qa_tools/__init__.py (100%) rename source/lambda/online/{functions => __functions}/lambda_aws_qa_tools/aws_ec2_price.py (100%) rename source/lambda/online/{functions => __functions}/lambda_aws_qa_tools/check_service_availability.py (100%) rename source/lambda/online/{functions => __functions}/lambda_aws_qa_tools/comfort.py (100%) rename source/lambda/online/{functions => __functions}/lambda_aws_qa_tools/explain_abbr.py (100%) rename source/lambda/online/{functions => __functions}/lambda_aws_qa_tools/service_org.py (100%) rename source/lambda/online/{functions => __functions}/lambda_aws_qa_tools/transfer.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/__init__.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/chat.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/comparison_rag.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/get_weather.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/give_final_response.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/give_rhetorical_question.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/rag.py (100%) rename source/lambda/online/{functions => __functions}/lambda_common_tools/step_back_rag.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/__init__.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/customer_complain.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/daily_reception.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/goods_exchange.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/order_info.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/product_aftersales.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/product_information_search.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/promotion.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/rule_response.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/size_guide.py (100%) rename source/lambda/online/{functions => __functions}/lambda_retail_tools/transfer.py (100%) rename source/lambda/online/{functions => __functions}/lambda_tool.py (100%) rename source/lambda/online/{functions => __functions}/tool_calling_parse.py (100%) rename source/lambda/online/{functions => __functions}/tool_execute_result_format.py (100%) diff --git a/source/lambda/online/functions/__init__.py b/source/lambda/online/__functions/__init__.py similarity index 100% rename from source/lambda/online/functions/__init__.py rename to source/lambda/online/__functions/__init__.py diff --git a/source/lambda/online/functions/_tool_base.py b/source/lambda/online/__functions/_tool_base.py similarity index 100% rename from source/lambda/online/functions/_tool_base.py rename to source/lambda/online/__functions/_tool_base.py diff --git a/source/lambda/online/functions/functions_utils/retriever/retriever.py b/source/lambda/online/__functions/functions_utils/retriever/retriever.py similarity index 100% rename from source/lambda/online/functions/functions_utils/retriever/retriever.py rename to source/lambda/online/__functions/functions_utils/retriever/retriever.py diff --git a/source/lambda/online/functions/functions_utils/retriever/utils/aos_retrievers.py b/source/lambda/online/__functions/functions_utils/retriever/utils/aos_retrievers.py similarity index 100% rename from source/lambda/online/functions/functions_utils/retriever/utils/aos_retrievers.py rename to source/lambda/online/__functions/functions_utils/retriever/utils/aos_retrievers.py diff --git a/source/lambda/online/functions/functions_utils/retriever/utils/aos_utils.py b/source/lambda/online/__functions/functions_utils/retriever/utils/aos_utils.py similarity index 100% rename from source/lambda/online/functions/functions_utils/retriever/utils/aos_utils.py rename to source/lambda/online/__functions/functions_utils/retriever/utils/aos_utils.py diff --git a/source/lambda/online/functions/functions_utils/retriever/utils/context_utils.py b/source/lambda/online/__functions/functions_utils/retriever/utils/context_utils.py similarity index 100% rename from source/lambda/online/functions/functions_utils/retriever/utils/context_utils.py rename to source/lambda/online/__functions/functions_utils/retriever/utils/context_utils.py diff --git a/source/lambda/online/functions/functions_utils/retriever/utils/reranker.py b/source/lambda/online/__functions/functions_utils/retriever/utils/reranker.py similarity index 100% rename from source/lambda/online/functions/functions_utils/retriever/utils/reranker.py rename to source/lambda/online/__functions/functions_utils/retriever/utils/reranker.py diff --git a/source/lambda/online/functions/functions_utils/retriever/utils/test.py b/source/lambda/online/__functions/functions_utils/retriever/utils/test.py similarity index 100% rename from source/lambda/online/functions/functions_utils/retriever/utils/test.py rename to source/lambda/online/__functions/functions_utils/retriever/utils/test.py diff --git a/source/lambda/online/functions/functions_utils/retriever/utils/websearch_retrievers.py b/source/lambda/online/__functions/functions_utils/retriever/utils/websearch_retrievers.py similarity index 100% rename from source/lambda/online/functions/functions_utils/retriever/utils/websearch_retrievers.py rename to source/lambda/online/__functions/functions_utils/retriever/utils/websearch_retrievers.py diff --git a/source/lambda/online/functions/lambda_aws_qa_tools/__init__.py b/source/lambda/online/__functions/lambda_aws_qa_tools/__init__.py similarity index 100% rename from source/lambda/online/functions/lambda_aws_qa_tools/__init__.py rename to source/lambda/online/__functions/lambda_aws_qa_tools/__init__.py diff --git a/source/lambda/online/functions/lambda_aws_qa_tools/aws_ec2_price.py b/source/lambda/online/__functions/lambda_aws_qa_tools/aws_ec2_price.py similarity index 100% rename from source/lambda/online/functions/lambda_aws_qa_tools/aws_ec2_price.py rename to source/lambda/online/__functions/lambda_aws_qa_tools/aws_ec2_price.py diff --git a/source/lambda/online/functions/lambda_aws_qa_tools/check_service_availability.py b/source/lambda/online/__functions/lambda_aws_qa_tools/check_service_availability.py similarity index 100% rename from source/lambda/online/functions/lambda_aws_qa_tools/check_service_availability.py rename to source/lambda/online/__functions/lambda_aws_qa_tools/check_service_availability.py diff --git a/source/lambda/online/functions/lambda_aws_qa_tools/comfort.py b/source/lambda/online/__functions/lambda_aws_qa_tools/comfort.py similarity index 100% rename from source/lambda/online/functions/lambda_aws_qa_tools/comfort.py rename to source/lambda/online/__functions/lambda_aws_qa_tools/comfort.py diff --git a/source/lambda/online/functions/lambda_aws_qa_tools/explain_abbr.py b/source/lambda/online/__functions/lambda_aws_qa_tools/explain_abbr.py similarity index 100% rename from source/lambda/online/functions/lambda_aws_qa_tools/explain_abbr.py rename to source/lambda/online/__functions/lambda_aws_qa_tools/explain_abbr.py diff --git a/source/lambda/online/functions/lambda_aws_qa_tools/service_org.py b/source/lambda/online/__functions/lambda_aws_qa_tools/service_org.py similarity index 100% rename from source/lambda/online/functions/lambda_aws_qa_tools/service_org.py rename to source/lambda/online/__functions/lambda_aws_qa_tools/service_org.py diff --git a/source/lambda/online/functions/lambda_aws_qa_tools/transfer.py b/source/lambda/online/__functions/lambda_aws_qa_tools/transfer.py similarity index 100% rename from source/lambda/online/functions/lambda_aws_qa_tools/transfer.py rename to source/lambda/online/__functions/lambda_aws_qa_tools/transfer.py diff --git a/source/lambda/online/functions/lambda_common_tools/__init__.py b/source/lambda/online/__functions/lambda_common_tools/__init__.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/__init__.py rename to source/lambda/online/__functions/lambda_common_tools/__init__.py diff --git a/source/lambda/online/functions/lambda_common_tools/chat.py b/source/lambda/online/__functions/lambda_common_tools/chat.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/chat.py rename to source/lambda/online/__functions/lambda_common_tools/chat.py diff --git a/source/lambda/online/functions/lambda_common_tools/comparison_rag.py b/source/lambda/online/__functions/lambda_common_tools/comparison_rag.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/comparison_rag.py rename to source/lambda/online/__functions/lambda_common_tools/comparison_rag.py diff --git a/source/lambda/online/functions/lambda_common_tools/get_weather.py b/source/lambda/online/__functions/lambda_common_tools/get_weather.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/get_weather.py rename to source/lambda/online/__functions/lambda_common_tools/get_weather.py diff --git a/source/lambda/online/functions/lambda_common_tools/give_final_response.py b/source/lambda/online/__functions/lambda_common_tools/give_final_response.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/give_final_response.py rename to source/lambda/online/__functions/lambda_common_tools/give_final_response.py diff --git a/source/lambda/online/functions/lambda_common_tools/give_rhetorical_question.py b/source/lambda/online/__functions/lambda_common_tools/give_rhetorical_question.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/give_rhetorical_question.py rename to source/lambda/online/__functions/lambda_common_tools/give_rhetorical_question.py diff --git a/source/lambda/online/functions/lambda_common_tools/rag.py b/source/lambda/online/__functions/lambda_common_tools/rag.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/rag.py rename to source/lambda/online/__functions/lambda_common_tools/rag.py diff --git a/source/lambda/online/functions/lambda_common_tools/step_back_rag.py b/source/lambda/online/__functions/lambda_common_tools/step_back_rag.py similarity index 100% rename from source/lambda/online/functions/lambda_common_tools/step_back_rag.py rename to source/lambda/online/__functions/lambda_common_tools/step_back_rag.py diff --git a/source/lambda/online/functions/lambda_retail_tools/__init__.py b/source/lambda/online/__functions/lambda_retail_tools/__init__.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/__init__.py rename to source/lambda/online/__functions/lambda_retail_tools/__init__.py diff --git a/source/lambda/online/functions/lambda_retail_tools/customer_complain.py b/source/lambda/online/__functions/lambda_retail_tools/customer_complain.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/customer_complain.py rename to source/lambda/online/__functions/lambda_retail_tools/customer_complain.py diff --git a/source/lambda/online/functions/lambda_retail_tools/daily_reception.py b/source/lambda/online/__functions/lambda_retail_tools/daily_reception.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/daily_reception.py rename to source/lambda/online/__functions/lambda_retail_tools/daily_reception.py diff --git a/source/lambda/online/functions/lambda_retail_tools/goods_exchange.py b/source/lambda/online/__functions/lambda_retail_tools/goods_exchange.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/goods_exchange.py rename to source/lambda/online/__functions/lambda_retail_tools/goods_exchange.py diff --git a/source/lambda/online/functions/lambda_retail_tools/order_info.py b/source/lambda/online/__functions/lambda_retail_tools/order_info.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/order_info.py rename to source/lambda/online/__functions/lambda_retail_tools/order_info.py diff --git a/source/lambda/online/functions/lambda_retail_tools/product_aftersales.py b/source/lambda/online/__functions/lambda_retail_tools/product_aftersales.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/product_aftersales.py rename to source/lambda/online/__functions/lambda_retail_tools/product_aftersales.py diff --git a/source/lambda/online/functions/lambda_retail_tools/product_information_search.py b/source/lambda/online/__functions/lambda_retail_tools/product_information_search.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/product_information_search.py rename to source/lambda/online/__functions/lambda_retail_tools/product_information_search.py diff --git a/source/lambda/online/functions/lambda_retail_tools/promotion.py b/source/lambda/online/__functions/lambda_retail_tools/promotion.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/promotion.py rename to source/lambda/online/__functions/lambda_retail_tools/promotion.py diff --git a/source/lambda/online/functions/lambda_retail_tools/rule_response.py b/source/lambda/online/__functions/lambda_retail_tools/rule_response.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/rule_response.py rename to source/lambda/online/__functions/lambda_retail_tools/rule_response.py diff --git a/source/lambda/online/functions/lambda_retail_tools/size_guide.py b/source/lambda/online/__functions/lambda_retail_tools/size_guide.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/size_guide.py rename to source/lambda/online/__functions/lambda_retail_tools/size_guide.py diff --git a/source/lambda/online/functions/lambda_retail_tools/transfer.py b/source/lambda/online/__functions/lambda_retail_tools/transfer.py similarity index 100% rename from source/lambda/online/functions/lambda_retail_tools/transfer.py rename to source/lambda/online/__functions/lambda_retail_tools/transfer.py diff --git a/source/lambda/online/functions/lambda_tool.py b/source/lambda/online/__functions/lambda_tool.py similarity index 100% rename from source/lambda/online/functions/lambda_tool.py rename to source/lambda/online/__functions/lambda_tool.py diff --git a/source/lambda/online/functions/tool_calling_parse.py b/source/lambda/online/__functions/tool_calling_parse.py similarity index 100% rename from source/lambda/online/functions/tool_calling_parse.py rename to source/lambda/online/__functions/tool_calling_parse.py diff --git a/source/lambda/online/functions/tool_execute_result_format.py b/source/lambda/online/__functions/tool_execute_result_format.py similarity index 100% rename from source/lambda/online/functions/tool_execute_result_format.py rename to source/lambda/online/__functions/tool_execute_result_format.py From cdb3e156df4b87818e8793f1895bdd48eb4aea8b Mon Sep 17 00:00:00 2001 From: zhouxss Date: Wed, 6 Nov 2024 08:21:31 +0000 Subject: [PATCH 13/29] add lambda tool test --- .../common_utils/lambda_invoke_utils.py | 17 +- .../common_utils/pydantic_models.py | 4 +- .../chains/chat_chain.py | 2 +- .../chains/conversation_summary_chain.py | 5 +- .../chains/tool_calling_chain_api.py | 7 - .../langgraph_integration.py | 12 -- .../retrievers/retriever.py | 11 +- .../langchain_integration/tools/__init__.py | 15 +- .../tools/common_tools/__init__.py | 14 +- .../tools/common_tools/rag.py | 32 ++-- source/lambda/online/lambda_main/main.py | 4 +- .../main_utils/online_entries/common_entry.py | 158 +++++++----------- .../test/main_local_test_common.py | 62 +++++-- 13 files changed, 154 insertions(+), 189 deletions(-) delete mode 100644 source/lambda/online/common_logic/langchain_integration/langgraph_integration.py diff --git a/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py b/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py index 6f90d9ee..923f3ddd 100644 --- a/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py +++ b/source/lambda/online/common_logic/common_utils/lambda_invoke_utils.py @@ -17,7 +17,9 @@ from .exceptions import LambdaInvokeError logger = get_logger("lambda_invoke_utils") +# thread_local = threading.local() thread_local = threading.local() +CURRENT_STATE = None __FUNC_NAME_MAP = { "query_preprocess": "Preprocess for Multi-round Conversation", @@ -37,17 +39,22 @@ def __init__(self,state): @classmethod def get_current_state(cls): - state = getattr(thread_local,'state',None) + # print("thread id",threading.get_ident(),'parent id',threading.) + # state = getattr(thread_local,'state',None) + state = CURRENT_STATE assert state is not None,"There is not a valid state in current context" return state @classmethod def set_current_state(cls, state): - setattr(thread_local, 'state', state) + global CURRENT_STATE + assert CURRENT_STATE is None, "Parallel node executions are not alowed" + CURRENT_STATE = state @classmethod def clear_state(cls): - setattr(thread_local, 'state', None) + global CURRENT_STATE + CURRENT_STATE = None def __enter__(self): self.set_current_state(self.state) @@ -125,8 +132,9 @@ def invoke_with_lambda(self, lambda_name: str, event_body: dict): ) response_body = invoke_response["Payload"] response_str = response_body.read().decode() - response_body = json.loads(response_str) + if "body" in response_body: + response_body = json.loads(response_body['body']) if "errorType" in response_body: error = ( @@ -136,7 +144,6 @@ def invoke_with_lambda(self, lambda_name: str, event_body: dict): + f"{response_body['errorType']}: {response_body['errorMessage']}" ) raise LambdaInvokeError(error) - return response_body def invoke_with_local( diff --git a/source/lambda/online/common_logic/common_utils/pydantic_models.py b/source/lambda/online/common_logic/common_utils/pydantic_models.py index bfb60bcc..2cfc90f9 100644 --- a/source/lambda/online/common_logic/common_utils/pydantic_models.py +++ b/source/lambda/online/common_logic/common_utils/pydantic_models.py @@ -88,7 +88,7 @@ class RagToolConfig(AllowBaseModel): class AgentConfig(ForbidBaseModel): llm_config: LLMConfig = Field(default_factory=LLMConfig) - tools: list[str] = Field(default_factory=list) + tools: list[Union[str,dict]] = Field(default_factory=list) only_use_rag_tool: bool = False @@ -114,7 +114,7 @@ class ChatbotConfig(AllowBaseModel): private_knowledge_config: PrivateKnowledgeConfig = Field( default_factory=PrivateKnowledgeConfig ) - tools_config: dict[str, Any] = Field(default_factory=dict) + # tools_config: dict[str, Any] = Field(default_factory=dict) def update_llm_config(self, new_llm_config: dict): """unified update llm config diff --git a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py index b51e342d..434e592a 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py @@ -313,7 +313,7 @@ class Qwen2Instruct72BChatChain(Qwen2Instruct7BChatChain): class ChatGPT35ChatChain(LLMChain): - model_id = LLMModelType.CHATGPT_35_TURBO + model_id = LLMModelType.CHATGPT_35_TURBO_0125 intent_type = LLMTaskType.CHAT @classmethod diff --git a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py index 61b67598..80ee00b2 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py @@ -2,8 +2,7 @@ from typing import List import json from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough, + RunnableLambda ) @@ -215,8 +214,6 @@ class CohereCommandRPlusConversationSummaryChain(Claude2ConversationSummaryChain - - class Qwen2Instruct72BConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.QWEN2INSTRUCT72B diff --git a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py index 35c7c0fa..27e96c72 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py @@ -1,15 +1,9 @@ # tool calling chain import json from typing import List,Dict,Any -import re from collections import defaultdict -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough -) from common_logic.common_utils.prompt_utils import get_prompt_template -from common_logic.common_utils.logger_utils import print_llm_messages from langchain_core.messages import( AIMessage, SystemMessage @@ -142,7 +136,6 @@ def create_chain(cls, model_kwargs=None, **kwargs): ] ) - # chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | llm chain = tool_calling_template | llm return chain diff --git a/source/lambda/online/common_logic/langchain_integration/langgraph_integration.py b/source/lambda/online/common_logic/langchain_integration/langgraph_integration.py deleted file mode 100644 index 61b264e0..00000000 --- a/source/lambda/online/common_logic/langchain_integration/langgraph_integration.py +++ /dev/null @@ -1,12 +0,0 @@ - -# set global langgraph app - -current_app = None - -def set_currrent_app(app): - global current_app - current_app = app - -def get_current_app(): - assert current_app is not None - return current_app \ No newline at end of file diff --git a/source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py b/source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py index ba411211..d1c9884c 100644 --- a/source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py +++ b/source/lambda/online/common_logic/langchain_integration/retrievers/retriever.py @@ -6,20 +6,19 @@ import boto3 from common_logic.common_utils.chatbot_utils import ChatbotManager -from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper -from functions.functions_utils.retriever.utils.aos_retrievers import ( +from common_logic.langchain_integration.retrievers.utils.aos_retrievers import ( QueryDocumentBM25Retriever, QueryDocumentKNNRetriever, QueryQuestionRetriever, ) -from functions.functions_utils.retriever.utils.context_utils import ( +from common_logic.langchain_integration.retrievers.utils.context_utils import ( retriever_results_format, ) -from functions.functions_utils.retriever.utils.reranker import ( +from common_logic.langchain_integration.retrievers.utils.reranker import ( BGEReranker, MergeReranker, ) -from functions.functions_utils.retriever.utils.websearch_retrievers import ( +from common_logic.langchain_integration.retrievers.utils.websearch_retrievers import ( GoogleRetriever, ) from langchain.retrievers import ( @@ -125,8 +124,6 @@ def get_custom_retrievers(retriever): return retriever_dict[retriever["index_type"]](retriever) - - def lambda_handler(event, context=None): logger.info(f"Retrieval event: {event}") event_body = event diff --git a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py index f25fd76e..7a704728 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py @@ -10,17 +10,14 @@ from datamodel_code_generator.model import get_data_model_types from datamodel_code_generator.parser.jsonschema import JsonSchemaParser from langchain.tools.base import StructuredTool as _StructuredTool ,BaseTool -# from langchain_core.pydantic_v1 import BaseModel from common_logic.common_utils.constant import SceneType from common_logic.common_utils.lambda_invoke_utils import invoke_with_lambda from functools import partial - class StructuredTool(_StructuredTool): - pass - # pass_state:bool = False # if pass state into tool invoke - # pass_state_name:str = "state" # pass state name + pass + class ToolIdentifier(BaseModel): scene: SceneType @@ -158,9 +155,7 @@ def register_common_rag_tool( scene=None, name=None, tool_identifier=None, - return_direct=False, - # pass_state=True, - # pass_state_name='state' + return_direct=False ): assert scene == SceneType.COMMON, scene from .common_tools.rag import rag_tool @@ -185,9 +180,7 @@ class Config: tool_def=RagModel ), description=description, - return_direct=return_direct, - # pass_state=pass_state, - # pass_state_name=pass_state_name + return_direct=return_direct ) return ToolManager.register_lc_tool( diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py index 170daa44..b3f5aa15 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py @@ -102,8 +102,7 @@ def _load_rag_tool(tool_identifier:ToolIdentifier): "description": "query for retrieve", "type": "string" } - }, - # "required": ["query"] + } } ToolManager.register_func_as_tool( scene=tool_identifier.scene, @@ -114,7 +113,6 @@ def _load_rag_tool(tool_identifier:ToolIdentifier): ) - ################### langchain tools ####################### @lazy_tool_load_decorator(SceneType.COMMON,"python_repl") @@ -122,10 +120,16 @@ def _loadd_python_repl_tool(tool_identifier:ToolIdentifier): from langchain_core.tools import Tool from langchain_experimental.utilities import PythonREPL python_repl = PythonREPL() + + def _run(command: str, timeout = None) -> str: + res = python_repl.run(command=command,timeout=timeout) + if not res: + raise ValueError(f"The output is empty, please call this tool again and refine you code, use `print` function to output the value you want to obtain.") + return res repl_tool = Tool( name="python_repl", - description="A Python shell. Use this to execute python commands. Input should be a valid python command. If you want to see the output of a value, you SHOULD print it out with `print(...)`.", - func=python_repl.run + description="This tool is for arbitrary python code execution, typically scenes include scientific problems, such as math problems, physics problems, etc. Use this to execute python code. Input should be a valid python code. If you want to see the output of a value, you must print it out with `print(...)` statement.", + func=_run ) ToolManager.register_lc_tool( scene=tool_identifier.scene, diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py index 1e727de1..e318574e 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py @@ -5,6 +5,8 @@ ) from common_logic.common_utils.lambda_invoke_utils import send_trace from common_logic.langchain_integration.retrievers.retriever import lambda_handler as retrieve_fn +from common_logic.langchain_integration.chains import LLMChain +import threading def rag_tool(retriever_config:dict,query=None): state = StateContext.get_current_state() @@ -14,15 +16,7 @@ def rag_tool(retriever_config:dict,query=None): context_list.extend(state['qq_match_results']) figure_list = [] retriever_params = retriever_config - # retriever_params = state["chatbot_config"]["private_knowledge_config"] retriever_params["query"] = query or state[retriever_config.get("query_key","query")] - # retriever_params["query"] = query - # output: str = invoke_lambda( - # event_body=retriever_params, - # lambda_name="Online_Functions", - # lambda_module_path="functions.functions_utils.retriever.retriever", - # handler_name="lambda_handler", - # ) output = retrieve_fn(retriever_params) for doc in output["result"]["docs"]: @@ -34,7 +28,7 @@ def rag_tool(retriever_config:dict,query=None): unique_figure_list = [dict(t) for t in unique_set] state['extra_response']['figures'] = unique_figure_list - send_trace(f"\n\n**rag-contexts:** {context_list}", enable_trace=state["enable_trace"]) + send_trace(f"\n\n**rag-contexts:**\n\n {context_list}", enable_trace=state["enable_trace"]) group_name = state['chatbot_config']['group_name'] llm_config = state["chatbot_config"]["private_knowledge_config"]['llm_config'] @@ -47,23 +41,21 @@ def rag_tool(retriever_config:dict,query=None): chatbot_id=chatbot_id ) - output: str = invoke_lambda( - lambda_name="Online_LLM_Generate", - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name="lambda_handler", - event_body={ - "llm_config": { + llm_config = { **prompt_templates_from_ddb, **llm_config, "stream": state["stream"], "intent_type": task_type, - }, - "llm_input": { + } + + llm_input = { "contexts": context_list, "query": state["query"], - "chat_history": state["chat_history"], - }, - }, + "chat_history": state["chat_history"] + } + chain = LLMChain.get_chain( + **llm_config ) + output = chain.invoke(llm_input) return output diff --git a/source/lambda/online/lambda_main/main.py b/source/lambda/online/lambda_main/main.py index aca2226e..ba555bd4 100644 --- a/source/lambda/online/lambda_main/main.py +++ b/source/lambda/online/lambda_main/main.py @@ -2,6 +2,7 @@ import traceback import uuid from datetime import datetime, timezone +import traceback import boto3 from botocore.exceptions import ClientError @@ -16,6 +17,7 @@ from lambda_main.main_utils.online_entries import get_entry from common_logic.common_utils.response_utils import process_response + logger = get_logger("main") sessions_table_name = os.environ.get("SESSIONS_TABLE_NAME", "") @@ -368,5 +370,5 @@ def lambda_handler(event_body: dict, context: dict): except Exception as e: error_response = {"answer": str(e), "extra_response": {}} process_response(event_body, error_response) - logger.error(f"An error occurred: {str(e)}") + logger.error(f"{traceback.format_exc()}\nAn error occurred: {str(e)}") return {"error": str(e)} diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index c9236951..8512a644 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -1,7 +1,6 @@ -import json import traceback +import json from typing import Annotated, Any, TypedDict, List,Union -import copy from common_logic.common_utils.chatbot_utils import ChatbotManager from common_logic.common_utils.constant import ( @@ -21,7 +20,6 @@ from common_logic.common_utils.prompt_utils import get_prompt_templates_from_ddb from common_logic.common_utils.python_utils import add_messages, update_nest_dict from common_logic.common_utils.response_utils import process_response -from common_logic.common_utils.serialization_utils import JSONEncoder from common_logic.langchain_integration.tools import ToolManager from langchain_core.tools import BaseTool from langchain_core.messages.tool import ToolCall @@ -29,7 +27,6 @@ from common_logic.langchain_integration.chains import LLMChain from lambda_main.main_utils.parse_config import CommonConfigParser from langgraph.graph import END, StateGraph -from common_logic.langchain_integration.langgraph_integration import set_currrent_app from common_logic.langchain_integration.retrievers.retriever import lambda_handler as retrieve_fn from common_logic.common_utils.monitor_utils import ( format_preprocess_output, @@ -37,6 +34,9 @@ format_intention_output ) from lambda_intention_detection.intention import get_intention_results +from common_logic.langchain_integration.chains import LLMChain +from common_logic.common_utils.serialization_utils import JSONEncoder + logger = get_logger("common_entry") @@ -308,17 +308,13 @@ def agent(state: ChatbotState): **llm_config ) - - # print(state['chat_history'] + state['agent_tool_history']) agent_message:AIMessage = tool_calling_chain.invoke({ "query":state['query'], "chat_history":state['chat_history'], "agent_tool_history":state['agent_tool_history'] }) - send_trace( - # f"\n\n**agent_current_output:** \n{agent_message}\n\n **agent_current_call_number:** {agent_current_call_number}", f"\n\n**agent_current_output:** \n{agent_message}\n\n", state["stream"], state["ws_connection_id"] @@ -340,23 +336,23 @@ def llm_direct_results_generation(state: ChatbotState): ) logger.info(prompt_templates_from_ddb) - answer: dict = invoke_lambda( - event_body={ - "llm_config": { + llm_config = { **llm_config, "stream": state["stream"], "intent_type": task_type, **prompt_templates_from_ddb, - }, - "llm_input": { + } + + llm_input = { "query": state["query"], "chat_history": state["chat_history"], - }, - }, - lambda_name="Online_LLM_Generate", - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name="lambda_handler", + } + + chain = LLMChain.get_chain( + **llm_config ) + answer = chain.invoke(llm_input) + return {"answer": answer} @@ -371,7 +367,6 @@ def tool_execution(state): """ tools:List[BaseTool] = state['tools'] - def handle_tool_errors(e): content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) logger.error(f"Tool execution error:\n{traceback.format_exc()}") @@ -383,48 +378,12 @@ def handle_tool_errors(e): ) last_agent_message:AIMessage = state["agent_tool_history"][-1] - # print(last_agent_message) - # pass state to tools if needed - # tools_map = {tool.name:tool for tool in tools} tool_calls = last_agent_message.tool_calls - # tool_calls:List[ToolCall] = copy.deepcopy(last_agent_message.tool_calls) - - # for tool_call in tool_calls: - # tool = tools_map[tool_call['name']] - # if tool.pass_state: - # tool_call['args'].update({tool.pass_state_name:state}) tool_messages:List[ToolMessage] = tool_node.invoke( [AIMessage(content="",tool_calls=tool_calls)] ) - print("tool result",tool_messages[0].content) - - # tool_calls = state['function_calling_parsed_tool_calls'] - # assert len(tool_calls) == 1, tool_calls - # tool_call_results = [] - # for tool_call in tool_calls: - # tool_name = tool_call["name"] - # tool_kwargs = tool_call['kwargs'] - # # call tool - # output = invoke_lambda( - # event_body = { - # "tool_name":tool_name, - # "state":state, - # "kwargs":tool_kwargs - # }, - # lambda_name="Online_Tool_Execute", - # lambda_module_path="functions.lambda_tool", - # handler_name="lambda_handler" - # ) - # tool_call_results.append({ - # "name": tool_name, - # "output": output, - # "kwargs": tool_call['kwargs'], - # "model_id": tool_call['model_id'] - # }) - - # output = format_tool_call_results(tool_call['model_id'],tool_call_results) send_trace(f'**tool_execute_res:** \n{tool_messages}', enable_trace=state["enable_trace"]) return { "agent_tool_history": tool_messages, @@ -550,40 +509,20 @@ def build_graph(chatbot_state_cls): ##################################### # define online sub-graph for agent # ##################################### -# app_agent = None app = None -# def register_rag_tool( -# name: str, -# description: str, -# scene=SceneType.COMMON, -# lambda_name: str = "lambda_common_tools", -# ): -# tool_manager.register_tool( -# { -# "name": name, -# "scene": scene, -# "lambda_name": lambda_name, -# "lambda_module_path": rag.lambda_handler, -# "tool_def": { -# "name": name, -# "description": description, -# }, -# "running_mode": ToolRuningMode.ONCE, -# } -# ) - def register_rag_tool_from_config(event_body: dict): group_name = event_body.get("chatbot_config").get("group_name", "Admin") chatbot_id = event_body.get("chatbot_config").get("chatbot_id", "admin") chatbot_manager = ChatbotManager.from_environ() chatbot = chatbot_manager.get_chatbot(group_name, chatbot_id) - logger.info(chatbot) + logger.info(f"chatbot info: {chatbot}") registered_tool_names = [] for index_type, item_dict in chatbot.index_ids.items(): if index_type != IndexType.INTENTION: for index_content in item_dict["value"].values(): + if "indexId" in index_content and "description" in index_content: # Find retriever contain index_id retrievers = event_body["chatbot_config"]["private_knowledge_config"]['retrievers'] @@ -592,26 +531,54 @@ def register_rag_tool_from_config(event_body: dict): if retriever["index_name"] == index_content["indexId"]: break assert retriever is not None,retrievers - reranks = event_body["chatbot_config"]["private_knowledge_config"]['reranks'] - index_name = index_content["indexId"] + rerankers = event_body["chatbot_config"]["private_knowledge_config"]['rerankers'] + if rerankers: + rerankers = [rerankers[0]] + index_name = index_content["indexId"].replace("-","_") # TODO give specific retriever config ToolManager.register_common_rag_tool( retriever_config={ "retrievers":[retriever], - "reranks":[reranks[0]], + "rerankers":rerankers, "llm_config": event_body["chatbot_config"]["private_knowledge_config"]['llm_config'] }, - # event_body["chatbot_config"]["private_knowledge_config"], name=index_name, scene=SceneType.COMMON, - description=index_content["description"], - # pass_state=True, - # pass_state_name='state' + description=index_content["description"] ) registered_tool_names.append(index_name) return registered_tool_names +def register_custom_lambda_tools_from_config(event_body): + agent_config_tools = event_body['chatbot_config']['agent_config']['tools'] + new_agent_config_tools = [] + for tool in agent_config_tools: + if isinstance(tool,str): + new_agent_config_tools.append(tool) + elif isinstance(tool, dict): + tool_name = tool['name'] + assert tool_name not in new_agent_config_tools, f"repeat tool: {tool_name}\n{agent_config_tools}" + if "lambda_name" in tool: + ToolManager.register_aws_lambda_as_tool( + lambda_name=tool["lambda_name"], + tool_def={ + "description":tool["description"], + "properties":tool['properties'], + "required":tool.get('required',[]) + }, + name=tool_name, + scene=SceneType.COMMON, + return_direct=tool.get("return_direct",False) + ) + new_agent_config_tools.append(tool_name) + else: + raise ValueError(f"tool type {type(tool)}: {tool} is not supported") + + event_body['chatbot_config']['agent_config']['tools'] = new_agent_config_tools + return new_agent_config_tools + + def common_entry(event_body): """ Entry point for the Lambda function. @@ -622,17 +589,11 @@ def common_entry(event_body): if app is None: app = build_graph(ChatbotState) - # if app_agent is None: - # app_agent = build_agent_graph(ChatbotState) - # debuging if is_running_local(): with open("common_entry_workflow.png", "wb") as f: f.write(app.get_graph().draw_mermaid_png()) - # with open("common_entry_agent_workflow.png", "wb") as f: - # f.write(app_agent.get_graph().draw_mermaid_png()) - ################################################################################ # prepare inputs and invoke graph event_body["chatbot_config"] = CommonConfigParser.from_chatbot_config( @@ -650,22 +611,28 @@ def common_entry(event_body): agent_config = event_body["chatbot_config"]["agent_config"] # register as rag tool for each aos index + # print('private_knowledge_config',event_body["chatbot_config"]["private_knowledge_config"]) registered_tool_names = register_rag_tool_from_config(event_body) # update private knowledge tool to agent config for registered_tool_name in registered_tool_names: if registered_tool_name not in agent_config['tools']: agent_config['tools'].append(registered_tool_name) - # define all knowledge rag tool - print('private_knowledge_config',event_body["chatbot_config"]["private_knowledge_config"]) + + + # register lambda tools + register_custom_lambda_tools_from_config(event_body) + # + logger.info(f'event body to graph:\n{json.dumps(event_body,ensure_ascii=False,cls=JSONEncoder)}') + + + # define all knowledge rag tool all_knowledge_rag_tool = ToolManager.register_common_rag_tool( retriever_config=event_body["chatbot_config"]["private_knowledge_config"], name="all_knowledge_rag_tool", scene=SceneType.COMMON, description="all knowledge rag tool", - # pass_state=True, - # pass_state_name='state' ) # invoke graph and get results @@ -687,12 +654,11 @@ def common_entry(event_body): "last_tool_messages":None, "all_knowledge_rag_tool":all_knowledge_rag_tool, "tools":None, - # "agent_repeated_call_limit": chatbot_config["agent_repeated_call_limit"], - # "agent_current_call_number": 0, "ddb_additional_kwargs": {} }, config={"recursion_limit": 10} ) + # print('extra_response',response['extra_response']) return response["app_response"] diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index 67546ca1..f850bde5 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -139,38 +139,66 @@ def test_multi_turns_agent_pr(): }, ] - # default_index_names = { - # "intention":["pr_test-intention-default"], - # "qq_match": [], - # "private_knowledge": ['pr_test-qd-sso_poc'] - # } + default_index_names = { + "intention":[], + "qq_match": [], + "private_knowledge": [] + } # user_queries = [{ # "query": "今天天气怎么样", # "use_history": True, # "enable_trace": False # }] + # user_queries = [{ + # # "query": "199乘以98等于多少", + # "query": "1234乘以89878等于多少?", + # "use_history": True, + # "enable_trace": True + # }] + # user_queries = [{ + # "query": "199乘以98等于多少", + # # "query": "介绍一下MemGPT", + # "use_history": True, + # "enable_trace": True + # }] user_queries = [{ - # "query": "199乘以98等于多少", - "query": "1234乘以89878等于多少?", + "query": "”我爱北京天安门“包含多少个字符?", + # "query": "介绍一下MemGPT", "use_history": True, "enable_trace": True }] - default_index_names = { - "intention":[], - "qq_match": [], - "private_knowledge": [] - } + # default_index_names = { + # "intention":[], + # "qq_match": [], + # "private_knowledge": [] + # } default_llm_config = { - # 'model_id': 'anthropic.claude-3-sonnet-20240229-v1:0', + 'model_id': 'anthropic.claude-3-sonnet-20240229-v1:0', # 'model_id': "meta.llama3-1-70b-instruct-v1:0", # 'model_id':"mistral.mistral-large-2407-v1:0", - 'model_id':"cohere.command-r-plus-v1:0", + # 'model_id':"cohere.command-r-plus-v1:0", 'model_kwargs': { 'temperature': 0.1, 'max_tokens': 4096 } } + # agent_config={"tools":["python_repl"]} + agent_config={ + "tools":[{ + "lambda_name":"intelli-agent-lambda-tool-example1", + "name": "count_char", + "description": "Count the number of chars contained in a sentence.", + "properties": { + "phrase": { + "type": "string", + "description": "The phrase needs to count chars" + } + }, + "required": ["phrase"], + "return_direct":False + }] + } for query in user_queries: print("==" * 50) @@ -181,11 +209,11 @@ def test_multi_turns_agent_pr(): query=query['query'], use_history=query['use_history'], chatbot_id="admin", - group_name='admin', + group_name='Admin', only_use_rag_tool=False, default_index_names=default_index_names, enable_trace = query.get('enable_trace',True), - agent_config={"tools":["python_repl"]}, + agent_config=agent_config, default_llm_config=default_llm_config ) print() @@ -228,11 +256,9 @@ def complete_test_pr(): print("start test in agent mode") test_multi_turns_agent_pr() print("finish test in agent mode") - print("start test in rag mode") test_multi_turns_rag_pr() print("finish test in rag mode") - print("start test in chat mode") test_multi_turns_chat_pr() # print(srg) From bcc3742fff65f0da1ea7b253443d196d9b84443c Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 7 Nov 2024 09:37:17 +0000 Subject: [PATCH 14/29] fix bug in streaming --- .../chains/chat_chain.py | 24 +++++++++---------- .../langchain_integration/chains/rag_chain.py | 15 ++++++------ .../langchain_integration/tools/__init__.py | 3 ++- .../tools/common_tools/rag.py | 3 ++- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py index 434e592a..b27a6e6d 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py @@ -4,7 +4,7 @@ from langchain_core.messages import AIMessage,SystemMessage from langchain.prompts import ChatPromptTemplate,HumanMessagePromptTemplate from langchain_core.messages import convert_to_messages - +from langchain_core.output_parsers import StrOutputParser from ..chat_models import Model from . import LLMChain @@ -62,15 +62,14 @@ def create_chain(cls, model_kwargs=None, **kwargs): messages_template = ChatPromptTemplate.from_messages(messages) llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) chain = messages_template | RunnableLambda(lambda x: x.messages) + chain = chain | llm | StrOutputParser() + if stream: - chain = ( - chain | RunnableLambda(lambda messages: llm.stream(messages)) - | RunnableLambda(lambda x: (i.content for i in x)) - ) + final_chain = RunnableLambda(lambda x: chain.stream(x)) else: - chain = chain | llm | RunnableLambda(lambda x: x.content) + final_chain = RunnableLambda(lambda x: chain.invoke(x)) - return chain + return final_chain class Claude21ChatChain(Claude2ChatChain): @@ -334,15 +333,14 @@ def create_chain(cls, model_kwargs=None, **kwargs): messages_template = ChatPromptTemplate.from_messages(messages) llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) chain = messages_template | RunnableLambda(lambda x: x.messages) + chain = chain | llm | StrOutputParser() + if stream: - chain = ( - chain | RunnableLambda(lambda messages: llm.stream(messages)) - | RunnableLambda(lambda x: (i.content for i in x)) - ) + final_chain = RunnableLambda(lambda x: chain.stream(x)) else: - chain = chain | llm | RunnableLambda(lambda x: x.content) + final_chain = RunnableLambda(lambda x: chain.invoke(x)) - return chain + return final_chain class ChatGPT4ChatChain(ChatGPT35ChatChain): model_id = LLMModelType.CHATGPT_4_TURBO diff --git a/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py index 60c6b33b..d566cdea 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py @@ -4,6 +4,7 @@ HumanMessagePromptTemplate, SystemMessagePromptTemplate ) +from langchain_core.output_parsers import StrOutputParser from langchain.schema.runnable import RunnableLambda, RunnablePassthrough from common_logic.common_utils.constant import ( @@ -55,15 +56,15 @@ def create_chain(cls, model_kwargs=None, **kwargs): ) llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) chain = context_chain | ChatPromptTemplate.from_messages(chat_messages) | RunnableLambda(lambda x: print_llm_messages(f"rag messages: {x.messages}") or x) + + chain = chain | llm | StrOutputParser() + if stream: - chain = ( - chain - | RunnableLambda(lambda x: llm.stream(x.messages)) - | RunnableLambda(lambda x: (i.content for i in x)) - ) + final_chain = RunnableLambda(lambda x: chain.stream(x)) else: - chain = chain | llm | RunnableLambda(lambda x: x.content) - return chain + final_chain = RunnableLambda(lambda x: chain.invoke(x)) + + return final_chain class Claude21RagLLMChain(Claude2RagLLMChain): diff --git a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py index 7a704728..d669ad08 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py @@ -180,7 +180,8 @@ class Config: tool_def=RagModel ), description=description, - return_direct=return_direct + return_direct=return_direct, + response_format="content_and_artifact" ) return ToolManager.register_lc_tool( diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py index e318574e..1d35e710 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/rag.py @@ -53,9 +53,10 @@ def rag_tool(retriever_config:dict,query=None): "query": state["query"], "chat_history": state["chat_history"] } + chain = LLMChain.get_chain( **llm_config ) output = chain.invoke(llm_input) - return output + return output,output From de41e8070f57a177a7a7f1503377a8d4a1c142ad Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 7 Nov 2024 09:37:59 +0000 Subject: [PATCH 15/29] add new model in ui --- .../common_logic/common_utils/prompt_utils.py | 71 ------------------- .../common_utils/response_utils.py | 1 - source/portal/src/utils/const.ts | 3 + 3 files changed, 3 insertions(+), 72 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index 03bbed15..73e41d9f 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -389,77 +389,6 @@ def prompt_template_render(self, prompt_template: dict): ) -# AGENT_SYSTEM_PROMPT_LLAMA = """\ -# You are a helpful AI assistant. Today is {date},{weekday}. -# Here are some guidelines for you: -# -# - Always referece each answer with a reflection and write the reflection process in the tag. Please follow the steps below to think about it: -# 1. Determine whether the current context is sufficient to answer the user's question. -# 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. -# 3. If the current context is not sufficient to answer the user's question, you can consider calling the provided tools. -# 4. If the parameters of the tool you call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. If the tool does not require parameters, do not call the `give_rhetorical_question` tool. -# 5. Finally, output the name of the tool you want to call. -# - Always output with the same language as the content within . If the content is english, use english to output. If the content is chinese, use chinese to output. -# """ - -# register_prompt_templates( -# model_ids=[ -# LLMModelType.LLAMA3_1_70B_INSTRUCT, -# ], -# task_type=LLMTaskType.TOOL_CALLING_API, -# prompt_template=AGENT_SYSTEM_PROMPT, -# prompt_name="agent_system_prompt" -# ) - -# AGENT_GUIDELINES_PROMPT = """ -# - 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考:。 -# 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 -# 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用提供的工具。 -# 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。如果调用工具不需要参数,则不需要调用反问工具。 -# 5. 最后给出你要调用的工具名称。 -# - Always output with the same language as user's query. If the content is english, use englisth to output. If the content is Chinese, use Chinese to output. -# """ -# register_prompt_templates( -# model_ids=[ -# LLMModelType.CLAUDE_3_HAIKU, -# LLMModelType.CLAUDE_3_SONNET, -# LLMModelType.CLAUDE_3_5_SONNET, -# LLMModelType.LLAMA3_1_70B_INSTRUCT, -# LLMModelType.MISTRAL_LARGE_2407, -# LLMModelType.COHERE_COMMAND_R_PLUS, -# ], -# task_type=LLMTaskType.TOOL_CALLING_API, -# prompt_template=AGENT_USER_PROMPT, -# prompt_name="agent_prompt" -# ) - -# AGENT_GUIDELINES_PROMPT = """ -# - 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考: -# 1. 判断根据当前的上下文是否足够回答用户的问题。 -# 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 -# 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用提供的工具。 -# 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。如果调用工具不需要参数,则不需要调用反问工具。 -# 5. 最后给出你要调用的工具名称。 -# - Always output with the same language as user's query. If the content is english, use englisth to output. If the content is Chinese, use Chinese to output. -# -# """ - -# register_prompt_templates( -# model_ids=[ -# LLMModelType.CLAUDE_2, -# LLMModelType.CLAUDE_21, -# LLMModelType.CLAUDE_3_HAIKU, -# LLMModelType.CLAUDE_3_SONNET, -# LLMModelType.CLAUDE_3_5_SONNET, -# LLMModelType.LLAMA3_1_70B_INSTRUCT, -# LLMModelType.MISTRAL_LARGE_2407, -# LLMModelType.COHERE_COMMAND_R_PLUS, -# ], -# task_type=LLMTaskType.TOOL_CALLING_API, -# prompt_template=AGENT_GUIDELINES_PROMPT, -# prompt_name="guidelines_prompt" -# ) - TOOL_FEWSHOT_PROMPT = """\ Input: {query} Args: {args}""" diff --git a/source/lambda/online/common_logic/common_utils/response_utils.py b/source/lambda/online/common_logic/common_utils/response_utils.py index 5b94e8c1..fe54fe08 100644 --- a/source/lambda/online/common_logic/common_utils/response_utils.py +++ b/source/lambda/online/common_logic/common_utils/response_utils.py @@ -107,7 +107,6 @@ def stream_response(event_body:dict, response:dict): }, ws_connection_id=ws_connection_id ) - answer_str += chunk if log_first_token_time: diff --git a/source/portal/src/utils/const.ts b/source/portal/src/utils/const.ts index cb71514b..fe7b9ed6 100644 --- a/source/portal/src/utils/const.ts +++ b/source/portal/src/utils/const.ts @@ -24,6 +24,9 @@ export const LLM_BOT_MODEL_LIST = [ export const LLM_BOT_COMMON_MODEL_LIST = [ 'anthropic.claude-3-sonnet-20240229-v1:0', 'anthropic.claude-3-haiku-20240307-v1:0', + 'meta.llama3-1-70b-instruct-v1:0', + 'mistral.mistral-large-2407-v1:0', + 'cohere.command-r-plus-v1:0' // 'anthropic.claude-3-5-sonnet-20240620-v1:0', ]; From c94f7b17e92b02b1dcfa96ddf3e78a2f947886b7 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 7 Nov 2024 10:29:06 +0000 Subject: [PATCH 16/29] modify logger, fix bug about inaccurate filename output --- .../common_logic/common_utils/logger_utils.py | 17 +++++------------ .../main_utils/online_entries/common_entry.py | 4 +++- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/logger_utils.py b/source/lambda/online/common_logic/common_utils/logger_utils.py index 5216c8ef..11845942 100644 --- a/source/lambda/online/common_logic/common_utils/logger_utils.py +++ b/source/lambda/online/common_logic/common_utils/logger_utils.py @@ -9,15 +9,13 @@ logger_lock = threading.Lock() -def cloud_print_wrapper(fn): - @wraps(fn) - def _inner(msg, *args, **kwargs): +class CloudStreamHandler(logging.StreamHandler): + def emit(self, record): from common_logic.common_utils.lambda_invoke_utils import is_running_local if not is_running_local: # enable multiline as one message in cloudwatch - msg = msg.replace("\n", "\r") - return fn(msg, *args, **kwargs) - return _inner + record.msg = record.msg.replace("\n", "\r") + return super().emit(record) class Logger: @@ -36,16 +34,11 @@ def _get_logger( logger = logging.getLogger(name) logger.propagate = 0 # Create a handler - c_handler = logging.StreamHandler() + c_handler = CloudStreamHandler() formatter = logging.Formatter(format, datefmt=datefmt) c_handler.setFormatter(formatter) logger.addHandler(c_handler) logger.setLevel(level) - logger.info = cloud_print_wrapper(logger.info) - logger.error = cloud_print_wrapper(logger.error) - logger.warning = cloud_print_wrapper(logger.warning) - logger.critical = cloud_print_wrapper(logger.critical) - logger.debug = cloud_print_wrapper(logger.debug) cls.logger_map[name] = logger return logger diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index c23d2f3d..b6d8ac26 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -526,6 +526,7 @@ def register_rag_tool_from_config(event_body: dict): if rerankers: rerankers = [rerankers[0]] index_name = index_content["indexId"].replace("-","_") + description = index_content["description"] # TODO give specific retriever config ToolManager.register_common_rag_tool( retriever_config={ @@ -535,10 +536,11 @@ def register_rag_tool_from_config(event_body: dict): }, name=index_name, scene=SceneType.COMMON, - description=index_content["description"], + description=description, return_direct=True ) registered_tool_names.append(index_name) + logger.info(f"registered rag tool: {index_name}, description: {description}") return registered_tool_names From 406a72f91786816a0a1f6b0ab6ec08b9de84a08b Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 7 Nov 2024 13:37:26 +0000 Subject: [PATCH 17/29] remove llm_generate_utils --- .../{llm_generate_utils => __llm_generate_utils}/__init__.py | 0 .../llm_chains/__init__.py | 0 .../llm_chains/chat_chain.py | 2 +- .../llm_chains/conversation_summary_chain.py | 0 .../llm_chains/hyde_chain.py | 0 .../llm_chains/intention_chain.py | 0 .../llm_chains/llm_chain_base.py | 0 .../llm_chains/marketing_chains/__init__.py | 0 .../llm_chains/marketing_chains/mkt_conversation_summary.py | 0 .../llm_chains/marketing_chains/mkt_rag_chain.py | 0 .../llm_chains/query_rewrite_chain.py | 0 .../llm_chains/rag_chain.py | 0 .../llm_chains/retail_chains/__init__.py | 0 .../llm_chains/retail_chains/auto_evaluation_chain.py | 0 .../retail_chains/retail_conversation_summary_chain.py | 0 .../retail_chains/retail_tool_calling_chain_claude_xml.py | 0 .../llm_chains/retail_chains/retail_tool_calling_chain_json.py | 0 .../llm_chains/stepback_chain.py | 0 .../llm_chains/tool_calling_chain_claude_xml.py | 0 .../llm_chains/translate_chain.py | 0 .../{llm_generate_utils => __llm_generate_utils}/llm_models.py | 0 source/lambda/online/lambda_llm_generate/llm_generate.py | 2 +- 22 files changed, 2 insertions(+), 2 deletions(-) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/__init__.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/__init__.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/chat_chain.py (99%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/conversation_summary_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/hyde_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/intention_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/llm_chain_base.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/marketing_chains/__init__.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/marketing_chains/mkt_conversation_summary.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/marketing_chains/mkt_rag_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/query_rewrite_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/rag_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/retail_chains/__init__.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/retail_chains/auto_evaluation_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/retail_chains/retail_conversation_summary_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/retail_chains/retail_tool_calling_chain_json.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/stepback_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/tool_calling_chain_claude_xml.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_chains/translate_chain.py (100%) rename source/lambda/online/lambda_llm_generate/{llm_generate_utils => __llm_generate_utils}/llm_models.py (100%) diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/__init__.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/__init__.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/__init__.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/__init__.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/__init__.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/__init__.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/chat_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/chat_chain.py similarity index 99% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/chat_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/chat_chain.py index 730a8490..44a51542 100644 --- a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/chat_chain.py +++ b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/chat_chain.py @@ -300,7 +300,7 @@ class Qwen2Instruct72BChatChain(Qwen2Instruct7BChatChain): class ChatGPT35ChatChain(LLMChain): - model_id = LLMModelType.CHATGPT_35_TURBO + model_id = LLMModelType.CHATGPT_35_TURBO_0125 intent_type = LLMTaskType.CHAT @classmethod diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/conversation_summary_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/conversation_summary_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/conversation_summary_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/conversation_summary_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/hyde_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/hyde_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/hyde_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/hyde_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/intention_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/intention_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/intention_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/intention_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/llm_chain_base.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/llm_chain_base.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/llm_chain_base.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/llm_chain_base.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/marketing_chains/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/__init__.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/marketing_chains/__init__.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/__init__.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/query_rewrite_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/query_rewrite_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/query_rewrite_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/query_rewrite_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/rag_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/rag_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/rag_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/rag_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/__init__.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/__init__.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/__init__.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/stepback_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/stepback_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/stepback_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/stepback_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/translate_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/translate_chain.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_chains/translate_chain.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/translate_chain.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_models.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_models.py similarity index 100% rename from source/lambda/online/lambda_llm_generate/llm_generate_utils/llm_models.py rename to source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_models.py diff --git a/source/lambda/online/lambda_llm_generate/llm_generate.py b/source/lambda/online/lambda_llm_generate/llm_generate.py index 38408ce5..0ce1506c 100644 --- a/source/lambda/online/lambda_llm_generate/llm_generate.py +++ b/source/lambda/online/lambda_llm_generate/llm_generate.py @@ -1,5 +1,5 @@ from common_logic.common_utils.logger_utils import get_logger -from lambda_llm_generate.llm_generate_utils import LLMChain +from common_logic.langchain_integration.chains import LLMChain from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper logger = get_logger("llm_generate") From 2442434f22823f68002146eaa91e8105959c3b3b Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 7 Nov 2024 13:48:13 +0000 Subject: [PATCH 18/29] add CLAUDE_3_5_SONNET_V2 and CLAUDE_3_5_HAIKU models --- .../common_logic/common_utils/constant.py | 2 ++ .../common_logic/common_utils/prompt_utils.py | 20 ++++++++++++- .../chains/chat_chain.py | 10 ++++++- .../chains/conversation_summary_chain.py | 8 ++++++ .../langchain_integration/chains/rag_chain.py | 7 +++++ .../chains/tool_calling_chain_api.py | 8 ++++++ .../chat_models/__init__.py | 4 +++ .../chat_models/bedrock_models.py | 8 ++++++ .../langchain_integration/tools/__init__.py | 4 ++- .../main_utils/online_entries/common_entry.py | 23 +++++++++++---- .../test/main_local_test_common.py | 28 +++++++++++++++---- .../query_preprocess.py | 2 +- source/portal/src/utils/const.ts | 4 +-- 13 files changed, 111 insertions(+), 17 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/constant.py b/source/lambda/online/common_logic/common_utils/constant.py index 208f464e..37f5a1cc 100644 --- a/source/lambda/online/common_logic/common_utils/constant.py +++ b/source/lambda/online/common_logic/common_utils/constant.py @@ -128,8 +128,10 @@ class LLMModelType(ConstantBase): CLAUDE_2 = "anthropic.claude-v2" CLAUDE_21 = "anthropic.claude-v2:1" CLAUDE_3_HAIKU = "anthropic.claude-3-haiku-20240307-v1:0" + CLAUDE_3_5_HAIKU = "anthropic.claude-3-5-haiku-20241022-v1:0" CLAUDE_3_SONNET = "anthropic.claude-3-sonnet-20240229-v1:0" CLAUDE_3_5_SONNET = "anthropic.claude-3-5-sonnet-20240620-v1:0" + CLAUDE_3_5_SONNET_V2 = "anthropic.claude-3-5-sonnet-20241022-v2:0" MIXTRAL_8X7B_INSTRUCT = "mistral.mixtral-8x7b-instruct-v0:1" BAICHUAN2_13B_CHAT = "Baichuan2-13B-Chat-4bits" INTERNLM2_CHAT_7B = "internlm2-chat-7b" diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index 73e41d9f..3d020404 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -17,6 +17,8 @@ EXPORT_MODEL_IDS = [ LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, + LLMModelType.CLAUDE_3_5_HAIKU, + LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.LLAMA3_1_70B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS @@ -143,6 +145,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.CLAUDE_3_5_HAIKU, + LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.CLAUDE_INSTANCE, LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.LLAMA3_1_70B_INSTRUCT, @@ -257,6 +261,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.CLAUDE_3_5_HAIKU, + LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.CLAUDE_INSTANCE, LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.QWEN2INSTRUCT72B, @@ -279,6 +285,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.CLAUDE_3_5_HAIKU, + LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.CLAUDE_INSTANCE, LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.QWEN2INSTRUCT72B, @@ -301,6 +309,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.CLAUDE_3_5_HAIKU, + LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.CLAUDE_INSTANCE, LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.QWEN2INSTRUCT72B, @@ -326,6 +336,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.CLAUDE_3_5_HAIKU, + LLMModelType.CLAUDE_3_5_SONNET_V2, ], task_type=LLMTaskType.TOOL_CALLING_XML, prompt_template=AGENT_USER_PROMPT, @@ -351,6 +363,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.CLAUDE_3_5_HAIKU, + LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.LLAMA3_1_70B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS, @@ -369,7 +383,7 @@ def prompt_template_render(self, prompt_template: dict): 1. Determine whether the current context is sufficient to answer the user's question. 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. 3. If the current context is not sufficient to answer the user's question, you can consider calling the provided tools. - 4. If the parameters of the tool you call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. If the tool does not require parameters, do not call the `give_rhetorical_question` tool. + 4. If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. If the tool does not require parameters, do not call the `give_rhetorical_question` tool. 5. Finally, output the name of the tool you want to call. - Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. """ @@ -379,6 +393,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, LLMModelType.CLAUDE_3_5_SONNET, + LLMModelType.CLAUDE_3_5_SONNET_V2, + LLMModelType.CLAUDE_3_5_HAIKU, LLMModelType.LLAMA3_1_70B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS, @@ -399,6 +415,8 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_21, LLMModelType.CLAUDE_3_HAIKU, LLMModelType.CLAUDE_3_SONNET, + LLMModelType.CLAUDE_3_5_SONNET_V2, + LLMModelType.CLAUDE_3_5_HAIKU, LLMModelType.CLAUDE_3_5_SONNET, LLMModelType.LLAMA3_1_70B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, diff --git a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py index b27a6e6d..e4732576 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py @@ -89,7 +89,15 @@ class Claude3HaikuChatChain(Claude2ChatChain): class Claude35SonnetChatChain(Claude2ChatChain): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" + model_id = LLMModelType.CLAUDE_3_5_SONNET + + +class Claude35SonnetV2ChatChain(Claude2ChatChain): + model_id = LLMModelType.CLAUDE_3_5_SONNET_V2 + + +class Claude35HaikuChatChain(Claude2ChatChain): + model_id = LLMModelType.CLAUDE_3_5_HAIKU class Mixtral8x7bChatChain(Claude2ChatChain): diff --git a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py index 80ee00b2..24d5b89b 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py @@ -196,6 +196,14 @@ class Claude3HaikuConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.CLAUDE_3_HAIKU +class Claude35HaikuConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_5_HAIKU + + +class Claude3SonnetV2ConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_5_SONNET_V2 + + class Mixtral8x7bConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT default_model_kwargs = {"max_tokens": 4096, "temperature": 0.01} diff --git a/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py index d566cdea..b55a6074 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py @@ -86,6 +86,13 @@ class Claude35SonnetRAGLLMChain(Claude2RagLLMChain): model_id = LLMModelType.CLAUDE_3_5_SONNET +class Claude35SonnetV2RAGLLMChain(Claude2RagLLMChain): + model_id = LLMModelType.CLAUDE_3_5_SONNET_V2 + +class Claude35HaikuRAGLLMChain(Claude2RagLLMChain): + model_id = LLMModelType.CLAUDE_3_5_HAIKU + + class Llama31Instruct70B(Claude2RagLLMChain): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT diff --git a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py index 27e96c72..7796971d 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py @@ -156,6 +156,14 @@ class Claude35SonnetToolCallingChain(Claude2ToolCallingChain): model_id = LLMModelType.CLAUDE_3_5_SONNET +class Claude35SonnetV2ToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_5_SONNET_V2 + + +class Claude35HaikuToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.CLAUDE_3_5_HAIKU + + class Llama31Instruct70BToolCallingChain(Claude2ToolCallingChain): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py index 56ee8dd5..4658616f 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py @@ -58,6 +58,8 @@ def _import_bedrock_models(): Claude3Sonnet, Claude3Haiku, Claude35Sonnet, + Claude35Haiku, + Claude35SonnetV2, MistralLarge2407, Llama3d1Instruct70B, CohereCommandRPlus @@ -89,6 +91,8 @@ def _load_module(model_id): LLMModelType.LLAMA3_1_70B_INSTRUCT:_import_bedrock_models, LLMModelType.MISTRAL_LARGE_2407:_import_bedrock_models, LLMModelType.COHERE_COMMAND_R_PLUS:_import_bedrock_models, + LLMModelType.CLAUDE_3_5_SONNET_V2:_import_bedrock_models, + LLMModelType.CLAUDE_3_5_HAIKU:_import_bedrock_models, } diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py index 4c82373f..8acc0c3e 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py @@ -67,6 +67,14 @@ class Claude35Sonnet(Claude2): model_id = LLMModelType.CLAUDE_3_5_SONNET +class Claude35SonnetV2(Claude2): + model_id = LLMModelType.CLAUDE_3_5_SONNET_V2 + + +class Claude35Haiku(Claude2): + model_id = LLMModelType.CLAUDE_3_5_HAIKU + + class MistralLarge2407(Claude2): model_id = LLMModelType.MISTRAL_LARGE_2407 diff --git a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py index d669ad08..95a38fb0 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/__init__.py @@ -7,6 +7,7 @@ import types from datamodel_code_generator import DataModelType, PythonVersion +from datamodel_code_generator.format import DatetimeClassType from datamodel_code_generator.model import get_data_model_types from datamodel_code_generator.parser.jsonschema import JsonSchemaParser from langchain.tools.base import StructuredTool as _StructuredTool ,BaseTool @@ -39,7 +40,8 @@ def convert_tool_def_to_pydantic(tool_id,tool_def:Union[dict,BaseModel]): current_python_version = ".".join(platform.python_version().split(".")[:-1]) data_model_types = get_data_model_types( DataModelType.PydanticBaseModel, - target_python_version=PythonVersion(current_python_version) + target_python_version=PythonVersion(current_python_version), + target_datetime_class=DatetimeClassType.Datetime ) parser = JsonSchemaParser( json.dumps(tool_def,ensure_ascii=False,indent=2), diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index b6d8ac26..db2f3396 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -36,6 +36,7 @@ format_intention_output ) from lambda_intention_detection.intention import get_intention_results +from lambda_query_preprocess.query_preprocess import conversation_query_rewrite from common_logic.langchain_integration.chains import LLMChain from common_logic.common_utils.serialization_utils import JSONEncoder @@ -125,11 +126,23 @@ class ChatbotState(TypedDict): @node_monitor_wrapper def query_preprocess(state: ChatbotState): - output: str = invoke_lambda( - event_body=state, - lambda_name="Online_Query_Preprocess", - lambda_module_path="lambda_query_preprocess.query_preprocess", - handler_name="lambda_handler", + + # output: str = invoke_lambda( + # event_body=state, + # lambda_name="Online_Query_Preprocess", + # lambda_module_path="lambda_query_preprocess.query_preprocess", + # handler_name="lambda_handler", + # ) + + + query_rewrite_llm_type = state.get("query_rewrite_llm_type",None) or LLMTaskType.CONVERSATION_SUMMARY_TYPE + output = conversation_query_rewrite( + query=state['query'], + chat_history=state['chat_history'], + message_id=state['message_id'], + trace_infos=state['trace_infos'], + chatbot_config=state['chatbot_config'], + query_rewrite_llm_type=query_rewrite_llm_type ) preprocess_md = format_preprocess_output(state["query"], output) diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index f850bde5..368f1d94 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -161,12 +161,24 @@ def test_multi_turns_agent_pr(): # "use_history": True, # "enable_trace": True # }] - user_queries = [{ - "query": "”我爱北京天安门“包含多少个字符?", + user_queries = [ + { + # "query": "”我爱北京天安门“包含多少个字符?", + # "query": "11133乘以97892395等于多少", # 1089836033535 + "query": "今天天气怎么样?", + # "query": "介绍一下MemGPT", + "use_history": True, + "enable_trace": True + }, + { + # "query": "”我爱北京天安门“包含多少个字符?", + # "query": "11133乘以97892395等于多少", # 1089836033535 + "query": "我在上海", # "query": "介绍一下MemGPT", "use_history": True, "enable_trace": True - }] + }, + ] # default_index_names = { # "intention":[], @@ -174,7 +186,8 @@ def test_multi_turns_agent_pr(): # "private_knowledge": [] # } default_llm_config = { - 'model_id': 'anthropic.claude-3-sonnet-20240229-v1:0', + # 'model_id': "anthropic.claude-3-5-sonnet-20240620-v1:0", + 'model_id': "anthropic.claude-3-5-haiku-20241022-v1:0", # 'model_id': "meta.llama3-1-70b-instruct-v1:0", # 'model_id':"mistral.mistral-large-2407-v1:0", # 'model_id':"cohere.command-r-plus-v1:0", @@ -185,7 +198,8 @@ def test_multi_turns_agent_pr(): } # agent_config={"tools":["python_repl"]} agent_config={ - "tools":[{ + "tools":[ + { "lambda_name":"intelli-agent-lambda-tool-example1", "name": "count_char", "description": "Count the number of chars contained in a sentence.", @@ -197,7 +211,9 @@ def test_multi_turns_agent_pr(): }, "required": ["phrase"], "return_direct":False - }] + }, + "python_repl" + ] } for query in user_queries: diff --git a/source/lambda/online/lambda_query_preprocess/query_preprocess.py b/source/lambda/online/lambda_query_preprocess/query_preprocess.py index 56fba45d..de9dab40 100644 --- a/source/lambda/online/lambda_query_preprocess/query_preprocess.py +++ b/source/lambda/online/lambda_query_preprocess/query_preprocess.py @@ -39,7 +39,7 @@ def conversation_query_rewrite(query:str, chat_history:list, message_id:str, tra chatbot_id=chatbot_id ) logger.info(f'conversation summary prompt templates: {prompt_templates_from_ddb}') - + cqr_llm_chain = RunnableLambda(lambda x: invoke_lambda( lambda_name='Online_LLM_Generate', lambda_module_path="lambda_llm_generate.llm_generate", diff --git a/source/portal/src/utils/const.ts b/source/portal/src/utils/const.ts index fe7b9ed6..b30fd0da 100644 --- a/source/portal/src/utils/const.ts +++ b/source/portal/src/utils/const.ts @@ -22,8 +22,8 @@ export const LLM_BOT_MODEL_LIST = [ ]; export const LLM_BOT_COMMON_MODEL_LIST = [ - 'anthropic.claude-3-sonnet-20240229-v1:0', - 'anthropic.claude-3-haiku-20240307-v1:0', + 'anthropic.claude-3-5-sonnet-20240620-v1:0', + 'anthropic.claude-3-5-haiku-20241022-v1:0', 'meta.llama3-1-70b-instruct-v1:0', 'mistral.mistral-large-2407-v1:0', 'cohere.command-r-plus-v1:0' From 882e17a1e07db1ac4be7a3f58f079e51dc12a47d Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 7 Nov 2024 13:48:40 +0000 Subject: [PATCH 19/29] modify online requirements --- .../chains/query_rewrite_chain.py | 10 +++++++++- source/lambda/online/requirements.txt | 12 +++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py index 9379b84e..05c5efe0 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py @@ -86,7 +86,15 @@ class Claude3SonnetQueryRewriteChain(Claude2QueryRewriteChain): class Claude35SonnetQueryRewriteChain(Claude2QueryRewriteChain): - mdoel_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" + mdoel_id = LLMModelType.CLAUDE_3_5_SONNET + + +class Claude35SonnetV2QueryRewriteChain(Claude2QueryRewriteChain): + mdoel_id = LLMModelType.CLAUDE_3_5_SONNET_V2 + + +class Claude35HaikuQueryRewriteChain(Claude2QueryRewriteChain): + mdoel_id = LLMModelType.CLAUDE_3_5_HAIKU internlm2_meta_instruction = """You are an AI language model assistant. Your task is to generate 1 - 5 different sub questions OR alternate versions of the given user question to retrieve relevant documents from a vector database. diff --git a/source/lambda/online/requirements.txt b/source/lambda/online/requirements.txt index 030f6808..2795b8c1 100644 --- a/source/lambda/online/requirements.txt +++ b/source/lambda/online/requirements.txt @@ -1,8 +1,8 @@ -langchain==0.2.4 +langchain-aws==0.2.6 httpx==0.26.0 -langgraph==0.0.68 -langchain_openai==0.1.8 -langchain_community==0.2.4 +langgraph==0.2.43 +langchain_openai==0.2.6 +langchain-community==0.3.5 langchainhub==0.1.14 opensearch-py==2.2.0 requests_aws4auth==1.2.2 @@ -12,4 +12,6 @@ beautifulsoup4==4.12.2 validators==0.28.3 openpyxl==3.1.3 xlrd==2.0.1 -pydantic==1.10.17 +pydantic==2.9.2 +datamodel-code-generator==0.26.2 +langchain_experimental==0.3.3 \ No newline at end of file From 57f5fdcb70136ab7a5fbe14a84127ff731fe1f68 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Thu, 7 Nov 2024 14:59:07 +0000 Subject: [PATCH 20/29] add enable_prefill parameter;optimize prompt --- .../common_logic/common_utils/prompt_utils.py | 10 ++++----- .../chains/conversation_summary_chain.py | 22 +++++++++++++------ .../chat_models/__init__.py | 1 + .../chat_models/bedrock_models.py | 7 +++++- .../tools/common_tools/__init__.py | 4 ++-- .../test/main_local_test_common.py | 6 ++--- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index 3d020404..f2ff08e3 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -376,18 +376,18 @@ def prompt_template_render(self, prompt_template: dict): ################# api agent prompt ##################### AGENT_SYSTEM_PROMPT = """\ -You are a helpful AI assistant. Today is {date},{weekday}. +You are a helpful and honest AI assistant. Today is {date},{weekday}. Here are some guidelines for you: -- Here are some tips for tool use: +- Here are steps for you to decide to use which tool: 1. Determine whether the current context is sufficient to answer the user's question. 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. - 3. If the current context is not sufficient to answer the user's question, you can consider calling the provided tools. - 4. If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. If the tool does not require parameters, do not call the `give_rhetorical_question` tool. - 5. Finally, output the name of the tool you want to call. + 3. If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. + 4. If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. - Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. """ +# - Output your thinking before to call one tool. register_prompt_templates( model_ids=[ LLMModelType.CLAUDE_3_HAIKU, diff --git a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py index 24d5b89b..81e60fcd 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py @@ -137,6 +137,7 @@ def create_messages_inputs(cls,x:dict,user_prompt,few_shots:list[dict]): @classmethod def create_messages_chain(cls,**kwargs): + enable_prefill = kwargs['enable_prefill'] system_prompt = get_prompt_template( model_id=cls.model_id, task_type=cls.intent_type, @@ -157,13 +158,17 @@ def create_messages_chain(cls,**kwargs): system_prompt = kwargs.get("system_prompt", system_prompt) user_prompt = kwargs.get('user_prompt', user_prompt) + - cqr_template = ChatPromptTemplate.from_messages([ + messages = [ SystemMessage(content=system_prompt), ('placeholder','{few_shots}'), - HumanMessagePromptTemplate.from_template(user_prompt), - AIMessage(content=cls.prefill) - ]) + HumanMessagePromptTemplate.from_template(user_prompt) + ] + if enable_prefill: + messages.append(AIMessage(content=cls.prefill)) + + cqr_template = ChatPromptTemplate.from_messages(messages) return RunnableLambda(lambda x: cls.create_messages_inputs(x,user_prompt=user_prompt,few_shots=json.loads(few_shots))) | cqr_template @classmethod @@ -174,9 +179,9 @@ def create_chain(cls, model_kwargs=None, **kwargs): model_id=cls.model_id, model_kwargs=model_kwargs, ) - messages_chain = cls.create_messages_chain(**kwargs) + messages_chain = cls.create_messages_chain(**kwargs,enable_prefill=llm.enable_prefill) chain = messages_chain | RunnableLambda(lambda x: print_llm_messages(f"conversation summary messages: {x.messages}") or x.messages) \ - | llm | RunnableLambda(lambda x: x.content) + | llm | RunnableLambda(lambda x: x.content.replace(cls.prefill,"").strip()) return chain @@ -200,7 +205,10 @@ class Claude35HaikuConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.CLAUDE_3_5_HAIKU -class Claude3SonnetV2ConversationSummaryChain(Claude2ConversationSummaryChain): +class Claude35SonnetConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.CLAUDE_3_5_SONNET + +class Claude35SonnetV2ConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.CLAUDE_3_5_SONNET_V2 diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py index 4658616f..ac930c10 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py @@ -38,6 +38,7 @@ def __new__(cls, name, bases, attrs): class Model(ModeMixins,metaclass=ModelMeta): model_id: str = None enable_auto_tool_choice: bool = True + enable_prefill: bool = True model_map = {} @classmethod diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py index 8acc0c3e..e5b68ddc 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py @@ -11,7 +11,8 @@ class ChatBedrockConverse(_ChatBedrockConverse): - enable_auto_tool_choice: bool = True + enable_auto_tool_choice: bool = False + enable_prefill: bool = True # Bedrock model type @@ -40,6 +41,7 @@ def create_model(cls, model_kwargs=None, **kwargs): region_name=region_name, model=cls.model_id, enable_auto_tool_choice=cls.enable_auto_tool_choice, + enable_prefill=cls.enable_prefill, **model_kwargs, ) llm.client.converse_stream = llm_messages_print_decorator(llm.client.converse_stream) @@ -77,16 +79,19 @@ class Claude35Haiku(Claude2): class MistralLarge2407(Claude2): model_id = LLMModelType.MISTRAL_LARGE_2407 + enable_prefill = False class Llama3d1Instruct70B(Claude2): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT enable_auto_tool_choice = False + enable_prefill = False class CohereCommandRPlus(Claude2): model_id = LLMModelType.COHERE_COMMAND_R_PLUS enable_auto_tool_choice = False + enable_prefill = False diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py index b3f5aa15..bc237b3f 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py @@ -9,7 +9,7 @@ def _load_weather_tool(tool_identifier:ToolIdentifier): "description": "Get the current weather for `city_name`", "properties": { "city_name": { - "description": "The name of the city to be queried", + "description": "The name of the city", "type": "string" }, }, @@ -28,7 +28,7 @@ def _load_weather_tool(tool_identifier:ToolIdentifier): def _load_rhetorical_tool(tool_identifier:ToolIdentifier): from . import give_rhetorical_question tool_def = { - "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", + "description": "This tool is designed to handle the scenario when required parameters are missing from other tools. It prompts the user to provide the necessary information, ensuring that all essential parameters are collected before proceeding. This tools enhances user interaction by clarifying what is needed and improving the overall usability of the application.", "properties": { "question": { "description": "The rhetorical question to user", diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index 368f1d94..1ff248b5 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -187,12 +187,12 @@ def test_multi_turns_agent_pr(): # } default_llm_config = { # 'model_id': "anthropic.claude-3-5-sonnet-20240620-v1:0", - 'model_id': "anthropic.claude-3-5-haiku-20241022-v1:0", + # 'model_id': "anthropic.claude-3-5-haiku-20241022-v1:0", # 'model_id': "meta.llama3-1-70b-instruct-v1:0", # 'model_id':"mistral.mistral-large-2407-v1:0", - # 'model_id':"cohere.command-r-plus-v1:0", + 'model_id':"cohere.command-r-plus-v1:0", 'model_kwargs': { - 'temperature': 0.1, + 'temperature': 0.01, 'max_tokens': 4096 } } From ec997bafc6640df10f33c4a3dd81dd37b41e9c7d Mon Sep 17 00:00:00 2001 From: zhouxss Date: Fri, 8 Nov 2024 05:55:17 +0000 Subject: [PATCH 21/29] modify PythonREPL, fix bug running on lambda --- .../common_logic/common_utils/prompt_utils.py | 29 +++++++- .../chains/tool_calling_chain_api.py | 2 +- .../tools/common_tools/__init__.py | 66 ++++++++++++++++++- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index f2ff08e3..bb634a41 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -385,6 +385,7 @@ def prompt_template_render(self, prompt_template: dict): 3. If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. 4. If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. - Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. +- Always call one tool at a time. """ # - Output your thinking before to call one tool. @@ -397,7 +398,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_5_HAIKU, LLMModelType.LLAMA3_1_70B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, - LLMModelType.COHERE_COMMAND_R_PLUS, + # LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.TOOL_CALLING_API, prompt_template=AGENT_SYSTEM_PROMPT, @@ -405,6 +406,32 @@ def prompt_template_render(self, prompt_template: dict): ) + +AGENT_SYSTEM_PROMPT_COHERE = """\ +You are a helpful and honest AI assistant. Today is {date},{weekday}. +You need to carefully understand the user's intent and then invoke the tool to reply. + +## Tool use guidelines +Here are steps for you to decide to use which tool: +* Determine whether the current context is sufficient to answer the user's question. +* If the current context is sufficient to answer the user's question, call the `give_final_response` tool. +* If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. +* If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. + +## Reply rules +* Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. +* Always call one tool at a time.""" + +register_prompt_templates( + model_ids=[ + LLMModelType.COHERE_COMMAND_R_PLUS + ], + task_type=LLMTaskType.TOOL_CALLING_API, + prompt_template=AGENT_SYSTEM_PROMPT_COHERE, + prompt_name="agent_system_prompt" +) + + TOOL_FEWSHOT_PROMPT = """\ Input: {query} Args: {args}""" diff --git a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py index 7796971d..99713e0e 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py @@ -126,7 +126,7 @@ def create_chain(cls, model_kwargs=None, **kwargs): ) llm = cls.bind_tools(llm,tools,fewshot_examples,fewshot_template=tool_fewshot_prompt) - + tool_calling_template = ChatPromptTemplate.from_messages( [ SystemMessage(content=agent_system_prompt), diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py index bc237b3f..c9b520c4 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py @@ -1,7 +1,12 @@ +from typing import Optional,Dict,Any +import sys +from io import StringIO + from .. import lazy_tool_load_decorator,ToolIdentifier,ToolManager from common_logic.common_utils.constant import SceneType + @lazy_tool_load_decorator(SceneType.COMMON,"get_weather") def _load_weather_tool(tool_identifier:ToolIdentifier): from . import get_weather @@ -9,7 +14,7 @@ def _load_weather_tool(tool_identifier:ToolIdentifier): "description": "Get the current weather for `city_name`", "properties": { "city_name": { - "description": "The name of the city", + "description": "The name of the city. If the city name does not appear visibly in the user's response, please call the `give_rhetorical_question` to ask for city name.", "type": "string" }, }, @@ -118,7 +123,64 @@ def _load_rag_tool(tool_identifier:ToolIdentifier): @lazy_tool_load_decorator(SceneType.COMMON,"python_repl") def _loadd_python_repl_tool(tool_identifier:ToolIdentifier): from langchain_core.tools import Tool - from langchain_experimental.utilities import PythonREPL + from langchain_experimental.utilities import PythonREPL as _PythonREPL + from langchain_experimental.utilities.python import warn_once + import multiprocessing + + + # modify LangChain's PythonREPL to adapt aws lambda, + # where it's execution environment not having /dev/shm + class PythonREPL(_PythonREPL): + @classmethod + def worker( + cls, + command: str, + globals: Optional[Dict], + locals: Optional[Dict], + conn: Any, + ) -> None: + old_stdout = sys.stdout + sys.stdout = mystdout = StringIO() + try: + cleaned_command = cls.sanitize_input(command) + exec(cleaned_command, globals, locals) + sys.stdout = old_stdout + conn.send(mystdout.getvalue()) + except Exception as e: + sys.stdout = old_stdout + conn.send(repr(e)) + conn.close() + def run(self, command: str, timeout: Optional[int] = None) -> str: + """Run command with own globals/locals and returns anything printed. + Timeout after the specified number of seconds.""" + + # Warn against dangers of PythonREPL + warn_once() + + # queue: multiprocessing.Queue = multiprocessing.Queue() + parent_conn, child_conn = multiprocessing.Pipe() + + # Only use multiprocessing if we are enforcing a timeout + if timeout is not None: + # create a Process + p = multiprocessing.Process( + target=self.worker, args=(command, self.globals, self.locals, child_conn) + ) + + # start it + p.start() + + # wait for the process to finish or kill it after timeout seconds + p.join(timeout) + + if p.is_alive(): + p.terminate() + return "Execution timed out" + else: + self.worker(command, self.globals, self.locals, child_conn) + # get the result from the worker function + return parent_conn.recv() + python_repl = PythonREPL() def _run(command: str, timeout = None) -> str: From 6a4543355be9a3dc7d1354b007c24ff80bfb2130 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Tue, 12 Nov 2024 08:06:11 +0000 Subject: [PATCH 22/29] add llama-3.2 --- .../common_logic/common_utils/constant.py | 1 + .../common_logic/common_utils/prompt_utils.py | 49 +++++++++++++++++-- .../chains/chat_chain.py | 3 ++ .../chains/conversation_summary_chain.py | 4 ++ .../langchain_integration/chains/rag_chain.py | 3 ++ .../chains/tool_calling_chain_api.py | 16 +++--- .../chat_models/__init__.py | 1 + .../chat_models/bedrock_models.py | 7 ++- .../tools/common_tools/__init__.py | 14 ++++-- .../main_utils/online_entries/common_entry.py | 11 +++-- .../test/main_local_test_common.py | 31 ++++++++++-- 11 files changed, 120 insertions(+), 20 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/constant.py b/source/lambda/online/common_logic/common_utils/constant.py index 37f5a1cc..3501520e 100644 --- a/source/lambda/online/common_logic/common_utils/constant.py +++ b/source/lambda/online/common_logic/common_utils/constant.py @@ -144,6 +144,7 @@ class LLMModelType(ConstantBase): QWEN2INSTRUCT72B = "qwen2-72B-instruct" QWEN15INSTRUCT32B = "qwen1_5-32B-instruct" LLAMA3_1_70B_INSTRUCT = "meta.llama3-1-70b-instruct-v1:0" + LLAMA3_2_90B_INSTRUCT = "us.meta.llama3-2-90b-instruct-v1:0" MISTRAL_LARGE_2407 = "mistral.mistral-large-2407-v1:0" COHERE_COMMAND_R_PLUS = "cohere.command-r-plus-v1:0" diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index bb634a41..f060efbe 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -150,6 +150,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_INSTANCE, LLMModelType.MIXTRAL_8X7B_INSTRUCT, LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.LLAMA3_2_90B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS, ], @@ -269,6 +270,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.QWEN2INSTRUCT7B, LLMModelType.GLM_4_9B_CHAT, LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.LLAMA3_2_90B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS, @@ -293,6 +295,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.QWEN2INSTRUCT7B, LLMModelType.GLM_4_9B_CHAT, LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.LLAMA3_2_90B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS, ], @@ -317,6 +320,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.QWEN2INSTRUCT7B, LLMModelType.GLM_4_9B_CHAT, LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.LLAMA3_2_90B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS, ], @@ -383,11 +387,48 @@ def prompt_template_render(self, prompt_template: dict): 1. Determine whether the current context is sufficient to answer the user's question. 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. 3. If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. - 4. If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. -- Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. + 4. If any of required parameters of the tool you want to call do not appears in context, call the `give_rhetorical_question` tool to ask the user for more information. +- Always output with the same language as the content from user. If the content is English, use English to output. If the content is Chinese, use Chinese to output. - Always call one tool at a time. """ +# AGENT_SYSTEM_PROMPT = """\ +# You are a helpful and honest AI assistant. Today is {date},{weekday}. +# Here are some guidelines for you: +# +# - Output your step by step thinking in one pair of and tags, here are steps for you to think about deciding to use which tool: +# 1. If the context contains the result of last tool call, it needs to be analyzed. +# 2. Determine whether the current context is sufficient to answer the user's question. +# 3. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. +# 4. If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. +# 5. If any of required parameters of the tool you want to call do not appears in context, call the `give_rhetorical_question` tool to ask the user for more information. +# - Always output with the same language as the content from user. If the content is English, use English to output. If the content is Chinese, use Chinese to output. +# - Always invoke one tool. +# - Before invoking any tool, be sure to first output your thought process in one pair of and tag. +# """ + + +# AGENT_SYSTEM_PROMPT = """\ +# You are a helpful and honest AI assistant. Today is {date},{weekday}. +# Here are some guidelines for you: +# +# - Output your step by step thinking in one pair of and tags, here are steps for you to think about deciding to use which tool: +# 1. If the context contains the result of last tool call, it needs to be analyzed. +# 2. Determine whether the current context is sufficient to answer the user's question. +# 3. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. +# 4. If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. +# 5. If any of required parameters of the tool you want to call do not appears in context, call the `give_rhetorical_question` tool to ask the user for more information. +# - Always output with the same language as the content from user. If the content is English, use English to output. If the content is Chinese, use Chinese to output. +# - Always invoke one tool. +# + +# # Output example +# +# write your thinking according to guidlines. +# +# [invoke some tools]""" + + # - Output your thinking before to call one tool. register_prompt_templates( model_ids=[ @@ -397,6 +438,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.CLAUDE_3_5_HAIKU, LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.LLAMA3_2_90B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, # LLMModelType.COHERE_COMMAND_R_PLUS, ], @@ -416,7 +458,7 @@ def prompt_template_render(self, prompt_template: dict): * Determine whether the current context is sufficient to answer the user's question. * If the current context is sufficient to answer the user's question, call the `give_final_response` tool. * If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. -* If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information. +* If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information about the . ## Reply rules * Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. @@ -446,6 +488,7 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_5_HAIKU, LLMModelType.CLAUDE_3_5_SONNET, LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.LLAMA3_2_90B_INSTRUCT, LLMModelType.MISTRAL_LARGE_2407, LLMModelType.COHERE_COMMAND_R_PLUS, ], diff --git a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py index e4732576..d6a50d8b 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py @@ -108,6 +108,9 @@ class Mixtral8x7bChatChain(Claude2ChatChain): class Llama31Instruct70BChatChain(Claude2ChatChain): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT +class Llama32Instruct90BChatChain(Claude2ChatChain): + model_id = LLMModelType.LLAMA3_2_90B_INSTRUCT + class MistraLlargeChat2407ChatChain(Claude2ChatChain): model_id = LLMModelType.MISTRAL_LARGE_2407 diff --git a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py index 81e60fcd..1e7275fa 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py @@ -221,6 +221,10 @@ class Llama31Instruct70BConversationSummaryChain(Claude2ConversationSummaryChain model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT +class Llama32Instruct90BConversationSummaryChain(Claude2ConversationSummaryChain): + model_id = LLMModelType.LLAMA3_2_90B_INSTRUCT + + class MistraLlargeChat2407ConversationSummaryChain(Claude2ConversationSummaryChain): model_id = LLMModelType.MISTRAL_LARGE_2407 diff --git a/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py index b55a6074..bfacac8f 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/rag_chain.py @@ -96,6 +96,9 @@ class Claude35HaikuRAGLLMChain(Claude2RagLLMChain): class Llama31Instruct70B(Claude2RagLLMChain): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT +class Llama32Instruct90B(Claude2RagLLMChain): + model_id = LLMModelType.LLAMA3_2_90B_INSTRUCT + class MistraLlarge2407(Claude2RagLLMChain): model_id = LLMModelType.MISTRAL_LARGE_2407 diff --git a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py index 99713e0e..dfd56043 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py @@ -92,12 +92,12 @@ def create_chain(cls, model_kwargs=None, **kwargs): tools:list = kwargs['tools'] assert all(isinstance(tool,BaseTool) for tool in tools),tools fewshot_examples = kwargs.get('fewshot_examples',[]) - if fewshot_examples: - fewshot_examples.append({ - "name": "give_rhetorical_question", - "query": "今天天气怎么样?", - "kwargs": {"question": "请问你想了解哪个城市的天气?"} - }) + # if fewshot_examples: + # fewshot_examples.append({ + # "name": "give_rhetorical_question", + # "query": "今天天气怎么样?", + # "kwargs": {"question": "请问您想了解哪个城市的天气?"} + # }) agent_system_prompt = get_prompt_template( model_id=cls.model_id, task_type=cls.intent_type, @@ -168,6 +168,10 @@ class Llama31Instruct70BToolCallingChain(Claude2ToolCallingChain): model_id = LLMModelType.LLAMA3_1_70B_INSTRUCT +class Llama32Instruct90BToolCallingChain(Claude2ToolCallingChain): + model_id = LLMModelType.LLAMA3_2_90B_INSTRUCT + + class MistraLlarge2407ToolCallingChain(Claude2ToolCallingChain): model_id = LLMModelType.MISTRAL_LARGE_2407 diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py index ac930c10..8f0bd1be 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/__init__.py @@ -90,6 +90,7 @@ def _load_module(model_id): LLMModelType.CLAUDE_3_HAIKU:_import_bedrock_models, LLMModelType.CLAUDE_3_5_SONNET:_import_bedrock_models, LLMModelType.LLAMA3_1_70B_INSTRUCT:_import_bedrock_models, + LLMModelType.LLAMA3_2_90B_INSTRUCT:_import_bedrock_models, LLMModelType.MISTRAL_LARGE_2407:_import_bedrock_models, LLMModelType.COHERE_COMMAND_R_PLUS:_import_bedrock_models, LLMModelType.CLAUDE_3_5_SONNET_V2:_import_bedrock_models, diff --git a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py index e5b68ddc..8282392f 100644 --- a/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py +++ b/source/lambda/online/common_logic/langchain_integration/chat_models/bedrock_models.py @@ -19,7 +19,7 @@ class ChatBedrockConverse(_ChatBedrockConverse): class Claude2(Model): model_id = LLMModelType.CLAUDE_2 default_model_kwargs = {"max_tokens": 2000, "temperature": 0.7, "top_p": 0.9} - enable_auto_tool_choice = True + enable_auto_tool_choice = False @classmethod def create_model(cls, model_kwargs=None, **kwargs): @@ -87,6 +87,11 @@ class Llama3d1Instruct70B(Claude2): enable_auto_tool_choice = False enable_prefill = False +class Llama3d2Instruct90B(Claude2): + model_id = LLMModelType.LLAMA3_2_90B_INSTRUCT + enable_auto_tool_choice = False + enable_prefill = False + class CohereCommandRPlus(Claude2): model_id = LLMModelType.COHERE_COMMAND_R_PLUS diff --git a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py index c9b520c4..0586a00b 100644 --- a/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/tools/common_tools/__init__.py @@ -36,7 +36,7 @@ def _load_rhetorical_tool(tool_identifier:ToolIdentifier): "description": "This tool is designed to handle the scenario when required parameters are missing from other tools. It prompts the user to provide the necessary information, ensuring that all essential parameters are collected before proceeding. This tools enhances user interaction by clarifying what is needed and improving the overall usability of the application.", "properties": { "question": { - "description": "The rhetorical question to user", + "description": "The rhetorical question to user. Example:\nInput: 今天天气怎么样?\nOutput: 请问您想了解哪个城市的天气?", "type": "string" }, }, @@ -186,11 +186,19 @@ def run(self, command: str, timeout: Optional[int] = None) -> str: def _run(command: str, timeout = None) -> str: res = python_repl.run(command=command,timeout=timeout) if not res: - raise ValueError(f"The output is empty, please call this tool again and refine you code, use `print` function to output the value you want to obtain.") + raise ValueError(f"The current tool does not produce a result, modify your code and continue to call the `python_repl` tool, making sure to use the `print` function to output the final result.") return res + + description = """\ +This tool handles scientific computing problems by executing python code. Typical scenarios include the follows: +1. Mathematical arithmetic/numerical comparisons. +2. Code execution scenarios, such as data analysis, visualization, etc. + +Input should be a valid python code. If you want to see the output of a value, you must print it out with `print(...)` statement. +""" repl_tool = Tool( name="python_repl", - description="This tool is for arbitrary python code execution, typically scenes include scientific problems, such as math problems, physics problems, etc. Use this to execute python code. Input should be a valid python code. If you want to see the output of a value, you must print it out with `print(...)` statement.", + description=description, func=_run ) ToolManager.register_lc_tool( diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index db2f3396..264930e5 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -1,6 +1,7 @@ import traceback import json import uuid +import re from typing import Annotated, Any, TypedDict, List,Union from common_logic.common_utils.chatbot_utils import ChatbotManager @@ -389,6 +390,10 @@ def handle_tool_errors(e): def final_results_preparation(state: ChatbotState): + answer = state['answer'] + if isinstance(answer,str): + answer = re.sub(".*?","",answer,flags=re.S).strip() + state['answer'] = answer app_response = process_response(state["event_body"], state) return {"app_response": app_response} @@ -538,6 +543,7 @@ def register_rag_tool_from_config(event_body: dict): rerankers = event_body["chatbot_config"]["private_knowledge_config"]['rerankers'] if rerankers: rerankers = [rerankers[0]] + # index_name = index_content["indexId"] index_name = index_content["indexId"].replace("-","_") description = index_content["description"] # TODO give specific retriever config @@ -625,11 +631,8 @@ def common_entry(event_body): if registered_tool_name not in agent_config['tools']: agent_config['tools'].append(registered_tool_name) - - # register lambda tools register_custom_lambda_tools_from_config(event_body) - # logger.info(f'event body to graph:\n{json.dumps(event_body,ensure_ascii=False,cls=JSONEncoder)}') @@ -663,7 +666,7 @@ def common_entry(event_body): "tools":None, "ddb_additional_kwargs": {} }, - config={"recursion_limit": 10} + config={"recursion_limit": 20} ) # print('extra_response',response['extra_response']) return response["app_response"] diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index 1ff248b5..64d67529 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -164,8 +164,9 @@ def test_multi_turns_agent_pr(): user_queries = [ { # "query": "”我爱北京天安门“包含多少个字符?", - # "query": "11133乘以97892395等于多少", # 1089836033535 - "query": "今天天气怎么样?", + # "query": "What does 245346356356 times 346357457 equal?", # 1089836033535 + # "query": "9.11和9.9哪个更大?", # 1089836033535 + "query": "今天天气如何?", # "query": "介绍一下MemGPT", "use_history": True, "enable_trace": True @@ -186,9 +187,10 @@ def test_multi_turns_agent_pr(): # "private_knowledge": [] # } default_llm_config = { + # "model_id":'anthropic.claude-3-sonnet-20240229-v1:0', # 'model_id': "anthropic.claude-3-5-sonnet-20240620-v1:0", # 'model_id': "anthropic.claude-3-5-haiku-20241022-v1:0", - # 'model_id': "meta.llama3-1-70b-instruct-v1:0", + # 'model_id': "us.meta.llama3-2-90b-instruct-v1:0", # 'model_id':"mistral.mistral-large-2407-v1:0", 'model_id':"cohere.command-r-plus-v1:0", 'model_kwargs': { @@ -197,6 +199,7 @@ def test_multi_turns_agent_pr(): } } # agent_config={"tools":["python_repl"]} + agent_config = {} agent_config={ "tools":[ { @@ -216,6 +219,28 @@ def test_multi_turns_agent_pr(): ] } + +# { +# "agent_config":{ +# "tools":[ +# { +# "lambda_name":"intelli-agent-lambda-tool-example1", +# "name": "count_char", +# "description": "Count the number of chars contained in a sentence.", +# "properties": { +# "phrase": { +# "type": "string", +# "description": "The phrase needs to count chars" +# } +# }, +# "required": ["phrase"], +# "return_direct":False +# }, +# "python_repl" +# ] +# } +# } + for query in user_queries: print("==" * 50) if isinstance(query, str): From 5b622d79f2474726017479d4d9cc5fc83d1ba1f4 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Tue, 12 Nov 2024 12:39:26 +0000 Subject: [PATCH 23/29] modify agent prompt; add new intent logic --- .../common_logic/common_utils/constant.py | 1 + .../common_logic/common_utils/prompt_utils.py | 43 ++++++----- .../chains/tool_calling_chain_api.py | 17 ++-- .../main_utils/online_entries/common_entry.py | 77 +++++++++++++------ .../test/main_local_test_common.py | 23 +++--- 5 files changed, 97 insertions(+), 64 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/constant.py b/source/lambda/online/common_logic/common_utils/constant.py index a03c2d04..0f44b288 100644 --- a/source/lambda/online/common_logic/common_utils/constant.py +++ b/source/lambda/online/common_logic/common_utils/constant.py @@ -188,4 +188,5 @@ class KBType(Enum): class Threshold(ConstantBase): QQ_IN_RAG_CONTEXT = 0.5 + INTENTION_ALL_KNOWLEDGAE_RETRIEVE = 0.5 diff --git a/source/lambda/online/common_logic/common_utils/prompt_utils.py b/source/lambda/online/common_logic/common_utils/prompt_utils.py index f060efbe..2adec285 100644 --- a/source/lambda/online/common_logic/common_utils/prompt_utils.py +++ b/source/lambda/online/common_logic/common_utils/prompt_utils.py @@ -390,7 +390,11 @@ def prompt_template_render(self, prompt_template: dict): 4. If any of required parameters of the tool you want to call do not appears in context, call the `give_rhetorical_question` tool to ask the user for more information. - Always output with the same language as the content from user. If the content is English, use English to output. If the content is Chinese, use Chinese to output. - Always call one tool at a time. -""" + +Here's some context for reference: + +{context} +""" # AGENT_SYSTEM_PROMPT = """\ # You are a helpful and honest AI assistant. Today is {date},{weekday}. @@ -437,9 +441,9 @@ def prompt_template_render(self, prompt_template: dict): LLMModelType.CLAUDE_3_5_SONNET, LLMModelType.CLAUDE_3_5_SONNET_V2, LLMModelType.CLAUDE_3_5_HAIKU, - LLMModelType.LLAMA3_1_70B_INSTRUCT, - LLMModelType.LLAMA3_2_90B_INSTRUCT, - LLMModelType.MISTRAL_LARGE_2407, + # LLMModelType.LLAMA3_1_70B_INSTRUCT, + # LLMModelType.LLAMA3_2_90B_INSTRUCT, + # LLMModelType.MISTRAL_LARGE_2407, # LLMModelType.COHERE_COMMAND_R_PLUS, ], task_type=LLMTaskType.TOOL_CALLING_API, @@ -448,25 +452,28 @@ def prompt_template_render(self, prompt_template: dict): ) - AGENT_SYSTEM_PROMPT_COHERE = """\ -You are a helpful and honest AI assistant. Today is {date},{weekday}. -You need to carefully understand the user's intent and then invoke the tool to reply. - -## Tool use guidelines -Here are steps for you to decide to use which tool: -* Determine whether the current context is sufficient to answer the user's question. -* If the current context is sufficient to answer the user's question, call the `give_final_response` tool. -* If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. -* If the parameters of the tool you want to call do not meet the requirements, call the `give_rhetorical_question` tool to ask the user for more information about the . +## Task & Context +You are a helpful and honest AI assistant. Today is {date},{weekday}. +Here's some context for reference: +{context} -## Reply rules -* Always output with the same language as the content from user. If the content is english, use english to output. If the content is chinese, use chinese to output. -* Always call one tool at a time.""" +## Guidlines +Here are some guidelines for you: +- Here are strategies for you to decide to use which tool: + 1. Determine whether the current context is sufficient to answer the user's question. + 2. If the current context is sufficient to answer the user's question, call the `give_final_response` tool. + 3. If the current context is not sufficient to answer the user's question, you can consider calling one of the provided tools. + 4. If any of required parameters of the tool you want to call do not appears in context, call the `give_rhetorical_question` tool to ask the user for more information. +- Always output with the same language as the content from user. If the content is English, use English to output. If the content is Chinese, use Chinese to output. +- Always call one tool at a time.""" register_prompt_templates( model_ids=[ - LLMModelType.COHERE_COMMAND_R_PLUS + LLMModelType.COHERE_COMMAND_R_PLUS, + LLMModelType.LLAMA3_1_70B_INSTRUCT, + LLMModelType.LLAMA3_2_90B_INSTRUCT, + LLMModelType.MISTRAL_LARGE_2407 ], task_type=LLMTaskType.TOOL_CALLING_API, prompt_template=AGENT_SYSTEM_PROMPT_COHERE, diff --git a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py index dfd56043..62d982a3 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/tool_calling_chain_api.py @@ -41,12 +41,14 @@ def create_chat_history(cls,x): return chat_history @classmethod - def get_common_system_prompt(cls,system_prompt_template:str): + def get_common_system_prompt(cls,system_prompt_template:str,all_knowledge_retrieved_list=None): + all_knowledge_retrieved_list = all_knowledge_retrieved_list or [] + all_knowledge_retrieved = "\n\n".join(all_knowledge_retrieved_list) now = get_china_now() date_str = now.strftime("%Y年%m月%d日") weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] weekday = weekdays[now.weekday()] - system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) + system_prompt = system_prompt_template.format(date=date_str,weekday=weekday,context=all_knowledge_retrieved) return system_prompt @@ -92,12 +94,6 @@ def create_chain(cls, model_kwargs=None, **kwargs): tools:list = kwargs['tools'] assert all(isinstance(tool,BaseTool) for tool in tools),tools fewshot_examples = kwargs.get('fewshot_examples',[]) - # if fewshot_examples: - # fewshot_examples.append({ - # "name": "give_rhetorical_question", - # "query": "今天天气怎么样?", - # "kwargs": {"question": "请问您想了解哪个城市的天气?"} - # }) agent_system_prompt = get_prompt_template( model_id=cls.model_id, task_type=cls.intent_type, @@ -105,9 +101,10 @@ def create_chain(cls, model_kwargs=None, **kwargs): ).prompt_template agent_system_prompt = kwargs.get("agent_system_prompt",None) or agent_system_prompt - + + all_knowledge_retrieved_list = kwargs.get('all_knowledge_retrieved_list',[]) agent_system_prompt = cls.get_common_system_prompt( - agent_system_prompt + agent_system_prompt,all_knowledge_retrieved_list ) # tool fewshot prompt diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index 1004bedc..59a0dfe7 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -31,9 +31,6 @@ from common_logic.common_utils.serialization_utils import JSONEncoder from common_logic.common_utils.monitor_utils import format_intention_output, format_preprocess_output, format_qq_data from common_logic.common_utils.ddb_utils import custom_index_desc -from lambda_main.main_utils.online_entries.agent_base import ( - tool_execution, -) from lambda_main.main_utils.parse_config import CommonConfigParser from langgraph.graph import END, StateGraph from common_logic.langchain_integration.retrievers.retriever import lambda_handler as retrieve_fn @@ -51,6 +48,7 @@ logger = get_logger("common_entry") + class ChatbotState(TypedDict): ########### input/output states ########### # inputs @@ -93,6 +91,7 @@ class ChatbotState(TypedDict): intent_fewshot_examples: list # tools of retrieved intention samples in search engine, e.g. OpenSearch intent_fewshot_tools: list + all_knowledge_retrieved_list: list ########### retriever states ########### # contexts information retrieved in search engine, e.g. OpenSearch @@ -204,6 +203,12 @@ def intention_detection(state: ChatbotState): } ) + intent_fewshot_tools: list[str] = list( + set([e["intent"] for e in intent_fewshot_examples]) + ) + all_knowledge_retrieved_list = [] + markdown_table = format_intention_output(intent_fewshot_examples) + group_name = state["chatbot_config"]["group_name"] chatbot_id = state["chatbot_config"]["chatbot_id"] custom_qd_index = custom_index_desc(group_name, chatbot_id) @@ -211,35 +216,49 @@ def intention_detection(state: ChatbotState): # TODO need to modify with new intent logic if not intention_ready and not custom_qd_index: - return { - "answer": GUIDE_INTENTION_NOT_FOUND, - "intent_type": "intention not ready", - } - elif not intention_ready and custom_qd_index: - intent_fewshot_examples = [] - intent_fewshot_tools: list[str] = [] - else: - intent_fewshot_tools: list[str] = list( - set([e["intent"] for e in intent_fewshot_examples]) - ) - - markdown_table = format_intention_output(intent_fewshot_examples) - send_trace( - f"{markdown_table}", - state["stream"], - state["ws_connection_id"], - state["enable_trace"], - ) + # retrieve all knowledge + retriever_params = state["chatbot_config"]["private_knowledge_config"] + retriever_params["query"] = state[ + retriever_params.get("retriever_config", {}).get("query_key", "query") + ] + threshold = Threshold.INTENTION_ALL_KNOWLEDGAE_RETRIEVE + output = retrieve_fn(retriever_params) + all_knowledge_retrieved_list = [ + doc["page_content"] + for doc in output["result"]["docs"] + if doc['score'] >= threshold + + ] + # return { + # "answer": GUIDE_INTENTION_NOT_FOUND, + # "intent_type": "intention not ready", + # } + # elif not intention_ready and custom_qd_index: + # intent_fewshot_examples = [] + # intent_fewshot_tools: list[str] = [] + # else: + send_trace( + f"{markdown_table}", + state["stream"], + state["ws_connection_id"], + state["enable_trace"], + ) + + # rename tool name + intent_fewshot_tools = [tool_rename(i) for i in intent_fewshot_tools] + intent_fewshot_examples = [ + {**e, "intent": tool_rename(e["intent"])} for e in intent_fewshot_examples + ] return { "intent_fewshot_examples": intent_fewshot_examples, "intent_fewshot_tools": intent_fewshot_tools, + "all_knowledge_retrieved_list":all_knowledge_retrieved_list, "qq_match_results": context_list, "qq_match_contexts": qq_match_contexts, - "intent_type": "intention detected", + "intent_type": "intention detected" } - @node_monitor_wrapper def agent(state: ChatbotState): # two cases to invoke rag function @@ -309,6 +328,7 @@ def agent(state: ChatbotState): **agent_config['llm_config'], "tools": tools, "fewshot_examples": state['intent_fewshot_examples'], + "all_knowledge_retrieved_list":state['all_knowledge_retrieved_list'] } group_name = state['chatbot_config']['group_name'] chatbot_id = state['chatbot_config']['chatbot_id'] @@ -540,6 +560,13 @@ def build_graph(chatbot_state_cls): ##################################### app = None +def tool_rename(name:str) -> str: + """ + rename the tool name + """ + return name.replace("-","_") + + def register_rag_tool_from_config(event_body: dict): group_name = event_body.get("chatbot_config").get("group_name", "Admin") @@ -564,7 +591,7 @@ def register_rag_tool_from_config(event_body: dict): if rerankers: rerankers = [rerankers[0]] # index_name = index_content["indexId"] - index_name = index_content["indexId"].replace("-","_") + index_name = tool_rename(index_content["indexId"]) description = index_content["description"] # TODO give specific retriever config ToolManager.register_common_rag_tool( diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index 64d67529..ffcc8657 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -164,21 +164,21 @@ def test_multi_turns_agent_pr(): user_queries = [ { # "query": "”我爱北京天安门“包含多少个字符?", - # "query": "What does 245346356356 times 346357457 equal?", # 1089836033535 + "query": "What does 245346356356 times 346357457 equal?", # 1089836033535 # "query": "9.11和9.9哪个更大?", # 1089836033535 - "query": "今天天气如何?", - # "query": "介绍一下MemGPT", - "use_history": True, - "enable_trace": True - }, - { - # "query": "”我爱北京天安门“包含多少个字符?", - # "query": "11133乘以97892395等于多少", # 1089836033535 - "query": "我在上海", + # "query": "今天天气如何?", # "query": "介绍一下MemGPT", "use_history": True, "enable_trace": True }, + # { + # # "query": "”我爱北京天安门“包含多少个字符?", + # # "query": "11133乘以97892395等于多少", # 1089836033535 + # "query": "我在上海", + # # "query": "介绍一下MemGPT", + # "use_history": True, + # "enable_trace": True + # }, ] # default_index_names = { @@ -191,8 +191,9 @@ def test_multi_turns_agent_pr(): # 'model_id': "anthropic.claude-3-5-sonnet-20240620-v1:0", # 'model_id': "anthropic.claude-3-5-haiku-20241022-v1:0", # 'model_id': "us.meta.llama3-2-90b-instruct-v1:0", + 'model_id': "meta.llama3-1-70b-instruct-v1:0", # 'model_id':"mistral.mistral-large-2407-v1:0", - 'model_id':"cohere.command-r-plus-v1:0", + # 'model_id':"cohere.command-r-plus-v1:0", 'model_kwargs': { 'temperature': 0.01, 'max_tokens': 4096 From 0e8907213df3b0c1e66f50adbb3317a3b847614c Mon Sep 17 00:00:00 2001 From: zhouxss Date: Tue, 12 Nov 2024 13:17:31 +0000 Subject: [PATCH 24/29] debug intention logic --- .../main_utils/online_entries/common_entry.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index 59a0dfe7..e2d5d790 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -216,6 +216,7 @@ def intention_detection(state: ChatbotState): # TODO need to modify with new intent logic if not intention_ready and not custom_qd_index: + # if not intention_ready: # retrieve all knowledge retriever_params = state["chatbot_config"]["private_knowledge_config"] retriever_params["query"] = state[ @@ -223,16 +224,20 @@ def intention_detection(state: ChatbotState): ] threshold = Threshold.INTENTION_ALL_KNOWLEDGAE_RETRIEVE output = retrieve_fn(retriever_params) - all_knowledge_retrieved_list = [ - doc["page_content"] - for doc in output["result"]["docs"] - if doc['score'] >= threshold - ] - # return { - # "answer": GUIDE_INTENTION_NOT_FOUND, - # "intent_type": "intention not ready", - # } + info_to_log = [] + all_knowledge_retrieved_list = [] + for doc in output["result"]["docs"]: + if doc['score'] >= threshold: + all_knowledge_retrieved_list.append(doc["page_content"]) + info_to_log.append(f"score: {doc['score']}, page_content: {doc['page_content'][:200]}") + + send_trace( + f"all knowledge retrieved:\n {'\n'.join(info_to_log)}", + state["stream"], + state["ws_connection_id"], + state["enable_trace"], + ) # elif not intention_ready and custom_qd_index: # intent_fewshot_examples = [] # intent_fewshot_tools: list[str] = [] @@ -253,7 +258,7 @@ def intention_detection(state: ChatbotState): return { "intent_fewshot_examples": intent_fewshot_examples, "intent_fewshot_tools": intent_fewshot_tools, - "all_knowledge_retrieved_list":all_knowledge_retrieved_list, + "all_knowledge_retrieved_list": all_knowledge_retrieved_list, "qq_match_results": context_list, "qq_match_contexts": qq_match_contexts, "intent_type": "intention detected" From 0686dc3911d3942340e1af87c952e7335072d42d Mon Sep 17 00:00:00 2001 From: zhouxss Date: Tue, 12 Nov 2024 14:29:05 +0000 Subject: [PATCH 25/29] add sso example --- .../lambda/online/common_logic/common_utils/constant.py | 2 +- .../lambda_main/main_utils/online_entries/common_entry.py | 4 ++-- .../online/lambda_main/test/main_local_test_common.py | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/constant.py b/source/lambda/online/common_logic/common_utils/constant.py index 0f44b288..38c33782 100644 --- a/source/lambda/online/common_logic/common_utils/constant.py +++ b/source/lambda/online/common_logic/common_utils/constant.py @@ -188,5 +188,5 @@ class KBType(Enum): class Threshold(ConstantBase): QQ_IN_RAG_CONTEXT = 0.5 - INTENTION_ALL_KNOWLEDGAE_RETRIEVE = 0.5 + INTENTION_ALL_KNOWLEDGAE_RETRIEVE = 0.4 diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index e2d5d790..a2a4d025 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -215,8 +215,8 @@ def intention_detection(state: ChatbotState): # TODO need to modify with new intent logic - if not intention_ready and not custom_qd_index: - # if not intention_ready: + # if not intention_ready and not custom_qd_index: + if not intention_ready: # retrieve all knowledge retriever_params = state["chatbot_config"]["private_knowledge_config"] retriever_params["query"] = state[ diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index ffcc8657..34d16568 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -164,8 +164,10 @@ def test_multi_turns_agent_pr(): user_queries = [ { # "query": "”我爱北京天安门“包含多少个字符?", - "query": "What does 245346356356 times 346357457 equal?", # 1089836033535 + # "query": "What does 245346356356 times 346357457 equal?", # 1089836033535 # "query": "9.11和9.9哪个更大?", # 1089836033535 + # "query": "what happened in the history of Morgan Stanley in Emerging market in 1989 ?", + "query": "Tell me the team members of Morgan Stanley in China", # "query": "今天天气如何?", # "query": "介绍一下MemGPT", "use_history": True, @@ -190,8 +192,8 @@ def test_multi_turns_agent_pr(): # "model_id":'anthropic.claude-3-sonnet-20240229-v1:0', # 'model_id': "anthropic.claude-3-5-sonnet-20240620-v1:0", # 'model_id': "anthropic.claude-3-5-haiku-20241022-v1:0", - # 'model_id': "us.meta.llama3-2-90b-instruct-v1:0", - 'model_id': "meta.llama3-1-70b-instruct-v1:0", + 'model_id': "us.meta.llama3-2-90b-instruct-v1:0", + # 'model_id': "meta.llama3-1-70b-instruct-v1:0", # 'model_id':"mistral.mistral-large-2407-v1:0", # 'model_id':"cohere.command-r-plus-v1:0", 'model_kwargs': { From 75f194cd7f698cf2d27e7d2d2ff4dd237e10f6f6 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Wed, 13 Nov 2024 02:48:05 +0000 Subject: [PATCH 26/29] modify .viperlightignore --- .viperlightignore | 1 + .../lambda/online/lambda_main/test/main_local_test_common.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.viperlightignore b/.viperlightignore index d92077aa..3d491d0e 100644 --- a/.viperlightignore +++ b/.viperlightignore @@ -13,6 +13,7 @@ api_test/test_data/* api_test/gen-report-lambda.py source/portal/src/utils/const.ts source/lambda/online/lambda_main/test/main_local_test_retail.py +source/lambda/online/lambda_main/test/main_local_test_common.py source/lambda/online/functions/retail_tools/lambda_product_information_search/product_information_search.py source/lambda/job/test/prepare_data.py README.md diff --git a/source/lambda/online/lambda_main/test/main_local_test_common.py b/source/lambda/online/lambda_main/test/main_local_test_common.py index 34d16568..052ef7e4 100644 --- a/source/lambda/online/lambda_main/test/main_local_test_common.py +++ b/source/lambda/online/lambda_main/test/main_local_test_common.py @@ -192,7 +192,8 @@ def test_multi_turns_agent_pr(): # "model_id":'anthropic.claude-3-sonnet-20240229-v1:0', # 'model_id': "anthropic.claude-3-5-sonnet-20240620-v1:0", # 'model_id': "anthropic.claude-3-5-haiku-20241022-v1:0", - 'model_id': "us.meta.llama3-2-90b-instruct-v1:0", + # 'model_id': "us.meta.llama3-2-90b-instruct-v1:0", + 'model_id': "anthropic.claude-3-5-sonnet-20241022-v2:0", # 'model_id': "meta.llama3-1-70b-instruct-v1:0", # 'model_id':"mistral.mistral-large-2407-v1:0", # 'model_id':"cohere.command-r-plus-v1:0", From 8f8df04ff8c9bb419ca8f0108490063ccb479f4c Mon Sep 17 00:00:00 2001 From: zhouxss Date: Wed, 13 Nov 2024 08:22:13 +0000 Subject: [PATCH 27/29] remove __functions __llm_generate_utils --- source/lambda/online/__functions/__init__.py | 11 - .../lambda/online/__functions/_tool_base.py | 56 -- .../functions_utils/retriever/retriever.py | 179 ---- .../retriever/utils/aos_retrievers.py | 826 ------------------ .../retriever/utils/aos_utils.py | 217 ----- .../retriever/utils/context_utils.py | 81 -- .../retriever/utils/reranker.py | 217 ----- .../functions_utils/retriever/utils/test.py | 176 ---- .../retriever/utils/websearch_retrievers.py | 124 --- .../lambda_aws_qa_tools/__init__.py | 177 ---- .../lambda_aws_qa_tools/aws_ec2_price.py | 191 ---- .../check_service_availability.py | 80 -- .../lambda_aws_qa_tools/comfort.py | 3 - .../lambda_aws_qa_tools/explain_abbr.py | 4 - .../lambda_aws_qa_tools/service_org.py | 80 -- .../lambda_aws_qa_tools/transfer.py | 3 - .../lambda_common_tools/__init__.py | 121 --- .../__functions/lambda_common_tools/chat.py | 7 - .../lambda_common_tools/comparison_rag.py | 51 -- .../lambda_common_tools/get_weather.py | 34 - .../give_final_response.py | 7 - .../give_rhetorical_question.py | 7 - .../__functions/lambda_common_tools/rag.py | 70 -- .../lambda_common_tools/step_back_rag.py | 50 -- .../lambda_retail_tools/__init__.py | 337 ------- .../lambda_retail_tools/customer_complain.py | 51 -- .../lambda_retail_tools/daily_reception.py | 48 - .../lambda_retail_tools/goods_exchange.py | 49 -- .../lambda_retail_tools/order_info.py | 49 -- .../lambda_retail_tools/product_aftersales.py | 86 -- .../product_information_search.py | 40 - .../lambda_retail_tools/promotion.py | 53 -- .../lambda_retail_tools/rule_response.py | 49 -- .../lambda_retail_tools/size_guide.py | 83 -- .../lambda_retail_tools/transfer.py | 3 - .../lambda/online/__functions/lambda_tool.py | 24 - .../online/__functions/tool_calling_parse.py | 364 -------- .../__functions/tool_execute_result_format.py | 193 ---- .../__llm_generate_utils/__init__.py | 12 - .../llm_chains/__init__.py | 94 -- .../llm_chains/chat_chain.py | 338 ------- .../llm_chains/conversation_summary_chain.py | 215 ----- .../llm_chains/hyde_chain.py | 103 --- .../llm_chains/intention_chain.py | 224 ----- .../llm_chains/llm_chain_base.py | 26 - .../llm_chains/marketing_chains/__init__.py | 15 - .../mkt_conversation_summary.py | 120 --- .../marketing_chains/mkt_rag_chain.py | 55 -- .../llm_chains/query_rewrite_chain.py | 143 --- .../llm_chains/rag_chain.py | 161 ---- .../llm_chains/retail_chains/__init__.py | 26 - .../retail_chains/auto_evaluation_chain.py | 99 --- .../retail_conversation_summary_chain.py | 208 ----- .../retail_tool_calling_chain_claude_xml.py | 354 -------- .../retail_tool_calling_chain_json.py | 455 ---------- .../llm_chains/stepback_chain.py | 138 --- .../tool_calling_chain_claude_xml.py | 320 ------- .../llm_chains/translate_chain.py | 40 - .../__llm_generate_utils/llm_models.py | 382 -------- 59 files changed, 7729 deletions(-) delete mode 100644 source/lambda/online/__functions/__init__.py delete mode 100644 source/lambda/online/__functions/_tool_base.py delete mode 100644 source/lambda/online/__functions/functions_utils/retriever/retriever.py delete mode 100644 source/lambda/online/__functions/functions_utils/retriever/utils/aos_retrievers.py delete mode 100644 source/lambda/online/__functions/functions_utils/retriever/utils/aos_utils.py delete mode 100644 source/lambda/online/__functions/functions_utils/retriever/utils/context_utils.py delete mode 100644 source/lambda/online/__functions/functions_utils/retriever/utils/reranker.py delete mode 100644 source/lambda/online/__functions/functions_utils/retriever/utils/test.py delete mode 100644 source/lambda/online/__functions/functions_utils/retriever/utils/websearch_retrievers.py delete mode 100644 source/lambda/online/__functions/lambda_aws_qa_tools/__init__.py delete mode 100644 source/lambda/online/__functions/lambda_aws_qa_tools/aws_ec2_price.py delete mode 100644 source/lambda/online/__functions/lambda_aws_qa_tools/check_service_availability.py delete mode 100644 source/lambda/online/__functions/lambda_aws_qa_tools/comfort.py delete mode 100644 source/lambda/online/__functions/lambda_aws_qa_tools/explain_abbr.py delete mode 100644 source/lambda/online/__functions/lambda_aws_qa_tools/service_org.py delete mode 100644 source/lambda/online/__functions/lambda_aws_qa_tools/transfer.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/__init__.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/chat.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/comparison_rag.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/get_weather.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/give_final_response.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/give_rhetorical_question.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/rag.py delete mode 100644 source/lambda/online/__functions/lambda_common_tools/step_back_rag.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/__init__.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/customer_complain.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/daily_reception.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/goods_exchange.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/order_info.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/product_aftersales.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/product_information_search.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/promotion.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/rule_response.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/size_guide.py delete mode 100644 source/lambda/online/__functions/lambda_retail_tools/transfer.py delete mode 100644 source/lambda/online/__functions/lambda_tool.py delete mode 100644 source/lambda/online/__functions/tool_calling_parse.py delete mode 100644 source/lambda/online/__functions/tool_execute_result_format.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/__init__.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/__init__.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/chat_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/conversation_summary_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/hyde_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/intention_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/llm_chain_base.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/__init__.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/query_rewrite_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/rag_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/__init__.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/stepback_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/translate_chain.py delete mode 100644 source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_models.py diff --git a/source/lambda/online/__functions/__init__.py b/source/lambda/online/__functions/__init__.py deleted file mode 100644 index 12aa317a..00000000 --- a/source/lambda/online/__functions/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# tool -# from ._tool_base import get_tool_by_name,Tool,tool_manager - -# def init_common_tools(): -# from . import lambda_common_tools - -# def init_aws_qa_tools(): -# from . import lambda_aws_qa_tools - -# def init_retail_tools(): -# from . import lambda_retail_tools \ No newline at end of file diff --git a/source/lambda/online/__functions/_tool_base.py b/source/lambda/online/__functions/_tool_base.py deleted file mode 100644 index c4084d2c..00000000 --- a/source/lambda/online/__functions/_tool_base.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Union,Callable -from langchain.pydantic_v1 import BaseModel,Field -from enum import Enum -from common_logic.common_utils.constant import SceneType,ToolRuningMode - - -class ToolDefType(Enum): - openai = "openai" - - -class Tool(BaseModel): - name: str = Field(description="tool name") - lambda_name: str = Field(description="lambda name") - lambda_module_path: Union[str, Callable] = Field(description="local module path") - handler_name:str = Field(description="local handler name", default="lambda_handler") - tool_def: dict = Field(description="tool definition") - tool_init_kwargs:dict = Field(description="tool initial kwargs",default=None) - running_mode: str = Field(description="tool running mode, can be loop or output", default=ToolRuningMode.LOOP) - tool_def_type: ToolDefType = Field(description="tool definition type",default=ToolDefType.openai.value) - scene: str = Field(description="tool use scene",default=SceneType.COMMON) - # should_ask_parameter: bool = Field(description="tool use scene") - - -class ToolManager: - def __init__(self) -> None: - self.tools = {} - - def get_tool_id(self,tool_name:str,scene:str): - return f"{tool_name}__{scene}" - - def register_tool(self,tool_info:dict): - tool_def = tool_info['tool_def'] - default_paramters = { - "type": "object", - "properties": {}, - "required": [] - } - if "parameters" not in tool_def: - tool_def['parameters'] = default_paramters - else: - tool_def['parameters'] = {**default_paramters, **tool_def['parameters']} - - tool = Tool(**tool_info) - assert tool.tool_def_type == ToolDefType.openai.value, f"tool_def_type: {tool.tool_def_type} not support" - self.tools[self.get_tool_id(tool.name,tool.scene)] = tool - - def get_tool_by_name(self,name,scene=SceneType.COMMON): - return self.tools[self.get_tool_id(name,scene)] - -tool_manager = ToolManager() -get_tool_by_name = tool_manager.get_tool_by_name - - - - - diff --git a/source/lambda/online/__functions/functions_utils/retriever/retriever.py b/source/lambda/online/__functions/functions_utils/retriever/retriever.py deleted file mode 100644 index 977855e2..00000000 --- a/source/lambda/online/__functions/functions_utils/retriever/retriever.py +++ /dev/null @@ -1,179 +0,0 @@ -import json -import os -os.environ["PYTHONUNBUFFERED"] = "1" -import logging -import sys - -import boto3 -from common_logic.common_utils.chatbot_utils import ChatbotManager -from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper -from functions.functions_utils.retriever.utils.aos_retrievers import ( - QueryDocumentBM25Retriever, - QueryDocumentKNNRetriever, - QueryQuestionRetriever, -) -from functions.functions_utils.retriever.utils.context_utils import ( - retriever_results_format, -) -from functions.functions_utils.retriever.utils.reranker import ( - BGEReranker, - MergeReranker, -) -from functions.functions_utils.retriever.utils.websearch_retrievers import ( - GoogleRetriever, -) -from langchain.retrievers import ( - ContextualCompressionRetriever, -) -from langchain_community.retrievers import AmazonKnowledgeBasesRetriever -from langchain.retrievers.merger_retriever import MergerRetriever -from langchain.schema.runnable import RunnableLambda, RunnablePassthrough -from langchain_community.retrievers import AmazonKnowledgeBasesRetriever - -logger = logging.getLogger("retriever") -logger.setLevel(logging.INFO) - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(os.path.dirname(SCRIPT_DIR)) - -kb_enabled = os.environ["KNOWLEDGE_BASE_ENABLED"].lower() == "true" -kb_type = json.loads(os.environ["KNOWLEDGE_BASE_TYPE"]) -chatbot_table_name = os.environ.get("CHATBOT_TABLE", "") -model_table_name = os.environ.get("MODEL_TABLE", "") -index_table_name = os.environ.get("INDEX_TABLE", "") -dynamodb = boto3.resource("dynamodb") -chatbot_table = dynamodb.Table(chatbot_table_name) -model_table = dynamodb.Table(model_table_name) -index_table = dynamodb.Table(index_table_name) -chatbot_manager = ChatbotManager(chatbot_table, index_table, model_table) - -region = boto3.Session().region_name - -knowledgebase_client = boto3.client("bedrock-agent-runtime", region) -sm_client = boto3.client("sagemaker-runtime") - - -def get_bedrock_kb_retrievers(knowledge_base_id_list, top_k: int): - retriever_list = [ - AmazonKnowledgeBasesRetriever( - knowledge_base_id=knowledge_base_id, - retrieval_config={"vectorSearchConfiguration": {"numberOfResults": top_k}}, - ) - for knowledge_base_id in knowledge_base_id_list - ] - return retriever_list - - -def get_websearch_retrievers(top_k: int): - retriever_list = [GoogleRetriever(top_k)] - return retriever_list - - -def get_custom_qd_retrievers(config: dict, using_bm25=False): - qd_retriever = QueryDocumentKNNRetriever(**config) - - if using_bm25: - bm25_retrievert = QueryDocumentBM25Retriever( - **{ - "index_name": config["index_name"], - "using_whole_doc": config.get("using_whole_doc", False), - "context_num": config["context_num"], - "enable_debug": config.get("enable_debug", False), - } - ) - return [qd_retriever, bm25_retrievert] - return [qd_retriever] - - -def get_custom_qq_retrievers(config: dict): - qq_retriever = QueryQuestionRetriever(**config) - return [qq_retriever] - - -def get_whole_chain(retriever_list, reranker_config): - lotr = MergerRetriever(retrievers=retriever_list) - if len(reranker_config): - default_reranker_config = { - "enable_debug": False, - "target_model": "bge_reranker_model.tar.gz", - "top_k": 10, - } - reranker_config = {**default_reranker_config, **reranker_config} - compressor = BGEReranker(**reranker_config) - else: - compressor = MergeReranker() - - compression_retriever = ContextualCompressionRetriever( - base_compressor=compressor, base_retriever=lotr - ) - whole_chain = RunnablePassthrough.assign( - docs=compression_retriever | RunnableLambda(retriever_results_format) - ) - return whole_chain - - -retriever_dict = { - "qq": get_custom_qq_retrievers, - "intention": get_custom_qq_retrievers, - "qd": get_custom_qd_retrievers, - "websearch": get_websearch_retrievers, - "bedrock_kb": get_bedrock_kb_retrievers, -} - - -def get_custom_retrievers(retriever): - return retriever_dict[retriever["index_type"]](retriever) - - -def lambda_handler(event, context=None): - logger.info(f"Retrieval event: {event}") - event_body = event - retriever_list = [] - for retriever in event_body["retrievers"]: - if not kb_enabled: - retriever["vector_field"] = "sentence_vector" - retriever["source_field"] = "source" - retriever["text_field"] = "paragraph" - retriever_list.extend(get_custom_retrievers(retriever)) - rerankers = event_body.get("rerankers", None) - if rerankers: - reranker_config = rerankers[0]["config"] - else: - reranker_config = {} - - if len(retriever_list) > 0: - whole_chain = get_whole_chain(retriever_list, reranker_config) - else: - whole_chain = RunnablePassthrough.assign(docs=lambda x: []) - docs = whole_chain.invoke({"query": event_body["query"], "debug_info": {}}) - return {"code": 0, "result": docs} - - -if __name__ == "__main__": - query = """test""" - event = { - "retrievers": [ - { - "index_type": "qd", - "top_k": 5, - "context_num": 1, - "using_whole_doc": False, - "query_key": "query", - "index_name": "admin-qd-default", - "kb_type": "aos", - "target_model": "amazon.titan-embed-text-v1", - "embedding_model_endpoint": "amazon.titan-embed-text-v1", - "model_type": "bedrock", - "group_name": "Admin", - } - ], - "rerankers": [], - "llm_config": { - "model_id": "anthropic.claude-3-sonnet-20240229-v1:0", - "model_kwargs": {"temperature": 0.01, "max_tokens": 1000}, - "endpoint_name": "", - }, - "query": "亚马逊云计算服务可以通过超文本传输协议(HTTP)访问吗?", - } - response = lambda_handler(event, None) - print(response) diff --git a/source/lambda/online/__functions/functions_utils/retriever/utils/aos_retrievers.py b/source/lambda/online/__functions/functions_utils/retriever/utils/aos_retrievers.py deleted file mode 100644 index 6a7b5cae..00000000 --- a/source/lambda/online/__functions/functions_utils/retriever/utils/aos_retrievers.py +++ /dev/null @@ -1,826 +0,0 @@ -import asyncio -import json -import logging -import os -import traceback -from typing import Any, Dict, List, Union - -import boto3 -from common_logic.common_utils.time_utils import timeit -from langchain.callbacks.manager import CallbackManagerForRetrieverRun -from langchain.docstore.document import Document -from langchain.schema.retriever import BaseRetriever -from langchain_community.embeddings import BedrockEmbeddings -from sm_utils import SagemakerEndpointVectorOrCross - -from .aos_utils import LLMBotOpenSearchClient - -logger = logging.getLogger() -logger.setLevel(logging.INFO) - -# region = os.environ["AWS_REGION"] -kb_enabled = os.environ["KNOWLEDGE_BASE_ENABLED"].lower() == "true" -kb_type = json.loads(os.environ["KNOWLEDGE_BASE_TYPE"]) -intelli_agent_kb_enabled = kb_type.get("intelliAgentKb", {}).get("enabled", False) -aos_endpoint = os.environ.get("AOS_ENDPOINT", "") -aos_domain_name = os.environ.get("AOS_DOMAIN_NAME", "smartsearch") -aos_secret = os.environ.get("AOS_SECRET_NAME", "opensearch-master-user") -sm_client = boto3.client("secretsmanager") -bedrock_region = os.environ.get("BEDROCK_REGION", "us-east-1") -try: - master_user = sm_client.get_secret_value(SecretId=aos_secret)[ - "SecretString" - ] - if not aos_endpoint: - opensearch_client = boto3.client("opensearch") - response = opensearch_client.describe_domain( - DomainName=aos_domain_name - ) - aos_endpoint = response["DomainStatus"]["Endpoint"] - cred = json.loads(master_user) - username = cred.get("username") - password = cred.get("password") - auth = (username, password) - aos_client = LLMBotOpenSearchClient(aos_endpoint, auth) -except sm_client.exceptions.ResourceNotFoundException: - logger.info(f"Secret '{aos_secret}' not found in Secrets Manager") - aos_client = LLMBotOpenSearchClient(aos_endpoint) -except Exception as e: - logger.error(f"Error retrieving secret '{aos_secret}': {str(e)}") - raise - -DEFAULT_TEXT_FIELD_NAME = "text" -DEFAULT_VECTOR_FIELD_NAME = "vector_field" -DEFAULT_SOURCE_FIELD_NAME = "source" - - -def remove_redundancy_debug_info(results): - # filtered_results = copy.deepcopy(results) - filtered_results = results - for result in filtered_results: - for field in list(result["detail"].keys()): - if field.endswith("embedding") or field.startswith("vector"): - del result["detail"][field] - return filtered_results - - -@timeit -def get_similarity_embedding( - query: str, - embedding_model_endpoint: str, - target_model: str, - model_type: str = "vector", -) -> List[List[float]]: - if model_type.lower() == "bedrock": - embeddings = BedrockEmbeddings(model_id=embedding_model_endpoint, region_name=bedrock_region) - response = embeddings.embed_query(query) - else: - query_similarity_embedding_prompt = query - response = SagemakerEndpointVectorOrCross( - prompt=query_similarity_embedding_prompt, - endpoint_name=embedding_model_endpoint, - model_type=model_type, - stop=None, - region_name=None, - target_model=target_model, - ) - return response - - -@timeit -def get_relevance_embedding( - query: str, - query_lang: str, - embedding_model_endpoint: str, - target_model: str, - model_type: str = "vector", -): - if model_type == "bedrock": - embeddings = BedrockEmbeddings(model_id=embedding_model_endpoint, region_name=bedrock_region) - response = embeddings.embed_query(query) - else: - if model_type == "vector": - if query_lang == "zh": - query_relevance_embedding_prompt = ( - "为这个句子生成表示以用于检索相关文章:" + query - ) - elif query_lang == "en": - query_relevance_embedding_prompt = ( - "Represent this sentence for searching relevant passages: " + query - ) - else: - query_relevance_embedding_prompt = query - elif model_type == "m3" or model_type == "bce": - query_relevance_embedding_prompt = query - else: - raise ValueError(f"invalid embedding model type: {model_type}") - response = SagemakerEndpointVectorOrCross( - prompt=query_relevance_embedding_prompt, - endpoint_name=embedding_model_endpoint, - model_type=model_type, - region_name=None, - stop=None, - target_model=target_model, - ) - - return response - - -def get_filter_list(parsed_query: dict): - filter_list = [] - if "is_api_query" in parsed_query and parsed_query["is_api_query"]: - filter_list.append({"term": {"metadata.is_api": True}}) - return filter_list - - -def get_faq_answer(source, index_name, source_field): - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=source, - field=f"metadata.{source_field}", - ) - for r in opensearch_query_response["hits"]["hits"]: - if ( - "field" in r["_source"]["metadata"] - and "answer" == r["_source"]["metadata"]["field"] - ): - return r["_source"]["content"] - elif "jsonlAnswer" in r["_source"]["metadata"]: - return r["_source"]["metadata"]["jsonlAnswer"]["answer"] - return "" - - -def get_faq_content(source, index_name): - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=source, - field="metadata.source", - ) - for r in opensearch_query_response["hits"]["hits"]: - if r["_source"]["metadata"]["field"] == "all_text": - return r["_source"]["content"] - return "" - - -def get_doc(file_path, index_name): - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=file_path, - field="metadata.file_path", - size=100, - ) - chunk_list = [] - chunk_id_set = set() - for r in opensearch_query_response["hits"]["hits"]: - try: - if "chunk_id" not in r["_source"]["metadata"] or not r["_source"][ - "metadata" - ]["chunk_id"].startswith("$"): - continue - chunk_id = r["_source"]["metadata"]["chunk_id"] - content_type = r["_source"]["metadata"]["content_type"] - chunk_group_id = int(chunk_id.split("-")[0].strip("$")) - chunk_section_id = int(chunk_id.split("-")[-1]) - if (chunk_id, content_type) in chunk_id_set: - continue - except Exception as e: - logger.error(traceback.format_exc()) - continue - chunk_id_set.add((chunk_id, content_type)) - chunk_list.append( - ( - chunk_id, - chunk_group_id, - content_type, - chunk_section_id, - r["_source"]["text"], - ) - ) - sorted_chunk_list = sorted(chunk_list, key=lambda x: (x[1], x[2], x[3])) - chunk_text_list = [x[4] for x in sorted_chunk_list] - return "\n".join(chunk_text_list) - - -def get_child_context(chunk_id, index_name, window_size): - next_content_list = [] - previous_content_list = [] - previous_pos = 0 - next_pos = 0 - chunk_id_prefix = "-".join(chunk_id.split("-")[:-1]) - section_id = int(chunk_id.split("-")[-1]) - previous_section_id = section_id - next_section_id = section_id - while previous_pos < window_size: - previous_section_id -= 1 - if previous_section_id < 1: - break - previous_chunk_id = f"{chunk_id_prefix}-{previous_section_id}" - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=previous_chunk_id, - field="metadata.chunk_id", - size=1, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - previous_content_list.insert(0, r["_source"]["text"]) - previous_pos += 1 - else: - break - while next_pos < window_size: - next_section_id += 1 - next_chunk_id = f"{chunk_id_prefix}-{next_section_id}" - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=next_chunk_id, - field="metadata.chunk_id", - size=1, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - next_content_list.insert(0, r["_source"]["text"]) - next_pos += 1 - else: - break - return [previous_content_list, next_content_list] - - -def get_sibling_context(chunk_id, index_name, window_size): - next_content_list = [] - previous_content_list = [] - previous_pos = 0 - next_pos = 0 - chunk_id_prefix = "-".join(chunk_id.split("-")[:-1]) - section_id = int(chunk_id.split("-")[-1]) - previous_section_id = section_id - next_section_id = section_id - while previous_pos < window_size: - previous_section_id -= 1 - if previous_section_id < 1: - break - previous_chunk_id = f"{chunk_id_prefix}-{previous_section_id}" - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=previous_chunk_id, - field="metadata.chunk_id", - size=1, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - previous_content_list.insert(0, r["_source"]["text"]) - previous_pos += 1 - else: - break - while next_pos < window_size: - next_section_id += 1 - next_chunk_id = f"{chunk_id_prefix}-{next_section_id}" - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=next_chunk_id, - field="metadata.chunk_id", - size=1, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - next_content_list.insert(0, r["_source"]["text"]) - next_pos += 1 - else: - break - return [previous_content_list, next_content_list] - - -def get_context(aos_hit, index_name, window_size): - previous_content_list = [] - next_content_list = [] - if "chunk_id" not in aos_hit["_source"]["metadata"]: - return previous_content_list, next_content_list - chunk_id = aos_hit["_source"]["metadata"]["chunk_id"] - inner_previous_content_list, inner_next_content_list = get_sibling_context( - chunk_id, index_name, window_size - ) - if ( - len(inner_previous_content_list) == window_size - and len(inner_next_content_list) == window_size - ): - return inner_previous_content_list, inner_next_content_list - - if "heading_hierarchy" not in aos_hit["_source"]["metadata"]: - return [previous_content_list, next_content_list] - if "previous" in aos_hit["_source"]["metadata"]["heading_hierarchy"]: - previous_chunk_id = aos_hit["_source"]["metadata"]["heading_hierarchy"][ - "previous" - ] - previous_pos = 0 - while ( - previous_chunk_id - and previous_chunk_id.startswith("$") - and previous_pos < window_size - ): - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=previous_chunk_id, - field="metadata.chunk_id", - size=1, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - previous_chunk_id = r["_source"]["metadata"]["heading_hierarchy"][ - "previous" - ] - previous_content_list.insert(0, r["_source"]["text"]) - previous_pos += 1 - else: - break - if "next" in aos_hit["_source"]["metadata"]["heading_hierarchy"]: - next_chunk_id = aos_hit["_source"]["metadata"]["heading_hierarchy"]["next"] - next_pos = 0 - while ( - next_chunk_id and next_chunk_id.startswith("$") and next_pos < window_size - ): - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=next_chunk_id, - field="metadata.chunk_id", - size=1, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - next_chunk_id = r["_source"]["metadata"]["heading_hierarchy"]["next"] - next_content_list.append(r["_source"]["text"]) - next_pos += 1 - else: - break - return [previous_content_list, next_content_list] - - -def get_parent_content(previous_chunk_id, next_chunk_id, index_name): - previous_content_list = [] - while previous_chunk_id.startswith("$"): - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=previous_chunk_id, - field="metadata.chunk_id", - size=10, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - previous_chunk_id = r["_source"]["metadata"]["chunk_id"] - previous_content_list.append(r["_source"]["text"]) - else: - break - next_content_list = [] - while next_chunk_id.startswith("$"): - opensearch_query_response = aos_client.search( - index_name=index_name, - query_type="basic", - query_term=next_chunk_id, - field="metadata.chunk_id", - size=10, - ) - if len(opensearch_query_response["hits"]["hits"]) > 0: - r = opensearch_query_response["hits"]["hits"][0] - next_chunk_id = r["_source"]["metadata"]["chunk_id"] - next_content_list.append(r["_source"]["text"]) - else: - break - return [previous_content_list, next_content_list] - - -def organize_faq_results( - response, index_name, source_field="file_path", text_field="text" -): - """ - Organize results from aos response - - :param query_type: query type - :param response: aos response json - """ - results = [] - if not response: - return results - aos_hits = response["hits"]["hits"] - for aos_hit in aos_hits: - result = {} - try: - result["score"] = aos_hit["_score"] - data = aos_hit["_source"] - metadata = data["metadata"] - if "field" in metadata: - result["answer"] = get_faq_answer( - result["source"], index_name, source_field - ) - result["content"] = aos_hit["_source"]["content"] - result["question"] = aos_hit["_source"]["content"] - result[source_field] = aos_hit["_source"]["metadata"][source_field] - elif "answer" in metadata: - # Intentions - result["answer"] = metadata["answer"] - result["question"] = data["text"] - result["content"] = data["text"] - result["source"] = metadata[source_field] - result["kwargs"] = metadata.get("kwargs", {}) - elif "jsonlAnswer" in aos_hit["_source"]["metadata"] and "answer" in aos_hit["_source"]["metadata"]["jsonlAnswer"]: - # Intention - result["answer"] = aos_hit["_source"]["metadata"]["jsonlAnswer"]["answer"] - result["question"] = aos_hit["_source"]["metadata"]["jsonlAnswer"]["question"] - result["content"] = aos_hit["_source"]["text"] - if source_field in aos_hit["_source"]["metadata"]["jsonlAnswer"].keys(): - result[source_field] = aos_hit["_source"]["metadata"]["jsonlAnswer"][source_field] - else: - result[source_field] = aos_hit["_source"]["metadata"]["file_path"] - elif "jsonlAnswer" in aos_hit["_source"]["metadata"] and "answer" not in aos_hit["_source"]["metadata"]["jsonlAnswer"]: - # QQ match - result["answer"] = aos_hit["_source"]["metadata"]["jsonlAnswer"] - result["question"] = aos_hit["_source"]["text"] - result["content"] = aos_hit["_source"]["text"] - result[source_field] = aos_hit["_source"]["metadata"]["file_path"] - else: - result["answer"] = aos_hit["_source"]["metadata"] - result["content"] = aos_hit["_source"][text_field] - result["question"] = aos_hit["_source"][text_field] - result[source_field] = aos_hit["_source"]["metadata"][source_field] - except Exception as e: - logger.error(e) - logger.error(traceback.format_exc()) - logger.error(aos_hit) - continue - results.append(result) - return results - - -class QueryQuestionRetriever(BaseRetriever): - index_name: str - vector_field: str = "vector_field" - source_field: str = "source" - top_k: int = 10 - embedding_model_endpoint: str - target_model: str - model_type: str = "vector" - enable_debug: bool = False - - @timeit - def _get_relevant_documents( - self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun - ) -> List[Document]: - query = question["query"] - debug_info = question["debug_info"] - opensearch_knn_results = [] - query_repr = get_similarity_embedding( - query, self.embedding_model_endpoint, self.target_model, self.model_type - ) - opensearch_knn_response = aos_client.search( - index_name=self.index_name, - query_type="knn", - query_term=query_repr, - field=self.vector_field, - size=self.top_k, - ) - opensearch_knn_results.extend( - organize_faq_results( - opensearch_knn_response, self.index_name, self.source_field - ) - ) - docs = [] - for result in opensearch_knn_results: - docs.append( - Document( - page_content=result["content"], - metadata={ - "source": result[self.source_field], - "score": result["score"], - "retrieval_score": result["score"], - "retrieval_content": result["content"], - "answer": result["answer"], - "question": result["question"], - }, - ) - ) - if self.enable_debug: - debug_info[f"qq-knn-recall-{self.index_name}"] = ( - remove_redundancy_debug_info(opensearch_knn_results) - ) - return docs - - -class QueryDocumentKNNRetriever(BaseRetriever): - index_name: str - vector_field: str = "vector_field" - source_field: str = "file_path" - text_field: str = "text" - using_whole_doc: bool = False - context_num: int = 2 - top_k: int = 10 - # lang: Any - model_type: str = "vector" - embedding_model_endpoint: Any - target_model: Any - enable_debug: bool = False - lang: str = "zh" - - async def __ainvoke_get_context(self, aos_hit, window_size, loop): - return await loop.run_in_executor( - None, get_context, aos_hit, self.index_name, window_size - ) - - async def __spawn_task(self, aos_hits, context_size): - loop = asyncio.get_event_loop() - task_list = [] - for aos_hit in aos_hits: - if context_size: - task = asyncio.create_task( - self.__ainvoke_get_context(aos_hit, context_size, loop) - ) - task_list.append(task) - return await asyncio.gather(*task_list) - - @timeit - def organize_results( - self, - response, - aos_index=None, - source_field="file_path", - text_field="text", - using_whole_doc=True, - context_size=0, - ): - """ - Organize results from aos response - - :param query_type: query type - :param response: aos response json - """ - results = [] - if not response: - return results - aos_hits = response["hits"]["hits"] - if len(aos_hits) == 0: - return results - for aos_hit in aos_hits: - result = {"data": {}} - source = aos_hit["_source"]["metadata"][source_field] - result["source"] = source - result["score"] = aos_hit["_score"] - result["detail"] = aos_hit["_source"] - result["content"] = aos_hit["_source"][text_field] - result["doc"] = result["content"] - results.append(result) - if kb_enabled: - if using_whole_doc: - for result in results: - doc = get_doc(result["source"], aos_index) - if doc: - result["doc"] = doc - else: - response_list = asyncio.run(self.__spawn_task(aos_hits, context_size)) - for context, result in zip(response_list, results): - result["doc"] = "\n".join(context[0] + [result["content"]] + context[1]) - return results - - @timeit - def __get_knn_results(self, query_term, filter): - opensearch_knn_response = aos_client.search( - index_name=self.index_name, - query_type="knn", - query_term=query_term, - field=self.vector_field, - size=self.top_k, - filter=filter, - ) - opensearch_knn_results = self.organize_results( - opensearch_knn_response, - self.index_name, - self.source_field, - self.text_field, - self.using_whole_doc, - self.context_num, - )[: self.top_k] - return opensearch_knn_results - - @timeit - def _get_relevant_documents( - self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun - ) -> List[Document]: - query = question["query"] - # if "query_lang" in question and question["query_lang"] != self.lang and "translated_text" in question: - # query = question["translated_text"] - debug_info = question["debug_info"] - query_repr = get_relevance_embedding( - query, - self.lang, - self.embedding_model_endpoint, - self.target_model, - self.model_type, - ) - # question["colbert"] = query_repr["colbert_vecs"][0] - filter = get_filter_list(question) - # Get AOS KNN results. - opensearch_knn_results = self.__get_knn_results(query_repr, filter) - final_results = opensearch_knn_results - doc_list = [] - content_set = set() - for result in final_results: - if result["doc"] in content_set: - continue - content_set.add(result["content"]) - # TODO: add jsonlans - result_metadata = { - "source": result["source"], - "retrieval_content": result["content"], - "retrieval_data": result["data"], - "retrieval_score": result["score"], - # Set common score for llm. - "score": result["score"], - } - if "figure" in result["detail"]["metadata"]: - result_metadata["figure"] = result["detail"]["metadata"]["figure"] - if "content_type" in result["detail"]["metadata"]: - result_metadata["content_type"] = result["detail"]["metadata"][ - "content_type" - ] - doc_list.append( - Document(page_content=result["doc"], metadata=result_metadata) - ) - if self.enable_debug: - debug_info[f"qd-knn-recall-{self.index_name}"] = ( - remove_redundancy_debug_info(opensearch_knn_results) - ) - - return doc_list - - -class QueryDocumentBM25Retriever(BaseRetriever): - index_name: str - vector_field: str = "vector_field" - source_field: str = "source" - text_field: str = "text" - using_whole_doc: bool = False - context_num: Any - top_k: int = 5 - enable_debug: Any - config: Dict = {"run_name": "BM25"} - - async def __ainvoke_get_context(self, aos_hit, window_size, loop): - return await loop.run_in_executor( - None, get_context, aos_hit, self.index_name, window_size - ) - - async def __spawn_task(self, aos_hits, context_size): - loop = asyncio.get_event_loop() - task_list = [] - for aos_hit in aos_hits: - if context_size: - task = asyncio.create_task( - self.__ainvoke_get_context(aos_hit, context_size, loop) - ) - task_list.append(task) - return await asyncio.gather(*task_list) - - @timeit - def organize_results( - self, - response, - aos_index=None, - source_field="file_path", - text_field="text", - using_whole_doc=True, - context_size=0, - ): - """ - Organize results from aos response - - :param query_type: query type - :param response: aos response json - """ - results = [] - if not response: - return results - aos_hits = response["hits"]["hits"] - if len(aos_hits) == 0: - return results - for aos_hit in aos_hits: - result = {"data": {}} - source = aos_hit["_source"]["metadata"][source_field] - result["source"] = source - result["score"] = aos_hit["_score"] - result["detail"] = aos_hit["_source"] - # result["content"] = aos_hit['_source'][text_field] - result["content"] = aos_hit["_source"][text_field] - result["doc"] = result["content"] - # if 'additional_vecs' in aos_hit['_source']['metadata'] and \ - # 'colbert_vecs' in aos_hit['_source']['metadata']['additional_vecs']: - # result["data"]["colbert"] = aos_hit['_source']['metadata']['additional_vecs']['colbert_vecs'] - if "jsonlAnswer" in aos_hit["_source"]["metadata"]: - result["jsonlAnswer"] = aos_hit["_source"]["metadata"]["jsonlAnswer"] - results.append(result) - if using_whole_doc: - for result in results: - doc = get_doc(result["source"], aos_index) - if doc: - result["doc"] = doc - else: - response_list = asyncio.run(self.__spawn_task(aos_hits, context_size)) - for context, result in zip(response_list, results): - result["doc"] = "\n".join(context[0] + [result["doc"]] + context[1]) - # context = get_context(aos_hit['_source']["metadata"]["heading_hierarchy"]["previous"], - # aos_hit['_source']["metadata"]["heading_hierarchy"]["next"], - # aos_index, - # context_size) - # if context: - # result["doc"] = "\n".join(context[0] + [result["doc"]] + context[1]) - return results - - @timeit - def __get_bm25_results(self, query_term, filter): - opensearch_bm25_response = aos_client.search( - index_name=self.index_name, - query_type="fuzzy", - query_term=query_term, - field=self.text_field, - size=self.top_k, - filter=filter, - ) - opensearch_bm25_results = self.organize_results( - opensearch_bm25_response, - self.index_name, - self.source_field, - self.text_field, - self.using_whole_doc, - self.context_num, - )[: self.top_k] - return opensearch_bm25_results - - @timeit - def _get_relevant_documents( - self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun - ) -> List[Document]: - query = question["query"] - # if "query_lang" in question and question["query_lang"] != self.lang and "translated_text" in question: - # query = question["translated_text"] - debug_info = question["debug_info"] - # query_repr = get_relevance_embedding(query, self.lang, self.embedding_model_endpoint, self.model_type) - # question["colbert"] = query_repr["colbert_vecs"][0] - filter = get_filter_list(question) - opensearch_bm25_results = self.__get_bm25_results(query, filter) - final_results = opensearch_bm25_results - doc_list = [] - content_set = set() - for result in final_results: - if result["doc"] in content_set: - continue - content_set.add(result["content"]) - result_metadata = { - "source": result["source"], - "retrieval_content": result["content"], - "retrieval_data": result["data"], - "retrieval_score": result["score"], - # Set common score for llm. - "score": result["score"], - } - if "figure" in result["detail"]["metadata"]: - result_metadata["figure"] = result["detail"]["metadata"]["figure"] - if "content_type" in result["detail"]["metadata"]: - result_metadata["content_type"] = result["detail"]["metadata"][ - "content_type" - ] - doc_list.append( - Document(page_content=result["doc"], metadata=result_metadata) - ) - if self.enable_debug: - debug_info[f"qd-bm25-recall-{self.index_name}"] = ( - remove_redundancy_debug_info(opensearch_bm25_results) - ) - return doc_list - - -def index_results_format(docs: list, threshold=-1): - results = [] - for doc in docs: - if doc.metadata["score"] < threshold: - continue - results.append( - { - "score": doc.metadata["score"], - "source": doc.metadata["source"], - "answer": doc.metadata["answer"], - "question": doc.metadata["question"], - } - ) - # output = {"answer": json.dumps(results, ensure_ascii=False), "sources": [], "contexts": []} - output = { - "answer": results, - "sources": [], - "contexts": [], - "context_docs": [], - "context_sources": [], - } - return output diff --git a/source/lambda/online/__functions/functions_utils/retriever/utils/aos_utils.py b/source/lambda/online/__functions/functions_utils/retriever/utils/aos_utils.py deleted file mode 100644 index be14d765..00000000 --- a/source/lambda/online/__functions/functions_utils/retriever/utils/aos_utils.py +++ /dev/null @@ -1,217 +0,0 @@ -import os -import threading - -import boto3 -from opensearchpy import OpenSearch, RequestsHttpConnection -from requests_aws4auth import AWS4Auth - -open_search_client_lock = threading.Lock() - -credentials = boto3.Session().get_credentials() - -region = boto3.Session().region_name -awsauth = AWS4Auth( - credentials.access_key, - credentials.secret_key, - region, - "es", - session_token=credentials.token, -) - -IMPORT_OPENSEARCH_PY_ERROR = ( - "Could not import OpenSearch. Please install it with `pip install opensearch-py`." -) - - -def _import_not_found_error(): - """Import not found error if available, otherwise raise error.""" - try: - from opensearchpy.exceptions import NotFoundError - except ImportError: - raise ImportError(IMPORT_OPENSEARCH_PY_ERROR) - return NotFoundError - - -class LLMBotOpenSearchClient: - instance = None - - def __new__(cls, host, auth=None): - with open_search_client_lock: - if cls.instance is not None and cls.instance.host == host: - return cls.instance - obj = object.__new__(cls) - cls.instance = obj - return obj - - def __init__(self, host, auth=None): - """ - Initialize OpenSearch client using OpenSearch Endpoint - """ - self.host = host - self.client = OpenSearch( - hosts=[ - { - "host": host.replace("https://", ""), - "port": int(os.environ.get("AOS_PORT", 443)), - } - ], - http_auth=auth if auth is not None else awsauth, - use_ssl=True, - verify_certs=True, - connection_class=RequestsHttpConnection, - ) - self.query_match = { - "knn": self._build_knn_search_query, - "exact": self._build_exactly_match_query, - "fuzzy": self._build_fuzzy_search_query, - "basic": self._build_basic_search_query, - } - - def _build_basic_search_query( - self, index_name, query_term, field, size, filter=None - ): - """ - Build basic search query - - :param index_name: Target Index Name - :param query_term: query term - :param field: search field - :param size: number of results to return from aos - - :return: aos response json - """ - query = { - "size": size, - "query": { - "bool": { - "should": [{"match_phrase": {field: query_term}}], - } - }, - "sort": [{"_score": {"order": "desc"}}], - } - if filter: - query["query"]["bool"]["filter"] = filter - - return query - - def _build_fuzzy_search_query( - self, index_name, query_term, field, size, filter=None - ): - """ - Build basic search query - - :param index_name: Target Index Name - :param query_term: query term - :param field: search field - :param size: number of results to return from aos - - :return: aos response json - """ - query = { - "size": size, - "query": {"match": {"text": query_term}}, - "_source": {"excludes": ["*.additional_vecs", "vector_field"]}, - } - if filter: - query["query"]["bool"]["filter"] = filter - - return query - - def _build_knn_search_query(self, index_name, query_term, field, size, filter=None): - """ - Build knn search query - - :param index_name: Target Index Name - :param query_term: query term - :param field: search field - :param size: number of results to return from aos - - :return: aos response json - """ - if filter: - query = { - "size": size, - "query": { - "bool": { - "filter": {"bool": {"must": filter}}, - "must": [{"knn": {field: {"vector": query_term, "k": size}}}], - } - }, - "_source": {"excludes": ["*.additional_vecs", "vector_field"]}, - } - else: - query = { - "size": size, - "query": {"knn": {field: {"vector": query_term, "k": size}}}, - "_source": {"excludes": ["*.additional_vecs", "vector_field"]}, - } - return query - - def _build_exactly_match_query(self, index_name, query_term, field, size): - """ - Build exactly match query - - :param index_name: Target Index Name - :param query_term: query term - :param field: search field - :param size: number of results to return from aos - - :return: aos response json - """ - query = {"query": {"match_phrase": {field: query_term}}} - return query - - def organize_results(self, query_type, response, field): - """ - Organize results from aos response - - :param query_type: query type - :param response: aos response json - """ - results = [] - aos_hits = response["hits"]["hits"] - if query_type == "exact": - for aos_hit in aos_hits: - doc = aos_hit["_source"][field] - source = aos_hit["_source"]["metadata"]["source"] - score = aos_hit["_score"] - results.append({"doc": doc, "score": score, "source": source}) - else: - for aos_hit in aos_hits: - doc = f"{aos_hit['_source'][field]}" - source = aos_hit["_source"]["metadata"]["source"] - score = aos_hit["_score"] - results.append({"doc": doc, "score": score, "source": source}) - return results - - def search( - self, - index_name, - query_type, - query_term, - field: str = "text", - size: int = 10, - filter=None, - ): - """ - Perform search on aos - - :param index_name: Target Index Name - :param query_type: query type - :param query_term: query term - :param field: search field - :param size: number of results to return from aos - :param filter: filter query - - :return: aos response json - """ - not_found_error = _import_not_found_error() - try: - self.client.indices.get(index=index_name) - except not_found_error: - return [] - query = self.query_match[query_type]( - index_name, query_term, field, size, filter - ) - response = self.client.search(body=query, index=index_name) - return response diff --git a/source/lambda/online/__functions/functions_utils/retriever/utils/context_utils.py b/source/lambda/online/__functions/functions_utils/retriever/utils/context_utils.py deleted file mode 100644 index 0b228a47..00000000 --- a/source/lambda/online/__functions/functions_utils/retriever/utils/context_utils.py +++ /dev/null @@ -1,81 +0,0 @@ -import logging -import os - -from langchain.docstore.document import Document - -from common_logic.common_utils.time_utils import timeit - -logger = logging.getLogger("context_utils") -logger.setLevel(logging.INFO) - - -def contexts_trunc(docs: list[dict], context_num=2): - docs = [doc for doc in docs[:context_num]] - # the most related doc will be placed last - docs.sort(key=lambda x: x["score"]) - # filter same docs - s = set() - context_strs = [] - context_docs = [] - context_sources = [] - for doc in docs: - content = doc["page_content"] - if content not in s: - context_strs.append(content) - s.add(content) - context_docs.append( - {"doc": content, - "source": doc["source"], "score": doc["score"]} - ) - context_sources.append(doc["source"]) - return { - "contexts": context_strs, - "context_docs": context_docs, - "context_sources": context_sources, - } - - -@timeit -def retriever_results_format( - docs: list[Document], - print_source=True, - print_content=os.environ.get("print_content", False), -): - doc_dicts = [] - - for doc in docs: - doc_dicts.append( - { - "page_content": doc.page_content, - "retrieval_score": doc.metadata["retrieval_score"], - "rerank_score": doc.metadata["score"], - "score": doc.metadata["score"], - "source": doc.metadata["source"], - "answer": doc.metadata.get("answer", ""), - "question": doc.metadata.get("question", ""), - "figure": doc.metadata.get("figure", []), - "retrieval_content": doc.metadata.get("retrieval_content", ""), - } - ) - - if print_source: - source_strs = [] - for doc_dict in doc_dicts: - content = "" - if print_content: - content = f', content: {doc_dict["page_content"]}' - source_strs.append( - f'source: {doc_dict["source"]}, score: {doc_dict["score"]}{content}, retrieval score: {doc_dict["retrieval_score"]}' - ) - logger.info("retrieved sources:\n" + "\n".join(source_strs)) - return doc_dicts - - -def documents_list_filter(doc_dicts: list[dict], filter_key="score", threshold=-1): - results = [] - for doc_dict in doc_dicts: - if doc_dict[filter_key] < threshold: - continue - results.append(doc_dict) - - return results diff --git a/source/lambda/online/__functions/functions_utils/retriever/utils/reranker.py b/source/lambda/online/__functions/functions_utils/retriever/utils/reranker.py deleted file mode 100644 index 7405d59d..00000000 --- a/source/lambda/online/__functions/functions_utils/retriever/utils/reranker.py +++ /dev/null @@ -1,217 +0,0 @@ -import json -import os -import time -import logging -import asyncio -import numpy as np -logger = logging.getLogger() -logger.setLevel(logging.INFO) - -from typing import Dict, Optional, Sequence, Any - -from langchain.callbacks.manager import Callbacks -from langchain.schema import Document -from langchain.retrievers.document_compressors.base import BaseDocumentCompressor - -from sm_utils import SagemakerEndpointVectorOrCross - -rerank_model_endpoint = os.environ.get("RERANK_ENDPOINT", "") - -"""Document compressor that uses BGE reranker model.""" -class BGEM3Reranker(BaseDocumentCompressor): - - """Number of documents to return.""" - def _colbert_score_np(self, q_reps, p_reps): - token_scores = np.einsum('nik,njk->nij', q_reps, p_reps) - scores = token_scores.max(-1) - scores = np.sum(scores) / q_reps.shape[0] - return scores - - async def __ainvoke_rerank_model(self, query_batch, doc_batch, loop): - return await loop.run_in_executor(None, - self._colbert_score_np, - np.asarray(query_batch), - np.asarray(doc_batch)) - - async def __spawn_task(self, query_colbert_list, doc_colbert_list): - batch_size = 1 - task_list = [] - loop = asyncio.get_event_loop() - for batch_start in range(0, len(query_colbert_list), batch_size): - task = asyncio.create_task(self.__ainvoke_rerank_model( - query_colbert_list[batch_start:batch_start + batch_size], - doc_colbert_list[batch_start:batch_start + batch_size], loop)) - task_list.append(task) - return await asyncio.gather(*task_list) - - def compress_documents( - self, - documents: Sequence[Document], - query: dict, - callbacks: Optional[Callbacks] = None, - ) -> Sequence[Document]: - """ - Compress documents using BGE M3 Colbert Score. - - Args: - documents: A sequence of documents to compress. - query: The query to use for compressing the documents. - callbacks: Callbacks to run during the compression process. - - Returns: - A sequence of compressed documents. - """ - start = time.time() - if len(documents) == 0: # to avoid empty api call - return [] - doc_list = list(documents) - _docs = [d.metadata["retrieval_data"]['colbert'] for d in doc_list] - - rerank_text_length = 1024 * 10 - query_colbert_list = [] - doc_colbert_list = [] - for doc in _docs: - query_colbert_list.append(query["colbert"][:rerank_text_length]) - doc_colbert_list.append(doc[:rerank_text_length]) - score_list = [] - logger.info(f'rerank pair num {len(query_colbert_list)}, m3 method: colbert score') - score_list = asyncio.run(self.__spawn_task(query_colbert_list, doc_colbert_list)) - final_results = [] - debug_info = query["debug_info"] - debug_info["knowledge_qa_rerank"] = [] - for doc, score in zip(doc_list, score_list): - doc.metadata["rerank_score"] = score - # set common score for llm. - doc.metadata["score"] = doc.metadata["rerank_score"] - final_results.append(doc) - debug_info["knowledge_qa_rerank"].append((doc.page_content, doc.metadata["retrieval_content"], doc.metadata["source"], score)) - final_results.sort(key=lambda x: x.metadata["rerank_score"], reverse=True) - debug_info["knowledge_qa_rerank"].sort(key=lambda x: x[-1], reverse=True) - recall_end_time = time.time() - elpase_time = recall_end_time - start - logger.info(f"runing time of rerank: {elpase_time}s seconds") - return final_results - -"""Document compressor that uses BGE reranker model.""" -class BGEReranker(BaseDocumentCompressor): - - """Number of documents to return.""" - config: Dict={"run_name": "BGEReranker"} - enable_debug: Any - target_model: Any - rerank_model_endpoint: str=rerank_model_endpoint - top_k: int=10 - - def __init__(self,enable_debug=False, rerank_model_endpoint=rerank_model_endpoint, target_model=None, top_k=10): - super().__init__() - self.enable_debug = enable_debug - self.rerank_model_endpoint = rerank_model_endpoint - self.target_model = target_model - self.top_k = top_k - - async def __ainvoke_rerank_model(self, batch, loop): - logging.info("invoke endpoint") - return await loop.run_in_executor(None, - SagemakerEndpointVectorOrCross, - json.dumps(batch), - self.rerank_model_endpoint, - None, - "rerank", - None, - self.target_model) - - async def __spawn_task(self, rerank_pair): - batch_size = 128 - task_list = [] - loop = asyncio.get_event_loop() - for batch_start in range(0, len(rerank_pair), batch_size): - task = asyncio.create_task(self.__ainvoke_rerank_model(rerank_pair[batch_start:batch_start + batch_size], loop)) - task_list.append(task) - return await asyncio.gather(*task_list) - - def compress_documents( - self, - documents: Sequence[Document], - query: str, - callbacks: Optional[Callbacks] = None, - ) -> Sequence[Document]: - """ - Compress documents using BGE rerank model. - - Args: - documents: A sequence of documents to compress. - query: The query to use for compressing the documents. - callbacks: Callbacks to run during the compression process. - - Returns: - A sequence of compressed documents. - """ - start = time.time() - if len(documents) == 0: # to avoid empty api call - return [] - doc_list = list(documents) - _docs = [d.metadata["retrieval_content"] for d in doc_list] - - rerank_pair = [] - rerank_text_length = 1024 * 10 - for doc in _docs: - rerank_pair.append([query["query"], doc[:rerank_text_length]]) - score_list = [] - logger.info(f'rerank pair num {len(rerank_pair)}, endpoint_name: {self.rerank_model_endpoint}') - response_list = asyncio.run(self.__spawn_task(rerank_pair)) - for response in response_list: - score_list.extend(json.loads(response)) - final_results = [] - debug_info = query["debug_info"] - debug_info["knowledge_qa_rerank"] = [] - for doc, score in zip(doc_list, score_list): - doc.metadata["rerank_score"] = score - # set common score for llm. - doc.metadata["retrieval_score"] = doc.metadata["retrieval_score"] - doc.metadata["score"] = doc.metadata["rerank_score"] - final_results.append(doc) - if self.enable_debug: - debug_info["knowledge_qa_rerank"].append((doc.page_content, doc.metadata["retrieval_content"], doc.metadata["source"], score)) - final_results.sort(key=lambda x: x.metadata["rerank_score"], reverse=True) - debug_info["knowledge_qa_rerank"].sort(key=lambda x: x[-1], reverse=True) - recall_end_time = time.time() - elpase_time = recall_end_time - start - logger.info(f"runing time of rerank: {elpase_time}s seconds") - return final_results[:self.top_k] - -"""Document compressor that uses retriever score.""" -class MergeReranker(BaseDocumentCompressor): - - """Number of documents to return.""" - - def compress_documents( - self, - documents: Sequence[Document], - query: str, - callbacks: Optional[Callbacks] = None, - ) -> Sequence[Document]: - """ - Compress documents using BGE rerank model. - - Args: - documents: A sequence of documents to compress. - query: The query to use for compressing the documents. - callbacks: Callbacks to run during the compression process. - - Returns: - A sequence of compressed documents. - """ - start = time.time() - if len(documents) == 0: # to avoid empty api call - return [] - final_results = [] - debug_info = query["debug_info"] - debug_info["knowledge_qa_rerank"] = [] - final_results = list(documents) - final_results.sort(key=lambda x: x.metadata["score"], reverse=True) - debug_info["knowledge_qa_rerank"].append([(doc.page_content, doc.metadata["retrieval_content"], - doc.metadata["source"], doc.metadata["score"]) for doc in final_results]) - recall_end_time = time.time() - elpase_time = recall_end_time - start - logger.info(f"runing time of rerank: {elpase_time}s seconds") - return final_results \ No newline at end of file diff --git a/source/lambda/online/__functions/functions_utils/retriever/utils/test.py b/source/lambda/online/__functions/functions_utils/retriever/utils/test.py deleted file mode 100644 index 2c7daa75..00000000 --- a/source/lambda/online/__functions/functions_utils/retriever/utils/test.py +++ /dev/null @@ -1,176 +0,0 @@ -import json -import os - -os.environ["PYTHONUNBUFFERED"] = "1" -import logging -import sys - -import boto3 -from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper -from lambda_retriever.utils.aos_retrievers import ( - QueryDocumentBM25Retriever, - QueryDocumentKNNRetriever, - QueryQuestionRetriever, -) -from lambda_retriever.utils.context_utils import retriever_results_format -from lambda_retriever.utils.reranker import MergeReranker -from langchain.retrievers import ( - AmazonKnowledgeBasesRetriever, - ContextualCompressionRetriever, -) -from langchain.retrievers.merger_retriever import MergerRetriever -from langchain.schema.runnable import RunnableLambda, RunnablePassthrough -from langchain_community.retrievers import AmazonKnowledgeBasesRetriever - -logger = logging.getLogger("retriever") -logger.setLevel(logging.INFO) - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -sys.path.append(os.path.dirname(SCRIPT_DIR)) - -region = boto3.Session().region_name - -knowledgebase_client = boto3.client("bedrock-agent-runtime", region) -sm_client = boto3.client("sagemaker-runtime") - - -def get_bedrock_kb_retrievers(knowledge_base_id_list, top_k: int): - retriever_list = [ - AmazonKnowledgeBasesRetriever( - knowledge_base_id=knowledge_base_id, - retrieval_config={"vectorSearchConfiguration": {"numberOfResults": top_k}}, - ) - for knowledge_base_id in knowledge_base_id_list - ] - return retriever_list - - -def get_custom_qd_retrievers(retriever_config, using_bm25=False): - default_qd_config = { - "using_whole_doc": False, - "context_num": 1, - "top_k": 10, - "query_key": "query", - } - # qd_config = {**default_qd_config, **qd_config} - retriever_list = [QueryDocumentKNNRetriever(retriever_config)] - if using_bm25: - retriever_list += [QueryDocumentBM25Retriever(retriever_config)] - return retriever_list - - -def get_custom_qq_retrievers(retriever_config): - default_qq_config = {"top_k": 10, "query_key": "query"} - - return [ - QueryQuestionRetriever( - retriever_config, - # **qq_config - ) - ] - - -def get_whole_chain(retriever_list, reranker_config): - lotr = MergerRetriever(retrievers=retriever_list) - # if len(reranker_config): - # default_reranker_config = { - # "enable_debug": False, - # "target_model": "bge_reranker_model.tar.gz", - # "query_key": "query", - # "top_k": 10 - # } - # reranker_config = {**default_reranker_config, **reranker_config} - # compressor = BGEReranker(**reranker_config) - # else: - compressor = MergeReranker() - - compression_retriever = ContextualCompressionRetriever( - base_compressor=compressor, base_retriever=lotr - ) - whole_chain = RunnablePassthrough.assign( - docs=compression_retriever | RunnableLambda(retriever_results_format) - ) - return whole_chain - - -def get_custom_retrievers(retriever_config, retriever_type="qd"): - retriever_dict = { - "qq": get_custom_qq_retrievers, - "qd": get_custom_qd_retrievers, - "bedrock_kb": get_bedrock_kb_retrievers, - } - # retriever_type = retriever_config["type"] - return retriever_dict[retriever_type](retriever_config) - - -@chatbot_lambda_call_wrapper -def lambda_handler(event, context=None): - event_body = event - retriever_list = [] - retriever_type = event_body["type"] - for retriever_config in event_body["retrievers"]: - # retriever_type = retriever_config["type"] - retriever_list.extend(get_custom_retrievers(retriever_config, retriever_type)) - - # Re-rank not used. - # rerankers = event_body.get("rerankers", None) - # if rerankers: - # reranker_config = rerankers[0]["config"] - # else: - # reranker_config = {} - reranker_config = {} - if len(retriever_list) > 0: - whole_chain = get_whole_chain(retriever_list, reranker_config) - else: - whole_chain = RunnablePassthrough.assign(docs=lambda x: []) - docs = whole_chain.invoke({"query": event_body["query"], "debug_info": {}}) - return {"code": 0, "result": docs} - - -if __name__ == "__main__": - query = """test""" - event = { - "body": json.dumps( - { - "retrievers": [ - { - "index": "test-intent", - "config": {"top_k": "3"}, - "embedding": { - "type": "Bedrock", - "model_id": "cohere.embed-multilingual-v3", - }, - } - ], - "query": query, - "type": "qq", - } - ) - } - - event2 = { - "body": json.dumps( - { - "retrievers": [ - { - "index": "test-qa", - "config": { - "top_k": "3", - "vector_field_name": "sentence_vector", - "text_field_name": "paragraph", - "source_field_name": "source", - }, - "embedding": { - "type": "Bedrock", - "model_id": "amazon.titan-embed-text-v2:0", - }, - } - ], - "query": query, - "type": "qd", - } - ) - } - - response = lambda_handler(event2, None) - print(response) diff --git a/source/lambda/online/__functions/functions_utils/retriever/utils/websearch_retrievers.py b/source/lambda/online/__functions/functions_utils/retriever/utils/websearch_retrievers.py deleted file mode 100644 index babdeb9b..00000000 --- a/source/lambda/online/__functions/functions_utils/retriever/utils/websearch_retrievers.py +++ /dev/null @@ -1,124 +0,0 @@ -import asyncio -import aiohttp -import time -import re -from bs4 import BeautifulSoup -import os -from typing import Any, Dict, List -import logging -logger = logging.getLogger() -logger.setLevel(logging.INFO) - -from langchain_community.utilities import GoogleSearchAPIWrapper -from langchain.callbacks.manager import CallbackManagerForRetrieverRun -from langchain.docstore.document import Document -from langchain.schema.retriever import BaseRetriever -from langchain.agents import Tool - -GOOGLE_API_KEY=os.environ.get('GOOGLE_API_KEY',None) -GOOGLE_CSE_ID=os.environ.get('GOOGLE_CSE_ID',None) - - -class GoogleSearchTool(): - tool:Tool - topk:int = 5 - - def __init__(self,top_k=5): - self.topk = top_k - search = GoogleSearchAPIWrapper() - def top_results(query): - return search.results(query, self.topk) - self.tool = Tool( - name="Google Search Snippets", - description="Search Google for recent results.", - func=top_results, - ) - - def run(self,query): - return self.tool.run(query) - -def remove_html_tags(text): - soup = BeautifulSoup(text, 'html.parser') - text = soup.get_text() - text = re.sub(r'\r{1,}',"\n\n",text) - text = re.sub(r'\t{1,}',"\t",text) - text = re.sub(r'\n{2,}',"\n\n",text) - return text - -async def fetch(session, url, timeout): - try: - async with session.get(url) as response: - return await asyncio.wait_for(response.text(), timeout=timeout) - except asyncio.TimeoutError: - print(f"timeout:{url}") - return '' - except Exception as e: - print(f"ClientError:{url}", str(e)) - return '' - - -async def fetch_all(urls, timeout): - async with aiohttp.ClientSession() as session: - tasks = [] - for url in urls: - task = asyncio.create_task(fetch(session, url, timeout)) - tasks.append(task) - - results = await asyncio.gather(*tasks) - return results - -def web_search(**args): - if not GOOGLE_API_KEY or not GOOGLE_CSE_ID: - logger.info('Missing google API key') - return [] - tool = GoogleSearchTool(args['top_k']) - result = tool.run(args['query']) - return [item for item in result if 'title' in item and 'link' in item and 'snippet' in item] - - -def add_webpage_content(snippet_results): - t1 = time.time() - urls = [item['doc_author'] for item in snippet_results] - loop = asyncio.get_event_loop() - fetch_results = loop.run_until_complete(fetch_all(urls,5)) - t2= time.time() - logger.info(f'deep web search time:{t2-t1:1f}s') - final_results = [] - for i, result in enumerate(fetch_results): - if not result: - continue - page_content = remove_html_tags(result) - final_results.append({**snippet_results[i], - 'doc':snippet_results[i]['doc']+'\n'+page_content[:10000] - }) - return final_results - -class GoogleRetriever(BaseRetriever): - search: Any - result_num: Any - - def __init__(self, result_num): - super().__init__() - self.result_num = result_num - - def _get_relevant_documents( - self, question: Dict, *, run_manager: CallbackManagerForRetrieverRun - ) -> List[Document]: - query = question[self.query_key] - result_list = web_search(query=query, top_k=self.result_num) - doc_list = [] - for result in result_list: - doc_list.append( - Document( - page_content=result["doc"], - metadata={ - "source": result["link"], - "retrieval_content": result["title"] + '\n' + result["snippet"], - "retrieval_data": result["title"] - } - ) - ) - return doc_list - - def get_whole_doc(self, results) -> Dict: - return add_webpage_content(self._get_relevant_documents(results)) \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_aws_qa_tools/__init__.py b/source/lambda/online/__functions/lambda_aws_qa_tools/__init__.py deleted file mode 100644 index 635eee67..00000000 --- a/source/lambda/online/__functions/lambda_aws_qa_tools/__init__.py +++ /dev/null @@ -1,177 +0,0 @@ -from common_logic.common_utils.constant import SceneType,ToolRuningMode -from .._tool_base import tool_manager -from . import ( - check_service_availability, - explain_abbr, - service_org, - aws_ec2_price, - transfer -) - -SCENE = SceneType.AWS_QA -LAMBDA_NAME = "lambda_aws_qa_tools" - -tool_manager.register_tool({ - "name": "service_availability", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": check_service_availability.lambda_handler, - "tool_def":{ - "name": "service_availability", - "description":"query the availability of service in specified region", - "parameters":{ - "type":"object", - "properties":{ - "service":{ - "type":"string", - "description":"the AWS service name" - }, - "region":{ - "type":"string", - "description":"the AWS region name where the service is located in, for example us-east-1(N.Virginal), us-west-2(Oregon), eu-west-2(London), ap-southeast-1(Singapore)" - } - }, - "required":[ - "service", - "region" - ] - }, - "running_mode": ToolRuningMode.LOOP - } -}) - -tool_manager.register_tool({ - "name": "explain_abbr", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": explain_abbr.lambda_handler, - "tool_def":{ - "name": "explain_abbr", - "description": "explain abbreviation for user", - "parameters": { - "type": "object", - "properties": { - "abbr": { - "type": "string", - "description": "the abbreviation of terms in AWS" - } - }, - "required": ["abbr"] - }, - "running_mode": ToolRuningMode.ONCE - } -}) - - -tool_manager.register_tool({ - "name": "get_contact", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": service_org.lambda_handler, - "tool_def":{ - "name":"get_contact", - "description":"query the contact person in the 'SSO' organization", - "parameters":{ - "type":"object", - "properties":{ - "employee":{ - "type":"string", - "description":"employee name in the 'SSO' organization" - }, - "role":{ - "type":"string", - "description":"employee's role, usually it's Sales, Product Manager, Tech, Program Manager, Leader" - }, - "domain":{ - "type":"string", - "description":"Techical domain for the employee,For Example AIML, Analytics, Compute" - }, - "scope":{ - "type":"string", - "description":"employee's scope of responsibility. For Sales role, it could be territory like north/east/south/west, For tech role, it could be specific service" - } - }, - "required":[ - "employee" - ] - }, - "running_mode": ToolRuningMode.LOOP - } -}) - -tool_manager.register_tool({ - "name": "ec2_price", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": aws_ec2_price.lambda_handler, - "tool_def": { - "name": "ec2_price", - "description": "query the price of AWS ec2 instance", - "parameters": { - "type": "object", - "properties": { - "instance_type": { - "type": "string", - "description": "the AWS ec2 instance type, for example, c5.xlarge, m5.large, t3.mirco, g4dn.2xlarge, if it is a partial of the instance type, you should try to auto complete it. for example, if it is r6g.2x, you can complete it as r6g.2xlarge" - }, - "region": { - "type": "string", - "description": "the AWS region name where the ec2 is located in, for example us-east-1, us-west-1, if it is common words such as 'us east 1','美东1','美西2',you should try to normalize it to standard AWS region name, for example, 'us east 1' is normalized to 'us-east-1', '美东2' is normalized to 'us-east-2','美西2' is normalized to 'us-west-2','北京' is normalized to 'cn-north-1', '宁夏' is normalized to 'cn-northwest-1', '中国区' is normalized to 'cn-north-1'" - }, - "os": { - "type": "string", - "description": "the operating system of ec2 instance, the valid value should be 'Linux' or 'Windows'" - }, - "term": { - "type": "string", - "description": "the payment term, the valid value should be 'OnDemand' or 'Reserved' " - }, - "purchase_option": { - "type": "string", - "description": "the purchase option of Reserved instance, the valid value should be 'No Upfront', 'Partial Upfront' or 'All Upfront' " - } - }, - "required": ["instance_type"] - }, - "running_mode": ToolRuningMode.LOOP - } -}) - - -tool_manager.register_tool({ - "name":"transfer", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": transfer.lambda_handler, - "tool_def": { - "name": "转人工", - "description": "转人工" - }, - "running_mode": ToolRuningMode.ONCE -}) - -# tool_manager.register_tool({ -# "name":"assist", -# "lambda_name": "", -# "lambda_module_path": "", -# "tool_def": { -# "name": "assist", -# "description": "assist user to do some office work", -# "parameters": { -# "type": "object", -# "properties": { -# "response": { -# "description": "Response to user", -# "type": "string" -# } -# }, -# "required": ["response"] -# }, -# }, -# "running_mode":ToolRuningMode.ONCE -# }) - - - - - diff --git a/source/lambda/online/__functions/lambda_aws_qa_tools/aws_ec2_price.py b/source/lambda/online/__functions/lambda_aws_qa_tools/aws_ec2_price.py deleted file mode 100644 index b307879b..00000000 --- a/source/lambda/online/__functions/lambda_aws_qa_tools/aws_ec2_price.py +++ /dev/null @@ -1,191 +0,0 @@ - -import json -from typing import Union,Optional, Union -import os -import boto3 -import requests -from pydantic import BaseModel,ValidationInfo, field_validator, Field -import re - -class EC2PriceRequest(BaseModel): - region: Optional[str] = Field (description='region name', default='us-east-1') - term: Optional[str] = Field (description='purchase term', default='OnDemand') - instance_type: str - purchase_option: Optional[str] = Field (description='purchase option', default='') - os:Optional[str] = Field(description='Operation system', default='Linux') - - @classmethod - def validate_ec2_instance_type(cls,instance_type): - # support other instance ml.m5.xlarge - # pattern = r'^(?:[a-z0-9][a-z0-9.-]*[a-z0-9])?(?:[a-z](?:[a-z0-9-]*[a-z0-9])?)?(\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.[a-z0-9]{2,63}$' - ## only ec2, for m5.xlarge - pattern = r"^([a-z0-9]+\.[a-z0-9]+)$" - - return re.match(pattern, instance_type) is not None and not instance_type.endswith(".") - - @classmethod - def validate_region_name(cls,region_name): - pattern = r"^[a-z]{2}(-gov)?-(central|east|north|south|west|northeast|northwest|southeast|southwest)-\d$" - return re.match(pattern, region_name) is not None - - @field_validator('region') - def validate_region(cls, value:str,info: ValidationInfo): - if not cls.validate_region_name(value): - raise ValueError(f"{value} is not a valid AWS region name.") - return value - - @field_validator('term') - def validate_term(cls, value:str,info: ValidationInfo): - allowed_values = ['OnDemand','Reserved'] - if value not in allowed_values: - raise ValueError(f'value must be one of {allowed_values}') - return value - - @field_validator('purchase_option') - def validate_option(cls, value:str,info: ValidationInfo): - allowed_values = ['No Upfront','All Upfront','Partial Upfront',''] - if value not in allowed_values: - raise ValueError(f'value must be one of {allowed_values}') - return value - - @field_validator('os') - def validate_os(cls, value:str,info: ValidationInfo): - allowed_values = ['Linux','Windows'] - if value not in allowed_values: - raise ValueError(f'value must be one of {allowed_values}') - return value - - @field_validator('instance_type') - def validate_instance_type(cls, value:str,info: ValidationInfo): - if not cls.validate_ec2_instance_type(value): - raise ValueError(f'{value} is not a valid EC2 instance type name.') - return value - -def purchase_option_filter(term_attri:dict, value:str) -> dict: - if not value: - return True - if term_attri: - purchaseOption = term_attri.get('PurchaseOption') - if purchaseOption == value: - return True - return None - - -def remote_proxy_call(**args): - api = os.environ.get('api_endpoint') - key = os.environ.get('api_key') - payload = json.dumps(args) - if not api or not key: - return None - try: - resp = requests.post(api,headers={"Content-Type":"application/json","Authorization":f"Bearer {key}"},data=payload) - data = resp.json() - return data.get('message') - except Exception as e: - print(e) - return None - - -def query_ec2_price(args) -> Union[str,None]: - request = EC2PriceRequest(**args) - region = request.region - term = request.term - instance_type = request.instance_type - os = request.os - purchase_option = request.purchase_option - if region.startswith('cn-'): - return remote_proxy_call(**args) - else: - pricing_client = boto3.client('pricing',region_name='us-east-1') - def parse_price(products,term): - ret = [] - for product in products: - product = json.loads(product) - on_demand_terms = product['terms'].get(term) - if on_demand_terms and term == 'Reserved': - for _, term_details in on_demand_terms.items(): - price_dimensions = term_details['priceDimensions'] - term_attri = term_details.get('termAttributes') - is_valid = purchase_option_filter(term_attri,purchase_option) - option = term_attri.get('PurchaseOption') - if is_valid: - for _, price_dimension in price_dimensions.items(): - price = price_dimension['pricePerUnit']['CNY'] if region.startswith('cn-') else price_dimension['pricePerUnit']['USD'] - dollar = 'CNY' if region.startswith('cn-') else 'USD' - desc = price_dimension['description'] - unit = price_dimension['unit'] - if not desc.startswith("$0.00 per") and not desc.startswith("USD 0.0 per") \ - and not desc.startswith("0.00 CNY per") and not desc.startswith("CNY 0.0 per"): - ret.append(f"Region: {region}, Purchase option: {option}, Lease contract length: {term_attri.get('LeaseContractLength')}, Offering Class: {term_attri.get('OfferingClass')}, Price per {unit}: {dollar} {price} , description: {desc}") - elif on_demand_terms: - for _, term_details in on_demand_terms.items(): - price_dimensions = term_details['priceDimensions'] - if price_dimensions: - for _, price_dimension in price_dimensions.items(): - price = price_dimension['pricePerUnit']['CNY'] if region.startswith('cn-') else price_dimension['pricePerUnit']['USD'] - desc = price_dimension['description'] - unit = price_dimension['unit'] - if not desc.startswith("$0.00 per") and not desc.startswith("USD 0.0 per") and not desc.startswith("0.00 CNY per"): - ret.append(f"Region: {region}, Price per {unit}: {price}, description: {desc}") - return ret - filters = [ - { - 'Type': 'TERM_MATCH', - 'Field': 'instanceType', - 'Value': instance_type - }, - { - 'Type': 'TERM_MATCH', - 'Field': 'ServiceCode', - 'Value': 'AmazonEC2' - }, - { - 'Type': 'TERM_MATCH', - 'Field': 'regionCode', - 'Value': region - }, - { - 'Type': 'TERM_MATCH', - 'Field': 'tenancy', - 'Value': 'Shared' - }, - { - 'Type': 'TERM_MATCH', - 'Field': 'operatingSystem', - 'Value': os - } - ] - - if purchase_option: - filters = filters + [{ - 'Type': 'TERM_MATCH', - 'Field': 'PurchaseOption', - 'Value': purchase_option - }] - - response = pricing_client.get_products( - ServiceCode='AmazonEC2', - Filters=filters - ) - products = response['PriceList'] - prices = parse_price(products,term) - - return '\n'.join(prices) if prices else None - -def lambda_handler(event, context=None): - ''' - event: { - "body": "{ - \"instance_type\":\"m5.xlarge\", - \"region\":\"us-east-1\", - \"term\":\"eserved\", - \"purchase_option\":\"All Upfront\" - }" - } - ''' - result = query_ec2_price(event["kwargs"]) - return {"code":0, "result": result} - -if __name__ == "__main__": - args = {'instance_type':'m5.xlarge','region':'us-east-1','term':'Reserved','purchase_option':'All Upfront'} - print(query_ec2_price(args)) \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_aws_qa_tools/check_service_availability.py b/source/lambda/online/__functions/lambda_aws_qa_tools/check_service_availability.py deleted file mode 100644 index a3bd18c0..00000000 --- a/source/lambda/online/__functions/lambda_aws_qa_tools/check_service_availability.py +++ /dev/null @@ -1,80 +0,0 @@ - -import json -from typing import Union,Optional, Union -import os -import boto3 -import requests -from pydantic import BaseModel,ValidationInfo, field_validator, Field -import re - -def get_all_regions(): - ec2 = boto3.client('ec2') - regions = ec2.describe_regions() - return [region['RegionName'] for region in regions['Regions']] + ['cn-north-1', 'cn-northwest-1'] - -def get_all_services(): - session = boto3.Session() - services = session.get_available_services() - return services - -class ServiceAvailabilityRequest(BaseModel): - region: str = Field (description='region name') - service: str = Field (description='service name') - - # def __init__(self, **data): - # super().__init__(**data) - # self.region = self.region.lower() - # self.service = self.service.lower() - - @field_validator('region') - @classmethod - def validate_region(cls, region): - if region not in region_list: - raise ValueError("region must be in aws region list.") - return region - - @field_validator('service') - @classmethod - def validate_service(cls, service): - if service not in service_list: - raise ValueError("service must be in aws service list.") - return service - -region_list = get_all_regions() -service_list = get_all_services() - -def check_service_availability(args): - try: - request = ServiceAvailabilityRequest(**args) - except Exception as e: - return str(e) - service = request.service - region = request.region - try: - # Attempt to create a client for the specified service in the specified region - boto3.client(service, region_name=region) - return "available" - except Exception as e: - # Handle exceptions, which may indicate that the service is not available in the region - print(f"Service {service} is not available in {region}: {e}") - return "unavailable" - -def lambda_handler(event, context=None): - ''' - event: { - "body": "{ - \"instance_type\":\"m5.xlarge\", - \"region\":\"us-east-1\", - \"term\":\"eserved\", - \"purchase_option\":\"All Upfront\" - }" - } - ''' - result = check_service_availability(event) - return {"code":0, "result": result} - -if __name__ == "__main__": - # Example usage - args = {'service':'bedrock','region':'cn-north-1'} - is_available = check_service_availability(args) - print(f'Service {args["service"]} is available in {args["region"]}: {is_available}') \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_aws_qa_tools/comfort.py b/source/lambda/online/__functions/lambda_aws_qa_tools/comfort.py deleted file mode 100644 index 04bd4d97..00000000 --- a/source/lambda/online/__functions/lambda_aws_qa_tools/comfort.py +++ /dev/null @@ -1,3 +0,0 @@ -# give comfort response -def lambda_handler(event_body,context=None): - return {"code": 0, "result": "不好意思没能帮到您,是否帮你转人工客服?"} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_aws_qa_tools/explain_abbr.py b/source/lambda/online/__functions/lambda_aws_qa_tools/explain_abbr.py deleted file mode 100644 index 76743bac..00000000 --- a/source/lambda/online/__functions/lambda_aws_qa_tools/explain_abbr.py +++ /dev/null @@ -1,4 +0,0 @@ -# explain abbr tool -def lambda_handler(event_body,context=None): - return {"code": 0, "result":event_body['kwargs']['abbr']} - \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_aws_qa_tools/service_org.py b/source/lambda/online/__functions/lambda_aws_qa_tools/service_org.py deleted file mode 100644 index 235f324a..00000000 --- a/source/lambda/online/__functions/lambda_aws_qa_tools/service_org.py +++ /dev/null @@ -1,80 +0,0 @@ -from langchain.prompts import PromptTemplate -from langchain.chains import LLMChain -from langchain.llms.bedrock import Bedrock -from common_logic.common_utils.constant import LLMModelType -import boto3 -import os - -BEDROCK_REGION = os.environ.get('region','us-west-2') -def service_org(**args): - context = """placeholder""" - - prompt_tmp = """ - 你是云服务AWS的智能客服机器人AWSBot - - 给你 SSO (Service Specialist Organization) 的组织信息 - {context} - - Job role (角色, 岗位类型) description: - - GTMS: Go To Market Specialist - - SS: Specialist Sales - - SSA: Specialist Solution Architechure - - TPM: - - PM: Project Manager - - Scope means job scope - service_name equal to business unit - - If the context does not contain the knowleage for the question, truthfully says you does not know. - Don't put two people's names together. For example, zheng zhang not equal to zheng hao and xueqing not equal to Xueqing Lai - - Find out the most relevant context, and give the answer according to the context - Skip the preamble; go straight to the point. - Only give the final answer. - Do not repeat similar answer. - 使用中文回复,人名不需要按照中文习惯回复 - - {question} - """ - - def create_prompt_templete(prompt_template): - PROMPT = PromptTemplate( - template=prompt_template, - input_variables=["context",'question','chat_history'] - ) - return PROMPT - - - boto3_bedrock = boto3.client( - service_name="bedrock-runtime", - region_name=BEDROCK_REGION - ) - - parameters = { - "max_tokens_to_sample": 8096, - "stop_sequences": ["\nObservation"], - "temperature":0.01, - "top_p":0.85 - } - - model_id = LLMModelType.CLAUDE_2 - llm = Bedrock(model_id=model_id, client=boto3_bedrock, model_kwargs=parameters) - - prompt = create_prompt_templete(prompt_tmp) - llmchain = LLMChain(llm=llm,verbose=False,prompt = prompt) - answer = llmchain.run({'question':args.get('query'), "context": context}) - answer = answer.strip() - return answer - -def lambda_handler(event, context=None): - ''' - event: { - \"query\":\"Bruce负责哪一块?\" - } - ''' - result = service_org(**event['kwargs']) - return {"code":0, "result": result} - -if __name__ == "__main__": - args = {'query': 'Bruce负责哪一块?'} - print(service_org(**args)) \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_aws_qa_tools/transfer.py b/source/lambda/online/__functions/lambda_aws_qa_tools/transfer.py deleted file mode 100644 index 7fa33fcf..00000000 --- a/source/lambda/online/__functions/lambda_aws_qa_tools/transfer.py +++ /dev/null @@ -1,3 +0,0 @@ -# give transfer response -def lambda_handler(event_body,context=None): - return {"code": 0, "result": "立即为您转人工客服,请稍后"} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_common_tools/__init__.py b/source/lambda/online/__functions/lambda_common_tools/__init__.py deleted file mode 100644 index c5706989..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/__init__.py +++ /dev/null @@ -1,121 +0,0 @@ -from common_logic.common_utils.constant import SceneType, ToolRuningMode -from .._tool_base import tool_manager -from . import ( - get_weather, - give_rhetorical_question, - give_final_response, - chat, - rag -) - - -SCENE = SceneType.COMMON -LAMBDA_NAME = "lambda_common_tools" - -tool_manager.register_tool({ - "name": "get_weather", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": get_weather.lambda_handler, - "tool_def": { - "name": "get_weather", - "description": "Get the current weather for `city_name`", - "parameters": { - "type": "object", - "properties": { - "city_name": { - "description": "The name of the city to be queried", - "type": "string" - }, - }, - "required": ["city_name"] - } - }, - "running_mode": ToolRuningMode.LOOP -}) - - -tool_manager.register_tool( - { - "name": "give_rhetorical_question", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_rhetorical_question.lambda_handler, - "tool_def": { - "name": "give_rhetorical_question", - "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", - "parameters": { - "type": "object", - "properties": { - "question": { - "description": "The rhetorical question to user", - "type": "string" - }, - }, - "required": ["question"], - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool( - { - "name": "give_final_response", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_final_response.lambda_handler, - "tool_def": { - "name": "give_final_response", - "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", - "parameters": { - "type": "object", - "properties": { - "response": { - "description": "Response to user", - "type": "string" - } - }, - "required": ["response"] - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool({ - "name": "chat", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": chat.lambda_handler, - "tool_def": { - "name": "chat", - "description": "casual talk with AI", - "parameters": { - "type": "object", - "properties": { - "response": { - "description": "response to users", - "type": "string" - }}, - "required": ["response"] - }, - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name": "rag_tool", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": rag.lambda_handler, - "tool_def": { - "name": "rag_tool", - "description": "private knowledge", - "parameters": {} - }, - "running_mode": ToolRuningMode.ONCE -}) diff --git a/source/lambda/online/__functions/lambda_common_tools/chat.py b/source/lambda/online/__functions/lambda_common_tools/chat.py deleted file mode 100644 index 7f4cb60d..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/chat.py +++ /dev/null @@ -1,7 +0,0 @@ -# give chat response -def lambda_handler(event_body,context=None): - try: - result = event_body['kwargs']['response'] - return {"code": 0, "result":result} - except KeyError: - return {"code": 1, "result": "The parameter “response” not found."} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_common_tools/comparison_rag.py b/source/lambda/online/__functions/lambda_common_tools/comparison_rag.py deleted file mode 100644 index 3bf57396..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/comparison_rag.py +++ /dev/null @@ -1,51 +0,0 @@ -# knowledge base retrieve -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.constant import ( - LLMTaskType -) - -def knowledge_base_retrieve(retriever_params, context=None): - output: str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler", - ) - contexts = [doc["page_content"] for doc in output["result"]["docs"]] - return contexts - -def lambda_handler(event_body, context=None): - state = event_body['state'] - retriever_params = state["chatbot_config"]["comparison_rag_config"]["retriever_config"] - contexts = [] - retriever_params["query"] = event_body['kwargs']['query_a'] - contexts.extend(knowledge_base_retrieve(retriever_params, context=context)) - retriever_params["query"] = event_body['kwargs']['query_b'] - contexts.extend(knowledge_base_retrieve(retriever_params, context=context)) - context = "\n\n".join(contexts) - - # llm generate - system_prompt = (f"请根据context内的信息回答问题:\n" - "\n" - " - 回复内容需要展现出礼貌。回答内容为一句话,言简意赅。\n" - " - 使用中文回答。\n" - "\n" - "\n" - f"{context}\n" - "" - ) - - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_daily_reception_config']['llm_config'], - "system_prompt": system_prompt, - "intent_type": LLMTaskType.CHAT}, - "llm_input": {"query": state['query'], "chat_history": state['chat_history']} - } - ) - - return {"code":0, "result":output} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_common_tools/get_weather.py b/source/lambda/online/__functions/lambda_common_tools/get_weather.py deleted file mode 100644 index 9f19fada..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/get_weather.py +++ /dev/null @@ -1,34 +0,0 @@ -# test tool -import requests - -def get_weather(city_name:str): - if not isinstance(city_name, str): - raise TypeError("City name must be a string") - - key_selection = { - "current_condition": [ - "temp_C", - "FeelsLikeC", - "humidity", - "weatherDesc", - "observation_time", - ], - } - - try: - resp = requests.get(f"https://wttr.in/{city_name}?format=j1") - resp.raise_for_status() - resp = resp.json() - ret = {k: {_v: resp[k][0][_v] for _v in v} for k, v in key_selection.items()} - except: - import traceback - - ret = ("Error encountered while fetching weather data!\n" + traceback.format_exc() - ) - - return str(ret) - - -def lambda_handler(event_body,context=None): - result = get_weather(**event_body['kwargs']) - return {"code":0, "result": result} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_common_tools/give_final_response.py b/source/lambda/online/__functions/lambda_common_tools/give_final_response.py deleted file mode 100644 index 654ad136..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/give_final_response.py +++ /dev/null @@ -1,7 +0,0 @@ -# give final response -def lambda_handler(event_body,context=None): - try: - result = event_body['kwargs']['response'] - return {"code": 0, "result":result} - except KeyError: - return {"code": 1, "result": "The parameter “response” not found."} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_common_tools/give_rhetorical_question.py b/source/lambda/online/__functions/lambda_common_tools/give_rhetorical_question.py deleted file mode 100644 index eeb17e4c..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/give_rhetorical_question.py +++ /dev/null @@ -1,7 +0,0 @@ -# give rhetorical question -def lambda_handler(event_body,context=None): - try: - result = event_body['kwargs']['question'] - return {"code": 0, "result":result} - except KeyError: - return {"code": 1, "result": "The parameter “question” not found."} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_common_tools/rag.py b/source/lambda/online/__functions/lambda_common_tools/rag.py deleted file mode 100644 index 1e41e405..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/rag.py +++ /dev/null @@ -1,70 +0,0 @@ -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.prompt_utils import get_prompt_templates_from_ddb -from common_logic.common_utils.constant import ( - LLMTaskType -) -from common_logic.common_utils.lambda_invoke_utils import send_trace -from common_logic.common_utils.monitor_utils import format_rag_data - - -def lambda_handler(event_body, context=None): - state = event_body["state"] - context_list = [] - # Add qq match results - context_list.extend(state["qq_match_results"]) - figure_list = [] - retriever_params = state["chatbot_config"]["private_knowledge_config"] - retriever_params["query"] = state[retriever_params.get( - "retriever_config", {}).get("query_key", "query")] - - output: str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler", - ) - - for doc in output["result"]["docs"]: - context_list.append(doc["page_content"]) - figure_list = figure_list + doc.get("figure", []) - - # Remove duplicate figures - unique_set = {tuple(d.items()) for d in figure_list} - unique_figure_list = [dict(t) for t in unique_set] - state['extra_response']['figures'] = unique_figure_list - - context_md = format_rag_data(output["result"]["docs"], state["qq_match_contexts"]) - send_trace( - f"\n\n{context_md}\n\n", enable_trace=state["enable_trace"]) - - group_name = state['chatbot_config']['group_name'] - llm_config = state["chatbot_config"]["private_knowledge_config"]['llm_config'] - chatbot_id = state["chatbot_config"]["chatbot_id"] - task_type = LLMTaskType.RAG - prompt_templates_from_ddb = get_prompt_templates_from_ddb( - group_name, - model_id=llm_config['model_id'], - task_type=task_type, - chatbot_id=chatbot_id - ) - - output: str = invoke_lambda( - lambda_name="Online_LLM_Generate", - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name="lambda_handler", - event_body={ - "llm_config": { - **prompt_templates_from_ddb, - **llm_config, - "stream": state["stream"], - "intent_type": task_type, - }, - "llm_input": { - "contexts": context_list, - "query": state["query"], - "chat_history": state["chat_history"], - }, - }, - ) - - return {"code": 0, "result": output} diff --git a/source/lambda/online/__functions/lambda_common_tools/step_back_rag.py b/source/lambda/online/__functions/lambda_common_tools/step_back_rag.py deleted file mode 100644 index cbc09a57..00000000 --- a/source/lambda/online/__functions/lambda_common_tools/step_back_rag.py +++ /dev/null @@ -1,50 +0,0 @@ -# knowledge base retrieve -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.constant import ( - LLMTaskType -) - -def knowledge_base_retrieve(retriever_params, context=None): - output: str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler", - ) - contexts = [doc["page_content"] for doc in output["result"]["docs"]] - return contexts - -def lambda_handler(event_body, context=None): - state = event_body['state'] - retriever_params = state["chatbot_config"]["step_back_rag_config"]["retriever_config"] - contexts = [] - retriever_params["query"] = event_body['kwargs']['step_back_query'] - contexts.extend(knowledge_base_retrieve(retriever_params, context=context)) - context = "\n\n".join(contexts) - - # llm generate - system_prompt = (f"请根据context内的信息回答问题:\n" - "\n" - " - 回复内容需要展现出礼貌。回答内容为一句话,言简意赅。\n" - " - 每次回答总是先进行思考,并将思考过程写在标签中。\n" - " - 使用中文回答。\n" - "\n" - "\n" - f"{context}\n" - "" - ) - - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_daily_reception_config']['llm_config'], - "system_prompt": system_prompt, - "intent_type": LLMTaskType.CHAT}, - "llm_input": {"query": state['query'], "chat_history": state['chat_history']} - } - ) - - return {"code":0, "result":output} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/__init__.py b/source/lambda/online/__functions/lambda_retail_tools/__init__.py deleted file mode 100644 index 42e88a3c..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/__init__.py +++ /dev/null @@ -1,337 +0,0 @@ -from common_logic.common_utils.constant import SceneType,ToolRuningMode -from .._tool_base import tool_manager -from . import daily_reception -from . import goods_exchange -from . import customer_complain -from . import size_guide -from . import product_information_search -from . import order_info -from . import product_aftersales -from ..lambda_common_tools import give_rhetorical_question -from ..lambda_common_tools import give_final_response -from ..lambda_common_tools import comparison_rag, step_back_rag -from . import rule_response -from . import transfer -from . import promotion - - -SCENE = SceneType.RETAIL -LAMBDA_NAME = "lambda_retail_tools" - - -tool_manager.register_tool({ - "name":"daily_reception", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": daily_reception.lambda_handler, - "tool_def": { - "name": "daily_reception", - "description": "daily reception", - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name":"goods_exchange", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": goods_exchange.lambda_handler, - "tool_def": { - "name": "goods_exchange", - "description": "This tool handles user requests for product returns or exchanges.", - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name": "customer_complain", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": customer_complain.lambda_handler, - "tool_def": { - "name": "customer_complain", - "description": "有关于客户抱怨的工具,比如商品质量,错发商品,漏发商品等", - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name":"promotion", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": promotion.lambda_handler, - "tool_def": { - "name": "promotion", - "description": "有关于商品促销的信息,比如返点,奖品和奖励等", - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name":"size_guide", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": size_guide.lambda_handler, - "tool_def": { - "name": "size_guide", - "description": """size guide for customer - Step1: Determin what type of goods the customer wants to buy according to the goods information in <商品信息> xml tag, - such as shoes or apparel. - Step2: If the customer wants to buy shoes, you should provide the customer's shoes_size or foot_length. - Step3: If the customer wants to buy apparel, you should provide the customer's height and weight. - Notice: if the customer's weight unit is 斤, you should convert it to kg, 1斤=0.5kg""", - "parameters": { - "type": "object", - "properties": { - "height": { - "description": "height of the customer", - "type": "int" - }, - "weight": { - "description": "weight of the customer", - "type": "int" - }, - "shoes_size": { - "description": "size of the customer's shoes", - "type": "float" - }, - "foot_length": { - "description": "length of the customer's foot", - "type": "float" - } - }, - "required": [] - }, - }, - "running_mode": ToolRuningMode.LOOP -}) - - -tool_manager.register_tool({ - "name":"goods_recommendation", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": product_information_search.lambda_handler, - "tool_def": { - "name": "goods_recommendation", - "description": "recommend the product to the customer", - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name":"order_pipeline", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": order_info.lambda_handler, - "tool_def": { - "name": "order_pipeline", - "description": "query the order information", - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool({ - "name":"product_logistics", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": order_info.lambda_handler, - "tool_def": { - "name": "product_logistics", - "description": "查询商品物流信息,运费规则和物流规则,其中运费规则包括退货,换货,错发商品,漏发商品等。物流规则包括发货时间等", - }, - "running_mode": ToolRuningMode.ONCE, -}) - - -tool_manager.register_tool({ - "name":"goods_storage", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": order_info.lambda_handler, - "tool_def": { - "name": "goods_storage", - "description": "商品的库存信息,比如应对没货的情况等", - }, - "running_mode": ToolRuningMode.ONCE, -}) - - -tool_manager.register_tool({ - "name": "rule_response", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": rule_response.lambda_handler, - "tool_def": { - "name": "rule_response", - "description": "If a user's reply contains just a link or a long number, use this tool to reply.", - }, - "running_mode": ToolRuningMode.ONCE, -}) - - -tool_manager.register_tool({ - "name":"transfer", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": transfer.lambda_handler, - "tool_def": { - "name": "转人工", - "description": "转人工" - }, - "running_mode": ToolRuningMode.ONCE -}) - - -tool_manager.register_tool( - { - "name":"product_quality", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": product_aftersales.lambda_handler, - "tool_def": { - "name": "product_quality", - "description": "商品的售后处理,主要包括客户关于商品质量的抱怨,比如开胶等问题的", - "parameters": { - "type": "object", - "properties": { - "shop": { - "description": """The shop which the customer bought the product. - If the customer do not provide the shop name, the shop name is 'tianmao' by default. - The shop name must be in the list of ['tianmao', 'taobao','jingdong','dewu','other']""", - "type": "str" - } - }, - "required": [] - } - }, - "running_mode": ToolRuningMode.ONCE - } -) - -tool_manager.register_tool( - { - "name":"step_back_rag", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": step_back_rag.lambda_handler, - "tool_def": { - "name": "step_back_rag", - "description": "如果用户的问题过于具体,请把改写为更加通用的问题", - "parameters": { - "type": "object", - "properties": { - "query": { - "description": """基于历史消息改写的问题""", - "type": "str" - }, - "step_back_query": { - "description": """改写后的问题""", - "type": "str" - } - }, - "required": ["query", "step_back_query"] - } - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool( - { - "name":"comparison_rag", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": comparison_rag.lambda_handler, - "tool_def": { - "name": "comparison_rag", - "description": "在处理比较类型的问题,比如比较两个产品的区别时,使用这个工具", - "parameters": { - "type": "object", - "properties": { - "query": { - "description": """基于历史消息改写的问题""", - "type": "str" - }, - "query_a": { - "description": """比较对象A的查询语句""", - "type": "str" - }, - "query_b": { - "description": """比较对象B的查询语句""", - "type": "str" - } - }, - "required": ["query", "query_a", "query_b"] - } - }, - "running_mode": ToolRuningMode.ONCE - } -) - -tool_manager.register_tool( - { - "name":"give_rhetorical_question", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_rhetorical_question.lambda_handler, - "tool_def":{ - "name": "give_rhetorical_question", - "description": "If the user's question is not clear and specific, resulting in the inability to call other tools, please call this tool to ask the user a rhetorical question", - "parameters": { - "type": "object", - "properties": { - "question": { - "description": "The rhetorical question to user", - "type": "string" - }, - }, - "required": ["question"], - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - -tool_manager.register_tool( - { - "name": "give_final_response", - "scene": SCENE, - "lambda_name": LAMBDA_NAME, - "lambda_module_path": give_final_response.lambda_handler, - "tool_def":{ - "name": "give_final_response", - "description": "If none of the other tools need to be called, call the current tool to complete the direct response to the user.", - "parameters": { - "type": "object", - "properties": { - "response": { - "description": "Response to user", - "type": "string" - } - }, - "required": ["response"] - }, - }, - "running_mode": ToolRuningMode.ONCE - } -) - - - - - - - - - - - diff --git a/source/lambda/online/__functions/lambda_retail_tools/customer_complain.py b/source/lambda/online/__functions/lambda_retail_tools/customer_complain.py deleted file mode 100644 index f20b18ac..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/customer_complain.py +++ /dev/null @@ -1,51 +0,0 @@ -# customer complain -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.lambda_invoke_utils import send_trace -from common_logic.common_utils.constant import ( - LLMTaskType -) - - -def lambda_handler(event_body,context=None): - state = event_body['state'] - # call retriever - retriever_params = state["chatbot_config"]["rag_customer_complain_config"]["retriever_config"] - retriever_params["query"] = state["query"] - output:str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler" - ) - contexts = [doc['page_content'] for doc in output['result']['docs']] - - context = "\n\n".join(contexts) - send_trace(f'**rag_customer_complain_retriever** {context}', state["stream"], state["ws_connection_id"]) - - # llm generate - # prompt = dedent(f"""你是安踏的客服助理,正在处理有关于客户抱怨的问题,这些问题有关于商品质量等方面,需要你按照下面的guidelines进行回复: - system_prompt = ("你是安踏的客服助理,正在处理有关于消费者抱怨的问题。context列举了一些可能和客户问题有关的具体场景及回复,你可以进行参考:\n" - "\n" - f"{context}\n" - "\n" - "需要你按照下面的guidelines进行回复:\n" - "\n" - " - 回答内容为一句话,言简意赅。\n" - " - 尽量安抚客户情绪。\n" - " - 直接回答,不要说\"亲爱的顾客,您好\"\n" - "\n" - ) - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_customer_complain_config']['llm_config'], - "system_prompt":system_prompt, - "intent_type": LLMTaskType.CHAT}, - "llm_input": { "query": state['query'], "chat_history": state['chat_history']} - } - ) - - return {"code":0, "result":output} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/daily_reception.py b/source/lambda/online/__functions/lambda_retail_tools/daily_reception.py deleted file mode 100644 index cbd04891..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/daily_reception.py +++ /dev/null @@ -1,48 +0,0 @@ -# daily reception tool -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.lambda_invoke_utils import send_trace -from common_logic.common_utils.constant import ( - LLMTaskType -) - -def lambda_handler(event_body,context=None): - state = event_body['state'] - # retriver - retriever_params = state["chatbot_config"]["rag_daily_reception_config"]['retriever_config'] - retriever_params["query"] = state["query"] - output:str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler" - ) - contexts = [doc['page_content'] for doc in output['result']['docs']] - context = "\n\n".join(contexts) - send_trace(f'**rag_daily_reception_retriever** {context}') - - # llm generate - system_prompt = (f"你是安踏的客服助理,正在帮用户解答问题,客户提出的问题大多是属于日常接待类别,你需要按照下面的guidelines进行回复:\n" - "\n" - " - 回复内容需要展现出礼貌。回答内容为一句话,言简意赅。\n" - " - 使用中文回答。\n" - "\n" - "下面列举了一些具体的场景下的回复,你可以结合用户的问题进行参考:\n" - "\n" - f"{context}\n" - "" - ) - - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_daily_reception_config']['llm_config'], - "system_prompt": system_prompt, - "intent_type": LLMTaskType.CHAT}, - "llm_input": {"query": state['query'], "chat_history": state['chat_history']} - } - ) - - return {"code":0, "result":output} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/goods_exchange.py b/source/lambda/online/__functions/lambda_retail_tools/goods_exchange.py deleted file mode 100644 index bd492ab2..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/goods_exchange.py +++ /dev/null @@ -1,49 +0,0 @@ -# goods exchange -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.lambda_invoke_utils import send_trace -from common_logic.common_utils.constant import ( - LLMTaskType -) - -def lambda_handler(event_body,context=None): - state = event_body['state'] - - # retriver - retriever_params = state["chatbot_config"]["rag_goods_exchange_config"]['retriever_config'] - retriever_params["query"] = state["query"] - output:str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler" - ) - contexts = [doc['page_content'] for doc in output['result']['docs']] - - context = "\n\n".join(contexts) - send_trace(f'**rag_goods_exchange_retriever** {context}', state["stream"], state["ws_connection_id"]) - - # llm generate - system_prompt = (f"你是安踏的客服助理,正在帮用户解答问题,客户提出的问题大多是属于商品退换货范畴,你需要按照下面的guidelines进行回复:\n" - "\n" - " - 回复内容需要展现出礼貌。回答内容为一句话,言简意赅。\n" - " - 使用中文回答。\n" - "\n" - "下面列举了一些具体的场景下的回复,你可以结合用户的问题进行参考回答:\n" - "\n" - f"{context}\n" - "\n" - ) - - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_goods_exchange_config']['llm_config'], - "system_prompt":system_prompt, - "intent_type": LLMTaskType.CHAT}, - "llm_input": { "query": state['query'], "chat_history": state['chat_history']} - } - ) - return {"code":0, "result":output} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/order_info.py b/source/lambda/online/__functions/lambda_retail_tools/order_info.py deleted file mode 100644 index 0dd7f57f..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/order_info.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import re -import boto3 -import json - -from common_logic.common_utils.s3_utils import download_file_from_s3,check_local_folder -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda - -data_bucket_name = os.environ.get("RES_BUCKET", "aws-chatbot-knowledge-base-test") -order_info_path = "/tmp/functions/retail_tools/lambda_order_info/order_info.json" -check_local_folder(order_info_path) -download_file_from_s3(data_bucket_name, "retail_json/order_info.json", order_info_path) -order_dict = json.load(open(order_info_path)) - -def lambda_handler(event_body, context=None): - state = event_body["state"] - if "goods_id" not in state["chatbot_config"]: - return {"code":1, "result":"goods_id is required"} - goods_id = state["chatbot_config"]["goods_id"] -# ans = """ -# 如果遇到是关于是关于发货的问题,请回答‘根据您的问题,FILA实施全国多仓发货,下单后正常会48小时内帮您安排发货的哦。如果超过48小时还未发货,您可以随时联系我们客服,我们会尽快为您解决。’ -# """ - result = order_dict.get(goods_id, {}).get("status","") - - if result == "": - search_result = 0 - retriever_params = state["chatbot_config"]["rag_delivery_track_config"]["retriever_config"] - retriever_params["query"] = state["query"] - output:str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler" - ) - contexts = [doc['page_content'] for doc in output['result']['docs']] - - result = "\n\n".join(contexts) - - # search_result = f"当前没有查询到相关内容,请参考下面的内容,用一句话进行简洁回复:\n<内容>\n{result}\n" - # result = search_result - # result = contexts - else: - search_result = 1 - - return {"code":0, "result":result, "name": "product_information_search", "search_result": search_result} - -if __name__ == "__main__": - event_body = {} - lambda_handler(event_body) \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/product_aftersales.py b/source/lambda/online/__functions/lambda_retail_tools/product_aftersales.py deleted file mode 100644 index 0eaac2fb..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/product_aftersales.py +++ /dev/null @@ -1,86 +0,0 @@ -# goods after sales -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.lambda_invoke_utils import send_trace -from common_logic.common_utils.constant import ( - LLMTaskType -) -from datetime import datetime - - -def lambda_handler(event_body,context=None): - state = event_body['state'] - recent_tool_calling:list[dict] = state['function_calling_parsed_tool_calls'][0] - if "shop" in recent_tool_calling['kwargs'] and recent_tool_calling['kwargs']['shop'] != "tianmao": - contexts = ["顾客不是在天猫购买的商品,请他咨询其他商家"] - # return {"contexts": contexts} - else: - retriever_params = state["chatbot_config"]["rag_product_aftersales_config"]["retriever_config"] - retriever_params["query"] = state["query"] - output:str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler" - ) - contexts = [doc['page_content'] for doc in output['result']['docs']] - - context = "\n\n".join(contexts) - send_trace(f'**rag_product_aftersales_retriever** {context}', state["stream"], state["ws_connection_id"]) - - - # llm generate - create_time = state.get('create_time', None) - goods_id = state.get('chatbot_config').get('goods_id', 757492962957) - try: - create_datetime_object = datetime.strptime(create_time, '%Y-%m-%d %H:%M:%S.%f') - except Exception as e: - create_datetime_object = datetime.now() - print(f"create_time: {create_time} is not valid, use current time instead.") - create_time_str = create_datetime_object.strftime('%Y-%m-%d') - # TODO: fix received time format - - from lambda_main.main_utils.online_entries.retail_entry import order_dict - - received_time = order_dict.get(str(goods_id), {}).get("received_time", "2023/9/129:03:13") - order_time = " ".join([received_time[:9], received_time[9:]]) - try: - order_date_str = datetime.strptime(order_time, '%Y/%m/%d %H:%M:%S').strftime('%Y-%m-%d') - receive_elapsed_days = (create_datetime_object - datetime.strptime(order_date_str, '%Y-%m-%d')).days - receive_elapsed_months = receive_elapsed_days // 30 - except Exception as e: - order_date_str = "2023-9-12" - receive_elapsed_months = 6 - - system_prompt = (f"你是安踏的客服助理,正在帮消费者解答问题,消费者提出的问题大多是属于商品的质量和物流规则。context列举了一些可能有关的具体场景及回复,你可以进行参考:\n" - f"客户咨询的问题所对应的订单日期为{order_date_str}。\n" - f"当前时间{create_time_str}\n" - f"客户收到商品已经超过{receive_elapsed_months}个月\n" - "\n" - f"{context}\n" - "\n" - "你需要按照下面的guidelines对消费者的问题进行回答:\n" - "\n" - " - 回答内容为一句话,言简意赅。\n" - " - 如果问题与context内容不相关,就不要采用。\n" - " - 消费者的问题里面可能包含口语化的表达,比如鞋子开胶的意思是用胶黏合的鞋体裂开。这和胶丝遗留没有关系\n" - " - 洗涤后出现问题也属于质量问题\n" - " - 消费者的回复不够清晰的时候,直接回复: 不知道刚才给您的建议是否有帮助?。不要有额外补充\n" - " - 如果客户问到质量相关问题,请根据前面的订单信息和三包规则,确定是否超出三包期限,如果超出三包期限请告知消费者无法处理,如果在三包期限内请按照三包要求处理,并安抚。\n" - "\n" - ) - # print('llm config',state['chatbot_config']['rag_product_aftersales_config']['llm_config']) - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_product_aftersales_config']['llm_config'], - "system_prompt":system_prompt, - "intent_type": LLMTaskType.CHAT - }, - "llm_input": { "query": state['query'], "chat_history": state['chat_history']} - } - ) - - return {"code":0, "result":output} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/product_information_search.py b/source/lambda/online/__functions/lambda_retail_tools/product_information_search.py deleted file mode 100644 index 147d3239..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/product_information_search.py +++ /dev/null @@ -1,40 +0,0 @@ -import json -import os -from common_logic.common_utils.s3_utils import download_file_from_s3 -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda,node_monitor_wrapper -from common_logic.common_utils.lambda_invoke_utils import send_trace,is_running_local - -goods_info_path = "/tmp/functions/retail_tools/lambda_order_info/goods_info.json" -parent_path = '/'.join((goods_info_path).split('/')[:-1]) -os.system(f"mkdir -p {parent_path}") - -data_bucket_name = os.environ.get("RES_BUCKET", "aws-chatbot-knowledge-base-test") -download_file_from_s3(data_bucket_name, "retail_json/goods_info.json", goods_info_path) -goods_dict = json.load(open(goods_info_path)) - -def lambda_handler(event_body, context=None): - state = event_body["state"] - if "goods_id" not in state["chatbot_config"]: - return {"code":1, "result":"goods_id is required"} - goods_id = str(state["chatbot_config"]["goods_id"]) - context_goods_info = goods_dict[goods_id] - - retriever_params = state["chatbot_config"]["rag_goods_info_config"]["retriever_config"] - retriever_params["query"] = state["query"] - output:str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler" - ) - goods_info_list = [doc['page_content'] for doc in output['result']['docs'] if doc['score'] > 0.6] - - query_goods_info = "\n\n".join(goods_info_list) - send_trace(f'**rag_goods_info_retriever** {context}', state["stream"], state["ws_connection_id"]) - result = f"**用户当前咨询的商品是** {context_goods_info}\n\n**用户可能想找的商品是** {query_goods_info}" - - return {"code":0, "result":result, "name": "product_information_search"} - -if __name__ == "__main__": - event_body = {} - lambda_handler(event_body) \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/promotion.py b/source/lambda/online/__functions/lambda_retail_tools/promotion.py deleted file mode 100644 index 0bf5aca9..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/promotion.py +++ /dev/null @@ -1,53 +0,0 @@ -# promotion tool - -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.lambda_invoke_utils import send_trace -from common_logic.common_utils.constant import ( - LLMTaskType -) - - -def lambda_handler(event_body,context=None): - state = event_body['state'] - - # retrieve - retriever_params = state["chatbot_config"]["rag_promotion_config"]["retriever_config"] - retriever_params["query"] = state["query"] - output:str = invoke_lambda( - event_body=retriever_params, - lambda_name="Online_Functions", - lambda_module_path="functions.functions_utils.retriever.retriever", - handler_name="lambda_handler" - ) - contexts = [doc['page_content'] for doc in output['result']['docs']] - - context = "\n\n".join(contexts) - send_trace(f'**rag_promotion_retriever** {context}', state["stream"], state["ws_connection_id"]) - - # llm generate - system_prompt = ("你是安踏的客服助理,正在帮消费者解答有关于商品促销的问题,这些问题是有关于积分、奖品、奖励等方面。\n" - "context列举了一些可能有关的具体场景及回复,你可以进行参考:\n" - f"\n{context}\n\n" - "你需要按照下面的guidelines对消费者的问题进行回答:\n" - "\n" - " - 回答内容为一句话,言简意赅。\n" - " - 如果问题与context内容不相关,就不要采用。\n" - " - 使用中文进行回答。\n" - "" - ) - - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_promotion_config']['llm_config'], - "system_prompt":system_prompt, - "intent_type": LLMTaskType.CHAT}, - "llm_input": { "query": state['query'], "chat_history": state['chat_history']} - } - ) - - return {"code":0, "result": output} - diff --git a/source/lambda/online/__functions/lambda_retail_tools/rule_response.py b/source/lambda/online/__functions/lambda_retail_tools/rule_response.py deleted file mode 100644 index 3b5a8f02..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/rule_response.py +++ /dev/null @@ -1,49 +0,0 @@ -# rule_url_reply -import random -import re -from functions.lambda_retail_tools.product_information_search import goods_dict -from common_logic.common_utils.constant import LLMTaskType -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda - - -def lambda_handler(event_body, context=None): - state = event_body["state"] - state["extra_response"]["current_agent_intent_type"] = "rule reply" - goods_info_tag = state['goods_info_tag'] - if state['query'].endswith(('.jpg','.png')): - answer = random.choice([ - "收到,亲。请问我们可以怎么为您效劳呢?", - "您好,请问有什么需要帮助的吗?" - ]) - return {"code":0, "result": answer} - # product information - r = re.findall(r"item.htm\?id=(.*)",state['query']) - if r: - goods_id = r[0] - else: - goods_id = 0 - if goods_id in goods_dict: - # call llm to make summary of goods info - human_goods_info = state['human_goods_info'] - output = f"您好,该商品的特点是:\n{human_goods_info}" - if human_goods_info: - system_prompt = (f"你是安踏的客服助理,当前用户对下面的商品感兴趣:\n" - f"<{goods_info_tag}>\n{human_goods_info}\n\n" - "请你结合商品的基础信息,特别是卖点信息返回一句推荐语。" - ) - output:str = invoke_lambda( - lambda_name='Online_LLM_Generate', - lambda_module_path="lambda_llm_generate.llm_generate", - handler_name='lambda_handler', - event_body={ - "llm_config": { - **state['chatbot_config']['rag_daily_reception_config']['llm_config'], - "system_prompt": system_prompt, - "intent_type": LLMTaskType.CHAT}, - "llm_input": {"query": state['query'], "chat_history": state['chat_history']} - } - ) - - return {"code":0, "result":output} - - return {"code":0, "result":"您好"} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/size_guide.py b/source/lambda/online/__functions/lambda_retail_tools/size_guide.py deleted file mode 100644 index cc148432..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/size_guide.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import re -import json - -import numpy as np - -from common_logic.common_utils.s3_utils import download_file_from_s3, check_local_folder - -data_bucket_name = os.environ.get("RES_BUCKET", "aws-chatbot-knowledge-base-test") -good2type_dict_path = "/tmp/functions/retail_tools/lambda_size_guide/good2type_dict.json" -size_dict_path = "/tmp/functions/retail_tools/lambda_size_guide/size_dict.json" -check_local_folder(good2type_dict_path) -check_local_folder(size_dict_path) -download_file_from_s3(data_bucket_name, "retail_json/good2type_dict.json", good2type_dict_path) -download_file_from_s3(data_bucket_name, "retail_json/size_dict.json", size_dict_path) -good2type_dict = json.load(open(good2type_dict_path)) -size_dict = json.load(open(size_dict_path)) - -def find_nearest(array, value): - float_array = np.asarray([float(x) for x in array]) - array = np.asarray(array) - idx = (np.abs(float_array - value)).argmin() - return array[idx] - -def lambda_handler(event_body, context=None): - state = event_body["state"] - if "goods_id" not in state["chatbot_config"]: - return {"code":1, "result":"goods_id is required"} - goods_id = str(state["chatbot_config"]["goods_id"]) - kwargs = event_body["kwargs"] - if goods_id not in good2type_dict: - return {"code":1, "result":"该商品的尺码信息缺失,请不要使用尺码工具"} - goods_type_1, goods_type_2 = good2type_dict[goods_id] - if goods_type_1 == "shoes": - if "shoes_size" in kwargs: - try: - shoe_size = float(kwargs["shoes_size"]) - except: - return {"code":1, "result":"shoes_size should be a number"} - if goods_type_1 == "shoes" and goods_type_2 == "童鞋": - return {"code":1, "result":"童鞋不存在鞋码,请输入脚长查询"} - std_shoe_size = find_nearest(list(size_dict.get(goods_type_1).get(goods_type_2).get("shoes_size").keys()), shoe_size) - result = size_dict.get(goods_type_1).get(goods_type_2).get("shoes_size").get(std_shoe_size, "42") - # No sutabale size for the input shoes size or foot length - if result == "此款暂无适合亲的尺码": - result += ",您当前输入的鞋码为{},请确认一下参数是否正确,如果有修改可以再次调用尺码工具".format(shoe_size) - elif "foot_length" in kwargs: - try: - foot_length = float(kwargs["foot_length"]) - except: - return {"code":1, "result":"foot_length should be a number"} - std_foot_length = find_nearest(list(size_dict.get(goods_type_1).get(goods_type_2).get("foot_length").keys()), foot_length) - result = size_dict.get(goods_type_1).get(goods_type_2).get("foot_length").get(std_foot_length, "28") - # No sutabale size for the input foot length - if result == "此款暂无适合亲的尺码": - result += ",您当前输入的脚长为{}cm,请确认一下参数是否正确,如果有修改可以再次调用尺码工具".format(foot_length) - else: - return {"code":1, "result":"请继续询问用户的脚长或鞋码"} - elif goods_type_1 == "apparel": - if "height" not in kwargs: - return {"code":1, "result":"请继续询问用户的身高"} - if "weight" not in kwargs: - return {"code":1, "result":"请继续询问用户的体重"} - try: - height = float(kwargs["height"]) - weight = float(kwargs["weight"]) - except: - return {"code":1, "result":"身高和体重必须是数值,请确认输入是否正确"} - std_height = find_nearest(list(size_dict.get(goods_type_1).get(goods_type_2).\ - get("height_weight").keys()), height) - std_weight = find_nearest(list(size_dict.get(goods_type_1).get(goods_type_2).\ - get("height_weight").get(std_height).keys()), weight) - result = size_dict.get(goods_type_1).get(goods_type_2).get("height_weight").\ - get(std_height).get(std_weight) - # No sutabale size for the input height and weight - if result == "亲亲,很抱歉,这款暂时没有适合您的尺码": - result += ",您当前输入的身高为{}cm,体重为{}kg,请确认一下参数是否正确,如果有修改可以再次调用尺码工具".\ - format(height, weight) - return {"code":0, "result":result, "name": "尺码查询"} - -if __name__ == "__main__": - event_body = {} - lambda_handler(event_body) \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_retail_tools/transfer.py b/source/lambda/online/__functions/lambda_retail_tools/transfer.py deleted file mode 100644 index 24a6a01d..00000000 --- a/source/lambda/online/__functions/lambda_retail_tools/transfer.py +++ /dev/null @@ -1,3 +0,0 @@ -# retail transfer -def lambda_handler(event_body,context=None): - return {"code":0, "result":"您好,我是安踏官方客服,很高兴为您服务。请问您有什么需要帮助的吗?"} \ No newline at end of file diff --git a/source/lambda/online/__functions/lambda_tool.py b/source/lambda/online/__functions/lambda_tool.py deleted file mode 100644 index dec440ef..00000000 --- a/source/lambda/online/__functions/lambda_tool.py +++ /dev/null @@ -1,24 +0,0 @@ -# unified lambda tool calling -from functions import get_tool_by_name,Tool -from common_logic.common_utils.lambda_invoke_utils import invoke_lambda -from common_logic.common_utils.lambda_invoke_utils import chatbot_lambda_call_wrapper - -@chatbot_lambda_call_wrapper -def lambda_handler(event_body,context=None): - tool_name = event_body['tool_name'] - state = event_body['state'] - tool:Tool = get_tool_by_name(tool_name,scene=state['chatbot_config']['scene']) - - output:dict = invoke_lambda( - event_body=event_body, - lambda_name=tool.lambda_name, - lambda_module_path=tool.lambda_module_path, - handler_name=tool.handler_name - ) - return output - - - - - - \ No newline at end of file diff --git a/source/lambda/online/__functions/tool_calling_parse.py b/source/lambda/online/__functions/tool_calling_parse.py deleted file mode 100644 index ddc756d2..00000000 --- a/source/lambda/online/__functions/tool_calling_parse.py +++ /dev/null @@ -1,364 +0,0 @@ -""" -tool calling parse, convert content by llm to dict -""" -from typing import List -import re -import json -from langchain_core.messages import( - ToolCall -) -from common_logic.common_utils.exceptions import ( - ToolNotExistError, - ToolParameterNotExistError, - MultipleToolNameError, - ToolNotFound -) -from functions.tool_execute_result_format import format_tool_call_results -from common_logic.common_utils.constant import ( - LLMModelType, - MessageType -) - - - -class ToolCallingParseMeta(type): - def __new__(cls, name, bases, attrs): - new_cls = type.__new__(cls, name, bases, attrs) - - if name == "ToolCallingParse": - return new_cls - new_cls.model_map[new_cls.model_id] = new_cls - return new_cls - - -class ToolCallingParse(metaclass=ToolCallingParseMeta): - model_map = {} - - @classmethod - def parse_tool(cls,agent_output): - target_cls = cls.model_map[agent_output['current_agent_model_id']] - return target_cls.parse_tool(agent_output) - - -class Claude3SonnetFToolCallingParse(ToolCallingParse): - model_id = LLMModelType.CLAUDE_3_SONNET - tool_format = ("\n" - "\n" - "$TOOL_NAME\n" - "\n" - "<$PARAMETER_NAME>$PARAMETER_VALUE\n" - "...\n" - "\n" - "\n" - "\n" - ) - - @classmethod - def convert_anthropic_xml_to_dict(cls,model_id,function_calls:List[str], tools:list[dict]) -> List[dict]: - # formatted_tools = [convert_to_openai_function(tool) for tool in tools] - tool_calls:list[ToolCall] = [] - tools_mapping = {tool['name']:tool for tool in tools} - for function_call in function_calls: - tool_names = re.findall(r'(.*?)', function_call, re.S) - if len(tool_names) > 1: - raise MultipleToolNameError(function_call_content=function_call) - - tool_name = tool_names[0].strip() - - if tool_name not in tools_mapping: - raise ToolNotExistError( - tool_name=tool_name, - function_call_content=function_call - ) - cur_tool:dict = tools_mapping[tool_name] - arguments = {} - for parameter_key in cur_tool['parameters']['required']: - value = re.findall(f'<{parameter_key}>(.*?)', function_call, re.DOTALL) - if not value: - # expand search region - # search_region = re.findall(f'\n{parameter_key}(.*?)', function_call, re.DOTALL) - # # print(search_region) - # value = re.findall(f'(.*?)', search_region, re.DOTALL) - # if not value: - raise ToolParameterNotExistError( - tool_name=tool_name, - parameter_key=parameter_key, - function_call_content=function_call, - tool_format=f"\n注意正确的工具调用格式应该是下面的:\n{cls.tool_format}\n" - ) - # TODO, add too many parameters error - assert len(value) == 1,(parameter_key,function_call) - arguments[parameter_key] = value[0].strip() - for parameter_key in cur_tool['parameters']['properties'].keys(): - value = re.findall(f'<{parameter_key}>(.*?)', function_call, re.DOTALL) - if value: - arguments[parameter_key] = value[0].strip() - tool_calls.append(dict(name=tool_name,kwargs=arguments,model_id=model_id)) - return tool_calls - - @classmethod - def tool_not_found(cls,agent_message): - tool_format = cls.tool_format - e = ToolNotFound() - e.agent_message = agent_message - e.error_message = { - "role": MessageType.HUMAN_MESSAGE_TYPE, - "content": f"当前没有解析到tool,请检查tool调用的格式是否正确,并重新输出某个tool的调用。注意正确的tool调用格式应该为: {tool_format}。\n如果你认为当前不需要调用其他工具,请直接调用“give_final_response”工具进行返回。" - } - return e - - @classmethod - def parse_tool( - cls, - agent_output - ) -> list: - function_calls = agent_output['agent_output']['function_calls'] - tools = agent_output['current_agent_tools_def'] - agent_message = { - "role": MessageType.AI_MESSAGE_TYPE, - "content": agent_output['agent_output']['content'], - "additional_kwargs": {} - } - - if not function_calls: - raise cls.tool_not_found(agent_message=agent_message) - try: - tool_calls = cls.convert_anthropic_xml_to_dict( - cls.model_id, - function_calls=function_calls, - tools=tools - ) - if not tool_calls: - raise cls.tool_not_found(agent_message=agent_message) - - agent_message['additional_kwargs']['tool_calls'] = tool_calls - return {"agent_message": agent_message,"tool_calls":tool_calls} - except (ToolNotExistError,ToolParameterNotExistError,MultipleToolNameError) as e: - e.error_message = format_tool_call_results( - model_id = agent_output['current_agent_model_id'], - tool_output=[{ - "output":{ - "code": 1, - "result": e.to_agent(), - "tool_name": e.tool_name} - }] - )['tool_message'] - e.agent_message = agent_message - raise e - - -class Claude3HaikuToolCallingParse(Claude3SonnetFToolCallingParse): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35SonnetFToolCallingParse(Claude3SonnetFToolCallingParse): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" - - -class Claude2ToolCallingParse(Claude3SonnetFToolCallingParse): - model_id = LLMModelType.CLAUDE_2 - - -class Claude21ToolCallingParse(Claude3SonnetFToolCallingParse): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceToolCallingParse(Claude3SonnetFToolCallingParse): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Mixtral8x7bToolCallingParse(Claude3SonnetFToolCallingParse): - model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - - -class GLM4Chat9BToolCallingParse(ToolCallingParse): - model_id = LLMModelType.GLM_4_9B_CHAT - - @classmethod - def parse_tool_kwargs(cls,content:str,tools_def:list[dict]): - tool_name = content.split("\n")[0] - cur_tool_def = None - for tool_def in tools_def: - if tool_def['name'] == tool_name: - cur_tool_def = tool_def - break - if cur_tool_def is None: - raise ToolNotExistError(tool_name=tool_name,function_call_content=content) - tool_params = "\n".join(content.split("\n")[1:]).replace("<|observation|>","").strip() - tool_params = json.loads(tool_params) - - - cur_params_names = set(list(tool_params.keys())) - tool_def_requires = set(cur_tool_def['parameters'].get("required",[])) - remain_requires = list(tool_def_requires.difference(cur_params_names)) - if remain_requires: - raise ToolParameterNotExistError( - tool_name=tool_name, - parameter_key=remain_requires[0], - function_call_content=content - ) - - return {"name":tool_name,"kwargs":tool_params,"model_id":cls.model_id} - - - @classmethod - def parse_tool(cls,agent_output): - try: - content = agent_output['agent_output'].strip() - # check use tool or direct reply - tools = agent_output['current_agent_tools_def'] - agent_message = { - "role": MessageType.AI_MESSAGE_TYPE, - "content": content, - "additional_kwargs": {} - } - - assert content.endswith(("<|user|>","<|observation|>")), content - - if content.endswith("<|observation|>"): - # use one tool - tool_call = cls.parse_tool_kwargs(content,tools_def=tools) - agent_message['content'] = agent_message['content'].replace(tool_call['name'],"").strip() - agent_message['additional_kwargs']['tool_calls'] = [tool_call] - else: - # default tool is give_final_response - # response = content.replace("<|user|>","") - tool_call = {"name":"give_final_response","kwargs":{"response":content},"model_id":cls.model_id} - return { - "agent_message": agent_message, - "tool_calls": [tool_call] - } - except (ToolNotExistError, ToolParameterNotExistError, MultipleToolNameError) as e: - e.agent_message = agent_message - e.error_message = format_tool_call_results( - model_id = agent_output['current_agent_model_id'], - tool_output=[{ - "output":{ - "code": 1, - "result": e.to_agent(), - "tool_name": e.tool_name} - }] - )['tool_message'] - raise e - - - -class Qwen2Instruct7BToolCallingParse(ToolCallingParse): - model_id = LLMModelType.QWEN2INSTRUCT7B - FN_NAME = '✿FUNCTION✿' - FN_ARGS = '✿ARGS✿' - FN_RESULT = '✿RESULT✿' - FN_EXIT = '✿RETURN✿' - - tool_format = (f"{FN_NAME}: 工具名称\n" - f"{FN_ARGS}: 工具输入\n" - f"{FN_RESULT}" - ) - - thinking_tag = "思考" - fix_reply_tag = "固定回复" - - @classmethod - def parse_tool_kwargs(cls,content:str,tools_def:list[dict],agent_message): - try: - r = re.match(f"{cls.FN_NAME}(.*?){cls.FN_ARGS}(.*?){cls.FN_RESULT}",content,re.S) - tool_name = r.group(1).strip().lstrip(":").strip() - tool_params = json.loads(r.group(2).strip().lstrip(":").strip()) - except Exception as e: - e = cls.tool_not_found(agent_message,error=str(e)) - raise e - - cur_tool_def = None - for tool_def in tools_def: - if tool_def['name'] == tool_name: - cur_tool_def = tool_def - break - if cur_tool_def is None: - raise ToolNotExistError(tool_name=tool_name,function_call_content=content) - - cur_params_names = set(list(tool_params.keys())) - tool_def_requires = set(cur_tool_def['parameters'].get("required",[])) - remain_requires = list(tool_def_requires.difference(cur_params_names)) - - if remain_requires: - raise ToolParameterNotExistError( - tool_name=tool_name, - parameter_key=remain_requires[0], - function_call_content=content - ) - return {"name":tool_name,"kwargs":tool_params,"model_id":cls.model_id} - - - @classmethod - def tool_not_found(cls,agent_message,error=""): - tool_format = cls.tool_format - e = ToolNotFound() - e.agent_message = agent_message - e.error_message = { - "role": MessageType.TOOL_MESSAGE_TYPE, - "content": f"\n{cls.FN_RESULT}: 当前没有解析到tool,{error}\n请检查tool调用的格式是否正确,并重新输出某个tool的调用。注意正确的tool调用格式应该为: {tool_format}。" - } - return e - - @classmethod - def parse_tool(cls,agent_output): - thinking_tag = cls.thinking_tag - try: - output:dict = agent_output['agent_output'] - tools = agent_output['current_agent_tools_def'] - agent_message = { - "role": MessageType.AI_MESSAGE_TYPE, - "content": output['content'], - "additional_kwargs": {} - } - - function_calls = output['function_calls'] - if function_calls: - tool_call = cls.parse_tool_kwargs(function_calls[0],tools_def=tools,agent_message=agent_message) - agent_message['additional_kwargs']['tool_calls'] = [tool_call] - else: - if any(s in output['content'] for s in [cls.FN_ARGS,cls.FN_EXIT,cls.FN_NAME,cls.FN_RESULT]): - e = cls.tool_not_found(agent_message=agent_message,error="如果你想调用某个工具,请确保格式正确。") - raise e - response = re.sub(f"<{thinking_tag}>.*?","",output['content'],flags=re.DOTALL).strip() - response = response.replace(f'<{cls.fix_reply_tag}>',"").replace(f'',"").replace(f"","").strip() - tool_call = {"name":"give_final_response","kwargs":{"response":response},"model_id":cls.model_id} - - return { - "agent_message": agent_message, - "tool_calls": [tool_call] - } - except (ToolNotExistError, ToolParameterNotExistError, MultipleToolNameError) as e: - e.agent_message = agent_message - e.error_message = format_tool_call_results( - model_id = agent_output['current_agent_model_id'], - tool_output=[{ - "output":{ - "code": 1, - "result": e.to_agent(), - "tool_name": e.tool_name} - }] - )['tool_message'] - raise e - - -class Qwen2Instruct72BToolCallingParse(Qwen2Instruct7BToolCallingParse): - model_id = LLMModelType.QWEN2INSTRUCT72B - - - -class QWEN15INSTRUCT32BToolCallingParse(Qwen2Instruct7BToolCallingParse): - model_id = LLMModelType.QWEN15INSTRUCT32B - - -parse_tool_calling = ToolCallingParse.parse_tool - - - - - - - - - - diff --git a/source/lambda/online/__functions/tool_execute_result_format.py b/source/lambda/online/__functions/tool_execute_result_format.py deleted file mode 100644 index c8ab9c0f..00000000 --- a/source/lambda/online/__functions/tool_execute_result_format.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -tool execute format -""" - -from common_logic.common_utils.constant import ( - LLMModelType, - MessageType -) - -class FormatMeta(type): - def __new__(cls, name, bases, attrs): - new_cls = type.__new__(cls, name, bases, attrs) - - if name == "FormatToolResult": - return new_cls - new_cls.model_map[new_cls.model_id] = new_cls - return new_cls - - -class FormatToolResult(metaclass=FormatMeta): - model_map = {} - - @classmethod - def format(cls,model_id,tool_output:dict): - target_cls = cls.model_map[model_id] - return target_cls.format(tool_output) - - -CLAUDE_TOOL_EXECUTE_SUCCESS_TEMPLATE = """ - - -{tool_name} - -{result} - - - -""" - -CLAUDE_TOOL_EXECUTE_FAIL_TEMPLATE = """ - - -{error} - - -""" - -MIXTRAL8X7B_TOOL_EXECUTE_SUCCESS_TEMPLATE = """工具: {tool_name} 的执行结果如下: -{result}""" - -MIXTRAL8X7B_TOOL_EXECUTE_FAIL_TEMPLATE = """工具: {tool_name} 执行错误,错误如下: -{error}""" - -class Claude3SonnetFormatToolResult(FormatToolResult): - model_id = LLMModelType.CLAUDE_3_SONNET - execute_success_template = CLAUDE_TOOL_EXECUTE_SUCCESS_TEMPLATE - execute_fail_template = CLAUDE_TOOL_EXECUTE_FAIL_TEMPLATE - - @classmethod - def format_one_tool_output(cls,tool_output:dict): - exe_code = tool_output['code'] - if exe_code == 1: - # failed - return cls.execute_fail_template.format( - error=tool_output['result'], - tool_name = tool_output['tool_name'] - ) - elif exe_code == 0: - # succeed - return cls.execute_success_template.format( - tool_name=tool_output['tool_name'], - result=tool_output['result'] - ) - else: - raise ValueError(f"Invalid tool execute: {tool_output}") - - @classmethod - def format(cls,tool_call_outputs:list[dict]): - tool_call_result_strs = [] - for tool_call_result in tool_call_outputs: - tool_exe_output = tool_call_result['output'] - if 'name' in tool_call_result.keys(): - tool_exe_output['tool_name'] = tool_call_result['name'] - ret:str = cls.format_one_tool_output( - tool_exe_output - ) - tool_call_result_strs.append(ret) - - ret = "\n".join(tool_call_result_strs) - return { - "tool_message": { - "role": MessageType.HUMAN_MESSAGE_TYPE, - "content": ret, - "additional_kwargs": { - "original": [out['output'] for out in tool_call_outputs], - "raw_tool_call_results": tool_call_outputs, - }, - } - } - -class Claude3HaikuFormatToolResult(Claude3SonnetFormatToolResult): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35SonnetFormatToolResult(Claude3SonnetFormatToolResult): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" - - -class Claude2FormatToolResult(Claude3SonnetFormatToolResult): - model_id = LLMModelType.CLAUDE_2 - - -class Claude21FormatToolResult(Claude3SonnetFormatToolResult): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceFormatToolResult(Claude3SonnetFormatToolResult): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Mixtral8x7bFormatToolResult(Claude3SonnetFormatToolResult): - model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - execute_success_template = MIXTRAL8X7B_TOOL_EXECUTE_SUCCESS_TEMPLATE - execute_fail_template = MIXTRAL8X7B_TOOL_EXECUTE_FAIL_TEMPLATE - - -class GLM4Chat9BFormatToolResult(FormatToolResult): - model_id = LLMModelType.GLM_4_9B_CHAT - - @classmethod - def format(cls,tool_call_outputs:list[dict]): - tool_call_result_strs = [] - for tool_call_result in tool_call_outputs: - tool_exe_output = tool_call_result['output'] - tool_call_result_strs.append(str(tool_exe_output['result'])) - # print(tool_exe_output['result']) - ret = "\n".join(tool_call_result_strs) - return { - "tool_message": { - "role": MessageType.TOOL_MESSAGE_TYPE, - "content": ret, - "additional_kwargs": { - "original": [out['output'] for out in tool_call_outputs], - "raw_tool_call_results":tool_call_outputs, - }, - } - } - -class Qwen2Instruct7BFormatToolResult(FormatToolResult): - model_id = LLMModelType.QWEN2INSTRUCT7B - FN_RESULT = '✿RESULT✿' - FN_EXIT = '✿RETURN✿' - - @classmethod - def format(cls,tool_call_outputs:list[dict]): - tool_call_result_strs = [] - for tool_call_result in tool_call_outputs: - tool_exe_output = tool_call_result['output'] - result = tool_exe_output["result"] - tool_call_result_strs.append(f'\n{cls.FN_RESULT}: {result}\n{cls.FN_EXIT}:') - - ret = "\n".join(tool_call_result_strs) - return { - "tool_message": { - "role": MessageType.TOOL_MESSAGE_TYPE, - "content": ret, - "additional_kwargs": { - "original": [out['output'] for out in tool_call_outputs], - "raw_tool_call_results":tool_call_outputs, - }, - } - } - - -class Qwen2Instruct72BFormatToolResult(Qwen2Instruct7BFormatToolResult): - model_id = LLMModelType.QWEN2INSTRUCT72B - - -class QWEN15INSTRUCT32BFormatToolResult(Qwen2Instruct7BFormatToolResult): - model_id = LLMModelType.QWEN15INSTRUCT32B - - -format_tool_call_results = FormatToolResult.format - - - - - - - - - - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/__init__.py deleted file mode 100644 index f2c1de10..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .llm_chains import LLMChain -from .llm_models import Model - - -def get_llm_chain(model_id, intent_type, model_kwargs=None, **kwargs): - return LLMChain.get_chain( - model_id, intent_type, model_kwargs=model_kwargs, **kwargs - ) - - -def get_llm_model(model_id, model_kwargs=None): - return Model.get_model(model_id, model_kwargs=model_kwargs) diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/__init__.py deleted file mode 100644 index 1577b1eb..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/__init__.py +++ /dev/null @@ -1,94 +0,0 @@ -from .llm_chain_base import LLMChain - -from .chat_chain import ( - Claude2ChatChain, - Claude21ChatChain, - ClaudeInstanceChatChain, - Iternlm2Chat7BChatChain, - Iternlm2Chat20BChatChain, - Baichuan2Chat13B4BitsChatChain, - Claude3HaikuChatChain, - Claude3SonnetChatChain, - # ChatGPT35ChatChain, - # ChatGPT4ChatChain, - # ChatGPT4oChatChain, -) - -from .conversation_summary_chain import ( - Iternlm2Chat7BConversationSummaryChain, - ClaudeInstanceConversationSummaryChain, - Claude21ConversationSummaryChain, - Claude3HaikuConversationSummaryChain, - Claude3SonnetConversationSummaryChain, - Iternlm2Chat20BConversationSummaryChain -) - -from .intention_chain import ( - Claude21IntentRecognitionChain, - Claude2IntentRecognitionChain, - ClaudeInstanceIntentRecognitionChain, - Claude3HaikuIntentRecognitionChain, - Claude3SonnetIntentRecognitionChain, - Iternlm2Chat7BIntentRecognitionChain, - Iternlm2Chat20BIntentRecognitionChain, - -) - -from .rag_chain import ( - Claude21RagLLMChain, - Claude2RagLLMChain, - ClaudeInstanceRAGLLMChain, - Claude3HaikuRAGLLMChain, - Claude3SonnetRAGLLMChain, - Baichuan2Chat13B4BitsKnowledgeQaChain -) - - -from .translate_chain import ( - Iternlm2Chat7BTranslateChain, - Iternlm2Chat20BTranslateChain -) - - -from .marketing_chains import * - -from .stepback_chain import ( - Claude21StepBackChain, - ClaudeInstanceStepBackChain, - Claude2StepBackChain, - Claude3HaikuStepBackChain, - Claude3SonnetStepBackChain, - Iternlm2Chat7BStepBackChain, - Iternlm2Chat20BStepBackChain -) - - -from .hyde_chain import ( - Claude21HydeChain, - Claude2HydeChain, - Claude3HaikuHydeChain, - Claude3SonnetHydeChain, - ClaudeInstanceHydeChain, - Iternlm2Chat20BHydeChain, - Iternlm2Chat7BHydeChain -) - -from .query_rewrite_chain import ( - Claude21QueryRewriteChain, - Claude2QueryRewriteChain, - ClaudeInstanceQueryRewriteChain, - Claude3HaikuQueryRewriteChain, - Claude3SonnetQueryRewriteChain, - Iternlm2Chat20BQueryRewriteChain, - Iternlm2Chat7BQueryRewriteChain -) - -from .tool_calling_chain_claude_xml import ( - Claude21ToolCallingChain, - Claude3HaikuToolCallingChain, - Claude2ToolCallingChain, - Claude3SonnetToolCallingChain, - ClaudeInstanceToolCallingChain -) - -from .retail_chains import * \ No newline at end of file diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/chat_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/chat_chain.py deleted file mode 100644 index 44a51542..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/chat_chain.py +++ /dev/null @@ -1,338 +0,0 @@ -# chat llm chains - -from langchain.schema.runnable import RunnableLambda, RunnablePassthrough -from langchain_core.messages import AIMessage,SystemMessage -from langchain.prompts import ChatPromptTemplate,HumanMessagePromptTemplate -from langchain_core.messages import convert_to_messages - - -from ..llm_models import Model -from .llm_chain_base import LLMChain - -from common_logic.common_utils.constant import ( - MessageType, - LLMTaskType, - LLMModelType, -) -from common_logic.common_utils.time_utils import get_china_now -from common_logic.common_utils.prompt_utils import get_prompt_template - -AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE -HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE -QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE -SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE - - -class Claude2ChatChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.CHAT - - - @classmethod - def get_common_system_prompt(cls,system_prompt_template:str): - now = get_china_now() - date_str = now.strftime("%Y年%m月%d日") - weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] - weekday = weekdays[now.weekday()] - system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) - return system_prompt - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - stream = kwargs.get("stream", False) - system_prompt_template = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - system_prompt = kwargs.get('system_prompt',system_prompt_template) or "" - system_prompt = cls.get_common_system_prompt(system_prompt) - prefill = kwargs.get('prefill',None) - messages = [ - ("placeholder", "{chat_history}"), - HumanMessagePromptTemplate.from_template("{query}") - ] - if system_prompt: - messages.insert(0,SystemMessage(content=system_prompt)) - - if prefill is not None: - messages.append(AIMessage(content=prefill)) - - messages_template = ChatPromptTemplate.from_messages(messages) - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - chain = messages_template | RunnableLambda(lambda x: x.messages) - if stream: - chain = ( - chain | RunnableLambda(lambda messages: llm.stream(messages)) - | RunnableLambda(lambda x: (i.content for i in x)) - ) - else: - chain = chain | llm | RunnableLambda(lambda x: x.content) - - return chain - - -class Claude21ChatChain(Claude2ChatChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceChatChain(Claude2ChatChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetChatChain(Claude2ChatChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuChatChain(Claude2ChatChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35SonnetChatChain(Claude2ChatChain): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" - - -class Mixtral8x7bChatChain(Claude2ChatChain): - model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - default_model_kwargs = {"max_tokens": 4096, "temperature": 0.01} - - -class Baichuan2Chat13B4BitsChatChain(LLMChain): - model_id = LLMModelType.BAICHUAN2_13B_CHAT - intent_type = LLMTaskType.CHAT - default_model_kwargs = { - "max_new_tokens": 2048, - "temperature": 0.3, - "top_k": 5, - "top_p": 0.85, - "do_sample": True, - } - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - stream = kwargs.get("stream", False) - # chat_history = kwargs.pop('chat_history',[]) - model_kwargs = model_kwargs or {} - model_kwargs.update({"stream": stream}) - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - llm_chain = RunnableLambda(lambda x: llm.invoke(x, stream=stream)) - return llm_chain - - -class Iternlm2Chat7BChatChain(LLMChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - intent_type = LLMTaskType.CHAT - - default_model_kwargs = {"temperature": 0.5, "max_new_tokens": 1000} - - @staticmethod - def build_prompt( - query: str, - history=[], - meta_instruction="You are an AI assistant whose name is InternLM (书生·浦语).\n" - "- InternLM (书生·浦语) is a conversational language model that is developed by Shanghai AI Laboratory (上海人工智能实验室). It is designed to be helpful, honest, and harmless.\n" - "- InternLM (书生·浦语) can understand and communicate fluently in the language chosen by the user such as English and 中文.", - ): - prompt = "" - if meta_instruction: - prompt += f"""<|im_start|>system\n{meta_instruction}<|im_end|>\n""" - for record in history: - prompt += f"""<|im_start|>user\n{record[0]}<|im_end|>\n<|im_start|>assistant\n{record[1]}<|im_end|>\n""" - prompt += f"""<|im_start|>user\n{query}<|im_end|>\n<|im_start|>assistant\n""" - return prompt - - @classmethod - def create_history(cls, x): - chat_history = x.get("chat_history", []) - chat_history = convert_to_messages(chat_history) - - assert len(chat_history) % 2 == 0, chat_history - history = [] - for i in range(0, len(chat_history), 2): - user_message = chat_history[i] - ai_message = chat_history[i + 1] - assert ( - user_message.type == HUMAN_MESSAGE_TYPE - and ai_message.type == AI_MESSAGE_TYPE - ), chat_history - history.append((user_message.content, ai_message.content)) - return history - - @classmethod - def create_prompt(cls, x,system_prompt=None): - history = cls.create_history(x) - if system_prompt is None: - system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - prompt = cls.build_prompt( - query=x["query"], - history=history, - meta_instruction=system_prompt, - ) - return prompt - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - stream = kwargs.get("stream", False) - system_prompt = kwargs.get("system_prompt",None) - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - - prompt_template = RunnablePassthrough.assign( - prompt=RunnableLambda(lambda x: cls.create_prompt(x,system_prompt=system_prompt)) - ) - llm_chain = prompt_template | RunnableLambda( - lambda x: llm.invoke(x, stream=stream) - ) - return llm_chain - - -class Iternlm2Chat20BChatChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B - - -class GLM4Chat9BChatChain(LLMChain): - model_id = LLMModelType.GLM_4_9B_CHAT - intent_type = LLMTaskType.CHAT - default_model_kwargs = { - "max_new_tokens": 1024, - "timeout": 60, - "temperature": 0.1, - } - @classmethod - def create_chat_history(cls,x, system_prompt=None): - if system_prompt is None: - system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - chat_history = x['chat_history'] - - if system_prompt is not None: - chat_history = [{"role":"system","content": system_prompt}] + chat_history - chat_history = chat_history + [{"role":MessageType.HUMAN_MESSAGE_TYPE,"content":x['query']}] - - return chat_history - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - system_prompt = kwargs.get("system_prompt",None) - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - **kwargs - ) - - chain = RunnablePassthrough.assign( - chat_history = RunnableLambda(lambda x: cls.create_chat_history(x,system_prompt=system_prompt)) - ) | RunnableLambda(lambda x: llm.invoke(x)) - - return chain - - -class Qwen2Instruct7BChatChain(LLMChain): - model_id = LLMModelType.QWEN2INSTRUCT7B - intent_type = LLMTaskType.CHAT - default_model_kwargs = { - "max_tokens": 1024, - "temperature": 0.1, - } - - @classmethod - def create_chat_history(cls,x, system_prompt=None): - if system_prompt is None: - system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - chat_history = x['chat_history'] - - if system_prompt is not None: - chat_history = [{"role":"system", "content": system_prompt}] + chat_history - - chat_history = chat_history + [{"role": MessageType.HUMAN_MESSAGE_TYPE, "content":x['query']}] - return chat_history - - - @classmethod - def parse_function_calls_from_ai_message(cls,message:dict): - return message['text'] - - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - stream = kwargs.get("stream", False) - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - system_prompt = kwargs.get("system_prompt",None) - - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - **kwargs - ) - - chain = RunnablePassthrough.assign( - chat_history = RunnableLambda(lambda x: cls.create_chat_history(x,system_prompt=system_prompt)) - ) | RunnableLambda(lambda x: llm.invoke(x)) | RunnableLambda(lambda x: cls.parse_function_calls_from_ai_message(x)) - - return chain - -class Qwen2Instruct72BChatChain(Qwen2Instruct7BChatChain): - model_id = LLMModelType.QWEN2INSTRUCT72B - - -class Qwen2Instruct72BChatChain(Qwen2Instruct7BChatChain): - model_id = LLMModelType.QWEN15INSTRUCT32B - - -class ChatGPT35ChatChain(LLMChain): - model_id = LLMModelType.CHATGPT_35_TURBO_0125 - intent_type = LLMTaskType.CHAT - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - stream = kwargs.get("stream", False) - system_prompt = kwargs.get('system_prompt',None) - prefill = kwargs.get('prefill',None) - messages = [ - ("placeholder", "{chat_history}"), - HumanMessagePromptTemplate.from_template("{query}") - ] - if system_prompt is not None: - messages.insert(SystemMessage(content=system_prompt),0) - - if prefill is not None: - messages.append(AIMessage(content=prefill)) - - messages_template = ChatPromptTemplate.from_messages(messages) - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - chain = messages_template | RunnableLambda(lambda x: x.messages) - if stream: - chain = ( - chain | RunnableLambda(lambda messages: llm.stream(messages)) - | RunnableLambda(lambda x: (i.content for i in x)) - ) - else: - chain = chain | llm | RunnableLambda(lambda x: x.content) - - return chain - -class ChatGPT4ChatChain(ChatGPT35ChatChain): - model_id = LLMModelType.CHATGPT_4_TURBO - -class ChatGPT4oChatChain(ChatGPT35ChatChain): - model_id = LLMModelType.CHATGPT_4O diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/conversation_summary_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/conversation_summary_chain.py deleted file mode 100644 index c3f1aa1d..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/conversation_summary_chain.py +++ /dev/null @@ -1,215 +0,0 @@ -# conversation summary chain -from typing import List -import json -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough, -) - - -from ..llm_models import Model -from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain -from common_logic.common_utils.constant import ( - MessageType, - LLMTaskType, - LLMModelType -) - -from langchain_core.messages import( - AIMessage, - BaseMessage, - HumanMessage, - SystemMessage, - convert_to_messages -) -from langchain.prompts import ( - HumanMessagePromptTemplate, - ChatPromptTemplate -) - -from common_logic.common_utils.prompt_utils import get_prompt_template -from common_logic.common_utils.logger_utils import get_logger,print_llm_messages - -logger = get_logger("conversation_summary") - -AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE -HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE -QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE -SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE - - -class Iternlm2Chat20BConversationSummaryChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B - default_model_kwargs = { - "max_new_tokens": 300, - "temperature": 0.1, - "stop_tokens": ["\n\n"], - } - - @classmethod - def create_prompt(cls, x,system_prompt=None): - chat_history = x["chat_history"] - conversational_contexts = [] - for his in chat_history: - role = his['role'] - assert role in [HUMAN_MESSAGE_TYPE, AI_MESSAGE_TYPE] - if role == HUMAN_MESSAGE_TYPE: - conversational_contexts.append(f"USER: {his['content']}") - else: - conversational_contexts.append(f"AI: {his['content']}") - if system_prompt is None: - system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - conversational_context = "\n".join(conversational_contexts) - prompt = cls.build_prompt( - system_prompt.format( - history=conversational_context, question=x["query"] - ) - ) - prompt = prompt + "Standalone Question: " - return prompt - -class Iternlm2Chat7BConversationSummaryChain(Iternlm2Chat20BConversationSummaryChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - - -class Claude2ConversationSummaryChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.CONVERSATION_SUMMARY_TYPE - - default_model_kwargs = {"max_tokens": 2000, "temperature": 0.1, "top_p": 0.9} - prefill = "From PersonU's point of view, here is the single standalone sentence:" - - @staticmethod - def create_conversational_context(chat_history:List[BaseMessage]): - conversational_contexts = [] - for his in chat_history: - assert isinstance(his,(AIMessage,HumanMessage)), his - content = his.content - if isinstance(his,HumanMessage): - conversational_contexts.append(f"USER: {content}") - else: - conversational_contexts.append(f"AI: {content}") - conversational_context = "\n".join(conversational_contexts) - return conversational_context - - @classmethod - def format_conversation(cls,conversation:list[BaseMessage]): - conversation_strs = [] - for message in conversation: - assert isinstance(message,(AIMessage,HumanMessage)), message - content = message.content - if isinstance(message, HumanMessage): - conversation_strs.append(f"PersonU: {content}") - elif isinstance(message, AIMessage): - conversation_strs.append(f"PersonA: {content}") - return "\n".join(conversation_strs) - - @classmethod - def create_messages_inputs(cls,x:dict,user_prompt,few_shots:list[dict]): - # create few_shots - few_shot_messages = [] - for few_shot in few_shots: - conversation=cls.format_conversation( - convert_to_messages(few_shot['conversation']) - ) - few_shot_messages.append(HumanMessage(content=user_prompt.format( - conversation=conversation, - current_query=few_shot['conversation'][-1]['content'] - ))) - few_shot_messages.append(AIMessage(content=f"{cls.prefill} {few_shot['rewrite_query']}")) - - # create current cocnversation - cur_messages = convert_to_messages( - x['chat_history'] + [{"role":MessageType.HUMAN_MESSAGE_TYPE,"content":x['query']}] - ) - - conversation = cls.format_conversation(cur_messages) - return { - "conversation":conversation, - "few_shots":few_shot_messages, - "current_query": x['query'] - } - - @classmethod - def create_messages_chain(cls,**kwargs): - system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - user_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="user_prompt" - ).prompt_template - - few_shots = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="few_shots" - ).prompt_template - - system_prompt = kwargs.get("system_prompt", system_prompt) - user_prompt = kwargs.get('user_prompt', user_prompt) - - cqr_template = ChatPromptTemplate.from_messages([ - SystemMessage(content=system_prompt), - ('placeholder','{few_shots}'), - HumanMessagePromptTemplate.from_template(user_prompt), - AIMessage(content=cls.prefill) - ]) - return RunnableLambda(lambda x: cls.create_messages_inputs(x,user_prompt=user_prompt,few_shots=json.loads(few_shots))) | cqr_template - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - ) - messages_chain = cls.create_messages_chain(**kwargs) - chain = messages_chain | RunnableLambda(lambda x: print_llm_messages(f"conversation summary messages: {x.messages}") or x.messages) \ - | llm | RunnableLambda(lambda x: x.content) - return chain - - -class Claude21ConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Qwen2Instruct72BConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.QWEN2INSTRUCT72B - - -class Qwen2Instruct72BConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.QWEN15INSTRUCT32B - - -class Qwen2Instruct7BConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.QWEN2INSTRUCT7B - - -class GLM4Chat9BConversationSummaryChain(Claude2ConversationSummaryChain): - model_id = LLMModelType.GLM_4_9B_CHAT - - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/hyde_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/hyde_chain.py deleted file mode 100644 index 45609a82..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/hyde_chain.py +++ /dev/null @@ -1,103 +0,0 @@ -# hyde - -from langchain.prompts import ( - ChatPromptTemplate, - HumanMessagePromptTemplate, -) -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough, -) - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType -) - -from ..llm_chains import LLMChain -from ..llm_models import Model as LLM_Model -from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain - -HYDE_TYPE = LLMTaskType.HYDE_TYPE - -WEB_SEARCH_TEMPLATE = """Please write a passage to answer the question -Question: {query} -Passage:""" -# hyde_web_search_template = PromptTemplate(template=WEB_SEARCH_TEMPLATE, input_variables=["query"]) - - -class Claude2HydeChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = HYDE_TYPE - - default_model_kwargs = { - "temperature": 0.5, - "max_tokens": 1000, - "stop_sequences": ["\n\nHuman:"], - } - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - # query_key = kwargs.pop("query_key", "query") - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - llm = LLM_Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - ) - prompt = ChatPromptTemplate.from_messages( - [HumanMessagePromptTemplate.from_template(WEB_SEARCH_TEMPLATE)] - ) - chain = RunnablePassthrough.assign( - hyde_doc=prompt | llm | RunnableLambda(lambda x: x.content) - ) - return chain - - -class Claude21HydeChain(Claude2HydeChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceHydeChain(Claude2HydeChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetHydeChain(Claude2HydeChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuHydeChain(Claude2HydeChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35SonnetHydeChain(Claude2HydeChain): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" - - -internlm2_meta_instruction = "You are a helpful AI Assistant." - - -class Iternlm2Chat7BHydeChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - intent_type = HYDE_TYPE - - default_model_kwargs = {"temperature": 0.1, "max_new_tokens": 200} - - @classmethod - def create_prompt(cls, x): - query = f"""Please write a brief passage to answer the question. \nQuestion: {prompt}""" - prompt = ( - cls.build_prompt( - query=query, - meta_instruction=internlm2_meta_instruction, - ) - + "Passage: " - ) - return prompt - - -class Iternlm2Chat20BHydeChain(Iternlm2Chat7BHydeChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B - intent_type = HYDE_TYPE diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/intention_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/intention_chain.py deleted file mode 100644 index 292023fd..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/intention_chain.py +++ /dev/null @@ -1,224 +0,0 @@ -import json -import os -from functools import lru_cache -from random import Random - -from langchain.prompts import ( - ChatPromptTemplate, - HumanMessagePromptTemplate, -) -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough, -) - -from common_logic.common_utils.constant import LLMTaskType,LLMModelType -from ..llm_models import Model -from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain - -abs_dir = os.path.dirname(__file__) - -intent_save_path = os.path.join( - os.path.dirname(os.path.dirname(abs_dir)), - "intent_utils", - "intent_examples", - "examples.json", -) - - -@lru_cache() -def load_intention_file(intent_save_path=intent_save_path, seed=42): - intent_few_shot_examples = json.load(open(intent_save_path)) - intent_indexs = { - intent_d["intent"]: intent_d["index"] - for intent_d in intent_few_shot_examples["intents"] - } - few_shot_examples = [] - intents = list(intent_few_shot_examples["examples"].keys()) - for intent in intents: - examples = intent_few_shot_examples["examples"][intent] - for query in examples: - few_shot_examples.append({"intent": intent, "query": query}) - # shuffle - Random(seed).shuffle(few_shot_examples) - return { - "few_shot_examples": few_shot_examples, - "intent_indexs": intent_indexs, - } - - -class Iternlm2Chat7BIntentRecognitionChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - intent_type =LLMTaskType.INTENT_RECOGNITION_TYPE - - default_model_kwargs = { - "temperature": 0.1, - "max_new_tokens": 100, - "stop_tokens": ["\n", "。", "."], - } - - @classmethod - def create_prompt(cls, x): - r = load_intention_file(intent_save_path) - few_shot_examples = r["few_shot_examples"] - # intent_indexs = r['intent_indexs'] - exmaple_template = "问题: {query}\n类别: {label}" - example_strs = [] - for example in few_shot_examples: - example_strs.append( - exmaple_template.format(query=example["query"], label=example["intent"]) - ) - - example_str = "\n\n".join(example_strs) - - meta_instruction = f"你是一个问题分类助理,正在对用户的问题进行分类。为了辅助你进行问题分类,下面给出一些示例:\n{example_str}" - query_str = exmaple_template.format(query=x["query"], label="") - prompt_template = """请对下面的问题进行分类: - {query_str} - """ - prompt = cls.build_prompt( - prompt_template.format(query_str=query_str), - meta_instruction=meta_instruction, - ) - prompt = prompt + f"根据前面给到的示例, 问题{x['query']}属于类别:" - - return prompt - - @staticmethod - def postprocess(intent): - intent = intent.replace("。", "").replace(".", "").strip().strip("**") - r = load_intention_file(intent_save_path) - intent_indexs = r["intent_indexs"] - assert intent in intent_indexs, (intent, intent_indexs) - return intent - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) - chain = chain | RunnableLambda(lambda x: cls.postprocess(x)) - return chain - - -class Iternlm2Chat20BIntentRecognitionChain(Iternlm2Chat7BIntentRecognitionChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B - - -INTENT_RECOGINITION_PROMPT_TEMPLATE_CLUADE = """Please classify this query: {query}. The categories are: - -{categories} - -Some examples of how to classify queries: -{examples} - -Now classify the original query. Respond with just one letter corresponding to the correct category. -""" - - -INTENT_RECOGINITION_EXAMPLE_TEMPLATE = """{query}\n{label}""" - - -class Claude2IntentRecognitionChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.INTENT_RECOGNITION_TYPE - - default_model_kwargs = { - "temperature": 0, - "max_tokens": 2000, - "stop_sequences": ["\n\n", "\n\nHuman:"], - } - - @classmethod - def create_few_shot_examples(cls): - ret = [] - for intent in cls.intents: - examples = cls.intent_few_shot_examples["examples"][intent] - for query in examples: - ret.append({"intent": intent, "query": query}) - return ret - - @classmethod - def create_few_shot_example_string( - cls, example_template=INTENT_RECOGINITION_EXAMPLE_TEMPLATE - ): - example_strs = [] - intent_indexs = cls.intent_indexs - for example in cls.few_shot_examples: - example_strs.append( - example_template.format( - label=intent_indexs[example["intent"]], query=example["query"] - ) - ) - return "\n\n".join(example_strs) - - @classmethod - def create_all_labels_string(cls): - intent_few_shot_examples = cls.intent_few_shot_examples - label_strs = [] - labels = intent_few_shot_examples["intents"] - for i, label in enumerate(labels): - label_strs.append(f"({label['index']}) {label['describe']}") - return "\n".join(label_strs) - - def postprocess(self, output: str): - out = output.strip() - assert out, output - return self.index_intents[out[0]] - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - r = load_intention_file(intent_save_path) - cls.few_shot_examples = r["few_shot_examples"] - cls.intent_indexs = r["intent_indexs"] - - cls.index_intents = {v: k for k, v in cls.intent_indexs.items()} - cls.intents = list(cls.intent_few_shot_examples["examples"].keys()) - cls.few_shot_examples = cls.create_few_shot_examples() - - cls.examples_str = cls.create_few_shot_example_string( - example_template=INTENT_RECOGINITION_EXAMPLE_TEMPLATE - ) - cls.categories_str = cls.create_all_labels_string() - - intent_recognition_prompt = ChatPromptTemplate.format_messages( - [ - HumanMessagePromptTemplate.from_template( - INTENT_RECOGINITION_PROMPT_TEMPLATE_CLUADE - ) - ] - ) - - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs) - - chain = ( - RunnablePassthrough.assign( - categories=lambda x: cls.categories_str, - examples=lambda x: cls.examples_str, - ) - | intent_recognition_prompt - | llm - | RunnableLambda(lambda x: cls.postprocess(x.content)) - ) - - return chain - - -class Claude21IntentRecognitionChain(Claude2IntentRecognitionChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceIntentRecognitionChain(Claude2IntentRecognitionChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetIntentRecognitionChain(Claude2IntentRecognitionChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuIntentRecognitionChain(Claude2IntentRecognitionChain): - model_id = LLMModelType.CLAUDE_3_HAIKU diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/llm_chain_base.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/llm_chain_base.py deleted file mode 100644 index 98ae93d3..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/llm_chain_base.py +++ /dev/null @@ -1,26 +0,0 @@ -class LLMChainMeta(type): - def __new__(cls, name, bases, attrs): - new_cls = type.__new__(cls, name, bases, attrs) - if name == "LLMChain": - return new_cls - new_cls.model_map[new_cls.get_chain_id()] = new_cls - return new_cls - - -class LLMChain(metaclass=LLMChainMeta): - model_map = {} - - @classmethod - def get_chain_id(cls): - return cls._get_chain_id(cls.model_id, cls.intent_type) - - @staticmethod - def _get_chain_id(model_id, intent_type): - return f"{model_id}__{intent_type}" - - @classmethod - def get_chain(cls, model_id, intent_type, model_kwargs=None, **kwargs): - return cls.model_map[cls._get_chain_id(model_id, intent_type)].create_chain( - model_kwargs=model_kwargs, **kwargs - ) - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/__init__.py deleted file mode 100644 index 1307aab1..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from .mkt_conversation_summary import ( - Claude21MKTConversationSummaryChain, - ClaudeInstanceMKTConversationSummaryChain, - Claude2MKTConversationSummaryChain, - Claude3HaikuMKTConversationSummaryChain, - Claude3SonnetMKTConversationSummaryChain, - Iternlm2Chat7BMKTConversationSummaryChain, - Iternlm2Chat20BMKTConversationSummaryChain -) - -from .mkt_rag_chain import ( - Iternlm2Chat7BKnowledgeQaChain, - Iternlm2Chat20BKnowledgeQaChain -) - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py deleted file mode 100644 index 4b04e90b..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_conversation_summary.py +++ /dev/null @@ -1,120 +0,0 @@ - -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough, -) - -from ..chat_chain import Claude2ChatChain, Iternlm2Chat7BChatChain - -from common_logic.common_utils.constant import ( - MessageType, - LLMTaskType, - LLMModelType -) - -AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE -HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE -QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE -SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE -MKT_CONVERSATION_SUMMARY_TYPE = LLMTaskType.MKT_CONVERSATION_SUMMARY_TYPE - -CHIT_CHAT_SYSTEM_TEMPLATE = """You are a helpful AI Assistant""" - -class Iternlm2Chat7BMKTConversationSummaryChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - intent_type = MKT_CONVERSATION_SUMMARY_TYPE - - @classmethod - def create_prompt(cls, x): - return x["prompt"] - - @classmethod - def _create_prompt(cls, x): - chat_history = x["chat_history"] - assert len(chat_history) % 2 == 0, chat_history - - history = [] - questions = [] - for i in range(0, len(chat_history), 2): - assert chat_history[i].type == HUMAN_MESSAGE_TYPE, chat_history - assert chat_history[i + 1].type == AI_MESSAGE_TYPE, chat_history - questions.append(chat_history[i].content) - history.append((chat_history[i].content, chat_history[i + 1].content)) - - questions_str = "" - for i, question in enumerate(questions): - questions_str += f"问题{i+1}: {question}\n" - # print(questions_str) - query_input = """请总结上述对话中的内容,为每一轮对话单独做一个不超过50个字的简短总结。\n""" - prompt = cls.build_prompt( - meta_instruction=CHIT_CHAT_SYSTEM_TEMPLATE, - history=history, - query=query_input, - ) - prompt_assist = f"好的,根据提供历史对话信息,共有{len(history)}段对话:\n{questions_str}\n对它们的总结如下(每一个总结要先复述一下问题):\n" - prefix = f"问题1: {questions[0]}\n总结:" - # thread_local.mkt_conversation_prefix = prefix - # print(thread_local,thread_local.mkt_conversation_prefix) - prompt = prompt + prompt_assist + prefix - # prompt = prompt - return {"prompt": prompt, "prefix": prefix} - - @staticmethod - def stream_postprocess_fn(x): - yield x["prefix"] - yield from x["llm_output"] - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - stream = kwargs.get("stream", False) - llm_chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) - chain = ( - RunnablePassthrough.assign(prompt_dict=lambda x: cls._create_prompt(x)) - | RunnablePassthrough.assign( - prompt=lambda x: x["prompt_dict"]["prompt"], - prefix=lambda x: x["prompt_dict"]["prefix"], - ) - | RunnablePassthrough.assign(llm_output=llm_chain) - ) - if stream: - chain = chain | RunnableLambda(lambda x: cls.stream_postprocess_fn(x)) - else: - chain = chain | RunnableLambda(lambda x: x["prefix"] + x["llm_output"]) - return chain - - -class Iternlm2Chat20BMKTConversationSummaryChain( - Iternlm2Chat7BMKTConversationSummaryChain -): - model_id = LLMModelType.INTERNLM2_CHAT_20B - - -class Claude2MKTConversationSummaryChain(Claude2ChatChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = MKT_CONVERSATION_SUMMARY_TYPE - - default_model_kwargs = {"max_tokens": 2000, "temperature": 0.1, "top_p": 0.9} - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) - query_input = """请简要总结上述对话中的内容,每一个对话单独一个总结,并用 '- '开头。 每一个总结要先说明问题。\n""" - chain = RunnablePassthrough.assign(query=lambda x: query_input) | chain - return chain - - -class Claude21MKTConversationSummaryChain(Claude2MKTConversationSummaryChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceMKTConversationSummaryChain(Claude2MKTConversationSummaryChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetMKTConversationSummaryChain(Claude2MKTConversationSummaryChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuMKTConversationSummaryChain(Claude2MKTConversationSummaryChain): - model_id = LLMModelType.CLAUDE_3_HAIKU diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py deleted file mode 100644 index 43754b02..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/marketing_chains/mkt_rag_chain.py +++ /dev/null @@ -1,55 +0,0 @@ -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType -) -from ..chat_chain import Iternlm2Chat7BChatChain -from common_logic.common_utils.prompt_utils import register_prompt_templates,get_prompt_template - -INTERLM2_RAG_PROMPT_TEMPLATE = "你是一个Amazon AWS的客服助理小Q,帮助的用户回答使用AWS过程中的各种问题。\n面对用户的问题,你需要给出中文回答,注意不要在回答中重复输出内容。\n下面给出相关问题的背景知识, 需要注意的是如果你认为当前的问题不能在背景知识中找到答案, 你需要拒答。\n背景知识:\n{context}\n\n" - -register_prompt_templates( - model_ids=[LLMModelType.INTERNLM2_CHAT_7B,LLMModelType.INTERNLM2_CHAT_20B], - task_type=LLMTaskType.RAG, - prompt_template=INTERLM2_RAG_PROMPT_TEMPLATE, - prompt_name="system_prompt" -) - -class Iternlm2Chat7BKnowledgeQaChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - intent_type = LLMTaskType.RAG - default_model_kwargs = {"temperature": 0.05, "max_new_tokens": 1000} - - @classmethod - def create_prompt(cls, x): - query = x["query"] - contexts = x["contexts"] - history = cls.create_history(x) - context = "\n".join(contexts) - prompt_template = get_prompt_template( - model_id = cls.model_id, - task_type = cls.task_type, - prompt_name = "system_prompt" - ).prompt_template - meta_instruction = prompt_template.format(context) - # meta_instruction = f"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use simplified Chinese to response the qustion. I’m going to tip $300K for a better answer! " - # meta_instruction = f'You are an expert AI on a question and answer task. \nUse the "Following Context" when answering the question. If you don't know the answer, reply to the "Following Text" in the header and answer to the best of your knowledge, or if you do know the answer, answer without the "Following Text"' - # meta_instruction = """You are an expert AI on a question and answer task. - # Use the "Following Context" when answering the question. If you don't know the answer, reply to the "Following Text" in the header and answer to the best of your knowledge, or if you do know the answer, answer without the "Following Text". If a question is asked in Korean, translate it to English and always answer in Korean. - # Following Text: "I didn't find the answer in the context given, but here's what I know! **I could be wrong, so cross-verification is a must!**""" - # meta_instruction = """You are an expert AI on a question and answer task. - # Use the "Following Context" when answering the question. If you don't know the answer, reply to the "Sorry, I don't know". """ - # query = f"Question: {query}\nContext:\n{context}" - # query = f"""Following Context: {context} - # Question: {query}""" - query = f"问题: {query}" - prompt = cls.build_prompt( - query=query, history=history, meta_instruction=meta_instruction - ) - # prompt = prompt + "回答: 让我先来判断一下问题的答案是否包含在背景知识中。" - prompt = prompt + f"回答: 经过慎重且深入的思考, 根据背景知识, 我的回答如下:\n" - print("internlm2 prompt: \n", prompt) - return prompt - - -class Iternlm2Chat20BKnowledgeQaChain(Iternlm2Chat7BKnowledgeQaChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B \ No newline at end of file diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/query_rewrite_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/query_rewrite_chain.py deleted file mode 100644 index 6eab55f9..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/query_rewrite_chain.py +++ /dev/null @@ -1,143 +0,0 @@ -# query rewrite -import re - -from langchain.prompts import PromptTemplate -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough, -) - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType -) -from ..llm_chains import LLMChain -from ..llm_models import Model as LLM_Model -from .chat_chain import Iternlm2Chat7BChatChain -from .llm_chain_base import LLMChain - -QUERY_REWRITE_TYPE = LLMTaskType.QUERY_REWRITE_TYPE -query_expansion_template_claude = PromptTemplate.from_template("""You are an AI language model assistant. Your task is to generate 1 - 5 different sub questions OR alternate versions of the given user question to retrieve relevant documents from a vector database. - -By generating multiple versions of the user question, -your goal is to help the user overcome some of the limitations -of distance-based similarity search. - -By generating sub questions, you can break down questions that refer to multiple concepts into distinct questions. This will help you get the relevant documents for constructing a final answer - -If multiple concepts are present in the question, you should break into sub questions, with one question for each concept - -Provide these alternative questions separated by newlines between XML tags. For example: - - -- Question 1 -- Question 2 -- Question 3 - - -Original question: {question}""") - - -class Claude2QueryRewriteChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = QUERY_REWRITE_TYPE - - default_model_kwargs = { - "temperature": 0.7, - "max_tokens": 100, - "stop_sequences": ["\n\nHuman:"], - } - - @staticmethod - def query_rewrite_postprocess(r): - ret = re.findall(".*?", r, re.S)[0] - questions = re.findall("- (.*?)\n", ret, re.S) - return questions - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - query_key = kwargs.pop("query_key", "query") - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - llm = LLM_Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - chain = ( - RunnablePassthrough.assign(question=lambda x: x[query_key]) - | query_expansion_template_claude - | llm - | RunnableLambda(cls.query_rewrite_postprocess) - ) - return chain - - -class Claude21QueryRewriteChain(Claude2QueryRewriteChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceQueryRewriteChain(Claude2QueryRewriteChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3HaikuQueryRewriteChain(Claude2QueryRewriteChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude3SonnetQueryRewriteChain(Claude2QueryRewriteChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude35SonnetQueryRewriteChain(Claude2QueryRewriteChain): - mdoel_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" - - -internlm2_meta_instruction = """You are an AI language model assistant. Your task is to generate 1 - 5 different sub questions OR alternate versions of the given user question to retrieve relevant documents from a vector database. - -By generating multiple versions of the user question, -your goal is to help the user overcome some of the limitations -of distance-based similarity search. - -By generating sub questions, you can break down questions that refer to multiple concepts into distinct questions. This will help you get the relevant documents for constructing a final answer - -If multiple concepts are present in the question, you should break into sub questions, with one question for each concept - -Provide these alternative questions separated by newlines between XML tags. For example: - - -- Question 1 -- Question 2 -- Question 3 -""" - - -class Iternlm2Chat7BQueryRewriteChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - intent_type = QUERY_REWRITE_TYPE - - default_model_kwargs = {"temperature": 0.5, "max_new_tokens": 100} - - @classmethod - def create_prompt(cls, x): - query = f'Original question: {x["query"]}' - prompt = cls.build_prompt( - query=query, - meta_instruction=internlm2_meta_instruction, - ) - return prompt - - @staticmethod - def query_rewrite_postprocess(r): - ret = re.findall(".*?", r, re.S)[0] - questions = re.findall("- (.*?)\n", ret, re.S) - return questions - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) - chain = chain | RunnableLambda(lambda x: cls.query_rewrite_postprocess(x)) - return chain - - -class Iternlm2Chat20BQueryRewriteChain(Iternlm2Chat7BQueryRewriteChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B - intent_type = QUERY_REWRITE_TYPE diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/rag_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/rag_chain.py deleted file mode 100644 index f04750f6..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/rag_chain.py +++ /dev/null @@ -1,161 +0,0 @@ -# rag llm chains -from langchain.prompts import ( - ChatPromptTemplate, - HumanMessagePromptTemplate, - SystemMessagePromptTemplate -) - -from langchain.schema.runnable import RunnableLambda, RunnablePassthrough -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType -) -from common_logic.common_utils.prompt_utils import get_prompt_template -from common_logic.common_utils.logger_utils import print_llm_messages - -# from ...prompt_template import convert_chat_history_from_fstring_format -from ..llm_models import Model -from .llm_chain_base import LLMChain - - -def get_claude_rag_context(contexts: list): - assert isinstance(contexts, list), contexts - context_xmls = [] - context_template = """\n{content}\n""" - for i, context in enumerate(contexts): - context_xml = context_template.format(index=i + 1, content=context) - context_xmls.append(context_xml) - - context = "\n".join(context_xmls) - return context - - -class Claude2RagLLMChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.RAG - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - stream = kwargs.get("stream", False) - system_prompt_template = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - system_prompt_template = kwargs.get("system_prompt",system_prompt_template) - - chat_messages = [ - SystemMessagePromptTemplate.from_template(system_prompt_template), - ("placeholder", "{chat_history}"), - HumanMessagePromptTemplate.from_template("{query}") - ] - context_chain = RunnablePassthrough.assign( - context=RunnableLambda(lambda x: get_claude_rag_context(x["contexts"])) - ) - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - chain = context_chain | ChatPromptTemplate.from_messages(chat_messages) | RunnableLambda(lambda x: print_llm_messages(f"rag messages: {x.messages}") or x) - if stream: - chain = ( - chain - | RunnableLambda(lambda x: llm.stream(x.messages)) - | RunnableLambda(lambda x: (i.content for i in x)) - ) - else: - chain = chain | llm | RunnableLambda(lambda x: x.content) - return chain - - -class Claude21RagLLMChain(Claude2RagLLMChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceRAGLLMChain(Claude2RagLLMChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetRAGLLMChain(Claude2RagLLMChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuRAGLLMChain(Claude2RagLLMChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - -class Mixtral8x7bChatChain(Claude2RagLLMChain): - model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - - -from .chat_chain import GLM4Chat9BChatChain - -class GLM4Chat9BRagChain(GLM4Chat9BChatChain): - model_id = LLMModelType.GLM_4_9B_CHAT - intent_type = LLMTaskType.RAG - - @classmethod - def create_chat_history(cls,x, system_prompt=None): - if system_prompt is None: - system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - context = ("\n" + "="*50+ "\n").join(x['contexts']) - system_prompt = system_prompt.format(context=context) - - return super().create_chat_history(x,system_prompt=system_prompt) - - -from .chat_chain import Qwen2Instruct7BChatChain - -class Qwen2Instruct7BRagChain(Qwen2Instruct7BChatChain): - model_id = LLMModelType.QWEN2INSTRUCT7B - intent_type = LLMTaskType.RAG - - @classmethod - def create_chat_history(cls,x, system_prompt=None): - if system_prompt is None: - system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="system_prompt" - ).prompt_template - - context = ("\n\n").join(x['contexts']) - system_prompt = system_prompt.format(context=context) - return super().create_chat_history(x,system_prompt=system_prompt) - - -class Qwen2Instruct72BRagChain(Qwen2Instruct7BRagChain): - model_id = LLMModelType.QWEN2INSTRUCT72B - - -class Qwen2Instruct72BRagChain(Qwen2Instruct7BRagChain): - model_id = LLMModelType.QWEN15INSTRUCT32B - - -from .chat_chain import Baichuan2Chat13B4BitsChatChain - -class Baichuan2Chat13B4BitsKnowledgeQaChain(Baichuan2Chat13B4BitsChatChain): - model_id = LLMModelType.BAICHUAN2_13B_CHAT - intent_type = LLMTaskType.RAG - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - llm_chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) - - def add_system_prompt(x): - context = "\n".join(x["contexts"]) - _chat_history = x["chat_history"] + [ - ("system", f"给定下面的背景知识:\n{context}\n回答下面的问题:\n") - ] - return _chat_history - - chat_history_chain = RunnablePassthrough.assign( - chat_history=RunnableLambda(lambda x: add_system_prompt(x)) - ) - llm_chain = chat_history_chain | llm_chain - return llm_chain - - - - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/__init__.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/__init__.py deleted file mode 100644 index 83c50b0a..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from .retail_conversation_summary_chain import ( - Claude2RetailConversationSummaryChain, - Claude21RetailConversationSummaryChain, - Claude3HaikuRetailConversationSummaryChain, - Claude3SonnetRetailConversationSummaryChain, - ClaudeInstanceRetailConversationSummaryChain -) - -from .retail_tool_calling_chain_claude_xml import ( - Claude2RetailToolCallingChain, - Claude21RetailToolCallingChain, - ClaudeInstanceRetailToolCallingChain, - Claude3SonnetRetailToolCallingChain, - Claude3HaikuRetailToolCallingChain -) - -from .retail_tool_calling_chain_json import ( - GLM4Chat9BRetailToolCallingChain -) - -from .auto_evaluation_chain import ( - Claude3HaikuAutoEvaluationChain, - Claude21AutoEvaluationChain, - Claude2AutoEvaluationChain - -) \ No newline at end of file diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py deleted file mode 100644 index bcdd7011..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/auto_evaluation_chain.py +++ /dev/null @@ -1,99 +0,0 @@ -# auto evaluation based on llms -import re - -from langchain.schema.runnable import RunnableLambda, RunnablePassthrough -from langchain_core.messages import AIMessage,SystemMessage,HumanMessage -from common_logic.common_utils.logger_utils import get_logger -from langchain.prompts import ChatPromptTemplate,HumanMessagePromptTemplate -from langchain_core.messages import convert_to_messages -from common_logic.common_utils.constant import ( - MessageType, - LLMTaskType, - LLMModelType, -) -from ...llm_models import Model -from ..llm_chain_base import LLMChain - -from ..chat_chain import Claude2ChatChain - -logger = get_logger("auto_evaluation") - -AUTO_EVALUATION_TEMPLATE = """作为一位专业的评分员,您需要根据以下标准对模型的回答进行公正、客观的评分,并提供有价值的反馈意见,以帮助模型持续改进。 - -### 评分标准 - -- 满分为10分,最低分为1分, 分值为一个 float 类型。 -- 模型回答与标准答案的相关性越高,得分越高。 -- 如果模型的回答出现大量重复内容,可以直接给0分。 -- 除了内容相关性,还需考虑回答的完整性、逻辑性和语言表达。 -- 请先在xml 标签 中写下你的评分理由。 -- 最后在 xml 标签 中写下你的最终评分。 - -### 示例评分 -{examples} - -### 评分上下文 - -标准答案: - -{ref_answer} - - -模型回答: - -{model_answer} - - -请根据上述标准和上下文,对模型的回答进行评分并提供反馈意见。让我们一起努力,提高模型的表现! -""" - - -class Claude2AutoEvaluationChain(Claude2ChatChain): - intent_type = LLMTaskType.AUTO_EVALUATION - model_id = LLMModelType.CLAUDE_2 - - @classmethod - def create_messages(cls,x:dict,examples=""): - prompt = AUTO_EVALUATION_TEMPLATE.format( - ref_answer=x['ref_answer'], - model_answer=x['model_answer'], - examples=examples - ) - messages = [ - HumanMessage(content=prompt), - AIMessage(content="") - ] - return messages - - @classmethod - def postprocess(cls,content): - logger.info(f"auto eval content: {content}") - try: - score = float(re.findall("(.*?)",content)[0].strip()) - return score - except Exception as e: - logger.error(f"error: {e}, content: {content}") - raise e - - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - chain = RunnableLambda(lambda x: cls.create_messages(x)) | llm | RunnableLambda(lambda x: cls.postprocess(x.content)) - return chain - - -class Claude21AutoEvaluationChain(Claude2AutoEvaluationChain): - model_id = LLMModelType.CLAUDE_21 - - - -class Claude3HaikuAutoEvaluationChain(Claude2AutoEvaluationChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude3SonnetAutoEvaluationChain(Claude2AutoEvaluationChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py deleted file mode 100644 index d5be022e..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_conversation_summary_chain.py +++ /dev/null @@ -1,208 +0,0 @@ -# conversation summary chain -from typing import List - -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough, -) - - -from ...llm_models import Model -from ..llm_chain_base import LLMChain -from common_logic.common_utils.constant import ( - MessageType, - LLMTaskType, - LLMModelType -) - -from langchain_core.messages import( - AIMessage, - HumanMessage, - BaseMessage, - convert_to_messages -) -from langchain.prompts import ( - HumanMessagePromptTemplate, - ChatPromptTemplate -) -from ..chat_chain import GLM4Chat9BChatChain - -AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE -HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE -QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE -SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE - - -CQR_TEMPLATE = """# CONTEXT # -下面有一段客户和客服的对话数据(包含在里面),以及当前客户的一个回复(包含在)。 - -{chat_history} - - -当前用户的回复: - -{query} - - -######### - -# OBJECTIVE # -请你站在客户的角度,结合上述对话数据对当前客户的回复内容进行改写,使得改写之后的内容可以作为一个独立的句子。 - -######### - -# STYLE # -改写后的回复需要和里面的内容意思一致。 - -######### - -# RESPONSE FORMAT # -请直接用中文进行回答 -""" - - -class Claude2RetailConversationSummaryChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.RETAIL_CONVERSATION_SUMMARY_TYPE - default_model_kwargs = {"max_tokens": 2000, "temperature": 0.1, "top_p": 0.9} - CQR_TEMPLATE = CQR_TEMPLATE - @staticmethod - def create_conversational_context(chat_history:List[BaseMessage]): - conversational_contexts = [] - for his in chat_history: - role = his.type - content = his.content - assert role in [HUMAN_MESSAGE_TYPE, AI_MESSAGE_TYPE],(role,[HUMAN_MESSAGE_TYPE, AI_MESSAGE_TYPE]) - if role == HUMAN_MESSAGE_TYPE: - conversational_contexts.append(f"客户: {content}") - else: - conversational_contexts.append(f"客服: {content}") - conversational_context = "\n".join(conversational_contexts) - return conversational_context - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - cqr_template = ChatPromptTemplate.from_messages([ - HumanMessagePromptTemplate.from_template(cls.CQR_TEMPLATE), - AIMessage(content="好的,站在客户的角度,我将当前用户的回复内容改写为: ") - ]) - - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - ) - cqr_chain = RunnablePassthrough.assign( - conversational_context=RunnableLambda( - lambda x: cls.create_conversational_context( - convert_to_messages(x["chat_history"]) - ) - )) \ - | RunnableLambda(lambda x: cqr_template.format(chat_history=x['conversational_context'],query=x['query'])) \ - | llm | RunnableLambda(lambda x: x.content) - - return cqr_chain - - -class Claude21RetailConversationSummaryChain(Claude2RetailConversationSummaryChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35SonnetRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" - - -MIXTRAL_CQR_TEMPLATE = """下面有一段客户和客服的对话,以及当前客户的一个回复,请你站在客户的角度,结合上述对话数据对当前客户的回复内容进行改写,使得改写之后的内容可以作为一个独立的句子。下面是改写的要求: -- 改写后的回复需要和当前客户的一个回复的内容意思一致。 -- 请直接用中文进行回答。 - -# 客户和客服的对话: -{chat_history} - -# 当前客户的回复: -{query} -""" - - -class Mixtral8x7bRetailConversationSummaryChain(Claude2RetailConversationSummaryChain): - model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - default_model_kwargs = {"max_tokens": 1000, "temperature": 0.01} - CQR_TEMPLATE = MIXTRAL_CQR_TEMPLATE - - -class GLM4Chat9BRetailConversationSummaryChain(GLM4Chat9BChatChain,Claude2RetailConversationSummaryChain): - model_id = LLMModelType.GLM_4_9B_CHAT - intent_type = LLMTaskType.RETAIL_CONVERSATION_SUMMARY_TYPE - CQR_TEMPLATE = MIXTRAL_CQR_TEMPLATE - - @classmethod - def create_chat_history(cls,x): - conversational_context = cls.create_conversational_context( - convert_to_messages(x["chat_history"]) - ) - prompt = cls.CQR_TEMPLATE.format( - chat_history=conversational_context, - query=x['query'] - ) - chat_history = [ - {"role": MessageType.HUMAN_MESSAGE_TYPE, - "content": prompt - }, - { - "role":MessageType.AI_MESSAGE_TYPE, - "content": "好的,站在客户的角度,我将当前用户的回复内容改写为: " - } - ] - - return chat_history - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - **kwargs - ) - - cqr_chain = RunnablePassthrough.assign( - chat_history = RunnableLambda(lambda x: cls.create_chat_history(x)) - ) | RunnableLambda(lambda x: llm.invoke(x)) - - return cqr_chain - - -class Qwen2Instruct7BRetailConversationSummaryChain(GLM4Chat9BRetailConversationSummaryChain): - model_id = LLMModelType.QWEN2INSTRUCT7B - default_model_kwargs = { - "max_tokens": 1024, - "temperature": 0.1, - } - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - chain = super().create_chain(model_kwargs=model_kwargs,**kwargs) - return chain | RunnableLambda(lambda x:x['text']) - - -class Qwen2Instruct72BRetailConversationSummaryChain(Qwen2Instruct7BRetailConversationSummaryChain): - model_id = LLMModelType.QWEN2INSTRUCT72B - - -class Qwen2Instruct72BRetailConversationSummaryChain(Qwen2Instruct7BRetailConversationSummaryChain): - model_id = LLMModelType.QWEN15INSTRUCT32B \ No newline at end of file diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py deleted file mode 100644 index 803e4ef2..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_claude_xml.py +++ /dev/null @@ -1,354 +0,0 @@ -# tool calling chain -import json -from typing import List,Dict,Any -import re -from datetime import datetime - -from langchain.schema.runnable import ( - RunnableLambda, -) - -from langchain_core.messages import( - AIMessage, - SystemMessage -) -from langchain.prompts import ChatPromptTemplate - -from langchain_core.messages import AIMessage,SystemMessage,HumanMessage - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType, - SceneType -) -from functions import get_tool_by_name -from ..llm_chain_base import LLMChain -from ...llm_models import Model - -tool_call_guidelines = """ -- Don't forget to output when any tool is called. -- 每次回答总是先进行思考,并将思考过程写在标签中。请你按照下面的步骤进行思考: - 1. 判断根据当前的上下文是否足够回答用户的问题。 - 2. 如果当前的上下文足够回答用户的问题,请调用 `give_final_response` 工具。 - 3. 如果当前的上下文不能支持回答用户的问题,你可以考虑调用 标签中列举的工具。 - 4. 如果调用工具对应的参数不够,请调用反问工具 `give_rhetorical_question` 来让用户提供更加充分的信息。 - 5. 最后给出你要调用的工具名称。 -- Always output with "中文". - -""" - - -SYSTEM_MESSAGE_PROMPT=("你是安踏的客服助理小安, 主要职责是处理用户售前和售后的问题。下面是当前用户正在浏览的商品信息:\n\n{goods_info}\n" - "In this environment you have access to a set of tools you can use to answer the customer's question." - "\n" - "You may call them like this:\n" - "\n" - "\n" - "$TOOL_NAME\n" - "\n" - "<$PARAMETER_NAME>$PARAMETER_VALUE\n" - "...\n" - "\n" - "\n" - "\n" - "\n" - "Here are the tools available:\n" - "\n" - "{tools}" - "\n" - "\nAnswer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided." - f"\nHere are some guidelines for you:\n{tool_call_guidelines}" - ) - -SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES = SYSTEM_MESSAGE_PROMPT + ( - "Some examples of tool calls are given below, where the content within represents the most recent reply in the dialog." - "\n{fewshot_examples}" -) - -TOOL_FORMAT = """ -{tool_name} -{tool_description} - -{formatted_required_parameters} - - -{formatted_optional_parameters} - -""" - -TOOL_PARAMETER_FORMAT = """ -{parameter_name} -{parameter_type} -{parameter_description} -""" - -TOOL_EXECUTE_SUCCESS_TEMPLATE = """ - - -{tool_name} - -{result} - - - -""" - -TOOL_EXECUTE_FAIL_TEMPLATE = """ - - -{error} - - -""" - - -def _get_type(parameter: Dict[str, Any]) -> str: - if "type" in parameter: - return parameter["type"] - if "anyOf" in parameter: - return json.dumps({"anyOf": parameter["anyOf"]}) - if "allOf" in parameter: - return json.dumps({"allOf": parameter["allOf"]}) - return json.dumps(parameter) - - -def convert_openai_tool_to_anthropic(tools:list[dict])->str: - formatted_tools = tools - tools_data = [ - { - "tool_name": tool["name"], - "tool_description": tool["description"], - "formatted_required_parameters": "\n".join( - [ - TOOL_PARAMETER_FORMAT.format( - parameter_name=name, - parameter_type=_get_type(parameter), - parameter_description=parameter.get("description"), - ) for name, parameter in tool["parameters"]["properties"].items() - if name in tool["parameters"].get("required", []) - ] - ), - "formatted_optional_parameters": "\n".join( - [ - TOOL_PARAMETER_FORMAT.format( - parameter_name=name, - parameter_type=_get_type(parameter), - parameter_description=parameter.get("description"), - ) for name, parameter in tool["parameters"]["properties"].items() - if name not in tool["parameters"].get("required", []) - ] - ), - } - for tool in formatted_tools - ] - tools_formatted = "\n".join( - [ - TOOL_FORMAT.format( - tool_name=tool["tool_name"], - tool_description=tool["tool_description"], - formatted_required_parameters=tool["formatted_required_parameters"], - formatted_optional_parameters=tool["formatted_optional_parameters"], - ) - for tool in tools_data - ] - ) - return tools_formatted - - -class Claude2RetailToolCallingChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.RETAIL_TOOL_CALLING - default_model_kwargs = { - "max_tokens": 2000, - "temperature": 0.1, - "top_p": 0.9, - "stop_sequences": ["\n\nHuman:", "\n\nAssistant",""], - } - - @staticmethod - def format_fewshot_examples(fewshot_examples:list[dict]): - fewshot_example_strs = [] - for fewshot_example in fewshot_examples: - param_strs = [] - for p,v in fewshot_example['kwargs'].items(): - param_strs.append(f"<{p}>{v}\n" - f"{fewshot_example['query']}\n" - f"\n" - "\n" - "\n" - f"{fewshot_example['name']}\n" - "\n" - f"{param_str}" - "\n" - "\n" - "\n" - "\n" - "" - ) - fewshot_example_strs.append(fewshot_example_str) - fewshot_example_str = '\n'.join(fewshot_example_strs) - return f"\n{fewshot_example_str}\n" - - @classmethod - def parse_function_calls_from_ai_message(cls,message:AIMessage): - content = "" + message.content + "" - function_calls:List[str] = re.findall("(.*?)", content,re.S) - if not function_calls: - content = "" + message.content - - return { - "function_calls": function_calls, - "content": content - } - - - @staticmethod - def generate_chat_history(state:dict): - chat_history = state['chat_history'] \ - + [{"role": "user","content":state['query']}] \ - + state['agent_tool_history'] - return {"chat_history":chat_history} - - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - tools:list[dict] = kwargs['tools'] - - tool_names = [tool['name'] for tool in tools] - - # add two extral tools - if "give_rhetorical_question" not in tool_names: - tools.append(get_tool_by_name("give_rhetorical_question",scene=SceneType.RETAIL).tool_def) - - if "give_final_response" not in tool_names: - tools.append(get_tool_by_name("give_final_response",scene=SceneType.RETAIL).tool_def) - - fewshot_examples = kwargs.get('fewshot_examples',[]) - if fewshot_examples: - fewshot_examples.append({ - "name": "give_rhetorical_question", - "query": "今天天气怎么样?", - "kwargs": {"question": "请问你想了解哪个城市的天气?"} - }) - - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - tools_formatted = convert_openai_tool_to_anthropic(tools) - goods_info = kwargs['goods_info'] - - if fewshot_examples: - system_prompt = SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES.format( - tools=tools_formatted, - fewshot_examples=cls.format_fewshot_examples( - fewshot_examples - ), - goods_info = goods_info - ) - else: - system_prompt = SYSTEM_MESSAGE_PROMPT.format( - tools=tools_formatted, - goods_info=goods_info - ) - - tool_calling_template = ChatPromptTemplate.from_messages( - [ - SystemMessage(content=system_prompt), - ("placeholder", "{chat_history}"), - AIMessage(content="") - ]) - - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - ) - chain = RunnableLambda(cls.generate_chat_history) | tool_calling_template \ - | RunnableLambda(lambda x: x.messages) \ - | llm | RunnableLambda(lambda message:cls.parse_function_calls_from_ai_message( - message - )) - - return chain - - -class Claude21RetailToolCallingChain(Claude2RetailToolCallingChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceRetailToolCallingChain(Claude2RetailToolCallingChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetRetailToolCallingChain(Claude2RetailToolCallingChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuRetailToolCallingChain(Claude2RetailToolCallingChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -MIXTRAL8X7B_QUERY_TEMPLATE = """下面是客户和客服的历史对话信息: -{chat_history} - -当前客户的问题是: {query} - -请你从安踏客服助理小安的角度回答客户当前的问题。你需要使用上述提供的各种工具进行回答。""" - - -class Mixtral8x7bRetailToolCallingChain(Claude2RetailToolCallingChain): - model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - default_model_kwargs = {"max_tokens": 1000, "temperature": 0.01,"stop":[""]} - - @classmethod - def parse_function_calls_from_ai_message(cls,message:AIMessage): - content = message.content.replace("\_","_") - function_calls:List[str] = re.findall("(.*?)", content + "",re.S) - if function_calls: - function_calls = [function_calls[0]] - if not function_calls: - content = message.content - return { - "function_calls": function_calls, - "content": content - } - - @staticmethod - def chat_history_to_string(chat_history:list[dict]): - chat_history_lc = ChatPromptTemplate.from_messages([ - ("placeholder", "{chat_history}") - ]).invoke({"chat_history":chat_history}).messages - - chat_history_strs = [] - for message in chat_history_lc: - assert isinstance(message,(HumanMessage,AIMessage)),message - if isinstance(message,HumanMessage): - chat_history_strs.append(f"客户: {message.content}") - else: - chat_history_strs.append(f"客服: {message.content}") - return "\n".join(chat_history_strs) - - - @classmethod - def generate_chat_history(cls,state:dict): - chat_history_str = cls.chat_history_to_string(state['chat_history']) - - chat_history = [{ - "role": "user", - "content": MIXTRAL8X7B_QUERY_TEMPLATE.format( - chat_history=chat_history_str, - query = state['query'] - ) - }] + state['agent_tool_history'] - return {"chat_history": chat_history} - - - - - - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py deleted file mode 100644 index d20bb6c0..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/retail_chains/retail_tool_calling_chain_json.py +++ /dev/null @@ -1,455 +0,0 @@ -# tool calling chain -import json -from typing import List,Dict,Any -import re -from datetime import datetime -import copy - -from langchain.schema.runnable import ( - RunnableLambda, -) - -from langchain_core.messages import( - AIMessage, - SystemMessage -) -from langchain.prompts import ChatPromptTemplate - -from langchain_core.messages import AIMessage,SystemMessage,HumanMessage - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType, - MessageType, - SceneType -) -from functions import get_tool_by_name - -from ..llm_chain_base import LLMChain -from ...llm_models import Model -from ..chat_chain import GLM4Chat9BChatChain -from common_logic.common_utils.logger_utils import get_logger - -logger = get_logger("retail_tool_calling_chain_json") - -GLM4_SYSTEM_PROMPT = """你是安踏的客服助理小安, 主要职责是处理用户售前和售后的问题。{date_prompt} -请遵守下面的规范回答用户的问题。 -## 回答规范 -- 如果用户的提供的信息不足以回答问题,尽量反问用户。 -- 回答简洁明了,一句话以内。 - -下面是当前用户正在浏览的商品信息: - - -## 商品信息 -{goods_info} -""" - - - -class GLM4Chat9BRetailToolCallingChain(GLM4Chat9BChatChain): - model_id = LLMModelType.GLM_4_9B_CHAT - intent_type = LLMTaskType.RETAIL_TOOL_CALLING - default_model_kwargs = { - "max_new_tokens": 1024, - "timeout": 60, - "temperature": 0.1, - } - DATE_PROMPT = "当前日期: %Y-%m-%d" - - @staticmethod - def convert_openai_function_to_glm(tools:list[dict]): - glm_tools = [] - for tool_def in tools: - tool_name = tool_def['name'] - description = tool_def['description'] - params = [] - required = tool_def['parameters'].get("required",[]) - for param_name,param in tool_def['parameters'].get('properties',{}).items(): - params.append({ - "name": param_name, - "description": param["description"], - "type": param["type"], - "required": param_name in required, - }) - glm_tools.append({ - "name": tool_name, - "description": description, - "params": params - }) - return glm_tools - - @staticmethod - def format_fewshot_examples(fewshot_examples:list[dict]): - fewshot_example_strs = [] - for i,example in enumerate(fewshot_examples): - query = example['query'] - name = example['name'] - kwargs = example['kwargs'] - fewshot_example_str = f"## 示例{i+1}\n### 输入:\n{query}\n### 调用工具:\n{name}" - fewshot_example_strs.append(fewshot_example_str) - return "\n\n".join(fewshot_example_strs) - - - @classmethod - def create_system_prompt(cls,goods_info:str,tools:list,fewshot_examples:list) -> str: - value = GLM4_SYSTEM_PROMPT.format( - goods_info=goods_info, - date_prompt=datetime.now().strftime(cls.DATE_PROMPT) - ) - if tools: - value += "\n\n# 可用工具" - contents = [] - for tool in tools: - content = f"\n\n## {tool['name']}\n\n{json.dumps(tool, ensure_ascii=False,indent=4)}" - content += "\n在调用上述函数时,请使用 Json 格式表示调用的参数。" - contents.append(content) - value += "".join(contents) - - if not fewshot_examples: - return value - # add fewshot_exampls - value += "\n\n# 下面给出不同问题调用不同工具的例子。" - value += f"\n\n{cls.format_fewshot_examples(fewshot_examples)}" - value += "\n\n请参考上述例子进行工具调用。" - return value - - @classmethod - def create_chat_history(cls,x,system_prompt=None): - _chat_history = x['chat_history'] + \ - [{"role":MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ - x['agent_tool_history'] - - chat_history = [] - for message in _chat_history: - new_message = message - if message['role'] == MessageType.AI_MESSAGE_TYPE: - new_message = { - **message - } - tool_calls = message.get('additional_kwargs',{}).get("tool_calls",[]) - if tool_calls: - new_message['metadata'] = tool_calls[0]['name'] - chat_history.append(new_message) - chat_history = [{"role": "system", "content": system_prompt}] + chat_history - return chat_history - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - tools:list = kwargs.get('tools',[]) - fewshot_examples = kwargs.get('fewshot_examples',[]) - glm_tools = cls.convert_openai_function_to_glm(tools) - system_prompt = cls.create_system_prompt( - goods_info=kwargs['goods_info'], - tools=glm_tools, - fewshot_examples=fewshot_examples - ) - kwargs['system_prompt'] = system_prompt - return super().create_chain(model_kwargs=model_kwargs,**kwargs) - - -from ..chat_chain import Qwen2Instruct7BChatChain - - - -class Qwen2Instruct72BRetailToolCallingChain(Qwen2Instruct7BChatChain): - model_id = LLMModelType.QWEN2INSTRUCT72B - intent_type = LLMTaskType.RETAIL_TOOL_CALLING - default_model_kwargs = { - "max_tokens": 1024, - "temperature": 0.1, - } - - DATE_PROMPT = "当前日期: %Y-%m-%d 。" - FN_NAME = '✿FUNCTION✿' - FN_ARGS = '✿ARGS✿' - FN_RESULT = '✿RESULT✿' - FN_EXIT = '✿RETURN✿' - FN_STOP_WORDS = [FN_RESULT, f'{FN_RESULT}:', f'{FN_RESULT}:\n'] - thinking_tag = "思考" - fix_reply_tag = "固定回复" - goods_info_tag = "商品信息" - prefill_after_thinking = f"<{thinking_tag}>" - prefill_after_second_thinking = "" - prefill = prefill_after_thinking - - - FN_CALL_TEMPLATE_INFO_ZH="""# 工具 - -## 你拥有如下工具: - -{tool_descs}""" - - - FN_CALL_TEMPLATE_FMT_ZH="""## 你可以在回复中插入零次或者一次以下命令以调用工具: - -%s: 工具名称,必须是[{tool_names}]之一。 -%s: 工具输入 -%s: 工具结果 -%s: 根据工具结果进行回复""" % ( - FN_NAME, - FN_ARGS, - FN_RESULT, - FN_EXIT, -) - TOOL_DESC_TEMPLATE="""### {name_for_human}\n\n{name_for_model}: {description_for_model} 输入参数:{parameters} {args_format}""" - - FN_CALL_TEMPLATE=FN_CALL_TEMPLATE_INFO_ZH + '\n\n' + FN_CALL_TEMPLATE_FMT_ZH - -# SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} - -# {{tools}} -# {{fewshot_examples}} - -# ## 当前用户正在浏览的商品信息 -# {{goods_info}} - -# # 思考 -# 你每次给出最终回复前都要按照下面的步骤输出你的思考过程, 注意你并不需要每次都进行所有步骤的思考。并将思考过程写在 XML 标签 <{thinking_tag}> 和 中: -# Step 1. 根据各个工具的描述,分析当前用户的回复和各个示例中的Input相关性,如果跟某个示例对应的Input相关性强,直接跳过后续所有步骤,之后按照示例中Output的工具名称进行调用。 -# Step 2. 如果你觉得可以依据商品信息 <{goods_info_tag}> 里面的内容进行回答,就直接就回答,不需要调用任何工具。并结束思考。 -# Step 3. 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户下面 XML 标签 <{fix_reply_tag}> 里面的内容: -# <{fix_reply_tag}> 亲亲,请问还有什么问题吗? -# Step 4. 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。结束思考,输出结束思考符号。 - -# ## 回答规范 -# - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的 -# - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!” -# - 如果客户的回复里面包含订单号,则直接回复 ”您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“ -# - 只能思考一次,在结束思考符号“”之后给出最终的回复。不要重复输出文本,段落,句子。思考之后的文本保持简洁,有且仅能包含一句话。{{non_ask_rules}}""" -# SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} - -# {{tools}} -# {{fewshot_examples}} - -# ## 当前用户正在浏览的商品信息 -# {{goods_info}} - -# # 你每次给出最终回复前都要参考下面的回复策略: -# 1. 根据各个工具的描述,分析当前用户的回复和各个示例中的Input相关性,如果跟某个示例对应的Input相关性强,直接跳过后续所有步骤,之后按照示例中Output的工具名称进行调用。 -# 2. 如果你觉得可以依据商品信息 <{goods_info_tag}> 里面的内容进行回答,就直接就回答,不需要调用任何工具。 -# 3. 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户下面 XML 标签 <{fix_reply_tag}> 里面的内容: -# <{fix_reply_tag}> 亲亲,请问还有什么问题吗? -# 4. 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。 - -# ## 回答规范 -# - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的 -# - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!“ -# - 如果客户的回复里面包含订单号,则直接回复 “您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“{{non_ask_rules}}""" - - SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} - -{{tools}} -{{fewshot_examples}} - -## 当前用户正在浏览的商品信息 -{{goods_info}} - -# 回复策略 -在你给出最终回复前可以在XML标签 <{thinking_tag}> 和 中输出你的回复策略。下面是一些常见的回复策略: - - 如果根据各个工具的描述,当前用户的回复跟某个示例对应的Input相关性强,直接按照示例中Output的工具名称进行调用。 - - 考虑使用商品信息 <{goods_info_tag}> 里面的内容回答用户的问题。 - - 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户: “ 亲亲,请问还有什么问题吗?“ - - 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。 - - 如果客户的回复里面包含订单号,则直接回复 “您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“ - - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!“ - -## Tips - - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的。 - - 回答必须简洁,不允许出现超过2句话的回复。{{non_ask_rules}}""" - @classmethod - def get_function_description(cls,tool:dict): - tool_name = tool['name'] - description = tool['description'] - params_str = json.dumps(tool.get('parameters',{}),ensure_ascii=False) - args_format = '此工具的输入应为JSON对象。' - return cls.TOOL_DESC_TEMPLATE.format( - name_for_human=tool_name, - name_for_model=tool_name, - description_for_model=description, - parameters=params_str, - args_format=args_format - ).rstrip() - - - @classmethod - def format_fewshot_examples(cls,fewshot_examples:list[dict]): - fewshot_example_strs = [] - for i,example in enumerate(fewshot_examples): - query = example['query'] - name = example['name'] - kwargs = example['kwargs'] - fewshot_example_str = f"""## 工具调用例子{i+1}\nInput:\n{query}\nOutput:\n{cls.FN_NAME}: {name}\n{cls.FN_ARGS}: {json.dumps(kwargs,ensure_ascii=False)}\n{cls.FN_RESULT}""" - fewshot_example_strs.append(fewshot_example_str) - return "\n\n".join(fewshot_example_strs) - - - @classmethod - def create_system_prompt(cls,goods_info:str,tools:list[dict],fewshot_examples:list,create_time=None) -> str: - tool_descs = '\n\n'.join(cls.get_function_description(tool) for tool in tools) - tool_names = ','.join(tool['name'] for tool in tools) - tool_system = cls.FN_CALL_TEMPLATE.format( - tool_descs=tool_descs, - tool_names=tool_names - ) - fewshot_examples_str = "" - if fewshot_examples: - fewshot_examples_str = "\n\n# 下面给出不同客户回复下调用不同工具的例子。" - fewshot_examples_str += f"\n\n{cls.format_fewshot_examples(fewshot_examples)}" - fewshot_examples_str += "\n\n请参考上述例子进行工具调用。" - - non_ask_tool_list = [] - # for tool in tools: - # should_ask_parameter = get_tool_by_name(tool['name']).should_ask_parameter - # if should_ask_parameter != "True": - # format_string = tool['name']+"工具"+should_ask_parameter - # non_ask_tool_list.append(format_string) - if len(non_ask_tool_list) == 0: - non_ask_rules = "" - else: - non_ask_rules = "\n - " + ','.join(non_ask_tool_list) - - if create_time: - datetime_object = datetime.strptime(create_time, '%Y-%m-%d %H:%M:%S.%f') - else: - datetime_object = datetime.now() - logger.info(f"create_time: {create_time} is not valid, use current time instead.") - return cls.SYSTEM_PROMPT.format( - goods_info=goods_info, - tools=tool_system, - fewshot_examples=fewshot_examples_str, - non_ask_rules=non_ask_rules, - date_prompt=datetime_object.strftime(cls.DATE_PROMPT) - ) - - @classmethod - def create_chat_history(cls,x,system_prompt=None): - # deal with function - _chat_history = x['chat_history'] + \ - [{"role": MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ - x['agent_tool_history'] - - # print(f'chat_history_before create: {_chat_history}') - # merge chat_history - chat_history = [] - if system_prompt is not None: - chat_history.append({ - "role": MessageType.SYSTEM_MESSAGE_TYPE, - "content":system_prompt - }) - - # move tool call results to assistant - for i,message in enumerate(copy.deepcopy(_chat_history)): - role = message['role'] - if i==0: - assert role == MessageType.HUMAN_MESSAGE_TYPE, f"The first message should comes from human role" - - if role == MessageType.TOOL_MESSAGE_TYPE: - assert chat_history[-1]['role'] == MessageType.AI_MESSAGE_TYPE,_chat_history - chat_history[-1]['content'] += message['content'] - continue - elif role == MessageType.AI_MESSAGE_TYPE: - # continue ai message - if chat_history[-1]['role'] == MessageType.AI_MESSAGE_TYPE: - chat_history[-1]['content'] += message['content'] - continue - - chat_history.append(message) - - # move the last tool call message to user - if chat_history[-1]['role'] == MessageType.AI_MESSAGE_TYPE: - assert chat_history[-2]['role'] == MessageType.HUMAN_MESSAGE_TYPE,chat_history - tool_calls = chat_history[-1].get("additional_kwargs",{}).get("tool_calls",[]) - if tool_calls: - chat_history[-2]['content'] += ("\n\n" + chat_history[-1]['content']) - chat_history = chat_history[:-1] - - return chat_history - - - @classmethod - def parse_function_calls_from_ai_message(cls,message:dict): - stop_reason = message['stop_reason'] - content = f"{cls.prefill}" + message['text'] - content = content.strip() - stop_reason = stop_reason or "" - - - function_calls = re.findall(f"{cls.FN_NAME}.*?{cls.FN_RESULT}", content + stop_reason,re.S) - return { - "function_calls":function_calls, - "content":content - } - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - tools:list = kwargs.get('tools',[]) - # add extral tools - if "give_rhetorical_question" not in tools: - tools.append(get_tool_by_name("give_rhetorical_question",scene=SceneType.RETAIL).tool_def) - fewshot_examples = kwargs.get('fewshot_examples',[]) - system_prompt = cls.create_system_prompt( - goods_info=kwargs['goods_info'], - create_time=kwargs.get('create_time',None), - tools=tools, - fewshot_examples=fewshot_examples - ) - - agent_current_call_number = kwargs['agent_current_call_number'] - - # give different prefill - if agent_current_call_number == 0: - cls.prefill = cls.prefill_after_thinking - else: - cls.prefill = cls.prefill_after_second_thinking - - # cls.prefill = '' - - model_kwargs = model_kwargs or {} - kwargs['system_prompt'] = system_prompt - model_kwargs = {**model_kwargs} - # model_kwargs["stop"] = model_kwargs.get("stop",[]) + ['✿RESULT✿', '✿RESULT✿:', '✿RESULT✿:\n','✿RETURN✿',f'<{cls.thinking_tag}>',f'<{cls.thinking_tag}/>'] - model_kwargs["stop"] = model_kwargs.get("stop",[]) + ['✿RESULT✿', '✿RESULT✿:', '✿RESULT✿:\n','✿RETURN✿',f'<{cls.thinking_tag}/>'] - # model_kwargs["prefill"] = "我先看看调用哪个工具,下面是我的思考过程:\n\nstep 1." - if "prefill" not in model_kwargs: - model_kwargs["prefill"] = f'{cls.prefill}' - return super().create_chain(model_kwargs=model_kwargs,**kwargs) - - -class Qwen2Instruct7BRetailToolCallingChain(Qwen2Instruct72BRetailToolCallingChain): - model_id = LLMModelType.QWEN2INSTRUCT7B - goods_info_tag = "商品信息" - SYSTEM_PROMPT=f"""你是安踏天猫的客服助理小安, 主要职责是处理用户售前和售后的问题。{{date_prompt}} - -{{tools}} -{{fewshot_examples}} - -## 当前用户正在浏览的商品信息 -{{goods_info}} - -# 回复策略 -下面是一些常见的回复策略: - - 如果根据各个工具的描述,当前用户的回复跟某个示例对应的Input相关性强,直接按照示例中Output的工具名称进行调用。 - - 考虑使用商品信息 <{goods_info_tag}> 里面的内容回答用户的问题。 - - 如果你觉得当前用户的回复意图不清晰,或者仅仅是表达一些肯定的内容,或者和历史消息没有很强的相关性,同时当前不是第一轮对话,直接回复用户: “ 亲亲,请问还有什么问题吗?“ - - 如果需要调用某个工具,检查该工具的必选参数是否可以在上下文中找到。 - - 如果客户的回复里面包含订单号,则直接回复 “您好,亲亲,这就帮您去查相关订单信息。请问还有什么问题吗?“ - - 当前主要服务天猫平台的客户,如果客户询问其他平台的问题,直接回复 “不好意思,亲亲,这里是天猫店铺,只能为您解答天猫的问题。建议您联系其他平台的客服或售后人员给您提供相关的帮助和支持。谢谢!“ - -## Tips - - 如果客户没有明确指出在哪里购买的商品,则默认都是在天猫平台购买的。 - - 回答必须简洁,不允许出现超过2句话的回复。{{non_ask_rules}}""" - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs["prefill"] = "" - res = super().create_chain(model_kwargs=model_kwargs,**kwargs) - cls.prefill = "" - return res - -class Qwen15Instruct32BRetailToolCallingChain(Qwen2Instruct7BRetailToolCallingChain): - model_id = LLMModelType.QWEN15INSTRUCT32B - - - - diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/stepback_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/stepback_chain.py deleted file mode 100644 index f17b4e4b..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/stepback_chain.py +++ /dev/null @@ -1,138 +0,0 @@ -from langchain.prompts import ( - ChatPromptTemplate, - FewShotChatMessagePromptTemplate, -) -from langchain.schema.runnable import RunnableLambda - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType -) -from ..llm_chains.chat_chain import Iternlm2Chat7BChatChain -from ..llm_chains.llm_chain_base import LLMChain -from ..llm_models import Model - -STEPBACK_PROMPTING_TYPE = LLMTaskType.STEPBACK_PROMPTING_TYPE - -class Iternlm2Chat7BStepBackChain(Iternlm2Chat7BChatChain): - model_id = LLMModelType.INTERNLM2_CHAT_7B - intent_type = STEPBACK_PROMPTING_TYPE - - default_model_kwargs = {"temperature": 0.1, "max_new_tokens": 200} - - @classmethod - def create_prompt(cls, x): - meta_instruction_template = "You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples: {few_examples}" - # meta_instruction_template = "你是一个拥有世界知识的专家. 你的任务是将问题转述为更通用的问题,这样更容易回答。更通用指的是将问题进行抽象表达,省略问题中的各种细节,包括具体时间,地点等。 下面有一些例子: {few_examples}" - - few_examples = [ - { - "input": "阿尔伯特-爱因斯坦的出生地是哪里?", - "output": "阿尔伯特-爱因斯坦的个人经历是怎样的?", - }, - { - "input": "特斯拉在中国上海有多少门店", - "output": "特斯拉在中国的门店分布情况", - }, - ] - - few_examples_template = """origin question: {origin_question} - step-back question: {step_back_question} - """ - few_examples_strs = [] - for few_example in few_examples: - few_examples_strs.append( - few_examples_template.format( - origin_question=few_example["input"], - step_back_question=few_example["output"], - ) - ) - meta_instruction = meta_instruction_template.format( - few_examples="\n\n".join(few_examples_strs) - ) - prompt = ( - cls.build_prompt( - query=f"origin question: {x['query']}", - history=[], - meta_instruction=meta_instruction, - ) - + "step-back question: " - ) - return prompt - - -class Iternlm2Chat20BStepBackChain(Iternlm2Chat7BStepBackChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B - intent_type = STEPBACK_PROMPTING_TYPE - - -class Claude2StepBackChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = STEPBACK_PROMPTING_TYPE - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - stream = kwargs.get("stream", False) - examples = [ - { - "input": "Could the members of The Police perform lawful arrests?", - "output": "what can the members of The Police do?", - }, - { - "input": "Jan Sindel’s was born in what country?", - "output": "what is Jan Sindel’s personal history?", - }, - ] - # We now transform these to example messages - example_prompt = ChatPromptTemplate.from_messages( - [ - ("human", "{input}"), - ("ai", "{output}"), - ] - ) - few_shot_prompt = FewShotChatMessagePromptTemplate( - example_prompt=example_prompt, - examples=examples, - ) - - prompt = ChatPromptTemplate.from_messages( - [ - ( - "system", - """You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:""", - ), - # Few shot examples - few_shot_prompt, - # New question - ("user", "{query}"), - ] - ) - - llm = Model.get_model(cls.model_id, model_kwargs=model_kwargs, **kwargs) - chain = prompt | llm - if stream: - chain = ( - prompt - | RunnableLambda(lambda x: llm.stream(x.messages)) - | RunnableLambda(lambda x: (i.content for i in x)) - ) - - else: - chain = prompt | llm | RunnableLambda(lambda x: x.content) - return chain - - -class Claude21StepBackChain(Claude2StepBackChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceStepBackChain(Claude2StepBackChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetStepBackChain(Claude2StepBackChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuStepBackChain(Claude2StepBackChain): - model_id = LLMModelType.CLAUDE_3_HAIKU diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py deleted file mode 100644 index 3fc57da5..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/tool_calling_chain_claude_xml.py +++ /dev/null @@ -1,320 +0,0 @@ -# tool calling chain -import json -from typing import List,Dict,Any -import re - -from langchain.schema.runnable import ( - RunnableLambda, - RunnablePassthrough -) -from common_logic.common_utils.prompt_utils import get_prompt_template -from common_logic.common_utils.logger_utils import print_llm_messages -from langchain_core.messages import( - AIMessage, - SystemMessage -) -from langchain.prompts import ChatPromptTemplate - -from langchain_core.messages import AIMessage,SystemMessage - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType, - MessageType -) -from common_logic.common_utils.time_utils import get_china_now - -from .llm_chain_base import LLMChain -from ..llm_models import Model - -incorrect_tool_call_example = """Here is an example of an incorrectly formatted tool call, which you should avoid. - - - -tool_name - - -question -string -value - - - - - - -In this incorrect tool calling example, the parameter `name` should form a XLM tag. -""" - - -SYSTEM_MESSAGE_PROMPT =(f"In this environment you have access to a set of tools you can use to answer the user's question.\n" - "\n" - "You may call them like this:\n" - "\n" - "\n" - "$TOOL_NAME\n" - "\n" - "<$PARAMETER_NAME>$PARAMETER_VALUE\n" - "...\n" - "\n" - "\n" - "\n" - "\n" - "Here are the tools available:\n" - "\n" - "{tools}" - "\n" - "\nAnswer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided." - "\nHere are some guidelines for you:\n{tool_call_guidelines}." - f"\n{incorrect_tool_call_example}" - ) - -SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES = SYSTEM_MESSAGE_PROMPT + ( - "Some examples of tool calls are given below, where the content within represents the most recent reply in the dialog." - "\n{fewshot_examples}" -) - -TOOL_FORMAT = """ -{tool_name} -{tool_description} - -{formatted_required_parameters} - - -{formatted_optional_parameters} - -""" - -TOOL_PARAMETER_FORMAT = """ -{parameter_name} -{parameter_type} -{parameter_description} -""" - -TOOL_EXECUTE_SUCCESS_TEMPLATE = """ - - -{tool_name} - -{result} - - - -""" - -TOOL_EXECUTE_FAIL_TEMPLATE = """ - - -{error} - - -""" - -AGENT_SYSTEM_PROMPT = "你是一个亚马逊云科技的AI助理,你的名字是亚麻小Q。今天是{date_str},{weekday}. " - - -def _get_type(parameter: Dict[str, Any]) -> str: - if "type" in parameter: - return parameter["type"] - if "anyOf" in parameter: - return json.dumps({"anyOf": parameter["anyOf"]}) - if "allOf" in parameter: - return json.dumps({"allOf": parameter["allOf"]}) - return json.dumps(parameter) - - -def convert_openai_tool_to_anthropic(tools:list[dict])->str: - formatted_tools = tools - tools_data = [ - { - "tool_name": tool["name"], - "tool_description": tool["description"], - "formatted_required_parameters": "\n".join( - [ - TOOL_PARAMETER_FORMAT.format( - parameter_name=name, - parameter_type=_get_type(parameter), - parameter_description=parameter.get("description"), - ) for name, parameter in tool["parameters"]["properties"].items() - if name in tool["parameters"].get("required", []) - ] - ), - "formatted_optional_parameters": "\n".join( - [ - TOOL_PARAMETER_FORMAT.format( - parameter_name=name, - parameter_type=_get_type(parameter), - parameter_description=parameter.get("description"), - ) for name, parameter in tool["parameters"]["properties"].items() - if name not in tool["parameters"].get("required", []) - ] - ), - } - for tool in formatted_tools - ] - tools_formatted = "\n".join( - [ - TOOL_FORMAT.format( - tool_name=tool["tool_name"], - tool_description=tool["tool_description"], - formatted_required_parameters=tool["formatted_required_parameters"], - formatted_optional_parameters=tool["formatted_optional_parameters"], - ) - for tool in tools_data - ] - ) - return tools_formatted - - -class Claude2ToolCallingChain(LLMChain): - model_id = LLMModelType.CLAUDE_2 - intent_type = LLMTaskType.TOOL_CALLING_XML - default_model_kwargs = { - "max_tokens": 2000, - "temperature": 0.1, - "top_p": 0.9, - "stop_sequences": ["\n\nHuman:", "\n\nAssistant",""], - } - - @staticmethod - def format_fewshot_examples(fewshot_examples:list[dict]): - fewshot_example_strs = [] - for fewshot_example in fewshot_examples: - param_strs = [] - for p,v in fewshot_example['kwargs'].items(): - param_strs.append(f"<{p}>{v}\n" - f"{fewshot_example['query']}\n" - f"\n" - "\n" - "\n" - f"{fewshot_example['name']}\n" - "\n" - f"{param_str}" - "\n" - "\n" - "\n" - "\n" - "" - ) - fewshot_example_strs.append(fewshot_example_str) - fewshot_example_str = '\n'.join(fewshot_example_strs) - return f"\n{fewshot_example_str}\n" - - @classmethod - def parse_function_calls_from_ai_message(cls,message:AIMessage): - content = "" + message.content + "" - function_calls:List[str] = re.findall("(.*?)", content,re.S) - if not function_calls: - content = "" + message.content - - return { - "function_calls": function_calls, - "content": content - } - - @classmethod - def create_chat_history(cls,x): - chat_history = x['chat_history'] + \ - [{"role": MessageType.HUMAN_MESSAGE_TYPE,"content": x['query']}] + \ - x['agent_tool_history'] - return chat_history - - @classmethod - def get_common_system_prompt(cls,system_prompt_template:str): - now = get_china_now() - date_str = now.strftime("%Y年%m月%d日") - weekdays = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'] - weekday = weekdays[now.weekday()] - system_prompt = system_prompt_template.format(date=date_str,weekday=weekday) - return system_prompt - - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - tools:list = kwargs['tools'] - fewshot_examples = kwargs.get('fewshot_examples',[]) - if fewshot_examples: - fewshot_examples.append({ - "name": "give_rhetorical_question", - "query": "今天天气怎么样?", - "kwargs": {"question": "请问你想了解哪个城市的天气?"} - }) - user_system_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="user_prompt" - ).prompt_template - - user_system_prompt = kwargs.get("user_prompt",None) or user_system_prompt - - user_system_prompt = cls.get_common_system_prompt( - user_system_prompt - ) - guidelines_prompt = get_prompt_template( - model_id=cls.model_id, - task_type=cls.intent_type, - prompt_name="guidelines_prompt" - ).prompt_template - - guidelines_prompt = kwargs.get("guidelines_prompt",None) or guidelines_prompt - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - tools_formatted = convert_openai_tool_to_anthropic(tools) - - if fewshot_examples: - system_prompt = SYSTEM_MESSAGE_PROMPT_WITH_FEWSHOT_EXAMPLES.format( - tools=tools_formatted, - fewshot_examples=cls.format_fewshot_examples(fewshot_examples), - tool_call_guidelines=guidelines_prompt - ) - else: - system_prompt = SYSTEM_MESSAGE_PROMPT.format( - tools=tools_formatted, - tool_call_guidelines=guidelines_prompt - ) - - system_prompt = user_system_prompt + system_prompt - tool_calling_template = ChatPromptTemplate.from_messages( - [ - SystemMessage(content=system_prompt), - ("placeholder", "{chat_history}"), - AIMessage(content="") - ]) - - llm = Model.get_model( - model_id=cls.model_id, - model_kwargs=model_kwargs, - ) - chain = RunnablePassthrough.assign(chat_history=lambda x: cls.create_chat_history(x)) | tool_calling_template \ - | RunnableLambda(lambda x: print_llm_messages(f"Agent messages: {x.messages}") or x.messages ) \ - | llm | RunnableLambda(lambda message:cls.parse_function_calls_from_ai_message( - message - )) - return chain - - -class Claude21ToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_21 - - -class ClaudeInstanceToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude3SonnetToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3HaikuToolCallingChain(Claude2ToolCallingChain): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35SonnetToolCallingChain(Claude2ToolCallingChain): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/translate_chain.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/translate_chain.py deleted file mode 100644 index 07b92b3b..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_chains/translate_chain.py +++ /dev/null @@ -1,40 +0,0 @@ -# translate chain -from langchain.schema.runnable import RunnableLambda - -from common_logic.common_utils.constant import ( - LLMTaskType, - LLMModelType -) -from .chat_chain import Iternlm2Chat7BChatChain - -QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE - - -class Iternlm2Chat7BTranslateChain(Iternlm2Chat7BChatChain): - intent_type = QUERY_TRANSLATE_TYPE - default_model_kwargs = {"temperature": 0.1, "max_new_tokens": 200} - - @classmethod - def create_prompt(cls, x): - query = x["query"] - target_lang = x["target_lang"] - history = cls.create_history(x) - meta_instruction = f"你是一个有经验的翻译助理, 正在将用户的问题翻译成{target_lang},不要试图去回答用户的问题,仅仅做翻译。" - query = f'将文本:\n "{query}" \n 翻译成{target_lang}。\n直接翻译文本,不要输出多余的文本。' - - prompt = cls.build_prompt( - query=query, history=history, meta_instruction=meta_instruction - ) - return prompt - - @classmethod - def create_chain(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - llm_chain = super().create_chain(model_kwargs=model_kwargs, **kwargs) - llm_chain = llm_chain | RunnableLambda(lambda x: x.strip('"')) # postprocess - return llm_chain - - -class Iternlm2Chat20BTranslateChain(Iternlm2Chat7BTranslateChain): - model_id = LLMModelType.INTERNLM2_CHAT_20B diff --git a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_models.py b/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_models.py deleted file mode 100644 index 5ca6dc14..00000000 --- a/source/lambda/online/lambda_llm_generate/__llm_generate_utils/llm_models.py +++ /dev/null @@ -1,382 +0,0 @@ -import json -import logging -import os -from datetime import datetime -from langchain_aws.chat_models import ChatBedrockConverse - - -import boto3 -from langchain_openai import ChatOpenAI -from langchain_community.chat_models import BedrockChat -from langchain_community.llms.sagemaker_endpoint import LineIterator - -from common_logic.common_utils.constant import ( - MessageType, - LLMModelType -) -from common_logic.common_utils.logger_utils import get_logger - -AI_MESSAGE_TYPE = MessageType.AI_MESSAGE_TYPE -HUMAN_MESSAGE_TYPE = MessageType.HUMAN_MESSAGE_TYPE -SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE - -logger = get_logger("llm_model") - -class ModeMixins: - @staticmethod - def convert_messages_role(messages:list[dict],role_map:dict): - """ - Args: - messages (list[dict]): - role_map (dict): {"current_role":"targe_role"} - - Returns: - _type_: as messages - """ - valid_roles = list(role_map.keys()) - new_messages = [] - for message in messages: - message = {**message} - role = message['role'] - assert role in valid_roles,(role,valid_roles,messages) - message['role'] = role_map[role] - new_messages.append(message) - return new_messages - - -class ModelMeta(type): - def __new__(cls, name, bases, attrs): - new_cls = type.__new__(cls, name, bases, attrs) - if name == "Model" or new_cls.model_id is None: - return new_cls - new_cls.model_map[new_cls.model_id] = new_cls - return new_cls - - -class Model(ModeMixins,metaclass=ModelMeta): - model_id = None - model_map = {} - - @classmethod - def get_model(cls, model_id, model_kwargs=None, **kwargs): - return cls.model_map[model_id].create_model(model_kwargs=model_kwargs, **kwargs) - -# Bedrock model type -class Claude2(Model): - model_id = LLMModelType.CLAUDE_2 - default_model_kwargs = {"max_tokens": 2000, "temperature": 0.7, "top_p": 0.9} - - @classmethod - def create_model(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - credentials_profile_name = ( - kwargs.get("credentials_profile_name", None) - or os.environ.get("AWS_PROFILE", None) - or None - ) - region_name = ( - kwargs.get("region_name", None) - or os.environ.get("BEDROCK_REGION", None) - or None - ) - llm = BedrockChat( - credentials_profile_name=credentials_profile_name, - region_name=region_name, - model_id=cls.model_id, - model_kwargs=model_kwargs, - ) - - return llm - - -class ClaudeInstance(Claude2): - model_id = LLMModelType.CLAUDE_INSTANCE - - -class Claude21(Claude2): - model_id = LLMModelType.CLAUDE_21 - - -class Claude3Sonnet(Claude2): - model_id = LLMModelType.CLAUDE_3_SONNET - - -class Claude3Haiku(Claude2): - model_id = LLMModelType.CLAUDE_3_HAIKU - - -class Claude35Sonnet(Claude2): - model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" - - -class Mixtral8x7b(Claude2): - model_id = LLMModelType.MIXTRAL_8X7B_INSTRUCT - default_model_kwargs = {"max_tokens": 4096, "temperature": 0.01} - -# Sagemker Inference type -class SagemakerModelBase(Model): - default_model_kwargs = None - content_type = "application/json" - accepts = "application/json" - - @classmethod - def create_client(cls, region_name): - client = boto3.client("sagemaker-runtime", region_name=region_name) - return client - - def __init__(self, model_kwargs=None, **kwargs) -> None: - self.model_kwargs = model_kwargs or {} - if self.default_model_kwargs is not None: - self.model_kwargs = {**self.default_model_kwargs, **self.model_kwargs} - - self.region_name = ( - kwargs.get("region_name", None) - or os.environ.get("AWS_REGION", None) - or None - ) - self.kwargs = kwargs - self.endpoint_name = kwargs["endpoint_name"] - self.client = self.create_client(self.region_name) - - @classmethod - def create_model(cls, model_kwargs=None, **kwargs): - return cls(model_kwargs=model_kwargs, **kwargs) - - def transform_input(self, x): - raise NotImplementedError - - def transform_output(self, output): - response = json.loads(output.read().decode("utf-8")) - return response - - def _stream(self, x): - body = self.transform_input(x) - resp = self.client.invoke_endpoint_with_response_stream( - EndpointName=self.endpoint_name, - Body=body, - ContentType=self.content_type, - ) - iterator = LineIterator(resp["Body"]) - for line in iterator: - resp = json.loads(line) - error_msg = resp.get("error_msg", None) - if error_msg: - raise RuntimeError(error_msg) - resp_output = resp.get("outputs") - yield resp_output - - def _invoke(self, x): - body = self.transform_input(x) - try: - response = self.client.invoke_endpoint( - EndpointName=self.endpoint_name, - Body=body, - ContentType=self.content_type, - Accept=self.accepts, - ) - except Exception as e: - raise ValueError(f"Error raised by inference endpoint: {e}") - response = self.transform_output(response["Body"]) - return response - - def invoke(self, x, stream=False): - x["stream"] = stream - if stream: - return self._stream(x) - else: - return self._invoke(x) - - -class Baichuan2Chat13B4Bits(SagemakerModelBase): - model_id = LLMModelType.BAICHUAN2_13B_CHAT - # content_handler=Baichuan2ContentHandlerChat() - default_model_kwargs = { - "max_new_tokens": 2048, - "temperature": 0.3, - "top_k": 5, - "top_p": 0.85, - # "repetition_penalty": 1.05, - "do_sample": True, - "timeout": 60, - } - - def transform_input(self, x): - query = x["query"] - _chat_history = x["chat_history"] - _chat_history = [ - {"role": message.type, "content": message.content} - for message in _chat_history - ] - - chat_history = [] - for message in _chat_history: - content = message["content"] - role = message["role"] - assert role in [ - MessageType.HUMAN_MESSAGE_TYPE, - MessageType.AI_MESSAGE_TYPE, - MessageType.SYSTEM_MESSAGE_TYPE, - ], f"invalid role: {role}" - if role == MessageType.AI_MESSAGE_TYPE: - role = "assistant" - elif role == MessageType.HUMAN_MESSAGE_TYPE: - role = "user" - - chat_history.append({"role": role, "content": content}) - _messages = chat_history + [{"role": "user", "content": query}] - messages = [] - system_messages = [] - for message in _messages: - if message["role"] == MessageType.SYSTEM_MESSAGE_TYPE: - system_messages.append(message) - else: - messages.append(message) - - if system_messages: - system_prompt = "\n".join([s["content"] for s in system_messages]) - first_content = messages[0]["content"] - messages[0]["content"] = f"{system_prompt}\n{first_content}" - - input_str = json.dumps( - { - "messages": messages, - "parameters": {"stream": x["stream"], **self.model_kwargs}, - } - ) - return input_str - - -class Internlm2Chat7B(SagemakerModelBase): - model_id = LLMModelType.INTERNLM2_CHAT_7B - default_model_kwargs = { - "max_new_tokens": 1024, - "timeout": 60, - # 'repetition_penalty':1.05, - # "do_sample":True, - "temperature": 0.1, - "top_p": 0.8, - } - - # meta_instruction = "You are a helpful AI Assistant" - - def transform_input(self, x): - logger.info(f'prompt char num: {len(x["prompt"])}') - body = { - "query": x["prompt"], - # "meta_instruction": x.get('meta_instruction',self.meta_instruction), - "stream": x["stream"], - # "history": history - } - body.update(self.model_kwargs) - # print('body',body) - input_str = json.dumps(body) - return input_str - - -class Internlm2Chat20B(Internlm2Chat7B): - model_id = LLMModelType.INTERNLM2_CHAT_20B - - -class GLM4Chat9B(SagemakerModelBase): - model_id = LLMModelType.GLM_4_9B_CHAT - default_model_kwargs = { - "max_new_tokens": 1024, - "timeout": 60, - "temperature": 0.1, - } - role_map={ - MessageType.SYSTEM_MESSAGE_TYPE: 'system', - MessageType.HUMAN_MESSAGE_TYPE: 'user', - MessageType.AI_MESSAGE_TYPE: "assistant", - MessageType.TOOL_MESSAGE_TYPE: "observation" - } - - def transform_input(self, x:dict): - _chat_history = self.convert_messages_role( - x['chat_history'], - role_map=self.role_map - ) - chat_history = [] - for message in _chat_history: - if message['role'] == "assistant": - content = message['content'] - if not content.endswith("<|observation|>"): - if not content.endswith("<|user|>"): - message['content'] = message['content'] + "<|user|>" - chat_history.append(message) - - logger.info(f"glm chat_history: {chat_history}") - body = { - "chat_history": chat_history, - "stream": x["stream"], - **self.model_kwargs - } - input_str = json.dumps(body) - return input_str - -class Qwen2Instruct7B(SagemakerModelBase): - model_id = LLMModelType.QWEN2INSTRUCT7B - default_model_kwargs = { - "max_tokens": 1024, - "stop":["<|endoftext|>","<|im_end|>"], - "temperature": 0.1, - } - role_map={ - MessageType.SYSTEM_MESSAGE_TYPE: 'system', - MessageType.HUMAN_MESSAGE_TYPE: 'user', - MessageType.AI_MESSAGE_TYPE: "assistant" - } - - def transform_input(self, x:dict): - chat_history = self.convert_messages_role( - x['chat_history'], - role_map=self.role_map - ) - - body = { - "chat_history": chat_history, - "stream": x["stream"], - **self.model_kwargs - } - logger.info(f"qwen body: {body}") - input_str = json.dumps(body) - return input_str - - -class Qwen2Instruct72B(Qwen2Instruct7B): - model_id = LLMModelType.QWEN2INSTRUCT72B - - -class Qwen2Instruct72B(Qwen2Instruct7B): - model_id = LLMModelType.QWEN15INSTRUCT32B - - -# ChatGPT model type -class ChatGPT35(Model): - model_id = "gpt-3.5-turbo-0125" - default_model_kwargs = {"max_tokens": 2000, "temperature": 0.7, "top_p": 0.9} - - @classmethod - def create_model(cls, model_kwargs=None, **kwargs): - model_kwargs = model_kwargs or {} - model_kwargs = {**cls.default_model_kwargs, **model_kwargs} - - credentials_profile_name = ( - kwargs.get("credentials_profile_name", None) - or os.environ.get("AWS_PROFILE", None) - or None - ) - region_name = ( - kwargs.get("region_name", None) - or os.environ.get("AWS_REGION", None) - or None - ) - - llm = ChatOpenAI( - model=cls.model_id, - model_kwargs=model_kwargs, - ) - - return llm From 7aa5eecd7a884a755e0998f97d16e22bae741a9a Mon Sep 17 00:00:00 2001 From: zhouxss Date: Wed, 13 Nov 2024 08:23:49 +0000 Subject: [PATCH 28/29] modify according to the pr comments --- .../common_logic/common_utils/constant.py | 2 +- .../langchain_integration/chains/__init__.py | 37 +++++++++---------- .../chains/conversation_summary_chain.py | 6 +-- .../chains/hyde_chain.py | 6 +-- .../chains/intention_chain.py | 6 +-- .../chains/marketing_chains/__init__.py | 8 ++-- .../mkt_conversation_summary.py | 8 ++-- .../chains/marketing_chains/mkt_rag_chain.py | 6 +-- .../chains/query_rewrite_chain.py | 6 +-- .../chains/stepback_chain.py | 6 +-- .../chains/translate_chain.py | 6 +-- .../main_utils/online_entries/common_entry.py | 3 +- source/portal/src/utils/const.ts | 4 +- 13 files changed, 52 insertions(+), 52 deletions(-) diff --git a/source/lambda/online/common_logic/common_utils/constant.py b/source/lambda/online/common_logic/common_utils/constant.py index 38c33782..e9f9c349 100644 --- a/source/lambda/online/common_logic/common_utils/constant.py +++ b/source/lambda/online/common_logic/common_utils/constant.py @@ -188,5 +188,5 @@ class KBType(Enum): class Threshold(ConstantBase): QQ_IN_RAG_CONTEXT = 0.5 - INTENTION_ALL_KNOWLEDGAE_RETRIEVE = 0.4 + INTENTION_ALL_KNOWLEDGE_RETRIEVAL = 0.4 diff --git a/source/lambda/online/common_logic/langchain_integration/chains/__init__.py b/source/lambda/online/common_logic/langchain_integration/chains/__init__.py index 9ba61fa1..fe79ec23 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/__init__.py @@ -10,7 +10,6 @@ def __new__(cls, name, bases, attrs): new_cls.model_map[new_cls.get_chain_id()] = new_cls return new_cls - class LLMChain(metaclass=LLMChainMeta): model_map = {} @@ -35,8 +34,8 @@ def _import_chat_chain(): Claude2ChatChain, Claude21ChatChain, ClaudeInstanceChatChain, - Iternlm2Chat7BChatChain, - Iternlm2Chat20BChatChain, + Internlm2Chat7BChatChain, + Internlm2Chat20BChatChain, Baichuan2Chat13B4BitsChatChain, Claude3HaikuChatChain, Claude3SonnetChatChain, @@ -44,12 +43,12 @@ def _import_chat_chain(): def _import_conversation_summary_chain(): from .conversation_summary_chain import ( - Iternlm2Chat7BConversationSummaryChain, + Internlm2Chat7BConversationSummaryChain, ClaudeInstanceConversationSummaryChain, Claude21ConversationSummaryChain, Claude3HaikuConversationSummaryChain, Claude3SonnetConversationSummaryChain, - Iternlm2Chat20BConversationSummaryChain + Internlm2Chat20BConversationSummaryChain ) def _import_intention_chain(): @@ -59,8 +58,8 @@ def _import_intention_chain(): ClaudeInstanceIntentRecognitionChain, Claude3HaikuIntentRecognitionChain, Claude3SonnetIntentRecognitionChain, - Iternlm2Chat7BIntentRecognitionChain, - Iternlm2Chat20BIntentRecognitionChain, + Internlm2Chat7BIntentRecognitionChain, + Internlm2Chat20BIntentRecognitionChain, ) @@ -78,8 +77,8 @@ def _import_rag_chain(): def _import_translate_chain(): from .translate_chain import ( - Iternlm2Chat7BTranslateChain, - Iternlm2Chat20BTranslateChain + Internlm2Chat7BTranslateChain, + Internlm2Chat20BTranslateChain ) def _import_mkt_conversation_summary_chains(): @@ -89,14 +88,14 @@ def _import_mkt_conversation_summary_chains(): Claude2MKTConversationSummaryChain, Claude3HaikuMKTConversationSummaryChain, Claude3SonnetMKTConversationSummaryChain, - Iternlm2Chat7BMKTConversationSummaryChain, - Iternlm2Chat20BMKTConversationSummaryChain + Internlm2Chat7BMKTConversationSummaryChain, + Internlm2Chat20BMKTConversationSummaryChain ) def _import_mkt_rag_chain(): from marketing_chains.mkt_rag_chain import ( - Iternlm2Chat7BKnowledgeQaChain, - Iternlm2Chat20BKnowledgeQaChain + Internlm2Chat7BKnowledgeQaChain, + Internlm2Chat20BKnowledgeQaChain ) def _import_stepback_chain(): @@ -106,8 +105,8 @@ def _import_stepback_chain(): Claude2StepBackChain, Claude3HaikuStepBackChain, Claude3SonnetStepBackChain, - Iternlm2Chat7BStepBackChain, - Iternlm2Chat20BStepBackChain + Internlm2Chat7BStepBackChain, + Internlm2Chat20BStepBackChain ) def _import_hyde_chain(): @@ -117,8 +116,8 @@ def _import_hyde_chain(): Claude3HaikuHydeChain, Claude3SonnetHydeChain, ClaudeInstanceHydeChain, - Iternlm2Chat20BHydeChain, - Iternlm2Chat7BHydeChain + Internlm2Chat20BHydeChain, + Internlm2Chat7BHydeChain ) def _import_query_rewrite_chain(): @@ -128,8 +127,8 @@ def _import_query_rewrite_chain(): ClaudeInstanceQueryRewriteChain, Claude3HaikuQueryRewriteChain, Claude3SonnetQueryRewriteChain, - Iternlm2Chat20BQueryRewriteChain, - Iternlm2Chat7BQueryRewriteChain + Internlm2Chat20BQueryRewriteChain, + Internlm2Chat7BQueryRewriteChain ) diff --git a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py index 1e7275fa..8b7dc100 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/conversation_summary_chain.py @@ -7,7 +7,7 @@ from ..chat_models import Model -from .chat_chain import Iternlm2Chat7BChatChain +from .chat_chain import Internlm2Chat7BChatChain from . import LLMChain from common_logic.common_utils.constant import ( MessageType, @@ -38,7 +38,7 @@ SYSTEM_MESSAGE_TYPE = MessageType.SYSTEM_MESSAGE_TYPE -class Iternlm2Chat20BConversationSummaryChain(Iternlm2Chat7BChatChain): +class Internlm2Chat20BConversationSummaryChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_20B default_model_kwargs = { "max_new_tokens": 300, @@ -73,7 +73,7 @@ def create_prompt(cls, x,system_prompt=None): prompt = prompt + "Standalone Question: " return prompt -class Iternlm2Chat7BConversationSummaryChain(Iternlm2Chat20BConversationSummaryChain): +class Internlm2Chat7BConversationSummaryChain(Internlm2Chat20BConversationSummaryChain): model_id = LLMModelType.INTERNLM2_CHAT_7B diff --git a/source/lambda/online/common_logic/langchain_integration/chains/hyde_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/hyde_chain.py index 8fda3f2c..2d34d5dc 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/hyde_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/hyde_chain.py @@ -16,7 +16,7 @@ from ..chains import LLMChain from ..chat_models import Model as LLM_Model -from .chat_chain import Iternlm2Chat7BChatChain +from .chat_chain import Internlm2Chat7BChatChain from . import LLMChain HYDE_TYPE = LLMTaskType.HYDE_TYPE @@ -79,7 +79,7 @@ class Claude35SonnetHydeChain(Claude2HydeChain): internlm2_meta_instruction = "You are a helpful AI Assistant." -class Iternlm2Chat7BHydeChain(Iternlm2Chat7BChatChain): +class Internlm2Chat7BHydeChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_7B intent_type = HYDE_TYPE @@ -98,6 +98,6 @@ def create_prompt(cls, x): return prompt -class Iternlm2Chat20BHydeChain(Iternlm2Chat7BHydeChain): +class Internlm2Chat20BHydeChain(Internlm2Chat7BHydeChain): model_id = LLMModelType.INTERNLM2_CHAT_20B intent_type = HYDE_TYPE diff --git a/source/lambda/online/common_logic/langchain_integration/chains/intention_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/intention_chain.py index 4c2d3d20..bc2602be 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/intention_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/intention_chain.py @@ -14,7 +14,7 @@ from common_logic.common_utils.constant import LLMTaskType,LLMModelType from ..chat_models import Model -from .chat_chain import Iternlm2Chat7BChatChain +from .chat_chain import Internlm2Chat7BChatChain from . import LLMChain abs_dir = os.path.dirname(__file__) @@ -48,7 +48,7 @@ def load_intention_file(intent_save_path=intent_save_path, seed=42): } -class Iternlm2Chat7BIntentRecognitionChain(Iternlm2Chat7BChatChain): +class Internlm2Chat7BIntentRecognitionChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_7B intent_type =LLMTaskType.INTENT_RECOGNITION_TYPE @@ -102,7 +102,7 @@ def create_chain(cls, model_kwargs=None, **kwargs): return chain -class Iternlm2Chat20BIntentRecognitionChain(Iternlm2Chat7BIntentRecognitionChain): +class Internlm2Chat20BIntentRecognitionChain(Internlm2Chat7BIntentRecognitionChain): model_id = LLMModelType.INTERNLM2_CHAT_20B diff --git a/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/__init__.py b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/__init__.py index 1307aab1..a78a05a3 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/__init__.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/__init__.py @@ -4,12 +4,12 @@ Claude2MKTConversationSummaryChain, Claude3HaikuMKTConversationSummaryChain, Claude3SonnetMKTConversationSummaryChain, - Iternlm2Chat7BMKTConversationSummaryChain, - Iternlm2Chat20BMKTConversationSummaryChain + Internlm2Chat7BMKTConversationSummaryChain, + Internlm2Chat20BMKTConversationSummaryChain ) from .mkt_rag_chain import ( - Iternlm2Chat7BKnowledgeQaChain, - Iternlm2Chat20BKnowledgeQaChain + Internlm2Chat7BKnowledgeQaChain, + Internlm2Chat20BKnowledgeQaChain ) diff --git a/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py index 4b04e90b..87cc8b58 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_conversation_summary.py @@ -4,7 +4,7 @@ RunnablePassthrough, ) -from ..chat_chain import Claude2ChatChain, Iternlm2Chat7BChatChain +from ..chat_chain import Claude2ChatChain, Internlm2Chat7BChatChain from common_logic.common_utils.constant import ( MessageType, @@ -20,7 +20,7 @@ CHIT_CHAT_SYSTEM_TEMPLATE = """You are a helpful AI Assistant""" -class Iternlm2Chat7BMKTConversationSummaryChain(Iternlm2Chat7BChatChain): +class Internlm2Chat7BMKTConversationSummaryChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_7B intent_type = MKT_CONVERSATION_SUMMARY_TYPE @@ -84,8 +84,8 @@ def create_chain(cls, model_kwargs=None, **kwargs): return chain -class Iternlm2Chat20BMKTConversationSummaryChain( - Iternlm2Chat7BMKTConversationSummaryChain +class Internlm2Chat20BMKTConversationSummaryChain( + Internlm2Chat7BMKTConversationSummaryChain ): model_id = LLMModelType.INTERNLM2_CHAT_20B diff --git a/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_rag_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_rag_chain.py index 9fc9ca7d..3ce5f263 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_rag_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/marketing_chains/mkt_rag_chain.py @@ -2,7 +2,7 @@ LLMTaskType, LLMModelType ) -from ..chat_chain import Iternlm2Chat7BChatChain +from ..chat_chain import Internlm2Chat7BChatChain from common_logic.common_utils.prompt_utils import register_prompt_templates,get_prompt_template INTERLM2_RAG_PROMPT_TEMPLATE = "你是一个Amazon AWS的客服助理小Q,帮助的用户回答使用AWS过程中的各种问题。\n面对用户的问题,你需要给出中文回答,注意不要在回答中重复输出内容。\n下面给出相关问题的背景知识, 需要注意的是如果你认为当前的问题不能在背景知识中找到答案, 你需要拒答。\n背景知识:\n{context}\n\n" @@ -14,7 +14,7 @@ prompt_name="system_prompt" ) -class Iternlm2Chat7BKnowledgeQaChain(Iternlm2Chat7BChatChain): +class Internlm2Chat7BKnowledgeQaChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_7B intent_type = LLMTaskType.MTK_RAG default_model_kwargs = {"temperature": 0.05, "max_new_tokens": 1000} @@ -51,5 +51,5 @@ def create_prompt(cls, x): return prompt -class Iternlm2Chat20BKnowledgeQaChain(Iternlm2Chat7BKnowledgeQaChain): +class Internlm2Chat20BKnowledgeQaChain(Internlm2Chat7BKnowledgeQaChain): model_id = LLMModelType.INTERNLM2_CHAT_20B \ No newline at end of file diff --git a/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py index 05c5efe0..480902b8 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/query_rewrite_chain.py @@ -13,7 +13,7 @@ ) from ..chains import LLMChain from ..chat_models import Model as LLM_Model -from .chat_chain import Iternlm2Chat7BChatChain +from .chat_chain import Internlm2Chat7BChatChain from . import LLMChain QUERY_REWRITE_TYPE = LLMTaskType.QUERY_REWRITE_TYPE @@ -116,7 +116,7 @@ class Claude35HaikuQueryRewriteChain(Claude2QueryRewriteChain): """ -class Iternlm2Chat7BQueryRewriteChain(Iternlm2Chat7BChatChain): +class Internlm2Chat7BQueryRewriteChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_7B intent_type = QUERY_REWRITE_TYPE @@ -146,6 +146,6 @@ def create_chain(cls, model_kwargs=None, **kwargs): return chain -class Iternlm2Chat20BQueryRewriteChain(Iternlm2Chat7BQueryRewriteChain): +class Internlm2Chat20BQueryRewriteChain(Internlm2Chat7BQueryRewriteChain): model_id = LLMModelType.INTERNLM2_CHAT_20B intent_type = QUERY_REWRITE_TYPE diff --git a/source/lambda/online/common_logic/langchain_integration/chains/stepback_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/stepback_chain.py index 01e21bb4..4fb49a41 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/stepback_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/stepback_chain.py @@ -8,13 +8,13 @@ LLMTaskType, LLMModelType ) -from .chat_chain import Iternlm2Chat7BChatChain +from .chat_chain import Internlm2Chat7BChatChain from . import LLMChain from ..chat_models import Model STEPBACK_PROMPTING_TYPE = LLMTaskType.STEPBACK_PROMPTING_TYPE -class Iternlm2Chat7BStepBackChain(Iternlm2Chat7BChatChain): +class Internlm2Chat7BStepBackChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_7B intent_type = STEPBACK_PROMPTING_TYPE @@ -61,7 +61,7 @@ def create_prompt(cls, x): return prompt -class Iternlm2Chat20BStepBackChain(Iternlm2Chat7BStepBackChain): +class Internlm2Chat20BStepBackChain(Internlm2Chat7BStepBackChain): model_id = LLMModelType.INTERNLM2_CHAT_20B intent_type = STEPBACK_PROMPTING_TYPE diff --git a/source/lambda/online/common_logic/langchain_integration/chains/translate_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/translate_chain.py index 07b92b3b..638128b7 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/translate_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/translate_chain.py @@ -5,12 +5,12 @@ LLMTaskType, LLMModelType ) -from .chat_chain import Iternlm2Chat7BChatChain +from .chat_chain import Internlm2Chat7BChatChain QUERY_TRANSLATE_TYPE = LLMTaskType.QUERY_TRANSLATE_TYPE -class Iternlm2Chat7BTranslateChain(Iternlm2Chat7BChatChain): +class Internlm2Chat7BTranslateChain(Internlm2Chat7BChatChain): intent_type = QUERY_TRANSLATE_TYPE default_model_kwargs = {"temperature": 0.1, "max_new_tokens": 200} @@ -36,5 +36,5 @@ def create_chain(cls, model_kwargs=None, **kwargs): return llm_chain -class Iternlm2Chat20BTranslateChain(Iternlm2Chat7BTranslateChain): +class Internlm2Chat20BTranslateChain(Internlm2Chat7BTranslateChain): model_id = LLMModelType.INTERNLM2_CHAT_20B diff --git a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py index a2a4d025..394bcbdc 100644 --- a/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py +++ b/source/lambda/online/lambda_main/main_utils/online_entries/common_entry.py @@ -10,7 +10,6 @@ LLMTaskType, SceneType, GUIDE_INTENTION_NOT_FOUND, - GUIDE_INTENTION_NOT_FOUND, Threshold, ) from common_logic.common_utils.lambda_invoke_utils import ( @@ -222,7 +221,7 @@ def intention_detection(state: ChatbotState): retriever_params["query"] = state[ retriever_params.get("retriever_config", {}).get("query_key", "query") ] - threshold = Threshold.INTENTION_ALL_KNOWLEDGAE_RETRIEVE + threshold = Threshold.INTENTION_ALL_KNOWLEDGE_RETRIEVAL output = retrieve_fn(retriever_params) info_to_log = [] diff --git a/source/portal/src/utils/const.ts b/source/portal/src/utils/const.ts index b30fd0da..24f80fd2 100644 --- a/source/portal/src/utils/const.ts +++ b/source/portal/src/utils/const.ts @@ -26,7 +26,9 @@ export const LLM_BOT_COMMON_MODEL_LIST = [ 'anthropic.claude-3-5-haiku-20241022-v1:0', 'meta.llama3-1-70b-instruct-v1:0', 'mistral.mistral-large-2407-v1:0', - 'cohere.command-r-plus-v1:0' + 'cohere.command-r-plus-v1:0', + 'anthropic.claude-3-sonnet-20240229-v1:0', + 'anthropic.claude-3-haiku-20240307-v1:0' // 'anthropic.claude-3-5-sonnet-20240620-v1:0', ]; From 6976c5a6b28b6f80e9d195ae5bee126940653666 Mon Sep 17 00:00:00 2001 From: zhouxss Date: Wed, 13 Nov 2024 08:36:04 +0000 Subject: [PATCH 29/29] modify glue job requirements --- .../knowledge-base/knowledge-base-stack.ts | 2 +- source/lambda/job/dep/requirements.txt | 3 ++- .../online/common_entry_agent_workflow.png | Bin 15859 -> 0 bytes .../lambda/online/common_entry_workflow.png | Bin 42508 -> 51758 bytes .../common_logic/common_utils/constant.py | 3 +-- .../chains/chat_chain.py | 4 ++-- source/lambda/online/lambda_main/main.py | 1 - 7 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 source/lambda/online/common_entry_agent_workflow.png diff --git a/source/infrastructure/lib/knowledge-base/knowledge-base-stack.ts b/source/infrastructure/lib/knowledge-base/knowledge-base-stack.ts index 52812678..29fef8a1 100644 --- a/source/infrastructure/lib/knowledge-base/knowledge-base-stack.ts +++ b/source/infrastructure/lib/knowledge-base/knowledge-base-stack.ts @@ -233,7 +233,7 @@ export class KnowledgeBaseStack extends NestedStack implements KnowledgeBaseStac "--PORTAL_BUCKET": this.uiPortalBucketName, "--CHATBOT_TABLE": props.sharedConstructOutputs.chatbotTable.tableName, "--additional-python-modules": - "langchain==0.1.11,beautifulsoup4==4.12.2,requests-aws4auth==1.2.3,boto3==1.28.84,openai==0.28.1,pyOpenSSL==23.3.0,tenacity==8.2.3,markdownify==0.11.6,mammoth==1.6.0,chardet==5.2.0,python-docx==1.1.0,nltk==3.8.1,pdfminer.six==20221105,smart-open==7.0.4,lxml==5.2.2,pandas==2.1.2,openpyxl==3.1.5,xlrd==2.0.1", + "langchain==0.1.11,beautifulsoup4==4.12.2,requests-aws4auth==1.2.3,boto3==1.28.84,openai==0.28.1,pyOpenSSL==23.3.0,tenacity==8.2.3,markdownify==0.11.6,mammoth==1.6.0,chardet==5.2.0,python-docx==1.1.0,nltk==3.8.1,pdfminer.six==20221105,smart-open==7.0.4,lxml==5.2.2,pandas==2.1.2,openpyxl==3.1.5,xlrd==2.0.1,langchain_community==0.3.5", // Add multiple extra python files "--extra-py-files": extraPythonFilesList }, diff --git a/source/lambda/job/dep/requirements.txt b/source/lambda/job/dep/requirements.txt index f6284f98..37eb0483 100644 --- a/source/lambda/job/dep/requirements.txt +++ b/source/lambda/job/dep/requirements.txt @@ -16,4 +16,5 @@ opensearch-py==2.6.0 lxml==5.2.2 pandas==2.1.2 openpyxl==3.1.5 -xlrd==2.0.1 \ No newline at end of file +xlrd==2.0.1 +langchain_community==0.3.5 \ No newline at end of file diff --git a/source/lambda/online/common_entry_agent_workflow.png b/source/lambda/online/common_entry_agent_workflow.png deleted file mode 100644 index 851ca8d2b95dfcd73de8e6c80037a7cdbad8b679..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15859 zcmd73bzIcXw>Z2=skD@ID$>%igb0!XBGM(SG%T^e(kUq2UD8N*ceAh{-QC?t^Q`yt z(eJ&#=X1aJ{$9^N&%AeM&YqbwC+5t1W+(0^?-v2MvXU~A03;*;015E`?w64!WyHnx z-o00plzIE+uZ$i5BA~nk04%|Fw(q50Q>$xeQll^aMe!?79|W=fmHsyYQSbTqui62C z5!U~v%)diF`eXqz@5aasUMI9v}sH{Y!tu zZ$z+42LSlb0088$f8h+`0f1^>0O0BLzi{;5005lN06^8?zi|JuiM771{(n@5f`}g& z836#tIRF5b1^|Hn0|0oe^&feN=)aMT8j(egkjo137y~Q-h5%}S3;+xO0oV`_C*UQ3 z1HgMf0}uxwKX~x#g9s>y59&iyR1_4{$LQ#24>2EOVq!eTz`(+Of{TTXhmC=OON5Ju zPe4dWh>1h|l!)Nz69PhlUzH#sBWO@iAEBZ?BEZ7HBKW@!_bmXthe#igxRH@)0T1wy zknxc2I{*|2b3Z^vLWJMi0?^PAA|fL_c!bDS!37|pAR(lGgpQ8>2o3$0w?06qfQE2P(nDHKWdj@Ai1gMaF7DT$vioTOCNe@iJY+n8DB$jUI_(2mYTEx4VQGxvFG&6{X@pRom(R)asn{2|Rd_$>^| zA5vlSjANB?95uHC)Ks}=iu4nLS-Gq{ImS=vaR7IX0lg{<7~AcN!g-)#Olzq@^<&8& zLpM{Ws0M6J92|toQylo?wA%F*7CQ<255J~`ef|3ItLMLo7=w{7Bfamw5dVC=P>AMr z)7$DH9(bK*HPN}%yr*+Bauho`a`y4v#t~GeT#JI9GyCnyCkyssLerFYZ{ z21=_X)PfXI*;Kffyj9vgqS$bB+w-zw$Q*MIRl$$wOC|g0cs^R2Mh}ZTuMoQI*E*>l z7JO1Y4GPxeQdjGf59}v#$-{YEOK`?e%>8ax(R8Y(s}VkxWAH<4N=1F4*KW5D&0JPaC3FS6Q1Eep}ML2<7a8( zQXWmomQf95_ql%P4$)$=r@8tD1P$ECWi-3OQSWz-=TOteFki+K3Ja9guko-_UX(8s z$5hPi>Hol5_6X>NEcWF5S(7vlJ$4#=RjA1N@#x6Zm2(D;7NdBZ(32LM(6+ojO@$Cv zU^lbo`5F2E+MNf-BMm5(u0LRE}>RoYs0jmhUz-NgUDJHP&1sOjrfzL!S*mO2Z;#P9mICqSbsZ>Uh$N^%&y) z9wt2PnSx|!3bokjHutJf6>+&=U$ChhNW%NZX4CgcN4TbhkM}n$`X$~nt+Zg1)I``} zC()nLFE7#G10E;-NYf&etYos5QcX6Q!F>7p$GBvdC+WBsVk7{#XVu-w=HA*Q{#;Exkk1- zZ~VZ4EG4y2@eg0c)x6LNl4iFXg6?#qZBd%)psJL^5*CHKO9jmJHJ zNh#~p0YhRaA||zcs7*OSoNF;nr=)a{`oS_OfWfH3>dHIX;F)OC$ z=ekF97?O$zpm?^{yZZl;CmF*6BUpQ_nSR$#nj6ivQO zI{mZ>rr8YSv~-yg0>t@&vCnG`@bJX({%sro_5fnwO`u0Vxyn7@YkcuRR~-$cxu^Q3 zwNvlHKjH4iM3ClxRm;De>HkB02CHTE)~ANG4i2h?xTwcMyJ#C9MK$O>0oyVFdu2Q> z{#UR!deh4y3M>8{`lQp>;Hl%tIE=b0T668C(@X7~GZsDaGibiP`)HqKRJWxP<UCcan?`3}=vEDKj{64*XaZFBn`*)6QIJ*im9 zX_1j27%w+V^qy(rH_CA%zftSadA58ML`s>_#T)iU^Wtxm;(zcI8SO0#G1)d`x7Lu( z|4R%6Y9*!U%z|M$o>vJ@@iuE8gOSkuH%bUHHedMpTKPFlAme*fW2+%;q3ho$hTpM~ltH6wR5dOHwiS3TK!4;al|4}D}pY1smzU)cQWwW0m!m~Byi zrI?)K1DhI`>8$P)%Xc~l!RL5=c&C&@H-$5UFl{Ng-dJy?QC>M_xSS#?%X{w9T@N$O zMQ2>G6W9DM4~-%$ zx6S>}R?U9@6E!NJ^0%Q=ep~l%vj0CCoPC66An)@RQIfA!3vwi8>WXJix^Zr=@D1B? zw+*dq3qC6~0*k(H6a>6)72#s&gD=K-9=Dvt%|9}4IrW56hVi|Bll%dW@2kl30nX+7 zj+Qn(J+F8*q`1GChGwICB>D)nyx;&9iXD);>Nw%a`7y{lyFcICFaON-&6fTwoE!(j zdk;|O4$Dh^(Iu62xMwu$odm??UFe}u9XhpyE6o5K?mUR)#L}6H%u?3j$Gk5wW=S7m zp>~5achS#_^P(>mL2RqJhnG?1yf)Z^^%PXXCh1N%kRFTFD3ISSowZQ6`?un`ndw30 zBg%FE4)aIW@3wY%l(HIb z4mZk12UhOP*6B`Hu0lRg4km5>#=4K=8b3+bmyEt(jn569+o%=H=` zM{P?1v)o)#(0h>$u|b*}m4ozOEO?Qv1Up*H$%_h?^)SB;$YC`!VZ1lPWDP8lnkpOe zOKdm9JmIb_0xwedRL^W;63$30eJQ^iH7M*g#gr;scbzXW+p{h~ihXk4W6#=eGqsHByi*Q{rXHu_8ddTpjR&M={3yQ6(q z_OLn}lswLolCAgDs#$Tot8My6nTgX1=-Eu=TmyV=0OB=yO17V0^mv^I{P{=2lS+{c zBSl3sd~BKv<=6Z(Dnh10U3fXVq+>RMVir7a& z8mXfAC9!2C-Or;4Xz{vbwYz^wJkgl3wz3DXJI#O#}wvFs0 zis3fkkXOLa#l9Oh04j0$=q`%;h^>)lK7Tl)+&;6vy!GjWr99-jO_FJ2=$Nzj06H;i zbuRiS_23kHYm=zc0&Qi0m7+i3+=x%ZZMrfJ!hMg`8j` zj;Le?=33*!&_{b@l>6%t2R87mC*|ioQ28VK$F%Fo_MNx#UgI&gaV+b~0y!^?cMPn? z_RWS?s5cV$4ZpO>ecq4Gx(5h&!Lg?@orauu0_aHW$}Tq0xNAFjzJX6@ujRrTDi=8mN41I5D zOO@!dH_VFhaEJ_wdr6rJ=-A*Muy*pwqmKG+yJOkiSVgWg86&HN)N}ELVeV>9GZ)hv zq2};IcX29{3Bckfx}p_hidJe1ExpY*$_rltrry9cifC-;-kHG_14eKL<Mj8OIAmTq1v_B6j-8qqV?PK?;1VedWD{r4KvD$WkkwE$6L)4!e=uR6H_o$B zEDuAI!6V(NoMM}uHqZ5wlRgE6Yf_=(c-qC1$?O(uafy?`HigdznUrzTnNin)w zF7QijoIQ3x`>Oc{XtZR~Q^+v=^J|tXj9PR_idNJ{Uh3d4@vl!O3i&^VrRO+S7M6|H z1bPjRo$0ut22}@Ehr5G2aC?kn7`nzMj4d$G;4Ya^1p~4lAz7+5B3w4;?-wg5vA_4r zCBgJ}H*5vSRY>1!y1pI?po9V00-j5X`&82mtZ@W~to3@AJ1{1_yu6H6rQJ}sPbDx; zef`mP+;I4kyb65#h;1@U>INpQZ^Fjnv!4{ z52=ezXfds;RX3f$uR~X{3}^VXA%&5R#r6@UGIQ5qQ|x1Kg_MO{))0g(ed0!an4E8X zg0(xmVB6x`jU)*8{<8ySndT>m#6`XgJ=lub3wzD1p23b5O1k^7)L=s_LEzbabG})x{<$6sJ}TL@pu5b`2E`>P_TH!C^ODQD zgmV`8`24Y=i6-?z8T{vV;3quARxh4?i-8b3b-}EP&M39~X5V#b*G9Uh=SxW%TGau$ z&8m$1)L&_@!+LzC4HsbM(ngCG^IwCD-N~vQi!(n^;;5MyvV;8Mlf}NL4gMH+pvy5Q zg5kY}al$oDI?Nk=lHsPIVduUxKFx13hqWape59@K0UE3J(mfM!v!{nuB^wkd*5{lW z5%r&N%$=@ZX-ZY z5GVIn4xT7djjd8$_uE#h#wwd?PRBge{@GNx-BDNB)8s0#*FUlep(t55-E=_DNEqhH zHZU;BFLP&C4RrzOa!oNMeicNg=x{~0ae7oZx?9j`TM2vebyqco0bPm(OzpWSm5Dcc zs#MU7%(!Opz5%ZCy>zOFG0F8ZfsZa^Y*L`F{kd?s{u&#UIbpoEKKZLEYXLa3gTHKm zWJ;3Tt}-w@5m=lpKu*3eiP3D@VAMEqBg^>{Sbc|&Jw6O~Ir>br0XW^UakkBV zGOj2)n6uNAMRsA1W$4#-8x-EqGg0+wgPZ?^J4L@{vbe+=M}D|t3uc$oJ5=u7Bg&x1 zfK|BHCzLc-pY7tcu_vBHRkMxV4i13K6W=DmU2>L_mprEWC+gf8W9wVS%e!zH4qXj+ zsK=s@or0AUzFG7IbZyvrRWdcXN7xFK)WXR#Dwa+7Xk(v78D|lig{K?So{RRpYDIZ6+{9+he135l3Hh6bT@BagwCoBtY@ixk zp2~>Kj&h4+TJ~Zo;<>-#^A20svXi*8*ZK(w^q<&0U@E6LEM|61wp+8Pbdr?fzIkTh z51SZKtQraS8x)94EK_I^AeWb)|@kyCr$ z92J8|=N=9V4i?+CW`W3I^X|I&oG8IxjW%-hwsHR9cxe~zP6J8znthgzSoomvvC?P* zr;7D>5znak6eg?rhl!M!!TQ=5@53z4 zmpU@x`>?cXwL{y2_6E7Bf)Kr|wKYurV?ta2@3bTe`x%w=Hwx8-g zk-i=sq2|pU7UM#X4LaEBSxfLw6x?6h8P#(wVDaKV%BwNek!FPjZ6zot2GP+(&tR-Z&W4`E+ zFe0F_-=)wltHXTsRzVGG=!AwOJbG{&eYv-2>+o$IDj6CvdP$?N?}9)fLo|GS(yA{0 zU%JP9UeQfhc75M0E63~FA&?ak23ifWs@gZ^L?2nl*eiU4H_7F6VVZOqa@;oG z#*834kXh^BFr71z0UN(mFcf+||IHWxk_fc;RA_ zVPVAE-VB0mwul&4c3Wd#`(Af^v?s>2KD8SU8hP3f@*v3NjWJ_t48{}T0rv1glYHvg z#sT^hd-_vKLxJDDLE(4z=$l(KpALbg6F{GGJCsedBZ2oLYIeBw_;W3J)a8*J%7C_v{Ot zJiNKe2E2;IT!poUY~=OBqVh5Yd8^^fQk?L+EkzNCmB^}1zl1I!xM$^)O`p!52@5M; zs3>3Z%qftu`k!7isAszayZVpITVH^ZQJxSW7m;4$jALnHuC&zlqcxIx5%O+`#W*?C)V!8t1H4#)`#S88 z3+4RgYq)>Nv%BQSF{amf>^xFplIUyzyo~t}%er8{Pv$kgHM3!UEuF>6;BKw6c0w_K zfS8bHEJ^adZK7fb@A(8#J*t^eZ*uinajYc#!ZA$Zqb?A-I*m4)Vc<;o{2v=|D&*po zT`IuZS()og^ zs7*eWl^QWiZ_(}Paj^(mPo&5P3M7dEkgMjP+u1sQk3Gz%eI!mjmTv5^(Hrn-&KIw4 zvyO4@0pDZm7bsr_yKpRE*3y6CXp*5Emu%7A;NJyM+R83iPY|=%6&vnKZf5nusK~8O z%FwXYe_jbRO97gCp0I!nVFB|#sK-RId zaqO*n{04n^(+n7$D*-PxN{yInN?uCYCy66AQExqcqd8K4UI-PxTG;>LWf`uk*T(k4 zZ4J1oHT1J1<-<)TQZ2TI+YJr&)Q=F^;x+_n_p4nrbV~54Jt|TK+7DKk5 z`?UjoOVd6;2DRxDs8At8Zg67Q(8#a@*)D4Zio`3ucD*RJr25u_DAlSikcE6;OpK=K z_g=9pHHrU}iHd{LH|eU7KS5Vr4%s{zdz7CQ;?=iS&dM*5vWAEM~*Z ziG#kUXjZgb#6Y>{rM&$2%JIgOfyhBav(BJ=hcr5Tmf%&EJ#}6SdQYgei526s&Vgs+ zJv1%2V(7Uv=%W&^BqR_T{JZE)*0^&wPWv8fv(WKWY{ z>yIz!k}Osz@xAJ>=QI0b6&583N)J=}bjuFbmx7#n83r7H0L3)51qKByqwRH0a+PI8 z)}m+IPg@9Yj`SH*3UVHb7@*B=Y;DyZCtVz%R)F=^kc-W2%5{fgQkZJ!B)0O!vzRvS z0gcg3AnrtT?kltotMZ52D3tG~{m^ZDxozq#&mo@7NArYkMpO-tQyq~TVVaqzh(Kub@->_ z1EVHkMV0jlH1yIswGi?ji^FZs5U@|ibz5;k`%f?6U&*_0n-Nsrut33S4K^n(UG^ZQ z08vb0SLzl6){QkXTwO)JezLpF#Ng2v=1eu@DM_P_Uo98-VeyHhnlN4#C+z}IdtXD5 zGHkhJ!WriS@q(kuS)BVJ!(R!TLUm{VJo^_{gMOXru;@w5vuT3el@}8UW`t8HY!=B; zA;A$3`({@LmK76=Uu7jk2A|r^eSKMJ6F;Y%^mJUWqMwyW)=XkOMZ;)r&s^@S4W=n1 zq}wRW`=+NHKQ?!}x)WHbt+^rokyG-?1Uh$2a@SG|-YM>5|4p)lt|||8sr=%l!leNT znVj8sLJ=3tagRcb>;a{70=51&Y(Q?bT=WM#3W^%mdeX+_yjZ)1Hx{Za{d6GA2)tUM zLZl3jZvX;uKfM>G<<3AAGgf_{#vG0~xmcfLd?{SdH<6s_XT@}`+u7$`Y^;5D4k-V)XU28e8>jg*IKF1xl&0OLUM(bM zWuHpb&}Q=J%fMoCT=BCc;f7A$c5Im-D*+DNpSVs!B@=!T^Yzr53WxbSFK-wBPCh6C zcodYito5We%2gZqm_4Y-&n+A*Lo1vXAg~rv(T_-uz1^60S4Hdi_&I0T>6T={-SfT6 z-+5*2Stx2VTxQQ>iB7P=lJ&9f_0-LOyTotLaGK|PHzJtLqM)W=Cdf@+Pb6G>X;J|S zI>w#s78Mm`PFkFISI`-l$-7oR(o-?0@#toW*3L16%CF6+5wiPqxRQ#r<<~l}PThSd z1~%ZYxu=!bAN*JUa2uVKcE=B_u#OaE)QnOG)(X&AzM#@P4C<`4=u#Le4t1+_Le`nK z7baeja{XqV(9Mv&!-bX7sWuut2g37UPzo1=#ITL^o8mxxd<4F(n_y!-%W5|ePxHMR zpvJ&^;@Tp_ZVz|1T6^X_Dp)KJGFdzs%t)<&_SOijIn*sV6tq1IKXmTC z+HU=K?-3P^X*=nrrpDmV{8_?vZ)pO2F59 zCn<_)szm%;1=;-u;JG2pzHJMm!axoyRmEuEVcqN*LwG6<@R>R5C%Co7TPH4^& z*Ub)&#MJ>^})WqL0?wdWwT@Pl=;rTKI8Bh`T(<1=KDs!7;vj9%5y2nMv`@ zge*kz*y%0GJ|7CBuu6L}s5@zNoQDWtqN&0?FNdcSF&{8nK2_7jF?i!oSN1UouV36> zl$)hrBxb^SLFsT`_)RwL@Z>EeeG0raETE-X)JL$M3>owc9@+;PG$C~WSZ?Vk&2oE) z-GDE0{x-Vz*Gu45!+F3GFR{ECi#f;7!$NN4<&vq4N-`0PUDoX%i|*#0)|2}}FrIXd zx&vPl*K@R#5O7x8CrFiTH00eHO%Y8%e`Cg!kkxVDr1oAQvX3VfkKM-q*`|~e4J6|l zebW&dcH%sg-vs^RHAxc&RPibw?sl`-GnekK`!Dl47-n*OKH+aT7}v?$<^gbX7MQuSj;H2&VT{WEp^vyHEm`XU6&h-<A#prn24rz2a-vy@mMdgsxuAm=M9SI8%67!qZeb#&MP>1B;LFN_y5y1Uf7PlE(L) z!vw$OpAy6Lq|EklwU^N(PweK%rR|(xwGSjF>se@uy(Rbh_4;3#AjjXpks7+A)u)Z$ zgY09Hx{OQ46QTGKg%ou$q!ZjS_|xPQwlxm~@qTzp#d)e7ajkI`@;DSP@8>mtKV7|| zbxqFQ?aR4lXzMz+w6pFx6nD`OkV9;rOGW7(&#Tr)dB}S7!i$7#E?+L?$JH|jj1`BBFwRep_M&A>M;CVAkn7A&i1GzmiP!9sVNm8+iGV?}vj z?6_<@}e_q zF`|2JNEko8g!+%yPJS{0Q6g8fpw8BV0l*OdxZ=<539jL><$ahs<{4|tBCL~pN^I=H zZ@p2VC5OGe%}FIGg(BcW>eCa%krwm|9`pw<3>i)OBTFlec~RrjBItnPKH{&efCKIN z*9x~Xla`=#BjzQl>{R--J=+GCK~)zis{M;Q#(xQ}+Y=vfA+?p$(>J{alJiiN!=GB= z(&!(aH+mO@ch#*Y7;<e$;;y3Ll@)y)e=L3cv!-#MA>SW}_2s?m z>UYgC(3vVbW3KI_U{$?#He;BKEjE0m%%Zi6u3w6|8WRdbRWx!wbdRTensjslOzAUw|KB7vCr70!xuN$bt$5WbvY*Ne?W=bRl0#X7pB&*_Ckqg|x= z82GW@le;^egW8;j4`7EF($u?}Vw06E-Wbv-xMBd`xV%|i>8)Tn)jma5@}!~Bnn5YE zy?D%TPPMhQAW>-ubRj%VQ$~ja4{@kXl%F43D4%d9A)!(7|9-pS;SxG^=vtB64P2j# zV7Qz^4)kI#oW}gpZ(g+w#&xQd%Zg0Ug(PWu3!m?yGg^e-O>hj%xF^kKUdD_Y@sy-o z#O>f??Wz!HXMdMe1Db9f4^;;V!n&;ZB2(qoZH)=P2t3ky_wak^Qb=$S(jFUB66jFj zsfonk?vrCn?yJ75<`_X7JiW+!`^{$*u?^tt&C9IkJ?vP>yIb1*fp9=|<(QbyCFNQ2 zp1Jlt;6-vZrpcs*pi{$69|!lqlPjk0<8AtbH;k<|;!L+>c^RIehpJC}1hCGDH+{ZDjtH~Befw!ETN zkFzz!yx?pm9G-1I4meKouish&B~1EPY$vs;(=7eO@iojqm=xJdw^7g5`G3ydbcG6D zX4ep>RYVpifgkW**h{^O(@4>x9*3&E8e*O$AvrCrM;^(M==6gX(0PQOezCy;y4CG` ziAw8HCuBF>9j!9wggQ8oPoMRXpZk=5Wpn5_sM@d+P-(9ivy^Jbi6mpF56|s}tCl*5 z((t@TE*-A723b#CmjISVA1q=IP%n^LdDSt?teY5?)^8qr!L$jtvqor_AXp`K&4zlly1E^{!2?kc(s!+J^85Zo^iaL z$ty5&cM*ynFAro*<1bX^$Stc2+cfSA$qA|qgaJ#kc$Y##PEih+jlzQ0gjOLIPOjVc zfOqJJWa7Q?8PSoCxAuo|9iF>X(T64w`L%6yZDG(ukhRKFFzT$Db`I>1q^5Of?g3v4 zJ$9nFvzgM3p@ZJxMUH8mt|4W8i)_8J>p(sTaVRwCc(homBr-ZUCeW-gVjlphot5*s zA_}gWHJ6vqJMGFGbrmJi^5H2=Rj@#<*$7_`H3JHH`mXMGwM@=&y_$)wAhRg6h<$LX z3w?Ld8qp@jW~NLKV^k<~_{K=p5_U;r;T1~lRqAL%hMlJcRRD1c6o7T%{Oi3SEoLSS z>*KU1p7d+?026NHYl81Yc?Yo^Hhu3l7@IZLD-8E!CronFLf|u`UO$FHjNXv@@s6!R zKSxaPCG|+IZ5T76Wx+JF;xDCEUrZ(>UsNQM;mT{i?lo}CvVlX$d8xhmQ0!5d<&>?N zU+-Wx-lTw#C}srE9`03+KeyoJwL)w@%R7QnhDvY{d!+3KycmX57!?gpUymDyyBENG zbrjqYJtsW!Rl94b9daYXhbyCJ+sXtMJ6*<9$#-T4!UYz~_M&LdQ(4Ve+>*er$HfYh z*Q&@U(0T{y#BOpJ=i1$_l5e*0+O_wyayka6G!27a!&wXR=OA>W0`~x%gbvH%nNB`~ zr(_y40EggJi`%a#%!$Y?IvCkyH8;_iG?x9Yw&L&Qh%FST{dZaHtcCfU*JgJUbRIBNCx^H*!L!XeU5bTs>Dyu0mNRgJn)<|a_JVz5%WIo~Ds#-HLg);{ZtL%&U=P!n9 z*NVJ$I1lv@IuUA;x|GsnwF1jkt_7$L9^O43m6*ub zUHR!0Ka*En&!bl!es>tUm^_ttn-u!Zn-k924Lh)43J-|`wP)V0^ zC+Q1*-umvxL!1a95!Eiz2`kI+aDyi(o_5;{FD%@c?%p*$HRP#MP5I_f(>~lj`Gv;} zQ;SiEUophgN}uaI{93p@(Ps6s$y`}Cv^;cf-58v4`{awcXr-C zYUf`pHPyX;@;@tAVsNGbbgaj{HKv9)A8S;cDtzm0zzj~Pac;zjf3xm1D@>ORH`llp z*BRLqF()CgWDGtX!V|f?=x<{h-XD8v(B84k^s)3vDdSY;dpeeuE=ljF)09;QomeApUfWQMx&oT z@$+v+kNzg7o}~SQ!w@dQHlfRoBk`!yR&#I{8O!q|g6u%!6 ztky9J&X|emR)DeWhGClf@3hY=025E3Cl*EaZ){>;Z5tm+VHMdKCh573clP>Mi*tW6 zbWzcJ5oJkl35h#ztoMN}ihteo5szBF%P5X%->MU&spiS5M@&M!l~+le*1RplH4P+?Ig8TdM(fhh&y|G6cI*d+gLgmb$+FcK#!P5+!OgI>5cf%pxzc?x-(v=Dr1wtM|l-5KaP z<;Icx!%2=r5L&omOA=HCcKFggGu6;-HOU29tsIzv(S2JOa1hIy(6$?cRy@O~!Q@si zIMXp!Q&`I#bju3%_$-NO5nBr7dWQ3HW6jiDSD_;~D2JyIVpmg{_QHsRb49*EQgY!r zkpENO7GhIUI_qo8{3y2|XD=VP^WfM$*+|bT{yM=*LrnEfki2rvkSM>SPMPi-MR>uw zln5LJ{LxydVdlZZh5k$u3e-G%k>VfIp)A&CmIf~j^A?I_opjr-X`gpa5ldot;yJyU zt;hdCKpKpH?AyC7X(D&2Lu&0v0jl6;3re(*?n5J%C82-KQBF?h!R&*0K^m32`MC^aJWjNf)v`x$9{J*2|COk9;`8>v{`S>ISgjQRD=Ss(Q;n3K9UDX|t;D?{xY$-)ZF zsn2`v22`a~9g}E<5+mWv((728IJ+Fz+E^2%TaT}pqTzyeW2_SW9mLNgY{2Tk_O^bu zk3p3orVvj@h)?aSsrp#J+1LUL3tIx*IK#V$X`dO@NH+FU;=A!ecd0Ii*w(FPSQPg@ z+PI=}hfTzZ6-XzGr3=NWHxjDt9flw2$}8g&d%Km>CUH4K+v7E#6P7sb zOe06vBc1|Ni^7GtidgOtaQ$vwM9uWWmQ4YvJ89uR{q@hX#y_1tcal>jnS;}F^z2p5 z!BL7oh=tCbxoJ|>Kl&@dpuq+GbkkG)vfoy8!JNlkRGXHUJu+Gr-Wf8gW)lnChECd4 z;+cfC>9Hh*osk!y7E(BXR27Lu-jQN>v2+5pHx%${#b4yQKJN6Q35`s>%w6cKHd#on zx;|Y1RT8&Z9e=g{sZ`B}T|7!?Gli5?u7;yW@wJ(z@|8e(MG;bjyV7-XcZBP_wAk3~ zg4$TxbxHDHNf{oRD0J-1#C@0D8gbXI1Y`TZ+>!d-v;XtGslRYc2VWucv8qeCYQ3}F z6K-V64a%`$jqnJxYDnn`di}L(PCdJeRN~L(!`xb(MD;hYlK3DO@;$m?B5t#DC`b39 zmUN!(ZsQ)M)%-o6EqTgiT4vUX4(I*+y-)cuYB43$3VYrAnBG2pUiy6Y7-)26GwJd1 z3ZE+jGAKTN#a^9r-iY_1@sD`GvY!=B%`S*5B1U##r%mUDH|M8!eN=t#vZu^eY^q?$8a$7BD7BLs0m=)680Ph;|b z;bv-@J12-mhP&O`dw^=$4f}lE%{>5d=)RuKW2~SVBX$d?SZeLg@JJ*UQS00T+8hxT zC+!$!-%?UFG~Vc>O43@bK^%^W%u63byX3^lBPzIgt@a3P10Z=_WXVy;fh}Lh8{J8+ zM#{!JJ_`f~FXiBZ4Jvh$A}Mclq!D)IH!LQ2pJ-FZEFm~L8_H@CjM~sJ@1B*X)PV`Q z5G_3WEF_w1V>{TsQ_GH)r@eS>JqgnISP=<-VG(*9vNnVDQ*$@n<=GBwL{znC!H8UF z4k>B;)S9$d-t-zz-EH=Im$M+8C!6=F0cUek>;pc;-&Mppmw9^{?eAH3O`dHwYTkTy z_5szF3qCZgeWkNpgVqW3=jGsKxxT<^dhjEsAOcYZQnY zOlX|RE7LSCTi-(yd0cirvscY$g_&qsIumP^1YO7;(J{>|t4-SHdls0I1TNHcJ_uQK z!hRyi@Ib&lWbn>hW!bOjEQsO!ZOO2m*#Vqqh@mG1mSlMDS8zMAgWUjJmZwSPV(@|u6A87!%9Qe^n0u1gN zpto>^{b7*u!t6l3&Er#FcX(r0O3B7xhF$MmKIOwTO0q2$UD|o3l$f^B&Ka6RugZ`sj=-t?S z>F2xN$WAPsou`Vs@sh6*a~YUmzDVE*LzR#|eyDGq7U|aRk~M#_XpD7OXLL7+xvc+* zuy3|1J_-2@{)rcQzZjWsp)n2*kaB|Z#WtTOl+~VGxBHAD51cdqx9GjUjg>>^5@he& zsFe-vP+Z{T_+#!Sn4G-C?=^ z{olR!f4l$t-tNBL_xCx|r_Sl>uIf5HRoz{69^em)032y?DRBS-0sw&UcmN)j5#Ul{ zVmhA`<;A5we)zqh3-Fkb-v9uXR`zy^67OE9s;R#~Tl~GoZ)JKQu+4ARf07>CJ)iik zbpT+L`JdqYM~|Nx7=l5M2#1e98oNj0kIX)Qgr6J#4%7bz>;Dc5{sueR+t@$ieEJQx z`>ZJT2pc@Y^u~XI_5T8cZ0vr+4|&89w6t*ejn;4JH;yq3t-dHdUY|Vv$N*q~B0vK0 z?zjFw-ajVmbO3<&3;;m-{^v9OH~^sf8vsB&{pT~9Q~&_`D*#Y6_~)}fWn!aer}ww! zkRNkIBO?IdI0pd0R09C;hX8D^)FCL4?9{I9G2U~6=f5?Afb>`g8RaPo%G2kV=$Oy{x6?xl0QU)^1~L&6 z0u=xe7Xb+u;h`Ns{wQulB!tKGZ@mSepdzC^K|(}$`dF-u13*AVcw`;r87eyVV+T-v zLq|qI#YMw=iBG`xoRCWN#}guU1*Okll*31<^=yYn#yGj(+1WdIM?{sC({OO{h$$L? zeIheTD_WO`^+B|0?JB;9uilHNXFj$M{EhP8(jVjz5Rp-kP|+R>xp5!=90({VXh?{t zNWb%kfQyKPr!b8F{3TmBvZ$iIZA2P}Ua5C03IP@SyCv#hhh-1b0E|auL|i0XfC%6| zH=PQR>IKz*UB=|<{H*5OU%edDCBnhF>n1Klz+dpT2XVCt27W>`HpGRUbB%%qMa10LU^; zU>qo*8+|U6o025B{mY$AY&!00@*p0rlTPo-A;Rul`0bj?zntTQ?pDO}+v4p>{?pSA zY%}8xdH#}=kDt!7JsfV`dsq~n<*@;YLewoVJqJ0CMY!rU$?)K%`Sl)a1>&ej45BP4xouOVgh+QA#2ah7( zntrUfESRqub#hDc(4LOEZFG7S7MIq)4w@(9G4}>SQ+;=INaErO(ibZiaGa(>hxpU| z5{=M>a!;ajr6M?O2!Mj$$3fGukN3vqIUHUH4Fhy7tYZ6LmvjNiw59-Tg$KZ7-q`~n zpfZO1r6+Oe=Q1+Bmezu@FR96b%9W!o(T%2WF63so2C-3&-0;p3_gdg3`%ioZHA3U2 zgofi}QP!j7+?Xx27(X^6+hBE{2!GnjVo9WEM@0op5S_5gPD?PbA0C~MQ`BZ=x*qY3rj+2xW(j7KGq|-){z`C#+6@H&ol{2 z>Y})s=^yMta(3gJ8SKK^p`M-Vuz(TsYWR(jG@vwa?QrSGopg1@sZTl4G}Ty?@rnzI zJncEa`sQ({A!c~mBoM%LLg4mw?Mz#V`g0%&ovFGp)`0x`q-vzaq<}N8yjiTV$U~WL zm$J0&n@27rHPX8k)L_PrdbwIzzM28YBpk^4xVQsX_$mey}x+21;%dU8q(aLDoj~*N4mx3kbR>^{M^0|MnT1k{R zzFY!`ekFBj(Q-)LaxOt zq{XcT%3;Ois|4IJfJtoT>dA;9a@QwezSDBE2f%7eI1lYyQlT3Rs1H)@^kYj!t<2Cx zYbP4HnyC&7C!=~Aj!V@4uQYo8ffw}~QjJFw{L^nEp+CfEWxP6S;np)|{QSD=*(7}K z?g4N{)H>52V*Jzk=Jf;M_^we9a+V6G5AX^Ic=9iuRqk_Q-@V=D=o@jH?6?+ipqz8Y zdel0?R8@GBvHi_wC_k^0ZYMiVXYN>E?ueEQ|7#iFf13zAhdA<}B2id=oa|XX?|6=j zxxfh(;0i^{UQNM!i)1GW2+!v>8taH%r%_N$Ngf|`;bPto4m7!* zsJg+>_LSVGx92Q3$n{L>(=#2{ORk{U)7du_2mN{{WR#V%7p8SH1N^NY<_M^px%~TV zJjQwEay-njV;Ar8tm|bb=4~gQv25m&Ulj<0e9v>6_5}WD)~f0RTB6{niIOceALlk% z_uB1)6@eTVg*5*(xIVKF^_)XYzb)~ix%*=6mTked1cPou1@s38l|OBoHm*uMHa`{< zy-4#r$R8I#_%ui#L=vzT(Iy$@;aFmqbk9SnBfewJyMsdhIw*W0zHOaX#4KTd^bbz6 z|5iw3{~3BaLCMy7^LYW1^arOr8qP3P`v03=Ta57+A?k3m5*Trxv8mKj8`#WT&<^i4 zw?9QX`exE@Xuf_lG?%x*Pa+Bz$|^EN_UCFjL&g!MNPl*vtvOR4G{{L2z! z08W8zW9*aXNSB|38T1baZ&5riryl@!9|N6FnO$J}jlE`6tZe_FYySI;D2n_t(e51e zt?dI~Sxx01?zFTLwqcp~w0iNCDs>W{*D3~l(g-Q3c4@9cCPXklG3o0@Ewwpf}(--6gYb1s;oI4Xn-0P?85%9lh3&yNN%n0|oMFoNi7=N*bI_+Ns0l zYxQSGC5^#JeMYSF!hL?;gQM06%Y`n+UeiEHqtxCNn_?2)*HWUOKa!1=^}tt214sQYDI&wx;8th~HtY0>B$Ht)ubRfxdr88}?#NXrWr&~!Q%P_c zy`x}dLog4#Ykq>bdd9Zv=qT6I{$Bq+eTh88OJ(MZYOi)NxM$hg^!c$$;8E#t?P7IYn+-PruTZ^z zO76KW#^T|1hQN=sgc0Z933EesMU9A%@i>y8K6z)>u-LBk;|jlh)E1mV(w*7yZwX)P zs%1=i!a)Zc`o+(q*<%Xe*cOWZl|>N}l7~ZlEl8>BhroQRxJ70E5%R-bBmD9NR@ISO z{f^pQSsG*zMp9=VmA4=GkQ~c$g&C_ymPqD5|C%?~M6c!`n%z5LQpXJmHxF9=dNI z1t+fOM45KT@5tp_n6sEb9zW9%=c2~k4c04nRWkx7?<*WSI%aH7z3G}r)JglX$n(rD zhI2i8j089(Q*-MWZAR8sP!$jj>AXsy3H3CPbS9fEGM8t-*eVUlYSZTY1ltaBYM2DK zx}r023BW$f$Hz1I3w*SLF;!;R%QEe$Ls$UOH3&UbP3T^A_;O3fmw7d~+#;+<=32fl z&0Kv<5}K)b;tpOGuG<1E5QMmOh^@i*zAbuS;&>-1L4Q^xMor7$b7QygHxZp^-PDN_ z6*ar0Z-J*O7@=Ix)nk-w?$6O=)F&Ly2$6)eWo2@X>mKbyZe4xqY^&n^s+|a=XA;bl zV2lZWg6?di^J#@k0@LQ=h=_eM7neyW+qq*hfyS;eLV``06*Rq$Ke>3Bf=|2vPQi}z z@*t^U&``M0`0iHNHBFeLSHM;!2CCeX!l2D?JVpKn9*LL7O*_34q<9vhI{QUDx=o$= z+-jf2b~Zwag(gDk@tn=aWusF2tI_#K6BO*2@@}%gV`sK1o$3VVzt8OagZ2BXg|+!q zmyg0?_m!%m(CPpev6agr~`8uMbOsyBUkOtm4f@&S^#Qh4@Un28n1^rBuT#-e&<-_ zxosssoqKovCuDIuykDOCewtqHeDF@T`{s*C^PiAxO1#j71AW4Wr}I;fBd=D(EazhD zFJSqJMBTt|{UyP42IF!n7;rX;Q5cnz^XgcQ^Tu(GSWk7r_;txPa7zFOO>OO7SpqX6 zXFPO!pYQLuJxngt#?meSVFV$>R1$=}6JjuG@Xye(TZ6QV07I-@t8PQ-ju^$`e>pe$ z2gSusB_vB_C&a{5L`%dpwpc;P1_Uq%+VpCt+={cA=49hLj6gryH^O`H0sFNMz+v3Mq4;*_WHPOpHxIzOc{nW?vTXkHq&lf#zfO^@AIhv)rd`;KyKu8 z0=0pVm0`@3_moVa-xu5&x)Y&ZlsGz)|9(X(LN;_%?qxGe9`K3lG}4yaydK-d%~H=! z1Co0%Bc|IW^lJC650o&1$UcO1Exy!GorX-_iaxGV#;>rqx4XDs zj~T%^BZnxKokI+|ij~!dl!EYE!RzD^TIr z`W?WtwnI30D$k3#-Jd8^MTq^X|*TI|GDQ@z`h-@wpTdjK37r2O#g zH?wr^NMdf|`D~Tm)hl%#f6)#%9W?AHm`Q4X4mGTdCrL+GuKb2 z(e%?MI_!wXq`+|wT#`-xMD}U-SYNRuY7%_S<(84BkQvO*Ro17MJL!z37UF4i^|BKsj*jaRk~`!Z_K@23i9yIa;-;(iU}WW7j>*Nk5I34_ zjrIO-(qM(Hn~INYsOch2RP?iOne_R^^%<}9!*oajoP%zw)1u3_yGMxr4l))$Cy9zZb?K4Ga=M9Cz^dX{U24fQk z(;#{=0{Rz?Q)($R*h{GxbA#Rm0!g#Z2%ao6d^5auCmx_Ta5FoFO1nXKLsODIl8?c} zH@J{?5+pKGB+|F$&Z>Z^R0nlN8P{|+{)1q9`~Bo<%m(~JZo4XG)f^$`_;XowVs%3d zRH`WoXJ1LdM)IM2;pmlU{|V==Qew9qrun2|2cxEjVfG>2xz48)E!|$^jv@6qI>^9I zbOm)&HbFt8(w8njEjeyTtrs2uQy)|Rsk`cy+O~B2`E!0fc(6qbrg0ZzKSU!YPjZwA zf)RRp<>EH-W(v<^bLB3o3KbQP;Q z^}p(0&|iE$(%QGQ)IU_JtkRDV%_GjF{Jn!yW7iP`Q~oX{FRzM@QX6-~^Ub0*@cUH7 z^yQ5Lnn2~euw~X%z4*RS15z_%wJtJW>rcA>mL)wh#PgPfZjJIpbGTE3WVBuIgZZcJ zEl!!zGC>XrZUwzWw0r`vTHNR8Gc=hc3hT=d-Fk0ZgY!Gr)yZTSj=U#s;S}FncTn| zJ^L>h9|ifxh^x)!RMNNfik6twM($N)UMg-?Pj_8Re(Q^U(;(SKsjV$;-PBT)JE@8J z%a+Pa$qeItRZb?w^vPr+ml`$pOe|U(H5M$Y-UqVzd?03N&XUMC->-F9zjK8Vk*fG9 zLWZ5@%Gmznd{1sQLP~TnHuR}FGQZe(RIeD=mP)Q>q`a#u-rviH7 z;j|e4JeJLQGq`ZgsQ!;@P0?Wtx~iWpE|!COu=ttZUa!p znBd!Fv>LE9@x1Xa&lxslPq`n~Ch!S2WfNr*8vPNzs2Te7K>&#hFR*%Rc+j+9lmYhC zq`OU7qz(2I(jVRrq|mxmMT53V*osMS%WwX`{|Wu5B$eHSe9h z;&J53oHu1M|Fx$)m(PS{tLqBEHbE}APUJzh!fLm|{*=Mlc!NKd9MxuJ4xa)lxrx|AXkien_ECDf`g z!88RgkGx-Ow+&2OYZpJqr!Ot7dQ}b07m6>+(pQbWGUr=bC&GEE2X-M3lZALY>I(!M=!TK_P|J7w3b<- zJK+(Hg&%@_oMu%o7c9N|Y>tqvhc4}J&D$1~B-~BfZwx$>JjU}eNslHAF&q{{(?Wt? z`Bi#30npwOsD*u(sp&8OK|rEW}X zO6r^wQV%ZA5+J7aPb%4Jb`;@(Q~D zA)nDdh2~0SQDm1~n%25HK)-oW!)X>GzPig(Py7I=E0>@)3@rxv!WYRVIrpEO;e4_8 zS`-EP4*8T+06RWLr*^#OV;D=OYsI-_drFPcZ{0qxEBUzl1h>}_=uIGgH6C<07&b%C zD^J^jON37K&thnvZD5yXU}oj7s0CO(d#0qCuN}$kETe(+%2nUV6FL~ecAJyWcO|c! zPg=BjYuhRFlZx0+U*AJf4Zg0n*HD~P;F?2y8wZ{}dJIyj_C#aE`>&1G`HwVJt}V}b zv;5@@J3HKckiLdH>T4|~Gpr~XZ?uwR}? z-?6OnMBJ!-0-t(0QT$$tbikPNI7KSee!+HB5ZCEL*76FW2h#wx)K(+aoy2#3^<<~1 zuJr`KYWQGCnG}W`OQh4Zm@mW@T)jF2R_i-jP~J{lKrQ2^#@2RI{^@givSP=F@VS@C zhBwy0$>-fL2}*1K<$H^_r)x<}zd|>7k?;+MyI<2Ap zpQIrB#`NnosaEZpJFzR!!m;UI-}6)P*MT>gU)k^ZmllC^{nW@ND%;{(+sl8h z?XbO_|5=f=U{7H#4U6+tHSy#*t~@=P6N45Kszs+a^b}x4qe;VI#*EH!HET9gGd>V_ zk0}<{8V7&1>@r84)OasG~qn~8_k-1>aNoLCd_`H zuv%*Nx#v=!$E9waYA)n9PS;?dYP`A3Tej3?BchZZI~a4&>lPYrj!J}twz=z?-CEUUzZ!iZ|QVDkNjkv14IvYJWX8pM_h;2Yn`SL zpP@GQ0ZFaj1E-m5pR(QCsP@2+BhKxJ*WzEVeQn}yECR+kY0V<%B=E*yy*=0wLW8{_0X?(&lr21TwG$m)}FmYzZOET^KD!rzZb z0%9of7ZHRrfz$(Gr`DG#m-74~!_CW{OJF);hxPiLU%foXq*hvt5xyJ&mJofgY{^ZC zJKT2}8#C-j_$JQ5d;iPuy6UZc=JjI|W(A*~&HJ5kUKiZeofS}z1gA=BWCaFuVx_l# zG96cIn=V8kd>nQ6B;yWvPj`?1cZ!-Yi8*S|FS=ZN&xjkeWYv7yMCdd@Cnc+9LJ2|* zbLG@iWo-Sko$C&#?s9V$EU4S$Qh83GoMV^dD6{Hag+j(xk3I;@Y<@z4D$m6X+{X5r zXe?ck0eWunSK~}VyZw1J6Rq5e@LJPlV~S}7zK^(x^oI=w zbNiI-!P{Txum72W`>(V||2d2E8ur}2qtLQVNGr#r8$-qJGT7;oreqmDLsQbk9fR`} z6Q#}FTfil#Jbg>NczBkzXKVkJ(wMS^k~_V2FX9m26DRmV)6MC# zNWVqlvJrLGZ#lSNG|*?0ULws)mR=51^`Ne(VnlJ+z$D;2eABXqy49YY!4EZg7>u>j&X`Q;KSfHWJGfm-n=^NX zfyt!#sa=!CqNG<~Ud9=ML8ZJ`9=Pd&3hJDvKu`8GSNr`;Y)bkF;?!Od%V2i*y1qvkT;hz6y=HW6!Ci;3dv z2q8PE{(Q^`$uv*C0pBkBSx{6aZHSwjA9oiqWR};rn0u+s!Iv|4_{ zI#NqcV$8}S&R5o#sFU-26jJ_nf~_IKLi1iCrrOFXb+0V8vQ1V-BG@I23kspwj~;h; z{iXL&c%|ya@G{A6-|%n6>jueK%uh}TK@ z^0_kK!6bU9&1W_d509T%x6bZ2J2|$^Q`rH zT2s%W&jVjNomq`m_1aVEk;X{c94K$@X%wdVX)-rb|12Y9?~j+FHN2#YCuAEwMkVA+ z>_baPa1Y$fyEgZp9`&yopfEVyE5IyN@ET3FT+K5ujH}ptr$y#H+|$)Nz1~f+0&x*- zcVSEof73E_&FV$c&y!bS8oD_&nRRx@=hQ8$OT|Adze1Kn#WLrw^!8lE$7Jx#T{s6FyFTObQ zEK__|o^z;Vz?EH=j4>bvq>0k|IY`S!*uEiAlT;8MW*S~^zmC!`9pAdrpu!I;>5M?@3l|F2;W=aKVWd?x-z(vo-bTE3h*Npz z)&7BW&aS*#M=n;@ooj1lrRhadabsV;W^3^ggi{ErPD4eD+12mHA&}4z%X6O{r84T;{<&wXf+HTW;bW;7_qyu8q*NQ zadsTI>F%$WpjPmt9XV>j{0T0BvC9-`6Uy5Lc?$XQBWE@xF|})V70PQ#zr?*nkk1?5 zeBV1~*;;CT?_7Kz`540f0AN^ezLvigxlLW4B|>%h&IRXK>CT=D8@($U z2M%b$Dr!UVtOsAM7}yf4y5o+^`lG4f9Ajni3EXgpfU!$P_3{cG z_(E^SW*Mukt;0e74+_cV4}i=EK-&YL2g_kdlVy^OqhwAf&?BbHB-j5z0x%2>7GhDHmn-j>~tCyYW zt!9}oy?g-RJ*D}7>iyFuvR3XKkLa(*{ZdFgcOrM#$?0a0?2T6UdhgCTD3|t9m)kCS zQ=Y(_%eFGJoJLWSN7qJIu#rDP5)+6gzT`38YX-#A4yN=8A`&&CPcMzEnUK(@o{&A;#gi;%i}H^z^Kd(O3CDndRymctEA zG;>H_U8ipxQleeiC7996%Db(*cT;>WzZTg9aJmte8&$Uhr09ijuUdsosEQ+?w~<;9 z!Dk2)wM?8=m#b|45W|cmqFrFkZ0PvofU>K-6Oog4v3@Z7B>*?+5_NYT~czQbue&O~?WrjwrhC#r1W0CNviXD1pQs5M zi5r<_Hm+1f2D~bnZfC!aH|xUuyY|6@n5qd1{_&UzjC}5N$qoJ&FUvd=bN)i)kD5mp z1;Z(yOHzi9Jc3;RB*IZDKKrKT!S5G8k`R@5&P%W*%$>F)JyTP#ea&dRX zU!D>j*9?C_ou1tgqE_;qA`1J9H2#8GSv%k&o`aU3Ji%Gp&Xti%vy0d^WP7D7Arcil zl4EBQr>*n35c&pv=BPUMJGS@Qto=&^*snWs{I;pAH=$s;#Sk|w?2|81Ar=O&J1 z7Nn>xD8aou#IO2;jt2b~_;@AgMyOZ_A6@LKleXjRpJXuWN~Zmki8G-lZPVwJgx}pH zmeuHe8o~b4W+z3$9|;Ki?J`W{1>bNp7!=JH1T@wQL^Y)~%PaS;4}42|Q=?ZMok1HP zlC7h`pqI|gu~^4V6wUSk0Jb@YdDE&6qu9;Ibn|L)p2;a@Y!s5=R31&n^B-7;^kW6b z4ccrjZV;YrB5cR}G|T3Bbxjfinxj#8#-;@mtRg|3%g6-NnmRdZd10;zS7QZr1~A5< zyAzpN6zH0WpbPqZ3<9Mn7eD>R4Y8qp^s58s{mzSXI68*ZG@s=kv2>2kuz)HZAK4G#!ChNpg!Q z;ZC!)I+V)h$~n+V6IFu<(`4dKPR!m!`L}4DpNVYN%a|Vk+k6|px=WL#ge)|v2&;OV z2m)3eIYVFAn>$xg(1EMw$4|uxftcmAujZ=g%Sm@s5-ntCAxUi%8xdNy+fv()4nyD0 ziH436b<3B=9Qw~J8eVk1-KtkzIOLmIQ+ z1EL}!euC1KIb9(*SXfVEU3jpDk~8+W{T-@0^q(C8U*25XW!5%o?zljBsp`M`IjWYv zDd`KeR3(13J8+l6N@v`=!v^-)GSpf+k8iT#*CZ$GhV(fy^lP70o%IbW=LcB`1zHXdA8m%CDiohwU&`cei!b zv2$5dNRHxJdjMknpGCo&&$P!zKPMB`U6>Q(-q~h4KCNU@@yTu({WJ z+J|^S)duS_UeVn#G~3eoGs)}FVuv5-Yn|{lH|Qk`$oGl8@fupXkO3p!3va82Q9l{B z#C>6&i8{8CDh(z*!^rWHq^{G!+$OrQ?6gt>s7H^oRM#@hj+a6O8^?qCGG;AkXhzrI zh}{U03`i~JMg3FAT~ADX$i;36oYlJ0JLeq7BUCv($v^6WvFi2oJ60mBXbwl7sz~G= zsLg<2ca0&=PK@DYaYQ1xf5)n0^+{OK%W~{6mODi)j!50a@o^IRIhMDY(@yz4@a4O4 zBdk7LlFM#ZiX6*flu!bQdVC(mY+Gc9`bN_fwZW-DSXs`^&)@`x^~hz97MEam(^nEo zllF;lX~I}L<{(zOt{Pq?VGcpz7s#P!ollV+u+LtUQWZOo+R<4W%NnL(bzity8SA}q zGjQP^r1@HfzqAE7bW=t9a@p@1aEp`Bo6B$E-Oi9q7S+2&V!JLd`q02 z+1oi(+(!TGr2GIdzwdgnG`?3FQ4S4ez^HxJXAn7V5W?O*dC;}-#=*pqv5Lk4zPDIb zzr4{~zl$DSxn4cyg3r9~)uq@YK|3o9Byk1mU0(l^zRUTM+}Bv@FWuV1y@y^kyH~ZQ zt`#xCYeglKaqQBhndP=0R~zoL!gjV4Np8eIt+(rCiWL%AjTw{Z(^?N*L%j~;GgC$@ zn-q}uO(;cickCF9afB05npkS(I;rieFU`XHl6KeSm1M#?K)UYIMf<~y6dO+xE?>S} z1J%ACabwY%t6JZSl9O z2$OXCfTn!$H!6e_VrsAe7}>YoN|(TzxSJ*F#F~-)R3i$Ar3A6oj5f zo@IMhQAI7JT%dZ%#4B(1G?NLuYC12p0t~GUlT+a-S{iLU0S z^04@5uGs0*ESq(E4s_H(4q|6-S9vy5l+Xn|rzK$(jU%H-(Ty zfmfBWP`JPE;s}y2WcK)A{G%zeZHFd{GE_+*Y+%iPCQr3I8Y1n-Uhzdt*FMBSNxf~= zhSkKF$s1;D-CrX*v_HmqTBzwxrk?%S;yy)b=W6F_grMboFm2I{ zpGv9v-W(HSd8f@7T-;l5CNAO=Kd7)Bus1en=H|XxLh$o_7X!EYNjHu`3k1rnH#OVt zBQIOEGv%VwId~&-Au9OqzI@D|3JHzBnD1I+(nZn*TtI8vgX+ks1a@g{x~cwlL$JL4qu;_6U4sPEY8P|H$ZKA%-e2~2 z)$m+$nVZ*-lGnuICWmx%P2pO(^JbFVvnezz?XZp_IEmM`J=DRf_z4#(NU@wwNh9v{ zY)3UE2HO|Mh+ZbUck591)?j0_+LjY?Ul#-v>Af3Oa#k$48Fs=gTMu=)nrRzl2Wa;> zF->?$X$UK1CdRUsE)T2eA9R9UHE9hjj}z*tXn0!X$Qa$Z)xE>im1Z z={{cFyU7y|bPqX*KQ>-5@5kuU@doguYW8+6n~S;JV2B&1xnj}9;Pl%m+R$U6uYLlc zr7agNvLWu54FZ2t>`xbngtZoQ)l))U+62Br!bh*I#(4Xha(XeqcF`~Kxf5lBqzu9f zgvjny&rKX<2X#>LD|v7o*8QyUN+ZoC4R>|vMn7hGXgZi=V%CBqJRp^cMJ#%23%US+ z7uKr3ysW-yS(H_snrr}71n(?`n3%SzCc4|xQ()<14C9RDz?J*&nrTqbd_~UqTm0xJ z83fA)E1nO{j95auCZHl<$hRl(R&^I#bdQqaJ(iTx&fAu==$a-`+gL0@eLA z=mP!8)p>O!Y%Gq$DS15&R$jhr0lzu+^DUbu3YbQ7%${L8f=!c`h@8B>qtNw zmd4tnV5>`AD}{DVj9dm5;nKfc2pj8tusfd(;cw6BbcBL_vbne_@JQ&}5Z5UUc8fED zwMxy9-k67Y`L%c2g-ICW$!BsaFW6aMeqE|Q(lQ{&_;(uRA8&mcbJ+Z%Q^z4#3}Yz0 z!_v(BlV+@z%BYaEl+~)$-79zZILG~{Zz}flObTQCWrOz3lxx+?u`>?g^o@^;%xORC z%NO%zBkn4eK>6RfO%{OY{Ml~|2VFd0x$n=wJYty6JEs5Y2EKF0>B--Sn$ z&=l)GO)uy?Gl-yo)JNT?PSp4wdXazmhNHq@-r$f1#aHPDtv3rsrTXpjn*GikLPk1TNHX9EiDYwThM4 z=1CCscYT8$pvIJq1a)fDKEagO`dC?awjf>0Wfai5#jY(kY7D;q$iggGxHT+lB4k-) zj)iFm<}X;!-3gkFp(k_0DXV~{@33{_$IrXB8Vz9CqQ!nE-AJ*i%i9h&&Kx8t>_r{zJJh@0Q z*%M5VqUC)=vi2+fsb1o2-Gix^mx*a1MJ6*)yi2Vog&bG#WioUFQ);7Yi9D?2-!(Rq ztIN-!4E?>tkS|*kpw;B|VslVn;cVl8?dA}-K;%-X=26C3bX8&elEw*X!CKxPo72#VboT!?3TF4egkTBQz{%?w#|`lG=8~)7ow#x#pEIL3SVvweF=tV@hdN)lKGU z`-I9N3R|9+*x2g;!DGKuIR9}~TJoAxfl3XXk1`sJ$$+oe6zKD0Qvvge9t4c> zw*{ytS4Fj$iJNRYS#m+U)T%Xr$j9oXhQ+HU2BAy_vbf}JZw2ag-#){BcO;>4?Syj{6 zmzuQPL-lA3RGtqS<#O31mzUYllf0JhEQdfczG(`rp83A&9{{xb*1z5Io?LrR#WS3sAe`loPt<3jaRo;Qvu6@oyvgYW{ae1J0 zm_EZyx8Zh~u+2TJ!re&|sgZ}s4%&AKM)CGpQLY{|aU;Z#rw~wEJ=1D@bK5v=>#qW> ziI5oBB>f-Ey>(dJOR_%fP)z$T>O{qDkCGI*NE0Xr$9bRH- z5gCNW{wtX}L-WTsGldjI4rN%r>bfHnO+N_vkEi!4Zhp7-A}PJpY}+;MEciDmW_}$h zZ{N(wEJPC)(jZ?%cO*2(+E@w`GtxPb` zxUA0sv7 zx8EAyK^N3Oo6$!dvi`jNNz;Ol>5RhZni@~7fAAyc3|(C(-bK_e;VQh<8VMgK4_Hit zYep37PUL0rXy`%_6pD47btZcj3Zk4FD365H#GP~$lDaP#9LIk(c^TrO7I_R^$$=9G z^nhU(@%1d6 zh-YmW(7x%f;&Han*dGVdTMOR1Eh`5NY5Wm;^1Jz`&i5VU{Sw>cg7}gvRTE_NY1E5Az)5gX3&g1%my*{-TC zotXoL3~lKAo>a(^>I#|;gwfn6uWuxYxy!{VdX>h70UI(5JXY%7NeO`)sR*{VHcz5^On?7w^mX*58 zLA}A#9(CQx`nfpHQe04K5+(fh&*R6IjNewNxC6S^`3B`sf|m4|ZOXTYSJFmdXGNA( zCIb;@9;ukilyL|C%M&_}x!=2{eL|Lxs5mlT6?I+=sbb472I+%kYjANf->qUUQ)SzT z{D}p`RRn|`=ds13m*cJ}S4DUd39Kf>ZhO%wYE?qKFia8qA5?EKI((L;bS{x`$CdX( zcjLLgi_W=k5pR7X;~X`A1mm&T&Y#a+-UXU)_SwDTQ(*EQF4AD8-EI{>uh6_34s%)5 zqh)i8DEdnAEAUd(kaK8Y$n7Dz9Cr`?`Xfo4344WEp|a=3NY{IPYx(Ykl@2(pS6LJC zwGPISoF7|gvIl1|yS}!9B}x*L57!TZd!Axd(RD*w76CkLLko@~Hq_H8Q_9HWyC5DD zYF`0XdXl%R%e9dn9wZ!A5f0IkdQ5vpfkz#0u<$grJ)SLo_Y{XkE)%Eqm^EHSu=lH; zB;Pi`S*Q@ig9Jn2WO`j zTX*<0Hc8$C@Iozq7u>(CO_>gBz-nE}*V2gIe*5HDHYsSTI3+*WJSD)UpH*RcZy6_0 zQ4IY|vt9ls4Vk8NECtB7+Bg;TQk>}IYSBHPUa;19NmxH<7RDdhbxLOQeWVV6HsPrD z`}F;;23~G9EN%`EohGn`Lzt2#YPjM}m-jCxf|e*^laKD|!7!ijc?Z_HNly|Ln#?PH z%&71V237O038_&}5%L5}Ka-Fq`MG73u7hQA6v(e+%r(zkk76$X_N22(P0Hpwv$@S^ zLJYptFMLG!gr`$6HYjvU9ZZXVRSoYh;BjKMo-Ob(XBkH@6L_toO>(0y-C@KYyBGcr z4T`(7pH#nF=^;DjbIWUrU8j~?l@}E=ovG)h+}$!d?ZOfJBQ=Ype$Hmf3cma2RZ}@& zjuvF90iARkSiQ-tK-z$(FBVGq=9}i{7ao+Rjau=iW8F<${@{Laj)^S42nm_q5^jiW4tt7!roD=+9%3^SM$y6llVAb6m2VQ?kf*ZV$#Bnx1W@xktQW5`X~SY+ z8OY{QZ!r5=^6={DD&qR&uv`_COx3Q_ZHF1Nw@6^2e;maYl{^AbwE|CHQ$>9L;lXa* zc-_D@^{4)MF!j9E-G1e9jdLgZ$uB;C-K7@vS#o%Gil~O*bUW39c8S z$4cC&do-K%bYGVLmV|@BxPQ#EKk4#A*MGN}{)T;(;mELu=~YC1!mQzfK!=j`v!Knj z+&m|TC>_m_74S-YJkQ_nwy)t@WD4`&Cx|bCdSnK%F~mcyrVneC>sT<;u*$~E=S8ym z`NK46-+)829!}-eDeBP0!0audG^x0f#gb6RKQAc^>iXYM9T?3qRbv|5>bYM1Q!OXbuV@ zMMcxbTm+rF3=p@%ITCD%16NPu68X`}=L1nFaKbyRE30a;cn*RP`V|84mSff=IPApL zCeqaSVA}G&OMz#)+c;|vkL^miYtd6C5C2S~RP2i@g-DxBNEgD;^+dbUxnV4E^K^(C z>AX;kroR~}Xj3y6vsyUi4De~rsAi9kUVN`Ow2?ia8C4-QTVck^*PhtkTV@J+i{cAi zD!Eggn6&%+!@v{)4fw``V>!E5;38#(#=jX6>8b^fDK|qG;7QfTq16BVBgBRpD(6qV zC+gi1xf_*r4Zxq>CU&I#8RRanCj)F)TyT6KNC@hbBU{??JqKqYCwT~5fhE^o zeaDXqkIF=XG9-Ov-&eIh9K)wQmxBqAS_u7J*ZobcYLqze;(CQ;OTFTaH^`4Hj0P71 zi|wKxYq6jr&CX;KQK>*S826)0TW1fQ!wrQ(lA{ zWtmWk*$zM8pZ6VkHQ&$nbqEkqHC1LHn=jnV+;qmVwDrpBMzwhe9FMGf26W*=EykVG z&jcJ;jIoYh92NI$YSQ&zo{{0RFK&S5zYBiJVOw&tTy^>7!{HSEle`5$O?RH07CWk;d`MJpsv&MsmJ zx!j&x9GW{{&T8S)ozxMH`(A>quJ-L|v`oH}2tvA%E3w|ejOlhY|S=n|-eK`CZikk>!pq|2SC{7QL2EETY z8rs7J(i~A%k_D9|)CU2nVxMk~;q@u31ODASRqqG+sMO+QKFSGlUPSgqI_07YOWA!eitu-<9fujcL@_8o8Nb4R?Q ziZ|Wl)=ME^+u41$cJoN?>h+}~?WUAC4!Ik4n*|oaDVq|x`{ z4KCH0+#^`%rIGW=^+d4!W@964US?Sy)@^*|K$>e{Lo(cGk8q(j`8J5&O>0dU;QiK? zVgTCN><-nUp0DhXv*-+e20k~b?*?S$B`4jZzJa6#JPLvQx`G{z>y zt^ZYk)nEOU8KMmE2`&XgxTZF{ATZ09K$SF&bv^<@Nt41tfx>RK@x{g)JqA(f2}%Q< zk8$pud8z6OVqKKuU*Cw%^%Swp*2*?28xBa&1Tv-#@N&y!5Y?Zy>TpvHNu3)VxF1e^ z?5-TS(~!&P5@mG*zbkeF`#D-2W9HN)fucPN3gyOM^^!83R$vjzio4K0tnR@XletO_+M(+bsQm^_vGg(MX}gch(qCNJ_`+8dqHj zj^-1-V<;ctOwknF6GML_NYv0;Y}b)??2#`TntMM`9A1C({{3{Nb+U$Q^D+ zqk$xUeDq>#PxLpG6pzMRqs`XPFJ8K%ak^EX4qm^e`KCY`y!qlVAlLVQ8Kb&?gV8^D zkNNvR8}kK6^ixd_7yK{%f={|@c$mNbF&%#l<=;lP`as#w5u$mBUv#i(ugc5!`Z3F= z9>FIDM_qTnlFIzPU8Mx0hl6Y8Vom?(MK0st==fhx+kc3g%<_8eR5S+364}>#&7jd= zAXYgdnnW@eZR9D`%0hd}et#Jo&}_zvYwaaL<( zC{!uX`PpjIeKqdeCpJ4T&~imBrdyZGy%agbD?2pG`L>C8b4g1#@>X6LUWz#F=749m z?u$}}Qm(h-FI+_nnX2^MJ=HI|{1yMGV}NXWHkb&fM(p%i5mCd1%hNyqQPn&DYpEn5 z!Zh=`BI$kiMug=TmB)WnIsKhP&Iso(@ErMzcYjp*Q{pSawUlcdKXZ6(4E`i0A z0pnpGW~kYk?bVm(ywKW^PRD%H96?Y;Q@{kl8e)m#Fqh7aFAk5 zIPk63iz0A&@T_>3RRYy*)kEu9ULl*M^@Mym2UaJmqEXIMQyw8yteLz*VRE;6{O3=u z7c@eh;Fd$~KfB2?-n8YNSQ1T-^Ko+ZR>D47zF43Ty#ZziAGlDWI?46S^L{^xNc{(8 z$stP#)CiCI*Ai6ypAr(uDQ#8?+0ojn#q99}?HuA=anph;vj@ZgEZpR2G#~T_lPTX4 z3h!OzJpVE}F{3%Y(=q)jKyX|f41SCIg-UK21y%h&EXV(@%tWtOl7Dz`7a6)Qig01X z7q=79f*(&ehX=y|wYE^2Jghw7&$GqHG#7qp_ZS@K9~Krk*g8X14P$sV$O(88`2=5c zy!h`SlLQ2qjl-hVL0(fc_lpt+D*I?R>VbD2Y5VJ{FZPx^v~(*f2PLjHd0`73Jop+Q zcB)m+53$WE-&Qf1*+g@=1d6S+p3P)cgHHMNDC<_@4`&0Ua1sXH*rgu*&>3@#DLnJz zJ!qYDmN}|GI(zRMXnurdy6<@~J5RPe24NB2c^kcxY{C1az3ErJWY-U)tOyN0(fp*# zXraqElviyHADbC4v3KQVHPtWU5@_(|@1-xKv3@^2i-b347oDZ*4CpKulZlY8p6iyc z`SE@PgQ7e1?ZSQ6{RaqUP96h`B!BEDymGL5)jkTa#GA|EDBE^94`6O7%g=N#rvlM1he}I5%4gOdn`D2x*D{nOo(-E^KH>}ozp}lLKzoOW5rKD@e zp7(a}I4b>wU>CL1v2R>X!X{XB_2!coiNamNgFzWi`x{E=zRazT=!LPRBS>nbeAnXG z*zN0kUDtm)VlzIxnk54suFg_VRb`Khw|2?9UDX7h?=v~>Y8{+aUIi3G2)+ngq?c*G zyHsim8F$_~xhl^{vr=~6Z&FSx~7 zp8@Q!jI1KW;?HCe3{UYQT+`BER&~6OT&1F76V1-;vp}vQoc9&zP39%XBNaCA9GYO@ z8oNC=E>avm+aopDWA8RIilGLySGnZ^4Y%(zqg(dG$%rso)Yj4y`N~_d%TKTx4Te*} z$N51Hiy4DrIS%&^uWX`aw5V0J>?@w(%=b)dFAqxt_VDJSeyMfEjlVcIN$4h0`?Rh;(#5X2`ldIPvVqT4n}$`gy1o?bYb7>iP&yp1d~$0 z)fnsbZS!+g5#rX~mYHV(k-6<|O_=>=%+`g8;{u>J>k2kBjh*e!EhXuYcF0*({@-Tr>QN%r0slrUV5a^yxq=>wyE_Tp5%;^^ zBC{`Y+PuiP^g>JB5X82>7A|5gC7iul-bv(OXrVYNJ0%h|6I%TpgP?%3LHV<-*5{pl z3B%?RW|$(2=1JjK%`6SLk=;Z%*P$dQiW%-4xZWvfn-hWMl)?UCVw|U@XiD3vyp-+d zzPPtfrovRS<8X+IP;hCuYkNW?a5U~zxLNEAAOfR45wBXJKly3DQk%scgUE*scXvYD z2u!4`xoX!G_WHLTCp)wed^6E!wwh^D6;@!s6{@7xn6}YTCp(};ELlEFzlg{_a$Ofl z(X$!&05Ri|NU@3nxv*X*NCvlz*yDziPK7Q8%C9@J32XV*7jY5(u)~XrH7H>mLdbnZ zu!LE;(VCgLoN24O;(jY7(exN1hu?VsU?w6t&F$P+q4^&~)Rzakj&V&(yO;L`_Q~{> z!$*4;+>{0Mh?8v;$jtJ-56Gqt{cshPJmLEm2CkI4VTEQXOD@Yam+`UV?o2Jd$uf;m zmly$(2Ha|N@10V!yIQy1AlUuf5$jor$$2<@Ofv(XQ61qssoU)Hty6LaCC5UV{r%*d zvM!{3ci)Np?RsowRe1Pl{BJ0Z;Saa+xxvs(YZ`oOSU&+5FuUxKDa0&&sgyeimTPmD zZu-1u8(rJ<>md01rGPEu1>N;PFU&Eb#4JH;?lD=Motq2)64a;u>f3$v$~`7Ba;+x6 zHX32a1Vx<@(AduR+`AG*Luhaq8lNdK3(Om)3%SZ0+-FJu0eK(R@VvZdQ;X+aK`m|+bFU34c;EitQZzvd{ z*FLWNqLLn7CXLZ7+J*v$-UHjk_({I0GS$4E@7EZ)q%#R6I9LZ46{+4@4~rJcMyQ@0 z&w)#LM}Ubh>evpPQnjjA0|F{}i+g@)0(5l;Z(L|W*X#=j0AWLfLBIywlQT);u>$0E&rhJXpx)7ew%Tu0BiF}Oj+&1~?6#wY2- zF~Cy-GQC;CWyyU2uFvjTZAj=^IG8)P4p?mO>MYt)V91J48mUd#a|6=?Gbg2js7Y~B z&nClGTDU{pam%ylbEA`3wCMNzYZmwznkXZnCklNN*dzG*dP|A}7~+z-JSFk}xGmpK2NogaPckqJNk*(Gw;&coF1nf4oi!4Qo;2 zg?5``R_{CGLL#`mai?p+O`SZH-s$$P?Jt7Hr{zS=+f3BB=J2yoU>(xano}QSy{9dy zmWZZ2lg{Vjx3=AlVJ_bt6bK#VtaP$6Mxz%yHLF1x>F^pW%HTz*$0;vN-;q;Xo#6>$jb`ip914Hcq-r86{ zK?f#&-<52aaH>@9X~B%MNBV z=KZ0ahCCe4&Z%CFtnorkRoYWLL7%}K4O$3@`Y+midn!X={6kAO`5#sMu~|iiH+Zd| zSvknh7WKpp9kmuLsKfe<0_ZE*#G6XWtON@+#c{mxqH@74bI;@#aisLmshIC$E*VCO zqYDj}9i_;RKTb)bI|pY*!u{$tTyRARwhAC?4i|3og&C42PxuR0dFNjD;E0txU-LAV z9GG#sLg~7DfM1w$m-wiGH!IO6vPH>}#WPVPUl1Y;%HzpNZfjxqGPN$D)~5YYA$E8j zx{${P;hVPU?W1_I<4Qsnm8m%{=LSyY>l>kQ(MI#8vD*=LCtkJSp&GOW%O;C!Vy?%O zp?a3QS({7@)wBbIkbAxIugvFVe+Zd($nLSM5(~!r$~zF%x%}2m-h~32UQ)o>_tE); ztIqXWnC32iL1m&8`8>u4)-JJpbA?`9agFQ?W+-&o(#wAzn2HLK)dYH@u&&m=+}H{dnx34`Kkeey%Jk=oLrl*!7Unti*s3YcdH z83;r04~5*47PNwuL0wqLYB5%f`GfX`s{VbpQotDM>S4O3#j_ zYb^#u-H6V6m6C?JU0W$FG3jxl8tLqX^yuj6R$bW{fP-ChyO}$3z+6rk_H!G?PaHa? zG(;uK#ya3`D8F#b^_8<9ppL9BS=8x8LbUq!V@UAC3vwq(3$B0W#CiB~tLDo_h19|3 zNNL7O7?nG50F0(*~_umEOatuW=RgW;r4q6euh%v zyK!aH&1{9vk$1xS0(l3tQzqZWlOZN= z8XpHY#*azyn}>!egl7kOL1hOPi`ns=xLX(ma@db&u1Bf;eD_Igpzb067pG`GOylb*|bY0Nqp+sOBhbRFWK zpI)w2nJCxG5oczEvFStwu4i5aZ2zNOm|y+)+g;t4!Gh;A_+%p0_CkF4bw~dylj*S< zX9DxcLifD?`JQS*(*T_R(G+j{BsT4f$$p{_o44Cz;}yAp$o5~#IJbc;SYQ*XEGrs* zq1>E%5p)iiLJ_B?h!FUs2G=gS6_YE^(%Q?+P|dz;(R;sDH&>re8}_rABXp8f0m9C} ztyFsq(dRZXw->lj#{9*J8})tf;sA96x@L>F@Thxmy9hUxTB*wV29HoQ1-Kr7^UXBe!*A9(a?v3NYFurG~dsaV2R1QfAfNw2`&nw?`sCDS-+R z#@l?~_p0SA8K6c~4Eb~|$3}p$h^T!Vqiz0en)+JYxjc*TI}*zEDkK`icOGF%arxps zdaRIITg66t^sv&1{e8}|`k|#Gw;JL-`NdGIJeD{j&f#q;mW>Km%vkdo)uTejFTLUV zLW2S3#U*6@!zY*zD8}9&KMAIj&~Fu4M~*hE`Kmg**n`NlZ;2?@cILT7Un%#@Uk=%f zf|@IXdLT7I9aZih8rvzELdPhWFGY54t-xBJ7Kg^W=XxJgnUrg)PJ_R@&>oWFS`QrA zFixX)fRqUAbZtHu?%37_&mx&S8!GWTa3Z=l+@vlh-u6r4(15O=+PeKpr=& zI1J73w=U}hX^6_=pd)9*>iMVk7Ah?o!~r^png#7C+}fqwb?w{|vu#U+ywGNKi&*t} z&7-~J>Pi^Hp&<*==K4OwSn0&WBk} z`vy$PA1)lANPZ{+%fojZfu)wQE|0y%9Jw^2*^XO$mY+P~cw=k8qE03>Ac;2HZPW{u zpJ*GzqZ>Kp-KWNCQ*+icNzrAs4OKNr%5_*L?{aG{p?+cQaww}YVI0+>L9@XY+qNHZ z8_(t?*Eh&fwwwCWV!#WJ=V)L(y;4}&x|sa~%pz?_XwN^;^e?sts<1oiG|`9#3ASdJ znVd{pUqe5yVRH-1P~nb5bTp!UBNl0`5ytyoVlm7z2sd+MPM@;2Sob^|Kziqry!_Qp zW+8D2qy^MKm~BJ=Ub)XmUmn@4un1Y*!-<~lN@%T0?PH1FKGEBWH9|8e+j?OWOkG9s zfN;#@RiLKRJz2g>9UE>ty%p5-`G1P3vuOT@I|RL9aGczG{Gf6p>JB#-L6%1sHZ8@a z8XB9-3lGNC(~@`RHw|i!?a!|o0nj3Fx_b@H_LlVO!s4P|`n&^<(lO0^#vq+}Q>x^F zDZXa-yDuAVE29&bs?QenBw4**?TbW65!bhS2DUjoVs{Dp_SQteEBl;Sw?>U6=a@_= zTOXaIL@C5<73Bb^i;Uwh?NIefsw-veRX^zSXpw`3pS8#jT70yS)tr%^Yr>31f!;iN z7b)=`Q#o)}i$A1$jZ4siAhK)CPl6emCqc_Au1P)upco}9L%~zzLo*f$@~!>H+x_Yx zLcr`pT&+@N&O!I!@Z&jq^|wztc#4aJwDj#OKZm#QjON4YFhVRNDc53e_Bm;mBk548 zuB=hX_NhN~7ngR%9%vA#7VQ+OlUm6pcd7?ue~40Gf9HprmwEz?f(3gcwo)5Utoo-s zzdoLw+SPV1^6gL-d5sPOKV%|F{~_QSY=%{u=i78<^iNKhjDK(pDB%O_7(lJ1fVV!a zI*qbAUia=2vZb0!PT6T4yh76+hzOt^{DgdA>*a}(Te*$R^eF`DegmBPv@`E6Ms=NmY$=Od237fewYtj<90{C=fU7^{PFd#isJruxmUtBQ(2>R zGVDtbJ?sAR;RIG-37%Qhd}C7}ZwcVA9OW8rln6p)&x%lN#e#Wy@pB52gUyqJ2e_QR+MkV}#4#?Lf~C$-02 z2{cBty}_?Vi!7Z$|?;&WuyW{$PVyjFaxwsIerpSLmo%&h4>a9f7U z7@SUIC6pTIBJL9Y8%nIRv4BA)#6_bbfs)R*H71@%20F<{M(=Ef(KG~+0TEZhhU5Y} zih92Jh}E|v6int%B|0gkJ~2^kR$%9?_S2vrkmKaXJrfd)c6Fq4GcO^~cdSV}`MA$a zHZaC0k__C-;cJ@AfW&Lf!s3U=>X9?a583--R|q4S)Y%EQuU$E$mm$tg*4Z)gily?^ z1OC{H;u*nV`=r!}nV24Y@v$`!lUB0O12~W{G3I1K`%GKMDXnvF-uvqTJG#!FuNM`a zAmKO8F~^0E=w7~`8YN|pGF=0O{e8MEl4!=UE;eoiIJWP;G8 z-Fd3wqO0Afk`LCSpt+q)V6I>H{MB*K3(P$$Quq8hF>`Y$)Q2oen2Y@$zCL{6zsmm* zGqI?sAnA=IcQGGrO2GJ%%UyKzSJQ_3q?Ft$2hw8iRnKhtbNK7IcDNKzoz+#yW#85w zrW1=eQ*_&!J(-YO{q{obC!k5u{^cTn9btPNRyyV9pL7^z^V}nc{r$1fZ$Bxr#wrHl zSU8>q}7EIu5YW9X~~GH%XYgJc(T0mQZ^|TQTnwJ56SM;^poDLoA)fN+xY zSz8=_fQvNVyqDEyg{JM7U*D^`U_T^%egB?7u=1iX;U44RF5PD=t$I)INWC@h`F)|8 zXWRe}Zg1`z6wjkPTt2a}x(aNXUc((xXOc|Fnd#JpMg8mV$l*luPMovsvFUs>z48qw z7Ljj~AnvnUR@lbO;hEMpI0)f_ILd?jQ#eiD6gx!e1RU&>l85x8Ln zklv4PYpfLs^?cdHOCM!oHma3wH~n-s-O3<2Q?&^`gY5+j68y3Ve7xHy1~>;$6?hQ( z#`zA<2EKronjaEy&oq?B487YBg1nBil^o<$0e5xyZPhuuYx$=HtptRR{o*q!ca_D0 z^^_mR)@-jCgqSoV*dTth!%iSK?(bU#>deXd>)NM+z%fflhtJS58OFP$A}&vrhvzW#*iw`x-l z?It|=gm65T%J4z@J)jMaz@+9(BZu>`Eh>OTCGWwF`?n zuQ8*^aYtidJY;_0>6b4~pjidZX+p`lSPKazv?r`of)we!-4TUU(oXqUrmA%$BgRI)M_A zp0s{}L}3sB_0o4nMjJnNp{6*jS-ZZ&2)U!LVj?yW?(%a?D3X~ zD3_DI>jNY-dv1Mt`cP(l4_Px=RbBjTzww9JODvl*TBhe8o^0R&%qVo991z0cKgZj3 zmio$v!Rf|sfUDCkK;Hulfov;oL<|e2+?fT44xPR9MfbZg9y@^tXe!USfau9+)ajFm zAl8nziU|sH0pa(QqIVQKR`1r^pk*UBt|fzOQxoek5jXzl<@dVqR>HEDjQ`0*JAQA| z{@}FJ0n(nK*i-iEP^RswpGWkoy0lLakZ3WwK4^%LgVxt=Yi5_9?ZqQbT%#&`rtj+K zh}(}0<}+;7 z`WO_KD6mkoZ?l3sdhHhmmp%deX zZ5Ok7F6^zKwK;$w?~{AvZzJjsRk#vudl3dq? zT+dhzmA*6)7kOydU%7jJK=6|>P1nmaEvanU$a7t!s#&AXyNk)a#&tThD0s*lsPVAb zj9jISKLkwqrw0(jm>qXo=DQHNZ8%^)`j=QA?iGQl;+i-Sx_+w~dEcIqdQxwDfZ{4+ zJI$ITxtS~=!m|#qLtLCdsZQ1DgY&n>E}vI&BD-g!5jsp?i%n$Zntr8szw2&jmtl*K zT%4eTPc* zgqx&Y2E6LAU=cS!QC&0PDN6EL#fWolS9sqYHcz-swaMAo6g6y#GJojuKI_pxcql8F zHwhAT(zy{-?D;g3n^wI5j1jwYOV{oVa55j)gS+y-=XcRAK1jcGXg{!?Jl;0rk(e*8 z&*$3XH*TbO^P!uBABuTe=Zyqr3A{NN8;gq_Tuf+oGC7d%!jV?><4p8lh;39B3K6I* z6Ags8Hax_kV!BE}Pf2H&k0n=~>Omju{N&mdROP_j$yPg;l&KXmWnC4L3HtuUugZR2 z{aV|b1&Y~5wVm+C2X3OBaod*rtsV z7f9J!;Ab6qw(&19{Jq4|z|yR1O!>-uilcisXEQyERbNN$JS$=HHuWvCxkTRrBr<1@ z4T<3Q-k6&t_WJw(G07Zv-il|s^)T0$uNH}IAPpG|9EKY!2oB4^^)8{!5-_T2u-wim z<2J_U@a0KrndLiQn-=-Mm((2o+IW2}$`l2EmD>Bc`~!cD_$Qd!d-?|~D(Ow>RgF5V zZ>TMEgqpH)Z3ONd99Fts_Ma8&<)}mYdJRq09A)9uI2(=c)Fvul1;spbD2LZ0tPdzk z=(2!_m(?9)S$ec;3tCEG8f)) zn!=d^2xi|Lbk$VOU%FadrLIzr{W$RL3!xz@IM&Fwm@2g@YUP#GCEaCvqiuvomwxE8 zEZsJM<3SG#dU=k{E z*O5Bu^Lla4F?>ZE$0cj|r4!{ay-&O+Nt(M$z@kD^#~V26EUKS$81wg1>;my+;Xq~< z+rOT&e>tE3pBptZY24Jn=Y#liN$P5?OdmGd^p)eWJDt?bY&a~YlKQZ8uI=x6^$&0j z&QkHG>2Eed^8vLKJgi9|>&*eFI_otE9ntu8%j50c-D5W2qzRAT5|H6rT5r^c)Mh)wLG}BKnoWtbAI9HH zoIYfO^#rExv)SUA0iH=N`^!SZ5v#4J{(Yk+z~sM5h0l|CYJGjL!e25a`cczB%~*FWTb&Tw}3PF2~Nkcpk4RTTNLR2+fsQ zK5{HRICQ5C9FI4Z^V4>r;g<5wyltMmeOrjSOzyIuB#;xnyT~kRin6zF(3n(4YnXRM zt-d38>lX28n&~fU?`1*M-~YV#@1?$j;Q#9z`;@V=_f&eG!d3U24$H&CiQ66rkdm(g z>ov7kwV^0=X{cKP2m5#wF1^2k;lm&rN)fL|Kt*^mU?EhvyYKp*#NsJt0fD`WvkSwLK;MJX3sY* zX`19(Mb2R*J1cRj8$6b`+tWR^e@&#^cm5IwpQ?3 zj=}b0dKo7i*kTc&+A2tv9UD4YAIf}qmak?BcGg8~$QpoM#KtN!g4-N?UJ$0-!n z+d*TwW?_ZL`hGsb>dJ1256M0>KGgPb7VDWCR|!@wpRy1yfB?-nMj}-Fw*I6|ZG2q%|CaRs{D}sN{SuK;1F%xFFY~7;m5KzI>`j`OUF>%}gv7BC+5yQ9zGPJt+1vxvQV0;Dd-wI}?nf?L2ush{YM|+kd3t zB#g*^{({l?0hCymzwDO#W^?gB5Y!u|faJPe<8~HFcbRA+psMe^v5-%pC9ItB_DS31t z6+zr+gEsCUN*;a+ukG!>z#mMv=Q#p0(I)k5=9+5}Xu>B&q*M|$Xs$30VF<_%fbSIH z=%^Sv;WKTePoQ0Hm>bNE6R9C0EsfN}E!i1gI7$Fynr%YxRZmd|gh>Rb<>X;1TDd&R zZ({oR(Mo?qaUhb;z|1M~`Y&3kf8}FXHY4b7^#6l(zhJBCv&X{%kNEYZc@J6)@;e5} z)`g#SFEqj+q26=VxwM%l2j)Jv>6UcjYdXL|{}_%>XeI|~)tJ_#Nk^$HJ=83KtA%H= ze#ebd%O&IcZISsH1)~+NW91c{tky`2X~~rz*KO#}se1mcR8K(}=aT7#SyhY5ye{)q z$LCqqO!JR4yW*aSlFb=h^*ss1*4p5A_W1@NHsY0torE5zy1-->nKPmNoX3 zwzKUao9R;EumN4XCKbbSij>)S1#_!0!?u3yb*GYdwiw}Ez3*5D(UA99i2X47_%$BZ zk6wc)9{dH~7_UUtA1Ze=+43pAh)}4cEHKhJi6(NLGdK%Pe|lQ`V2~bPBI=Yf@Y81V zkXsSzbeNJl@ zym7%GarV4g0@zOylJ<9x4h&Qud5D`pdds+)eL8rHVNsm~^cAafYtCMfZASHFAu_%X zP@<`|H&YPla~m5kVpz=+Y)Yjyt-I12HjqVlJh`iy5^^Iq8Vdt1AWORP++-f!C@BS? zu@Plyl%M41iNl>;mS2F*Yi4%+1wlajCS|#KFOJ&o8V*$FNiGg$iprB-{FtpONM1VE zZ9Qmhox)#)xRYk676oj5*%x2J5Z^8rOKZ`0aR2?tcHuJG;z#BbCUX_ zym2NW<$L(cO+uh}gigEDjI>)yHC#D}f~w8*l#kki#_FO`hStBV(PTC@3(Z7@ z50>~5?U!vUr)L2m4s>{$umLyS)6HsD*x9VKNa(Dqy9ZSG**n|DaQ<_Z1*julTBBd5 z*==l3<0SNF->?`O4PA5;_tNa%rcmUfT(jHRCs(ZnET8fEkB+jWMT;qcEV{DJjWZ3f zQoUD1fWb}4anv z+`3DOFa1_Ciyr)P+IHqTfWYxtYhk*Ou~MNx_IUMpGkq=VZjSA+rVIi+K3XQ}$jX$}kV?rLL4e+--7Uu`u$dQ=h%BSz zbG3!Qz*l7{x87F{wlz(1Li_uVEmCGNPhj0DUYDZVVF&&rMB7CNz*rqv&-{s&ow`v` zBYF*OLWQ?r<;^COMZit1*CW3v-tMPR(a-B|1Q4QmjuOe>_<^u1LAgY71!ugcZpjy? zpl^Z1Wdhz0*yg(;{KLa8_2wH@$jnfpr)8ZY>Bhv#4)nT;qnoFv#J(54$TDvy^fTt- zfG&%c=jID$c+^tdY7dB0I=>ofGy23;CbHlHGqlPz@=f2n8?}Gj2U<1D>e)s3Cbny- zuRiA(DK`(Rk%uCD#`(FTxF|eSE4Wysh$49GAzWgOn&fNr2hP}hd~QLB0dS{qm6aZQ z@tm_et?8T#&NTXjbIazz`P#9Ck+QwJZUx+64VAjsC{M6o-5j62;!xgp60v2wWK*nl zlahN$h*VtQ&tQj5 zfQXG(n0$b6QtbfntMFcM#jLJ*d^b|_b_4pecl(X^ubIBfXMjeR#G|QjgGRLg*xDjM z-IP;cEMQ3g21zpAK0Ban`DQ)7>okS{-wrGCG@$7X{2cYI9pr0*HI0QT7h|WpTrnm5 zr)(q_{})zu2g*p+rGy7W^ew(KD6KUyCeFuQglU0*EiZ9gsO;LAjc^iQ*E^O+cM1xM0U3V0(?4|(LF;64R&YCU2o~5PN52sk8lEh1obTa zAjAo>K6qB`e3`pG2GlMt(9pJOG7+mwB9-O!EB&R_{!y_)V1D^hNZ3ES(d8}^Fx|cmr$Nqikhp$&W4SP-hwGzR87zc7uG!96a#)Ek z`-*IWwCEm083h+*=$Ft9eVK@L%Pw!V{)NLPRP1QS&*anELD~!M2*hyzwrsrX$U>yLd#M=rBK_}T)u0bkP7q=#&0 zi=o<@@|_)cL883)6nV=Wie3C4cZ5nj-JN@yZc#3tFz8O%$JV!8NFsC#3*bYQlpr zQABt%%7o~q$RP|_j+XHSwI}VL*H2wO^16lDL`%3bh*#1<5s@8>QXs4P2!8gvCo{>8 zBQrY2*_SA!m8vURyglu^2#z=NXgeU!&t#tk%GnUYv3;`X=Z<8I zP%Ur~c#Y2r8a~q;szH;u-|E;H$hfobn`Lnz-0$R1-b$0Z+`H|e;8QJ$!vlFiW0km2 zb@Y%f*NtMT>(Ww8r1cR=ON)^Bstx|3GCAL5vb&m@3?F~XGBH=|`=-gl8ugTZ5^faK z&b4?#e2!AxR19-BQhAoo9ba5EKa`{whLi_)bDgn&foBauR=<@+_EMLDY77LH=)~AE=&7pd z$Eof`vwEWJ&Y2L6oONd`GqHUaU?xMM)6q`;E$fP8!5H*P=r)|Ach0*wHGr+yAG!?~ZF~+t!WJR5~gx^di!wg%XO03W$P&(tC$Ms0IQE0!r^l?*anSdkF!N z-g^(yLk|S$&6jh_-uIln&%Niqch7sj_vc!_HRoJwj=9E|qs;G{dz7&ld7Ilwg)*v3 zz0+LCKHFR0b-cpP+@-?0)$<`*Y+!YIfPX6O%wa)Z`m*@IGI#iOgyjvo@G*rTI)?On zEnOqwm|{~J|DIl|8cFce>m(LCo{G7(+2zw$XucXj&YVMh*X1sfMjZrse{Xb5V?^>n zxOt?fk-Hlp#ocQPR0B~{7Pl|8f1XUxAlBYUe_@q zjCFsbo9tX!>FvI;mt^5?7jVgxGCfB+Jm9mKY(jRSQ@Y~YQX8|j@$K&lmW3j2EAd=* zaKpKQPHdDsz8}?kzU$#QlHHk4wN^-j+{H96zs1hAR3d7k{5%5#?`DBP4-A#IQA;Xx zUVjS?_D8%E+<7&^zmRKWLY2V)3h!cg!NVol;^Hq`(E>!Imb~@#iY+m17BLfF$bAp$ zEu%b=yJqC64$fk=jF$LfUJX|^)i5tijr)eX8p`M?Nl*`5)-o0l-eZn)F`%up*^!AH zsq;8#H1N5XMCD2xkJb3vq^I{fJuEw z1~K`jBVlSCe4hdMHT=)~F!zl-XCNt9OQ?ux*KeB)q*I94*KxG>sd?D!xf?8b3d;I| zrPnNw;j=19iGO%SYBVG}xH&(nvk^GQYW3p(wVmPZq?5YFO|hH=%H8^*OF1 zjMTyV8g!h8NZTrWo9-ah>BV!06Vu=m8{f=V;?Ob8=`M@U3r!l4kfDp*XCPyXpijT( zpck9Ht-q@mWDSfRi}6SsdINspQj$D0b-7zk#+5WeTWpbQr3N6*ENwQ->4P0UIHV5e z&k@oE`F~y7qD3ekRZb-`=CV3r4S9tWm)L?tnWZ|AAlXZ_3sFav3rXSNJe8 z9TuttDHu)u#xefOLte~%Y(#%6a)R6Qoio8-LjCJ>>V%6l0%jpsA1NBY`bK7>}_ zwsnz0rdA`B2LMoP$(@ODP zk!|k3bEAM^!kJG@$7ou`W7))O*lg3eq5b{H&|`ZJybYQ6b-3Lbnd$JjiJY2pQr&5# zgWrx)Q491V^>^Kw!Ei*N5Cu{>g6A)Rzgv{PDl~)s+9d+q~wSA*O$us@89_PQ-b$P_86D+oSZ+@5s}f@ zAv~MO}Wu z&NV8`94``ROj|h{d%|O)&9kEGT+Y%gx4(a7`c`qe%$2N`pSWQilu2VWaXsJEcDQ{w^n6bDC5SfZlzs@HbLc87 zN5?4=7W0FL{wSvg^15u1WFU62dK>IF;;{{yIs`)9F0<9t&E`Y=o_~}^S~i(@_Xj<{ zUH`#>GE8I$#T0=8D88zhrAYf2aNo=XjDyns1&Dt4b(K`hA@(kl5uOhXY|F|uEco-7 zSeWNQjP@~JT6Pk`w>ePJgIA8Y04o)dt0#f_=ozgW)PFO4*{uw>hP*tO0`nAx4(0C$ z1xZ6kN@Mb5R2-k!7Hzvaw}{|Pv)`Di?w7g2o<;C-Ab4;+5Z1uU)+i`l_ubVW+ z%Pck~QYI=Kc8qxdAT{^qHDSA@NgOg&c&hYZI>>Ir;EB*`Bux^;%oXzsu<+?Nt|*v! z0kuT*opev@jel~B+1qv{l2mU6XGv(Vi-9v>&CfrXC3_7Dav>O*LTnA+HG2(lk zu|puk0Yzp)-kGDd$MNQ_J$<}zGD8MZR8lo{;S*JThEaJ zASx#M%2-yGeOwiDc_=3aT%2u zwJ9-ixXv`R%y*UPsZ7jRvjp0$APGyLzTfZMA+Dm4qGM_7V+Cf9WUq@+gt`~}c6Z*K7tMrt$rUCPH7yvf7v;1x-b_srp_+b+GN zHM@~5HC5iJVd*Ja)p3r@3F&3?r{pX5LG=7E3~)0nX`Z*OZ@~sHxrsha%85C$Fe~QO zMGV0i25&|%Hmy5I}4fH+LCRfA9{NT2vm(CXd;@W4m zsHngUdh}B6sFx#J)LVn{!q@Jxe5uSjXLl8;Lh+%cD^w*rl0~Te>P3Mt4fPe5#K|G2 zvatmCvuJyZ8)I&{lleRNOP2NUoP57t|JAiW&=UVvB>$!aOIQWU4Tu-vN~Dx1 z|Ec~F8RfXH%XBOXHXGWfvR^a!3b8_^oa zZW6xm_M+07#Z1O#(vx_j-$v+07_R=L{}k79S8kq4?d$X|TI+n}xGi?G#`84p>Qo$p zYBE!gi5c_wsb)ixPRyL#_qxMiV0NFu4rEUHHtAHQ9(ZyDybdE6F!R1WLq66JJSj|; z8yLd(VH~Px#In|9W}z8RcXR$zU~@v0Gg)?(4Va*XX{!X^$$yTsU1`6n+-bS0LyCH$ zLmt{iRps8KDAtHOwssQv-uh6Wei2qVo*2Qrcz^3T0b(Y-kuh#oJ7iB$*NTW@vighW zwhCOO2BIkBb|23e_^XGyxT23!VsWQDWn`z)&bjpfE--m%(VNq#+WR1ts?F< zPCEZC#@OFKdal&@H2fJ(o|O@kZ|zZj)eSUQDpicqi}hsM8t$E}QJjW%`OGaok3uRwBdT;Dx`L<HtB?rzrv7?9&sY|U(D>XQ;NjEZ>fSIds19_?OR zmAmY*jDbj<8rw-1DHoZ(?G=c{R8{?cT9AO58m}*!-1&#d-0c=G)WIQCY^G3ozMM0K zFP3H6ell5+rkhU{zKP4rqwBVplu=a5>y=J$C@)1-f|GXX-8v3G_i0h#s&Z`C5E_|d zCr{Kyx%rxFFQfnay`dzysIN8Hw9QZ>*e7^uoD-Rd`UQycdVJQcE_)L5PIJ&lxOx8f zpQ~5zFH7d@$SeQgB!2EH-L-4?*r@%RT<)wHbWq4RI9kV(`Mb)#603bLUjc-9lZ)%f4KhNe%}T?x=(p2otn)NwFGC>W>hVG(wLcZDJIo38w~?<%=baP=pX|BvaN zvNe&Zexc9|uEegfwq@g~e&>Uo0NP{L>?8~eX*XU0met#;noLl|6y0s<3cd!!4_=S?>AQ?41s~RIKpiluOXs_>krdEY>T_y zDwxat*lPLqD5}p&BJhsU9Gn~Nx6GE(GVqn*NEo3HaN}^Qv_ImUb)nwzOz5*!*BX>N zBrLPB3>_T-We4HO6tKtS8Z-}t)L)0!Y%EPBF38(`^l1IgqZ2#}(J{)<)3Ni@-dfR~ zSYB`h*H-4ww_wO$o`Mzq7Y@^YpU*#9551Y8j({q{D$@5%*Sx>}=RzR&9ltAfEnZ@| zTpb0b6`0_H~nq*SeVFYEZne z%st6%>WK?Gi5k}usf#Zc7N0stch70|xvrBX4+G}iB}^ifbb3kdYA>{cxd+>D!Js&< zL!G|47;i^tu9)xK~SY134JFt=mf>^Z0#Ft519L z{->YB`sU5wyW-Z`~J${7l58Ls7!?J!Z5vbl}2BCW!9X2pP<=jsC$Rb59l;2Ziz0Q}bYg1;j_vgH@Rm-SY!jO?Hm`H2 znhpbTq`I2M*8K;$%eZm?|9YEE+v(1;>~Ed-UUU`!ohR%9-?ad}IkCGfrpG5V4a3wtNH!{)UW-ZH*~J(bh!*oH=RpNrMty z!<#ax6iSmGXE1-OqjJki;f%p1Q_@)5WM(X&i?AAoS# z`~Y{~BH?c*72tBS+0$!zcLeY*Kl{*MTGby14m!TQ6eol)m&#@)>u1-V99Nu@;b`;h zMCBO+MibK{jmp}VHcS`xwl}oQ`OoPA>i7S!4gC}Qg9DG0dpQ;J1&>Qwl`o#B0vW-? z5*2-lu*tj_cZWYLs)bFSUzgO_slQLAcj$K~z?6%L1D9d$`}c$th* zW@8)u8<@Q7Y%C>MIy66C{ugGlTenqPu`*!-Hdl|XT|Ft+tXncVX&9Glhc#C?Pi-+~ zZ)hJ@pHxgb289lq{~eFvPdJJ1Fx(ES@7H2)Xk3+)Y`zWo4!C&lGr!DZFTb(7*Yx!Y z?AU2ZTkbwVqw>Io zUUdNqmp9Xg;ct)kUad5{uF<>KW8ROCIkZ2U&24KMbhNNjj?9bP#^D*Qe+$t1c}>X1 zLG@jWORbrfHR`f)!)Si`s;%%%xL8naazSFo-Dh^y57Cp&oG>YO1z(6i2YV92Pk!nU z|08xsZgJzhXV5umPFkb;7rh#(fZ89$hr|os}1zwE{yA?NR_k@L57pXWu i7wv5s8rvV4vCnX32K$oNr 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 z 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%0DH&Lh4S(lv6E{}dT<;5_uhizBVT8>fW?052qvgo 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 HM??PwVkC+G diff --git a/source/lambda/online/common_logic/common_utils/constant.py b/source/lambda/online/common_logic/common_utils/constant.py index e9f9c349..1ed41613 100644 --- a/source/lambda/online/common_logic/common_utils/constant.py +++ b/source/lambda/online/common_logic/common_utils/constant.py @@ -179,8 +179,7 @@ class IndexTag(Enum): @unique class KBType(Enum): - AOS = "aos" - + AOS = "aos" GUIDE_INTENTION_NOT_FOUND = "Intention not found, please add intentions first when using agent mode, refer to https://amzn-chn.feishu.cn/docx/HlxvduJYgoOz8CxITxXc43XWn8e" INDEX_DESC = "Answer question based on search result" diff --git a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py index d6a50d8b..325c55fd 100644 --- a/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py +++ b/source/lambda/online/common_logic/langchain_integration/chains/chat_chain.py @@ -144,7 +144,7 @@ def create_chain(cls, model_kwargs=None, **kwargs): return llm_chain -class Iternlm2Chat7BChatChain(LLMChain): +class Internlm2Chat7BChatChain(LLMChain): model_id = LLMModelType.INTERNLM2_CHAT_7B intent_type = LLMTaskType.CHAT @@ -217,7 +217,7 @@ def create_chain(cls, model_kwargs=None, **kwargs): return llm_chain -class Iternlm2Chat20BChatChain(Iternlm2Chat7BChatChain): +class Internlm2Chat20BChatChain(Internlm2Chat7BChatChain): model_id = LLMModelType.INTERNLM2_CHAT_20B diff --git a/source/lambda/online/lambda_main/main.py b/source/lambda/online/lambda_main/main.py index 1b07d96d..c04963dd 100644 --- a/source/lambda/online/lambda_main/main.py +++ b/source/lambda/online/lambda_main/main.py @@ -2,7 +2,6 @@ import traceback import uuid from datetime import datetime, timezone -import traceback import boto3 from botocore.exceptions import ClientError