Use LLMProcess implement Agent.OnMessage

This commit is contained in:
Liu Zhicong
2023-12-09 18:39:42 -08:00
parent 0708daf2ec
commit ddee31c6ab
20 changed files with 1689 additions and 116 deletions
+78 -57
View File
@@ -18,6 +18,7 @@ from ..proto.agent_task import *
from ..proto.compute_task import *
from .agent_base import *
from .llm_process import *
from .chatsession import *
from ..environment.workspace_env import WorkspaceEnvironment, TodoListType
@@ -64,6 +65,8 @@ logger = logging.getLogger(__name__)
# 我给你一段内容,尝试为期建立目录。目录的标题不能超过16个字,
# 目录要指向正文的位置(用字符偏移即可),整个目录的文本长度不能超过256个字节。并用json表达这个目录
# """
class AIAgentTemplete:
def __init__(self) -> None:
self.llm_model_name:str = "gpt-4-0613"
@@ -73,6 +76,7 @@ class AIAgentTemplete:
self.author:str = None
self.prompt:LLMPrompt = None
def load_from_config(self,config:dict) -> bool:
if config.get("llm_model_name") is not None:
self.llm_model_name = config["llm_model_name"]
@@ -87,9 +91,6 @@ class AIAgentTemplete:
return False
return True
class AIAgent(BaseAIAgent):
def __init__(self) -> None:
self.role_prompt:LLMPrompt = None
@@ -103,7 +104,6 @@ class AIAgent(BaseAIAgent):
self.enable_thread = False
self.can_do_unassigned_task = True
self.agent_id:str = None
self.template_id:str = None
self.fullname:str = None
@@ -135,7 +135,24 @@ class AIAgent(BaseAIAgent):
self.owenr_bus = None
self.enable_function_list = None
def load_from_config(self,config:dict) -> bool:
self.llm_process:Dict[str,BaseLLMProcess] = {}
async def initial(self,params:Dict = None):
self.memory = AgentMemory(self.agent_id,self.chat_db)
init_params = {}
init_params["memory"] = self.memory
for process_name in self.llm_process.keys():
init_result = await self.llm_process[process_name].initial(init_params)
if init_result is False:
logger.error(f"llm process {process_name} initial failed! initial return False")
return False
self.wake_up()
return True
async def load_from_config(self,config:dict) -> bool:
if config.get("instance_id") is None:
logger.error("agent instance_id is None!")
return False
@@ -203,8 +220,23 @@ class AIAgent(BaseAIAgent):
self.enable_timestamp = bool(config["enable_timestamp"])
if config.get("history_len"):
self.history_len = int(config.get("history_len"))
#load all LLMProcess
self.llm_process = {}
LLMProcess = config.get("LLMProcess")
for process_config_name in LLMProcess.keys():
process_config = LLMProcess[process_config_name]
real_config = {}
real_config.update(config)
real_config.update(process_config)
load_result = await LLMProcessLoader.get_instance().load_from_config(real_config)
if load_result:
self.llm_process[process_config_name] = load_result
else:
logger.error(f"load LLMProcess {process_config_name} failed!")
return False
self.wake_up()
return True
@@ -284,52 +316,14 @@ class AIAgent(BaseAIAgent):
return image_utils.to_base64(image_path, (1024, 1024))
else:
return image_path
async def _process_msg(self,msg:AgentMsg,workspace = None) -> AgentMsg:
msg_prompt = LLMPrompt()
async def llm_process_msg(self,msg:AgentMsg) -> AgentMsg:
need_process:bool = True
if msg.msg_type == AgentMsgType.TYPE_GROUPMSG:
need_process = False
if msg.is_image_msg():
image_prompt, images = msg.get_image_body()
if image_prompt is None:
content = [[{"type": "text", "text": f"{msg.sender}'s message"}]]
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}]
else:
content = [{"type": "text", "text": f"{msg.sender}:{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}]
elif msg.is_video_msg():
video_prompt, video = msg.get_video_body()
frames = video_utils.extract_frames(video, (1024, 1024))
if video_prompt is None:
content = [{"type": "text", "text": f"{msg.sender}'s message"}]
content.extend([{"type": "image_url", "image_url": {"url": frame}} for frame in frames])
msg_prompt.messages = [{"role": "user", "content": content}]
else:
content = [{"type": "text", "text": f"{msg.sender}:{video_prompt}"}]
content.extend([{"type": "image_url", "image_url": {"url": frame}} for frame in frames])
msg_prompt.messages = [{"role": "user", "content": content}]
elif msg.is_audio_msg():
prompt, audio_file = msg.get_audio_body()
resp = await ComputeKernel.get_instance().do_speech_to_text(audio_file, None, 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 is None or prompt == "":
msg.body_mime = "text/plain"
msg.body = resp.result_str
msg_prompt.messages = [{"role":"user","content":f"{msg.sender}:{resp.result_str}"}]
else:
msg.body_mime = "text/plain"
msg.body = f"{msg.sender} prompt:{prompt}\nasr response:{resp.result_str}"
msg_prompt.messages = [{"role": "user", "content": msg.body}]
else:
msg_prompt.messages = [{"role":"user","content":f"{msg.sender}:{msg.body}"}]
session_topic = msg.target + "#" + msg.topic
chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
if msg.mentions is not None:
if self.agent_id in msg.mentions:
need_process = True
@@ -339,6 +333,39 @@ class AIAgent(BaseAIAgent):
chatsession.append(msg)
resp_msg = msg.create_group_resp_msg(self.agent_id,"")
return resp_msg
input_parms = {
"msg":msg
}
msg_process = self.llm_process.get("message")
llm_result : LLMResult = await msg_process.process(input_parms)
if llm_result.state == LLMResultStates.ERROR:
error_resp = msg.create_error_resp(llm_result.error_str)
return error_resp
elif llm_result.state == LLMResultStates.IGNORE:
return None
else: # OK
resp_msg = llm_result.raw_result.get("resp_msg")
return resp_msg
async def _process_msg(self,msg:AgentMsg,workspace = None) -> AgentMsg:
msg.context_info = {}
msg.context_info["location"] = "SanJose"
msg.context_info["now"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
msg.context_info["weather"] = "Partly Cloudy, 60°F"
return await self.llm_process_msg(msg)
msg_prompt = LLMPrompt()
need_process = True
if msg.msg_type == AgentMsgType.TYPE_GROUPMSG:
need_process = False
session_topic = msg.target + "#" + msg.topic
chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
if msg.mentions is not None:
if self.agent_id in msg.mentions:
need_process = True
logger.info(f"agent {self.agent_id} recv a group chat message from {msg.sender},but is not mentioned,ignore!")
else:
if msg.is_image_msg():
image_prompt, images = msg.get_image_body()
@@ -358,20 +385,14 @@ class AIAgent(BaseAIAgent):
content.extend([{"type": "image_url", "image_url": {"url": frame}} for frame in frames])
msg_prompt.messages = [{"role": "user", "content": content}]
elif msg.is_audio_msg():
prompt, audio_file = msg.get_audio_body()
audio_file = msg.body
resp = await (ComputeKernel.get_instance().do_speech_to_text(audio_file, None, 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 is None or prompt == "":
msg.body_mime = "text/plain"
msg.body = resp.result_str
msg_prompt.messages = [{"role":"user","content":resp.result_str}]
else:
msg.body_mime = "text/plain"
msg.body = f"user prompt:{prompt}\nasr response:{resp.result_str}"
msg_prompt.messages = [{"role": "user", "content": msg.body}]
msg.body = resp.result_str
msg_prompt.messages = [{"role":"user","content":resp.result_str}]
else:
msg_prompt.messages = [{"role":"user","content":msg.body}]
session_topic = msg.get_sender() + "#" + msg.topic
+1
View File
@@ -22,6 +22,7 @@ logger = logging.getLogger(__name__)
class BaseAIAgent(abc.ABC):
@abstractmethod
def get_id(self) -> str:
+104
View File
@@ -0,0 +1,104 @@
from ast import Dict
from datetime import timedelta
from typing import List
from ..frame.compute_kernel import ComputeKernel
from ..proto.ai_function import SimpleAIOperation
from .chatsession import *
class AgentMemory:
def __init__(self,agent_id:str,db_path:str) -> None:
self.agent_id:str= agent_id
self.chat_db:str = db_path
self.model_name:str = "gp4-1106-preview"
self.threshold_hours = 72
self.actions = {}
self.init_actions()
def init_actions(self) -> Dict:
chatlog_append_op = SimpleAIOperation("chatlog_append","Append request & reply message to chatlog. No params",self.action_chatlog_append)
self.actions[chatlog_append_op.get_name()] = chatlog_append_op
def get_session_from_msg(self,msg:AgentMsg) -> AIChatSession:
if msg.msg_type == AgentMsgType.TYPE_GROUPMSG:
session_topic = msg.target + "#" + msg.topic
chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
else:
session_topic = msg.get_sender() + "#" + msg.topic
chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
return chatsession
async def load_chatlogs(self,msg:AgentMsg,n:int=6,m:int=64,token_limit=800)->str:
chatsession = self.get_session_from_msg(msg)
# 必定加载n条(n>=2),期望加载m条
# m条里的信息逐步添加,知道距离现在的时间未72小时以上,且消耗了足够的Token
messages_n = chatsession.read_history(n) # read
if len(messages_n) >= n:
messages_m = chatsession.read_history(m,n)
else:
messages_m = []
histroy_str = ""
read_count = 0
for msg in messages_n:
dt = datetime.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 <= 32:
break
read_count += 1
histroy_str = record_str + histroy_str
if len(messages_n) > 2:
if read_count < 3:
logging.warning(f"read history {read_count} < 3, will not load more")
now = datetime.datetime.now()
for msg in messages_m:
dt = datetime.datetime.fromtimestamp(float(msg.create_time))
time_diff = now - dt
if time_diff > timedelta(hours=self.threshold_hours):
break
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 <= 32:
break
read_count += 1
histroy_str = record_str + histroy_str
return histroy_str
async def action_chatlog_append(self,params:Dict) -> str:
# 使用params可以得到: LLM Process的输入,LLM Result,基于LLM Result构造的参数,当前actionItem
input_msg:AgentMsg = params.get("input").get("msg")
llm_result = params.get("llm_result")
chatsession = self.get_session_from_msg(input_msg)
resp_msg = params.get("resp_msg")
if resp_msg:
tags = llm_result.raw_result.get("tags")
chatsession.append(input_msg,tags)
chatsession.append(resp_msg,tags)
return "OK"
async def get_contact_summary(self,contact_id:str) -> str:
if contact_id is None:
return None
if contact_id == "lzc":
return "lzc is your master. Male, 40 years old, Mother tongue is Chinese, senior software engineer."
return None
def get_actions(self) -> Dict:
return self.actions
async def get_log_summary(self,msg:AgentMsg) -> str:
return None
+12 -8
View File
@@ -6,6 +6,7 @@ import threading
import datetime
import uuid
import json
from typing import List
from ..proto.agent_msg import AgentMsgType, AgentMsg, AgentMsgStatus
@@ -83,7 +84,8 @@ class ChatSessionDB:
ActionResult TEXT,
DoneTime TEXT,
Status INTEGER
Status INTEGER,
Tags TEXT
);
""")
conn.commit()
@@ -104,7 +106,7 @@ class ChatSessionDB:
logging.error("Error occurred while inserting session: %s", e)
return -1 # return -1 if an error occurs
def insert_message(self, msg:AgentMsg):
def insert_message(self, msg:AgentMsg,tags:List[str] = None):
""" insert a new message into the Messages table """
try:
action_name = None
@@ -128,13 +130,15 @@ class ChatSessionDB:
case AgentMsgType.TYPE_EVENT:
action_name = msg.event_name
action_params = json.dumps(msg.event_args)
if tags is None:
tags = []
str_tags = ','.join(tags)
conn = self._get_conn()
conn.execute("""
INSERT INTO Messages (MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status)
VALUES (?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (msg.msg_id, msg.session_id, msg.msg_type.value, msg.prev_msg_id, msg.sender, msg.target, msg.create_time, msg.topic,mentions,msg.body_mime,msg.body,action_name,action_params,action_result,msg.done_time,msg.status.value))
INSERT INTO Messages (MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status,Tags)
VALUES (?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?,?)
""", (msg.msg_id, msg.session_id, msg.msg_type.value, msg.prev_msg_id, msg.sender, msg.target, msg.create_time, msg.topic,mentions,msg.body_mime,msg.body,action_name,action_params,action_result,msg.done_time,msg.status.value,str_tags))
conn.commit()
if msg.inner_call_chain:
@@ -385,9 +389,9 @@ class AIChatSession:
result.append(agent_msg)
return result
def append(self,msg:AgentMsg) -> None:
def append(self,msg:AgentMsg,tags:List[str] = None) -> None:
msg.session_id = self.session_id
self.db.insert_message(msg)
self.db.insert_message(msg,tags)
def update_think_progress(self,progress:int,new_summary:str) -> None:
+443 -14
View File
@@ -3,34 +3,90 @@ from abc import ABC,abstractmethod
import copy
import json
import shlex
from typing import Any, Callable, Optional,Dict,Awaitable,List
from typing import Any, Callable, Coroutine, Optional,Dict,Awaitable,List
from enum import Enum
from aios.agent.chatsession import AIChatSession
from ..utils import video_utils
from ..proto.compute_task import *
from ..proto.ai_function import *
from .agent_base import *
from .agent_memory import *
from ..frame.compute_kernel import *
from ..environment.environment import *
from ..environment.workspace_env import *
import logging
logger = logging.getLogger(__name__)
MIN_PREDICT_TOKEN_LEN = 32
class BaseLLMProcess:
class LLMProcessContext:
def __init__(self) -> None:
pass
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
self.model_name = "gpt-4"
self.max_token = 2000 # include input prompt
self.max_token = 1000 # result_token
self.max_prompt_token = 1000 # not include input prompt
self.timeout = 1800 # 30 min
self.envs : Dict[str,BaseEnvironment] = []
self.env : CompositeEnvironment = None
@abstractmethod
async def prepare_prompt(self) -> LLMPrompt:
async def prepare_prompt(self,input:Dict) -> LLMPrompt:
pass
@abstractmethod
async def get_inner_function(self,func_name:str) -> AIFunction:
pass
@abstractmethod
async def exec_actions(self,actions:List[ActionItem],input:Dict,llm_result:LLMResult) -> bool:
pass
@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 append_envs(self,envs:Dict[str,BaseEnvironment]):
self.envs.update(envs)
self.env = CompositeEnvironment(self.envs)
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,prompt: LLMPrompt,stack_limit = 5) -> ComputeTaskResult:
arguments = None
try:
@@ -55,7 +111,7 @@ class BaseLLMProcess:
else:
resp_mode = "text"
max_result_token = self.max_token - ComputeKernel.llm_num_tokens(prompt)
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
@@ -67,7 +123,7 @@ class BaseLLMProcess:
resp_mode=resp_mode,
mode_name=self.model_name,
max_token=max_result_token,
inner_functions=prompt.inner_functions,
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:
@@ -94,23 +150,23 @@ class BaseLLMProcess:
else:
return task_result
async def process(self) -> LLMResult:
async def process(self,input:Dict) -> LLMResult:
if self.enable_json_resp:
resp_mode = "json"
else:
resp_mode = "text"
prompt = await self.prepare_prompt()
max_result_token = self.max_token - ComputeKernel.llm_num_tokens(prompt)
prompt = await self.prepare_prompt(input)
max_result_token = self.max_token - ComputeKernel.llm_num_tokens(prompt,self.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.model_name,
max_token=max_result_token,
inner_functions=prompt.inner_functions,
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:
@@ -136,12 +192,385 @@ class BaseLLMProcess:
else:
llm_result = LLMResult.from_str(task_result.result_str)
# execute op_list in LLM Result?
# use action to save history?
if llm_result.action_list or len(llm_result.action_list) > 0:
await self.exec_actions(llm_result.action_list,input,llm_result)
return llm_result
#class LLMProcess
class LLMAgentMessageProcess(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.known_info_tips :str = None
self.tools_tips:str = None
self.enable_inner_functions : Dict[str,bool] = None
self.enable_actions : Dict[str,AIOperation] = None
self.actions_desc : Dict[str,Dict] = None
self.workspace : WorkspaceEnvironment = None
self.memory : AgentMemory = None
self.enable_kb = False
self.kb = None
def init_actions(self):
self.enable_actions = {}
self.actions_desc = {}
self.enable_actions.update(self.memory.get_actions())
if self.workspace:
self.enable_actions.update(self.workspace.get_actions())
if self.enable_kb:
self.enable_actions.update(self.kb.get_actions())
for name,op in self.enable_actions.items():
self.actions_desc[name] = op.get_description()
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.init_actions()
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("known_info_tips"):
self.known_info_tips = config.get("known_info_tips")
if config.get("tools_tips"):
self.tools_tips = config.get("tools_tips")
if config.get("enable_kb"):
self.enable_kb = config.get("enable_kb") == "true"
if config.get("enable_function"):
self.enable_inner_functions = config.get("enable_function")
if config.get("enable_actions"):
self.enable_actions = config.get("enable_actions")
async def get_prompt_from_msg(self,msg:AgentMsg) -> LLMPrompt:
msg_prompt = LLMPrompt()
if msg.is_image_msg():
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}]
elif msg.is_video_msg():
video_prompt, video = msg.get_video_body()
frames = video_utils.extract_frames(video, (1024, 1024))
if video_prompt is None:
msg_prompt.messages = [{"role": "user", "content": [{"type": "image_url", "image_url": {"url": frame}} for frame in frames]}]
else:
content = [{"type": "text", "text": video_prompt}]
content.extend([{"type": "image_url", "image_url": {"url": frame}} for frame in frames])
msg_prompt.messages = [{"role": "user", "content": content}]
elif msg.is_audio_msg():
audio_file = msg.body
resp = await (ComputeKernel.get_instance().do_speech_to_text(audio_file, None, prompt=None, response_format="text"))
if resp.result_code != ComputeTaskResultCode.OK:
error_resp = msg.create_error_resp(resp.error_str)
return error_resp
else:
msg.body = resp.result_str
msg_prompt.messages = [{"role":"user","content":resp.result_str}]
else:
msg_prompt.messages = [{"role":"user","content":msg.body}]
return msg_prompt
async def get_action_desc(self) -> Dict:
result = {}
for name,op in self.enable_actions.items():
result[name] = op.get_description()
return result
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)->str:
## like
#sender,[2023-11-1 12:00:00]
#content
return await self.memory.load_chatlogs(msg)
async def get_log_summary(self,msg:AgentMsg)->str:
return await self.memory.get_log_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")
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 = {}
# System Prompt
## LLM的身份说明
system_prompt_dict["role_description"] = self.role_description
#prompt.append_system_message(self.role_description)
## 处理信息的流程说明
system_prompt_dict["process_rule"] = self.process_description
#prompt.append_system_message(self.process_description)
### 回复的格式
system_prompt_dict["reply_format"] = self.reply_format
#prompt.append_system_message(self.reply_format)
### 修改chatlog的action
### 修改todo/task的action
### workspace提供的额外的action
system_prompt_dict["support_actions"] = await self.get_action_desc()
#prompt.append_system_message(await self.get_action_desc())
## Context (文本替换),是否应该覆盖全部消息
context = self._format_content_by_env_value(self.context,msg.context_info)
system_prompt_dict["context"] = context
#prompt.append_system_message(context)
## 已知信息
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))
### 近期的聊天记录
chat_record = await self.load_chatlogs(msg)
if chat_record:
if len(chat_record) > 4:
known_info["chat_record"] = chat_record
#prompt.append_system_message(await self.load_chatlogs(self,msg))
### 交流总结
summary = await self.get_log_summary(msg)
if summary:
if len(summary) > 4:
known_info["summary"] = summary
#prompt.append_system_message(await self.get_log_summary(self,msg))
system_prompt_dict["known_info"] = known_info
## 可以使用的tools(inner function)的解释,注意不定义该tips,则不会导入任何workspace中的tools
if self.tools_tips:
system_prompt_dict["tools_tips"] = self.tools_tips
#prompt.append_system_message(self.tools_tips)
prompt.inner_functions.extend(self.get_inner_function_desc_from_env())
## 给予查询KB的权限
if self.enable_kb:
prompt.inner_functions.extend(self.get_inner_function_desc_from_kb())
prompt.append_system_message(json.dumps(system_prompt_dict))
## 扩展已知信息 (这可能是一个LLM过程)
prompt.append_system_message(await self.get_extend_known_info(msg,prompt))
return prompt
async def get_inner_function(self,func_name:str) -> AIFunction:
return None
async def exec_actions(self,actions:List[ActionItem],input:Dict,llm_result:LLMResult) -> bool:
msg = 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)
llm_result.raw_result["resp_msg"] = resp_msg
for action_item in actions:
op : AIOperation = self.enable_actions.get(action_item.name)
if op:
if action_item.parms is None:
action_item.parms = {}
action_item.parms["input"] = input
action_item.parms["resp_msg"] = resp_msg
action_item.parms["llm_result"] = llm_result
action_item.parms["start_at"] = datetime.now()
action_item.parms["result"] = await op.execute(action_item.parms)
action_item.parms["end_at"] = datetime.now()
else:
logger.warn(f"action {action_item.name} not found")
return False
return True
class ReviewTaskProcess(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(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
pass
class DoTodoProcess(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(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
pass
class CheckTodoProcess(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(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
pass
class SelfLearningProcess(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(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
pass
class SelfThinkingProcess(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(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
pass
class LLMProcessLoader:
def __init__(self) -> None:
self.loaders : Dict[str,Callable[[dict],Awaitable[BaseLLMProcess]]] = {}
return
@classmethod
def get_instance(cls)->"LLMProcessLoader":
if not hasattr(cls,"_instance"):
cls._instance = LLMProcessLoader()
return cls._instance
def register_loader(self, typename:str,loader:Callable[[dict],Awaitable[BaseLLMProcess]]):
self.loaders[typename] = loader
async def load_from_config(self,config:dict) -> BaseLLMProcess:
llm_type_name = config.get("type")
if llm_type_name:
loader = self.loaders.get(llm_type_name)
if loader:
return await loader(config)
selected_type = globals().get(llm_type_name)
if selected_type:
result : BaseLLMProcess = selected_type()
load_result = await result.load_from_config(config)
if load_result is False:
logger.warn(f"load LLMProcess {llm_type_name} from config failed! load_from_config return False")
return None
else:
return result
logger.warn(f"load LLMProcess {llm_type_name} from config failed! type not found")
return None
+7 -1
View File
@@ -101,4 +101,10 @@ class CompositeEnvironment(SimpleEnvironment):
self.functions[func.get_name()] = func
operations = env.get_all_ai_operations()
for op in operations:
self.operations[op.get_name()] = op
self.operations[op.get_name()] = op
def get_value(self,key:str) -> Optional[str]:
for env in self.envs:
val = env.get_value(key)
if val is not None:
return val
+5
View File
@@ -75,6 +75,11 @@ class AgentMsg:
self.inner_call_chain = []
self.resp_msg = None
self.action_list = []
#context info
self.context_info:dict= {}
@classmethod
def from_json(cls,json_obj:dict) -> 'AgentMsg':
msg = AgentMsg()
+17 -5
View File
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Dict,Coroutine,Callable
from typing import Dict,Coroutine,Callable,List
class ParameterDefine:
def __init__(self) -> None:
@@ -74,10 +74,11 @@ class AIFunction:
# pass
class ActionItem:
def __init__(self,name,args) -> None:
self.name = name
self.args = args
self.body = None
def __init__(self,name:str,args:List[str]) -> None:
self.name:str= name
self.args:List[str]= args
self.body:str = None
self.parms : Dict = None
def append_body(self,body:str) -> None:
if self.body is None:
@@ -88,6 +89,17 @@ class ActionItem:
def dumps(self) -> str:
pass
@classmethod
def from_json(cls,json_obj:dict) -> 'ActionItem':
args = json_obj.get("args",[])
r = ActionItem(json_obj["name"],args)
if json_obj.get("body"):
r.body = json_obj["body"]
r.parms = json_obj
return r
# call chain is a combination of ai_function,group of ai_function.
class CallChain:
def __init__(self) -> None:
+66 -9
View File
@@ -6,7 +6,8 @@ import shlex
import uuid
import time
from typing import List, Union
from ..proto.ai_function import *
from .ai_function import *
from .agent_msg import *
from ..knowledge import ObjectID
from ..storage.storage import AIStorage
@@ -40,20 +41,63 @@ class ComputeTaskType(Enum):
TEXT_EMBEDDING ="text_embedding"
IMAGE_EMBEDDING ="image_embedding"
# class Function(TypedDict, total=False):
# name: Required[str]
# """The name of the function to be called.
# Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length
# of 64.
# """
# parameters: Required[shared_params.FunctionParameters]
# """The parameters the functions accepts, described as a JSON Schema object.
# See the [guide](https://platform.openai.com/docs/guides/gpt/function-calling)
# for examples, and the
# [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for
# documentation about the format.
# To describe a function that accepts no parameters, provide the value
# `{"type": "object", "properties": {}}`.
# """
# description: str
# """
# A description of what the function does, used by the model to choose when and
# how to call the function.
# """
class LLMPrompt:
def __init__(self,prompt_str = None) -> None:
self.messages = []
self.messages : List[Dict] = []
if prompt_str:
self.messages.append({"role":"user","content":prompt_str})
self.system_message = None
self.system_message : Dict = None
self.inner_functions : List[Dict] = []
def append_system_message(self,content:str):
if content is None:
return
if self.system_message is None:
self.system_message = {"role":"system","content":content}
else:
self.system_message["content"] += content
def append_user_message(self,content:str):
if content is None:
return
self.messages.append({"role":"user","content":content})
def as_str(self)->str:
result_str = ""
if self.system_message:
result_str += self.system_message.get("role") + ":" + self.system_message.get("content") + "\n"
result_str = json.dumps(self.system_message)
if self.messages:
for msg in self.messages:
result_str += msg.get("role") + ":" + msg.get("content") + "\n"
result_str += json.dumps(self.messages)
if self.inner_functions:
result_str += json.dumps(self.inner_functions)
return result_str
@@ -63,10 +107,18 @@ class LLMPrompt:
result.append(self.system_message)
result.extend(self.messages)
return result
def append(self,prompt:'LLMPrompt'):
if prompt is None:
return
if prompt.inner_functions:
if self.inner_functions is None:
self.inner_functions = copy.deepcopy(prompt.inner_functions)
else:
self.inner_functions.extend(prompt.inner_functions)
if prompt.system_message is not None:
if self.system_message is None:
@@ -76,11 +128,11 @@ class LLMPrompt:
self.messages.extend(prompt.messages)
def load_from_config(self,config:list) -> bool:
def load_from_config(self,config:List[Dict]) -> bool:
if isinstance(config,list) is not True:
logger.error("prompt is not list!")
return False
self.messages = []
self.messages : List[Dict] = []
for msg in config:
if msg.get("content"):
if msg.get("role") == "system":
@@ -126,11 +178,16 @@ class LLMResult:
if llm_json_str == "**IGNORE**":
r.state = LLMResultStates.IGNORE
return r
r.state = LLMResultStates.OK
llm_json = json.loads(llm_json_str)
r.resp = llm_json.get("resp")
r.raw_result = llm_json
r.action_list = llm_json.get("actions")
action_list = llm_json.get("actions")
for action in action_list:
action_item = ActionItem.from_json(action)
r.action_list.append(action_item)
return r
+8 -3
View File
@@ -80,9 +80,14 @@ class AgentManager:
if the_agent is None:
logger.warn(f"load agent {agent_id} from media failed!")
return None
the_agent.chat_db = self.db_path
return the_agent
if await the_agent.initial():
return the_agent
else:
logger.warn(f"initial agent {agent_id} failed!")
return None
def remove(self,agent_id:str)->int:
pass
@@ -141,7 +146,7 @@ class AgentManager:
else:
init_env(owner_env)
if result_agent.load_from_config(config) is False:
if await result_agent.load_from_config(config) is False:
logger.error(f"load agent from {agent_media} failed!")
return None
return result_agent
+3
View File
@@ -0,0 +1,3 @@
[LLMAgentMessageProcess]
type="LLMAgentMessageProcess"