# Old name is behavior, I belive new name "llm_process" is better # pylint:disable=E0402 import os.path from .chatsession import AIChatSession from ..utils import video_utils,image_utils from ..proto.compute_task import LLMPrompt,LLMResult,ComputeTaskResult,ComputeTaskResultCode from ..proto.ai_function import AIFunction,AIAction,ActionNode from ..proto.agent_msg import AgentMsg,AgentMsgType from .agent_memory import AgentMemory from .workspace import AgentWorkspace from .llm_context import LLMProcessContext,GlobaToolsLibrary, SimpleLLMContext from ..frame.compute_kernel import ComputeKernel from ..knowledge.knowledge_base import BaseKnowledgeGraph from abc import ABC,abstractmethod import copy import json import datetime from datetime import datetime from typing import Any, Callable, Coroutine, Optional,Dict,Awaitable,List from enum import Enum import logging logger = logging.getLogger(__name__) MIN_PREDICT_TOKEN_LEN = 32 class BaseLLMProcess(ABC): def __init__(self) -> None: self.behavior:str = None #行为名字 self.goal:str = None #目标 self.input_example:str= None #输入样例 self.result_example:str = None #llm_result样例 self.enable_json_resp = False #None means system default, # TODO: support abcstract model name like: local-hight,local-low,local-medium,remote-hight,remote-low,remote-medium self.model_name = None self.max_token = 2000 # result_token self.max_prompt_token = 2000 # not include input prompt self.chat_summary_token_len = 500 self.timeout = 1800 # 30 min self.llm_context:LLMProcessContext = None def get_llm_model_name(self) -> str: return self.model_name @abstractmethod async def prepare_prompt(self,input:Dict) -> LLMPrompt: pass @abstractmethod async def get_inner_function_for_exec(self,func_name:str) -> AIFunction: pass @abstractmethod def prepare_inner_function_context_for_exec(self,inner_func_name:str,parameters:Dict): return @abstractmethod async def post_llm_process(self,actions:List[ActionNode],input:Dict,llm_result:LLMResult) -> bool: pass def get_remain_prompt_length(self,prompt:LLMPrompt,will_append_str:str) -> int: return self.max_prompt_token - ComputeKernel.llm_num_tokens(prompt,self.model_name) @abstractmethod async def load_from_config(self,config:dict) -> bool: #self.behavior = config.get("behavior") #self.goal = config.get("goal") self.input_example = config.get("input_example") self.result_example = config.get("result_example") if config.get("model_name"): self.model_name = config.get("model_name") if config.get("enable_json_resp"): self.enable_json_resp = config.get("enable_json_resp") == "true" if config.get("max_token"): self.max_token = config.get("max_token") if config.get("timeout"): self.timeout = config.get("timeout") return True @abstractmethod async def initial(self,params:Dict = None) -> bool: pass def _format_content_by_env_value(self,content:str,env)->str: return content.format_map(env) async def _execute_inner_func(self,inner_func_call_node:Dict,prompt: LLMPrompt,stack_limit = 1) -> ComputeTaskResult: arguments = None stack_limit = stack_limit - 1 try: func_name = inner_func_call_node.get("name") arguments = json.loads(inner_func_call_node.get("arguments")) logger.info(f"LLMProcess execute inner func:{func_name} :({json.dumps(arguments,ensure_ascii=False)})") func_node : AIFunction = await self.get_inner_function_for_exec(func_name) if func_node is None: result_str:str = f"execute {func_name} error,function not found" else: self.prepare_inner_function_context_for_exec(func_name,arguments) result_str:str = await func_node.execute(arguments) except Exception as e: result_str = f"execute {func_name} error:{str(e)}" logger.error(f"LLMProcess execute inner func:{func_name} error:\n\t{e}") logger.info("LLMProcess execute inner func result:" + result_str) prompt.messages.append({"role":"function","content":result_str,"name":func_name}) if self.enable_json_resp: resp_mode = "json" else: resp_mode = "text" max_result_token = self.max_token - ComputeKernel.llm_num_tokens(prompt,self.model_name) if max_result_token < MIN_PREDICT_TOKEN_LEN: task_result = ComputeTaskResult() task_result.result_code = ComputeTaskResultCode.ERROR task_result.error_str = f"prompt too long,can not predict" return task_result if stack_limit > 0: inner_functions=prompt.inner_functions else: inner_functions = None task_result: ComputeTaskResult = await (ComputeKernel.get_instance().do_llm_completion( prompt, resp_mode=resp_mode, mode_name=self.get_llm_model_name(), max_token=max_result_token, inner_functions=inner_functions, #NOTICE: inner_function in prompt can be a subset of get_inner_function timeout=self.timeout)) if task_result.result_code != ComputeTaskResultCode.OK: logger.error(f"llm compute error:{task_result.error_str}") return task_result inner_func_call_node = None result_message : dict = task_result.result.get("message") if result_message: inner_func_call_node = result_message.get("function_call") if inner_func_call_node: func_msg = copy.deepcopy(result_message) del func_msg["tool_calls"]#TODO: support tool_calls? prompt.messages.append(func_msg) if inner_func_call_node: return await self._execute_inner_func(inner_func_call_node,prompt,stack_limit-1) else: return task_result async def process(self,input:Dict) -> LLMResult: if self.enable_json_resp: resp_mode = "json" else: resp_mode = "text" # Action define in prompt, will be execute after llm compute prompt = await self.prepare_prompt(input) if prompt is None: logger.warn(f"prepare_prompt return None, break llm_process") return LLMResult.from_error_str("prepare_prompt return None") max_result_token = self.max_token - ComputeKernel.llm_num_tokens(prompt,self.get_llm_model_name()) #if max_result_token < MIN_PREDICT_TOKEN_LEN: # return LLMResult.from_error_str(f"prompt too long,can not predict") task_result: ComputeTaskResult = await (ComputeKernel.get_instance().do_llm_completion( prompt, resp_mode=resp_mode, mode_name=self.get_llm_model_name(), max_token=max_result_token, inner_functions=prompt.inner_functions, #NOTICE: inner_function in prompt can be a subset of get_inner_function timeout=self.timeout)) if task_result.result_code != ComputeTaskResultCode.OK: err_str = f"do_llm_completion error:{task_result.error_str}" logger.error(err_str) return LLMResult.from_error_str(err_str) result_message = task_result.result.get("message") inner_func_call_node = None if result_message: inner_func_call_node = result_message.get("function_call") if inner_func_call_node: call_prompt : LLMPrompt = copy.deepcopy(prompt) func_msg = copy.deepcopy(result_message) del func_msg["tool_calls"] call_prompt.messages.append(func_msg) task_result = await self._execute_inner_func(inner_func_call_node,call_prompt) # parse task_result to LLM Result if self.enable_json_resp: try: llm_result = LLMResult.from_json_str(task_result.result_str) except Exception as e: logger.error(f"parse llm result error:{e}") llm_result = LLMResult.from_str(task_result.result_str) else: llm_result = LLMResult.from_str(task_result.result_str) # use action to save history? await self.post_llm_process(llm_result.action_list,input,llm_result) return llm_result class LLMAgentBaseProcess(BaseLLMProcess): def __init__(self) -> None: super().__init__() self.role_description:str = None self.process_description:str = None self.reply_format:str = None self.context : str = None self.workspace : AgentWorkspace = None # If Workspace is not none , enable Agent Tasklist self.memory : AgentMemory = None self.enable_kb_list : List[str] = None async def initial(self,params:Dict = None) -> bool: self.memory = params.get("memory") if self.memory is None: logger.error(f"LLMAgeMessageProcess initial failed! memory not found") return False self.workspace = params.get("workspace") return True async def load_default_config(self) -> bool: return True async def load_from_config(self, config: dict,is_load_default=True) -> Coroutine[Any, Any, bool]: if is_load_default: await self.load_default_config() if await super().load_from_config(config) is False: return False self.role_description = config.get("role_desc") if self.role_description is None: logger.error(f"role_description not found in config") return False if config.get("process_description"): self.process_description = config.get("process_description") if config.get("reply_format"): self.reply_format = config.get("reply_format") if config.get("context"): self.context = config.get("context") if config.get("knowledge_grpah_introduce"): self.knowledge_grpah_introduce = config.get("knowledge_grpah_introduce") self.llm_context = SimpleLLMContext() if config.get("llm_context"): self.llm_context.load_from_config(config.get("llm_context")) def prepare_knowledge_grpah_prompt(self) -> Dict: result = {} result["introduce"] = BaseKnowledgeGraph.get_kb_default_desc_str() result["knowledge_graph_list"] = {} have_kb = False if self.memory.enable_knowledge_graph: result["knowledge_graph_list"][self.memory.knowledge_graph.kb_id] = self.memory.knowledge_graph.get_description() have_kb = True if self.enable_kb_list: for kb_id in self.enable_kb_list: kb = BaseKnowledgeGraph.get_kb(kb_id) if kb: have_kb = True result["knowledge_graph_list"][kb_id] = kb.get_description() else: logger.error(f"knowledge base {kb_id} not found") if have_kb is False: return None return result def prepare_role_system_prompt(self,context_info:Dict) -> Dict: system_prompt_dict = {} system_prompt_dict["role_description"] = self.role_description system_prompt_dict["process_rule"] = self.process_description system_prompt_dict["reply_format"] = self.reply_format kb_prompt = self.prepare_knowledge_grpah_prompt() if kb_prompt: system_prompt_dict["knowledge_graph"] = kb_prompt ## Context if self.context: context = self._format_content_by_env_value(self.context,context_info) system_prompt_dict["context"] = context #prompt.append_system_message(context) system_prompt_dict["support_actions"] = self.get_action_desc() return system_prompt_dict def prepare_inner_function_context_for_exec(self,inner_func_name:str,parameters:Dict): parameters["_workspace"] = self.workspace def get_action_desc(self) -> Dict: result = {} actions_list = [] actions_list.extend(self.llm_context.get_all_ai_action()) for action in actions_list: result[action.get_name()] = action.get_description() return result async def get_inner_function_for_exec(self,func_name:str) -> AIFunction: return self.llm_context.get_ai_function(func_name) async def _execute_actions(self,actions:List[ActionNode],action_params:Dict): for action_item in actions: op : AIAction = self.llm_context.get_ai_action(action_item.name) if op: if action_item.parms is None: action_item.parms = {} real_parms = {**action_params,**action_item.parms} action_item.parms["_result"] = await op.execute(real_parms) action_item.parms["_end_at"] = datetime.now() else: logger.warn(f"action {action_item.name} not found") return False class AgentMessageProcess(LLMAgentBaseProcess): def __init__(self) -> None: super().__init__() self.mutil_model = None self.enable_media2text = False self.is_mutil_model = False self.asr_model = None self.tts_model = None async def load_default_config(self) -> bool: return True async def load_from_config(self, config: dict,is_load_default=True) -> Coroutine[Any, Any, bool]: if is_load_default: await self.load_default_config() if await super().load_from_config(config) is False: return False self.enable_media2text = config.get('enable_media2text', 'false').lower() in ('true', '1', 't', 'y', 'yes') if config.get("mutil_model"): self.mutil_model = config.get("mutil_model") self.asr_model = config.get("asr_model") self.tts_model = config.get("tts_model") def get_llm_model_name(self) -> str: if self.is_mutil_model: return self.mutil_model else: return self.model_name def check_and_to_base64(self, image_path: str) -> str: if image_utils.is_file(image_path): return image_utils.to_base64(image_path, (1024, 1024)) else: return image_path async def get_prompt_from_msg(self,msg:AgentMsg) -> LLMPrompt: msg_prompt = LLMPrompt() self.is_mutil_model = False if msg.is_image_msg(): if self.enable_media2text: logger.error(f"enable_media2text is not supported yet") else: image_prompt, images = msg.get_image_body() if image_prompt is None: msg_prompt.messages = [{"role": "user", "content": [{"type": "image_url", "image_url": {"url": self.check_and_to_base64(image)}} for image in images]}] else: content = [{"type": "text", "text": image_prompt}] content.extend([{"type": "image_url", "image_url": {"url": self.check_and_to_base64(image)}} for image in images]) msg_prompt.messages = [{"role": "user", "content": content}] if self.mutil_model: self.is_mutil_model = True else: logger.warning(f"mutil_model is not set!") elif msg.is_video_msg(): if self.enable_media2text: logger.error(f"enable_media2text is not supported yet") else: video_prompt, video = msg.get_video_body() frames = video_utils.extract_frames(video, (1024, 1024)) audio_file = os.path.splitext(video)[0] + ".mp3" video_utils.extract_audio(video, audio_file) voice_content = None if self.asr_model is not None: resp = await (ComputeKernel.get_instance().do_speech_to_text(audio_file, model=self.asr_model, prompt=None, response_format="text")) if resp.result_code == ComputeTaskResultCode.OK: voice_content = resp.result_str content = [] if video_prompt is not None: content.append({"type": "text", "text": video_prompt}) if voice_content is not None and voice_content != "": content.append({"type": "text", "text": f"Voice content in video:{voice_content}"}) content.extend([{"type": "image_url", "image_url": {"url": frame}} for frame in frames]) msg_prompt.messages = [{"role": "user", "content": content}] if self.mutil_model: self.is_mutil_model = True else: logger.warning(f"mutil_model is not set!") elif msg.is_audio_msg(): if self.enable_media2text: logger.error(f"enable_media2text is not supported yet") else: prompt, audio_file = msg.get_audio_body() resp = await (ComputeKernel.get_instance().do_speech_to_text(audio_file, model=self.asr_model, prompt=None, response_format="text")) if resp.result_code != ComputeTaskResultCode.OK: error_resp = msg.create_error_resp(resp.error_str) return error_resp else: if prompt == "": msg.body = resp.result_str msg_prompt.messages = [{"role":"user","content":resp.result_str}] else: msg.body = f"{prompt}\nVoice content:{resp.result_str}" msg_prompt.messages = [{"role":"user","content": prompt}, {"role": "user", "content": f"Voice content:{resp.result_str}"}] else: msg_prompt.messages = [{"role":"user","content":msg.body}] return msg_prompt async def sender_info(self,msg:AgentMsg)->str: sender_id = msg.sender #TODO Is sender an agent? return await self.memory.get_contact_summary(sender_id) async def load_chatlogs(self,msg:AgentMsg,max_length_by_token:int)->str: ## like #sender,[2023-11-1 12:00:00] #content return await self.memory.load_chatlogs(msg,max_length_by_token) async def get_chat_summary(self,msg:AgentMsg)->str: return await self.memory.get_chat_summary(msg) async def get_extend_known_info(self,msg:AgentMsg,prompt:LLMPrompt)->str: return None async def prepare_prompt(self,input:Dict) -> LLMPrompt: prompt = LLMPrompt() # User Prompt ## Input Msg msg : AgentMsg = input.get("msg") context_info = input.get("context_info") if msg is None: logger.error(f"LLMAgeMessageProcess prepare_prompt failed! input msg not found") return None msg_prompt = await self.get_prompt_from_msg(msg) if msg_prompt is None: logger.error(f"LLMAgeMessageProcess prepare_prompt failed! get_prompt_from_msg return None") return None prompt.append(msg_prompt) ## 通用的角色相关的系统提示词 system_prompt_dict = self.prepare_role_system_prompt(context_info) ## 已知信息 known_info = {} #prompt.append_system_message(self.known_info_tips) ### 信息发送者资料 known_info["sender_info"] = await self.sender_info(msg) #prompt.append_system_message(await self.sender_info(self,msg)) system_prompt_dict["known_info"] = known_info prompt.inner_functions =LLMProcessContext.aifunctions_to_inner_functions(self.llm_context.get_all_ai_functions()) if self.workspace: #TODO eanble workspace functions? logger.info(f"workspace is not none,enable workspace functions") ### 根据Token Limit加载聊天记录 remain_token = self.get_remain_prompt_length(prompt,json.dumps(system_prompt_dict,ensure_ascii=False)) chat_record,is_all = await self.load_chatlogs(msg,remain_token - self.chat_summary_token_len) if chat_record: if len(chat_record) > 4: known_info["chat_record"] = chat_record if not is_all : ### 如果出触发了Token Limit,则删除几条信息后,加载summary (summary的长度基本是固定的) summary = await self.get_chat_summary(msg) if summary: if len(summary) > 4: known_info["chat_summary"] = summary # TODO: extend known info #prompt.append_system_message(await self.get_extend_known_info(msg,prompt)) prompt.append_system_message(json.dumps(system_prompt_dict,ensure_ascii=False)) return prompt async def post_llm_process(self,actions:List[ActionNode],input:Dict,llm_result:LLMResult) -> bool: msg:AgentMsg = input.get("msg") if msg.msg_type == AgentMsgType.TYPE_GROUPMSG: resp_msg = msg.create_group_resp_msg(self.memory.agent_id,llm_result.resp) else: resp_msg = msg.create_resp_msg(llm_result.resp) if llm_result.raw_result is not None: llm_result.raw_result["_resp_msg"] = resp_msg action_params = {} action_params["_input"] = input action_params["_memory"] = self.memory action_params["_workspace"] = self.workspace action_params["_resp_msg"] = resp_msg action_params["_llm_result"] = llm_result action_params["_agentid"] = self.memory.agent_id action_params["_start_at"] = datetime.now() await self._execute_actions(actions,action_params) chatsession = self.memory.get_session_from_msg(msg) chatsession.append(msg) chatsession.append(resp_msg) return True class AgentSelfThinking(LLMAgentBaseProcess): def __init__(self) -> None: super().__init__() async def load_from_config(self, config: dict) -> Coroutine[Any, Any, bool]: if await super().load_from_config(config) is False: return False async def _load_chat_history(self,token_limit:int): chat_history = {} session_list = AIChatSession.list_session(self.memory.agent_id ,self.memory.memory_db) total_read_msg = 0 for session_id in session_list: chatsession = AIChatSession.get_session_by_id(session_id,self.memory.memory_db) session_history = {} session_history["summary"] = chatsession.summary session_history["id"] = chatsession.session_id token_limit -= ComputeKernel.llm_num_tokens_from_text(chatsession.summary,self.model_name) read_history_msg = 0 if token_limit > 8: # load session chat history cur_pos = chatsession.summarize_pos messages = chatsession.read_history(0,cur_pos,"natural") # read history_str = "" for msg in messages: read_history_msg += 1 total_read_msg += 1 cur_pos += 1 dt = datetime.fromtimestamp(float(msg.create_time)) formatted_time = dt.strftime('%y-%m-%d %H:%M:%S') record_str = f"{msg.sender},[{formatted_time}]\n{msg.body}\n" token_limit -= ComputeKernel.llm_num_tokens_from_text(record_str,self.model_name) if token_limit < 8: break history_str = history_str + record_str if ComputeKernel.llm_num_tokens_from_text(history_str,self.model_name) > self.chat_summary_token_len: session_history["history"] = history_str chat_history[session_id] = session_history chatsession.summarize_pos = cur_pos else: logger.info(f"load_chat_history reach token limit,load {total_read_msg} history messages.") return chat_history if total_read_msg < 2: logger.info(f"load_chat_history: no history messages,return NONE") return None return chat_history async def prepare_prompt(self,input:Dict) -> LLMPrompt: prompt = LLMPrompt() context_info = input.get("context_info") system_prompt_dict = self.prepare_role_system_prompt(context_info) # Known_info is the SESSION summary of the existence, the current task work record summary, token_remain = self.get_remain_prompt_length(prompt,json.dumps(system_prompt_dict,ensure_ascii=False)) chat_history = await self._load_chat_history(token_remain) if chat_history is None: logger.info(f"prepare_prompt: no history messages,return NONE") return None prompt.inner_functions =LLMProcessContext.aifunctions_to_inner_functions(self.llm_context.get_all_ai_functions()) prompt.append_system_message(json.dumps(system_prompt_dict,ensure_ascii=False)) prompt.append_user_message(json.dumps(chat_history,ensure_ascii=False)) return prompt async def post_llm_process(self,actions:List[ActionNode],input:Dict,llm_result:LLMResult) -> bool: action_params = {} action_params["_input"] = input action_params["_memory"] = self.memory action_params["_workspace"] = self.workspace action_params["_llm_result"] = llm_result action_params["_agentid"] = self.memory.agent_id action_params["_start_at"] = datetime.now() try: if await self._execute_actions(actions,action_params) is False: result_str = "execute action failed!" except Exception as e: logger.error(f"execute action failed! {e}") result_str = "execute action failed!,error:" + str(e) class AgentSelfLearning(BaseLLMProcess): def __init__(self) -> None: super().__init__() async def load_from_config(self, config: dict) -> Coroutine[Any, Any, bool]: if await super().load_from_config(config) is False: return False async def prepare_prompt(self) -> LLMPrompt: prompt = LLMPrompt() pass async def get_inner_function_for_exec(self,func_name:str) -> AIFunction: pass async def post_llm_process(self,actions:List[ActionNode]) -> bool: pass class AgentSelfImprove(BaseLLMProcess): def __init__(self) -> None: super().__init__()