Workflow is almost done.

This commit is contained in:
Liu Zhicong
2023-09-01 00:39:36 -07:00
parent 27f163ddd3
commit b4990e4c57
11 changed files with 469 additions and 175 deletions
+20
View File
@@ -1,6 +1,7 @@
from enum import Enum
import uuid
import time
import re
class AgentMsgState(Enum):
RESPONSED = 0
@@ -37,3 +38,22 @@ class AgentMsg:
def get_target(self) -> str:
return self.target
@classmethod
def parse_function_call(cls,func_string:str):
match = re.search(r'\s*(\w+)\s*\(\s*(.*)\s*\)\s*', func_string)
if not match:
return None
func_name = match.group(1)
if func_name is None:
return None
if len(func_name) < 2:
return None
params_string = match.group(2).strip()
params = re.split(r'\s*,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)', params_string)
params = [param.strip('"') for param in params]
return func_name, params
+11 -1
View File
@@ -34,6 +34,7 @@ class AIBus:
self.unhandle_handler = None
async def post_message(self,target_id,msg:AgentMsg,use_unhandle=True) -> bool:
target_id = target_id.split(".")[0]
handler = self.handlers.get(target_id)
if handler:
handler.queue.put_nowait(msg)
@@ -62,6 +63,7 @@ class AIBus:
return handler.results.get(msg_id)
async def send_message(self,target_id:str,msg:AgentMsg) -> AgentMsg:
target_id = target_id.split(".")[0]
post_result = await self.post_message(target_id,msg)
if post_result is False:
return None
@@ -126,4 +128,12 @@ class AIBus:
handler.working_task = asyncio.create_task(self.process_queue(handler))
#send message to target logic:
# find target handler:
# process_message(msg):
# session = get_session(msg.sender,msg.target)
# history: open(sender,target,topic)
+2 -2
View File
@@ -237,13 +237,13 @@ class AIChatSession:
def append_post(self,msg:AgentMsg) -> None:
"""append msg to session, msg is post from session (owner => msg.target)"""
assert msg.sender == self.owner_id,"post message means msg.sender == self.owner_id"
#assert msg.sender == self.owner_id,"post message means msg.sender == self.owner_id"
self.append(msg)
def append_recv(self,msg:AgentMsg) -> None:
"""append msg to session, msg is recv from msg'sender (msg.sender => owner)"""
assert msg.target == self.owner_id,"recv message means msg.target == self.owner_id"
#assert msg.target == self.owner_id,"recv message means msg.target == self.owner_id"
self.append(msg)
#def attach_event_handler(self,handler) -> None:
+8 -2
View File
@@ -6,6 +6,7 @@ class AIRole:
def __init__(self) -> None:
self.agent_instance_id : str = None
self.role_name : str = None
self.role_id :str = None # $workflow_id.$sub_workflow_id.$role_name
self.fullname : str = None
self.agent_name : str = None
self.prompt : AgentPrompt = None
@@ -19,6 +20,7 @@ class AIRole:
return False
self.role_name = name_node
agent_id_node = config.get("agent")
if agent_id_node is None:
logging.error("agent id is not found!")
@@ -35,7 +37,10 @@ class AIRole:
intro_node = config.get("intro")
if intro_node is not None:
self.introduce = intro_node
def get_role_id(self) -> str:
return self.role_id
def get_intro(self) -> str:
return self.introduce
@@ -48,6 +53,7 @@ class AIRole:
class AIRoleGroup:
def __init__(self) -> None:
self.roles : dict[str,AIRole] = {}
self.owner_name : str = None
def load_from_config(self,config:dict) -> bool:
for k,v in config.items():
@@ -55,7 +61,7 @@ class AIRoleGroup:
if role.load_from_config(v) is False:
logging.error(f"load role {k} failed!")
return False
role.role_id = self.owner_name + "." + k
self.roles[k] = role
return True
+241 -68
View File
@@ -33,9 +33,20 @@ class MessageFilter:
return True
class LLMResult:
def __init__(self) -> None:
self.state : str = "ignore"
self.resp : str = ""
self.post_msgs = []
self.send_msgs = []
self.calls = []
self.post_calls = []
class Workflow:
def __init__(self) -> None:
self.workflow_name : str = None
self.workflow_id : str = None
self.rule_prompt : AgentPrompt = None
self.workflow_config = None
self.role_group : dict = None
@@ -62,6 +73,11 @@ class Workflow:
logger.error("workflow config must have name")
return False
self.workflow_name = config.get("name")
if self.owner_workflow is None:
self.workflow_id = self.workflow_name
else:
self.workflow_id = self.owner_workflow.workflow_id + "." + self.workflow_name
self.db_file = self.owner_workflow.db_file
#if config.get("rule_prompt") is None:
# logger.error("workflow config must have rule_prompt")
@@ -74,13 +90,14 @@ class Workflow:
logger.error("workflow config must have roles")
return False
self.role_group = AIRoleGroup()
self.role_group.owner_name = self.workflow_id
if self.role_group.load_from_config(config.get("roles")) is False:
logger.error("Workflow load role_group failed")
return False
if config.get("input_filter") is not None:
if config.get("filter") is not None:
self.input_filter = MessageFilter()
if self.input_filter.load_from_config(config.get("input_filter")) is False:
if self.input_filter.load_from_config(config.get("filter")) is False:
logger.error("Workflow load input_filter failed")
return False
@@ -98,15 +115,52 @@ class Workflow:
for k,v in config.items():
sub_workflow = Workflow()
sub_workflow.set_owner(self)
if sub_workflow.load_from_config(v) is False:
logger.error(f"load sub workflow {k} failed!")
return False
self.sub_workflows[k] = sub_workflow
return True
def _parse_msg_target(self,s:str)->list[str]:
return s.split(".")
async def _forword_msg(self,inner_obj_id,msg):
i : int = 1
current_workflow = self
while i < len(inner_obj_id):
if i == len(inner_obj_id) - 1:
the_role : AIRole = current_workflow.role_group.get(inner_obj_id[i])
if the_role is not None:
return await current_workflow.role_process_msg(msg,the_role)
sub_workflow = current_workflow.sub_workflows.get(inner_obj_id[i])
if sub_workflow is not None:
return await sub_workflow.process_msg(msg)
logger.error(f"{msg.target} not found! forword message failed!")
return None
else:
current_workflow = current_workflow.sub_workflows.get(inner_obj_id[i])
if current_workflow is None:
logger.error(f"sub workflow {inner_obj_id[i]} not found!")
return None
i += 1
logger.error(f"{msg.target} not found! forword message failed!")
return None
async def _process_msg(self,msg:AgentMsg):
# workflow can be a message handler, but never be a message sender
# all message forword to roles or sub workflow
# workflow no chatsession record, but role have
final_result = None
chatsession = None
targets = self._parse_msg_target(msg.target)
if len(targets) > 1:
return await self._forword_msg(targets,msg)
if self.input_filter is not None:
select_role_id = self.input_filter.select(msg)
if select_role_id is not None:
@@ -115,35 +169,206 @@ class Workflow:
logger.error(f"input_filter return invalid role id:{select_role_id}, role not found in role_group")
return None
result = await self._role_process_msg(msg,select_role)
result = await self.role_process_msg(msg,select_role)
if result is None:
logger.error(f"_process_msg return None for :{msg}")
return
if chatsession is not None:
chatsession.append_post(result)
final_result = result
else:
logger.error(f"input_filter return None for :{msg}")
return
else:
# no input filter, we would process all message, slowly,not suggest to use
results = {}
final_result:AgentMsg = None
for this_role in self.role_group.roles.values():
# TODO : we would do this in parallel
a_result = await self._role_process_msg(msg,this_role)
a_result = await self.role_process_msg(msg,this_role)
results[this_role.get_name()] = a_result
# merge result from all roles
# TODO: one input msg can have multiple result msg, at this while ,we only support one result msg
final_result:AgentMsg = self._merge_msg_result(results)
if chatsession is not None:
chatsession.append_post(final_result)
final_result = a_result
return final_result
@classmethod
def prase_llm_result(cls,llm_result_str:str)->LLMResult:
r = LLMResult()
if llm_result_str is None:
r.state = "ignore"
return r
if llm_result_str == "ignore":
r.state = "ignore"
return r
lines = llm_result_str.splitlines()
is_need_wait = False
for line in lines:
func_call = AgentMsg.parse_function_call(line)
if func_call:
func_args = func_call[1]
match func_call[0]:
case "sendmsg":# sendmsg($target_id,$msg_content)
if len(func_args) != 2:
logger.error(f"parse sendmsg failed! {func_call}")
continue
new_msg = AgentMsg()
target_id = func_args[0]
msg_content = func_args[1]
new_msg.set("_",target_id,msg_content)
r.send_msgs.append(new_msg)
is_need_wait = True
continue
case "postmsg":# postmsg($target_id,$msg_content)
if len(func_args) != 2:
logger.error(f"parse postmsg failed! {func_call}")
continue
new_msg = AgentMsg()
target_id = func_args[1]
msg_content = func_args[2]
new_msg.set("_",target_id,msg_content)
r.post_msgs.append(new_msg)
continue
case "call":# call($func_name,$args_str)
r.calls.append(func_call)
is_need_wait = True
continue
case "post_call": # post_call($func_name,$args_str)
r.post_calls.append(func_call)
continue
r.resp += line + "\n"
else:
r.resp += line + "\n"
if is_need_wait:
r.state = "waiting"
else:
r.state = "reponsed"
return r
async def role_post_msg(self,msg:AgentMsg,the_role:AIRole):
msg.sender = the_role.get_role_id()
target_role = self.role_group.get(msg.target)
if target_role:
msg.target = target_role.get_role_id()
logger.info(f"{msg.sender} post message {msg.id} to inner role: {msg.target}")
asyncio.create_task(self.role_process_msg(msg,target_role))
return
target_workflow = self.sub_workflows.get(msg.target)
if target_workflow:
msg.target = target_workflow.workflow_id
logger.info(f"{msg.sender} post message {msg.id} to sub workflow: {msg.target}")
asyncio.create_task(target_workflow._process_msg(msg))
logger.info(f"{msg.sender} post message {msg.id} to AIBus: {msg.target}")
await self.get_bus().post_message(msg.target,msg)
return
async def role_send_msg(self,msg:AgentMsg,the_role:AIRole):
msg.sender = the_role.get_role_id()
target_role = self.role_group.get(msg.target)
if target_role:
# msg.target = target_role.get_role_id()
logger.info(f"{msg.sender} send message {msg.id} to inner role: {msg.target}")
return await self.role_process_msg(msg,target_role)
target_workflow = self.sub_workflows.get(msg.target)
if target_workflow:
# msg.target = target_workflow.workflow_id
logger.info(f"{msg.sender} send message {msg.id} to sub workflow: {msg.target}")
return await target_workflow._process_msg(msg)
logger.info(f"{msg.sender} post message {msg.id} to AIBus: {msg.target}")
return await self.get_bus().send_message(msg.target,msg)
async def role_call(self,call:tuple,the_role:AIRole):
logger.info(f"{the_role.role_id} call {call[0]} with args {call[1]}")
return """{result:"timeout"}"""
async def role_post_call(self,call:tuple,the_role:AIRole):
logger.info(f"{the_role.role_id} post call {call[0]} with args {call[1]}")
return
async def role_process_msg(self,msg:AgentMsg,the_role:AIRole):
session_topic = f"{msg.sender}#{msg.topic}"
session_owner = the_role.get_role_id()
chatsession : AIChatSession = AIChatSession.get_session(session_owner,session_topic,self.db_file)
if chatsession is None:
logger.error(f"get session {session_topic}@{session_owner} failed!")
return None
prompt = AgentPrompt()
prompt.append(the_role.agent.prompt)
prompt.append(the_role.get_prompt())
# prompt.append(self.get_workflow_rule_prompt())
# prompt.append(self._get_function_prompt(the_role.get_name()))
# prompt.append(self._get_knowlege_prompt(the_role.get_name()))
prompt.append(await self._get_prompt_from_session(chatsession))
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":"user","content":msg.body}]
prompt.append(msg_prompt)
async def _do_process_msg():
#TODO: send msg to agent might be better?
result_str = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
result = Workflow.prase_llm_result(result_str)
for postmsg in result.post_msgs:
postmsg.topic = msg.topic
await self.role_post_msg(the_role,postmsg)
for post_call in result.post_calls:
await self.role_post_call(post_call,the_role)
result_prompt_str = ""
match result.state:
case "ignore":
return None
case "reponsed":
resp_msg = AgentMsg()
resp_msg.topic = msg.topic
resp_msg.set(session_owner,msg.sender,result.resp)
chatsession.append_recv(msg)
chatsession.append_post(resp_msg)
return resp_msg
case "waiting":
# TODO: Use role:"function" would be better
for sendmsg in result.send_msgs:
target = sendmsg.target
sendmsg.topic = msg.topic
send_resp = await self.role_send_msg(sendmsg,the_role)
if send_resp is not None:
result_prompt_str += f"\n{target} response is :{send_resp.body}"
for call in result.calls:
call_result = await self.role_call(call,the_role)
if call_result is not None:
result_prompt_str += f"\ncall {call[0]} result is :{call_result}"
result_prompt = AgentPrompt()
result_prompt.messages = [{"role":"user","content":result_prompt_str}]
prompt.append(result_prompt)
r = await _do_process_msg()
return r
return await _do_process_msg()
#obsolete
async def _role_process_msg(self,msg:AgentMsg,the_role:AIRole) -> None:
# TODO : we just record role's chatsession, but in future, we would record workflow's chatsession(like a groupo chat)
session_topic = f"{the_role.get_name()}#{msg.sender}#{msg.topic}"
@@ -162,7 +387,6 @@ class Workflow:
# prompt.append(self._get_knowlege_prompt(the_role.get_name()))
prompt.append(await self._get_prompt_from_session(chatsession))
#prompt.append(await self._get_prompt_from_session(chatsession,the_role.get_name())) # chat context
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":"user","content":msg.body}]
@@ -180,7 +404,7 @@ class Workflow:
resp = await callchain.exec()
if callchain.have_result():
# generator proc resp prompt with WAITING state
#proc_resp_prompt:AgentPrompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession)
proc_resp_prompt:AgentPrompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession)
final_result = await ComputeKernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
return final_result
@@ -218,31 +442,18 @@ class Workflow:
chatsession.append_post(resp_msg)
return resp_msg
async def _pop_msg(self) -> AgentMsg:
pass
def _get_chat_session_for_msg(self,msg:AgentMsg) -> AIChatSession:
pass
async def _get_prompt_from_session(self,chatsession:AIChatSession) -> AgentPrompt:
messages = chatsession.read_history() # read last 10 message
result_prompt = AgentPrompt()
for msg in reversed(messages):
if msg.target == chatsession.owner_id:
result_prompt.messages.append({"role":"user","content":f"{msg.sender}:{msg.body}"})
if msg.sender == chatsession.owner_id:
result_prompt.messages.append({"role":"assistant","content":msg.body})
else:
result_prompt.messages.append({"role":"user","content":f"{msg.body}"})
return result_prompt
def _get_msg_queue(self,session_id:str):
pass
def _merge_msg_result(self,results:dict) -> AgentMsg:
# TODO: one input msg can have multiple result msg, at this while ,we only support one result msg
for k,v in results.items():
if v is not None:
return v
def _get_function_prompt(self,role_name:str) -> AgentPrompt:
pass
@@ -250,48 +461,10 @@ class Workflow:
def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt:
pass
def _get_resp_prompt(self,resp:str,msg:AgentMsg,role:AIRole,prompt:AgentPrompt) -> AgentPrompt:
pass
def get_workflow_rule_prompt(self) -> AgentPrompt:
return self.rule_prompt
def _get_llm_result_type(self,llm_resp_str:str) -> str:
if llm_resp_str == "ignore":
return "ignore"
if llm_resp_str.find("sendmsg(") != -1:
return "send_message"
if llm_resp_str.find("postmsg(") != -1:
return "post_message"
if llm_resp_str.find("call(") != -1:
return "function"
return "text"
def _parse_function_call_chain(self,llm_resp_str) -> CallChain:
pass
def _parse_to_msg(self,llm_resp_str) -> AgentMsg:
lines = llm_resp_str.splitlines()
for line in lines:
if line.startswith("sendmsg("):
line = line[8:]
_index = line.find(",")
msg = AgentMsg()
msg.set("",line[:_index],line[_index+1:])
return msg
if line.startswith("postmsg("):
line = line[8:]
_index = line.find(",")
msg = AgentMsg()
msg.set("",line[:_index],line[_index+1:])
return msg
return None
def get_workflow(self,workflow_name:str):
"""get workflow from known workflow list or sub workflow list"""
@@ -25,6 +25,20 @@ class WorkflowManager:
async def get_agent_default_workflow(self,agent_id:str) -> Workflow:
pass
async def _load_workflow_agents(self,workflow:Workflow) -> bool:
for v in workflow.role_group.roles.values():
agent = await AgentManager().get(v.agent_name)
if agent is None:
logger.error(f"load agent {v.agent_name} failed!")
return False
v.agent = agent
for sub_workflow in workflow.sub_workflows.values():
if await self._load_workflow_agents(sub_workflow) is False:
return False
return True
async def get_workflow(self,workflow_id:str) -> Workflow:
the_workflow : Workflow = self.loaded_workflow.get(workflow_id)
if the_workflow:
@@ -39,14 +53,9 @@ class WorkflowManager:
if the_workflow is None:
logger.warn(f"load workflow {workflow_id} from media failed!")
for v in the_workflow.role_group.roles.values():
agent = await AgentManager().get(v.agent_name)
if agent is None:
logger.error(f"load agent {v.agent_name} failed!")
return None
v.agent = agent
if await self._load_workflow_agents(the_workflow) is False:
return None
return the_workflow
async def _load_workflow_from_media(self,workflow_media:PackageMediaInfo) -> Workflow:
@@ -64,10 +73,12 @@ class WorkflowManager:
config_data = await config_file.read()
config = toml.loads(config_data)
result_workflow = Workflow()
result_workflow.db_file = self.db_file
if result_workflow.load_from_config(config) is False:
logger.error(f"load workflow from {workflow_media} failed!")
return None
result_workflow.db_file = self.db_file
return result_workflow
except Exception as e:
logger.error(f"read workflow.toml cfg from {workflow_media} failed! unexpected error occurred: {str(e)}")
+41 -35
View File
@@ -41,16 +41,15 @@ class AIOS_Shell:
self.current_topic = "default"
async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool:
agent : AIAgent = await AgentManager().get(msg.target)
target_id = msg.target.split(".")[0]
agent : AIAgent = await AgentManager().get(target_id)
if agent is not None:
bus.register_message_handler(msg.target,agent._process_msg)
bus.register_message_handler(target_id,agent._process_msg)
return True
a_workflow = await WorkflowManager().get_workflow(msg.target)
a_workflow = await WorkflowManager().get_workflow(target_id)
if a_workflow is not None:
bus.register_message_handler(msg.target,a_workflow._process_msg)
for subflow in a_workflow.sub_workflows.values():
bus.register_message_handler(subflow.workflow_name,subflow._process_msg)
bus.register_message_handler(target_id,a_workflow._process_msg)
return True
return False
@@ -79,7 +78,7 @@ class AIOS_Shell:
agent_msg = AgentMsg()
agent_msg.set(sender,target_id,msg)
agent_msg.topic = topic
resp = await AIBus().get_default_bus().send_message(target_id,agent_msg)
resp = await AIBus.get_default_bus().send_message(target_id,agent_msg)
if resp is not None:
return resp.body
else:
@@ -111,10 +110,11 @@ class AIOS_Shell:
case 'history':
num = 10
offset = 0
if len(args) >= 1:
num = args[0]
if len(args) >= 2:
offset = args[1]
if args is not None:
if len(args) >= 1:
num = args[0]
if len(args) >= 2:
offset = args[1]
db_path = ""
if await self.is_agent(self.current_target):
@@ -140,37 +140,43 @@ class AIOS_Shell:
history = FileHistory('history.txt')
session = PromptSession(history=history)
def parse_function_call(s):
match = re.match(r'(\w+)\((.*)\)$', s)
if match:
func_name = match.group(1)
args_str = match.group(2)
args = []
buffer = ''
quote_count = 0 # Count of single or double quotes
for char in args_str:
if char in ['"', "'"]:
quote_count += 1
if char == ',' and quote_count % 2 == 0: # ',' is outside of quotes
args.append(buffer.strip())
buffer = ''
else:
buffer += char
if buffer:
args.append(buffer.strip())
return func_name, args
else:
def parse_function_call(func_string):
match = re.search(r'\s*(\w+)\s*\(\s*(.*)\s*\)\s*', func_string)
if not match:
return None
func_name = match.group(1)
params_string = match.group(2).strip()
params = re.split(r'\s*,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)', params_string)
params = [param.strip('"') for param in params]
if len(params[0]) == 0:
params = None
return func_name, params
async def main():
print("aios shell prepareing...")
logging.basicConfig(filename="aios_shell.log",filemode="w",level=logging.INFO,format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s')
logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8',
level=logging.INFO,
format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s')
shell = AIOS_Shell("user")
await shell.initial()
s = """了解。首先,我们需要进行方案讨论,定出一致的活动策略。我将任务分解如下:
财务组: 分析可见的预算,提供一份合理并被执行的财务方案。
行程预订组: 硅谷自然迷人的地方众多,寻找适合11人秋游的地点,以及一个可行的周末日期。策划一个一天或两天的安排,包括选择有南瓜园的地方,采摘苹果,参观当地的酗酒作坊等秋游活动。
嘉宾对接组: 把个人的食品饮料或过敏食物的需求事先了解并计入行程内。
酒店预订组:根据行程预订组的日期安排,活动时间在1天还是2天,在这个之内找一个合适的住宿,试着保持让住宿在预算边界以内。
活动摄像组: 准备活动拍摄与剪辑方案。\n\n然后,我将把这些拆分后的任务发来小组。
sendmsg(财务组,分析预算并提供一份财务方案.)
sendmsg(行程预订组,找出适合秋游的地方和日期.)
sendmsg(嘉宾对接组,了解个人的饮食需求.)
sendmsg(酒店预订组,查询并预订住宿.)
sendmsg(活动摄像组,提供活动拍摄方案.)
经过一两天的准备,一切就绪之后,我将向工作人员发送最后的行程计划,
"""
r = Workflow.prase_llm_result(s)
print(f"aios shell {shell.get_version()} ready.")
completer = WordCompleter(['send($target,$msg,$topic)',