From a6f03ff62a2a7db1a01850ba5672e8492aca8a33 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Thu, 31 Aug 2023 16:14:13 +0800 Subject: [PATCH 01/21] Add a service: email spider #43 --- .gitignore | 2 + rootfs/email/config.toml | 7 ++ src/service/spider/email_spider.py | 109 +++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 rootfs/email/config.toml create mode 100644 src/service/spider/email_spider.py diff --git a/.gitignore b/.gitignore index 4ef429f..84349bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .vscode/ *.pyc +rootfs/email/config.local.toml +rootfs/data diff --git a/rootfs/email/config.toml b/rootfs/email/config.toml new file mode 100644 index 0000000..1bf4bca --- /dev/null +++ b/rootfs/email/config.toml @@ -0,0 +1,7 @@ + + +EMAIL_IMAP_SERVER = "imap.gmail.com" +EMAIL_ADDRESS = '<>' +EMAIL_PASSWORD = '<>' +EMAIL_IMAP_PORT = 993 +LOCAL_DIR = 'rootfs/data' \ No newline at end of file diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py new file mode 100644 index 0000000..ae6e484 --- /dev/null +++ b/src/service/spider/email_spider.py @@ -0,0 +1,109 @@ +import imaplib +import os +import toml +import logging +import mailparser +import hashlib +import json + +# logger config +logger = logging.getLogger('email spider') +logger.setLevel(logging.DEBUG) +ch = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s [%(name)s] [%(levelname)s] %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) + +# read config from toml file +# and read from config config.local.toml if exists (config.local.toml is ignored by git) +config = toml.load('./rootfs/email/config.toml') +if os.path.exists('./rootfs/email/config.local.toml'): + config = toml.load('./rootfs/email/config.local.toml') + + +# create email client +def email_client() -> imaplib.IMAP4_SSL: + logger.info(f"read email config from {config.get('EMAIL_IMAP_SERVER')}") + client = imaplib.IMAP4_SSL(config.get('EMAIL_IMAP_SERVER')) + client.login(config.get('EMAIL_ADDRESS'), config.get('EMAIL_PASSWORD')) + return client + +def list_box(mail: imaplib.IMAP4_SSL): + _, mailbox_list = mail.list() + for mailbox in mailbox_list: + print(mailbox.decode()) + +def read_emails(client: imaplib.IMAP4_SSL, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"): + client.select(folder) + _, data = client.uid('search', None, imap_keyword) + # get email uid list + email_list = data[0].split() + logger.info(f"got {len(email_list)} emails") + for uid in email_list: + logger.info(f"read email uid {uid}") + if check_email_saved(client, uid): + logger.info(f"email uid {uid} already saved") + continue + else: + read_and_save_email(client, uid) + logger.info(f"email uid {uid} saved") + + +def read_and_save_email(client: imaplib.IMAP4_SSL, uid: str): + message_parts = "(BODY.PEEK[])" + _, email_data = client.uid('fetch', uid, message_parts) + mail = mailparser.parse_from_bytes(email_data[0][1]) + logger.info(f"got email subject [{mail.subject}]") + save_email(mail) + +def get_local_dir_name(mail: mailparser.MailParser) -> str: + dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" + name = f"{mail.subject}__{mail.date}" + name = hashlib.md5(name.encode('utf-8')).hexdigest() + return f"{dir}/{name}" + + +# check only need to check email header, not need to download email body +def check_email_saved(client: imaplib.IMAP4_SSL, uid: str): + message_parts = "(BODY[HEADER])" + _, email_data = client.uid('fetch', uid, message_parts) + mail = mailparser.parse_from_bytes(email_data[0][1]) + logger.info(f"check email subject [{mail.subject}]") + dir = get_local_dir_name(mail) + logger.info(f"check email saved {dir}") + file = f"{dir}/email.txt" + if os.path.exists(file): + return True + return False + + +# save email to local file by each folder +def save_email(mail: mailparser.MailParser): + # create email account dir + dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" + if not os.path.exists(dir): + os.makedirs(dir) + # create email local dir + email_dir = get_local_dir_name(mail) + logger.info(f"save email to {email_dir}") + if not os.path.exists(email_dir): + os.makedirs(email_dir) + + # save email content and meta info + with open(f"{email_dir}/email.txt", "w") as f: + f.write(mail.body) + with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f: + mail_dict = json.loads(mail.mail_json) + if 'body' in mail_dict: + del mail_dict['body'] + json.dump(mail_dict, f, ensure_ascii=False, indent=4) + logger.info(f"save email meta info {f.name}") + + + +if __name__ == "__main__": + mail = email_client() + folder = 'INBOX' + # imap_keyword = "UNSEEN" + imap_keyword = "ALL" + read_emails(mail, folder, imap_keyword) From b4990e4c57c1b7bac160ce370882467fc8585771 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 1 Sep 2023 00:39:36 -0700 Subject: [PATCH 02/21] Workflow is almost done. --- doc/mvp/process model.drawio | 80 ++--- doc/mvp/workflow.drawio | 4 +- rootfs/workflows/plan_maker/workflow.toml | 8 +- src/aios_kernel/agent_message.py | 20 ++ src/aios_kernel/bus.py | 12 +- src/aios_kernel/chatsession.py | 4 +- src/aios_kernel/role.py | 10 +- src/aios_kernel/workflow.py | 309 ++++++++++++++---- .../workflow_manager/workflow_manager.py | 29 +- src/service/aios_shell/aios_shell.py | 76 +++-- test/pkg.cfg.toml | 92 +++++- 11 files changed, 469 insertions(+), 175 deletions(-) diff --git a/doc/mvp/process model.drawio b/doc/mvp/process model.drawio index 86d56e7..d78b1db 100644 --- a/doc/mvp/process model.drawio +++ b/doc/mvp/process model.drawio @@ -1,132 +1,132 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index 2b3e239..54e724c 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -220,7 +220,7 @@ - + diff --git a/rootfs/workflows/plan_maker/workflow.toml b/rootfs/workflows/plan_maker/workflow.toml index e5b150f..1dc80bf 100644 --- a/rootfs/workflows/plan_maker/workflow.toml +++ b/rootfs/workflows/plan_maker/workflow.toml @@ -46,7 +46,7 @@ content="""你是一个活动策划公司的经理,与客户对接并向团队 1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。 3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。 -4. 当你决定要和工作小组通信时,请使用`sendmsg({小组名称},{内容}`)的形式。""" +4. 当你决定要和工作小组通信时,请使用 sendmsg(小组名称,内容) 的形式。""" @@ -68,7 +68,7 @@ agent="email_reader" role="system" content="""你是一家活动策划公司的嘉宾对接组的组长,你的工作是基于已知信息,当前活动信息、公司经理的指令与嘉宾沟通,收集嘉宾的信息,然后将信息反馈给经理。在你看来,参加活动的多少有成员都是嘉宾,你可以通过你知道的信息给不同的成员进行分级。你的基本工作模式是: 1. 处理收到的邮件,如果邮件来自嘉宾,你会尝试从邮件的表态和内容中分享嘉宾的需要,并结合你对当前活动方案的理解判断是否需要和经理沟通,如果需要和经理沟通,你会将嘉宾的需求总结和告诉经理。不需要沟通的事项可以直接回复嘉宾。 -2. 你总是通过`call_function(get_env,'parent.topic'`的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。 -3. 当你决定要和经理通信时,请使用`send_msg(manager,{内容}`)的形式,内容的长度不超过200字。 -4. 当你决定要回复嘉宾时,请使用`call_function(sendmail,{嘉宾邮件地址},{标题},{内容})的形式,内容的长度不超过500字。""" +2. 你总是通过 call_function(get_env,'parent.topic')的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。 +3. 当你决定要和经理通信时,请使用 sendmsg(manager,内容)的形式,内容的长度不超过200字。 +4. 当你决定要回复嘉宾时,请使用 call(sendmail,嘉宾邮件地址,邮件标题,内容) 的形式,内容的长度不超过500字。""" # 这里是孤立工作模式,组长只和经理沟通,也可以赋予其和其它组沟通的能力 diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 3aa05c1..242cc66 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -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 + diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py index 6c92a8a..ae1853e 100644 --- a/src/aios_kernel/bus.py +++ b/src/aios_kernel/bus.py @@ -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)) - \ No newline at end of file + + +#send message to target logic: +# find target handler: +# process_message(msg): +# session = get_session(msg.sender,msg.target) + + +# history: open(sender,target,topic) \ No newline at end of file diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py index 90890ed..ecd90a3 100644 --- a/src/aios_kernel/chatsession.py +++ b/src/aios_kernel/chatsession.py @@ -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: diff --git a/src/aios_kernel/role.py b/src/aios_kernel/role.py index c4d97ab..1d63e61 100644 --- a/src/aios_kernel/role.py +++ b/src/aios_kernel/role.py @@ -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 diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 88a13ed..9c90056 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -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""" diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index 28479dd..adb844a 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -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)}") diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index cded7d0..3789957 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -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)', diff --git a/test/pkg.cfg.toml b/test/pkg.cfg.toml index 46acf0a..68c46d5 100644 --- a/test/pkg.cfg.toml +++ b/test/pkg.cfg.toml @@ -1,15 +1,83 @@ +# workflow实例场景 +# 能展示sub workflow +# 能展示env的整合 +# 能展示filter的使用 +# 能展示function的调用 +# 能展示基于工作目标/KPI的sub workflow迭代流程 +# 多人场景安排 -[authors] -"buckyos.org" = {public="aaaa",name="BuckyOS Core Dev Team"} -[authors."cyfs.com"] -public="bbbb" -name="CYFS Core Dev Team" +# 例子:举办一个团队活动 +# 方案讨论(通过交互引导的方式收集主人的需求)与确定 -[env] -is_strict = false -prefixs = ["default","../m/s","abc"] -obj2 = {k1="bb",k2=300} -[env.obj] -k1="aa" -k2=200 +# 活动前: +# 通讯员,对接管理参加活动的人的情况 (email spider) +# 酒店预订 简单:搜索(酒店评价) 处理异常 +# 行程(票务)预订 :搜索,处理异常 +# 餐饮预订:给出方案,确定细节,预订 + +# 活动中: +# 进行统计和分析,调整设备 +# 安保,空调,音乐,拍照,录像 +# 响应紧急情况 +# 活动结束后: +# 整理照片,视频,进行必要的二次创作,发送给相关人员 +# 对活动进行总结,提出改件意见(指导下一次活动) + +# 1. 人员 +# 主管,负责和客户沟通,并对每个环境的结果进行总结 +# 嘉宾对接 +# 酒店组 +# 行程组 +# 财务组 +# 多媒体组 + + +[filter] +"*" = "manager" + +[roles.manager] +fullname = "经理" +agent="manager" +[[roles.manager.prompt]] +role="system" +content="""你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是:\ +1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 \ +2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。\ +3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。\ +4. 当你决定要和工作小组通信时,请使用`send_message({小组名称},{内容}`)的形式。"""}] + + + + +[sub_workflows] +[sub_workflows."嘉宾对接组"] +# 展现读取email和发送email与嘉宾沟通的能力 +[sub_workflows."嘉宾对接组".environments.email] +new_mail = "收到来自{event.data.from},标题为{event.data.subject}的邮件,内容为{event.data.content}的电子邮件" # 这里将new_mail事件转换为了一个来自环境的message + +[sub_workflows."嘉宾对接组".roles.leader] +name = "嘉宾对接组组长" +[[sub_workflows."嘉宾对接组".roles.leader.prompt]] +role="system" +content="""你是一家活动策划公司的嘉宾对接组的组长,你的工作是基于已知信息,当前活动信息、公司经理的指令与嘉宾沟通,收集嘉宾的信息,然后将信息反馈给经理。在你看来,参加活动的多少有成员都是嘉宾,你可以通过你知道的信息给不同的成员进行分级。你的基本工作模式是:\ +1. 处理收到的邮件,如果邮件来自嘉宾,你会尝试从邮件的表态和内容中分享嘉宾的需要,并结合你对当前活动方案的理解判断是否需要和经理沟通,如果需要和经理沟通,你会将嘉宾的需求总结和告诉经理。不需要沟通的事项可以直接回复嘉宾。\ +2. 你总是通过`call_function(get_env,'parent.topic'`的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。\ +3. 当你决定要和经理通信时,请使用`send_message(manager,{内容}`)的形式,内容的长度不超过200字。\ +4. 当你决定要回复嘉宾时,请使用`call_function(sendmail,{嘉宾邮件地址},{标题},{内容})的形式,内容的长度不超过500字。""" +# 这里是孤立工作模式,组长只和经理沟通,也可以赋予其和其它组沟通的能力 + +[sub_workflows."酒店预定组"] +# 展现使用搜索引擎,并调用预订酒店的能力 +[sub_workflows."酒店预定组".environments.email] + +[sub_workflows."酒店预定组".roles.leader] +name="酒店预定组组长" +prompt = [{role="xxx",content="yyy"}] + +[sub_workflows."酒店预定组".roles.research] +name="酒店搜索专家" + +[sub_workflows."行程预订组"] +# 展现处理冲突并反推 +nam3="3" From 25bba0742a2d16b930cf8ecafd1609de5fd61233 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 1 Sep 2023 12:05:03 -0700 Subject: [PATCH 03/21] 1. Build a more simple workflow:math_school 2. Fix some bug --- rootfs/agents/math_teacher/agent.toml | 5 + rootfs/workflows/math_school/workflow.toml | 50 ++++++++ src/aios_kernel/workflow.py | 109 ++---------------- .../workflow_manager/workflow_manager.py | 1 + src/service/aios_shell/aios_shell.py | 22 +--- 5 files changed, 74 insertions(+), 113 deletions(-) create mode 100644 rootfs/agents/math_teacher/agent.toml create mode 100644 rootfs/workflows/math_school/workflow.toml diff --git a/rootfs/agents/math_teacher/agent.toml b/rootfs/agents/math_teacher/agent.toml new file mode 100644 index 0000000..e24792c --- /dev/null +++ b/rootfs/agents/math_teacher/agent.toml @@ -0,0 +1,5 @@ +instance_id = "math_teacher" +fullname = "the one" +[[prompt]] +role = "system" +content = "你是精通数学的老师" diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml new file mode 100644 index 0000000..9f5af27 --- /dev/null +++ b/rootfs/workflows/math_school/workflow.toml @@ -0,0 +1,50 @@ +name = "math_school" + +[filter] +"*" = "小学老师" + +[roles."小学老师"] +name = "小学老师" +fullname = "Ada Zhang" +agent="math_teacher" +[[roles."小学老师".prompt]] +role="system" +content="""你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 +当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 +当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 +你会根据教导处主任的指示,定期调整教学方法""" + + +[roles."初中老师"] +name = "初中老师" +fullname = "Mark Wang" +agent="math_teacher" +[[roles."初中老师".prompt]] +role="system" +content="""你在学校任职,担任初中老师。 +当你发现学生的水平不是初中生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 +当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 +你会根据教导处主任的指示,定期调整教学方法""" + +[roles."高中老师"] +name = "高中老师" +fullname = "Hong Sun" +agent="math_teacher" + +[[roles."高中老师".prompt]] +role="system" +content="""你在学校任职,担任高中老师。 +当你发现学生的水平不是高中生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 +当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 +你会根据教导处主任的指示,定期调整教学方法""" + +[roles."教导处主任"] +name = "教导处主任" +fullname = "Green King" +agent="math_teacher" + +[[roles."教导处主任".prompt]] +role="system" +content="""你在学校任职,担任教导处主任。 +你收到老师发来的信息时,如果是类似 学生名_作业分数 的结果,会在合适的情况下根据学生作业的整体情况,对老师的教学方法进行必要的调整。""" + diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 9c90056..f7c13e8 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -1,6 +1,7 @@ import logging import asyncio +import json from asyncio import Queue from typing import Optional,Tuple from abc import ABC, abstractmethod @@ -79,13 +80,12 @@ class Workflow: 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") - # return False - #self.rule_prompt = AgentPrompt() - #if self.rule_prompt.load_from_config(config.get("rule_prompt")) is False: - # logger.error("Workflow load rule_prompt failed") - # return False + if config.get("prompt") is not None: + self.rule_prompt = AgentPrompt() + if self.rule_prompt.load_from_config(config.get("prompt")) is False: + logger.error("Workflow load prompt failed") + return False + if config.get("roles") is None: logger.error("workflow config must have roles") return False @@ -225,8 +225,8 @@ class Workflow: logger.error(f"parse postmsg failed! {func_call}") continue new_msg = AgentMsg() - target_id = func_args[1] - msg_content = func_args[2] + target_id = func_args[0] + msg_content = func_args[1] new_msg.set("_",target_id,msg_content) r.post_msgs.append(new_msg) continue @@ -307,9 +307,8 @@ class Workflow: prompt = AgentPrompt() prompt.append(the_role.agent.prompt) + prompt.append(self.get_workflow_rule_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)) @@ -323,16 +322,14 @@ class Workflow: #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) - - + logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}") for postmsg in result.post_msgs: postmsg.topic = msg.topic - await self.role_post_msg(the_role,postmsg) + await self.role_post_msg(postmsg,the_role) for post_call in result.post_calls: await self.role_post_call(post_call,the_role) - result_prompt_str = "" match result.state: case "ignore": @@ -367,82 +364,6 @@ class Workflow: 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}" - chatsession = AIChatSession.get_session(self.workflow_name,session_topic,self.db_file) - if chatsession is None: - logger.error(f"get session {session_topic}@{self.workflow_name} failed!") - return None - - # prompt generat progress is most important part of workflow(app) develope - 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) - - result = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) - chatsession.append_recv(msg) - final_result = result - - result_type : str = self._get_llm_result_type(result) - is_ignore = False - match result_type: - case "function": - callchain:CallChain = self._parse_function_call_chain(result) - 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) - 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 - - - case "send_message": - # send message to other / sub workflow - next_msg:AgentMsg = self._parse_to_msg(result) - if next_msg is not None: - next_msg.sender = self.workflow_name - logger.info(f"W#{self.workflow_name} send message to {next_msg.get_target()}") - resp_msg = await self.get_bus().send_message(next_msg.get_target(),next_msg) - if resp_msg is not None: - msg_prompt = AgentPrompt() - msg_prompt.messages = [{"role":"assistant","content":result},{"role":"user","content":f"{next_msg.get_target()}:{resp_msg.body}"}] - - final_result = await ComputeKernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) - - - case "post_message": - # post message to other / sub workflow - next_msg:AgentMsg = self._parse_to_msg(result) - if next_msg is not None: - next_msg.sender = self.workflow_name - logger.info(f"W#{self.workflow_name} post message to {next_msg.get_target()}") - self.get_bus().post_message(next_msg.get_target(),next_msg) - - case "ignore": - is_ignore = True - - if is_ignore: - return None - - resp_msg = AgentMsg() - resp_msg.set(self.workflow_name,msg.sender,final_result) - chatsession.append_post(resp_msg) - return resp_msg - - async def _get_prompt_from_session(self,chatsession:AIChatSession) -> AgentPrompt: messages = chatsession.read_history() # read last 10 message result_prompt = AgentPrompt() @@ -465,12 +386,6 @@ class Workflow: def get_workflow_rule_prompt(self) -> AgentPrompt: return self.rule_prompt - - def get_workflow(self,workflow_name:str): - """get workflow from known workflow list or sub workflow list""" - pass - - def _env_event_to_msg(self,env_event:EnvironmentEvent) -> AgentMsg: pass diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index adb844a..5603745 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -52,6 +52,7 @@ class WorkflowManager: the_workflow = await self._load_workflow_from_media(workflow_media_info) if the_workflow is None: logger.warn(f"load workflow {workflow_id} from media failed!") + return None if await self._load_workflow_agents(the_workflow) is False: return None diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 3789957..5236e7e 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -107,6 +107,10 @@ class AIOS_Shell: self.current_topic = topic show_text = FormattedText([("class:title", f"current session switch to {topic}@{target_id}")]) return show_text + case 'login': + if len(args) >= 1: + self.username = args[0] + return self.username + " login success!" case 'history': num = 10 offset = 0 @@ -157,31 +161,17 @@ def parse_function_call(func_string): async def main(): print("aios shell prepareing...") - logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8', + logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8',force=True, 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)', 'open($target,$topic)', 'history($num,$offset)', + 'login($username)' 'show()', 'exit()', 'help()'], ignore_case=True) From 1bbdf2e7225662666a8efd159c4066bd9cb19acc Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 4 Sep 2023 22:36:59 -0700 Subject: [PATCH 04/21] Add basic enviroment support. Can use env values in prompts right now. --- rootfs/workflows/math_school/workflow.toml | 17 ++- src/aios_kernel/__init__.py | 3 +- src/aios_kernel/environment.py | 100 ++++++++++++++- src/aios_kernel/workflow.py | 69 ++++++++--- src/aios_kernel/workflow_env.py | 136 +++++++++++++++++++++ src/aios_kernel/worksapce_env.py | 0 src/service/aios_shell/aios_shell.py | 6 +- test/env_test.py | 32 +++++ 8 files changed, 340 insertions(+), 23 deletions(-) create mode 100644 src/aios_kernel/workflow_env.py create mode 100644 src/aios_kernel/worksapce_env.py create mode 100644 test/env_test.py diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml index 9f5af27..48c9831 100644 --- a/rootfs/workflows/math_school/workflow.toml +++ b/rootfs/workflows/math_school/workflow.toml @@ -1,5 +1,15 @@ name = "math_school" +[enviroment] +GOAL="成为最好的学校" + + +[[connected_env]] +env_id = "calender" +[[connected_env.event2msg]] +timer = "现在是{data}" +role = "教导处主任" + [filter] "*" = "小学老师" @@ -9,7 +19,7 @@ fullname = "Ada Zhang" agent="math_teacher" [[roles."小学老师".prompt]] role="system" -content="""你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 +content="""现在时间是:{now}你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 你会根据教导处主任的指示,定期调整教学方法""" @@ -45,6 +55,7 @@ agent="math_teacher" [[roles."教导处主任".prompt]] role="system" -content="""你在学校任职,担任教导处主任。 -你收到老师发来的信息时,如果是类似 学生名_作业分数 的结果,会在合适的情况下根据学生作业的整体情况,对老师的教学方法进行必要的调整。""" +content="""你在学校任职,担任教导处主任。你的目标是{GOAL} +你收到老师发来的信息时,如果是类似 学生名_作业分数 的结果,会在合适的情况下根据学生作业的整体情况,对老师的教学方法进行必要的调整。 +当收到非老师发来的时间信息时,回复那一天学生的平均分。""" diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bd1434b..bef117f 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -7,4 +7,5 @@ from .compute_node import ComputeNode,LocalComputeNode from .open_ai_node import OpenAI_ComputeNode from .role import AIRole,AIRoleGroup from .workflow import Workflow -from .bus import AIBus \ No newline at end of file +from .bus import AIBus +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 64c6481..9979e2c 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -2,20 +2,112 @@ # we have some built-in environment: Calender(include timer),Home(connect to IoT device in your home), ,KnwoledgeBase,FileSystem, from abc import ABC, abstractmethod -from typing import Callable +from typing import Any, Callable, Optional,Dict,Awaitable,List +import logging + +logger = logging.getLogger(__name__) class EnvironmentEvent(ABC): @abstractmethod def display(self) -> str: pass +EnvironmentEventHandler = Callable[[str,EnvironmentEvent],Awaitable[Any]] + class Environment: - def __init__(self) -> None: - pass + _all_env = {} + @classmethod + def get_env_by_id(cls,env_id:str): + return cls._all_env.get(env_id) + + @classmethod + def set_env_by_id(cls,id,env): + assert id == env.get_id() + cls._all_env[env.get_id()] = env + + def __init__(self,env_id:str) -> None: + self.env_id = env_id + self.values:Dict[str,str] = {} + self.get_handlers:Dict[str,Callable] = {} + self.owner_env:Dict[str,Environment] = {} + # self.valid_keys:Dict[str,bool] = None + self.event_handlers:Dict[str,List[EnvironmentEventHandler]]= {} def get_id(self) -> str: + return self.env_id + + def add_owner_env(self,env) -> None: + self.owner_env[env.get_id()] = env + + #@abstractmethod + #TODO: how to use env? different env has different prompt + #def get_env_prompt(self) -> str: + # pass + + @abstractmethod + def _do_get_value(self,key:str) -> Optional[str]: pass + def register_get_handler(self,key:str,handler:Callable) -> None: + h = self.get_handlers.get(key) + if h is not None: + logger.warn(f"register get_handler {key} in env {self.env_id}:handler already exist") + + self.get_handlers[key] = handler + + def attach_event_handler(self,event_id:str,handler:Callable) -> None: - pass + handler_list = self.event_handlers.get(event_id) + if handler_list is None: + handler_list = [] + self.event_handlers[event_id] = handler_list + + handler_list.append(handler) + + def remove_event_handler(self,event_id:str,handler:Callable) -> None: + handler_list = self.event_handlers.get(event_id) + if handler is not None: + handler_list.remove(handler) + return + + logger.warn(f"remove event_handler {event_id} in env {self.env_id}:handler not found") + + async def fire_event(self,event_id:str,event:EnvironmentEvent) -> None: + handler_list = self.event_handlers.get(event_id) + if handler_list is not None: + for handler in handler_list: + await handler(self.env_id,event) + else: + logger.warn(f"fire event {event_id} in env {self.env_id}:handler not found") + return + + def __getitem__(self, key): + return self.get_value(key) + + def get_value(self,key:str) -> Optional[str]: + handler = self.get_handlers.get(key) + if handler is not None: + return handler(key) + + s = self.values.get(key) + if isinstance(s,str): + return s + else: + logger.warn(f"get value {key} in env {self.env_id} failed!,type is not str") + + s = self._do_get_value(key) + if s is not None: + return s + if self.owner_env is not None: + for env in self.owner_env.values(): + s = env.get_value(key) + if s is not None: + return s + + logger.warn(f"get value {key} in env {self.env_id} failed!,not found") + return None + + def set_value(self, key: str, str_value: str,is_storage:bool = True): + logger.info(f"set value {key} in env {self.env_id} to {str_value}") + self.values[key] = str_value diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index f7c13e8..69f39c3 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -2,6 +2,7 @@ import logging import asyncio import json +import os from asyncio import Queue from typing import Optional,Tuple from abc import ABC, abstractmethod @@ -14,6 +15,7 @@ from .role import AIRole,AIRoleGroup from .ai_function import CallChain from .compute_kernel import ComputeKernel from .bus import AIBus +from .workflow_env import WorkflowEnvironment logger = logging.getLogger(__name__) @@ -56,6 +58,8 @@ class Workflow: self.sub_workflows = {} self.owner_workflow = None self.db_file = None + self.env_db_file = None + self.workflow_env:WorkflowEnvironment = None self.is_start = False self.msg_queue = Queue() @@ -100,6 +104,27 @@ class Workflow: if self.input_filter.load_from_config(config.get("filter")) is False: logger.error("Workflow load input_filter failed") return False + + if self.owner_workflow is None: + self.env_db_file = os.path.dirname(self.db_file) + "/" + self.workflow_id + "_env.db" + else: + self.env_db_file = self.owner_workflow.env_db_file + self.workflow_env = WorkflowEnvironment(self.workflow_id,self.env_db_file) + + env_ndoe = config.get("enviroment") + if env_ndoe is not None: + if self._load_env_from_config(env_ndoe) is False: + logger.error("Workflow load env failed") + return False + + connected_env_ndoe = config.get("connected_env") + if connected_env_ndoe is not None: + for _node in connected_env_ndoe: + remote_env = Environment.get_env_by_id(_node.get("env_id")) + if remote_env is None: + logger.error(f"Workflow load connected_env failed, env {env_id} not found!") + return False + self.connect_to_environment(remote_env,_node.get("event2msg")) sub_workflows = config.get("sub_workflows") if sub_workflows is not None: @@ -107,10 +132,12 @@ class Workflow: logger.error("Workflow load sub workflows failed") return False - #TODO: load env - return True + def _load_env_from_config(self,config:dict) -> bool: + for k,v in config.items(): + self.workflow_env.set_value(k,v,False) + def _load_sub_workflows(self,config:dict) -> bool: for k,v in config.items(): sub_workflow = Workflow() @@ -296,6 +323,13 @@ class Workflow: logger.info(f"{the_role.role_id} post call {call[0]} with args {call[1]}") return + def _format_msg_by_env_value(self,prompt:AgentPrompt): + if self.workflow_env is None: + return + + for msg in prompt.messages: + old_content = msg.get("content") + msg["content"] = old_content.format_map(self.workflow_env) async def role_process_msg(self,msg:AgentMsg,the_role:AIRole): session_topic = f"{msg.sender}#{msg.topic}" @@ -316,8 +350,9 @@ class Workflow: msg_prompt = AgentPrompt() msg_prompt.messages = [{"role":"user","content":msg.body}] prompt.append(msg_prompt) - - + + self._format_msg_by_env_value(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()) @@ -392,16 +427,22 @@ class Workflow: def get_inner_environment(self,env_id:str) -> Environment: pass - def connect_to_environment(self,env:Environment) -> None: - the_env = self.connected_environment.get(env.get_id()) - if the_env is None: - self.connected_environment[env.get_id()] = env - def _env_msg_handler(env_event:EnvironmentEvent) -> None: - the_msg:AgentMsg= self._env_event_to_msg(env_event) - self.post_msg(the_msg) - - # register all event handler - the_env.attach_event_handler(None,_env_msg_handler) + def connect_to_environment(self,the_env:Environment,conn_info:dict) -> None: + if the_env is not None: + self.workflow_env.add_owner_env(the_env) + + #for event2msg in conn_info: + # for k,v in event2msg: + # if k == "role": + # continue + # else: + # + # def _env_msg_handler(env_event:EnvironmentEvent) -> None: + # the_msg:AgentMsg= self._env_event_to_msg(env_event) + # self.role_post_msg + + # the_env.attach_event_handler(k,_env_msg_handler) + # break else: logger.warn(f"environment {env.get_id()} already connected!") diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py new file mode 100644 index 0000000..9ef00cb --- /dev/null +++ b/src/aios_kernel/workflow_env.py @@ -0,0 +1,136 @@ + +from datetime import datetime +import asyncio +import sqlite3 # Because sqlite3 IO operation is small, so we can use sqlite3 directly.(so we don't need to use async sqlite3 now) +from sqlite3 import Error +import threading +import logging +from typing import Optional +from .environment import Environment,EnvironmentEvent + +logger = logging.getLogger(__name__) + +class CalenderEvent(EnvironmentEvent): + def __init__(self,data) -> None: + super().__init__() + self.event_name = "timer" + self.data = data + + def display(self) -> str: + return f"#event timer:{self.event_data}" + +# AI Calender GOAL: Let user use "create notify after 2 days" to create a timer event +class CalenderEnvironment(Environment): + def __init__(self, env_id: str) -> None: + super().__init__(env_id) + self.is_run = False + + def _do_get_value(self,key:str) -> Optional[str]: + return None + + def start(self) -> None: + if self.is_run: + return + self.is_run = True + + self.register_get_handler("now",self.get_now) + async def timer_loop(): + while True: + if self.is_run == False: + break + + await asyncio.sleep(1.0) + now = datetime.now() + formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') + env_event:CalenderEvent = CalenderEvent(formatted_time) + await self.fire_event("timer",env_event) + + return + + asyncio.create_task(timer_loop()) + + def stop(self): + self.is_run = False + + def get_now(self,key:str) -> str: + now = datetime.now() + formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') + return formatted_time + +# Default Workflow Environment(Context) +class WorkflowEnvironment(Environment): + def __init__(self, env_id: str,db_file:str) -> None: + super().__init__(env_id) + self.db_file = db_file + self.local = threading.local() + self.table_name = "WorkflowEnv_" + env_id + + def _get_conn(self): + """ get db connection """ + if not hasattr(self.local, 'conn'): + self.local.conn = self._create_connection() + return self.local.conn + + def _create_connection(self): + """ create a database connection to a SQLite database """ + conn = None + try: + conn = sqlite3.connect(self.db_file) + except Error as e: + logging.error("Error occurred while connecting to database: %s", e) + return None + + if conn: + self._create_table(conn) + + return conn + + def close(self): + if not hasattr(self.local, 'conn'): + return + self.local.conn.close() + + def _create_table(self, conn): + """ create table """ + try: + # create sessions table + conn.execute(f""" + CREATE TABLE IF NOT EXISTS """ + self.table_name + """ ( + EnvKey TEXT PRIMARY KEY, + EnvValue TEXT, + UpdateTime TEXT + ); + """) + conn.commit() + except Error as e: + logging.error("Error occurred while creating tables: %s", e) + + def _do_get_value(self, key: str) -> str | None: + try: + conn = self._get_conn() + c = conn.cursor() + c.execute("SELECT EnvValue FROM " + self.table_name +" WHERE EnvKey = ?", (key,)) + value = c.fetchone() + if value is None: + return None + return value[0] + except Error as e: + logging.error(f"Error occurred while _do_get_value{key}: {e}") + return None + + def set_value(self, key: str, str_value: str, is_storage:bool=True): + super().set_value(key,str_value) + if is_storage is False: + return + + try: + conn = self._get_conn() + conn.execute(""" + INSERT OR REPLACE INTO """ + self.table_name+ """ (EnvKey, EnvValue, UpdateTime) + VALUES (?, ?, ?) + """, (key, str_value, datetime.now())) + conn.commit() + return 0 # return 0 if successful + except Error as e: + logging.error(f"Error occurred while update env{self.env_id}.{key} ,error:{e}") + diff --git a/src/aios_kernel/worksapce_env.py b/src/aios_kernel/worksapce_env.py new file mode 100644 index 0000000..e69de29 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 5236e7e..053c5ec 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -31,7 +31,7 @@ from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,Op sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager - +from aios_kernel import CalenderEnvironment,Environment class AIOS_Shell: @@ -62,6 +62,10 @@ class AIOS_Shell: return False async def initial(self) -> bool: + cal_env = CalenderEnvironment("calender") + cal_env.start() + Environment.set_env_by_id("calender",cal_env) + AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) open_ai_node = OpenAI_ComputeNode() diff --git a/test/env_test.py b/test/env_test.py new file mode 100644 index 0000000..694acbb --- /dev/null +++ b/test/env_test.py @@ -0,0 +1,32 @@ +import asyncio +import os +import sys + +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../src') + +from aios_kernel import CalenderEnvironment,WorkflowEnvironment + + +async def test_buildin_envs(): + c_env = CalenderEnvironment("calender") + c_env.start() + print(c_env.get_value("now")) + async def show_event(eventid,event): + print(event.data) + c_env.attach_event_handler("timer",show_event) + + w_env = WorkflowEnvironment("workflow",os.path.abspath(directory + "/../rootfs/workflow_env.db")) + w_env.set_value("test","test_aaaa") + print(w_env.get_value("test")) + + await asyncio.sleep(10) + + +if __name__ == "__main__": + #test_rstr = "abc is {abc}" + #values = {"abc":"123"} + #new_str = test_rstr.format_map(values) + #print(new_str) + + asyncio.run(test_buildin_envs()) \ No newline at end of file From 3eac598c972c7a8c3a1ecc01cae76202118f898d Mon Sep 17 00:00:00 2001 From: Song Date: Thu, 7 Sep 2023 12:50:13 +0800 Subject: [PATCH 05/21] Add stability node (Text2Img) --- src/aios_kernel/compute_node.py | 25 +++--- src/aios_kernel/compute_task.py | 42 +++++---- src/aios_kernel/open_ai_node.py | 55 +++++------- src/aios_kernel/stability_node.py | 140 ++++++++++++++++++++++++++++++ src/requirements.txt | 8 ++ 5 files changed, 209 insertions(+), 61 deletions(-) create mode 100644 src/aios_kernel/stability_node.py create mode 100644 src/requirements.txt diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 6fe0059..7e159f7 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from .compute_task import ComputeTask +from .compute_task import ComputeTask, ComputeTaskType class ComputeNode(ABC): @@ -8,15 +8,15 @@ class ComputeNode(ABC): self.enable = True @abstractmethod - async def push_task(self,task:ComputeTask,proiority:int = 0): - pass - - @abstractmethod - async def remove_task(self,task_id:str): + async def push_task(self, task: ComputeTask, proiority: int = 0): pass @abstractmethod - def get_task_state(self,task_id:str): + async def remove_task(self, task_id: str): + pass + + @abstractmethod + def get_task_state(self, task_id: str): pass @abstractmethod @@ -28,7 +28,7 @@ class ComputeNode(ABC): pass @abstractmethod - def is_support(self,task_type:str) -> bool: + def is_support(self, task_type: ComputeTaskType) -> bool: pass @abstractmethod @@ -37,17 +37,14 @@ class ComputeNode(ABC): def is_trusted(self) -> bool: return True - + def get_fee_type(self) -> str: return "free" - - + class LocalComputeNode(ComputeNode): def display(self) -> str: return super().display() - + def is_local(self) -> bool: return True - - diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index f2253e0..7895283 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -3,6 +3,7 @@ from enum import Enum import uuid import time + class ComputeTaskState(Enum): DONE = 0 INIT = 1 @@ -11,22 +12,31 @@ class ComputeTaskState(Enum): PENDING = 4 +class ComputeTaskType(Enum): + NONE = -1 + LLM_COMPLETION = 0 + TEXT_2_IMAGE = 1 + IMAGE_2_IMAGE = 2 + VOICE_2_TEXT = 3 + TEXT_2_VOICE = 4 + + class ComputeTask: def __init__(self) -> None: self.task_type = "llm_completion" self.create_time = None - self.task_id:str = None - self.callchain_id:str = None - self.params:dict = {} - self.refers:dict = None - self.pading_data:bytearray = None + self.task_id: str = None + self.callchain_id: str = None + self.params: dict = {} + self.refers: dict = None + self.pading_data: bytearray = None self.state = ComputeTaskState.INIT self.result = None self.error_str = None - def set_llm_params(self,prompts,model_name,max_token_size,callchain_id = None): + def set_llm_params(self, prompts, model_name, max_token_size, callchain_id=None): self.task_type = "llm_completion" self.create_time = time.time() self.task_id = uuid.uuid4().hex @@ -34,7 +44,7 @@ class ComputeTask: self.params["prompts"] = prompts.messages if model_name is not None: self.params["model_name"] = model_name - else: + else: self.params["model_name"] = "gpt-4-0613" self.params["max_token_size"] = max_token_size @@ -45,16 +55,16 @@ class ComputeTask: class ComputeTaskResult: def __init__(self) -> None: self.create_time = None - self.task_id:str = None - self.callchain_id:str = None - self.worker_id:str = None - self.result_code:int = 0 - self.result_str:str = None + self.task_id: str = None + self.callchain_id: str = None + self.worker_id: str = None + self.result_code: int = 0 + self.result_str: str = None - self.result:dict = {} - self.result_refers:dict = None - self.pading_data:bytearray = None + self.result: dict = {} + self.result_refers: dict = None + self.pading_data: bytearray = None - def set_from_task(self,task:ComputeTask): + def set_from_task(self, task: ComputeTask): self.task_id = task.task_id self.callchain_id = task.callchain_id diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 7f5c8e6..06e0f09 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -5,47 +5,49 @@ import asyncio from asyncio import Queue import logging -from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState +from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode logger = logging.getLogger(__name__) + class OpenAI_ComputeNode(ComputeNode): _instance = None + def __new__(cls): if cls._instance is None: cls._instance = super(OpenAI_ComputeNode, cls).__new__(cls) cls._instance.is_start = False return cls._instance - + def __init__(self) -> None: super().__init__() if self.is_start is True: logger.warn("OpenAI_ComputeNode is already start") return - + self.is_start = True - #openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud + # openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud self.openai_api_key = "" self.node_id = "openai_node" self.task_queue = Queue() - if os.getenv("OPENAI_API_KEY") is not None: + if os.getenv("OPENAI_API_KEY") is not None: openai.api_key = os.getenv("OPENAI_API_KEY") else: openai.api_key = self.openai_api_key - + self.start() - - async def push_task(self,task:ComputeTask,proiority:int = 0): + + async def push_task(self, task: ComputeTask, proiority: int = 0): logger.info(f"openai_node push task: {task.display()}") self.task_queue.put_nowait(task) - - async def remove_task(self,task_id:str): + + async def remove_task(self, task_id: str): pass - - def _run_task(self,task:ComputeTask): + + def _run_task(self, task: ComputeTask): task.state = ComputeTaskState.RUNNING mode_name = task.params["model_name"] # max_token_size = task.params["max_token_size"] @@ -57,19 +59,19 @@ class OpenAI_ComputeNode(ComputeNode): max_tokens=4000, temperature=1.2) logger.info(f"openai response: {resp}") - + status_code = resp["choices"][0]["finish_reason"] if status_code != "stop": task.state = ComputeTaskState.ERROR - task.error_str =f"The status code was {status_code}." + task.error_str = f"The status code was {status_code}." return None - - result = ComputeTaskResult() + + result = ComputeTaskResult() result.set_from_task(task) result.worker_id = self.node_id result.result_str = resp["choices"][0]["message"]["content"] result.result = resp["choices"][0]["message"] - + return result def start(self): @@ -82,29 +84,20 @@ class OpenAI_ComputeNode(ComputeNode): if result is not None: task.state = ComputeTaskState.DONE task.result = result - + asyncio.create_task(_run_task_loop()) def display(self) -> str: return f"OpenAI_ComputeNode: {self.node_id}" - - def get_task_state(self,task_id:str): - pass + def get_task_state(self, task_id: str): + pass def get_capacity(self): pass - - def is_support(self,task_type:str) -> bool: - return True - + def is_support(self, task_type: ComputeTaskType) -> bool: + return task_type == ComputeTaskType.LLM_COMPLETION def is_local(self) -> bool: return False - - - - - - \ No newline at end of file diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py new file mode 100644 index 0000000..a5ed4e0 --- /dev/null +++ b/src/aios_kernel/stability_node.py @@ -0,0 +1,140 @@ +import os +import io +import asyncio +from asyncio import Queue +import logging + +from PIL import Image +from stability_sdk import client +import stability_sdk.interfaces.gooseai.generation.generation_pb2 as generation + +from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType +from .compute_node import ComputeNode + +logger = logging.getLogger(__name__) + + +class Stability_ComputeNode(ComputeNode): + _instanace = None + + def __new__(cls): + if cls._instanace is None: + cls._instanace = super(Stability_ComputeNode, cls).__new__(cls) + cls._instanace.is_start = False + return cls._instanace + + def __init__(self) -> None: + super().__init__() + if self.is_start is True: + logger.warn("Stability_ComputeNode is already start") + return + + self.is_start = True + self.node_id = "stability_node" + self.api_key = "" # "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" + self.engine = "" # stable-diffusion-512-v2-0 + + self.task_queue = Queue() + + if os.getenv("STABILITY_API_KEY") is not None: + self.api_key = os.getenv("STABILITY_API_KEY") + else: + self.api_key = "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" + + # Check out the following link for a list of available engines: https://platform.stability.ai/docs/features/api-parameters#engine + if os.getenv("STABILITY_ENGINE") is not None: + self.engine = os.getenv("STABILITY_ENGINE") + else: + self.engine = "stable-diffusion-512-v2-1" + + self.client = client.StabilityInference( + key=self.api_key, + verbose=True, # Print debug messages. + engine=self.engine, + ) + + self.start() + + async def push_task(self, task: ComputeTask, proiority: int = 0): + logger.info(f"stability_node push task: {task.display()}") + self.task_queue.put_nowait(task) + + async def remove_task(self, task_id: str): + pass + + def _run_task(self, task: ComputeTask): + task.state = ComputeTaskState.RUNNING + # model_name && max_token_size not used here + prompts = task.params["prompts"] + + logging.info(f"call stability {self.engine} prompts: {prompts}") + answers = self.client.generate( + prompt=prompts, + # If a seed is provided, the resulting generated image will be deterministic. + seed=0, + # What this means is that as long as all generation parameters remain the same, you can always recall the same image simply by generating it again. + # Note: This isn't quite the case for Clip Guided generations, which we'll tackle in a future example notebook. + # Amount of inference steps performed on image generation. Defaults to 30. + steps=30, + # Influences how strongly your generation is guided to match your prompt. + cfg_scale=7.0, + # Setting this value higher increases the strength in which it tries to match your prompt. + # Defaults to 7.0 if not specified. + width=512, # Generation width, defaults to 512 if not included. + height=512, # Generation height, defaults to 512 if not included. + # Number of images to generate, defaults to 1 if not included. + samples=1, + # Choose which sampler we want to denoise our generation with. + sampler=generation.SAMPLER_K_DPMPP_2M + # Defaults to k_dpmpp_2m if not specified. Clip Guidance only supports ancestral samplers. + # (Available Samplers: ddim, plms, k_euler, k_euler_ancestral, k_heun, k_dpm_2, k_dpm_2_ancestral, k_dpmpp_2s_ancestral, k_lms, k_dpmpp_2m, k_dpmpp_sde) + ) + + for resp in answers: + for artifact in resp.artifacts: + logger.info("artifact:", artifact.id, + artifact.type, artifact.finish_reason) + if artifact.finish_reason == generation.FILTER: + logging.warn("request activated the API's safety filters") + if artifact.type == generation.ARTIFACT_IMAGE: + img = Image.open(io.BytesIO(artifact.binary)) + # Save our generated images with the task_id as the filename. + file_name = task.task_id + ".png" # which dir to save? + img.save(file_name) + + result = ComputeTaskResult() + result.set_from_task(task) + result.worker_id = self.node_id + result.result = {"file": file_name} + + return result + + return None + + def start(self): + async def _run_task_loop(): + while True: + logger.info("stability_node is waiting for task...") + task = await self.task_queue.get() + logger.info(f"stability_node get task: {task.display()}") + result = self._run_task(task) + if result is not None: + task.state = ComputeTaskState.DONE + task.result = result + + asyncio.create_task(_run_task_loop()) + + def display(self) -> str: + return f"Stability_ComputeNode: {self.node_id}" + + def get_task_state(self, task_id: str): + pass + + def get_capacity(self): + pass + + def is_support(self, task_type: ComputeTaskType) -> bool: + return task_type == ComputeTaskType.TEXT_2_IMAGE + + def is_local(self) -> bool: + return False diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..605dc72 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,8 @@ +aiofiles==23.2.1 +aiohttp==3.8.5 +openai==0.28.0 +Pillow==10.0.0 +Pillow==10.0.0 +prompt_toolkit==3.0.39 +stability_sdk==0.8.4 +toml==0.10.2 From 0d793bd12d4fd5e24b13d9a829a81ad91adac3b4 Mon Sep 17 00:00:00 2001 From: Song Date: Sat, 9 Sep 2023 22:07:31 +0800 Subject: [PATCH 06/21] Remove some test information --- src/aios_kernel/compute_task.py | 4 ++-- src/aios_kernel/stability_node.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index 7895283..086d946 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -23,7 +23,7 @@ class ComputeTaskType(Enum): class ComputeTask: def __init__(self) -> None: - self.task_type = "llm_completion" + self.task_type = ComputeTaskType.NONE self.create_time = None self.task_id: str = None @@ -37,7 +37,7 @@ class ComputeTask: self.error_str = None def set_llm_params(self, prompts, model_name, max_token_size, callchain_id=None): - self.task_type = "llm_completion" + self.task_type = ComputeTaskType.LLM_COMPLETION self.create_time = time.time() self.task_id = uuid.uuid4().hex self.callchain_id = callchain_id diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index a5ed4e0..8d40878 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -31,21 +31,17 @@ class Stability_ComputeNode(ComputeNode): self.is_start = True self.node_id = "stability_node" - self.api_key = "" # "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" - self.engine = "" # stable-diffusion-512-v2-0 + self.api_key = "" + self.engine = "stable-diffusion-512-v2-1" self.task_queue = Queue() if os.getenv("STABILITY_API_KEY") is not None: self.api_key = os.getenv("STABILITY_API_KEY") - else: - self.api_key = "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" # Check out the following link for a list of available engines: https://platform.stability.ai/docs/features/api-parameters#engine if os.getenv("STABILITY_ENGINE") is not None: self.engine = os.getenv("STABILITY_ENGINE") - else: - self.engine = "stable-diffusion-512-v2-1" self.client = client.StabilityInference( key=self.api_key, From 27ed3e20e6cf448739a61da1ef97283be2c8b557 Mon Sep 17 00:00:00 2001 From: Song Date: Sat, 9 Sep 2023 22:08:39 +0800 Subject: [PATCH 07/21] The argument for is_support should be task_type --- src/aios_kernel/compute_kernel.py | 62 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 1d05afd..2501ad6 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -6,15 +6,18 @@ from asyncio import Queue from .agent import AgentPrompt from .compute_node import ComputeNode -from .compute_task import ComputeTask,ComputeTaskState,ComputeTaskResult +from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskResult logger = logging.getLogger(__name__) # How to dispatch different computing tasks (some tasks may contain a large amount of state for correct execution) -# to suitable computing nodes, achieving a balance of speed, cost, and power consumption, +# to suitable computing nodes, achieving a balance of speed, cost, and power consumption, # is the CORE GOAL of the entire computing task schedule system (aios_kernel). + + class ComputeKernel: _instance = None + def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) @@ -25,7 +28,7 @@ class ComputeKernel: def __init__(self) -> None: if self.is_start is True: return - + self.is_start = True self.task_queue = Queue() self.is_start = False @@ -33,70 +36,72 @@ class ComputeKernel: self.start() - def run(self,task:ComputeTask) -> None: + def run(self, task: ComputeTask) -> None: # check there is compute node can support this task if self.is_task_support(task) is False: - logger.error(f"task {task.display()} is not support by any compute node") + logger.error( + f"task {task.display()} is not support by any compute node") return # add task to working_queue self.task_queue.put_nowait(task) - def start(self): if self.is_start is True: logger.warn("compute_kernel is already start") return - + self.is_start = True + async def _run_task_loop(): while True: logger.info("compute_kernel is waiting for task...") task = await self.task_queue.get() logger.info(f"compute_kernel get task: {task.display()}") - c_node:ComputeNode = self._schedule(task) + c_node: ComputeNode = self._schedule(task) await c_node.push_task(task) - - logger.warn("compute_kernel is stoped!") - - asyncio.create_task(_run_task_loop()) - - def _schedule(self,task) -> ComputeNode: + logger.warn("compute_kernel is stoped!") + + asyncio.create_task(_run_task_loop()) + + def _schedule(self, task) -> ComputeNode: for node in self.compute_nodes.values(): - if node.is_support(task) is True: + if node.is_support(task.task_type) is True: return node - logger.warning(f"task {task.display()} is not support by any compute node") + logger.warning( + f"task {task.display()} is not support by any compute node") return None - def add_compute_node(self,node:ComputeNode): + def add_compute_node(self, node: ComputeNode): if self.compute_nodes.get(node.node_id) is not None: - logger.warn(f"compute_node {node.display()} already in compute_kernel") + logger.warn( + f"compute_node {node.display()} already in compute_kernel") return self.compute_nodes[node.node_id] = node logger.info(f"add compute_node {node.display()} to compute_kernel") - def disable_compute_node(self,node_id:str): + def disable_compute_node(self, node_id: str): node = self.compute_nodes.get(node_id) if node is None: logger.warn(f"compute_node {node_id} not in compute_kernel") return node.enable = False - def is_task_support(self,task:ComputeTask) -> bool: + def is_task_support(self, task: ComputeTask) -> bool: return True - # friendly interface for use: - def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0): + def llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0): # craete a llm_work_task ,push on queue's end # then task_schedule would run this task.(might schedule some work_task to another host) task_req = ComputeTask() - task_req.set_llm_params(prompt,mode_name,max_token) + task_req.set_llm_params(prompt, mode_name, max_token) self.run(task_req) return task_req - async def do_llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0) -> str: - task_req = self.llm_completion(prompt,mode_name,max_token) + async def do_llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0) -> str: + task_req = self.llm_completion(prompt, mode_name, max_token) + async def check_timer(): check_times = 0 while True: @@ -106,16 +111,15 @@ class ComputeKernel: if task_req.state == ComputeTaskState.ERROR: break - if check_times >= 20: + if check_times >= 20: task_req.state = ComputeTaskState.ERROR break await asyncio.sleep(0.5) check_times += 1 - + await asyncio.create_task(check_timer()) if task_req.state == ComputeTaskState.DONE: return task_req.result.result_str - + return "error!" - \ No newline at end of file From 6b45cb95701b570611965472804229d57f7c2016 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 10 Sep 2023 12:01:45 -0700 Subject: [PATCH 08/21] 1.Can use aios_shell from new jarvis telgram bot right now 2.Update MVP plan2.md 3.Refactor function support --- doc/mvp/compute_kernel.png | Bin 0 -> 139279 bytes doc/mvp/mvp plan 2.md | 500 ++++++++++++++++++++++++ src/aios_kernel/ai_function.py | 53 ++- src/aios_kernel/environment.py | 8 +- src/aios_kernel/workflow.py | 6 +- src/aios_kernel/workflow_env.py | 3 + src/service/aios_shell/tg_aios_shell.py | 223 +++++++++++ 7 files changed, 782 insertions(+), 11 deletions(-) create mode 100644 doc/mvp/compute_kernel.png create mode 100644 doc/mvp/mvp plan 2.md create mode 100644 src/service/aios_shell/tg_aios_shell.py diff --git a/doc/mvp/compute_kernel.png b/doc/mvp/compute_kernel.png new file mode 100644 index 0000000000000000000000000000000000000000..f0ddbf180a909a39a41efcd01f1575a2b0cb9ed5 GIT binary patch literal 139279 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%0APw!S$VdE$?Zj0_j3{_vz8#X7^6Om7Y&@B)ty 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 z zTvp9Xb;PZ_d4@I7h4kT4IQzkqPPl2^v34ny`!LBfpwyw$_XpCn&t&$#Lag!HwKe-z zoxl8sJkKmQ%=Gm&JPMNX6M|4wqdoZ~HA>8$R0jKy8(WO&WXzfmMpXJ2d{`|Darev5 zBqwDHK?jJbOj&Gxvy*se{Kou%2M73=PaI@!nLdcVo4nR!7vz?Be%oOSK`{n}gWAvt zX;Wbzm;!H%RN&uk!tkBLm_*V_!4!^kCoNnen38l>g;o_^RkNBteu&qAfyU&+>a4Pi z+=or)ox=*$uOlN3RraNfVz-IX9Z{R?$vc4#q$pNqm~qWfvo53EDP~ph6H#1oUC}9G zf!2oIh}fYWZzH|l+c4%`<}^3V6kL?j+)7X|jrEK?hlsQLg>}WaU?~y~>Cakzyl3n3 zQ0SS%&&rjxxY_!mVnBbXs1&tH*DFHs9>RO2Lj&dpa$W?@QKcZ}Ofja|2Wgdp_#$)SedOcx&xsZo zSpQy*L{3{N!lb3T0KYse7s-i38TrJWeK}y5Wg`Lr!l;`q@ud88@5sdv1%h!djKiOTv=cDiGkhCfkT_O!g*YIwyZUk-4ZbD zUvW}-rTl{?$@n2=A_wf1@!KSnV$kWwUQiZPpI*idmGq8Z`_x!mVi?U~RepG=cJr}k zA7h|WuID2d(--Xoak`u`^d-Zz_OfNDt@58HX+5_? z6Q|`uw^FF(R}g?SkVH`~l6R!?!2J|7Dp3X4ZeN_``@-q+9ouYzce)?qs!X$*`EyuA zci4B5333%vw_==sonwuIsR%=@nCz9!FRWn$xAMD;Tu0jZ6o`6(q%VM~2xk0T4jdT5 zxUdU|dQgov`4Cv+2MNyWj~yfI2%&#sA?Pd4?;DYy?t5LvML)Y5`_upeOnm|1KOaUhGH4f8RT?ESQ(8Bq@OqP6R3I4_xc8lVtlLv=M|Z5& zie<8#YWpii^RW+`e*dzvv#=SX;}dB_uy**nxJQ8ap>qqg;vK1~lsK&Ct;E783>~ro z%J;P4u4*`GqLE;aiqQ|<>Elgw&KbccWN=j5An{@mc@Rv1a|DF7Lnr1q55F{ z+2J^FL@`lyZX*+CvjVjfw#`sg4u^^|vm(Ozn`JlB)-1QIG7GtPCo;nVC9&j5V#tyr z4?xE@;%24J;IhW+o%4L2LaPS`N?IPFxi}5!M$0DBz2EQZ_XorYQY{Zl?$RCX<7#%M z>Z8uusqAmnC)x9a3yUp^qq8n$flP2Wbw*?Em>C}Q)2&1+FC;Ap$7k5TuukkueY3R- z+AC$TP*@qI)^Z|8?91Hqs)n!?Qpv1$vPI%uCnc48l^t0uxGXs#7s_hpW_eqG6r~&jz31(K6~KTH`4@EM@s2Y zmE|-s&O>b7;GiUn!)k3iyPRR*C8JW>mx&MWvSmXh_Sj{Hqkbdhm(-la%+#%|-@A}c zjGe1tC3`8N`6Gz5*_Z(5zSbzLOoGH^AxumW=$>9gm5{llRIJ5lg1|+#OLy3 z)#yN*STGE9GtZBS!OZH4_Fq%qcTeViYXw0|_Ipy>U$y`6-@NKzFElR}a?pGD1QNZ+ zv%Ax5+IVoO1wUC#G?c7BvW?%uiu$ui%@H}2b^Q&{^>D;6SF(Nuu%+ldL~gGqr-?N+ zl$5C)g0CfRQ}RfcXKg~|Nm?5B;+?aJ@n(s`%6lvP-ckzCWM`%!Equ4WK#On8eOgp- zR)80BQL}xpFlu;*r6hG>HhzjV*va_SzO$^;nQsBrAs;hCrCYsvXXS6{iMdq*I#Z|DA(3!2x|+*B-(z=Sw)k^&00`xCB^2! zW9`VKBIR(ViG>yWB~q-2eC`smkWxmuaMWa?r3D7G`CF14{<3!PFGKw&sjF;5?sgrn zbB4I5mE*?SUz7yuWkM(J`CSurRcqDj2yE3{CjWVPDK! zv1995&@n`{r{}V6Wrlhz*eHiiHuW0YFj^@JS@E=Md-7PrFPg8^TX?hn6wxXv;Jr@Q zu~So^1;-!RwJh1+oL>F%@K%vTs!j+j`usNoD9lD9_M`=UM)CR$LEKP)oW_qOb&Ql~ za~WfA;9o3#c^zC=_AyO6=mY60@BMoj!9sbq*_=BDO=oj0w*A!x=n{SN40RC~Dg9_g z+e>P+!Qw*j@RU3KSbTJmITY7zk%Jc4f&N-2kO);|;F~Hzivn4Z7OZ>N?CjNj2O3-? z5|FP!7PpB|r@V}C?MxKfeC{=0Ql^evte?63U%%hupxXROd(-a~3q0n*(JCd+6xEN^ z(6M3v1hl=*ZPcxU503DU3`M*l#$wbVA7KxzxXWy6IhouUZgf1T9(FLVhnACUa;&f_ zk!v2sRI!9I$bVSnVZ(!8S#2?az#t1F{C}QLB~V0JIodS)QoK7f`u0d5mLHt=FQ7bJ z_!m${#`oC%3n+gWVb&jGL3`N#86d2mxIa_g2c>ogW(GU3!y2s(Yi&A?LA@jQRr~p* zFm(cONwO2H--3AC*{DXoPTvR0{_dh!7_GHnv^O%j-L@3-QClw+rl4Y!6)gN^wvQIR z&1vrCu#{Dv+VkzS1umCJTf3u3Le6$1LlBz}hJMcN&Z`;Yer*ua{oLTsHcqB5X82qK zuR$b5?Y}%f2Eu(D#n!+xBh9U|@ozwX+%x+n= zJ>y~5JBQYJAVo9HYDy>!BtqWnMmP5)SQ$T1`~g$9&*mgu7kJXCfz2VY6!pCn=Z=G@ zX{PrGQi9gh8nNEQQ~n;9WPXWCpiGoWP4bu~Q|e)X6hD7GexdAo6UWD*GcCAg^Sb)d zp~_49orkg_e5A*g^XChkX!bBQB5Tnz6j9@eluZrvu!zCEiKw-=4_h` zctGQ~TI@<;e&?BRS#Y<~Wc2zW#_!L$&5O~IrAe{<_!c4u=~kkbn=S!rLl}8=5yxrT9dYx+&)o0ndt6r8 z-ytV9;qt_N`ekh_A}PUMF~me@^Lw4g=nd2cGy|V z21=tyWhr9Ts)jjd)a`8uNiM6OL<{0>I7uubUB6~$(W!@()mv<64|%5&1tP65Dklc| z>I4T#)XGJj03fFp681Ar+B(i-!U5OSvZ3pKqBC>wb!K+q%g^i!BPB&TQ-4DMGZH$` zj-Iz!efLcUal|I}TEzaO5kJ++W8&r&;>K*ht&TWsxeNG7L9fTfB$C^*faz}I!Fd8_ z{E@Hc%dG~K{D_P(FpM#gW(kqW*$6g_QP`e{2rk^HyV?(ZI=-twFv6}IhkKW`l=g04 zf&~Y4mjn`>m1yu%cB(-uBsOUv0a`@OSwNKiK@`B_tGl1+lk{e2Haz8{_iO47?xBoL zRR*tm7w|{kWTmBoGCZuK@<9o0*?V+{C{e(*h8igOFVAZol%Ajq16nfK=h@FdY2J?1 z#RNo*F7S(F;ui06TYNJeAl(n$!Cv07`uCy~^^V+h9F~&mBL^v;S^UG7f&K)&LAq+i z9_FDoq~IQV(%M7cw>#Ey zk*K(01TkGVk5g8DmWs@fkC;}>a7>{u{qYz05kchvFv7n;?g!6lBf`nz;DxLIJ}zqb z8?VIhZS4yB2!f&fzf~b=E4qi`8JxcTwR|2Oz)u>fynhcP39sy6+ zPShh@g-^6-d|TQrIm>yB?|MYfI@fQ!xQqxw_HMkh8$YXl*c(&7m{B!3pQ?{2WOHla zl84^X-;UaK=>N!NzsLDt__ebYC%5^YpRIdZ>cwp!oe1V7Ms{C70}>io;%l0_GbZTz z0~cuWzB^Uh+9*lnh@;FdYx1$kX>JkH^k0xGeAMNDi>gnd9_2?jH)C`3o%9R&0FE*K zJFhnmi|AAR=|MctJL?A~omNe~FKA^nti_D5PZv+M!H;%FXq#y`< zXew8CQoX{o#>#_$SM2kOC#}IBkUX|t6 z=RT+rhRf*?_Lv#+D)3;tEG@n3&Zj~Np#cV>qAw`UN5`14E}%dESac?duoemfeJH&% zB!l~cJkRZthR*Ev*o-yz8J6G|$Thbc3={JMyI!K-r6Y2ihq$M#U5wyatmju?jLz7W zJ8h)R7XM&E`03qf1@~sGidtDqw1ih3nWk8!FkGuL^8uz;={MV;*EM(i7{3i%Kj zQ$dbRVBDv?7l}7@@90L|22_{f0+Bn*%EInIK5GVjSD8`+_)50(5OF z&%qzPJQZK*-oJ8iLFv7O-Kz9Z&*M!*`f7-wyb}+5Q6fuw^7bt@wacsmZ?; zkbdVB$?nQgNi8{3L&Mkf&Hlt&TF#V-sQ1(}+Vx&%izdCF_11S{o_Y67+eDCaP?g=J z13wV^2A(HCe`Rhp;8u9&V8PX{g(kHBV$%oJE<_8qoF^3Wg4nNx$UH=ScI}qw>^dEA{MkPpdZ-ORSvM4Um`zz zxt(XEVl)SCN%0L^JX3Uio2AulfEYh&6)4@lSG>8tF-if9rudTMX$N$pKq#(txxVg z-aj&rg@8?JCtUN-t|s*@eu8eFiN&m+*#Z0vgdB+MR`=9YU=Mgg4dtjzK!}sh;4{az zQ|CZgwAyW(2g4PDlsEct_e19k)`I9RLpx90-uda;(s>@Wj_Od<-E-;nCm5p_u^X3| z&>o`D4@uLVj6=|nKp~*RnXY>yt?MI?~N5Ig?@&4C%px$#n*)5xRlF0qc6vr2cF z%VIm;nq6%2oT}ig=>)x%ZIUN)5bgYvoaOse>d0zqh-#ZQhrH4;y9Jkx{mgCn<=9z-k}eoLX_%{*FXlXD>P(t!W&Y=|gd@*KP)PV%Q~vIAN* zaQ!{z&v-`cFM@3pT|3Ngcmr6GZY8{lq15eJx(B*o1eM~we{!M(WzFitJ5T_go-m#^ z=P+*lt$s_i^bQnrHK3D=+LV@9?HLXZ7m-J9HI;SrH+L^^dECG~3H>~qjj+QZxRB#@ zG1)@6)EO)94oi!6UOKzD?8x6}J9{p#lHeS?`k;i;Z7E z(!ZVNY{mOVC}|q?$+EmzH<3OrY(in*Tc9F$Z2DK)6uOH*U0#VjrPF7`XKth|qTtST z8oM@{r877ZuWd)e?U~ytR~hNRKeB@ix!vJ_eCq{RL8<;Q;?doz^HrJs>w@#pyHAo! zJZRKB(_P+Gj~!d*mt#8QZWdesCVm=(P9p8&IRVt);Sr(kIlnTBtq7bIy%G$Z=dsqoq&$}9*>M%7vbH-yv2oo^QPZ_ zH8)hKD)``M$Zrb}NWP|Dl1S8@@D9oP7PM z<09ispmR@O6bt}X)?2v5w$$|c^s~_JVm>9msk7`>uQgl3;Ny0LJ{VwZQjyA0e8r%! zcE0Igs1%^z_~PuQ)*(`{qBaM{>aC#Zey&iQBPx2jJK=h8hJo70T>BFTPVeC&%)VX1oim5gb<1GlMaA63{g)4!oFe4e!@~P^ z!msE+#s=L)AABeEZ8CIMRwV}>qpc6;AXpvoSgwzZ)~${oACUr8LhC9pDpjr-bPj`< z(gq?|UmYFHUG%vK&=4)lH)FkUWW;Sg%O*qzMe)DgeUD$Yy;iYhiyQp>N>7-;YpR{x z>%p3Kl?L5m7}mDt6tGYteTWZupN&kdi2GgLm9AT(YR13BH9)yv82oV98K+e7JDiY+ zEG2s)Z`94hMgMCYT1R$Me7TNhvTPa?bR4vsR8B!ZcZVD>mi~a3R=2DdrkM*yyRG!W8+sPW1tu_D5+JEPChn;!$!4^{W|v$h~oTc=#`mRO@+AQLQuq`{$Q> zQp7D4>6Y+_J~%UAwz-$cDYtmXXuRu8y|Z{L#IcZs|3Xe>UeYjaTB_ zzQfv5eH~DRtAIr|t^cqd1G%}RJLxmVAamkZT6ZvO%AUmOO)CFW%n7QOuX4EEc*?f{^P>ZP0Z!q<@V#!WzK5o-b;;tQO|F-G0c0aHNhQbz{}_u=9m$ zoA-IXotK(O@Tmp^eX9ReTIFpv=I$iye6=Z7{p@$fn|*iG{vc|7bX<`g%kZ7Um zvs`RX3?b=w>PqBw;=zFG*n&Y~Y;c$wEL|wa`h=Z@MeR|tye0w9+Y5-dVj8EA7Ji^} zyFX>g@sa`Cq`e6Vui%&3e&KXHSio=DKf5|kasQDKNM-t^Y>y=nWI`lpJ`eWm(?e98 zF;I6w%o!o1LSc2)ziHAqCw*J69@48M1{1?+dJFv2Jl(dCd#))aj98`YaA{#m#NfJD z>b#fNUfPS)xYf}&7pBfFt%R0g82^a2lpJqPEX`8N#{Z{+DVHQxE6W2FyE)q(Qp7Q@W8xy1Vng(Brw!xySt*;~CFe zalqbdt$pq5JLhN4i{j+Nm(@4cLT|PBd<;$WJbE-7PZaNIAS1mX_Dp)D&aT)uC|~i5 zp@)((vg%TSZLhaF|7|1BKyuM;-6TaDB_^CQ+k%g?0-Y`vIuZ&lEw71#qgC*iX>J0c z8`>nuhjhb02b1qj>E5}z&_M^;H&V}2bWer!7cWz1H?v?tcjswc7Pq&*%ywr7!OAqDR)QGrPx@L(;Ge&KTAHTM)cf&zIFB^KJ&{01I!h@_WLQh&_ z{pvI@7pI-OtI>!UYkOC|?Xs<GOU%5-$vymI`he`WDD<_J&@YqM z4JU|hG%ROm+A&NdNqjcl!xRb6L=IRA*BaS^Ar2~(UmxYYId|#-$**Bwc z5GkKN6BDVr*MI6d?3ff}GA&|i;m00n(X6DJCkudKt~*nF_Gr1vPb($veu5=ceN#Y3 zefAV{BKN1n)_&xQ-VeM)&7m^Wi1Cd}!U@BXw2O2PJFP~9io+dBE&sR~>>T2iIfBT` zS;Z49$%gU-Q$RWIdI5;Y51vg@Nur^0URDqu(P;*)EhSV2cWG!E7h%? z5H6h?B63Og!fZ)?gZ;&cyh>iY7HQ&H&?M~lqYTxF9M`_r+8{!8&zl5o#;P18Ehzi4 ztFkh|(5U5JDNlm!3&;-9F|7!L(nY6Tyv7>SIbTd8dBEYY&{85q6c35v|B8w`5`5p} zA7gK4Df)eR{XX0+Vprz0lgY{V52<5*$`t7LBZN2akx3dCjAyJAaA~LMUPeb&?aXHr8OiqfN+8Ci5;E)eB1}YE@W}YN?dfNkVwZBqWYo|U;Rk1{Fn$sk*qZUgodiQaDOz^ zr@CNXxI7>SbRuCjgt|pNVq;HWXvcV8OYm(hi@GRk(oPdZy@TEn&P1bWoR+e72Fb-< zUmc+Z<8%*+T@!a^W3xuoG_+4+6^s73T@Q_LArqNTuF~|}$vNUH64OMS0KTz*FJ>p` z%vT(qt18*_BW=4M>`#^=2sIvhlRc^frHs+=?Rzw$z?w0Ji{?i__8c1H zaGIoC32EywbRjp^Z(Xi$dVsojyet&LL1Rc(DH*y!`=ZjY<>TxIaXu*>WfMhigS0kfBY`{WEUp_kd;*I; z(6t;C$*`Os^Swsn^^cF33Hv;F#c9t%Euz?`)9N^EW(Z4lJs?*t0gWq6P~j zepiS-Zv-F)UXex2Ql`u+9{2D4u=@fT%K^(gVnTZ78?JHcPA9ZDEImzPbPwql+d|D# z1}mRCtr&zbNL8*rTii~upN9b$B9w~3RhJasPjnCHOmUc7_CxV$!CA?k#@pf^@HK3) ze9xX`b?$zZs#7EAZ6%TH{%)FsqG|JA56oF~9i#-x}s|Zus2c z=G9t}W0qcmQtv{Eezin@`$qQKXbi*1lJj*?w?fX$5awo+ssy2AhsCy|5mpT9=r$U= zi)Va(S#OmYRz-*el&LW4)3^a*K_Q7vVKFo0Vu06G%7*?%o@Mh^xJgRAwi+9zt}5f; zhP}f(MA{WKM~B3sgu})D5*p8vDA%Y58w6)9%1kP@{49x4_<_$oO8UO2u6Y45)E%3x zbUo7$5t+vcFBk&~q}TQKT15$5{oW9XwU%!`SyTwpE!0de1xTD18$P&(D@`XW-|5Ua zOmWm+axYzKOs{5*^ij%w@OyeILnhG%@s#(Yp(pd(*3vZAg$|5MQXFykd=8Nv0v*2; zvN|ID!6O}qr{`sp%ONgyaYIe3bhzNJ{Kmzn=Si}Vs3uV#$Hwf_Oerzn;3S+@^1B>- z@MbpTSszy9PA@3X$CZ-gvu_px;1qkGtEo{Pl-PAp_fM5&Mf(Uik+izMO*Al(H?yHI zJK-|S-w<&My@8bUSa09y>737A?x{K$-J+uSnfL4mb9c0iV}wBF1pKKgBqfOowbP!WdjdUYG<05T1-$zWw>u;Ej|{?yQHG`3EuU1; zejif^wAXa#?b-6(_xD!>2|U;rwD5x-57Q^;Mc=8!H;9f(K(;qi)vi%@^5xT?H1=&U z9JAb{9VS%i7jkX4JF`x2AwkhN8BT_FPo{oBX6Shlt66x)Q-hP$G08iR+KkT=o+TrS z^6%BWypzL5)AW z2Qxz&N%)gkplKTyeP}9&VfXVtR_5gE{#o^@a2ZpRwNE&a|xBWFn+K1P=h6< zC68avkAaf+2LO7nhDwjD=8!fzU}W!!34+xyrxsFm+h7ai^cC1;RA3z)@yqhOK2JTt zL+|{;G6upbIEw@Pf)T;@hE8EeZ2dy2?yU1ILe=^b?_Q60YJwB}sD(}sGM^08>w@Ut zT3H&1WfNu3^z;v%yDf(4ogr|*vmCB2%gmVa4nLLVRM>-X?-yf|D?g6fH9dUCR zD|Vn<@bC!}?-jm&;hpHCwvh3~7#f9*O^e1FMWWXaYP!#;AS4g|%b%JU)Q5-@8rR!4 zEsH0b)xwUc2t}9%Wx-dDaQk87YvYBorT;@x$^rxdU(Yte4-04=WedzH1)11q%* ze+EZ39A^>N6q6qMeqSdkhmjt}e)OT%7YzE9a9sM^P9~GY zgK{=VNgKM3sK#{;OX<$d_y7~p00Wl+tn8blpM{_2bei5;vip&OP7{fFTHN%XCl@l9<2Y9f;Tu)=|8cDbP z{`YOM&dWr3&OBSC%43bq?$om`Y77=0FvXK%5yqm13p6|k5L9{>4H|EQlpiA=vIX(i z@WMWCT*C?I@0nMqc7zubWi6LvqYPB!EP)_-I3z_49HS!be6Si=V8 zB$=q6^j?Sui!5zXdvVW0+|O{13|s1RfN6 z48FtQm*1vp)1ehUXh|P0g&>ucVVRVG140gvMmzeg^X1idgg`CKEqm}Z-0wnP-&8AU z|CF|^%E55))ZjiHz7+LfYzbOXNgU*$_T0D%e2UwHc|uibBc{r%7C z`e|-Jc8N$ZVhpwW*I#(2?!sx!os#=2igE>3z_q-IupvSCuVtqp`#D)M`F|x#nqEFw zh(F=vQW)lVJ!N^(_3#-=D=Q>A0!~0O9@g3}kie?I`9~-ynYKtaY+3LLP`2n(9nOzv zZ+i_ESpT{;IJpJ-%q7n+lyRl!uN(xR;>jZZ=lTJW6$5wIK=C@l+`~Ouiwl5tl5AaC zEo~+;PSA}byO^ZD8U_9W5QC+&|kZZfA(oL->CNSpw;01&17k zB_`>5qDW2+ncF{hz#sTO1QFLtagS8-`r!~qd`-V1<9q-56mzB!3Dgq^`Bs665xRf} zvzJozhVuF?%@+TVz@u?3i*Er0@MoS=N52rHX9aYmOc969@4A35>8#HXCpPYfh!F2T zBP=*>h!9jHR*P6hIikLIskpftf6_la5iPBU;A5Oh&W@aynnty`AdImw(5OYcaDW}@ zb_>j3>gFfl6M^*S4)NS;4s>EX`#`zOe~Fwjl|R=8e64_g1N^slZ9~X4zfzY#UdXJZ ztjQbSfqiTo%eK=098cT0?AJ0`0lOVYtS0^^N5hSOq+_#_rCp0CMB1*%ZDIZmCgLP z5vX81#)w_hKWU#fE&-G^5W%tv%&6JL_8a1>h3Wc(e{c5$t98jw;v$yx%1=YrV^7rU zJvCGD$}@(+yvB$Ad6E~P+~6vu&U{FkqxCx>bFW;X%>G(LHaQ#(#W8U%qDz!^FlWBZ zmHZ}AN1JqR(_xZBr@4XVcOvF>g>IXb;04%1kT{ zmvZ6pUNA<8-dM<8ShWV@q=z2V3Y7GiSALUEl%C;AM{3eo!@cqu;-llPNY{CWD@J!# zcB}Cioh7EH#n->2+Bc9+qp#lq=#T!xDFH!+F^?@c%XCksv@sfs*ArU>h6(-&?FSNO z5eVfajIbo-{JMUew8fw)ugT6Pi`T0r2c>p=-TZs9X?rk#twUFT>Ds89tn1&R`62bd{GfWTzaqUS~Ix4yXz6ps~mszwG)=j0kBh$j7&ZQ={ zd1x&dv?!1w`E(K2gDQ!rSWlRD$|bk)Ln^M94>e~`Ch|r_%nmdf^&+wVkTl+pA!zpq z3HxFz!Zf>yLshOezPo_PAUcK_UlIAzpJZpKHX5Dw-3#hR(Kn3dUlE${1w$A=1l71` z$LWUGy!FXc!XCdZarwwfkJXNczM{lspJji9#REY*o@vbQ9OQg4lan=?I7dB9j|o_BCWM z?ULASm*D9DG{s1-UxsEw3ACJm>!u}QnLkgOZzmOlGOnZeHdA+T$WK_LIH;t@5>uRP zK77ZArJOun1h{*M3S~@9^5$L8$ic@+6lI=2;kh8;!dyc@AgySLgOcA!w2|yxi^(Nh z`?^l9f*P?gDC$1>qKh9iOrby6$t*F;KwgR5{1``ar_2=Q0rh>%)wGCmUxx+bff&88 zQaMT70QG86li*e@JKopvRqA5MF=gv``HxLfb$hbgL4p2sVF^Dig(iLYRZo7E5Q{Gw z2dS6p`z-BqQh_b8lD3WGYhYKP+(ule!dLqbfW6+jFwbu!L)Q?E#e9_JUl;h&IEC8F z*D#mltTRhW@2G%&$<^zE#+D9C4#?dY`kT$L@M2;5qLCHwioFToiWX~^oZOy=4-y!( z8S3+(6f5AT;Wy?(zE8Ag@JXPSqr=ZOvl9yD%3C*uh)e{ijLEL!vo%X!< z`A&PuTbW!0QY$vqcdE|IB#g@l?b7t7X@k%^*ACl1?-xvMFwB|C6{rw31Zi{)5E;ig z=Rj2>nP>SPbQ&f0S(#5Fo|k0RAQC;nr#)qTDgh&N&X(zRZTI4VQ(NaZln2`lZ2i2dKg8i7&TZ(Qr=2 z5?6P}zuxi5F_Ia$Wi{d`+C|q416<|DV2yLs-KKkL7pnP>;X5Lwx$z#F;UYCSYgz&(Lti8Dd1ttiNwI4<-->J@l-?JsMwnB*0ikC` z4mVj4Q5G|(xQkfh`d9?Xh98fTu&^K{`6 zzbBZh`o7cc2U8@jSk@z|W69_*Ii{HzH$}P`-Yfqlq-q0K<3WyLDXWHL6MJSwncGvg z&$j3b8!WUs88#jwt<+_y=-cpUn0YZ>b;BX&LuwGBw7y7XT$KZU)#i>{Y<{?>j{eGQyL zHsT${-zUfQ@kLMNc_EMz3K76Q*#3n2E$pSP-h-QX0lOKh70brs*bFip%U}ZLQq=P<(6Re$gG*!0l;H0RzS#ox4eT)7T z2%f}=cJ2BiA9N|q{gz9$6`jw;xrsH4Cl=IKjb1rZH(p{ioxXMs(x00n>EcZf-bdju z_wH-oY({VLeD(}!+ zHLH%^@0qI2VfrwwKkz#hz!oY8yKJ-Fe6ZMCX70j zuDa-zttmPArU)$-^ogXT{OdXlx9pR14c-vh3Ejf^_8W<}h@~SQu$j(AGz*YxEsNBk z@)9^iRWWhD40V#J$P4N>1I~(RG^dtYsh{)#XzMv-4`g_d9!$to2WMMMJ2Hwyan2Ml zZl658S#+m3mK!PKr#1b8I5lV|FTm9`2m`GU7~bjxR`0jRL_?tRo4kaNn5Xx?I26mS z(?OJj3dbzvGatzM6fBhzF7CC6tfmaAVUUR1TtOlFT0~aXt14ZlchnJ$!|TXUb6BZm zYqNt@l+K<1NgQbstk8;C;7V(!Hbj>_E(O#YJ4$MFYzrwJffPEo*DqBw+xK)%EyDkF z&|W@1A2bTn7^}yT@i8&F%b5r{I8hv|RjBmc`YR^*c7rJ2a@Y%^A015AzW_^M8=d9T zaup!Kr<2Qv5M5!8A9jhd-8?sgp~9~YG)O$wLKW^?UHFM^TS!u$=i?b@z4X$u;SC1a zh<@X?!Bi4Q7JbT?h<+;pE|w)8r-3NPt*gG!v!~1sSBIQW8`%Q_blPGvdXo;S&|DJ3 z#<7edVJD(@Y+w3?d#Xgb#F0dIljU>t$8NXbKEBEj#7ZoMkm5EUi7d(`bH_R`>noL6LO$DQZ%BolzkLFW41{wr z5dH3DZQ=TN+bn%4|T*P;W=R2W>h*9U%_mV$yaj|$wwgPL$)Vzl`)fo1t*9|{cpDovl>Mh zXF>cLVL090d^D@Yw`nsR7}BABW@xwDkb&GA&wW3~W2j~kWn@@JmP>0byj5|JG0)9F z_z9#**|~ZGrNN+>P{gVBfl6j?$TZdruQc!LrC}u%1h0A~+gli?^r!)T5Xz^kR(ePw zi)msqmZ!InwDGpbS*7|)jzhcwl!KkOIfjCG-EJXjKWBl=DZJp48@Sq_jW_5fq57ju zD{kVVw)!?|=;t#-v~}@fOD|-WR0jWrr=TSpdPL!)3|d)teqitu8Gt50KZx;r^kz`u zG>GOvv0!78@i-Yp=UJlaA3ft%CY-HX+n^({yoOixkHPaa?)Tt1mT9@H-6dOj2dNGlJeJd$ zGh!y`VA)D3I^F)lR}O-eZ&d46q|Fv+5HQT0_xMHwDTy1(ye^mHKAfzF9%}}1&a^El z5(jkrj1^`(_#}zeOB{+G0tilgvR-pJI1I5T9&Z25$_cInc6tapj1&>gylKW!W}p|X zJuPGV^ef4}GTn{(Ctf4@Nbgo2ayvD`1bE>l}Du&^X@-Hc8BP6qMcVdJp51?zMGWk#3WsWieuQ}>wfdA`H1a3 z207C{l*DIf7XvuyJqkJ6Hc|_Ae$w2!jsA`E$>!NTuJxO z_A82~2$@%Rgf^gn8j>M9o7(n~k+h%BWi&}*t0$dp@UGUj^{U{Z+wR&rREEgVX?cPC zTJ8>y-|9G-UJm!2wMu?V7;U)EUAj%~(U zIX61m^~PR-uB!^5c2WVs^_pflO?0(`y0k~fm(+-yFW|iBfhoCo6tT15Y0ThSOOL*4b|L}d840~gB)MA(^{KZw+{6DFmoNwu#4Lo>J^#y1N*t!IS(c$Z$` zhA9*l3E7fYH6ixOTAfQMY}w^y{33Q6V@S%%?YuX>UomjHv}6)L)sdU8$`(0E#VgOo z$4N?mpvg#a*|AhKhUJtw+u|>QgcDFhkq>eI)9-=zIhM?Oke|mov>`^13@m)ld$hiS z-)8hfj8`j(BIBwBBIxkNM&-672qyqg zv7ucB_*}jHpK)-A-_-u+zInNhNYB#Nw{L33ESQ6|9 zug`GMHO$bab;65Wi{7;P#fI>o@@sH!#iV*ri|22Afr3T;1qgB=0LcHj1st zkH!%tnHmT;itR($cq?S=!Ze5}TY5NJTf9Yc0PMuWAbzO_$-z8sU{*V|}T#zlY4j=?ppjM)S zEKtfK7rI72fG~81o)|KDQz@TnV}FG2m2fra5AstFEi29GDiBdW3KN6z$2L9&CmXaJ z98v4c$vnk9#E0GwL*yzESySSRUfXz*k4fEl7-gXc9h+JZ1|-f`+Ps>TYUMo7#~LuC z_OiCpq`P~S=^C}`zpK1nAQ0r*k#Y)f{0Y7e%&U)JN!hOP&d&lU$1i-7ja^V6-I$y* z{&0KEroZ)?&-O_G-RcP%>jSu22_>9#nb}lt7(bpCEBrb&XBcsw~5zLn7PxYQR`J2fA%L_baWD{F4i^oRg zgiu6Ycy*sdw8mu6P3kLJFXj74ueQzv&UGb=<~dHu=y_yFA8R|Z5C}R-KETr^2stC) z(MRcAgzJ9hMGyW?j<`tQr6wYDs%OdYc7U5~tmx_FQ~z0xD90RiVZ6FsyPV@ZnQQLiC`Ju5*Ov0vX-f4;Docdb4k#4N z=k~Eb&oLhE9dK-gBlsmDWjPlrPON-u2(ep)Otf3Rj@7_BW#0Oqu))UaKfBblG*p{l zEGv-Im-k;kbs~6U!bpA`CIiDmSrA*j*>Bx&8Ap=Lnw83YbUm>EN0vl3dO54Co|62H zw*-d=snjii$G$;BahI^rV*!?E#%?$pxj>m|{ysjAy zjPNqx1g*^^?K@-gaW}9Ha*pNP<#moWR!%T8hjs38q))!Z;R5O6pYXS@95WUymLFAD z9+#(wa&xI8X~Bq>#jG8l$7To2Um#9y0SI&leB?(>)roZ<`1Z&OF;B#M$1a82%yiv3 z1}3QbcmvqokhQS8Y@N)UoQzL^3;$tYb)gRKhdB787~@Gji*aiUflKA#h6;Xd9=IKZ z*%q&@#GoLRpR7;Ak4cOTJBrbUYL<-jU&H0p@60W<93y0nmk7^HG?P|Q(c^>sis4UZ zGPsT>aAnCh3Tuc>g6y;P_pa)A6o+_sW$8Tr%>@Z%@M&+X)F9?}cjX9PLu%|?4RcEo zJ;L)cwmdzQxwi=B%X81#ar(;VrzxVZji5vw%jUs z2Jp{|Y}i4qbV_$TBiUgy*Ep{~IPkO&sNdV=0HnShvc}fDO?$9Si~9e{m=+emiro>L z{6xN;iFjnkh$N;|bjB_l!Tva}S|cJ~LQ2y57Mw1Lj%T@*2nQ8nLEzA*Kizw6j~aOl zpPomxSC*r-xDM`+s+?1=Y^#h>8~imPSd1GrGQ3b&FQ0%x!u`CO3O3)+s;D3b+%50y z_fO2a5E-rZh-9byAGw0(_O@5R;U;Z9b*OuL?+64X_?;rPA=5;v%$@6kulaV>)qWVq%GOa-*cKoohkXs@Uy#FYUSXeU$r4VKA!g| zRX>Rq3ut`1ZM9!J^$DmGpP?vvcGx-%7XIZ7g;Vt4=5p&XmmsU}RN&ZJ^g=;yT0$jVDGCudTd`CSoPL38C%OKKN}_&WL-R9at-mUz=s?TKMA_ zZSm=V^JJZ0!~M_Bx*64_aIN9oavYNqcuRx(J8zXQHfR2;-y#~`i6hw^!0)2JTPTEF zS}NU5VaQ5AB`0lzKlV#i!9mm zhGu~5`~0cyWMlrny~#YshfZ`RV<9?Mr!he? zwI|9V6Im`mOU{0r3d+B!2WqI|<+&7VY#3H^zY~L>b0T)E$s_eb=(}#u(d_8_-TVgl zS}$(_jN}~lnl=zwg-j+j&d$0sr?9uH zyIKAvCn%w!)(E(!kf?#f&V8tz!^6TNVqj4fKd=Xq6RzC>p8`w5gnRwhfA z8Qz>}M4KcKe#uvC#V%$_Boa8R@b9ar`~;K^K4&I?ZpwO-ztpRCuNB^=dPj^~=v@Vu z{vbnfDLpo=47gFUZ1E4@BkRvYX+C_KqPu&krC1>QLTv43Lz!7*5X{E-lj3U|KU~di zw;oc8PU~n6cwO>gVou?ZF)5+9!ACz5NoDw8K|dSppbcDOylbd0ljzExJtg}2tbmoV z(x1gSFPQ|ogt_gjOi*L6O$;{M%+U^7!dlumi4c|4Yo^YVU**s{5ahPVqP7Ojwj~v|}=>anmxR%-Vx*{ z6wkg3#HiP{w&kM=mT#b%Y~n&C;M5rYa%v2ubyTVA;b`O&Nj?lKoIgD%ghdZik-m8k z?;IreQpX1Iv2q^;MjfR3B>~hP zcCxF_I9>c1>w)|Id<^5088hibi4U{etUBeLVLlYX%ssK+o1omI01KY;H_o2g4$=+cK|n`uQtsb?et4#=1TLO$oO*4B zM$r3Iu%9QaQ5WqN5{@ZQcUaeWK#)TyRLD(NNFMg30{jU3D!7^u=#uMNY@NoZJ z4`dBnYBw}WK6KwYDlGj##dEqq63!`!crq9bdB`NXA*52caXByxLkcM7za}M!BKJu(&gIv9rA?QX9 z$AyW9`nh1W!)*@>yz!nMb$98Z#m&7n2q2M~xHW0n($d1PXfgk*b_o*@TCJajR-kme z_>F$|2Oiu_bKxor^@GI;wVAzqP|4!!z&tlqokR7K@rw)(lm4*zvh@Nh)V)~2Wzh|( zdC;iz+8jNXvf;)tDzB}&&Sn^~x8$zxWmkMwATF*9MNRP z4Hlu#lTG<+bOMtSO$-H0Hby_Eig7=BYw4T|X_Z}Ba5r0fC50y;kC(^-QwHk@8b6ydkr>vJI4+(F^%(Nm{O}VfeIJsW`!*^f_Yx3wS@WNE( z5Y?az86>88i0B9N<5a-8X6onV@M}{|#e^&Rj@XHs0z5Tqn@F~!TMJg2`98BdWo_!; zDy6MvKCihagVL?3&JRrll^R?N!!c1rHCvkbZ?3N!iFcSMHY)4{xKa8i9^$CFRbdao z&RCfXB1yaGpWD!lc&y2I{OrUzGQ7rVZg@Mj^@Gslf0uWg1-UuubiiNV%(DJb(cRt6 zk;hstx%_-Q#FBP!2X%qkoL}8HAbO3U1@1_0c)Ru!V~q-P{;*6|4e`gus6N`nR$U!H zxxzBS7AykGL)$uJ`&+x>-ezDn==M>iMn$U`Zx_&a#2%Jx9!F0=N;)5;+2^wkPz^e6 zBau=Lbx5}s*j1iasFc42$jc1gFV!?IBu}Z;f038@SgSs(86 z2jw~z(JWQDsHNB{HPXx1iORES(IZp1e}pim5UJ_i!UJl~jE{FL*)s1z!%Q+0?R@|4 zdIrj{H_bCk_|`IeMF*WMN-WeB5|3BkQ5;`-sxJTL>;9=I;RHkj_~Y{^DhuB_O#j#J{!OB<;i>@*}66-Gw&i< z1{eWk_xCys0BZo7*?IXb?~T4!jdS_dVI5XjaeG(j5;)?PQvz`ObsEM z$lRH%Cu8%p0!T)-nFpOVK!=W2{?Dt%$^E|aUpMTzxmz6**uT^4rW~@%uaJ;O;zqbN zCK_CfzU@>0Qp%lNZnD5+3*Lj+iXyn&5^+7r+g*)!h}(J#{HDpjOV@#ip@*koIduCC z^fucP%4cJ|fUkX~elWYh$Y7_3liU5r>HYOwCFA`0!hD_|Cc2Kl{2MGi{bINI&1WmD zFmu`w0O-t4Uw$MRe4(S&J!JlAr;nourxhM5B=%oa-k6l<2gAK&)js+v>Ie=X>HWM+ z?t5nUHif6W=MT*okFt5~G!|xf)Et4Gu-=YHFVs8Oll_BfyV}0*PgS$>7sWG&=yCB- zz*=BBO#Pj4*6Ww2eeLqxhz+*RpcO>N`Lq9WNOYkp`7FZX%hB&>eCd}D0u?3=i}!Z@ zULBPZMC16--?=)>1MGf9jJ<`2yEEY%EAA#pS3b|;oahDV++cugy4i|;9MIVJ%m0cq zKQnSdWc>kUY3vzBziPDCDCGf=FVm4|8`;C#AP}g!_EO-}aR;9z+6=o3q|od4(>UMF zjGZ`Bz&0gFKVwHLsLIdu#%y&^SlHO6EQVm?zelF-L%t*`gBSw0k4v2 zD<*AlQH&b|H@&J|5R(XC2G z06MCL%22aSL_aHWk!@%ciXXSZDWySAQm3Jk8(YSWqX{()lF#_VVNhCgBwRY{rnP4! zD$e8hw|r{&b{=LD-++3M>9q*orhV&AkoXo0LG9bi?^tsoQaTcQjhjD*sxg}LOS`z>;rf$) zMgEzUE?T!=ZB3xzVp2OwC`*p5 zJYx6CGbAQCtb95E2OxT+3sF4mM&clrn5@EGgDzo!-8+Vw!#@v^r^>hDz3N&VM@aUe zhzp2WN(y|X%`sJOik+1^Q^0^+oXpT2@1*fa=?RRO-)lw_8onvm7m9@**SXnJrp)De z5ZQ;JW;mgh2PN|Lt5lmsAL+~8y!p27CiXCg+R3E)XIN4U!sUcrlyHj&LbG*mxt;l% z{c=#>Dw}|mxx?gx{GtptbUO}Yyc(0NVNQQB+3EB5TL!Qmf$l zIb#Ri=S>AIXsb6+X_4n@2T%Ckjt0gd*GhQDSWy*)wA_61(-1Oztu1_wr#?S)>bNc+ zp-m%h1^t{D6*0}HD-#fVZwsbk9WXq8rGDuFOSDbV$U!-sDf{tK_3l#AMWX_#yLGfA z7s2PiwZY0tbKULzu-UAl(7KyW>}HC73Dn<1ut?SnVJ5W=ypUoN9F>r2h-x zLY;^9yBBosXd~KQW&Q%CC7nI_rK~y}ecxkI6>ML8takhw#?kIUE(feU)96VP+sXyM zg}b0hJ6{PR;>J1oQI2b~F|862Cjj*K`*ak)dvW`UQ8b!oOwAv_wK}XfsPuUm8tXFm zw<9Z1$IT@-x%G?NaE)g$DmU_d*lYy`8! zTZSg#R&+vEp{rBzrw-TeW}Vs$;sZx4?iH#D@?wbhPDhJX!`8B-J=vXZYf_rxlE63~Ac`s@r$AT&eHl7m513G@_wOY0 z2R{s@IRxl8`Tw=G3-c2-eNVEYzWvj>`jlq!=HjU2yd)uI3gO;$WqEoL)sf!kb$?-B zD)-Npt0uwgdZDyh4|A`n4ug`~`o4H!HU=rr=e}O<*BS6yHyZlfSwS`%2uhEHJ_J}! zmA$R;g=q#kR;Gho+vA73^vykN{+ro3z_X^w@MkkgZf~oB7g;2J9W33h5xy~<}_3m816Rl{U2bBfO!?!B}P4p>Bn;bc5{hi)Z#G(%DFzmwo;uOnq z_3?Mb`rbwhSoA&AruOd)ej06rKv$VY^Ebf7g}dhWMNypsa+1Y$4{Nlu5pA&KQ`aiV zB#@HLI^XXWqw-?#M-9*86<(Oz4RVganI!e}s=NPK`2RRrYt@<+FQDUY?h5`It{0~l zW~@^?Cz`(j?KJ8;x7P_Ep!UHm$3~}hM}viOQ1PpICu*xBM@%*Z0?QLaDg5C$qhrwL z<`=%*{rbSbX(LM*_}Wc+3nrR{Cqbz;OPFr_9UggcjE-6z(r0Ifg@Mag9o(jLN{Pgj?Q5=wxWPMVBqPd^=6brO#aP^f~lmkEP6h zQOszIff6($c_J}++z3_3iqU$b6}2FbBI^QI)50_`1iyt8dD50QdYW#-a$Jhe|C1ztvyHcKuZddsfBRtNh5Q^BrfG*sq2V$X*&toTF79ZkdMiag$#v{oar$Li&au#n-c@OL`*9hs+5@^0gVcA*HHKUhJOAglzip&LW7> zVd`A^@in#Q$miqMhm@$tL2Y+ip%d+Ho0HK#)NPuU?xiAL_u{&_>!EZplBBM)&#M>L zUj^){&@jTqNo{<4wEd9kpVSCGm7w&jEzPrEpXhq^OcsN|A|MIQ%<~9fk75=eXktZc}sBr zjo`y&6GEyet|HFR4yonc-JAN}1y>AWk7I5I=R>egddIZ;ZR_bz6~1SAEh3M}j&vXQ zvYSrp)qlhRnD+>6`gBzJf%Ek_)cu(plXlR3WrMn%DI%QFJWD=Z<+9t}D|4rJEdQg+>0WkVxN8EJ~+ z*$+*st%jVuuzv|{?!ftXa^LI4fW)!L`(2^Lwqa_%cWaCa#s8gx^XT}`ko;9g`Em*{ z@of(Yj;GYg)y6aG&vRBacz~znpElQF{C|#C`hS$YWmFse+Be!3D6}{fcXugL9E!WU zrbuyjPm8-3m*S5w3uUUdJA zfcnY7s@Ra2hr5{u3KC(N%_dwfPAsxx2#{$pO!J>xLKa8=SIk3;^LD}@XKk@5h8u$Q z4g8Ta{Q0;qpZMcH7qh>}$iLicf1mzu3*tYs$IF+MLEkXkT|BC&06+nVh|XT?G;B_X zigT}c!^?S|H{0i=-{mam=)?T&ae{yd1@g1!bx;B~a<7l;piF1v?7k*q_SZYKsRJ@M zazS-S0Gf#EiN*iLoYv`B5dK{jBT~$|1JPJMsBfE#6de3ReIBTWe?senPZx45BYY~} zz3F{>A+JT3(P&WTTq^i)zID5y75;z1YRZ7S5ivDye&^%f&q_bnyO}S=;N;EKOKN}+ zO!N$re(}?_n1e*Z-RXtGLSf=)ckO~ZhY7cjfWX#me<;?kt=;l1uZl6$_GYvjpq<6` z!07+4kV$ciOZ#C$2j9lv)c0}Ll0u2V?eT;V!y~r;z?M83tlhW#u;qjOhNX@-dLQf7 zk6{A7qca=dc){IWId$c4)0E604e0}K%5Z{+=JC8e45FIwZ9S=5(A>MNj>6~7>J53O zs#NF8OdUoPQs`ok4+hIbU2PutMpNf~xq>UpX0qx4c5GipT>l4DQl{Bks%>H#JNAdW zZTbnRkgPvGc>~FhK={Y1HkpuCi9&?o{{*#9*##qb%@=g~nczW-4rvBkFnUbZ8o2w- z6#AgLf9!|MyJb916kk|`U-Rqf8Uf9eXb8!~2yWhQ8g8id4kT96b%~86=Ej!P%TR2- z`~$f-;XMs8U;Z=CA#_$T_lI#MvRdJVt+&}CKm97sNW8FMxQ1gvm~i~}pCU277Yq^l zg%A*2c#&fT=xd?yn*1k;&I`#2{0%9i`~8bSSN8%shmtSoCz6(F*YEMafpB3v#jX!f z>gTFUBTQGw(rt0?%6F$ksd_{qA>p>B1LqQtg{Xfpm7Yt5y$UY}R1Vp6fg>Aizn%=- z2-uE`(YbASx?3#)51cdkKDkdZ)0>78Q7@2k*`|-B{8W@>=sVD}D*;ne1MJs|P_n-* zWl{s5&1kIEqz?X#1VP`*rn5J=!&qkxs`xd9Y5pN@d4EoO;}B$Z%ZYkEdwdz4o64LE z66RUyh*5R>#V4vgM-3~7zku&=HvmfaN*fjiL3&8n#7xPNT%H zYgmfrehNkp-${@Mp?qwu{vp~w@v(fKJ(xebG!D|&rQ|uiTH>ZbayXXw!?9PkP~3LW zl0NMN>3w-8a226wg;Q8gT3BeCGuLU+uR@Kq*aPMF4f^bZT4|dsIEDUn2I9cn7AwNH zQ1EOJPkZ!pMp0Ux3yQmelU$8YiaX1>VB?1B*q~v;F+?qLz4qHn#20}1@$Xo>%KV-O znZd9|yca`bJs>C0Sr=t4@q5xEhPQgNp2A2|^v2ME69`w3)!t}T8$**Jm z>-;j#4qn?K8oPTTy4!_Eg^L2-;SEL=VKPaPKUaCtbo7c0X+%h7H7nw1%k}PnF3D<7 zICLiYI$dG^OH3N?(VbNI9)h#`FF4*ul_e4(^6^CwX|co8XiiD1+(YVcrI{#*HTE%7 zCRnZ8a>A3aN9RHe@d!KXrkpz}gxS{blcKBZ73_W;XBN%j(Fb)8Iz_HtyvcZ5#l-U+qum0 zjw%Zb%Ax*(V{lH@Iv~t{Wm2;@E&{!5b4ii>ENuhUvHZNuKndD$n$(E@fvrO20$P0N zo=54f#=)=^6HNDs3~Vi+A?s1*{y@Ju9}bP;6dND^4W+?9OFAlr1%{44_dxpd*XSloLW2fH?~rMO(>lZo9#nn@vhl0}O9HRg|M#C16zQepnBcq z5+thC4efUzlBhqa_rU}1dJB+n1E~i!PFygzHC`xm%@jMS$J_TrbO(k95uP3HkspJc z?jg;gF1T(lK&3FyeSqfV?6UG(NrqH@gN@~XB{?Vt$p?|6e&#lxA%#UHrl!*g#bPOh zF*$k>2q3-)1Y$_?&+z?Rk2dv(dvlV7vl#qmwM?@J5F0Ln;+IlUZfmgm4Q{}hFkGA} za*bAv#5oyKX6Nwz8qf10+QT4&dZkYFwJ}XcYl2g<}6LFb2 zBXWTZ&ZgQVwIn#A?t&pjj$L_B>Gy59X71`8MuZNoYnAPmf{3vdG>c~S$S;4JTf^5J z?8$xh%rO#Z0<`W%Y;#F3!AiId~bPe6C`GQvW`j~w4$D?qnMZBiPf?1 z{6EWM)mED#vn*X!>{yCNYYfUPz+^fzVef~(=p|-@b@3L$RjmZ8aD!j+<6tQo%Nu@) z!F>UshOJWswZOZ(xm{zfV3f{_;O1W$ims31`l!;<-7(z%Y2Ye^G;mSa6{tx}Gx4J% zSt)Qx33eajJ{-KB>kFPQ5pffIV+qbIilJjFM)NF|Gmbi)QNvLPk0tnSW&?d$XuX_1FOSAESiw>Y0Bw zCjzy-(Ym?4=@wDpE7XcWnaboU7pM}RZt%`6DZy3cW2buYfy8!hj1?=%AzgKXa3?tA z4Afsy&quA$Gjt97w>-YiyoRS`3I-rPSV!Rp9fv&TQqNO0*7Zyul#1S5cpg5~ZExRF ze0PEF3vT!9h%FoFoHL`Msq#I6RyF1n9(?cQkKqMwbFOm$O{0k+eh>BsCleYzVa zKXQhsW63$bc%GS=znlR@CK2P$kq*G=P2rB6AQgGrSI=a1v7Xj1o=Bl!%)sibHNvXU zPb6PW5tQ_n0t`st;{3ZS_+1f$*=OV&q*dlEyRIiY%xSx(VS~@- zG;buC5MAnjCOu|3GIBKf_fIO!-0ZWR*C5S&nM}pv(<9SMaDIj$JUb{i0Jb%SY0@w^ z>i3U8fmgmna$R*fOPqZ`&!o5CUJ$-8c+1X?6+&CSkQqARs9NaNo75WDqS$pw{ZTmJ znKB=}<_*tz0A{E=4dI6dDLa}c5uFKLCYr+TjB{qEo(HuZkShOZAp^gL-ci2J4NfKq zXCAAwcIk?8E+9aDa(xq%;>m5M?}^8(%ybx$GrKO-GegIfFNUDMUmaJ3(hS z<|n1J;0Gyd(g(^Jt7tRgH`byY`ODq3Rk*GKuR48%=R%qHAh8DO4uR2|!JnYPtR|dA zGRNr>iypPR(j05CqRLU_OS{A>LO;?6?hLnJy0}caB-K zDJX18!aKors9&rsIN_GKbCPAVJ|XsID0`shV&jAOI@gsQ%mree*Bf{@Aq|cP6x$(D zt+;)Tc zN)(#Hns#a|UzyS?JulUNOtak#EkSs>f-~n>QO$*Dw*pZL$9sY7BLDz9Q;9_rrnbSt zPw_(O-h==e^>skL9W zJYY51PVGc}sJ*wmPCUdez}>pL8HgMUIfts*l2;jna_BLB5qLxuj8O4JxcHb zMfC-mzj=Yy%O{YD1{(>L-S1W(X3jup)z7Q7gFCMK@|2C0!sh2S6)#Lt^7g8U)HD;3IuN*#b)T2lgH~ zz3sR7jj0@W{+pA2JZ1tUkCpQXMPY>Zqj5Jjc3-{V?(nQS&3((av;u*~medaOo)l-*#Jg0#olcXAy|097!2C5uUJXh2f z5f0ezRO@uAj5;M8bMS3xex8LSI$E%ZOT%xV%9Ke9|8GS(*)FPn&+QTt z?6otpX+()J7W&&j3E9w5s=RC<4_C8rMn;_9uaX{L+JyuYTyFl6SzTYl=K8lFcG5!^ zV7RL`9!YI~tYrTnJAe{~&I0?z(}*!p+qmHA<+*xruyK}}JTx;ilj{LNSjVkO$_yal zFDr=n>!rnQimK>FCCFlz=?Ga3fxx0YN__x@?-+mEo5D{UuZd0P>LW!J#=M$dSF-;O z3In1tqi{xRoPM?j_MRBvA;O#5MB_;T&%LQR>;Z8HB}VLNd^mHWLn;+q`)t8@e~@zn z?|d*qKpxOa-(%BFcS`_7vgi_)r9x|}fDQAG5z&414%7q~-J4_KZ5j1hH5rHolspxz zdj?~8_2JNMyT#KoT+Ft=<#)ykzNvsu zl-{~UxkatHyj>;jXSyS%AJ--LwOaW;40wq)JbLVQ4ny9g6#>fV%U?VbrXZB!q}meR zbn0!9ezoOv;R?7!wshO+o0g9G{CSRIzIJtahoM)1A%p)miPzE#v7}Qvy4aQ=_x7ez ziU)NoT_NC*$a6Hil*qVO@HaETgXr&P5>^J+@m9oRFl@`Rw7zl38HN=Y%Sf4f#>qS! z$C`zHzPj-0*xKUQqIP&~Pfs=B{zsvsmuGAHv=@caNZu1ND>JK_H+C%v|I80se%QQZ z4RTprXx~cLL!rH3zyOzK9nEG&R3wB`u|vZXWtQ9kBJQ$}>HXYAIX|;Rh-y8aex(sr zZWhTlsWD^29Uu12;qVbXG@kol zNXYDggulWDc7^dLgB-$B^De4VJ-0iTI9R#}ue!G|As$w74?i#Jo@uA|hD-^#S}L65qxN0;q7kwnn8&hPYEF&~SK^50|~V*m{H zhx!c`{>Q2}06KBf*1$bTOY_oxqf}=Ndk{Q%x!V(IMS}DIQeSFOTWrRBt~xv*A$}ou zEmU!7SH2H4zzNJIT26QMnz~XFU!^)5V^@eFxVbZ_Xm7>H8*CrI zu5N{YENyr*_72jmq5qo*1QJ_ox3^Tjf?4>a6J@zS_Sg{MCR9UJ!|tw7C6ES7h^wc| zZzFH%dq7(tQ)1yy9jJeqpy)YU;w`m>g{bRsYiQx~R&jjvLJGkx#uCr0UD`bPZ8q-4 znL2nGl_c+Cm&Xi#*@FX}j+LeCZi%2b`NQkOC8lfhLZQgf*#eEUMyR8|oY9Wghel%?b8r}}=;TOyJHZi@d%UZFoC(!+J(s~I*6T<3r)`<1WY$X}<3t9KlV zCmELZyhnNlx$=Qb{55LU<}zq`&CQ8)sKi7v>iyU&gfRd>FG;lF<%zfMz>cK6>+-0+j-O`vF^R>)utKY7)fzW z>*3n?6Vh;815wu<;HMv}LK<1>rLI|yq#f*H35DI|P`lhMaMNBd9V2AbSavK=DW4ks zR@4*J#t2j>8-auP-tP~#ToDM^nDOAFd3HXAX&>K6(*AM$?vem-A7!5!M`OL}7kSkG z$SxOl`d$e*pxP^=N7GUwaS($BVwruq)Z6V?i;AmW`VC(y0*7%Gqb`wZsGp!KR`AX2 zxGm@i5R9^WQ!tIOudRgK$LSAMAErZK?r=qDchK$adrMtht7`hbzx`i zNT^_R)MXOEjoNLDmRx`ztNxii^DrwQM0pZ^>pNTJ?Tz=H_76X<#-?6D;hr5-2$AKl zKJx{9MnhPNI#bX3S34RmXWsgDmoYUKD~l{8LpFBdlPGny6+n2f5n1GgbU z_*!|55{n5QH{fpBL^rJvQp~DR4nD8k8Tx~tm*S5(F`^J2{H1RgWgkvM{ug1OQFx-u ze4qQuWl^w0@wgNP-i%>iGV5~90b96dilhHPF|Jtfl*17!?FPP~`);GNXP#Ig>B{5j z;cM!1k_hj+gR+RvPYso>%n2P$g@QAs!`W8-Zgbf}la(B+o%{*qYWvO2gdoC)t!M#l zZ%XOgN)X_DHXfkgBV_*^w3zKs+SyWcaN5YK1|4w^Dsf3c3H=#v86N87c5CgPJ1o9iA4;<|f{&R{+{T)*#gzh_rkTE zo>!Wyepn2c99o&ox@bj5f;6sk|v35R>C^3%`cT zkrfaVKn-v)EW~x7%C^2L$HO?913cFIu?lUhY-%@`r3$J0E_xjBVS!`Wx+k!_R2Vv( z-`#R;xtiXM)S2DL*O}B~^f2B1?6F~U<{Rg)D`>7LjPi64eyabC)0(KhqAKIcko}-a zbMm&oXKw%nw0wDWu({EoWB`IM)k1mL)U5KF_o)x0$>yl)HXJWgd6E0xG7~qYL=Yl{ zlvUPaA{%M_Ey7zAdc)d}towtkyuVrl@qrR8E`t!oMn*fkP@zmfr)?77ZMa-SK^uY8 zPJ77h@IKy8xo0{N)uQ&ZM(F|S3@UBNW#*$ zT0VNQ$42lvtI6w|3(3Y$c4u#{3ep$NK+er90Oj4gx~D0%mXy>tESS$iii zP(8~%Ms&YCN}41BmVYj8yWzEqhpyXweUj6jUwS7UY9GwCVS;%>GPq}J43AFKVH2?w1VS!Fxhf>rY2G)HE$7ZC@t5+x+LpW~8O0w58kv2_K ziJwjCi$V&*YLvJsrQmU2!mu9w| z`~3CyO=SS^z7O!IKB$!Gme}$$4}J#Kz6|3^E!nR8%?MW6M`dvDA6^%a`4(9*C}V5Q zc$k^-50SeOKhV3P8qCU3Bc{{(>1Ii`ZB2J8DBVoEd@&?YO^x^@a6mQG0kpIg8VftJ z0Vc>nSo19r?5Ao(dh!X>BO7za-5ra4xH1b*8P@si9ISjod3&FY3v}Arbi2?M13!Nj zVwKpeM{J1KZCigNZFSl-g2~EY>gkS==3LQ>`|;NI`1G}$a#QJ#HOa5N%j;MS$g-Q`tj>xx3C+ALpsTi8{gl1)C%QKsL6OGboB8rDtxaM$h}yo z>iFsHjlHC9=iCRGQh&Y)^z!Pn!4uhgJ+byaEd5^k^8GQ2LDq2G8N1d;e#j|kQSc8b&Vcsc;itz~wuSY? z^HWkY!5piu0z{joR2~i`-X5jq5QC!XB!}*cD=GlXR#{xSBEjRuLRvzC*^KsWAZ30{ zHc;N6x?S2OL;yD7^z!wb|6jeona-bsTSvI)=7b`y*35ql{)gKF`Xji&>i7&;oahnb z0eVSig#=Q(`J>^UK>?J~b;i~kY`Fm3mYrKa!C5%q_&4^WsxB;k{j(u;qyQ z>DXDT3~Jfm4h3{{J7Gdo!rgnv)cR%EjTB-~t9ZW9;gEYc@;@TCmlrTI_=he3WlXQ; zof?@|e~yw{$2{Z*;j0HYNO>~$g(oGuskfkCIQh1G7gxUp)WnvS3bj_py_kywhNt-h zMdMphE+zT@?Z6qCF5&caujL&j#>F-%!rn2g#!LQ;kcYm&dQY*^Fe?L0=PW`|5VFtqBw-G68fYdC zmfPtV4#K6s#KipT!mv5{A{T(%gHQEY%Qn+=ittN0nF&84q<*V%(vJFpSzO2Vx>IZ| zQYa|)Tgu-E-d>@mOgMbM&D*5jd6vd@G2kgXh`Ho%uu;0*Q*Wco|MVLCe^CL%jejiJeE$l|lL_r8t$t{Gu`tH`mo5TX`9IQ~=^+IICA6-I4YUxw z5uVgR$o^XORp;bRrop-=`6CImd+{csn!`J1dLSX)*6VXev z4aOMu@D`NEMDgEf&<(~0Xr6?%)5Z4z;i{bzx@$vGPBv%T{Ic-xw;$gX@GS99t4)yL zO2%fiLFvDRc$KY{+!x9W9%w&M%b}G#=v8&-+|lsQm%-t%@6VNZJ?vR6UaVJQ&j)tA zZ)8liJ8Yf-cf{}}ifUt@lTw0~m8YEhW&C?}nc^uC-zHC z2FE2p_3GB3Xb!J!KN8S*Ez}o>;_lp!h+bW+v;(+Kz8Q@mYyvR13WaaV6oZJ|iF~+C z3l{?Bf_$|Bpmjul>ew{%`n=xstq&Pi8lHWW7eq(ACmH=T*7Rb$>v8d>JAblyQ49C;JzRR$RwJBP;)(^cl$E3O7-XhR>t)Cicu0U>p+(QoAcY zG?tINO-jVUdV~1x)KEN(8IS|ZrHf)$r@pssxvC#N^pV!o- zQr5?=AxA&RsOPiz_X7c2!et%q_g*wl{mNnm2w}#BAAk~($1y|>dswQ+Z%Dc=w^UX3 z_rHqBHVviRv$eKy(8gT++x(Ck{YpSiKQcvOS}Ml{`A~WoxO$Q7O=FX1kLdUv6o*$!SGR^yit5E z=l5$!=F^A2wHR(2u4(ulMxI_J+l$m2Q}&y!6)DY#{WfK&KL3uMb71_f(1jHZlBD}m zm&UEpnroYtPLiNL{s6)r9~KAqFc1CT>F9h%Rfbhc=VFRpFKZUHS$A?s^@X<;zrh>! zGR$JosvVw@3u&sKwpLX-^oVznL~_KUa{oKj0BB*EsJD4dd)Oa)%EcEjR5N@klFM8` zu7iCAYMEIa9>zs)X)34tk-Q{4ha&?BF+bQO`eUi7Uu%UB4kykHe9X&8U#At??vy8! zW&wSZ#AE&Vvl64{6K{ZB;gO1UplR1NE188u=DI>8%;H_tY})hB`=;g_+-}~{J{lVq z(xDw{*-}hf_?!k0-+j54lwDex^dG(x{WOZm;7WtF>qB}Ysh)SP6doUh9w1`RUK=Nx zAibko30b&IKJ>GENI;fXKh6D>pi}c^@ZN0ZCXed_o&?c!+F^Nz)X+)FkaB^S7fHq> zQb%g5oao6F`IqxN1)g7;Eo$%8-5k-kd26RjI5?q&_kVcRYY2yqM3mM1=lGBOn$yFj0ADCkQ^1<#dd9W{V4s!OtmG@T+U9K-?o5=um(} zU+&G|$lm5_@V~rhNZBQh=m!_LP84-bGgCF$_8>F%eAUFt_qJ83&KJ5Uj674m(7d>k zLa9>Tki1+Agr%MW{2bYpxAktkK_Us+?{su*@;~W=7-OpnPxuw?z9w$}6J8Nks}Y&? zB|P>siX{`pJ7ThH2<_N`LXITmOx8KYJLM(H{2SxrW9OJI+?KNcY{Q)B9TI}L=}(DS z2>pEQz8K!it3Y~$Nv%m~^xuAb4(d)Y@OzGPbDwteq$``ZMkHMK9#ffhlU;57xb1!8?>OA5jeXZIHx;LRf7AHaN#U0XaZ)gIhs!eD@AGgm zeATOB-5#lFydBg);-^Kk5|n;Z=^o^|0g2R&6g?XRX(UAl7ZLEB>OE=r&2LZ5VA%}? zZQB_LuoQ|BaN2n?AW02%LZWZ$ObPQyX|cYpAZ=hh#}07)Yz|2PAWYQ#qX`4Y|6^GY z&AmaaOCtB_U^p_;C;Z3QFa(b}@n~#zQ+&BV+T-7?lO@c9YI7@z9XY^QLd|oRO)poa zMKSU>6@VO{{SJ=2r3r>-Wk_Ux)--i^#g!$7TtHgiu;ew;V={ey<590!=eXesm|E60RQ6Dg*%Qf z9>$3c|CkZ;-jxAX2QSr@3ia@QOwYT6j)?WC-_6heDpNyVVx0Gj%ls(e=7bjl+Sh&& zCz%i*_|8P_S`Sq;C?qsZ!B+1{YDgl`Hm0A_4tM9S3ketKb3jLcI{L;&YpYjhuOKy9 zxc*OyP|Td$AHyWw;VBIK#G$fk(zJ`^CTOtp{M-JOew+947GkLo_cj-Rq-?moWRxSV z5mGQdFZifle9-m_fO+HXPk5}hOZi~ce6(durD)ckc_^BAA%Or92>KC-g~;If+$6E$ zesQ(&I^Sm4ACLXmNg6E0hmf9!aowtWjLO8M{D_vze^cwEzM%!}G%3|tHIh0@#>0*Y ztP@XmVcH(2%C`~v{^)7x=u?v!P*7;TQ?V3>r!njbwhz%S`FD>j8)XLm50KUj3b|Bg z#(|(SY71&B#9lEgZV #Lq)Z9!|JMMrxFgi+$>v7{#L&_dq4?Y%iE(hZoOF!IU_~ z4DYzLdY+AOaUi<9=>05?$aSNXoO49Ohv$E@E6qxgFL(--y{THSi{kXWG|qLN;8qeS z%)xwDrVpofMF58r+A?0-6i$5?xJhca>QP@i+iK{7^-1JsY1Pd)7ZImu*iH3xKFw_Y zhd}?VP&T6M*S(A)!KQ@i^KM_>2<2|ueaQGaQ|ZPm!}`ww(2WF%eR(X!H8tDcu!shT zWND!L{Hb+<1uX^K6iq*bOAr;}dvsL1*LH@F&i#|N`f*TEAy_^oH-C<_0+JYTCUw-{ zJyC8XlNd|=!ohrNH*n%Qf8C^BO{_~FLy73qu2~p89#a*+l`1T6`}FR%_qhgzY1>J8 z+1{dWA@rQ!OiiHtt6=vw-|+;xQ<>4Hn7B@#OQLn`V8K7B*aQb`wHluDhwS9nR^mcs z&_wmR-%7skJrFCUo{y7YnlT~9xbF_UuO*qVe)5{*-=a_$B_0f$nq#mP`+9$-vbZod z6ynMgZeRC8x{&rVsqS-=<31j>OdJt?dd(DB-{ZfVfI1U*K`U~^{#7&d`Z_fDUCBcI zg;}Lrxe`;e8zfxyVCf@2r1g2ra0fONHM@>kMtK#YSROiR9-0YcWVt&T&uv(G`YTx8 zuy;d`{NM8N^d5qnc9-`+h@6yLp@nrBJZ_*9So)!7-t72)_|ms%m-_pqbTIGBL!d$oJ5D`(1LDynO5Tc_#kRFM|p8svz;#I*Lk4O(w;r=syEnd@+;N z+RBTl!wR1UD`179;>xjwkK-CiicMshgWZ-sBnZl9f%ZIxAacnlei=`s@UkbMhYJ*u zs?!eQ16Gv2S@+f+wbOiWj;v5I&OLAN$a_s{b98OJhORgcyim0A98IAyxa2qfaJ?x( z)Nh8~wkuEhPh6sNDE^HmY72&+<27u2>U zYY>s;4%0(5@jL?xzWv!o|Bl{%3)Zg8(N-SHos0d9q@1XJU=bkdhd#a z;`{3?WB^k@ksfGF{N7ab75G*Ln$bZuRmrEI(z!3uc%}|(qshGVUP~P47hL?uxcKj_Y{(a1i%n?QgjT^L+XI@^=NZhaQDAzbAj;W!B2a`A} zhKhq8HdrC&(snW_N*xl29(rKvmuJMDESz!e{=ZOTK1hKBp~0%7{Mc^1JM=rSEmH z>xsD&y~x@)ZK`d`rF%uNhz{j;Mf%wrbi2Or5KRX~%ESvPA?26}Mote2KsU^oHg z*FE*THIwDD>y{mvLk?iyHj$>3@#7oX^$lK5=iM{euD$&aCe3!Hy;Uo03XrQxxGQRX zR}M5=25)g*Sg3KUS9M93Yq)+>_eZ^rEX|pT6Fgf)dgBpJT>_Pnx~&KEM8+r4p+hYI z&^hSpJW&QZpcZiq7;h@_Ut$#e$?L2kSAjy=p8uzjfa zz+z(*5K^oi>C3I*Q*$N$#0hR_7DF0i1J%CMzcvpFG6?JY`+}&7VIOg*{%kBWH9XRt z*vP^a!#$=N^uL}bB@OvVxgqlR_OaV2_2rF&f~dO_{~URc4+p~j^S)UFLx(Jfz(UOX z+7G{Q#s+jWi>(|cEZKeC#hI#f)s-9)zMRt;WZ1Xb9JQ}EPT`3-YKB%)kOwx&3$@m9 zmP6Zu4X&+oqnIx!Q~58;lUW#(AFrkw&^Fiuz_>lG;i1M$&+Ri{$=3R|nN}nnU~{sx zqyOE5!|LNTI5*S+%;5_7@1NY~sJ6NqBcVYtK#v5yfrjSu1Y?+6oyt&HgiLp+c(R>A zZIf(#vLs(A);weCjUCV8?{(K#M%KzrUVfKd^C&h#f4Tw1a+7x~Q}hkj+a-yZg(Z?{PSP9oNC~e zO)H`0m0gOmy{`^S4B128{Nfmb0rs7giG+{nRGqCehQ(bpPZvw0EK0+DLQtrJo;WG* zG@o9vvir2&)_psBQ?nu!0+VEaYlH0TR7M-Q5Nz1`$mKnK^@=t;egBY;PgnL1%=LBY zgzb_e&j8$1P)9cQMi~e;PQi;HYb>*K`{LOjMgJk5H>^lSk;>jpFtoS`8V|Yd0!PfX z)**B@qW0ZzaKqBG!)7VZsP}xCH+MOnxVn+y6x9T?i(CXiGp*+dXCb~w{0PU(4QQMr z>w)hdE7oSL$n$Q_JA{75K#{R=?8az;k7oY(V5~ZgsC>xBt(U;TL%bd)a^QQw<3f4K z#Vc+prElRja-vdY@EM3F95Ii3o{^ay36;zP*NLX*ZBw4mQwK9Yv-mYA#K#$FTb{=S z)VC={NNuWQ7e;~!)^C>R6Ux8b#NqU&c~U6_BwiW{M{NO3PR=zNvk4jQimoOme9c-d z4F~PPTz4K_u*Cw3SHUoe-hj52VeT%ku%m^}WRMCLJ83-aNauTn_cO&vPGdgyYgj#;^_Fg1-PU*;-d$_gl; zmKrJ$KwQp=ItFRi__o!r!D>7e8H?Y98nu3<5!`;|sowg^GPCYzdcdhKy5B|NYVFs* z9gfl`o~=4HC(?gH7{T~TPxtw^md<2=xuGUe44#~o7OxyVA3lv>J<~5-8bBEgWAd9!ZcjoBI zEw5a2w1!OiHULkGtm+1`f>~M$lhf~-t-BVIG4p@ZflZ$jGO&`l^9aYT{VVTU7qI2-;4wr@Eu`KAJ77~v}E?X*M!uOufp!XG(|%$K+Jj@ zcjmf~^MR^A-$sDnvdwF?ld4Spq4&?r%_PboqD2@9Rvty36YnB#@uBOhuz`ehN)-Vx z_9yol*8J>WmJHn?r0)CDAJ@io?>zPEB+lZ$YKp2UMOI`CUG5RElF5>zh33y)rXNeV zt0N+uk9l~Y4u z%9HiCKBKEZUm?6rywE{Bn~=3_*7YG`;GS(E(3G!7r~mbf^QW<^2Mgb6P@WwDqo|^! zxTZc~IE1_E(nxui-VGV4i;9mH_J7TATRwn=RDfAegBbVJp4KoGfrx(RZEs`%TKU`e z-L;>)u1n-S2Sh!AD>Nc^0@_g)wq$ih5jd?s! zbxuIwvSBY7Zr?=d$%pJWXT@-Q2zO}gQB3PkLturj zNLF&1!TZ^ST9ZYX8Dez)g==2#8pfabEvq<|(w1!v-yqWG`gILPl%9(7S0N|b5pUT6 z?|Hsy2c?6ruADit)=kb8kFT4fd_j2cnfqxYLB;DDi(_^=9<@{eSi;Eld!s377zr$( zJtr_WaL)q&?t!t)WHFJBM(^*OS4}4heuU@FF!nIUiXcn?rfNoW3T14Kga@!>i0IhQ zgo>2v9Tb0}nExo1K_?@8YzppamS8|C2Fq@fm!}P#g9q9kugRmSi_ayyDAcFUIe$Gq z?h*m!4L3xy-d9c9TuQ$@{2o$_JeCxoeuSvhoHx51G`;E3ivX|&uT3e`HTFGhCcylaYGY~&~G;Uo6lkQ8$Ovj9xAhn}^ z_b{A5P)8fFdcy_oNDf-NE26%sx(?xb3a)d^doR6n;wzIYd*$I7Aqo(T5C?lz>=j2= zB~0sYRGTz>dYOvOVsx1+)T5F4ODOh80k8<)y+@MWs9c^@nZ6E5F4SM z6cY4oFo7ZP#?n1|D;rAD)ErI?NLzuInoo%@ZiSm4pzb_I zQmWS$R-%G~@WM+^&g6fxTTgluE0d$6*$j=F_>Ya!8?umGwy=DNsDx?l=4&f(k*XHS zkB!v^2gb70mv>wNFEtZm0zPP#%GzYp&pOowMz*gdmm;PAQXP+todt5>X)V3|S{ zplA+tHbXGQ0?4r)p7Nm+M>SbDz48y8F;LdzqBZX_sjhitwp>n_wZ-7kdxhCmej^v_ zsqg#PIFAn;7E`3!uSV|4coKe=L2Su9k3?}C`obb7vUy4LoSC*f086BPu|d{c>)#-$ zC*4^27DFEE%Xk-k&Qi>q17c(yBKG#R7_UW=)O&oQ!UXR{!m56giO*F4=vdF}_#k(F zJ7{?}56#d*z2>tWvxy9wGWW*r!1k=IEn@_LLg=H7legs)Y|=E2!^K=i_IA!MVTbn> zK&^=W8E1nfd55U&p+_FOjS1w*0l7ufX6g*tx+hAjx3&|P6-)#YNg@5jUzeWMzQjz2 z-?(QUDP0J)Io$tY3lN2Fsa{s9MWJ-(Ob22Zm4&Oy~<$zC%Z(O3Iv$mE_8;}cId@&y9T zSLJdm4qSAzZ#-?Cta`O>o!ZN!GRfnObX4s7WAkeuA#D>-)7QzbJaS?Fcq@MfrCIS_ zf$fM8L0*X_s3&;dVT&;~xD1kLHJA$cy`qC0yo z&4|ASt={gtfOqnjJMGJgSo%d?5ylgr36Hwx8Mnu~2UaJ+ zfm!e=rRP%6NbQl(gBqUMLjCYwU&^J$M%B)06S!e5;-OYlfUmsuMle%sl!I(b+I{5S zBp+DHC%(-1*7Mq#9663?N;k2(p93|a1;If+Ew7}X5a>W_Nix5H=a8x~{;1i5EF~hw z{AYz2(>+^dVdk4|Z=D{(c4V+dc)D2@6;LYHQ;FuRbJ4sKU&0zPU zRq_o0d{dmC+e2b|l1m|*-*WXk8m}G{c|7|*U75x-5S$TH0+?Arr#%=WuLS!c$PnSI z!$~Fwu+43zZ`=sy@`v_P|6SRRd%KmI>YrNO)SP(5R4*_M!8uqt_{kl2kQ_02(y?GM z3D&Hz*JBz9G>bG6Xm!mGJYy{4I;!mSs;U!?)tJ?L5URkJ7GHd5(Q59Q1w`vrXzmi!XSoYyqR^D1hbLH)IHTklXijW9F zEWjh$J_s#Xnd!5D4nEiQKKy5kw7|_TbIKdlL{(b**ZL`++R{;G8Pg(O+caX|afv7w zBEF^WHy`QH$Qa4Jo`rMj4sNQ^uJC5hFDx8&JT7*vmV0v4Z~p~z5S>7HN*<= zphIeiEIP)-=mJ6oy0cr1bt*z{EkK@b+*T>DB@;-V&cl98-a~Jtfmle7g?QOU$QI-? zVV!>gxn@TzM$Y6#W<>Mu8d#|+`&+#ei^F6)-TgzBqV$+7N06+xyj_uT3*y9Qe+pPC2u+3-5Z& zz{g+cEYvTjeGWy zNIh0F8}f>h0C81LkoFT}3C{JBUJ9OEbgD=-(B16eO8S3d9IfsD!Z;*0#D*Tw;+hL0yYX!xSmT(_`4Ft)l_~%7XbICX zNqBDJWt+Xqb0a?IAap2$C>|m%5uwQKA}_sD&>NA#@8N|~c9SF(Kc6sXtx@~dAc&oE zOtzh+Fn=CXamUAeJVL9+tRjO2d19%Vt=>|bc8fht&R0(Nml*S>3W`+<@knWZcBnKGMA*C6*dk7i2 zLxzqsz<&1L&w2OrzUMmE`RB~P>$+yG^~-gC?)$rzWV*pJt%fGfsFqOUNOot0u`lbm z&vFi-+3}%?b8Ee&tHuspedz|5^7+xZk2qOPe9bX<-r#G*EdMeUa7X)uv$`56;7lJP z{Nzvm6NctsKLoSU!?U{VXNY!?z>?+R>$*__lzdBn2e zSBTJnaB}h*01k}!{S_HzbWJ+nDN=`CLBb-s>%S}=h0WD= zWmxN`3J%k#J;t>JUc&whF1%W1qL6iQ?_-Nb+^#6>d@N(A=(v~EMPEJA`(l{^0Q$fl zHSnpp8=RR^+TI|;MB7_Rr7#QTy&F-9g%=a*Q7md>FCO?-* zvA1R!)?LOcwvoR-G8PUi`epO02-8UUh)D4c_MgBEJzG}6kI;9Su@lvnX>TUVe!%%W zJ4seXX4n8YN*&;T*3&D{|6Kyy3oc()U}j-XKVGmBG}9M2-Gn$ZUq1z2^|sq1N9I;A zS1Y>#y$$~oDmEy}|KP-W*O8u-l)%w{(YYaiqYLQ61R|iI0op6I_KV< z{k1dB|Cj!~>Xtt-u&T%mVd>&3+yP{d6|`9Hp#kOfIzNiwTjvFSc@T5kN0rdx=V9EhZH|7TfqcC4ni}rNO=N9hKXpq6_Q0%IBK>fjS9&Y zFRkzgyRG^BJl4Pa)!u#HJvONrXCv{WZnT7l>!+CXc6*fOM>C$yRsEk6l-7Z7s;N-o zXQ#WXL&f8+^}4j&jZ8JV51BMCCHbu3(yDQ8tVDH)Fxt#rIKzeVs0eCVFOY`nRjKP-03C~~J#g9#p=8^)U`3q7 zW;J*$@%ghtq_fe~Z^rZTyD4hiSbuP(2F<-BOXwA?aHCNcR`ioS9u(7s=Cp?E`I3+I zHn3IA3OP@uxHNwip*rTE|ER<@7I=lnvevvK`gVmBVua;sg3T}$1f13@3@zag>Rs;_ zbHRhV93t4l4i1D$%pPv9#{nqeIMQEbW%Snzq;*6B}y4;xluh>>f zHV=7k3}r&Z``c*7KL{WV2sspcFP3~ssbtM(b%gxPI6*|2R6|G_Zf@esep!IcV$$Tf z4s3sHZP9K?Qj|)jL*UKk#~*R-%B?{@4MD1-PUo1@>+wu1lr?eJ0GNFlyj}OA;epcY zGBqCCUR_- zHgx7AIrkFdP6`t=)fQ)?w~FLjYl&hYFMnag1+~1Jzg3@AzIZU&Wjg+;MbB^l?mdUo za4Dm3GC?k_7tQ(X0$+)Fu$h~@V3IJ73T8%1UDt={a3jEnv6}u@_KwzLIZLxnb!>U< z>r$d&(zc#)$^Z*EcUt614ufmCrg)9LWh+h0*eMrhR0BI_NCdY-G^rq3{p<2p?lrAwLG{iAKNHHa~LPkuDB(N(r&CVy5SNJL4Yqf)!) zesc~jf$Kn+6vesOF6SihSjG3pd?}_ud8P@ymW`zbv9F#YegBYWbcbJN7Ov`i*@OwT zSEzp(^7CpJ#S$2BXnH17cjL0DLOYshjpCOVt7V?lbg`=9tDsTRX}dTl;*EdL-U%X! z>Izwu`y-$Ku4(+iLzEuAP)2EV-(A`9!Cv()_Hw%ABJTM9pt45LIok4d`^4ds=9-LZ znU*9?4r*&ALk^oNz6{uKYh8T`5Tij#8neQXv|OxZjTZ(s%1+`vmgAdpIdyeg{n2`5 zS@Zkbe9e4{ap#H>?1x88+jYGmsz%Wj=+8V0|G<*6rK0{7(P4>!n06^l(Y>f70N-(V zpSKulz8OF!EtP*%KHnyf9%VjAopO`ocb;0Tw>Bx<;wpcK*N>uy6Bqn4AMSOaBC@Y% z6BiDfJJXO%=6$xMw>p5H{FFD7-;n>laJzb7pI|1RMiderR*N^1J0(frP7F6W5>_{+ z1^AwZ++kgCE@g24b@8@te_n1~_88FAP(Nbs^cXdVe)o$(SVn*&ugiDZ4q-jsUtfuH zlD_(TD>lq6s~%7Xu*Fh#i2g3F(#3x!OfWIiSXny>Hl$4=!qzz&U`arWs;6>DNQ07ayVD5fa{a1f$g7eSXBsJockl+Y zpW*S0N2*!mldAVd!?>1TKp#8*q-X#GPS^kK_GXqu?bsrF^ncu~niXiCFM!Wzx+l14 zv#^9{j@i=R2dO#qQI*{drYnE+vnS9$$b$DbIFaIaO3B*EEX*&Bj5dZkV07VK)5797 zcw7u)+KogKhK(Q#-*)P;hsZNoF+6SabQizK34{j??4iWN73@d4{-_X1-oG{cLgGH2 z(^O9QZt-cUuuteLNV~`(qq0N(1Si@7@w4ublstHN`<^88mFE3m? zF)H2=Jn|y|eYTPhwUbzN^zBEewv8rlj&qHG9T)fHa3rg&i`x@0Z*EyE;Xl>G*-?pFo83FH}@^>dH(({L5 zf?Yv;&64KU{q)a~Ii^i?rUEE`DX;J6Uree{boHlWhatS1ZieE)pib(%zBQ(FTJXAz z-%My)uEiBE&Pb?o<7>zClF<%2xBduiG~(;;+FX>IDDs&j({pT=fvZ&cas;5JH?)*-sD zep7^?_p^;5Zqc=+6i55f^OO8jdb`#sT4EFx#u9tCp|W&Qeytpi<&y${NZyjS_@&0b)Z+l zq!Hd-b==wSM);-Wy{Vv8wkwwn zMb^2kP6ncMKOii}{;6t=Pi<^$TC>oaKO3EVqEs3OYfE;PyVe5OrVPUX6E$N?zL_0( zL+R!KaC*x!nMz|;FL(3W<2^DRUpU@!fCH}pe?g#D%OU#XV%rXM@pp21F^tZV@W`TZ z<-;raJU-!Z1;m>m&+a(9PCDbNNQgnU4UkT}EjQ}f|5i`%bvz|dhkCgC%?8@3{QAX+ z2ZM(Gi;UBiQpYK3cD4`i{S4Y6AY41I{&eq65#lr8f}=%%7AE)O4_ue z62%S6LYgiM@rAA5^~xtzfnZ+aVp1VU>Pz<-#Aoy;u#gFj(oAkCU&c)lSTXf}pn7!# zcFcAGv9f4FD$GNvhIeUBZBruT#=$55-FA^nvT5YLWaak50c&HgdrxTKqK9@<>X(y5 z5?0P5XM1D!9*UX*8d=wT)A7wu^GPG*kA^Z9BtvE*upum+c4g;(4jMJSN#;<&$uqER)jyh`=2Ri3Eg?<7pD zfifEfcjP$?+fok}>6-I9<=XZ8OiLJaHcZkXgd@Rw#qA6#dRu0pS#BOWq+zR=5BRbh z%pK<5n&Ef}%zg_OvVAs$u={}6$+c-0>0bVN9ZEW7wpV-9xwj2p3-b19UQ`7JpX2jc z`PtJ>ijzx4;OHM-2yNZEwHV!z#h!MZahl4C!)>m+6HXe7i~x8ie(E|<5t#q&>S{b7 zwdH5zT<(}DR}S7?@|*N}r*$e=11zu>&0ePOM4C$5UZVdC|F!m;vGX2zh3bu9gW|Df#Z zlv$8h@_pFnuQL2E8XJB5zf101>D2$71N^&AL}z^d85dduqUVJc*MT zX(Qirur))T_)L~9ti4We<9se#_@yxW{_#s|I)n4^Et&X25xp0C%&6*UFCg$e&M#zZ zMDVBBzl-{ywR@oRm9c>i|Mb=r4X2>YYSq5;VWWHg1Bd_8C9%*_c7xN>LoF5V=Y1!h=1kZ}gq>f$vWac*Wq>7{*Y;E#&1|Fvsn`>gewxuYU8* z6)A?S#~G*>^u=ji8F_9B z8oHaX$G^0HWc-F^?43%R$8Y$Dzl`fbD`R|LPL_MbFc>9|F-Uq+k=2)DLpyJkEj+0Z z%yqNEaTpRyd&&(iIjq+VH8a6@-Ae@UQYuszlID8XxbygT@z&y6*k&Lc0*x`*Eq7{x zWHEW_fzbado@H{ZREBa-kw5HhKuS0yQWbzjTJkoRDBm86RVj+zJIA^#z2*YH<8&kq z5rNLKI!N02C)!%Dcs@ykjdjwBLW$SB?8;nB6!~L(U8ouBKsxR^~wav_@a-dO?8abWFiA+ z=!8t|vP`hhroVb}T)6qJZNPAVQNzKqxv@r+f_&f8ax)qWrDCQ~xf$KaXTjgaa+Aq} zm|O$^GiQ)`uOudOXjAFLe8q^C0EM6<^C-#q{^H$boH|9MjZ*NCUK@*m zcW&Q$0sf-A>0OrZVPBWxr@#~DGl3eaG%E85wnIZZ06H%a@0uY3$6tS_O3&Krr5toZ zX)0^o`vSnh@l3ws{+bk?ii8AS-i+dm0LC57r+`XBgPeXg6UkLy7vDTMWs_aB-*&wF#&ZZir?wIcartM8|}*RJUgw zf}S*?@drHfO`G5rsKO~=c*1P_hlsowi{kRvNl+MnMv7c-L+P>|!KD9OOE~dng(Cxt z)!6uHxtX_47$Pz|?oq}}<5iT9aqyZpR+DMGbM}%nU#u@AvSY=!8rmzN8JQG+xA1~Q z%5sbdlDUqS+=su*&CI=|s9R%|cJ^V$!y%uagQ?WpLfcT=f#-_7Efk_|Q_Wc?)Z~%@ zzjw4qym^R;x786fJk(WLC`&>dr#%?GrAl1B_KMll?_RX_bo+69^bp9td!xv~==0Z7 z>b7tKG3T~P-Z_^)8w|S_wzY(0XDKTfqjlul6^rIm=dhr2Xk8Dr zdy1;0)AP}vwWg`UXjNB}>8j~}b3z<)K65?^+Dk|{%4O^8CwV)P;m<^B++(6soFs1l zvx@G};-uW{KtG25T;WpH?+Xa;N{|J$XEn?)Grrg=AJAo7I8(wQ(+mtLPoc)^vuhQz zXn75xo&71DhG@M18u7fK(_o(uRmfXMi zL(IGU0V(RD@nm!znXiIMq|$-%e9jvyvO4u}^SP8ntT+3YZ+txA?Q7V1H)(ZSq z%ZnIf;!sz0TQ+%-fa9lrF8A?c>jj?MEMiYQCz%cdW>BfmfwGyi7kX=B^MO6vSOAXN z4R4wNQ$pp}=UUzDdA@d=od<<(wRab-qrQN!uUg{?Q<0I=m^_g|T{a`v%T(Q&PtsC$ z!xqPR((?gN8p{}gpCsY&zl~t?xuEI9e?}q_Yl~o5&{497HW45(Yuk+r*t{-5?&bka z{ZLncN?$+moRn5Ye4h)fvs&m~ z=c4R&zAiXu$5z!Bq|8^TdV!GR%Bz_6f*N_Y%*OkZB-UQcVRZVn*oFwRu!dbAzeqxe zu{*;Btac}DX`y{hZd0J&&vrRS5n4HY_xej~I<`v#cS?6$jpB0}Yx;{nXv@45Cq&U) zDJ|3XkGE4|-hqzqV$8!P9IqPT-EAU78KaGEwIs)5>`OQq_E#$zB^4%U>hCsCW*_)Wg`Rxf zGu!askIcn~8HU#j*%vl;+eH3bEaWr0*B55XD=0NvXHb(mHdKjQrnT9mTEE^=f8Q}2 zZMH*-3dW)=)@N7!n#{bHg<66upJLe#p-p_8jpIWYb}00m(6phv9rdD)%E<|Vn8dKr zI&~|Q9LzR*wuB6m)@#yNGyW80h1T|v*+S%5D?yDab1$o;a9nZgJm16 zOnPCjZ{DbN-YiJjhyBj&!qiM z2F(!vzK#ru<4r)l+KDEubN!3#UeuITx;=KMcTPBC>WLp7Tj$555nPqrdAFj?El!Xa9;7PCl6ES!H&mK*>V6&i&C1ROV0=O-n86e@UH3i#vQ)<=!x zj?8YLMeSr^M?i?&fa6dE*%6}oDk}WBDhDKUwvHMopdR*sK0sB^ej_kt0Y}zMudlc9 zT*_`Cd=~=MBMMSVy?KLt5Vccmmngzb&bwXHxW%^S#(KIDb*7{&?U6`*`JFYgZTxlB z-BT9N^txS0u?YN;@J81q1*~bug-q)!W zdxK~JCQW6uzTYv`?BY7q% zXrYYpTd!e%)tFvEGR%s=VUxv;%Jmm^*%_4l@DaG-HJ#ijmsSf{8s+xuWXGVkWS%mH zHlm)%iR%VW0N=Z^+!Ovp^@*z#H1efdPgfl85fS>$(tRDCZw2Nl1-{TaZSdok34M}U zs_jDlBlt>4n=!YJK>KYOR)-IJ5I=oGRyEEgV+vP2yxmhU6J=lXp3{jHtQFivhu&Qe zB*QDjJy+r~%(%HCbN6mUpj#OGd?bakpaVd z6#cP(Rpa9A*KG1+%Wqc>UF3l-BQU9LL&^R_`4zE_-XNj1tzAy{LkRQybb)L2Q}<|~ zye8e=m?$G%iNp_$!Rg18IS|Zfph?kpxA`lPRmD2=?0ifI!v%x;tres)U~em7>k{?4a7SKx@WtdU0`DYaP^eF@UN%( zmUZdbA?uKB*T74x0QiTSvYgv3QeOi#%8oK?OYrM!r-*^>fsNBH?E)E#rnJPs434|k zOq`L?Xla-2V#bEC${Bem)}${+|Jo$AWvNt@;)6PQFgVes-B8W{B@FMbI6lod@LAn+ znRv15{rk&d4A=H!{m2!xSL|M$aHz4QfX;z~-cN6>^{$4im7iuo)R&@Ca>otBg4WzA zfOmd9t-V8y4-r0{!kDs0KeYbPo*%Im%s)GjKsr}R-WJoh9IwWrrXE4cI8EH-0Z6`8 zdI2Xh#WHlXPm=2?a}8y)p`A26)oPsc60?1GgL|-o6#%r*`j1V=r;J;q+ZApE=fs`) zYjgJQ7-ghC^wc5%Wep3!=Hu$!Fr^(Dh#7_O)0lI3V@Mvyi>x12WV z4g4jf;x+zwuo8woV0+RTx~6G*lHWa0z|s0-Mmm_qq}w1dfp^P`qzJ6^eIata!fhIV zxF9(=ah5(9Coxf1v;sa3P)s|&Z)hhzyLm46m-7V1cN~ob{J+B^z(@WT`f5ZO7b4BQ)`33KSwj)OgrztIL}SK^TydHwyHguxuz`WVk+}% zJTSp;OJzBjDzPjv>G=J>8M-&Q|0{U(@8J+RmO;0Pzc>5;9pd~eeewT-47E-WMe*c~ zrR=YhV}EhwF}(NB_!s?uf?faK2sf2O=qfri^k57X_UNnR^kY-$mMxq8sVhwN=VFa_ zUH%ld+lDnSc-?p=em5myz*=<0(M=n&wOut~?6LQoY#)}*3CEdCbg%q9ar^s=_`i?N z{2em;KWOc@XmWlW1)6?Y?*;tmc8JXSAMn`UkAUXGkgg|R_F8VDB$0.5.2),A8 +- [ ] Frame Services(0.5.2) + - [ ] Kernel Service + - [ ] System-Call Interface,A2 + - [ ] Name Service,A4 + - [ ] Node Daemon,A2 + - [ ] ACL Control,A4 + - [ ] Contact Manager,A2 + - [ ] Runtime Context (0.5.2),A4 + - [ ] Package System,@waterflier, A2+S4 +- [x] AI Compute System,@waterflier, A2 + - [ ] Scheduler,@streetycat, A2 + - [x] LLM + - [x] GPT4 (Cloud),@waterflier, S1 + - [ ] LLaMa2,@streetycat, A2 + - [ ] Claude2, S2 + - [ ] Falcon2, S2 + - [ ] MPT-7B, S2 + - [ ] Vicuna, S2 + - [ ] Embeding,@photosssa,@lurenpluto , A4 + - [ ] Txt2img,@glen0125,A4 + - [ ] Img2txt(0.5.2),A3 + - [ ] Txt2voice,A3 + - [ ] Voice2txt, @wugren,A3 + - [ ] Language Translate (Pending) +- [x] Storage System + - [ ] DFS (0.5.2),A4 + - [ ] Object Storage, @lurenpluto ,A2 + - [ ] D-RDB (0.5.2),A2 + - [ ] D-VDB,@lurenpluto , A4 +- [ ] Embeding Piplines,@photosssa, A2 +- [ ] Network Gateway,A6 + - [x] NDN Client, @waterflier, A1 +- [ ] Build-in Service + - [x] Spider,@alexsunxl, A2 + - [x] E-mail Spider,@alexsunxl, S2 + - [ ] Telegram Spider,S2 + - [ ] Twitter Spider (0.5.2) + - [ ] Facebook Spider (0.5.2) + - [ ] Agent Message Tunnel (0.5.2) + - [ ] E-mail Tunnel,A2 + - [ ] Telegram Tunnel,S2 + - [ ] Discord Tunnel,S2 + - [ ] Home IoT Environment (0.5.2), A4 + - [] Compatible Home Assistant (0.5.2), A4 +- [ ] Build-in Agents/Apps + - [ ] Agent: Personal Information Assistant,@photosssa,@lurenpluto , A2 + - [ ] Agent: Bulter Jarvis,@waterflier, A2 + - [ ] App: Personal Station (0.5.2),A4+S4 +- [ ] UI + - [x] CLI UI (aios_shell),@waterflier,S2 + - [ ] Web UI (0.5.2),A4+S4 +- [ ] 0.5.1 Integration Test (Senior*3) + - [x] Workflow -> AI Agent -> AI Agent,@waterflier,S1 + - [ ] Spider -> Pipline -> Knowledge Base,@photosssa,S2 + - [ ] AI Agent <- Functions <- Knowledge Base,@lurenpluto,S2 +- [ ] SDK + - [x] Workflow SDK,@waterflier, A2 + - [ ] AI Environments SDK (0.5.2), A2 + - [ ] Compute Kernel SDK (0.5.2), A2 +- [ ] Document (>0.5.2) + - [ ] System design document, including the design document of each subsystem + - [ ] Installation/use document for end users + - [ ] SDK document for developers + + +The following is the introduction of the adjustment of each component after the current implementation. + +# AIOS Kernel + +Define some of the important basic concepts of Intelligent Applications running on OpenDAN + +## Agent + +Agent is the core concept of the system, created through appropriate LLM, prompt words, and memory. Agents support our vision of a new relationship between humans and computation in the future: +``` +Human <-> Agent <-> Compute +``` +Agents form the basis of future intelligent applications. From the user's perspective, the strength of AIOS is primarily determined by "how many agents with different capabilities it has." +The above process has now been implemented. In practice, I found a key issue is that we need to continuously seek the optimal solution. This issue directly relates to how application developers of OpenDAN build intelligent applications, so I think it has a high priority. + +### Optimization of system prompts + +The goal is to allow Agents to communicate with other Agents (forming a team), call Functions at the right time, and read/write status through the environment at the right time, using prompt words. The existing implementation is usually: + +``` +When you decide to communicate with a work group, please use : sendmsg(group_name, content). +``` + +Our optimization direction is: + +1. To allow Agents to initiate calls accurately +2. To use as few precious prompt word resources as possible. + +If there are already systematic studies in this field, introductions are also welcome! + +## Workflow + +Workflow has realized the concept of allowing multiple Agents to play different roles and collaboratively solve complex problems within an organization. It is also the main form of intelligent applications on OpenDAN. Compared to a single Agent, building a team composed of Agents can effectively solve three inherent problems of LLM: + +1. The prompt word window will grow, but it will remain limited for a long time. +2. Like humans, Agents trained on different corpora and algorithms will have different personalities and will excel in different roles. +3. The inference results of LLM are uncontrollable, so accuracy cannot be guaranteed. Just like humans make mistakes, the collaboration of multiple Agents is needed to improve accuracy. + +The basic framework of Workflow has been completed (which is also the core of version 0.5.1). Following the subsequent SDK documentation, we now have a basic framework for third-party developers to develop applications on OpenDAN. + +## AI Environments + +Environments provide an abstraction for AI Agents to access the real world. + +Environments include properties, events, methods (Env.Function), and come with natural language descriptions that Agents can understand. This allows AI Agents to understand the current environment and when to access it. For example, an Agent planning a trip needs to understand the real weather conditions at the destination in the future to make the right decisions. This weather condition needs to be provided to the Agent through Environments. + +The events in Environments also provide logic for the autonomous work of Agents. For example, an Agent can track changes in the user's schedule and date, automatically helping the user plan and track the specific itinerary for the day. + +### Celender Environment + +The system's default environment, which can access the current time, the user's schedule, and the weather information at a specific location. It also contains some important and basic user information, including home address and office address. + +#### Compatible with common Celender services. (0.5.2) + +- Microsoft Outlook Calendar +- Google Calendar +- Apple Calendar + +### Workspace Environment (0.5.2) + +A file system-based workspace environment that allows the Agent to read/write files at appropriate times. + +## AI Functions + +Function is a core concept of AIOS, providing the Agent with descriptions of suitable callable Functions, allowing the Agent to invoke Functions at the right time. Through Functions, the Agent gains "execution power," rather than just being an advisor that only provides suggestions. The Function framework allows third-party developers to develop and publish Functions, supporting Agents and Workflows to have a list of available Functions, through which they can build appropriate prompt words, enabling the Agent to invoke Functions at the right time. + +**Under development.** + +### Basic AI Functions (0.5.2) + +There are already a plethora of basic services in the world, such as querying the weather at a specific location at a specific time, checking hotel prices, or booking plane tickets. The system should separate the definition and implementation of Basic (generic) Functions, allowing Agent developers to implement common scenarios with generic logic. The definition of generic Functions is undoubtedly similar to standard setting work. + + I know that many other projects have done a lot of work in this field, and ChatGPT also has dedicated function support. What we need to do is to find the open standards that are closest to our goals and then integrate them. + +## AI BUS + +The AI BUS connects various conceptual entities of OpenDAN. For example, if Agent A wants to send a message to another Agent B and wait for the processing result of the message, it can simply use the AI BUS: + +```python +resp = await AIBus.send_msg(agentA,agentB,msg) +``` + +The abstraction of AI BUS allows different Agents to choose suitable physical hosts to run according to the system's needs. This is also why we define AIOS as a "Network OS". All entities registered on the AI BUS can be accessed via the AI BUS interface. As needed, we will also persist the messages in the AI BUS, so that when a distributed system experiences regular failures, it can continue to work after being pulled up again. + +The concept of AI BUS has many similarities with traditional MessageQueues. + +## Chatsession + +Intuitively, ChatSession saves the "chat history". The chat history is currently the natural source of Agent Memory capability. +Determining a ChatSession has three key attributes: Owner, Topic, and Remote. An operation where A sends a message to B and gets B's reply will generate two messages, and save them in two different ChatSessions. + +Currently, ChatSession is saved based on sqlite. After the Zone-level D-RDB is set up in the future, it will be migrated to RD-DB. + +## Knowlege Base + +Provide a unified interface, support switching vector database kernel +Integrate open source vector database (pay attention to Lience selection) +When designing the interface, prepare for future access control + +**Under development.** + +## Personal Models (>0.5.2) + +The goal of this subsystem is to support users in training models based on their own data, including subsequent usage, management, deployment, and other operations of the model. In the early stages, invoking this module and adding new models should be operations performed by advanced users. + +It is still uncertain whether this module will be actively used in intelligent applications. + +# Frame Services + +The implementation offers a range of fundamental services for traditional Network OS. It connects users' devices to the same Zone via the network and provides a unified abstraction for application access. This component serves as a basic framework and computing resource for the operation of intelligent applications on the upper layer. On the lower layer, it connects various types of hardware through different protocols, integrates resources, and offers a unified abstraction for intelligent applications to access. + +## Kernel Service (0.5.2) + +The Kernel Service implements the System Calls for OpenDAN and provides a "kernel mode" abstraction. In version 0.5.1, since this component is not yet implemented, all code—whether system services or application code—runs in kernel mode. + +In the future, we plan to maintain the system running in this mode for an extended period, as it facilitates debugging. + +The Kernel Service is mainly composed of the following component: + +### System-Call Interface + +Centralizes the provision and management of system call interfaces. + +### Name Service + +It is the most crucial foundational state service in a cluster (Zone) comprised of all the user's devices. As the core service of the Zone, it provides the most basic guarantee for the availability and reliability of all services within the Zone. When a user needs to restore the Zone from a backup, the Name Service is the first service to be restored. + +Its functionality is similar to that of `etcd` but includes a on-chain component. From a deployment standpoint, it needs to be operationally optimized for small clusters made up of consumer-level user devices. + +### Node Daemon + +It is a foundational service that runs on all devices that join the Zone, responding to essential kernel scheduling commands. It adjusts the services and data running on that particular device. + +### ACL Control (>0.5.2) + +Another essential foundational service of the kernel, it is responsible for the overall management of permissions related to users, applications, and data. The Runtime Context reads the relevant information and implements proper isolation. + +### Contact Manager + +From the perspectives of permission control and some early application scenarios, understanding the user's basic interpersonal relationships is an important component of OpenDAN's intelligent permission system. Therefore, we provide a contact management component at the system kernel layer. This component can be considered an upgraded version of the traditional operating system's "User Group" module. + +## Runtime Context (0.5.2) + +It serves as the runtime container for user-mode code, offering isolation guarantees for user-mode code. + + Depending on the type of service, we offer three different Runtime Contexts. The most commonly used is Docker, followed by virtual machines, and finally, entire physical machines. + +## Package System + +The Package Manager is a fundamental component of the system for managing Packages. The sub system provides fundamental support for packaging, publishing, downloading, verifying, installing, and loading folders containing required packages under different scenarios. Based on relevant modules, it's easy to build a package management system similar to apt/pip/npm. + +The system design has deeply referenced Git and NDN networks. The distinction between client and server is not that important. Through cryptography, it achieves decentralized trustworthy verification. Any client can become an effective repo server through simple configuration. + +Based on the Package System, we can implement the publishing, downloading, and installation of extendable foundational entities such as Agents, Functions, and Environments. This enables the creation of an app store on OpenDAN. + +**Under development.** + +# AI Compute System + +The purpose of designing Compute System is to enable our users to use their computational resources more efficiently. These computational resources can come from devices they own (such as their workstations and gaming laptops), as well as from cloud computing and decentralized computing networks. + +[![compute kernel design](./compute_kernel.png)](compute_task.drawio) + +The interface of this component is designed from the perspective of the model user rather than the model trainer. The basic form of its interface is: + +```python +compute_kernel.do_compute(function_name, model_name,args) +``` + +## Scheduler + +The goal of the Scheduler component is to select an appropriate ComputeNode to run tasks based on the tasks in the task queue and the known status of all ComputeNodes (which may be delayed). In the current version (0.5.1), the implementation of the Scheduler is only to get the system up and running. In the next version (0.5.2), the overall framework for computing resource scheduling needs to be established. + +## LLM + +LLM support is the system's most core functionality. OpenDAN requires that there be at least one available LLM computing node in the system. The supported interfaces are as follows: +``` +def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0): +``` +In the current era, many teams are working hard to develop new LLMs . We will also actively integrate these LLMs into OpenDAN. + +- [x] GPT4 (Cloud) +- [ ] LLaMa2 **Under development.** +- [ ] Claude2 +- [ ] Falcon2 +- [ ] MPT-7B +- [ ] Vicuna + + +## Embeding + +Provides computational support for the vectorization of different types of user data. The specific algorithms supported depend on the requirements of the entire pipeline. + +***Under development.*** + +## Txt2img + +Generate images based on text descriptions. According to the implementation mode, we can interface with a cloud-based implementation and a local implementation. + +The local implementation will definitely use Stable Diffusion. + +***Under development.*** + +## Img2txt (>0.5.2) + +Generate appropriate text descriptions for the specified images. + +## Txt2voice + +Generate voice based on specified text, using a selected model (the focus is on personal models), and guided by certain emotional cue words. + +***To be developed*** + +## Voice2txt + +Extract text information from a segment of audio (or video) through speech recognition. + +***To be developed*** + +## Language Translate + +Translate a segment of text into a specified target language. + +Since LLM itself is developed based on the foundation of translation, I am currently considering whether it is necessary to provide a text translation interface within the computing kernel. Following the principle of not adding entities if they are not needed, it can be postponed from development. +***pending*** + +# Storage System + +The file system (state storage) has always been a critical part of operating systems. Its implementation directly impacts the system's reliability and performance. The challenge of this section is how to transfer key technologies that are already mature in traditional cloud computing to clusters composed of consumer-level electronic devices with low operational maintenance, while still maintaining sufficient reliability and performance. The implementation of the subsystems in this section is of limited stability. Therefore, I believe the focus of OpenDAN in the early stages for this section should be on establishing stable interfaces to get the system running as quickly as possible, with independent improvements to be made in the future. + +From the standpoint of trade-offs, our priorities are: + +- Abandoning continuous consistency guarantees, the system only provides strong assurance for reliability up to "backup points." This means we allow the loss of some newly added data if the system experiences a failure. + +- Allowing downtime, considering the consumer-level power supply, a short period of unavailability of the system itself will not have a significant impact. We can stop the service for backup/migration when necessary. + +## DFS + +Distributed file system, combining the public storage space on all devices to form a highly reliable, highly available file system. + +## Object Storage + +Distributed object storage, and based on MapObject, it implements trustworthy RootState management. + +(MapObject and RootState is a concept from CYFS) + +**Under development.** + +## D-RDB + +Distributed relational database, providing highly reliable and highly available relational database services (mainly used for OLTP - Online Transaction Processing). We do not encourage application developers to use RDB on a large scale; the main reason for offering this component is for compatibility considerations. + +***Pending.*** + +## D-VDB + +Distributed vector database, which currently appears to be the core foundational component of the Knowledge Base library. + +***Under development.*** + +# Embeding Piplines + +Read appropriate Raw Files and Meta Data from the specified location in the Storage System. After passing through a series of Embedding Pipelines, save the results to the Vector Database as defined by the Knowledge Base. + +***Under development.*** + +# Network Gateway (0.5.2) + +Obtain user data by recognizing network data. +The Gateway also provides an external access entrance for the entire system, and access control can be unified. +Provides the bus abstraction in the network operating system (the network cable is the bus), devices within the Zone are recognized by the system as plug-and-play devices, and can be called by applications/Agents + +## NDN Client + +AI-related models are all quite large, so we offer a download tool based on NDN (Named Data Networking) theory to replace curl. The NDN Client will continue to support new Content-Based protocols in the future, allowing OpenDAN developers to publish large packages more quickly, at lower costs, and more conveniently. + +# Build-in Service + +The basic functions of the system implemented by "user mode" can be regarded as pre -installed applications of the system.Let the system have basic availability without installing any intelligent applications. +We should build built-in applications for 1,2 early preset scenarios, rather than all possible scenarios.This allows us to run the system faster and allow us to discover the shortcomings of the system faster, so as to improve the system faster. + +## Spider + +A series of reptiles are provided to help users import their data into the system. + +### E-mail Spider + +The most basic spider is used to capture user mail data.The main purpose of this is to determine the general data format(include text,image,contact) and location to save the grabbed data. + +### Telegram Spider + +Allow users to capture their own Telegram chat records and save them in the Knowlege Base + +**To be developed.** + +### Twitter Spider (0.5.2) + +Allows users to scrape their own Twitter data and save it in the Knowledge Base. + +### Facebook Spider (0.5.2) + +Allows users to scrape their own Facebook data and save it in the Knowledge Base. + +## Agent Message Tunnel (0.5.2) + +The original ROBOT module, after considering its actual function, was renamed the Agent Message Tunnel. +This is the default function supported by the system. It supports users to configure different message channels for different Agent/Workflow, so that users can interact with Agent/Worflow through existing software/services.From the perspective of product, the goal of this module can use the core function of OpenDAN without installing any new software on the one hand. On the other hand, it also creates a stronger mental model for users: My Agent can registered social account, so that Agent has his own identity in the virtual world. + +### E-mail Tunnel + +Let Agent have its own email account. After registration, users can interact with Agent through mail. + +### Telegram Tunnel + +Let Agent have his own Telegram account. After registration, users can interact with Agent through Telegram. + +### Discord Tunnel + +Let Agent have its own discord account. After registration, users can interact with agent through Discord. + +## Home IoT Environment (0.5.2) + +We've implemented a significant built-in environment: the Home Environment. Through this environment, the AI Agent can access real-time status of the home via installed IoT devices, including reading temperature and humidity information, accessing security camera data, and controlling smart devices in the home. This allows users to better manage a large number of smart IoT devices through AI technology. For instance, a user can simply tell the Agent, "Richard is coming over to watch a movie this afternoon," and the AI Agent will automatically read the security camera data, recognize Richard upon arrival, turn on the home projector, close the curtains, and turn on the wall lights. + +Thanks to LLM's powerful natural language understanding, all we need to do is connect a smart microphone to the Home Environment and configure a simple voice-to-text feature. This makes it easy to implement a privately deployed and very intelligent version of Alexa. + +In terms of system design, we use the Home Environment as an intermediary layer, freeing OpenDAN from having to spend energy on dealing with compatibility issues with various existing, complex IoT protocols. This keeps the system simple and makes it easier to expand. + +### Compatible Home Assistant + +Home Assistant is a well-known, open-source IoT system. We could consider implementing the Home Environment based on the Home Assistant's API. + +# Build-in Agents/Apps + +Once users have installed OpenDAN, it should have some basic functionalities, even without the installation of any third-party smart applications. These basic functions are provided via built-in Agents/Applications. Built-in applications have two important implications for OpenDAN: + +1. They provide a developer's perspective to scrutinize whether our design is reasonable and the application development process is smooth. +2. Through one or two scenarios, OpenDAN can be quickly put into use by real users in a production environment, and these scenarios can serve as a basis for driving system improvements in OpenDAN. + +## Agent: Personal Information Assistant + +Through interacting with this Agent, users can use natural language to query information that has already been saved in the Knowledge-Base. For example, "Please show me the photos from my meeting with Richard last week." They can also find their information more accurately based on some interactive questions. + +***To be developed.*** + +## Agent: Bulter Jarvis (0.5.2) + +The Butler Agent Jarvis can recognize certain special commands. Through these commands, it can communicate with other Agents in the system, check the system's status, and use all the system's functionalities. It can be seen as another entry point to AIOS_Shell. + +Another important function of the Jarvis is to create sessions. When a user has many workflows/agents installed on their OpenDAN, they might not know which workflow/agent to talk to in order to solve a problem. I envision the future mode to be: "If you don't know who to turn to, ask the Jarvis." The Jarvis will create or find a suitable session based on a brief conversation with the user, and then guide the user into this session. + +Based on these two functions, the Jarvis might be the only "special Agent" that requires custom development among all Agents, and it is a part of the system. + +## App: Personal Station (0.5.2) + +The Personal Station is a built-in application that provides a graphical user interface for users to interact with the system. It is a web application that can be accessed through a browser. It is also the first application that users will see after installing OpenDAN. It provides a simple interface for users to interact with the system, and it also provides a way for users to install new applications. + +The main functions of Personal Station include: + +1. Library, with the help of Personal Information Assistant, you can better manage your own photos, videos, music, documents, etc., and share them with friends more effectively. (For example, ask the assistant to share photos from an event, selecting from those you've starred, and distribute them to friends based on the people appearing in the photos.) +2. HomePage, with functions similar to Facebook/Twitter, where you can post content you want to share. You can also open your Agent to friends and family, allowing them to interact with your Agent, discuss schedule arrangements, and query your KnowledgeBase for open content. + +Home Station is a mobile-first WebApp. + +# UI + +## CLI UI (aios_shell) + +The system provides the command line UI interface priority, facing developers and early senior users. + +## Web UI (0.5.2) + +Web UI interface for end users + +# 0.5.1 Integration Test (Senior*3) + +Can be divided into 3 parts +1.Workflow -> AI Agent -> AI Agent +2.Spider -> Pipline -> Knowledge Base +3.AI Agent <- Functions <- Knowledge Base + + +# SDK + +## Workflow SDK + +The SDK allows developers to expand the new workflow/agent to the system. +At present, the SDK has completed the most original version. In ROOTFS/, the .tmol file is written according to the directory structure, and a new workflow/ agent can be added to the system. + +## AI Environments SDK (>0.5.2) + +The SDK allows developers to expand the system that can be called by AI, including +- Expand the new environment +- Expand the new function + +## Compute Kernel SDK (>0.5.2) + +This SDK allows developers to expand more core capabilities to the system + +# Document (>0.5.2) + +When we release 0.5.3, we must complete at least 3 documents: + +1. OpenDan's complete system design document, including the design document of each subsystem. +2. Installation/use document for end users. +3. SDK document for developers. diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py index 74cbdcc..e830672 100644 --- a/src/aios_kernel/ai_function.py +++ b/src/aios_kernel/ai_function.py @@ -1,25 +1,62 @@ +from abc import ABC, abstractmethod +from typing import Dict + class AIFunction: def __init__(self) -> None: self.intro : str = None - def load_from_config(self,config:dict) -> bool: + @abstractmethod + def get_name(self) -> str: + """ + return the name of the function (should be snake case) + """ pass + @abstractmethod + def get_description(self) -> str: + """ + return a detailed description of what the function does + """ + pass + + @abstractmethod + def get_parameters(self) -> Dict: + """ + Return the list of parameters to execute this function in the form of + JSON schema as specified in the OpenAI documentation: + https://platform.openai.com/docs/api-reference/chat/create#chat/create-parameters + """ + pass + + @abstractmethod + def execute(self, **kwargs) -> Dict: + """ + Execute the function and return a JSON serializable dict. + The parameters are passed in the form of kwargs + """ + pass + + @abstractmethod def is_local(self) -> bool: + """ + is this function call need network? + """ pass + @abstractmethod def is_in_zone(self) -> bool: - pass - - def is_readyonly(self) -> bool: + """ + is this function call in Lan? + """ pass - def get_intro(self) -> str: - return self.intro - - async def execute(self): + @abstractmethod + def is_ready_only(self) -> bool: pass + #def load_from_config(self,config:dict) -> bool: + # pass + # call chain is a combination of ai_function,group of ai_function. class CallChain: def __init__(self) -> None: diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 9979e2c..4e064bf 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -48,6 +48,12 @@ class Environment: def _do_get_value(self,key:str) -> Optional[str]: pass + def get_functions(self): + # system functions + # env functions + # user install functions + pass + def register_get_handler(self,key:str,handler:Callable) -> None: h = self.get_handlers.get(key) if h is not None: @@ -78,7 +84,7 @@ class Environment: for handler in handler_list: await handler(self.env_id,event) else: - logger.warn(f"fire event {event_id} in env {self.env_id}:handler not found") + logger.debug(f"fire event {event_id} in env {self.env_id}:handler not found") return def __getitem__(self, key): diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 69f39c3..2a8e823 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -1,4 +1,3 @@ - import logging import asyncio import json @@ -412,7 +411,10 @@ class Workflow: def _get_function_prompt(self,role_name:str) -> AgentPrompt: - pass + # system functions + # env functions + # user install functions + pass def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt: pass diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 9ef00cb..864f4fb 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -134,3 +134,6 @@ class WorkflowEnvironment(Environment): except Error as e: logging.error(f"Error occurred while update env{self.env_id}.{key} ,error:{e}") + def get_functions(self): + pass + diff --git a/src/service/aios_shell/tg_aios_shell.py b/src/service/aios_shell/tg_aios_shell.py new file mode 100644 index 0000000..98844ae --- /dev/null +++ b/src/service/aios_shell/tg_aios_shell.py @@ -0,0 +1,223 @@ + +import asyncio +import sys +import os +import logging +import re + +from typing import Any, Optional, TypeVar, Tuple, Sequence +import argparse + +from telegram import ForceReply, Update +from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters + + +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../../') +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession + +sys.path.append(directory + '/../../component/') +from agent_manager import AgentManager +from workflow_manager import WorkflowManager +from aios_kernel import CalenderEnvironment,Environment + +import logging + +# Enable logging +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO +) +# set higher logging level for httpx to avoid all GET and POST requests being logged +logging.getLogger("httpx").setLevel(logging.WARNING) + +logger = logging.getLogger(__name__) + +class AIOS_Shell: + def __init__(self,username:str) -> None: + self.username = username + self.current_target = "_" + self.current_topic = "default" + + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: + target_id = msg.target.split(".")[0] + agent : AIAgent = await AgentManager().get(target_id) + if agent is not None: + bus.register_message_handler(target_id,agent._process_msg) + return True + + a_workflow = await WorkflowManager().get_workflow(target_id) + if a_workflow is not None: + bus.register_message_handler(target_id,a_workflow._process_msg) + return True + + return False + + async def is_agent(self,target_id:str) -> bool: + agent : AIAgent = await AgentManager().get(target_id) + if agent is not None: + return True + else: + return False + + async def initial(self) -> bool: + cal_env = CalenderEnvironment("calender") + cal_env.start() + Environment.set_env_by_id("calender",cal_env) + + AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) + WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) + open_ai_node = OpenAI_ComputeNode() + open_ai_node.start() + ComputeKernel().add_compute_node(open_ai_node) + AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) + return True + + + def get_version(self) -> str: + return "0.0.1" + + async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str: + 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) + if resp is not None: + return resp.body + else: + return "error!" + + async def install_workflow(self,workflow_id:Workflow) -> None: + pass + + async def call_func(self,func_name, args): + match func_name: + case 'send': + target_id = args[0] + msg_content = args[1] + topic = args[2] + resp = await self.send_msg(msg_content,target_id,topic,self.username) + + + return resp + case 'open': + if len(args) >= 1: + target_id = args[0] + if len(args) >= 2: + topic = args[1] + + self.current_target = target_id + self.current_topic = topic + show_text = f"current session switch to {topic}@{target_id}" + return show_text + + + case 'history': + num = 10 + offset = 0 + 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): + db_path = AgentManager().db_path + else: + db_path = WorkflowManager().db_file + chatsession:AIChatSession = AIChatSession.get_session(self.current_target,f"{self.username}#{self.current_topic}",db_path,False) + if chatsession is not None: + msgs = chatsession.read_history(num,offset) + result_str = "" + for msg in reversed(msgs): + result_str += f"{msg.sender} >>> {msg.body}" + result_str += f"\n-------------------\n" + return result_str + return f"chatsession not found" + case 'help': + return "help info " + +shell:AIOS_Shell = None + +async def _init_shell(): + global shell + if shell is None: + shell = AIOS_Shell("user") + await shell.initial() + return + +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 start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Send a message when the command /start is issued.""" + await _init_shell() + user = update.effective_user + await update.message.reply_html( + rf"Hi {user.mention_html()}!", + reply_markup=ForceReply(selective=True), + ) + + +async def on_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await _init_shell() + cmd_msg = update.message.text[1:] + logger.info("command:" + cmd_msg) + func_call = parse_function_call(cmd_msg) + if func_call: + shell.username = update.effective_user + show_text = await shell.call_func(func_call[0], func_call[1]) + await update.message.reply_text(show_text) + else: + await update.message.reply_text("command not found!") + +async def on_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await _init_shell() + shell.username = update.effective_user + resp = await shell.send_msg(update.message.text,shell.current_target,shell.current_topic,shell.username) + await update.message.reply_text(resp) + + + + +def main() -> None: + """Start the bot.""" + # Create the Application and pass it your bot's token. + tg_robot_token = os.getenv("TG_ROBOT_TOKEN") + if tg_robot_token is None: + print("TG_ROBOT_TOKEN not found!") + tg_robot_token = input("input your telegram robot token:") + + + if tg_robot_token is None: + return + + application = Application.builder().token(tg_robot_token).build() + + # on different commands - answer in Telegram + application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("help", on_command)) + application.add_handler(CommandHandler("send", on_command)) + application.add_handler(CommandHandler("open", on_command)) + application.add_handler(CommandHandler("history", on_command)) + + # on non command i.e message - echo the message on Telegram + application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_message)) + + # Run the bot until the user presses Ctrl-C + application.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main() \ No newline at end of file From 7e1f7173b43a963d27ee65dbf7c0defafd10e75f Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 10 Sep 2023 20:50:37 -0700 Subject: [PATCH 09/21] Complete AI Function support for Agent. --- doc/mvp/mvp plan 2.md | 4 +- src/aios_kernel/agent.py | 62 +++++++++++++++++++++++++--- src/aios_kernel/ai_function.py | 53 +++++++++++++++++++++--- src/aios_kernel/compute_kernel.py | 11 +++-- src/aios_kernel/compute_task.py | 16 ++++--- src/aios_kernel/environment.py | 22 ++++++++++ src/aios_kernel/open_ai_node.py | 29 ++++++++----- src/aios_kernel/workflow.py | 4 +- src/aios_kernel/workflow_env.py | 10 ++++- src/service/aios_shell/aios_shell.py | 1 + 10 files changed, 173 insertions(+), 39 deletions(-) diff --git a/doc/mvp/mvp plan 2.md b/doc/mvp/mvp plan 2.md index af01fd9..2623120 100644 --- a/doc/mvp/mvp plan 2.md +++ b/doc/mvp/mvp plan 2.md @@ -46,7 +46,7 @@ The previous plan, please see here: [MVP Plan](./mvp%20plan.md) - [ ] MPT-7B, S2 - [ ] Vicuna, S2 - [ ] Embeding,@photosssa,@lurenpluto , A4 - - [ ] Txt2img,@glen0125,A4 + - [x] Txt2img,@glen0125,A4 - [ ] Img2txt(0.5.2),A3 - [ ] Txt2voice,A3 - [ ] Voice2txt, @wugren,A3 @@ -70,7 +70,7 @@ The previous plan, please see here: [MVP Plan](./mvp%20plan.md) - [ ] Telegram Tunnel,S2 - [ ] Discord Tunnel,S2 - [ ] Home IoT Environment (0.5.2), A4 - - [] Compatible Home Assistant (0.5.2), A4 + - [ ] Compatible Home Assistant (0.5.2), A4 - [ ] Build-in Agents/Apps - [ ] Agent: Personal Information Assistant,@photosssa,@lurenpluto , A2 - [ ] Agent: Bulter Jarvis,@waterflier, A2 diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 2680695..0a3f8b0 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -5,9 +5,13 @@ import asyncio import logging import uuid import time +import json from .agent_message import AgentMsg from .chatsession import AIChatSession +from .compute_task import ComputeTaskResult +from .ai_function import AIFunction +from .environment import Environment logger = logging.getLogger(__name__) @@ -77,6 +81,7 @@ class AIAgent: self.chat_db = None self.unread_msg = Queue() # msg from other agent + self.owner_env : Environment = None @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -123,25 +128,70 @@ class AIAgent: return "ignore" return "text" + + def _get_inner_functions(self) -> dict: + if self.owner_env is None: + return None + + all_inner_function = self.owner_env.get_all_ai_functions() + if all_inner_function is None: + return None + + result_func = [] + for inner_func in all_inner_function: + this_func = {} + this_func["name"] = inner_func.get_name() + this_func["description"] = inner_func.get_description() + this_func["parameters"] = inner_func.get_parameters() + result_func.append(this_func) + + return result_func + + async def _execute_func(self,inenr_func_call_node:dict,msg_prompt:AgentPrompt) -> str: + from .compute_kernel import ComputeKernel + + func_name = inenr_func_call_node.get("name") + arguments = json.loads(inenr_func_call_node.get("arguments")) + + func_node : AIFunction = self.owner_env.get_ai_function(func_name) + if func_node is None: + return "execute failed,function not found" + + result_str:str = await func_node.execute(**arguments) + inner_functions = self._get_inner_functions() + msg_prompt.messages.append({"role":"function","content":result_str,"name":func_name}) + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(msg_prompt,self.llm_model_name,self.max_token_size,inner_functions) + + inner_func_call_node = task_result.result_message.get("function_call") + if inner_func_call_node: + return await self._execute_func(inner_func_call_node,msg_prompt) + else: + return task_result.result_str async def _process_msg(self,msg:AgentMsg) -> AgentMsg: from .compute_kernel import ComputeKernel + session_topic = msg.get_sender() + "#" + msg.topic chatsession = AIChatSession.get_session(self.instance_id,session_topic,self.chat_db) prompt = AgentPrompt() prompt.append(self.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)) # chat context msg_prompt = AgentPrompt() msg_prompt.messages = [{"role":"user","content":msg.body}] prompt.append(msg_prompt) - - result = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size) - final_result = result - result_type : str = self._get_llm_result_type(result) + + inner_functions = self._get_inner_functions() + + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) + final_result = task_result.result_str + + inner_func_call_node = task_result.result_message.get("function_call") + if inner_func_call_node: + final_result = await self._execute_func(inner_func_call_node,msg_prompt) + + result_type : str = self._get_llm_result_type(final_result) is_ignore = False match result_type: diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py index e830672..08738ab 100644 --- a/src/aios_kernel/ai_function.py +++ b/src/aios_kernel/ai_function.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod -from typing import Dict +from typing import Dict,Coroutine,Callable class AIFunction: def __init__(self) -> None: - self.intro : str = None + self.description : str = None @abstractmethod def get_name(self) -> str: @@ -17,7 +17,7 @@ class AIFunction: """ return a detailed description of what the function does """ - pass + return self.description @abstractmethod def get_parameters(self) -> Dict: @@ -25,11 +25,23 @@ class AIFunction: Return the list of parameters to execute this function in the form of JSON schema as specified in the OpenAI documentation: https://platform.openai.com/docs/api-reference/chat/create#chat/create-parameters + + str = run_code(code:str) + parameters = { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Python code which needs to be executed" + } + } + } + """ pass @abstractmethod - def execute(self, **kwargs) -> Dict: + async def execute(self, **kwargs) -> str: """ Execute the function and return a JSON serializable dict. The parameters are passed in the form of kwargs @@ -66,4 +78,35 @@ class CallChain: pass async def execute(self): - pass \ No newline at end of file + pass + +class SimpleAIFunction(AIFunction): + def __init__(self,func_id:str,description:str,func_handler:Coroutine,parameters:Dict = None) -> None: + self.func_id = func_id + self.description = description + self.func_handler = func_handler + self.parameters = parameters + + def get_name(self) -> str: + return self.func_id + + def get_parameters(self) -> Dict: + if self.parameters is not None: + return self.parameters + return {"type": "object", "properties": {}} + + async def execute(self,**kwargs) -> str: + if self.func_handler is None: + return "error: function not implemented" + + return await self.func_handler(**kwargs) + + def is_local(self) -> bool: + return True + + def is_in_zone(self) -> bool: + return True + + def is_ready_only(self) -> bool: + return False + diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 2501ad6..5fea958 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -54,7 +54,6 @@ class ComputeKernel: async def _run_task_loop(): while True: - logger.info("compute_kernel is waiting for task...") task = await self.task_queue.get() logger.info(f"compute_kernel get task: {task.display()}") c_node: ComputeNode = self._schedule(task) @@ -91,16 +90,16 @@ class ComputeKernel: return True # friendly interface for use: - def llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0): + def llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0,inner_functions = None): # craete a llm_work_task ,push on queue's end # then task_schedule would run this task.(might schedule some work_task to another host) task_req = ComputeTask() - task_req.set_llm_params(prompt, mode_name, max_token) + task_req.set_llm_params(prompt, mode_name, max_token,inner_functions) self.run(task_req) return task_req - async def do_llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0) -> str: - task_req = self.llm_completion(prompt, mode_name, max_token) + async def do_llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0, inner_functions = None) -> str: + task_req = self.llm_completion(prompt, mode_name, max_token,inner_functions) async def check_timer(): check_times = 0 @@ -120,6 +119,6 @@ class ComputeKernel: await asyncio.create_task(check_timer()) if task_req.state == ComputeTaskState.DONE: - return task_req.result.result_str + return task_req.result return "error!" diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index 086d946..0edd3cf 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -11,7 +11,6 @@ class ComputeTaskState(Enum): ERROR = 3 PENDING = 4 - class ComputeTaskType(Enum): NONE = -1 LLM_COMPLETION = 0 @@ -36,7 +35,7 @@ class ComputeTask: self.result = None self.error_str = None - def set_llm_params(self, prompts, model_name, max_token_size, callchain_id=None): + def set_llm_params(self, prompts, model_name, max_token_size, inner_functions = None, callchain_id=None): self.task_type = ComputeTaskType.LLM_COMPLETION self.create_time = time.time() self.task_id = uuid.uuid4().hex @@ -46,7 +45,13 @@ class ComputeTask: self.params["model_name"] = model_name else: self.params["model_name"] = "gpt-4-0613" - self.params["max_token_size"] = max_token_size + if max_token_size is None: + self.params["max_token_size"] = 4000 + else: + self.params["max_token_size"] = max_token_size + + if inner_functions is not None: + self.params["inner_functions"] = inner_functions def display(self) -> str: return f"ComputeTask: {self.task_id} {self.task_type} {self.state}" @@ -59,9 +64,8 @@ class ComputeTaskResult: self.callchain_id: str = None self.worker_id: str = None self.result_code: int = 0 - self.result_str: str = None - - self.result: dict = {} + self.result_str: str = None # easy to use,can read from result + self.result_message: dict = {} self.result_refers: dict = None self.pading_data: bytearray = None diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 4e064bf..3a11d3b 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -5,6 +5,8 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Optional,Dict,Awaitable,List import logging +from .ai_function import AIFunction + logger = logging.getLogger(__name__) class EnvironmentEvent(ABC): @@ -33,6 +35,8 @@ class Environment: # self.valid_keys:Dict[str,bool] = None self.event_handlers:Dict[str,List[EnvironmentEventHandler]]= {} + self.functions : Dict[str,AIFunction] = {} + def get_id(self) -> str: return self.env_id @@ -44,6 +48,24 @@ class Environment: #def get_env_prompt(self) -> str: # pass + def add_ai_function(self,func:AIFunction) -> None: + if self.functions.get(func.get_name()) is not None: + logger.warn(f"add ai_function {func.get_name()} in env {self.env_id}:function already exist") + + self.functions[func.get_name()] = func + + def get_ai_function(self,func_name:str) -> AIFunction: + return self.functions.get(func_name) + + #def enable_ai_function(self,func_name:str) -> None: + # pass + + #def disable_ai_function(self,func_name:str) -> None: + # pass + + def get_all_ai_functions(self) -> List[AIFunction]: + return self.functions.values() + @abstractmethod def _do_get_value(self,key:str) -> Optional[str]: pass diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 06e0f09..1be47fd 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -56,28 +56,35 @@ class OpenAI_ComputeNode(ComputeNode): logger.info(f"call openai {mode_name} prompts: {prompts}") resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, - max_tokens=4000, - temperature=1.2) + functions=task.params["inner_functions"], + max_tokens=task.params["max_token_size"], + temperature=0.7) # TODO: add temperature to task params? + logger.info(f"openai response: {resp}") - - status_code = resp["choices"][0]["finish_reason"] - if status_code != "stop": - task.state = ComputeTaskState.ERROR - task.error_str = f"The status code was {status_code}." - return None - + result = ComputeTaskResult() result.set_from_task(task) + + status_code = resp["choices"][0]["finish_reason"] + match status_code: + case "function_call": + task.state = ComputeTaskState.DONE + case "stop": + task.state = ComputeTaskState.DONE + case _: + task.state = ComputeTaskState.ERROR + task.error_str = f"The status code was {status_code}." + return None + result.worker_id = self.node_id result.result_str = resp["choices"][0]["message"]["content"] - result.result = resp["choices"][0]["message"] + result.result_message = resp["choices"][0]["message"] return result def start(self): async def _run_task_loop(): while True: - logger.info("openai_node is waiting for task...") task = await self.task_queue.get() logger.info(f"openai_node get task: {task.display()}") result = self._run_task(task) diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 2a8e823..0a01c29 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -13,6 +13,7 @@ from .chatsession import AIChatSession from .role import AIRole,AIRoleGroup from .ai_function import CallChain from .compute_kernel import ComputeKernel +from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState from .bus import AIBus from .workflow_env import WorkflowEnvironment @@ -354,7 +355,8 @@ class Workflow: 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()) + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + result_str = task_result.result_str result = Workflow.prase_llm_result(result_str) logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}") for postmsg in result.post_msgs: diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 864f4fb..9ca2ee3 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -7,9 +7,11 @@ import threading import logging from typing import Optional from .environment import Environment,EnvironmentEvent +from .ai_function import SimpleAIFunction logger = logging.getLogger(__name__) + class CalenderEvent(EnvironmentEvent): def __init__(self,data) -> None: super().__init__() @@ -17,7 +19,7 @@ class CalenderEvent(EnvironmentEvent): self.data = data def display(self) -> str: - return f"#event timer:{self.event_data}" + return f"#event timer:{self.data}" # AI Calender GOAL: Let user use "create notify after 2 days" to create a timer event class CalenderEnvironment(Environment): @@ -25,6 +27,10 @@ class CalenderEnvironment(Environment): super().__init__(env_id) self.is_run = False + self.add_ai_function(SimpleAIFunction("get_time", + "get current time", + self.get_now)) + def _do_get_value(self,key:str) -> Optional[str]: return None @@ -52,7 +58,7 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - def get_now(self,key:str) -> str: + async def get_now(self) -> str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 053c5ec..27f87e4 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -44,6 +44,7 @@ class AIOS_Shell: target_id = msg.target.split(".")[0] agent : AIAgent = await AgentManager().get(target_id) if agent is not None: + agent.owner_env = Environment.get_env_by_id("calender") bus.register_message_handler(target_id,agent._process_msg) return True From 8ffc61f9c5358668962da8d63d8186c25cf376ef Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Mon, 11 Sep 2023 18:49:56 +0800 Subject: [PATCH 10/21] Save email attachment files in folders --- src/service/spider/email_spider.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py index ae6e484..9f744ad 100644 --- a/src/service/spider/email_spider.py +++ b/src/service/spider/email_spider.py @@ -77,6 +77,29 @@ def check_email_saved(client: imaplib.IMAP4_SSL, uid: str): return False +def save_email_attachment(mail: mailparser.MailParser, email_dir: str): + # Traverse all attachments in the email + for attachment in mail.attachments: + if attachment['mail_content_type'] in ['image/png', 'image/jpeg', 'image/gif']: + print('current mail have image attachment') + img_dir = f"{email_dir}/image" + if not os.path.exists(img_dir): + os.makedirs(img_dir) + # get original image name + filename = attachment['filename'] + filefullname = f"{img_dir}/{filename}" + # image bytes data + image_data = attachment['payload'] + try: + # decode base64 image data + image_data = base64.b64decode(image_data) + except base64.binascii.Error: + image_data = image_data.encode() + with open(filefullname, 'wb') as f: + f.write(image_data) + logger.info(f"save email image {filename} success") + + # save email to local file by each folder def save_email(mail: mailparser.MailParser): # create email account dir @@ -98,6 +121,8 @@ def save_email(mail: mailparser.MailParser): del mail_dict['body'] json.dump(mail_dict, f, ensure_ascii=False, indent=4) logger.info(f"save email meta info {f.name}") + # save email attachment( like image) + save_email_attachment(mail, email_dir) From c6e18f96a906802feefe95d64b449c7f284da605 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Mon, 11 Sep 2023 19:58:16 +0800 Subject: [PATCH 11/21] Change the code style to the class style --- src/service/spider/email_spider.py | 212 ++++++++++++++--------------- 1 file changed, 100 insertions(+), 112 deletions(-) diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py index 9f744ad..a2a1154 100644 --- a/src/service/spider/email_spider.py +++ b/src/service/spider/email_spider.py @@ -5,130 +5,118 @@ import logging import mailparser import hashlib import json +import base64 -# logger config -logger = logging.getLogger('email spider') -logger.setLevel(logging.DEBUG) -ch = logging.StreamHandler() -formatter = logging.Formatter('%(asctime)s [%(name)s] [%(levelname)s] %(message)s') -ch.setFormatter(formatter) -logger.addHandler(ch) +class EmailSpider: + def __init__(self): + # logger config + self.logger = logging.getLogger('email spider') + self.logger.setLevel(logging.DEBUG) + ch = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s [%(name)s] [%(levelname)s] %(message)s') + ch.setFormatter(formatter) + self.logger.addHandler(ch) -# read config from toml file -# and read from config config.local.toml if exists (config.local.toml is ignored by git) -config = toml.load('./rootfs/email/config.toml') -if os.path.exists('./rootfs/email/config.local.toml'): - config = toml.load('./rootfs/email/config.local.toml') + # read config from toml file + # and read from config config.local.toml if exists (config.local.toml is ignored by git) + self.config = toml.load('./rootfs/email/config.toml') + if os.path.exists('./rootfs/email/config.local.toml'): + self.config = toml.load('./rootfs/email/config.local.toml') + self.client = self.email_client() -# create email client -def email_client() -> imaplib.IMAP4_SSL: - logger.info(f"read email config from {config.get('EMAIL_IMAP_SERVER')}") - client = imaplib.IMAP4_SSL(config.get('EMAIL_IMAP_SERVER')) - client.login(config.get('EMAIL_ADDRESS'), config.get('EMAIL_PASSWORD')) - return client + def email_client(self) -> imaplib.IMAP4_SSL: + self.logger.info(f"read email config from {self.config.get('EMAIL_IMAP_SERVER')}") + client = imaplib.IMAP4_SSL( + host=self.config.get('EMAIL_IMAP_SERVER'), + port=self.config.get('EMAIL_IMAP_PORT') + ) + client.login(self.config.get('EMAIL_ADDRESS'), self.config.get('EMAIL_PASSWORD')) + return client -def list_box(mail: imaplib.IMAP4_SSL): - _, mailbox_list = mail.list() - for mailbox in mailbox_list: - print(mailbox.decode()) - -def read_emails(client: imaplib.IMAP4_SSL, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"): - client.select(folder) - _, data = client.uid('search', None, imap_keyword) - # get email uid list - email_list = data[0].split() - logger.info(f"got {len(email_list)} emails") - for uid in email_list: - logger.info(f"read email uid {uid}") - if check_email_saved(client, uid): - logger.info(f"email uid {uid} already saved") - continue - else: - read_and_save_email(client, uid) - logger.info(f"email uid {uid} saved") + def list_box(self): + _, mailbox_list = self.client.list() + for mailbox in mailbox_list: + print(mailbox.decode()) + def read_emails(self, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"): + self.client.select(folder) + _, data = self.client.uid('search', None, imap_keyword) + + # get email uid list + email_list = data[0].split() + self.logger.info(f"got {len(email_list)} emails") + for uid in email_list: + if self.check_email_saved(uid): + self.logger.info(f"email uid {uid} already saved") + else: + self.read_and_save_email(uid) + self.logger.info(f"email uid {uid} saved") -def read_and_save_email(client: imaplib.IMAP4_SSL, uid: str): - message_parts = "(BODY.PEEK[])" - _, email_data = client.uid('fetch', uid, message_parts) - mail = mailparser.parse_from_bytes(email_data[0][1]) - logger.info(f"got email subject [{mail.subject}]") - save_email(mail) + def read_and_save_email(self, uid: str): + message_parts = "(BODY.PEEK[])" + _, email_data = self.client.uid('fetch', uid, message_parts) + mail = mailparser.parse_from_bytes(email_data[0][1]) + self.logger.info(f"got email subject [{mail.subject}]") + self.save_email(mail) -def get_local_dir_name(mail: mailparser.MailParser) -> str: - dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" - name = f"{mail.subject}__{mail.date}" - name = hashlib.md5(name.encode('utf-8')).hexdigest() - return f"{dir}/{name}" + def get_local_dir_name(self, mail: mailparser.MailParser) -> str: + dir = f"{self.config.get('LOCAL_DIR')}/{self.config.get('EMAIL_ADDRESS')}" + name = f"{mail.subject}__{mail.date}" + name = hashlib.md5(name.encode('utf-8')).hexdigest() + return f"{dir}/{name}" + def check_email_saved(self, uid: str): + message_parts = "(BODY[HEADER])" + _, email_data = self.client.uid('fetch', uid, message_parts) + mail = mailparser.parse_from_bytes(email_data[0][1]) + self.logger.info(f"[{uid}]check email subject [{mail.subject}]") + dir = self.get_local_dir_name(mail) + self.logger.info(f"check email saved {dir}") + file = f"{dir}/email.txt" + if os.path.exists(file): + return False + return False -# check only need to check email header, not need to download email body -def check_email_saved(client: imaplib.IMAP4_SSL, uid: str): - message_parts = "(BODY[HEADER])" - _, email_data = client.uid('fetch', uid, message_parts) - mail = mailparser.parse_from_bytes(email_data[0][1]) - logger.info(f"check email subject [{mail.subject}]") - dir = get_local_dir_name(mail) - logger.info(f"check email saved {dir}") - file = f"{dir}/email.txt" - if os.path.exists(file): - return True - return False - - -def save_email_attachment(mail: mailparser.MailParser, email_dir: str): - # Traverse all attachments in the email - for attachment in mail.attachments: - if attachment['mail_content_type'] in ['image/png', 'image/jpeg', 'image/gif']: - print('current mail have image attachment') - img_dir = f"{email_dir}/image" - if not os.path.exists(img_dir): - os.makedirs(img_dir) - # get original image name - filename = attachment['filename'] - filefullname = f"{img_dir}/{filename}" - # image bytes data - image_data = attachment['payload'] - try: - # decode base64 image data - image_data = base64.b64decode(image_data) - except base64.binascii.Error: - image_data = image_data.encode() - with open(filefullname, 'wb') as f: - f.write(image_data) - logger.info(f"save email image {filename} success") - - -# save email to local file by each folder -def save_email(mail: mailparser.MailParser): - # create email account dir - dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" - if not os.path.exists(dir): - os.makedirs(dir) - # create email local dir - email_dir = get_local_dir_name(mail) - logger.info(f"save email to {email_dir}") - if not os.path.exists(email_dir): - os.makedirs(email_dir) - - # save email content and meta info - with open(f"{email_dir}/email.txt", "w") as f: - f.write(mail.body) - with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f: - mail_dict = json.loads(mail.mail_json) - if 'body' in mail_dict: - del mail_dict['body'] - json.dump(mail_dict, f, ensure_ascii=False, indent=4) - logger.info(f"save email meta info {f.name}") - # save email attachment( like image) - save_email_attachment(mail, email_dir) - + # save email attachment(images) + def save_email_attachment(self, mail: mailparser.MailParser, email_dir: str): + for attachment in mail.attachments: + if attachment['mail_content_type'] in ['image/png', 'image/jpeg', 'image/gif']: + print('current mail have image attachment') + img_dir = f"{email_dir}/image" + if not os.path.exists(img_dir): + os.makedirs(img_dir) + filename = attachment['filename'] + filefullname = f"{img_dir}/{filename}" + image_data = attachment['payload'] + try: + image_data = base64.b64decode(image_data) + except base64.binascii.Error: + image_data = image_data.encode() + with open(filefullname, 'wb') as f: + f.write(image_data) + self.logger.info(f"save email image {filename} success") + def save_email(self, mail: mailparser.MailParser): + dir = f"{self.config.get('LOCAL_DIR')}/{self.config.get('EMAIL_ADDRESS')}" + if not os.path.exists(dir): + os.makedirs(dir) + email_dir = self.get_local_dir_name(mail) + self.logger.info(f"save email to {email_dir}") + if not os.path.exists(email_dir): + os.makedirs(email_dir) + with open(f"{email_dir}/email.txt", "w") as f: + f.write(mail.body) + with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f: + mail_dict = json.loads(mail.mail_json) + if 'body' in mail_dict: + del mail_dict['body'] + json.dump(mail_dict, f, ensure_ascii=False, indent=4) + self.logger.info(f"save email meta info {f.name}") + self.save_email_attachment(mail, email_dir) if __name__ == "__main__": - mail = email_client() + spider = EmailSpider() folder = 'INBOX' - # imap_keyword = "UNSEEN" imap_keyword = "ALL" - read_emails(mail, folder, imap_keyword) + spider.read_emails(folder, imap_keyword) \ No newline at end of file From 72257b8312196fdde7f65f82a0ace8baf6ae67c6 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Tue, 12 Sep 2023 15:51:48 +0800 Subject: [PATCH 12/21] Parse and save img tag in the email body --- .gitignore | 1 + src/service/spider/email_spider.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/.gitignore b/.gitignore index 84349bb..ae3bcfd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.pyc rootfs/email/config.local.toml rootfs/data +venv \ No newline at end of file diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py index a2a1154..cd44097 100644 --- a/src/service/spider/email_spider.py +++ b/src/service/spider/email_spider.py @@ -1,3 +1,21 @@ +""" +Capture your email locally, and parse out the pictures in the email body and the pictures, videos and other files in the attachment. Subsequently, it supports vectorized analysis of your personal data and serves as a knowledge base to enable large language model answers. Better results. + +An example of a local file is as follows: +├── data +│ └── alex0072@gmail.com +│ └── 5de3e52f3a6b90cabe6cbdd4ae3a5c5b +│ ├── email.txt +│ ├── meta.json +│ ├── image +│ │ ├── 0648B869@99C03070.DB94B354.jpg +│ └── body_image +│ ├── 11044884873.jpg +│ ├── 282985198265470.gif +│ └── dd-login-service-min.png + +""" + import imaplib import os import toml @@ -6,6 +24,8 @@ import mailparser import hashlib import json import base64 +from bs4 import BeautifulSoup +import requests class EmailSpider: def __init__(self): @@ -46,6 +66,7 @@ class EmailSpider: # get email uid list email_list = data[0].split() self.logger.info(f"got {len(email_list)} emails") + email_list.reverse() for uid in email_list: if self.check_email_saved(uid): self.logger.info(f"email uid {uid} already saved") @@ -97,6 +118,31 @@ class EmailSpider: f.write(image_data) self.logger.info(f"save email image {filename} success") + # save email body images(html content) + def save_body_images(self, html_content: str, email_dir: str): + # get all image urls + soup = BeautifulSoup(html_content, 'html.parser') + img_tags = soup.find_all('img') + img_urls = [img['src'] for img in img_tags if 'src' in img.attrs] + self.logger.info(f'Found {len(img_urls)} images in email body') + + if not os.path.exists(email_dir): + os.makedirs(email_dir) + + for img_url in img_urls: + # keep the original image filename(last of url) + img_filename = os.path.join(email_dir, img_url.split('/')[-1]) + # download image + response = requests.get(img_url, stream=True) + if response.status_code == 200: + with open(img_filename, 'wb') as img_file: + for chunk in response.iter_content(1024): + img_file.write(chunk) + self.logger.info(f'Downloaded {img_url} to {img_filename}') + else: + self.logger.info(f'Failed to download {img_url}') + + # save email content to local dir def save_email(self, mail: mailparser.MailParser): dir = f"{self.config.get('LOCAL_DIR')}/{self.config.get('EMAIL_ADDRESS')}" if not os.path.exists(dir): @@ -113,7 +159,10 @@ class EmailSpider: del mail_dict['body'] json.dump(mail_dict, f, ensure_ascii=False, indent=4) self.logger.info(f"save email meta info {f.name}") + self.save_email_attachment(mail, email_dir) + self.save_body_images(mail.body, f"{email_dir}/body_image") + if __name__ == "__main__": spider = EmailSpider() From dd3187fc8aba98bd69b5ff1fa48a0f41732f2688 Mon Sep 17 00:00:00 2001 From: zhangzhen Date: Thu, 14 Sep 2023 15:22:38 +0800 Subject: [PATCH 13/21] Provide the local llama service --- src/aios_kernel/compute_kernel.py | 2 +- src/aios_kernel/compute_node.py | 5 +- src/aios_kernel/local_llama_compute_node.py | 86 +++++++++++++++++++++ src/aios_kernel/open_ai_node.py | 4 +- src/aios_kernel/queue_compute_node.py | 69 +++++++++++++++++ src/aios_kernel/stability_node.py | 4 +- 6 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 src/aios_kernel/local_llama_compute_node.py create mode 100644 src/aios_kernel/queue_compute_node.py diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 5fea958..381dda9 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -65,7 +65,7 @@ class ComputeKernel: def _schedule(self, task) -> ComputeNode: for node in self.compute_nodes.values(): - if node.is_support(task.task_type) is True: + if node.is_support(task) is True: return node logger.warning( f"task {task.display()} is not support by any compute node") diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 7e159f7..873fecd 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -28,7 +28,7 @@ class ComputeNode(ABC): pass @abstractmethod - def is_support(self, task_type: ComputeTaskType) -> bool: + def is_support(self, task: ComputeTask) -> bool: pass @abstractmethod @@ -41,10 +41,9 @@ class ComputeNode(ABC): def get_fee_type(self) -> str: return "free" - class LocalComputeNode(ComputeNode): def display(self) -> str: return super().display() def is_local(self) -> bool: - return True + return True \ No newline at end of file diff --git a/src/aios_kernel/local_llama_compute_node.py b/src/aios_kernel/local_llama_compute_node.py new file mode 100644 index 0000000..0931f1f --- /dev/null +++ b/src/aios_kernel/local_llama_compute_node.py @@ -0,0 +1,86 @@ + +import logging +import requests +from typing import Optional +from pydantic import BaseModel + +from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskType +from .queue_compute_node import Queue_ComputeNode + +logger = logging.getLogger(__name__) + +""" +This is a custom implementation, it should be redesigned. +""" + +class LocalLlama_ComputeNode(Queue_ComputeNode): + async def execute_task(self, task: ComputeTask) -> { + "content": str, + "message": str, + "state": ComputeTaskState, + "error": { + "code": int, + "message": str, + } + }: + class GenerateResponse(BaseModel): + error: Optional[int] + msg: Optional[str] + results: Optional[str] + + try: + body = { + "prompts": task.params["prompts"] + } + + response = requests.post("http://aigc:7880/generate", data = body, verify=False) + response.close() + + logger.info(f"LocalLlama_ComputeNode task responsed, request: {body}, status-code: {response.status_code}, headers: {response.headers}, content: {response.content}") + + if response.status_code != 200: + return { + "state": ComputeTaskState.ERROR, + "error": { + "code": response.status_code, + "message": "http request failed: " + response.status_code + } + } + else: + resp = GenerateResponse.parse_raw(response.content.decode("utf-8")) + if resp.error: + return { + "state": ComputeTaskState.ERROR, + "error": { + "code": resp.error, + "message": "local llama failed:" + resp.msg + } + } + else: + return { + "content": str(resp.results), + "message": {} + } + except Exception as err: + import traceback + logger.error(f"{traceback.format_exc()}, error: {err}") + + return { + "state": ComputeTaskState.ERROR, + "error": { + "code": -1, + "message": "unknown exception: " + str(err) + } + } + + def display(self) -> str: + return f"LocalLlama_ComputeNode: {self.node_id}" + + def get_capacity(self): + pass + + def is_support(self, task: ComputeTask) -> bool: + return task.task_type == ComputeTaskType.LLM_COMPLETION and (not task.params["model_name"] or task.params["model_name"] == "llama") + + def is_local(self) -> bool: + return True diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 1be47fd..e2ef098 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -103,8 +103,8 @@ class OpenAI_ComputeNode(ComputeNode): def get_capacity(self): pass - def is_support(self, task_type: ComputeTaskType) -> bool: - return task_type == ComputeTaskType.LLM_COMPLETION + def is_support(self, task: ComputeTask) -> bool: + return task.task_type == ComputeTaskType.LLM_COMPLETION and (not task.params["model_name"] or task.params["model_name"] == "gpt-4-0613") def is_local(self) -> bool: return False diff --git a/src/aios_kernel/queue_compute_node.py b/src/aios_kernel/queue_compute_node.py new file mode 100644 index 0000000..e975cd8 --- /dev/null +++ b/src/aios_kernel/queue_compute_node.py @@ -0,0 +1,69 @@ + +import asyncio +from asyncio import Queue +import logging +from abc import abstractmethod + +from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType +from .compute_node import ComputeNode + +logger = logging.getLogger(__name__) + +class Queue_ComputeNode(ComputeNode): + def __init__(self): + super().__init__() + self.task_queue = Queue() + + @abstractmethod + async def execute_task(self, task: ComputeTask) -> { + "content": str, + "message": str, + "state": ComputeTaskState, + "error": { + "code": int, + "message": str, + } + }: + pass + + async def push_task(self, task: ComputeTask, proiority: int = 0): + logger.info(f"{self.display()} push task: {task.display()}") + self.task_queue.put_nowait(task) + + async def remove_task(self, task_id: str): + pass + + async def _run_task(self, task: ComputeTask): + task.state = ComputeTaskState.RUNNING + + resp = await self.execute_task(task) + + result = ComputeTaskResult() + result.set_from_task(task) + + task.state = resp["state"] + + if task.state == ComputeTaskState.ERROR: + task.error_str = resp["error"]["message"] + + + result.worker_id = self.node_id + result.result_str = resp["content"] + result.result_message = resp["message"] + + return result + + def start(self): + async def _run_task_loop(): + while True: + task = await self.task_queue.get() + logger.info(f"{self.display()} get task: {task.display()}") + result = await self._run_task(task) + if result is not None: + task.result = result + + asyncio.create_task(_run_task_loop()) + + + def get_task_state(self, task_id: str): + pass diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index 8d40878..b1f0da9 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -129,8 +129,8 @@ class Stability_ComputeNode(ComputeNode): def get_capacity(self): pass - def is_support(self, task_type: ComputeTaskType) -> bool: - return task_type == ComputeTaskType.TEXT_2_IMAGE + def is_support(self, task: ComputeTask) -> bool: + return task.task_type == ComputeTaskType.TEXT_2_IMAGE def is_local(self) -> bool: return False From c4d97942cb25d81d6f1cb040a115716bf34ae4eb Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 14 Sep 2023 01:50:18 -0700 Subject: [PATCH 14/21] Refactor chatsession impl, fix bug. --- doc/mvp/workflow.drawio | 28 ++- rootfs/agents/agent_1/agent.toml | 1 + rootfs/workflows/math_school/workflow.toml | 2 +- rootfs/workflows/plan_maker/workflow.toml | 6 +- src/aios_kernel/__init__.py | 2 +- src/aios_kernel/agent.py | 91 +++----- src/aios_kernel/agent_message.py | 96 +++++++- src/aios_kernel/bus.py | 95 ++++---- src/aios_kernel/chatsession.py | 100 ++++++--- src/aios_kernel/environment.py | 6 - src/aios_kernel/open_ai_node.py | 12 +- src/aios_kernel/workflow.py | 250 ++++++++++++++------- src/aios_kernel/workflow_env.py | 10 +- src/service/aios_shell/aios_shell.py | 12 +- src/service/aios_shell/tg_aios_shell.py | 2 +- 15 files changed, 477 insertions(+), 236 deletions(-) diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index 54e724c..1ca245b 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -365,4 +365,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rootfs/agents/agent_1/agent.toml b/rootfs/agents/agent_1/agent.toml index 81966ab..53d0605 100644 --- a/rootfs/agents/agent_1/agent.toml +++ b/rootfs/agents/agent_1/agent.toml @@ -1,5 +1,6 @@ instance_id = "agent_1" fullname = "tracy wang" + [[prompt]] role = "system" content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml index 48c9831..69b998b 100644 --- a/rootfs/workflows/math_school/workflow.toml +++ b/rootfs/workflows/math_school/workflow.toml @@ -7,7 +7,7 @@ GOAL="成为最好的学校" [[connected_env]] env_id = "calender" [[connected_env.event2msg]] -timer = "现在是{data}" +timer = "现在是{now}" role = "教导处主任" [filter] diff --git a/rootfs/workflows/plan_maker/workflow.toml b/rootfs/workflows/plan_maker/workflow.toml index 1dc80bf..3975313 100644 --- a/rootfs/workflows/plan_maker/workflow.toml +++ b/rootfs/workflows/plan_maker/workflow.toml @@ -42,11 +42,13 @@ fullname = "经理" agent="manager" [[roles.manager.prompt]] role="system" -content="""你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是: +content=""" +你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是: 1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。 3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。 -4. 当你决定要和工作小组通信时,请使用 sendmsg(小组名称,内容) 的形式。""" +4. 当你决定要和工作小组通信时,请使用 sendmsg(小组名称,内容) 的形式。 +""" diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bef117f..fb09d88 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -1,5 +1,5 @@ from .environment import Environment,EnvironmentEvent -from .agent_message import AgentMsg,AgentMsgState +from .agent_message import AgentMsg,AgentMsgStatus from .chatsession import AIChatSession from .agent import AIAgent,AIAgentTemplete,AgentPrompt from .compute_kernel import ComputeKernel,ComputeTask diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 0a3f8b0..0c3e106 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -7,7 +7,7 @@ import uuid import time import json -from .agent_message import AgentMsg +from .agent_message import AgentMsg, AgentMsgStatus, AgentMsgType from .chatsession import AIChatSession from .compute_task import ComputeTaskResult from .ai_function import AIFunction @@ -73,7 +73,7 @@ class AIAgent: self.prompt:AgentPrompt = None self.llm_model_name:str = None self.max_token_size:int = 3600 - self.instance_id:str = None + self.agent_id:str = None self.template_id:str = None self.fullname:str = None self.powerby = None @@ -82,6 +82,7 @@ class AIAgent: self.chat_db = None self.unread_msg = Queue() # msg from other agent self.owner_env : Environment = None + self.owenr_bus : AIBus = None @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -90,7 +91,7 @@ class AIAgent: result_agent.llm_model_name = templete.llm_model_name result_agent.max_token_size = templete.max_token_size result_agent.template_id = templete.template_id - result_agent.instance_id = "agent#" + uuid.uuid4().hex + result_agent.agent_id = "agent#" + uuid.uuid4().hex result_agent.fullname = fullname result_agent.powerby = templete.author result_agent.prompt = templete.prompt @@ -100,10 +101,10 @@ class AIAgent: if config.get("instance_id") is None: logger.error("agent instance_id is None!") return False - self.instance_id = config["instance_id"] + self.agent_id = config["instance_id"] if config.get("fullname") is None: - logger.error(f"agent {self.instance_id} fullname is None!") + logger.error(f"agent {self.agent_id} fullname is None!") return False self.fullname = config["fullname"] @@ -147,7 +148,7 @@ class AIAgent: return result_func - async def _execute_func(self,inenr_func_call_node:dict,msg_prompt:AgentPrompt) -> str: + async def _execute_func(self,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg) -> str: from .compute_kernel import ComputeKernel func_name = inenr_func_call_node.get("name") @@ -157,14 +158,21 @@ class AIAgent: if func_node is None: return "execute failed,function not found" + ineternal_call_record = AgentMsg.create_internal_call_msg(func_name,arguments,org_msg.get_msg_id(),org_msg.target) + result_str:str = await func_node.execute(**arguments) - inner_functions = self._get_inner_functions() - msg_prompt.messages.append({"role":"function","content":result_str,"name":func_name}) - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(msg_prompt,self.llm_model_name,self.max_token_size,inner_functions) + inner_functions = self._get_inner_functions() + prompt.messages.append({"role":"function","content":result_str,"name":func_name}) + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) + + ineternal_call_record.result_str = task_result.result_str + ineternal_call_record.done_time = time.time() + org_msg.inner_call_chain.append(ineternal_call_record) + inner_func_call_node = task_result.result_message.get("function_call") if inner_func_call_node: - return await self._execute_func(inner_func_call_node,msg_prompt) + return await self._execute_func(inner_func_call_node,prompt,org_msg) else: return task_result.result_str @@ -172,7 +180,13 @@ class AIAgent: from .compute_kernel import ComputeKernel session_topic = msg.get_sender() + "#" + msg.topic - chatsession = AIChatSession.get_session(self.instance_id,session_topic,self.chat_db) + chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db) + if msg.mentions is not None: + if not self.agent_id in msg.mentions: + chatsession.append(msg) + logger.info(f"agent {self.agent_id} recv a group chat message from {msg.sender},but is not mentioned,ignore!") + return None + prompt = AgentPrompt() prompt.append(self.prompt) # prompt.append(self._get_knowlege_prompt(the_role.get_name())) @@ -189,67 +203,27 @@ class AIAgent: inner_func_call_node = task_result.result_message.get("function_call") if inner_func_call_node: - final_result = await self._execute_func(inner_func_call_node,msg_prompt) + #TODO to save more token ,can i use msg_prompt? + final_result = await self._execute_func(inner_func_call_node,prompt,msg) result_type : str = self._get_llm_result_type(final_result) is_ignore = False match result_type: - # case "function": - # callchain:CallChain = self._parse_function_call_chain(result) - # 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) - # 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 - - - # case "send_message": - # # send message to other / sub workflow - # next_msg:AgentMsg = self._parse_to_msg(result) - # if next_msg is not None: - # # TODO: Next Target can be another role in workflow - # next_workflow:Workflow = self.get_workflow(next_msg.get_target()) - # inner_chat_session = the_role.agent.get_chat_session(next_msg.get_target(),next_msg.get_session_id()) - - # inner_chat_session.append_post(next_msg) - # resp = await next_workflow.send_msg(next_msg) - # inner_chat_session.append_recv(resp) - # # generator proc resp prompt with WAITING state - # 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 - - #case "post_message": - # # post message to other / sub workflow - # next_msg:AgentMsg = self._parse_to_msg(result) - # if next_msg is not None: - # next_workflow:Workflow = self.get_workflow(next_msg.get_target()) - # inner_chat_session = the_role.agent.get_chat_session(next_msg.get_target(),next_msg.get_session_id()) - # inner_chat_session.append_post(next_msg) - # next_workflow.post_msg(next_msg) - case "ignore": is_ignore = True if is_ignore is not True: - # TODO : how to get inner chat session? - resp_msg = AgentMsg() - resp_msg.set(self.instance_id,msg.sender,final_result) - resp_msg.topic = msg.topic - - if chatsession is not None: - chatsession.append_recv(msg) - chatsession.append_post(resp_msg) + resp_msg = msg.create_resp_msg(final_result) + chatsession.append(msg) + chatsession.append(resp_msg) return resp_msg return None def get_id(self) -> str: - return self.instance_id + return self.agent_id def get_fullname(self) -> str: return self.fullname @@ -263,7 +237,8 @@ class AIAgent: def get_max_token_size(self) -> int: return self.max_token_size - async def _get_prompt_from_session(self,chatsession:AIChatSession) -> AgentPrompt: + async def _get_prompt_from_session(self,chatsession:AIChatSession,is_groupchat=False) -> AgentPrompt: + # TODO: get prompt from group chat is different from single chat messages = chatsession.read_history() # read last 10 message result_prompt = AgentPrompt() for msg in reversed(messages): diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 242cc66..0640319 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -3,26 +3,100 @@ import uuid import time import re -class AgentMsgState(Enum): +class AgentMsgType(Enum): + TYPE_MSG = 0 + TYPE_INTERNAL_CALL = 1 + TYPE_ACTION = 2 + TYPE_EVENT = 3 + +class AgentMsgStatus(Enum): RESPONSED = 0 INIT = 1 SENDING = 2 PROCESSING = 3 ERROR = 4 + RECVED = 5 + EXECUTED = 6 + +# msg is a msg / msg resp +# msg body可以有内容类型(MIME标签),text, image, voice, video, file,以及富文本(html) +# msg is a inner function call with result +# msg is a Action with result + +# qutoe Msg +# forword msg +# reply msg + +# 逻辑上的同一个Message在同一个session中看到的msgid相同 +# 在不同的session中看到的msgid不同 class AgentMsg: - def __init__(self) -> None: + def __init__(self,msg_type=AgentMsgType.TYPE_MSG) -> None: + self.msg_id = "" + self.msg_type:AgentMsgType = msg_type + + self.prev_msg_id:str = None + self.quote_msg_id:str = None + self.rely_msg_id:str = None # if not none means this is a respone msg + self.session_id:str = None + self.create_time = 0 - self.sender:str = None + self.done_time = 0 + self.topic:str = None # topic is use to find session, not store in db + + self.sender:str = None # obj_id.sub_objid@tunnel_id self.target:str = None + self.mentions:[] = None #use in group chat only + #self.title:str = None self.body:str = None - self.topic:str = "T#" + uuid.uuid4().hex - #self.msg_type = 0 - self.state = AgentMsgState.INIT + self.body_mime:str = None #//default is "text/plain",encode is utf8 + + #type is call / action + self.func_name = None + self.args = None + self.result_str = None + + #type is event + self.event_name = None + self.event_args = None + + self.status = AgentMsgStatus.INIT + self.inner_call_chain = [] self.resp_msg = None + @classmethod + def create_internal_call_msg(self,func_name:str,args:dict,prev_msg_id:str,caller:str): + msg = AgentMsg(AgentMsgType.TYPE_INTERNAL_CALL) + msg.func_name = func_name + msg.args = args + msg.prev_msg_id = prev_msg_id + msg.sender = caller + return msg + + def create_action_msg(self,action_name:str,args:dict,caller:str): + msg = AgentMsg(AgentMsgType.TYPE_ACTION) + msg.func_name = action_name + msg.args = args + msg.prev_msg_id = self.msg_id + msg.topic = self.topic + msg.sender = caller + return msg + + def create_resp_msg(self,resp_body): + resp_msg = AgentMsg() + resp_msg.msg_id = "msg#" + uuid.uuid4().hex + self.create_time = time.time() + + resp_msg.rely_msg_id = self.msg_id + resp_msg.sender = self.target + resp_msg.target = self.sender + resp_msg.body = resp_body + resp_msg.topic = self.topic + + return resp_msg + def set(self,sender:str,target:str,body:str,topic:str=None) -> None: - self.id = "msg#" + uuid.uuid4().hex + self.msg_id = "msg#" + uuid.uuid4().hex self.sender = sender self.target = target self.body = body @@ -31,7 +105,7 @@ class AgentMsg: self.topic = topic def get_msg_id(self) -> str: - return self.id + return self.msg_id def get_sender(self) -> str: return self.sender @@ -39,6 +113,12 @@ class AgentMsg: def get_target(self) -> str: return self.target + def get_prev_msg_id(self) -> str: + return self.prev_msg_id + + def get_quote_msg_id(self) -> str: + return self.quote_msg_id + @classmethod def parse_function_call(cls,func_string:str): match = re.search(r'\s*(\w+)\s*\(\s*(.*)\s*\)\s*', func_string) diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py index ae1853e..22bf209 100644 --- a/src/aios_kernel/bus.py +++ b/src/aios_kernel/bus.py @@ -1,5 +1,5 @@ -from typing import Any -from .agent_message import AgentMsg,AgentMsgState +from typing import Coroutine,Dict,Any +from .agent_message import AgentMsg,AgentMsgStatus import asyncio from asyncio import Queue @@ -8,19 +8,32 @@ import logging logger = logging.getLogger(__name__) class AIBusHandler: - def __init__(self,handler:Any) -> None: + def __init__(self,handler:Coroutine,owner_bus,enable_defualt_proc=True) -> None: self.handler = handler self.working_task = None - self.results = {} + self.results = {} # recv resps self.queue:Queue = Queue() + self.enable_defualt_proc = enable_defualt_proc + self.owner_bus = owner_bus async def handle_message(self,msg:AgentMsg) -> Any: if self.handler is None: return None - return await self.handler(msg) + if self.enable_defualt_proc: + # do default process + if msg.rely_msg_id is not None: + self.results[msg.rely_msg_id] = msg + return None + + resp_msg = await self.handler(msg) + if self.enable_defualt_proc: + if resp_msg is not None: + await self.owner_bus.post_message(resp_msg,False) + return resp_msg + class AIBus: _instance = None @classmethod @@ -30,11 +43,13 @@ class AIBus: return cls._instance def __init__(self) -> None: - self.handlers = {} - self.unhandle_handler = None - - async def post_message(self,target_id,msg:AgentMsg,use_unhandle=True) -> bool: - target_id = target_id.split(".")[0] + self.handlers:Dict[AIBusHandler] = {} + self.unhandle_handler:Coroutine = None + + + async def post_message(self,msg:AgentMsg,use_unhandle=True) -> bool: + target_id = msg.target.split(".")[0] + handler = self.handlers.get(target_id) if handler: handler.queue.put_nowait(msg) @@ -44,46 +59,39 @@ class AIBus: if use_unhandle: if self.unhandle_handler is not None: if await self.unhandle_handler(self,msg): - return await self.post_message(target_id,msg,False) + return await self.post_message(msg,False) logger.warn(f"post message to {msg.target} failed!,target not found") return False - def resp_message(self,my_id:str,org_msg_id:str,resp:AgentMsg) -> None: - handler = self.handlers.get(my_id) - if handler is None: - return None - handler.results[org_msg_id] = resp + async def resp_message(self,org_msg_id:str,resp:AgentMsg) -> None: + assert resp.rely_msg_id == org_msg_id + return await self.post_message(resp) - async def get_message_resp(self,name:str,msg_id:str) -> AgentMsg: - handler = self.handlers.get(name) - if handler is None: + async def send_message(self,msg:AgentMsg) -> AgentMsg: + sender_id = msg.sender.split(".")[0] + sender_handler = self.handlers.get(sender_id) # sender already register on bus + if sender_handler is None: + logger.warn(f"sender {sender_id} not register on AI_BUS!") return None - 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) + post_result = await self.post_message(msg) if post_result is False: return None - - handler = self.handlers.get(target_id) - if handler is None: - return None - + retry_times = 0 while True: - resp = handler.results.get(msg.id) + resp = sender_handler.results.get(msg.msg_id) if resp is not None: msg.resp_msg = resp - msg.state = AgentMsgState.RESPONSED + msg.status = AgentMsgStatus.RESPONSED + del sender_handler.results[msg.msg_id] return resp await asyncio.sleep(0.2) retry_times += 1 if retry_times > 100: - msg.state = AgentMsgState.ERROR + msg.status = AgentMsgStatus.ERROR return None return None @@ -93,7 +101,7 @@ class AIBus: # means sub def register_message_handler(self,handler_name:str,handler:Any) -> Queue: - handler_node = AIBusHandler(handler) + handler_node = AIBusHandler(handler,self) self.handlers[handler_name] = handler_node return handler_node.queue @@ -102,13 +110,12 @@ class AIBus: # Wait for a message message = await handler.queue.get() - try: + #try: # Try to handle the message - result = await handler.handle_message(message) - handler.results[message.id] = result - except Exception as e: + await handler.handle_message(message) + #except Exception as e: # If an error occurs, put the message back into the queue - logger.error(f"handle message {message.id} failed! {e}") + # logger.error(f"handle message {message.msg_id} failed! {e}") #self.queues[name].put_nowait(message) return @@ -126,14 +133,4 @@ class AIBus: logger.warn(f"handler {target_name} is already working!") return - 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) \ No newline at end of file + handler.working_task = asyncio.create_task(self.process_queue(handler)) \ No newline at end of file diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py index ecd90a3..5893683 100644 --- a/src/aios_kernel/chatsession.py +++ b/src/aios_kernel/chatsession.py @@ -5,8 +5,9 @@ import logging import threading import datetime import uuid +import json -from .agent_message import AgentMsg +from .agent_message import AgentMsgType, AgentMsg, AgentMsgStatus class ChatSessionDB: def __init__(self, db_file): @@ -54,14 +55,31 @@ class ChatSessionDB: """) # create messages table + # reciver_id could be None + conn.execute(""" CREATE TABLE IF NOT EXISTS Messages ( MessageID TEXT PRIMARY KEY, SessionID TEXT, - SenderID TEXT, + MsgType INTEGER, + PrevMsgID TEXT, + QuoteMsgID TEXT, + RelyMsgID TEXT, + + SenderID TEXT, ReceiverID TEXT, Timestamp TEXT, + + Topic TEXT, + Mentions TEXT, + ContentMIME TEXT, Content TEXT, + + ActionName TEXT, + ActionParams TEXT, + ActionResult TEXT, + DoneTime TEXT, + Status INTEGER ); """) @@ -83,15 +101,43 @@ class ChatSessionDB: logging.error("Error occurred while inserting session: %s", e) return -1 # return -1 if an error occurs - def insert_message(self, message_id, session_id, sender_id, receiver_id, timestamp, content, status): + def insert_message(self, msg:AgentMsg): """ insert a new message into the Messages table """ try: + action_name = None + action_params = None + action_result = None + mentions = None + if msg.mentions: + mentions = json.dumps(msg.mentions) + + match msg.msg_type: + case AgentMsgType.TYPE_MSG: + pass + case AgentMsgType.TYPE_ACTION: + action_name = msg.func_name + action_params = json.dumps(msg.args) + action_result = msg.result_str + case AgentMsgType.TYPE_INTERNAL_CALL: + action_name = msg.func_name + action_params = json.dumps(msg.args) + action_result = msg.result_str + case AgentMsgType.TYPE_EVENT: + action_name = msg.event_name + action_params = json.dumps(msg.event_args) + + conn = self._get_conn() conn.execute(""" - INSERT INTO Messages (MessageID, SessionID, SenderID, ReceiverID, Timestamp, Content, Status) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, (message_id, session_id, sender_id, receiver_id, timestamp, content, status)) + 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)) conn.commit() + + if msg.inner_call_chain: + for inner_call in msg.inner_call_chain: + self.insert_message(inner_call) + return 0 # return 0 if successful except Error as e: logging.error("Error occurred while inserting message: %s", e) @@ -134,7 +180,7 @@ class ChatSessionDB: """Get a message by its ID""" conn =self._get_conn() c = conn.cursor() - c.execute("SELECT MessageID,SessionID,SenderID,ReceiverID,Timestamp,Content,Status FROM Messages WHERE MessageID = ?", (message_id,)) + c.execute("SELECT MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status FROM Messages WHERE MessageID = ?", (message_id,)) message = c.fetchone() return message @@ -144,7 +190,7 @@ class ChatSessionDB: conn = self._get_conn() cursor = conn.cursor() cursor.execute(""" - SELECT MessageID,SessionID,SenderID,ReceiverID,Timestamp,Content,Status FROM Messages + SELECT MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status FROM Messages WHERE SessionID = ? ORDER BY Timestamp DESC LIMIT ? OFFSET ? @@ -222,29 +268,31 @@ class AIChatSession: result = [] for msg in msgs: agent_msg = AgentMsg() - agent_msg.id = msg[0] - agent_msg.sender = msg[2] - agent_msg.target = msg[3] - agent_msg.create_time = msg[4] - agent_msg.body = msg[5] - # agent_msg.state = msg[6] + agent_msg.msg_id = msg[0] + agent_msg.session_id = msg[1] + agent_msg.msg_type = AgentMsgType(msg[2]) + agent_msg.prev_msg_id = msg[3] + agent_msg.sender = msg[4] + agent_msg.target = msg[5] + agent_msg.create_time = msg[6] + agent_msg.topic = msg[7] + if msg[8] is not None: + agent_msg.mentions = json.loads(msg[8]) + agent_msg.body_mime = msg[9] + agent_msg.body = msg[10] + agent_msg.func_name = msg[11] + if msg[12] is not None: + agent_msg.args = json.loads(msg[12]) + agent_msg.result_str = msg[13] + agent_msg.done_time = msg[14] + agent_msg.status = AgentMsgStatus(msg[15]) result.append(agent_msg) return result def append(self,msg:AgentMsg) -> None: - self.db.insert_message(msg.id,self.session_id,msg.sender,msg.target,msg.create_time,msg.body,0) - - 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" - 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" - self.append(msg) + msg.session_id = self.session_id + self.db.insert_message(msg) #def attach_event_handler(self,handler) -> None: # """chat session changed event handler""" diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 3a11d3b..49ea72b 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -70,12 +70,6 @@ class Environment: def _do_get_value(self,key:str) -> Optional[str]: pass - def get_functions(self): - # system functions - # env functions - # user install functions - pass - def register_get_handler(self,key:str,handler:Callable) -> None: h = self.get_handlers.get(key) if h is not None: diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 1be47fd..3a665a3 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -54,11 +54,17 @@ class OpenAI_ComputeNode(ComputeNode): prompts = task.params["prompts"] logger.info(f"call openai {mode_name} prompts: {prompts}") - resp = openai.ChatCompletion.create(model=mode_name, + if task.params.get("inner_functions") is None: + resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, - functions=task.params["inner_functions"], max_tokens=task.params["max_token_size"], - temperature=0.7) # TODO: add temperature to task params? + temperature=0.7) + else: + resp = openai.ChatCompletion.create(model=mode_name, + messages=prompts, + functions=task.params["inner_functions"], + max_tokens=task.params["max_token_size"], + temperature=0.7) # TODO: add temperature to task params? logger.info(f"openai response: {resp}") diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 0a01c29..58d0538 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -2,21 +2,23 @@ import logging import asyncio import json import os +import time from asyncio import Queue from typing import Optional,Tuple from abc import ABC, abstractmethod from .environment import Environment,EnvironmentEvent -from .agent_message import AgentMsg,AgentMsgState +from .agent_message import AgentMsg,AgentMsgStatus from .agent import AgentPrompt,AgentMsg from .chatsession import AIChatSession from .role import AIRole,AIRoleGroup -from .ai_function import CallChain +from .ai_function import AIFunction from .compute_kernel import ComputeKernel from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState from .bus import AIBus from .workflow_env import WorkflowEnvironment + logger = logging.getLogger(__name__) class MessageFilter: @@ -158,11 +160,12 @@ class Workflow: 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]) + current_workflow_chatsession = AIChatSession.get_session(current_workflow.workflow_id,msg.sender + "#" + msg.topic,current_workflow.db_file) if the_role is not None: - return await current_workflow.role_process_msg(msg,the_role) + return await current_workflow.role_process_msg(msg,the_role,current_workflow_chatsession) sub_workflow = current_workflow.sub_workflows.get(inner_obj_id[i]) if sub_workflow is not None: - return await sub_workflow.process_msg(msg) + return await sub_workflow._process_msg(msg) logger.error(f"{msg.target} not found! forword message failed!") return None else: @@ -176,18 +179,50 @@ class Workflow: logger.error(f"{msg.target} not found! forword message failed!") return None + def get_workflow_id_from_target(self,target:str) -> str: + target_list = target.split(".") + if len(target_list) == 0: + return target + else: + result_str = "" + p = 0 + for s in target_list: + p = p + 1 + result_str += s + if p < len(target_list)-1: + result_str += "." + else: + return result_str 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 - + real_target = msg.target.split(".")[0] targets = self._parse_msg_target(msg.target) if len(targets) > 1: return await self._forword_msg(targets,msg) - + #0 we don't support workflow join a group right now, this cloud be a feture in future + if msg.mentions is not None: + logger.warn(f"workflow {self.workflow_id} recv a group chat message,not support ignore!") + return None + + #1. workflow start process message + final_result = None + + # this is workflow's group_chat session + session_topic = msg.sender + "#" + msg.topic + chatsesssion = AIChatSession.get_session(self.workflow_id,session_topic,self.db_file) + + #2. find role by msg.mentions or workflow's selector logic + if msg.mentions is not None: + if not self.workflow_id in msg.mentions: + chatsesssion.append(msg) + logger.info(f"workflow {self.workflow_id} recv a group chat message from {msg.sender},but is not mentioned,ignore!") + return None + + for mention in msg.mentions: + this_role = self.role_group.get(mention) + if this_role is not None: + return await self.role_process_msg(msg,this_role,chatsesssion) + if self.input_filter is not None: select_role_id = self.input_filter.select(msg) if select_role_id is not None: @@ -195,27 +230,13 @@ class Workflow: if select_role is None: 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) - if result is None: - logger.error(f"_process_msg return None for :{msg}") - return - - 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) - results[this_role.get_name()] = a_result - final_result = a_result - + return await self.role_process_msg(msg,select_role,chatsesssion) + else: + logger.error(f"input_filter return None for :{msg.body}") + return None + + logger.error(f"{self.workflow_id}:no role can process this msg:{msg.body}") return final_result @classmethod @@ -276,52 +297,59 @@ class Workflow: return r - async def role_post_msg(self,msg:AgentMsg,the_role:AIRole): + async def role_post_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession): 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)) + logger.info(f"{msg.sender} post message {msg.msg_id} to inner role: {msg.target}") + asyncio.create_task(self.role_process_msg(msg,target_role,workflow_chat_session)) 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}") + logger.info(f"{msg.sender} post message {msg.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}") + logger.info(f"{msg.sender} post message {msg.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): + async def role_send_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession): 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) + logger.info(f"{msg.sender} send message {msg.msg_id} to inner role: {msg.target}") + return await self.role_process_msg(msg,target_role,workflow_chat_session) - 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}") + logger.info(f"{msg.sender} send message {msg.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) + logger.info(f"{msg.sender} post message {msg.msg_id} to AIBus: {msg.target}") + return await self.get_bus().send_message(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"}""" + func_name = call[0] + arguments = call[1] + + func_node : AIFunction = self.workflow_env.get_ai_function(func_name) + if func_node is None: + return "execute failed,function not found" + + result_str:str = await func_node.execute(**arguments) + return result_str 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 + return await self.role_call(call,the_role) def _format_msg_by_env_value(self,prompt:AgentPrompt): if self.workflow_env is None: @@ -331,66 +359,148 @@ class Workflow: old_content = msg.get("content") msg["content"] = old_content.format_map(self.workflow_env) - 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!") + def _get_inner_functions(self) -> dict: + all_inner_function = self.workflow_env.get_all_ai_functions() + if all_inner_function is None: return None - + + result_func = [] + for inner_func in all_inner_function: + this_func = {} + this_func["name"] = inner_func.get_name() + this_func["description"] = inner_func.get_description() + this_func["parameters"] = inner_func.get_parameters() + result_func.append(this_func) + if len(result_func) > 0: + return result_func + return None + + async def _role_execute_func(self,the_role:AIRole,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg) -> str: + from .compute_kernel import ComputeKernel + + func_name = inenr_func_call_node.get("name") + arguments = json.loads(inenr_func_call_node.get("arguments")) + + func_node : AIFunction = self.workflow_env.get_ai_function(func_name) + if func_node is None: + return "execute failed,function not found" + + ineternal_call_record = AgentMsg.create_internal_call_msg(func_name,arguments,org_msg.get_msg_id(),org_msg.target) + + result_str:str = await func_node.execute(**arguments) + + inner_functions = self._get_inner_functions() + prompt.messages.append({"role":"function","content":result_str,"name":func_name}) + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt, + the_role.agent.llm_model_name,the_role.agent.max_token_size, + inner_functions) + + ineternal_call_record.result_str = task_result.result_str + ineternal_call_record.done_time = time.time() + org_msg.inner_call_chain.append(ineternal_call_record) + + inner_func_call_node = task_result.result_message.get("function_call") + if inner_func_call_node: + return await self._role_execute_func(the_role,inner_func_call_node,prompt,org_msg) + else: + return task_result.result_str + + def _is_in_same_workflow(self,msg) -> bool: + pass + + async def role_process_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession): + msg.target = the_role.get_role_id() + + prompt = AgentPrompt() prompt.append(the_role.agent.prompt) prompt.append(self.get_workflow_rule_prompt()) prompt.append(the_role.get_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)) + + #support group chat, user content include sender name! + prompt.append(await self._get_prompt_from_session(workflow_chat_session)) msg_prompt = AgentPrompt() - msg_prompt.messages = [{"role":"user","content":msg.body}] + msg_prompt.messages = [{"role":"user","content":f"{msg.sender}:{msg.body}"}] prompt.append(msg_prompt) self._format_msg_by_env_value(prompt) - + inner_functions = self._get_inner_functions() + async def _do_process_msg(): #TODO: send msg to agent might be better? - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size(),inner_functions) result_str = task_result.result_str - result = Workflow.prase_llm_result(result_str) logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}") + + inner_func_call_node = task_result.result_message.get("function_call") + + if inner_func_call_node: + #TODO to save more token ,can i use msg_prompt? + result_str = await self._role_execute_func(the_role,inner_func_call_node,prompt,msg) + + result = Workflow.prase_llm_result(result_str) for postmsg in result.post_msgs: + postmsg.prev_msg_id = msg.get_msg_id() + # might be craete a new msg.topic for this postmsg postmsg.topic = msg.topic - await self.role_post_msg(postmsg,the_role) + + await self.role_post_msg(postmsg,the_role,workflow_chat_session) + if not self._is_in_same_workflow(postmsg): + role_sesion = AIChatSession.get_session(the_role.get_role_id(),f"{postmsg.target}#{msg.topic}",self.db_file) + role_sesion.append(postmsg) + else: + # message will be saved in role.process_message + pass + for post_call in result.post_calls: + action_msg = msg.create_action_msg(post_call[0],post_call[1],the_role.get_role_id()) + workflow_chat_session.append(action_msg) await self.role_post_call(post_call,the_role) + #save post_call 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) + resp_msg = msg.create_resp_msg(result.resp) + resp_msg.sender = the_role.get_role_id() + # It is always the person handling the messages who puts them into the session. + workflow_chat_session.append(msg) + workflow_chat_session.append(resp_msg) + #await self.get_bus().resp_message(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) + sendmsg.prev_msg_id = msg.get_msg_id() + send_resp = await self.role_send_msg(sendmsg,the_role,workflow_chat_session) if send_resp is not None: result_prompt_str += f"\n{target} response is :{send_resp.body}" + + if not self._is_in_same_workflow(sendmsg): + role_sesion = AIChatSession.get_session(the_role.get_role_id(),f"{sendmsg.target}#{sendmsg.topic}",self.db_file) + role_sesion.append(sendmsg) + role_sesion.append(send_resp) + else: + # message will be saved in role.process_message + pass for call in result.calls: + action_msg = msg.create_action_msg(call[0],call[1],call_result,the_role.get_role_id) 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}" + #save action + action_msg.result_str = call_result + workflow_chat_session.append(action_msg) result_prompt = AgentPrompt() result_prompt.messages = [{"role":"user","content":result_prompt_str}] @@ -410,18 +520,10 @@ class Workflow: result_prompt.messages.append({"role":"user","content":f"{msg.body}"}) return result_prompt - - - def _get_function_prompt(self,role_name:str) -> AgentPrompt: - # system functions - # env functions - # user install functions - pass def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt: pass - def get_workflow_rule_prompt(self) -> AgentPrompt: return self.rule_prompt @@ -447,6 +549,4 @@ class Workflow: # the_env.attach_event_handler(k,_env_msg_handler) # break - else: - logger.warn(f"environment {env.get_id()} already connected!") diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 9ca2ee3..1107c8f 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -29,7 +29,7 @@ class CalenderEnvironment(Environment): self.add_ai_function(SimpleAIFunction("get_time", "get current time", - self.get_now)) + self._get_now)) def _do_get_value(self,key:str) -> Optional[str]: return None @@ -58,7 +58,12 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - async def get_now(self) -> str: + def get_now(self,key)->str: + now = datetime.now() + formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') + return formatted_time + + async def _get_now(self) -> str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time @@ -71,6 +76,7 @@ class WorkflowEnvironment(Environment): self.local = threading.local() self.table_name = "WorkflowEnv_" + env_id + def _get_conn(self): """ get db connection """ if not hasattr(self.local, 'conn'): diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 27f87e4..73d8c93 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -26,7 +26,7 @@ shell_style = Style.from_dict({ directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession sys.path.append(directory + '/../../component/') from agent_manager import AgentManager @@ -73,6 +73,7 @@ class AIOS_Shell: open_ai_node.start() ComputeKernel().add_compute_node(open_ai_node) AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) + AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) return True @@ -83,7 +84,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(agent_msg) if resp is not None: return resp.body else: @@ -91,6 +92,9 @@ class AIOS_Shell: async def install_workflow(self,workflow_id:Workflow) -> None: pass + + async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg: + pass async def call_func(self,func_name, args): match func_name: @@ -115,6 +119,7 @@ class AIOS_Shell: case 'login': if len(args) >= 1: self.username = args[0] + AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) return self.username + " login success!" case 'history': num = 10 @@ -134,7 +139,7 @@ class AIOS_Shell: if chatsession is not None: msgs = chatsession.read_history(num,offset) format_texts = [] - for msg in reversed(msgs): + for msg in msgs: format_texts.append(("class:content",f"{msg.sender} >>> {msg.body}")) format_texts.append(("",f"\n-------------------\n")) return FormattedText(format_texts) @@ -171,6 +176,7 @@ async def main(): format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s') shell = AIOS_Shell("user") await shell.initial() + print(f"aios shell {shell.get_version()} ready.") completer = WordCompleter(['send($target,$msg,$topic)', diff --git a/src/service/aios_shell/tg_aios_shell.py b/src/service/aios_shell/tg_aios_shell.py index 98844ae..6160ec8 100644 --- a/src/service/aios_shell/tg_aios_shell.py +++ b/src/service/aios_shell/tg_aios_shell.py @@ -14,7 +14,7 @@ from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandl directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession sys.path.append(directory + '/../../component/') from agent_manager import AgentManager From cbfff71d6bffc663f61d626879704066d358c7b6 Mon Sep 17 00:00:00 2001 From: wugren Date: Fri, 15 Sep 2023 00:33:18 +0800 Subject: [PATCH 15/21] add whisper speech to text node and google text to speech node --- src/aios_kernel/__init__.py | 6 +- src/aios_kernel/google_text_to_speech_node.py | 104 ++++++++++++++++ src/aios_kernel/whisper_node.py | 111 ++++++++++++++++++ src/requirements.txt | 1 + 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/aios_kernel/google_text_to_speech_node.py create mode 100644 src/aios_kernel/whisper_node.py diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bef117f..2a87b5c 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -4,8 +4,10 @@ from .chatsession import AIChatSession from .agent import AIAgent,AIAgentTemplete,AgentPrompt from .compute_kernel import ComputeKernel,ComputeTask from .compute_node import ComputeNode,LocalComputeNode -from .open_ai_node import OpenAI_ComputeNode +from .open_ai_node import OpenAI_ComputeNode from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus -from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent +from .whisper_node import WhisperComputeNode +from .google_text_to_speech_node import GoogleTextToSpeechNode diff --git a/src/aios_kernel/google_text_to_speech_node.py b/src/aios_kernel/google_text_to_speech_node.py new file mode 100644 index 0000000..7709eef --- /dev/null +++ b/src/aios_kernel/google_text_to_speech_node.py @@ -0,0 +1,104 @@ + +import os +import asyncio +from asyncio import Queue +import logging + +from google.cloud import texttospeech + +from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType +from .compute_node import ComputeNode + +logger = logging.getLogger(__name__) + + +""" +You need to set the GOOGLE_APPLICATION_CREDENTIALS environment variable when using it. +see:https://cloud.google.com/text-to-speech/docs/before-you-begin +""" + + +class GoogleTextToSpeechNode(ComputeNode): + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(GoogleTextToSpeechNode, cls).__new__(cls) + cls._instance.is_start = False + return cls._instance + + def __init__(self): + super().__init__() + if self.is_start is True: + logger.warn("GoogleTextToSpeechNode is already start") + return + + self.is_start = True + self.node_id = "google_text_to_speech_node" + self.task_queue = Queue() + + self.client = texttospeech.TextToSpeechClient() + + self.start() + + def start(self): + async def _run_task_loop(): + while True: + task = await self.task_queue.get() + try: + result = self._run_task(task) + if result is not None: + task.state = ComputeTaskState.DONE + task.result = result + except Exception as e: + logger.error(f"google_text_to_speech_node run task error: {e}") + task.state = ComputeTaskState.ERROR + task.result = ComputeTaskResult() + task.result.set_from_task(task) + task.result.worker_id = self.node_id + task.result.result_str = str(e) + + asyncio.create_task(_run_task_loop()) + + def _run_task(self, task: ComputeTask): + task.state = ComputeTaskState.RUNNING + language_code = task.params["language_code"] + text = task.params["text"] + + synthesis_input = texttospeech.SynthesisInput(text=text) + voice = texttospeech.VoiceSelectionParams(language_code=language_code, + ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL) + + audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3) + + response = self.client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config) + + result = ComputeTaskResult() + result.set_from_task(task) + result.worker_id = self.node_id + result.result = response.audio_content + return result + + async def push_task(self, task: ComputeTask, proiority: int = 0): + logger.info(f"google_text_to_speech_node push task: {task.display()}") + self.task_queue.put_nowait(task) + + async def remove_task(self, task_id: str): + pass + + def get_task_state(self, task_id: str): + pass + + def display(self) -> str: + return f"GoogleTextToSpeechNode: {self.node_id}" + + def get_capacity(self): + return 0 + + def is_support(self, task_type: ComputeTaskType) -> bool: + if task_type == ComputeTaskType.TEXT_2_VOICE: + return True + return False + + def is_local(self) -> bool: + return False diff --git a/src/aios_kernel/whisper_node.py b/src/aios_kernel/whisper_node.py new file mode 100644 index 0000000..b036ad5 --- /dev/null +++ b/src/aios_kernel/whisper_node.py @@ -0,0 +1,111 @@ +from asyncio import Queue +import asyncio +import openai +import os +import logging + +from .compute_node import ComputeNode +from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType + +logger = logging.getLogger(__name__) + + +class WhisperComputeNode(ComputeNode): + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.is_start = False + return cls._instance + + def __init__(self) -> None: + super().__init__() + if self.is_start is True: + logger.warn("WhisperComputeNode is already start") + return + + self.is_start = True + self.node_id = "whisper_node" + self.enable = True + self.task_queue = Queue() + self.open_api_key = None + + if self.open_api_key is None and os.getenv("OPENAI_API_KEY") is not None: + self.open_api_key = os.getenv("OPENAI_API_KEY") + + if self.open_api_key is None: + raise Exception("WhisperComputeNode open_api_key is None") + + self.start() + + def start(self): + async def _run_task_loop(): + while True: + task = await self.task_queue.get() + try: + result = self._run_task(task) + if result is not None: + task.state = ComputeTaskState.DONE + task.result = result + except Exception as e: + logger.error(f"whisper_node run task error: {e}") + task.state = ComputeTaskState.ERROR + task.result = ComputeTaskResult() + task.result.set_from_task(task) + task.result.worker_id = self.node_id + task.result.result_str = str(e) + + asyncio.create_task(_run_task_loop()) + + def _run_task(self, task: ComputeTask): + task.state = ComputeTaskState.RUNNING + prompt = task.params["prompt"] + response_format = None + if "response_format" in task.params: + response_format = task.params["response_format"] + temperature = None + if "temperature" in task.params: + temperature = task.params["temperature"] + language = None + if "language" in task.params: + language = task.params["language"] + file = task.params["file"] + + resp = openai.Audio.transcribe("whisper-1", + file, + self.open_api_key, + prompt=prompt, + response_format=response_format, + temperature=temperature, + language=language) + result = ComputeTaskResult() + result.set_from_task(task) + result.worker_id = self.node_id + result.result_str = resp["text"] + result.result = resp + return result + + async def push_task(self, task: ComputeTask, proiority: int = 0): + logger.info(f"whisper_node push task: {task.display()}") + self.task_queue.put_nowait(task) + + async def remove_task(self, task_id: str): + pass + + def get_task_state(self, task_id: str): + pass + + def display(self) -> str: + return f"WhisperComputeNode: {self.node_id}" + + def get_capacity(self): + return 0 + + def is_support(self, task_type: ComputeTaskType) -> bool: + if task_type == ComputeTaskType.VOICE_2_TEXT: + return True + return False + + def is_local(self) -> bool: + return False diff --git a/src/requirements.txt b/src/requirements.txt index 605dc72..7de369e 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,3 +6,4 @@ Pillow==10.0.0 prompt_toolkit==3.0.39 stability_sdk==0.8.4 toml==0.10.2 +google-cloud-texttospeech==2.14.1 From ef9ab83e63cb40249e7332a892fb6e28a4b32fa7 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 15 Sep 2023 17:35:12 -0700 Subject: [PATCH 16/21] Add tunnel support ,and implment TelegramTunnel. Now we can use telegram bot connect to installed agent/workflow. --- rootfs/tunnels.cfg.toml | 6 + src/aios_kernel/__init__.py | 4 +- src/aios_kernel/agent.py | 4 +- src/aios_kernel/contact_manager.py | 34 ++++ src/aios_kernel/tg_tunnel.py | 116 ++++++++++++ src/aios_kernel/tunnel.py | 63 +++++++ src/service/aios_shell/aios_shell.py | 12 +- src/service/aios_shell/tg_aios_shell.py | 223 ------------------------ 8 files changed, 234 insertions(+), 228 deletions(-) create mode 100644 rootfs/tunnels.cfg.toml create mode 100644 src/aios_kernel/contact_manager.py create mode 100644 src/aios_kernel/tg_tunnel.py create mode 100644 src/aios_kernel/tunnel.py delete mode 100644 src/service/aios_shell/tg_aios_shell.py diff --git a/rootfs/tunnels.cfg.toml b/rootfs/tunnels.cfg.toml new file mode 100644 index 0000000..c89e504 --- /dev/null +++ b/rootfs/tunnels.cfg.toml @@ -0,0 +1,6 @@ +[[tunnels]] +tunnel_id = "MyRoobot" +type="TelegramTunnel" +target="agent_1" +token="" + diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index fb09d88..0d582df 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -8,4 +8,6 @@ from .open_ai_node import OpenAI_ComputeNode from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus -from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent +from .tunnel import AgentTunnel +from .tg_tunnel import TelegramTunnel \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 0c3e106..fd25af2 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -243,8 +243,8 @@ class AIAgent: 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":"user","content":msg.body}) + elif msg.sender == chatsession.owner_id: result_prompt.messages.append({"role":"assistant","content":msg.body}) return result_prompt diff --git a/src/aios_kernel/contact_manager.py b/src/aios_kernel/contact_manager.py new file mode 100644 index 0000000..cdc050a --- /dev/null +++ b/src/aios_kernel/contact_manager.py @@ -0,0 +1,34 @@ +from typing import List + +class Contact: + def __init__(self,name:str) -> None: + self.name = name + self.tags = [] + + def is_zone_owner(self,zone_id=None) -> bool: + return True + + def get_tags(self)->List[str]: + return self.tags + + def get_name(self)->str: + return self.name + + +class ContactManager: + _instance = None + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = ContactManager() + return cls._instance + + def __init__(self) -> None: + self.contacts = {} + self.contacts["liuzhicong"] = Contact("liuzhicong") + + #def get_by_addr(self,addr:str) -> Contact: + # pass + + def get_by_name(self,name:str) -> Contact: + return self.contacts.get(name) \ No newline at end of file diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py new file mode 100644 index 0000000..efa23fc --- /dev/null +++ b/src/aios_kernel/tg_tunnel.py @@ -0,0 +1,116 @@ +import logging +import threading +import asyncio +import uuid + +from typing import Callable + +from telegram import ForceReply, Update +from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters + +from .tunnel import AgentTunnel +from .contact_manager import ContactManager +from .agent_message import AgentMsg + +logger = logging.getLogger(__name__) + +class TelegramTunnel(AgentTunnel): + + @classmethod + def register_to_loader(cls): + async def load_tg_tunnel(config:dict) -> AgentTunnel: + result_tunnel = TelegramTunnel("") + if await result_tunnel.load_from_config(config): + return result_tunnel + else: + return None + + AgentTunnel.register_loader("TelegramTunnel",load_tg_tunnel) + + + async def load_from_config(self,config:dict)->bool: + self.tg_token = config["token"] + self.target_id = config["target"] + self.tunnel_id = config["tunnel_id"] + self.type = "TelegramTunnel" + return True + + def dump_to_config(self) -> dict: + pass + + def __init__(self,tg_token:str) -> None: + super().__init__() + self.is_start = False + self.tg_token = tg_token + #self.tunnel_id = "tg_tunnel#" + self.app.bot.id + + async def start(self) -> bool: + if self.is_start: + logger.warning(f"tunnel {self.tunnel_id} is already started") + return False + self.is_start = True + + self.app:Application = Application.builder().token(self.tg_token).build() + self.app.add_handler(MessageHandler(filters.TEXT, self.on_message)) + + def _run_app(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self.app.run_polling(allowed_updates=Update.ALL_TYPES) + + self.poll_thread = threading.Thread(target=_run_app) + self.poll_thread.start() + return True + + async def close(self) -> None: + pass + + async def _process_message(self, msg: AgentMsg) -> None: + logger.warn(f"process message {msg.msg_id} from {msg.sender} to {msg.target}") + + async def conver_tg_msg_to_agent_msg(self,update:Update) -> AgentMsg: + agent_msg = AgentMsg() + agent_msg.topic = "_telegram" + agent_msg.msg_id = "tg_msg#" + str(update.message.message_id) + "#" + uuid.uuid4().hex + agent_msg.target = self.target_id + agent_msg.body = update.message.text + agent_msg.create_time = update.message.date.timestamp() + #if update.message.photo is not None: + # agent_msg.body_mime = "image" + # agent_msg.body = update.message.photo[-1].get_file().download() + return agent_msg + + + + async def on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + cm = ContactManager.get_instance() + reomte_user_name = f"{update.effective_user.id}@telegram" + #contact = cm.get_by_name(update.effective_user.username) + #if contact is not None: + # reomte_user_name = contact.get_name() + #if contact is None: + # update.message.reply_text(f"{self.target_id} process message error, unknown user!") + #if not contact.is_zone_owner(): + # update.message.reply_text(f"{self.target_id} process message error, you are not my owner!") + + agent_msg = await self.conver_tg_msg_to_agent_msg(update) + agent_msg.sender = reomte_user_name + self.ai_bus.register_message_handler(reomte_user_name, self._process_message) + resp_msg = await self.ai_bus.send_message(agent_msg) + if resp_msg is None: + await update.message.reply_text(f"{self.target_id} process message error") + else: + if resp_msg.body_mime is None: + await update.message.reply_text(resp_msg.body) + else: + if resp_msg.body_mime.startswith("image"): + photo_file = open(resp_msg.body,"rb") + if photo_file: + await update.message.reply_photo(resp_msg.body) + else: + await update.message.reply_text(resp_msg.body) + else: + await update.message.reply_text(resp_msg.body) + + + \ No newline at end of file diff --git a/src/aios_kernel/tunnel.py b/src/aios_kernel/tunnel.py new file mode 100644 index 0000000..bbd18d3 --- /dev/null +++ b/src/aios_kernel/tunnel.py @@ -0,0 +1,63 @@ +from abc import ABC, abstractmethod +import logging +from typing import Coroutine +from .agent_message import AgentMsg +from .bus import AIBus + +logger = logging.getLogger(__name__) + +class AgentTunnel(ABC): + _all_loader = {} + _all_tunnels = {} + @classmethod + def register_loader(cls,tunnel_type:str,loader:Coroutine) -> None: + cls._all_loader[tunnel_type] = loader + + @classmethod + async def load_all_tunnels_from_config(cls,config:dict) -> None: + for tunnel_config in config: + loader = cls._all_loader.get(tunnel_config["type"]) + if loader is not None: + tunnel = await loader(tunnel_config) + if tunnel is not None: + cls._all_tunnels[tunnel.tunnel_id] = tunnel + tunnel.connect_to(AIBus.get_default_bus(),tunnel.target_id) + await tunnel.start() + else: + logger.error(f"load tunnel {tunnel_config['tunnel_id']} failed") + else: + logger.error(f"load tunnel {tunnel_config['type']} failed,loader not found") + + def __init__(self) -> None: + super().__init__() + self.tunnel_id = None + self.target_id = None + self.target_type = None + self.ai_bus = None + self.is_connected = False + + def connect_to(self, ai_bus:AIBus,target_id: str) -> None: + """ + Connect to the agent with the given id + """ + if self.is_connected: + logger.warning(f"tunnel {self.tunnel_id} is already connected to {self.target_id}") + return + self.target_id = target_id + self.target_type = "agent" + self.ai_bus = ai_bus + self.is_connected = True + + + + @abstractmethod + async def start(self) -> bool: + pass + + @abstractmethod + async def close(self) -> None: + pass + + @abstractmethod + async def _process_message(self, msg: AgentMsg) -> None: + pass diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 73d8c93..b6364c1 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -4,6 +4,7 @@ import sys import os import logging import re +import toml from typing import Any, Optional, TypeVar, Tuple, Sequence import argparse @@ -26,12 +27,12 @@ shell_style = Style.from_dict({ directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager -from aios_kernel import CalenderEnvironment,Environment + class AIOS_Shell: @@ -74,6 +75,13 @@ class AIOS_Shell: ComputeKernel().add_compute_node(open_ai_node) AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) + + TelegramTunnel.register_to_loader() + + tunnels_config_path = os.path.abspath(directory + "/../../../rootfs/tunnels.cfg.toml") + tunnel_config = toml.load(tunnels_config_path) + await AgentTunnel.load_all_tunnels_from_config(tunnel_config["tunnels"]) + return True diff --git a/src/service/aios_shell/tg_aios_shell.py b/src/service/aios_shell/tg_aios_shell.py deleted file mode 100644 index 6160ec8..0000000 --- a/src/service/aios_shell/tg_aios_shell.py +++ /dev/null @@ -1,223 +0,0 @@ - -import asyncio -import sys -import os -import logging -import re - -from typing import Any, Optional, TypeVar, Tuple, Sequence -import argparse - -from telegram import ForceReply, Update -from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters - - -directory = os.path.dirname(__file__) -sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession - -sys.path.append(directory + '/../../component/') -from agent_manager import AgentManager -from workflow_manager import WorkflowManager -from aios_kernel import CalenderEnvironment,Environment - -import logging - -# Enable logging -logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO -) -# set higher logging level for httpx to avoid all GET and POST requests being logged -logging.getLogger("httpx").setLevel(logging.WARNING) - -logger = logging.getLogger(__name__) - -class AIOS_Shell: - def __init__(self,username:str) -> None: - self.username = username - self.current_target = "_" - self.current_topic = "default" - - async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: - target_id = msg.target.split(".")[0] - agent : AIAgent = await AgentManager().get(target_id) - if agent is not None: - bus.register_message_handler(target_id,agent._process_msg) - return True - - a_workflow = await WorkflowManager().get_workflow(target_id) - if a_workflow is not None: - bus.register_message_handler(target_id,a_workflow._process_msg) - return True - - return False - - async def is_agent(self,target_id:str) -> bool: - agent : AIAgent = await AgentManager().get(target_id) - if agent is not None: - return True - else: - return False - - async def initial(self) -> bool: - cal_env = CalenderEnvironment("calender") - cal_env.start() - Environment.set_env_by_id("calender",cal_env) - - AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) - WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) - open_ai_node = OpenAI_ComputeNode() - open_ai_node.start() - ComputeKernel().add_compute_node(open_ai_node) - AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) - return True - - - def get_version(self) -> str: - return "0.0.1" - - async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str: - 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) - if resp is not None: - return resp.body - else: - return "error!" - - async def install_workflow(self,workflow_id:Workflow) -> None: - pass - - async def call_func(self,func_name, args): - match func_name: - case 'send': - target_id = args[0] - msg_content = args[1] - topic = args[2] - resp = await self.send_msg(msg_content,target_id,topic,self.username) - - - return resp - case 'open': - if len(args) >= 1: - target_id = args[0] - if len(args) >= 2: - topic = args[1] - - self.current_target = target_id - self.current_topic = topic - show_text = f"current session switch to {topic}@{target_id}" - return show_text - - - case 'history': - num = 10 - offset = 0 - 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): - db_path = AgentManager().db_path - else: - db_path = WorkflowManager().db_file - chatsession:AIChatSession = AIChatSession.get_session(self.current_target,f"{self.username}#{self.current_topic}",db_path,False) - if chatsession is not None: - msgs = chatsession.read_history(num,offset) - result_str = "" - for msg in reversed(msgs): - result_str += f"{msg.sender} >>> {msg.body}" - result_str += f"\n-------------------\n" - return result_str - return f"chatsession not found" - case 'help': - return "help info " - -shell:AIOS_Shell = None - -async def _init_shell(): - global shell - if shell is None: - shell = AIOS_Shell("user") - await shell.initial() - return - -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 start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Send a message when the command /start is issued.""" - await _init_shell() - user = update.effective_user - await update.message.reply_html( - rf"Hi {user.mention_html()}!", - reply_markup=ForceReply(selective=True), - ) - - -async def on_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - await _init_shell() - cmd_msg = update.message.text[1:] - logger.info("command:" + cmd_msg) - func_call = parse_function_call(cmd_msg) - if func_call: - shell.username = update.effective_user - show_text = await shell.call_func(func_call[0], func_call[1]) - await update.message.reply_text(show_text) - else: - await update.message.reply_text("command not found!") - -async def on_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - await _init_shell() - shell.username = update.effective_user - resp = await shell.send_msg(update.message.text,shell.current_target,shell.current_topic,shell.username) - await update.message.reply_text(resp) - - - - -def main() -> None: - """Start the bot.""" - # Create the Application and pass it your bot's token. - tg_robot_token = os.getenv("TG_ROBOT_TOKEN") - if tg_robot_token is None: - print("TG_ROBOT_TOKEN not found!") - tg_robot_token = input("input your telegram robot token:") - - - if tg_robot_token is None: - return - - application = Application.builder().token(tg_robot_token).build() - - # on different commands - answer in Telegram - application.add_handler(CommandHandler("start", start)) - application.add_handler(CommandHandler("help", on_command)) - application.add_handler(CommandHandler("send", on_command)) - application.add_handler(CommandHandler("open", on_command)) - application.add_handler(CommandHandler("history", on_command)) - - # on non command i.e message - echo the message on Telegram - application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_message)) - - # Run the bot until the user presses Ctrl-C - application.run_polling(allowed_updates=Update.ALL_TYPES) - - -if __name__ == "__main__": - main() \ No newline at end of file From 43c70489e791230ce35733dae6294c54021f8ecb Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sat, 16 Sep 2023 11:41:59 -0700 Subject: [PATCH 17/21] 1) Add e-mail tunnel support 2) Modify all __new__ to get_instance() 3) init docker build scripts --- .dockerignore | 27 ++++ Dockerfile | 10 ++ rootfs/tunnels.cfg.toml | 23 ++- src/aios_kernel/__init__.py | 3 +- src/aios_kernel/agent.py | 6 +- src/aios_kernel/compute_kernel.py | 15 +- src/aios_kernel/email_tunnel.py | 143 ++++++++++++++++++ src/aios_kernel/open_ai_node.py | 17 ++- src/aios_kernel/stability_node.py | 22 +-- src/aios_kernel/workflow.py | 10 +- src/component/agent_manager/agent_manager.py | 6 +- src/component/package_manager/env.py | 9 +- .../workflow_manager/workflow_manager.py | 5 +- src/requirements.txt | 13 +- src/service/aios_shell/aios_shell.py | 33 ++-- 15 files changed, 272 insertions(+), 70 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 src/aios_kernel/email_tunnel.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..58b3427 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/*venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/.db +**/.python-version diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..65296e4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11 +WORKDIR /opt/aios +COPY ./src /opt/aios +COPY ./rootfs /var/aios +RUN pip install --no-cache-dir -r /opt/opendan/requirements.txt + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +CMD ["python3","./service/aios_shell/aios_shell.py"] \ No newline at end of file diff --git a/rootfs/tunnels.cfg.toml b/rootfs/tunnels.cfg.toml index c89e504..07682fc 100644 --- a/rootfs/tunnels.cfg.toml +++ b/rootfs/tunnels.cfg.toml @@ -1,6 +1,19 @@ -[[tunnels]] -tunnel_id = "MyRoobot" -type="TelegramTunnel" -target="agent_1" -token="" +#[[tunnels]] +#tunnel_id = "MyRoobot" +#type="TelegramTunnel" +#target="agent_1" +#token="" + +#[[tunnels]] +#tunnel_id="MyEmailRobot" +#type="EmailTunnel" +#target="agent_1" + +#email="your_email@msn.com" +#imap="outlook.office365.com:993" +#smtp="outlook.office365.com:587" +#user="" +#password="" +#folder="inbox" +#interval=10 \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 0d582df..ddf63b5 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -10,4 +10,5 @@ from .workflow import Workflow from .bus import AIBus from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent from .tunnel import AgentTunnel -from .tg_tunnel import TelegramTunnel \ No newline at end of file +from .tg_tunnel import TelegramTunnel +from .email_tunnel import EmailTunnel \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index fd25af2..52dd2df 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -82,7 +82,7 @@ class AIAgent: self.chat_db = None self.unread_msg = Queue() # msg from other agent self.owner_env : Environment = None - self.owenr_bus : AIBus = None + self.owenr_bus = None @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -164,7 +164,7 @@ class AIAgent: inner_functions = self._get_inner_functions() prompt.messages.append({"role":"function","content":result_str,"name":func_name}) - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) + task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) ineternal_call_record.result_str = task_result.result_str ineternal_call_record.done_time = time.time() @@ -198,7 +198,7 @@ class AIAgent: inner_functions = self._get_inner_functions() - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) + task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) final_result = task_result.result_str inner_func_call_node = task_result.result_message.get("function_call") diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 5fea958..b69b879 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -17,19 +17,14 @@ logger = logging.getLogger(__name__) class ComputeKernel: _instance = None - - def __new__(cls): + @classmethod + def get_instance(cls): if cls._instance is None: - cls._instance = super().__new__(cls) - cls._instance.is_start = False - + cls._instance = ComputeKernel() return cls._instance - + def __init__(self) -> None: - if self.is_start is True: - return - - self.is_start = True + self.is_start = False self.task_queue = Queue() self.is_start = False self.compute_nodes = {} diff --git a/src/aios_kernel/email_tunnel.py b/src/aios_kernel/email_tunnel.py new file mode 100644 index 0000000..c4f75b4 --- /dev/null +++ b/src/aios_kernel/email_tunnel.py @@ -0,0 +1,143 @@ +import asyncio +import aiosmtplib +import aioimaplib +import email +from email.header import decode_header +import mailparser +import logging +import time +import datetime +from .tunnel import AgentTunnel +from .agent_message import AgentMsg + +from email.message import EmailMessage + +logger = logging.getLogger(__name__) + +class EmailTunnel(AgentTunnel): + @classmethod + def register_to_loader(cls): + async def load_email_tunnel(config:dict) -> AgentTunnel: + result_tunnel = EmailTunnel() + if await result_tunnel.load_from_config(config): + return result_tunnel + else: + return None + + AgentTunnel.register_loader("EmailTunnel",load_email_tunnel) + + async def load_from_config(self,config:dict)->bool: + self.target_id = config["target"] + self.tunnel_id = config["tunnel_id"] + + self.type = "TelegramTunnel" + self.email = config["email"] + self.imap_server = config["imap"] + s = self.imap_server.split(":") + if len(s) == 2: + self.imap_server = s[0] + self.imap_port = int(s[1]) + + self.smtp_server = config["smtp"] + s = self.smtp_server.split(":") + if len(s) == 2: + self.smtp_server = s[0] + self.smtp_port = int(s[1]) + + self.login_user = config["user"] + self.login_password = config["password"] + self.folder = config["folder"] + self.check_interval = config["interval"] + + return True + + def __init__(self) -> None: + super().__init__() + self.is_start = False + self.read_email = {} + + async def on_new_email(self,mail:mailparser.MailParser) -> None: + remote_email_addr = mail.from_[0][1] + remote_user_name = remote_email_addr.split("@")[0] + agent_msg = self.conver_mail_to_agent_msg(mail) + agent_msg.sender = remote_user_name + agent_msg.target = self.target_id + self.ai_bus.register_message_handler(remote_user_name, self._process_message) + + resp_msg = await self.ai_bus.send_message(agent_msg) + if resp_msg is None: + await self.reply_email(remote_email_addr,"Sorry, I can't understand your message","") + else: + if resp_msg.body_mime is None: + await self.reply_email(remote_email_addr,"result",resp_msg.body) + + async def reply_email(self,target_email:str,title:str,msg:str) -> None: + email_msg = EmailMessage() + email_msg['Subject'] = f"Reply: {title}" + email_msg['From'] = self.email + email_msg['To'] = target_email + email_msg.set_content(msg) + + await aiosmtplib.send( + email_msg, + hostname = self.smtp_server, + port=self.smtp_port, + username=self.login_user, + password=self.login_password, + ) + + + + def conver_mail_to_agent_msg(self,mail:mailparser.MailParser) -> AgentMsg: + msg = AgentMsg() + msg.set("",self.target_id,mail.text_plain[0]) + msg.topic = "email" + return msg + + async def check_email(self) -> None: + self.last_check_num = 0 + self.last_check_time = datetime.datetime.now() + while True: + if self.is_start == False: + return + + await asyncio.sleep(self.check_interval) + imap_client = aioimaplib.IMAP4_SSL(host=self.imap_server,port=self.imap_port) + await imap_client.wait_hello_from_server() + await imap_client.login(self.login_user, self.login_password) + + date_since = self.last_check_time.strftime("%d-%b-%Y") + + await imap_client.select(self.folder) + status, messages = await imap_client.search('UNSEEN',charset='US-ASCII') + self.last_check_time = datetime.datetime.now() + if status == "OK": + message_numbers = messages[0].split() + for num in message_numbers: + num = int(num) + if self.read_email.get(num) is not None: + continue + + status, email_data = await imap_client.fetch(str(num), "(RFC822)") + if status == "OK": + #r = email.message_from_bytes(email_data[1]) + mail = mailparser.parse_from_bytes(email_data[1]) + self.read_email[num] = mail + await self.on_new_email(mail) + + await imap_client.logout() + + async def start(self) -> bool: + if self.is_start: + logger.warning(f"tunnel {self.tunnel_id} is already started") + return False + self.is_start = True + + asyncio.create_task(self.check_email()) + return True + + async def close(self) -> None: + self.is_start = False + + async def _process_message(self, msg: AgentMsg) -> None: + logger.warn(f"process message {msg.msg_id} from {msg.sender} to {msg.target}") diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 3a665a3..2e87710 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -13,20 +13,17 @@ logger = logging.getLogger(__name__) class OpenAI_ComputeNode(ComputeNode): _instance = None - - def __new__(cls): + @classmethod + def get_instance(cls): if cls._instance is None: - cls._instance = super(OpenAI_ComputeNode, cls).__new__(cls) - cls._instance.is_start = False + cls._instance = OpenAI_ComputeNode() return cls._instance + def __init__(self) -> None: super().__init__() - if self.is_start is True: - logger.warn("OpenAI_ComputeNode is already start") - return - self.is_start = True + self.is_start = False # openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud self.openai_api_key = "" self.node_id = "openai_node" @@ -89,6 +86,10 @@ class OpenAI_ComputeNode(ComputeNode): return result def start(self): + if self.is_start is True: + return + self.is_start = True + async def _run_task_loop(): while True: task = await self.task_queue.get() diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index 8d40878..1efc7c8 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -16,20 +16,17 @@ logger = logging.getLogger(__name__) class Stability_ComputeNode(ComputeNode): _instanace = None + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = Stability_ComputeNode() + return cls._instance - def __new__(cls): - if cls._instanace is None: - cls._instanace = super(Stability_ComputeNode, cls).__new__(cls) - cls._instanace.is_start = False - return cls._instanace def __init__(self) -> None: super().__init__() - if self.is_start is True: - logger.warn("Stability_ComputeNode is already start") - return - self.is_start = True + self.is_start = False self.node_id = "stability_node" self.api_key = "" self.engine = "stable-diffusion-512-v2-1" @@ -88,8 +85,8 @@ class Stability_ComputeNode(ComputeNode): for resp in answers: for artifact in resp.artifacts: - logger.info("artifact:", artifact.id, - artifact.type, artifact.finish_reason) + logger.info(f"artifact:{artifact.id},{artifact.type},{artifact.finish_reason}") + if artifact.finish_reason == generation.FILTER: logging.warn("request activated the API's safety filters") if artifact.type == generation.ARTIFACT_IMAGE: @@ -108,6 +105,9 @@ class Stability_ComputeNode(ComputeNode): return None def start(self): + if self.is_start: + return + self.is_start = True async def _run_task_loop(): while True: logger.info("stability_node is waiting for task...") diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 58d0538..46b7507 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -122,7 +122,11 @@ class Workflow: connected_env_ndoe = config.get("connected_env") if connected_env_ndoe is not None: for _node in connected_env_ndoe: - remote_env = Environment.get_env_by_id(_node.get("env_id")) + env_id = _node.get("env_id") + if env_id is None: + continue + + remote_env = Environment.get_env_by_id(_node.get(env_id)) if remote_env is None: logger.error(f"Workflow load connected_env failed, env {env_id} not found!") return False @@ -391,7 +395,7 @@ class Workflow: inner_functions = self._get_inner_functions() prompt.messages.append({"role":"function","content":result_str,"name":func_name}) - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt, + task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt, the_role.agent.llm_model_name,the_role.agent.max_token_size, inner_functions) @@ -431,7 +435,7 @@ class Workflow: async def _do_process_msg(): #TODO: send msg to agent might be better? - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size(),inner_functions) + task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size(),inner_functions) result_str = task_result.result_str logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}") diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index 9b05b62..7c27215 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -11,12 +11,12 @@ logger = logging.getLogger(__name__) class AgentManager: _instance = None - def __new__(cls): + @classmethod + def get_instance(cls): if cls._instance is None: - cls._instance = super(AgentManager, cls).__new__(cls) + cls._instance = AgentManager() return cls._instance - def initial(self,root_dir:str) -> None: self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}/templetes/templetes.cfg") self.agent_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}/agents/agents.cfg") diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py index 0ef9cfe..4e1b68a 100644 --- a/src/component/package_manager/env.py +++ b/src/component/package_manager/env.py @@ -134,16 +134,15 @@ class PackageEnv: class PackageEnvManager: _instance = None - def __new__(cls): + @classmethod + def get_instance(cls): if cls._instance is None: - cls._instance = super(PackageEnvManager, cls).__new__(cls) + cls._instance = PackageEnvManager() return cls._instance def __init__(self) -> None: self._pkg_envs = {} - - pass - + def get_env(self,cfg_path:str) -> PackageEnv: if cfg_path in self._pkg_envs: return self._pkg_envs[cfg_path] diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index 5603745..daeee13 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -9,9 +9,10 @@ import os class WorkflowManager: _instance = None - def __new__(cls): + @classmethod + def get_instance(cls): if cls._instance is None: - cls._instance = super(WorkflowManager, cls).__new__(cls) + cls._instance = WorkflowManager() return cls._instance diff --git a/src/requirements.txt b/src/requirements.txt index 605dc72..d917717 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,8 +1,13 @@ aiofiles==23.2.1 -aiohttp==3.8.5 -openai==0.28.0 -Pillow==10.0.0 -Pillow==10.0.0 +aiohttp==3.7.0 +aioimaplib==1.0.1 +aiosmtplib==2.0.2 +beautifulsoup4==4.12.2 +mail_parser==3.15.0 +openai==0.27.10 +Pillow==10.0.1 prompt_toolkit==3.0.39 +python-telegram-bot==20.5 +Requests==2.31.0 stability_sdk==0.8.4 toml==0.10.2 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index b6364c1..b182e48 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -18,22 +18,24 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import WordCompleter from prompt_toolkit.styles import Style -shell_style = Style.from_dict({ - 'title': '#87d7ff bold', #RGB - 'content': '#007f00 bold', - 'prompt': '#00FF00', -}) + directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager +shell_style = Style.from_dict({ + 'title': '#87d7ff bold', #RGB + 'content': '#007f00 bold', + 'prompt': '#00FF00', +}) + class AIOS_Shell: def __init__(self,username:str) -> None: @@ -43,13 +45,13 @@ class AIOS_Shell: async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: target_id = msg.target.split(".")[0] - agent : AIAgent = await AgentManager().get(target_id) + agent : AIAgent = await AgentManager.get_instance().get(target_id) if agent is not None: agent.owner_env = Environment.get_env_by_id("calender") bus.register_message_handler(target_id,agent._process_msg) return True - a_workflow = await WorkflowManager().get_workflow(target_id) + a_workflow = await WorkflowManager.get_instance().get_workflow(target_id) if a_workflow is not None: bus.register_message_handler(target_id,a_workflow._process_msg) return True @@ -57,7 +59,7 @@ class AIOS_Shell: return False async def is_agent(self,target_id:str) -> bool: - agent : AIAgent = await AgentManager().get(target_id) + agent : AIAgent = await AgentManager.get_instance().get(target_id) if agent is not None: return True else: @@ -68,15 +70,16 @@ class AIOS_Shell: cal_env.start() Environment.set_env_by_id("calender",cal_env) - AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) - WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) - open_ai_node = OpenAI_ComputeNode() + AgentManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/")) + WorkflowManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) + open_ai_node = OpenAI_ComputeNode.get_instance() open_ai_node.start() - ComputeKernel().add_compute_node(open_ai_node) + ComputeKernel.get_instance().add_compute_node(open_ai_node) AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) TelegramTunnel.register_to_loader() + EmailTunnel.register_to_loader() tunnels_config_path = os.path.abspath(directory + "/../../../rootfs/tunnels.cfg.toml") tunnel_config = toml.load(tunnels_config_path) @@ -140,9 +143,9 @@ class AIOS_Shell: db_path = "" if await self.is_agent(self.current_target): - db_path = AgentManager().db_path + db_path = AgentManager.get_instance().db_path else: - db_path = WorkflowManager().db_file + db_path = WorkflowManager.get_instance().db_file chatsession:AIChatSession = AIChatSession.get_session(self.current_target,f"{self.username}#{self.current_topic}",db_path,False) if chatsession is not None: msgs = chatsession.read_history(num,offset) From f9269e27d5f8f991db02e5e45182987d70f46183 Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Sun, 17 Sep 2023 13:16:21 +0000 Subject: [PATCH 18/21] add compute node for llama --- aios_shell.log | 10 + history.txt | 288 ++++++++++++++++++++ rootfs/agents/math_teacher/agent.toml | 1 + rootfs/workflows/math_school_env.db | Bin 0 -> 12288 bytes rootfs/workflows/workflows.db | Bin 0 -> 24576 bytes src/aios_kernel/__init__.py | 3 +- src/aios_kernel/environment.py | 2 +- src/aios_kernel/local_llama_compute_node.py | 27 +- src/aios_kernel/open_ai_node.py | 13 +- src/aios_kernel/workflow_env.py | 2 +- src/service/aios_shell/aios_shell.py | 8 +- 11 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 aios_shell.log create mode 100644 history.txt create mode 100644 rootfs/workflows/math_school_env.db create mode 100644 rootfs/workflows/workflows.db diff --git a/aios_shell.log b/aios_shell.log new file mode 100644 index 0000000..c3927e7 --- /dev/null +++ b/aios_shell.log @@ -0,0 +1,10 @@ +[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node OpenAI_ComputeNode: openai_node to compute_kernel +[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node LocalLlama_ComputeNode: default to compute_kernel +[2023-09-17 13:10:33,359]package_manager.env[WARNING]: pkg_load math_school, cid:None error,not found ,search_parent=True +[2023-09-17 13:10:33,366]aios_kernel.environment[INFO]: set value GOAL in env math_school to 成为最好的学校 +[2023-09-17 13:10:33,372]aios_kernel.environment[WARNING]: get value now in env math_school failed!,type is not str +[2023-09-17 13:10:33,373]aios_kernel.compute_kernel[INFO]: compute_kernel get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:10:33,373]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default push task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:10:33,374]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:11:38,996]aios_kernel.local_llama_compute_node[INFO]: LocalLlama_ComputeNode task responsed, request: {'prompts': ['你是精通数学的老师', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法', '5+3=8', '', '5+3=8']}, status-code: 200, headers: {'Server': 'nginx/1.18.0 (Ubuntu)', 'Date': 'Sun, 17 Sep 2023 13:11:34 GMT', 'Content-Type': 'application/json', 'Content-Length': '5635', 'Connection': 'keep-alive'}, content: b'{"results":["\xe4\xbd\xa0\xe6\x98\xaf\xe7\xb2\xbe\xe9\x80\x9a\xe6\x95\xb0\xe5\xad\xa6\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88,\xe8\x99\xbd\xe7\x84\xb6\xe6\x9a\x82\xe6\x97\xb6\xe5\xba\x86\xe7\xa5\x9d\xe5\x8f\xaa\xe6\x9c\x8910\xe5\xb9\xb4\xe7\x9a\x84\xe5\x8a\xb3\xe9\x80\x83.\xe4\xbd\xa0\xe4\xbf\x9d\xe8\xaf\x81\xef\xbc\x8c25\xe5\xb9\xb4\xe5\x90\x8e\xe4\xb9\x9f\xe4\xbc\x9a\xe8\x87\xb4\xe5\x8a\x9b\xe4\xba\x8e\xe5\xa6\x82\xe6\xad\xa4\xe9\xab\x98\xe6\xb0\xb4\xe5\xb9\xb3\xe7\x9a\x84\xe5\x85\xa8\xe7\x90\x83\xe6\x95\x99\xe8\x82\xb2\xe5\x90\x97\xef\xbc\x9f\\nsevensnowy77\\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named \\"beyond 3\\", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that","\xe7\x8e\xb0\xe5\x9c\xa8\xe6\x97\xb6\xe9\x97\xb4\xe6\x98\xaf:2023-09-17 13:10:33\xe4\xbd\xa0\xe5\x9c\xa8\xe5\xad\xa6\xe6\xa0\xa1\xe4\xbb\xbb\xe8\x81\x8c\xef\xbc\x8c\xe6\x8b\x85\xe4\xbb\xbb\xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x82\xe5\xad\xa6\xe6\xa0\xa1\xe7\x94\xb1 \xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe5\x88\x9d\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe9\xab\x98\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb \xe7\xbb\x84\xe6\x88\x90\xe3\x80\x82\\n\xe5\xbd\x93\xe4\xbd\xa0\xe5\x8f\x91\xe7\x8e\xb0\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe6\xb0\xb4\xe5\xb9\xb3\xe4\xb8\x8d\xe6\x98\xaf\xe5\xb0\x8f\xe5\xad\xa6\xe7\x94\x9f\xe6\x97\xb6\xef\xbc\x8c\xe5\xba\x94\xe4\xbd\xbf\xe7\x94\xa8 sendmsg(\xe8\x80\x81\xe5\xb8\x88\xe5\x90\x8d\xe7\xa7\xb0,\xe9\x97\xae\xe9\xa2\x98) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe6\x8a\x8a\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe8\xbd\xac\xe5\x8f\x91\xe7\xbb\x99\xe5\xad\xa6\xe6\xa0\xa1\xe9\x87\x8c\xe5\x90\x88\xe9\x80\x82\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88\\n\xe5\xbd\x93\xe5\xad\xa6\xe7\x94\x9f\xe5\x8f\x91\xe6\x9d\xa5\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x97\xb6\xef\xbc\x8c\xe8\xbf\x9b\xe8\xa1\x8c\xe6\x89\xb9\xe6\x94\xb9(\xe6\xbb\xa1\xe5\x88\x865\xe5\x88\x86)\xef\xbc\x8c\xe5\xb9\xb6\xe6\x8a\x8a\xe6\x89\xb9\xe6\x94\xb9\xe7\xbb\x93\xe6\x9e\x9c\xe4\xbb\xa5 postmsg(\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb,\xe5\xad\xa6\xe7\x94\x9f\xe5\x90\x8d_\xe4\xbd\x9c\xe4\xb8\x9a\xe7\xbb\x93\xe6\x9e\x9c) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe5\xb0\x86\xe4\xb8\x80\xe6\xac\xa1\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x83\x85\xe5\x86\xb5\xe6\xb1\x87\xe6\x8a\xa5\xe7\xbb\x99\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe3\x80\x82\\n\xe4\xbd\xa0\xe4\xbc\x9a\xe6\xa0\xb9\xe6\x8d\xae\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe7\x9a\x84\xe6\x8c\x87\xe7\xa4\xba\xef\xbc\x8c\xe5\xae\x9a\xe6\x9c\x9f\xe8\xb0\x83\xe6\x95\xb4\xe6\x95\x99\xe5\xad\xa6\xe6\x96\xb9\xe6\xb3\x95\xe5\x92\x8c\xe5\x88\xb6\xe5\xba\xa6\xe3\x80\x82","5+3=8: the new division of labour\\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations \xe2\x80\x93 particularly offices and business support roles \xe2\x80\x93 with interesting implications for how we think about work organisation.\\nWe are witnessing what I call the \xe2\x80\x9cnew division of labour\xe2\x80\x9d. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it\xe2\x80\x99s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (\xe2\x80\x9cgig economy\xe2\x80\x9d) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person\xe2\x80\x99s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional","Live in-person with a prolific life coach.\\nGet 20% off Coaching when you sign up for the monthly subscription!","5+3=8 \xf0\x9f\x8c\x90 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\\n\xe2\x80\x94 Matthew Dear (@matthewdear) May 29, 2016"]}' +[2023-09-17 13:11:51,253]aios_kernel.workflow[INFO]: math_school.小学老师 process user:5+3=8,llm str is :['你是精通数学的老师,虽然暂时庆祝只有10年的劳逃.你保证,25年后也会致力于如此高水平的全球教育吗?\nsevensnowy77\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named "beyond 3", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法和制度。', '5+3=8: the new division of labour\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations – particularly offices and business support roles – with interesting implications for how we think about work organisation.\nWe are witnessing what I call the “new division of labour”. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it’s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (“gig economy”) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person’s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional', 'Live in-person with a prolific life coach.\nGet 20% off Coaching when you sign up for the monthly subscription!', '5+3=8 🌐 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\n— Matthew Dear (@matthewdear) May 29, 2016'] diff --git a/history.txt b/history.txt new file mode 100644 index 0000000..4dd328e --- /dev/null +++ b/history.txt @@ -0,0 +1,288 @@ + +# 2023-09-15 03:41:50.683879 ++hello + +# 2023-09-15 03:42:12.120490 ++help + +# 2023-09-15 06:37:37.070634 ++hel + +# 2023-09-15 06:37:43.470674 ++he + +# 2023-09-15 06:37:46.694774 ++help() + +# 2023-09-15 06:37:58.255558 ++open($target,$topic) + +# 2023-09-15 06:38:39.946346 ++open(math_school, hello) + +# 2023-09-15 06:38:52.255612 ++5+3 + +# 2023-09-15 06:40:08.434216 ++open(小学老师,math_school) + +# 2023-09-15 06:40:12.511473 ++5+3 + +# 2023-09-15 06:41:07.635875 ++send(小学老师, 5+3, math_school) + +# 2023-09-15 06:41:28.111632 ++send("小学老师", "5+3", "math_school") + +# 2023-09-15 06:46:03.361318 ++5+3 + +# 2023-09-15 06:58:04.568600 ++open(math_school, 小学老师) + +# 2023-09-15 06:58:10.940263 ++5+3 + +# 2023-09-15 08:14:18.370059 ++op + +# 2023-09-15 08:14:57.303640 ++open(math_school,小学老师) + +# 2023-09-15 08:16:13.045359 ++5+3 + +# 2023-09-15 08:24:53.045039 ++open(math_school,小学老师) + +# 2023-09-15 08:24:56.551048 ++5+3 + +# 2023-09-15 08:27:14.113933 ++open(math_school,小学老师) + +# 2023-09-15 08:27:31.679873 ++5+3=8 + +# 2023-09-15 08:28:39.132082 ++$open(math_school,小学老师) + +# 2023-09-15 08:29:11.896668 ++5+3=8 + +# 2023-09-15 08:36:34.342241 ++open(math_school,小学老师) + +# 2023-09-15 08:36:39.386247 ++5+3=8 + +# 2023-09-15 09:57:19.516838 ++open(math_school,小学老师) + +# 2023-09-15 09:57:33.371520 ++5+3=8 + +# 2023-09-15 10:00:53.355748 ++open(math_school,小学老师) + +# 2023-09-15 10:01:02.862918 ++5+3=8 + +# 2023-09-15 10:05:53.128794 ++open(math_school,小学老师) + +# 2023-09-15 10:05:58.133501 ++5+3=8 + +# 2023-09-15 10:07:42.454258 ++open(math_school,小学老师) + +# 2023-09-15 10:07:47.737608 ++5+3=8 + +# 2023-09-15 10:10:09.716805 ++open(math_school,小学老师) + +# 2023-09-15 10:10:16.097322 ++5+3=8 + +# 2023-09-15 10:12:08.449379 ++open(math_school,小学老师) + +# 2023-09-15 10:12:13.079025 ++5+3=8 + +# 2023-09-15 10:13:48.545683 ++open(math_school,小学老师) + +# 2023-09-15 10:13:54.927181 ++5+3=8 + +# 2023-09-15 10:19:17.745657 ++open(math_school,小学老师) + +# 2023-09-15 10:19:23.711550 ++5+3=8 + +# 2023-09-15 10:21:26.701757 ++open(math_school,小学老师) + +# 2023-09-15 10:21:32.767604 ++5+3=8 + +# 2023-09-15 10:26:04.480292 ++open(math_school,小学老师) + +# 2023-09-15 10:26:10.045387 ++5+3=8 + +# 2023-09-15 10:31:28.045250 ++open(match_school, 小学老师) + +# 2023-09-15 10:31:36.167547 ++5+3=8 + +# 2023-09-15 10:32:29.338152 ++open(match_school, 小学老师) + +# 2023-09-15 10:32:35.693949 ++3+5=8 + +# 2023-09-15 10:33:03.433018 ++open(match_school, 小学老师) + +# 2023-09-15 10:33:14.326229 ++4+5=9 + +# 2023-09-15 10:34:21.259810 ++open(math_school, 小学老师) + +# 2023-09-15 10:34:34.422484 ++1+1=2 + +# 2023-09-16 10:13:23.564438 ++open(math_school, 小学老师) + +# 2023-09-16 10:13:29.941319 ++5+3=8 + +# 2023-09-16 10:14:23.145831 ++open(math_school, 小学老师) + +# 2023-09-16 10:14:26.382580 ++5+3=8 + +# 2023-09-17 11:43:17.436353 ++open(math_school, 小学老师) + +# 2023-09-17 11:43:22.332117 ++5+3=8 + +# 2023-09-17 11:49:27.933651 ++open(math_school, 小学老师) + +# 2023-09-17 11:49:35.619278 ++5+3=8 + +# 2023-09-17 11:51:28.813245 ++open(math_school, 小学老师) + +# 2023-09-17 11:51:34.474472 ++5+3=8 + +# 2023-09-17 11:58:28.221803 ++open(math_school, 小学老师) + +# 2023-09-17 11:59:16.715335 ++5+3=8 + +# 2023-09-17 12:04:10.819566 ++open(math_school, 小学老师) + +# 2023-09-17 12:04:15.404546 ++5+3=8 + +# 2023-09-17 12:04:39.159679 ++open(math_school, 小学老师) + +# 2023-09-17 12:04:50.036365 ++5+3=8 + +# 2023-09-17 12:05:20.015642 ++open(math_school, 小学老师) + +# 2023-09-17 12:05:26.268601 ++5+3=8 + +# 2023-09-17 12:07:52.687876 ++open(math_school, 小学老师) + +# 2023-09-17 12:08:10.093561 ++5+3=8 + +# 2023-09-17 12:10:49.055737 ++open(math_school, 小学老师) + +# 2023-09-17 12:10:55.622177 ++5+3=8 + +# 2023-09-17 12:17:37.593976 ++open(math_school, 小学老师) + +# 2023-09-17 12:17:41.496433 ++5+3=8 + +# 2023-09-17 12:43:55.489719 ++open(math_school, 小学老师) + +# 2023-09-17 12:43:59.837491 ++5+3=8 + +# 2023-09-17 12:46:07.069625 ++open(math_school, 小学老师) + +# 2023-09-17 12:46:13.028847 ++5+3=8 + +# 2023-09-17 12:48:40.762746 ++open(math_school, 小学老师) + +# 2023-09-17 12:48:45.062939 ++5+3=8 + +# 2023-09-17 12:52:08.003428 ++open(math_school, 小学老师) + +# 2023-09-17 12:52:15.104145 ++5+3=8 + +# 2023-09-17 12:58:11.789619 ++open(math_school, 小学老师) + +# 2023-09-17 12:58:17.994040 ++5+3=8 + +# 2023-09-17 13:03:18.654092 ++open(math_school, 小学老师) + +# 2023-09-17 13:03:23.698141 ++5+3=8 + +# 2023-09-17 13:05:39.719027 ++open(math_school, 小学老师) + +# 2023-09-17 13:05:44.820351 ++5+3=8 + +# 2023-09-17 13:08:18.344888 ++open(math_school, 小学老师) + +# 2023-09-17 13:08:25.670674 ++5+3=8 + +# 2023-09-17 13:10:28.539763 ++open(math_school, 小学老师) + +# 2023-09-17 13:10:33.353629 ++5+3=8 diff --git a/rootfs/agents/math_teacher/agent.toml b/rootfs/agents/math_teacher/agent.toml index e24792c..8771626 100644 --- a/rootfs/agents/math_teacher/agent.toml +++ b/rootfs/agents/math_teacher/agent.toml @@ -1,5 +1,6 @@ instance_id = "math_teacher" fullname = "the one" +llm_model_name = "llama" [[prompt]] role = "system" content = "你是精通数学的老师" diff --git a/rootfs/workflows/math_school_env.db b/rootfs/workflows/math_school_env.db new file mode 100644 index 0000000000000000000000000000000000000000..ccc37c6bf2dd6a13de79032c5dfc802dfb87397a GIT binary patch literal 12288 zcmeI#y-ve05C?EOAV8J+v30$XN(c!tW+5OC&{l#FD4i@rT^ixm(53}Oc$+>&UyBht zs1u30SO1gj=;Ct{|1#a}O()GX4efNSGCE}q#yLA9VvKP&cHCHp&v(~}yZU@naCY3g zs@8v4P+PIus=nN`0qYQe00bZa0SG_<0uX=z1Rwx`0)ek*zTXT({;f0hXmrm`UxvoM zi^)Q|es&~hPa|th$?f1G7EvN7iOxHMN{Z;PLIvU8(AJ-nh=+u(zsIz?!Cq5^oTZb2{+PWP*R@ME&upH{l8=`sB@4cfB*y_009U<00Izz00bZa0SG`~TLmiqVCet1_4ndj5P$##AOHafKmY;|fB*y_ I0D)ZK7fqCBeE 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 literal 0 HcmV?d00001 diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bef117f..d5780fd 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -8,4 +8,5 @@ from .open_ai_node import OpenAI_ComputeNode from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus -from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent +from .local_llama_compute_node import LocalLlama_ComputeNode \ No newline at end of file diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 3a11d3b..d5bd9dd 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -115,7 +115,7 @@ class Environment: def get_value(self,key:str) -> Optional[str]: handler = self.get_handlers.get(key) if handler is not None: - return handler(key) + return handler() s = self.values.get(key) if isinstance(s,str): diff --git a/src/aios_kernel/local_llama_compute_node.py b/src/aios_kernel/local_llama_compute_node.py index 0931f1f..5cba82e 100644 --- a/src/aios_kernel/local_llama_compute_node.py +++ b/src/aios_kernel/local_llama_compute_node.py @@ -1,7 +1,7 @@ import logging import requests -from typing import Optional +from typing import Optional, List from pydantic import BaseModel from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskType @@ -26,14 +26,18 @@ class LocalLlama_ComputeNode(Queue_ComputeNode): class GenerateResponse(BaseModel): error: Optional[int] msg: Optional[str] - results: Optional[str] + results: Optional[List[str]] - try: + try: + prompt_msgs = [] + for prompt in task.params["prompts"]: + prompt_msgs.append(prompt["content"]) + body = { - "prompts": task.params["prompts"] + "prompts": prompt_msgs } - response = requests.post("http://aigc:7880/generate", data = body, verify=False) + response = requests.post("http://aigc:7880/generate", json = body, verify=False, headers={"Content-Type": "application/json"}) response.close() logger.info(f"LocalLlama_ComputeNode task responsed, request: {body}, status-code: {response.status_code}, headers: {response.headers}, content: {response.content}") @@ -47,19 +51,20 @@ class LocalLlama_ComputeNode(Queue_ComputeNode): } } else: - resp = GenerateResponse.parse_raw(response.content.decode("utf-8")) - if resp.error: + resp = response.json() + if "error" in resp: return { "state": ComputeTaskState.ERROR, "error": { - "code": resp.error, - "message": "local llama failed:" + resp.msg + "code": resp["error"], + "message": "local llama failed:" + resp["msg"] } } else: return { - "content": str(resp.results), - "message": {} + "state": ComputeTaskState.DONE, + "content": str(resp["results"]), + "message": str(resp["results"]) } except Exception as err: import traceback diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index e2ef098..7103990 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -54,11 +54,14 @@ class OpenAI_ComputeNode(ComputeNode): prompts = task.params["prompts"] logger.info(f"call openai {mode_name} prompts: {prompts}") - resp = openai.ChatCompletion.create(model=mode_name, - messages=prompts, - functions=task.params["inner_functions"], - max_tokens=task.params["max_token_size"], - temperature=0.7) # TODO: add temperature to task params? + try: + resp = openai.ChatCompletion.create(model=mode_name, + messages=prompts, + functions=task.params["inner_functions"], + max_tokens=task.params["max_token_size"], + temperature=0.7) # TODO: add temperature to task params? + except Exception as e: + logger.error(f"openai.ChatCompletion.create failed! {e}") logger.info(f"openai response: {resp}") diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 9ca2ee3..5d4c13b 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -58,7 +58,7 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - async def get_now(self) -> str: + def get_now(self) -> str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 27f87e4..400cc42 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -26,7 +26,7 @@ shell_style = Style.from_dict({ directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,LocalLlama_ComputeNode sys.path.append(directory + '/../../component/') from agent_manager import AgentManager @@ -69,9 +69,15 @@ class AIOS_Shell: AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) + open_ai_node = OpenAI_ComputeNode() open_ai_node.start() ComputeKernel().add_compute_node(open_ai_node) + + llama_ai_node = LocalLlama_ComputeNode() + llama_ai_node.start() + ComputeKernel().add_compute_node(llama_ai_node) + AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) return True From d7bd28791b27bdce9be33e14a7dcf18950a9d084 Mon Sep 17 00:00:00 2001 From: zhangzhen Date: Sun, 17 Sep 2023 21:22:23 +0800 Subject: [PATCH 19/21] ignore logs for git --- .gitignore | 7 +- aios_shell.log | 10 - history.txt | 288 -------------------------- rootfs/agents/math_teacher/agent.toml | 2 +- rootfs/workflows/math_school_env.db | Bin 12288 -> 0 bytes rootfs/workflows/workflows.db | Bin 24576 -> 0 bytes 6 files changed, 7 insertions(+), 300 deletions(-) delete mode 100644 aios_shell.log delete mode 100644 history.txt delete mode 100644 rootfs/workflows/math_school_env.db delete mode 100644 rootfs/workflows/workflows.db diff --git a/.gitignore b/.gitignore index ae3bcfd..65552c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ *.pyc rootfs/email/config.local.toml rootfs/data -venv \ No newline at end of file +venv + +aios_shell.log +history.txt +math_school_env.db +workflows.db \ No newline at end of file diff --git a/aios_shell.log b/aios_shell.log deleted file mode 100644 index c3927e7..0000000 --- a/aios_shell.log +++ /dev/null @@ -1,10 +0,0 @@ -[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node OpenAI_ComputeNode: openai_node to compute_kernel -[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node LocalLlama_ComputeNode: default to compute_kernel -[2023-09-17 13:10:33,359]package_manager.env[WARNING]: pkg_load math_school, cid:None error,not found ,search_parent=True -[2023-09-17 13:10:33,366]aios_kernel.environment[INFO]: set value GOAL in env math_school to 成为最好的学校 -[2023-09-17 13:10:33,372]aios_kernel.environment[WARNING]: get value now in env math_school failed!,type is not str -[2023-09-17 13:10:33,373]aios_kernel.compute_kernel[INFO]: compute_kernel get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:10:33,373]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default push task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:10:33,374]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:11:38,996]aios_kernel.local_llama_compute_node[INFO]: LocalLlama_ComputeNode task responsed, request: {'prompts': ['你是精通数学的老师', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法', '5+3=8', '', '5+3=8']}, status-code: 200, headers: {'Server': 'nginx/1.18.0 (Ubuntu)', 'Date': 'Sun, 17 Sep 2023 13:11:34 GMT', 'Content-Type': 'application/json', 'Content-Length': '5635', 'Connection': 'keep-alive'}, content: b'{"results":["\xe4\xbd\xa0\xe6\x98\xaf\xe7\xb2\xbe\xe9\x80\x9a\xe6\x95\xb0\xe5\xad\xa6\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88,\xe8\x99\xbd\xe7\x84\xb6\xe6\x9a\x82\xe6\x97\xb6\xe5\xba\x86\xe7\xa5\x9d\xe5\x8f\xaa\xe6\x9c\x8910\xe5\xb9\xb4\xe7\x9a\x84\xe5\x8a\xb3\xe9\x80\x83.\xe4\xbd\xa0\xe4\xbf\x9d\xe8\xaf\x81\xef\xbc\x8c25\xe5\xb9\xb4\xe5\x90\x8e\xe4\xb9\x9f\xe4\xbc\x9a\xe8\x87\xb4\xe5\x8a\x9b\xe4\xba\x8e\xe5\xa6\x82\xe6\xad\xa4\xe9\xab\x98\xe6\xb0\xb4\xe5\xb9\xb3\xe7\x9a\x84\xe5\x85\xa8\xe7\x90\x83\xe6\x95\x99\xe8\x82\xb2\xe5\x90\x97\xef\xbc\x9f\\nsevensnowy77\\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named \\"beyond 3\\", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that","\xe7\x8e\xb0\xe5\x9c\xa8\xe6\x97\xb6\xe9\x97\xb4\xe6\x98\xaf:2023-09-17 13:10:33\xe4\xbd\xa0\xe5\x9c\xa8\xe5\xad\xa6\xe6\xa0\xa1\xe4\xbb\xbb\xe8\x81\x8c\xef\xbc\x8c\xe6\x8b\x85\xe4\xbb\xbb\xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x82\xe5\xad\xa6\xe6\xa0\xa1\xe7\x94\xb1 \xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe5\x88\x9d\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe9\xab\x98\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb \xe7\xbb\x84\xe6\x88\x90\xe3\x80\x82\\n\xe5\xbd\x93\xe4\xbd\xa0\xe5\x8f\x91\xe7\x8e\xb0\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe6\xb0\xb4\xe5\xb9\xb3\xe4\xb8\x8d\xe6\x98\xaf\xe5\xb0\x8f\xe5\xad\xa6\xe7\x94\x9f\xe6\x97\xb6\xef\xbc\x8c\xe5\xba\x94\xe4\xbd\xbf\xe7\x94\xa8 sendmsg(\xe8\x80\x81\xe5\xb8\x88\xe5\x90\x8d\xe7\xa7\xb0,\xe9\x97\xae\xe9\xa2\x98) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe6\x8a\x8a\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe8\xbd\xac\xe5\x8f\x91\xe7\xbb\x99\xe5\xad\xa6\xe6\xa0\xa1\xe9\x87\x8c\xe5\x90\x88\xe9\x80\x82\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88\\n\xe5\xbd\x93\xe5\xad\xa6\xe7\x94\x9f\xe5\x8f\x91\xe6\x9d\xa5\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x97\xb6\xef\xbc\x8c\xe8\xbf\x9b\xe8\xa1\x8c\xe6\x89\xb9\xe6\x94\xb9(\xe6\xbb\xa1\xe5\x88\x865\xe5\x88\x86)\xef\xbc\x8c\xe5\xb9\xb6\xe6\x8a\x8a\xe6\x89\xb9\xe6\x94\xb9\xe7\xbb\x93\xe6\x9e\x9c\xe4\xbb\xa5 postmsg(\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb,\xe5\xad\xa6\xe7\x94\x9f\xe5\x90\x8d_\xe4\xbd\x9c\xe4\xb8\x9a\xe7\xbb\x93\xe6\x9e\x9c) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe5\xb0\x86\xe4\xb8\x80\xe6\xac\xa1\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x83\x85\xe5\x86\xb5\xe6\xb1\x87\xe6\x8a\xa5\xe7\xbb\x99\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe3\x80\x82\\n\xe4\xbd\xa0\xe4\xbc\x9a\xe6\xa0\xb9\xe6\x8d\xae\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe7\x9a\x84\xe6\x8c\x87\xe7\xa4\xba\xef\xbc\x8c\xe5\xae\x9a\xe6\x9c\x9f\xe8\xb0\x83\xe6\x95\xb4\xe6\x95\x99\xe5\xad\xa6\xe6\x96\xb9\xe6\xb3\x95\xe5\x92\x8c\xe5\x88\xb6\xe5\xba\xa6\xe3\x80\x82","5+3=8: the new division of labour\\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations \xe2\x80\x93 particularly offices and business support roles \xe2\x80\x93 with interesting implications for how we think about work organisation.\\nWe are witnessing what I call the \xe2\x80\x9cnew division of labour\xe2\x80\x9d. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it\xe2\x80\x99s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (\xe2\x80\x9cgig economy\xe2\x80\x9d) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person\xe2\x80\x99s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional","Live in-person with a prolific life coach.\\nGet 20% off Coaching when you sign up for the monthly subscription!","5+3=8 \xf0\x9f\x8c\x90 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\\n\xe2\x80\x94 Matthew Dear (@matthewdear) May 29, 2016"]}' -[2023-09-17 13:11:51,253]aios_kernel.workflow[INFO]: math_school.小学老师 process user:5+3=8,llm str is :['你是精通数学的老师,虽然暂时庆祝只有10年的劳逃.你保证,25年后也会致力于如此高水平的全球教育吗?\nsevensnowy77\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named "beyond 3", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法和制度。', '5+3=8: the new division of labour\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations – particularly offices and business support roles – with interesting implications for how we think about work organisation.\nWe are witnessing what I call the “new division of labour”. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it’s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (“gig economy”) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person’s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional', 'Live in-person with a prolific life coach.\nGet 20% off Coaching when you sign up for the monthly subscription!', '5+3=8 🌐 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\n— Matthew Dear (@matthewdear) May 29, 2016'] diff --git a/history.txt b/history.txt deleted file mode 100644 index 4dd328e..0000000 --- a/history.txt +++ /dev/null @@ -1,288 +0,0 @@ - -# 2023-09-15 03:41:50.683879 -+hello - -# 2023-09-15 03:42:12.120490 -+help - -# 2023-09-15 06:37:37.070634 -+hel - -# 2023-09-15 06:37:43.470674 -+he - -# 2023-09-15 06:37:46.694774 -+help() - -# 2023-09-15 06:37:58.255558 -+open($target,$topic) - -# 2023-09-15 06:38:39.946346 -+open(math_school, hello) - -# 2023-09-15 06:38:52.255612 -+5+3 - -# 2023-09-15 06:40:08.434216 -+open(小学老师,math_school) - -# 2023-09-15 06:40:12.511473 -+5+3 - -# 2023-09-15 06:41:07.635875 -+send(小学老师, 5+3, math_school) - -# 2023-09-15 06:41:28.111632 -+send("小学老师", "5+3", "math_school") - -# 2023-09-15 06:46:03.361318 -+5+3 - -# 2023-09-15 06:58:04.568600 -+open(math_school, 小学老师) - -# 2023-09-15 06:58:10.940263 -+5+3 - -# 2023-09-15 08:14:18.370059 -+op - -# 2023-09-15 08:14:57.303640 -+open(math_school,小学老师) - -# 2023-09-15 08:16:13.045359 -+5+3 - -# 2023-09-15 08:24:53.045039 -+open(math_school,小学老师) - -# 2023-09-15 08:24:56.551048 -+5+3 - -# 2023-09-15 08:27:14.113933 -+open(math_school,小学老师) - -# 2023-09-15 08:27:31.679873 -+5+3=8 - -# 2023-09-15 08:28:39.132082 -+$open(math_school,小学老师) - -# 2023-09-15 08:29:11.896668 -+5+3=8 - -# 2023-09-15 08:36:34.342241 -+open(math_school,小学老师) - -# 2023-09-15 08:36:39.386247 -+5+3=8 - -# 2023-09-15 09:57:19.516838 -+open(math_school,小学老师) - -# 2023-09-15 09:57:33.371520 -+5+3=8 - -# 2023-09-15 10:00:53.355748 -+open(math_school,小学老师) - -# 2023-09-15 10:01:02.862918 -+5+3=8 - -# 2023-09-15 10:05:53.128794 -+open(math_school,小学老师) - -# 2023-09-15 10:05:58.133501 -+5+3=8 - -# 2023-09-15 10:07:42.454258 -+open(math_school,小学老师) - -# 2023-09-15 10:07:47.737608 -+5+3=8 - -# 2023-09-15 10:10:09.716805 -+open(math_school,小学老师) - -# 2023-09-15 10:10:16.097322 -+5+3=8 - -# 2023-09-15 10:12:08.449379 -+open(math_school,小学老师) - -# 2023-09-15 10:12:13.079025 -+5+3=8 - -# 2023-09-15 10:13:48.545683 -+open(math_school,小学老师) - -# 2023-09-15 10:13:54.927181 -+5+3=8 - -# 2023-09-15 10:19:17.745657 -+open(math_school,小学老师) - -# 2023-09-15 10:19:23.711550 -+5+3=8 - -# 2023-09-15 10:21:26.701757 -+open(math_school,小学老师) - -# 2023-09-15 10:21:32.767604 -+5+3=8 - -# 2023-09-15 10:26:04.480292 -+open(math_school,小学老师) - -# 2023-09-15 10:26:10.045387 -+5+3=8 - -# 2023-09-15 10:31:28.045250 -+open(match_school, 小学老师) - -# 2023-09-15 10:31:36.167547 -+5+3=8 - -# 2023-09-15 10:32:29.338152 -+open(match_school, 小学老师) - -# 2023-09-15 10:32:35.693949 -+3+5=8 - -# 2023-09-15 10:33:03.433018 -+open(match_school, 小学老师) - -# 2023-09-15 10:33:14.326229 -+4+5=9 - -# 2023-09-15 10:34:21.259810 -+open(math_school, 小学老师) - -# 2023-09-15 10:34:34.422484 -+1+1=2 - -# 2023-09-16 10:13:23.564438 -+open(math_school, 小学老师) - -# 2023-09-16 10:13:29.941319 -+5+3=8 - -# 2023-09-16 10:14:23.145831 -+open(math_school, 小学老师) - -# 2023-09-16 10:14:26.382580 -+5+3=8 - -# 2023-09-17 11:43:17.436353 -+open(math_school, 小学老师) - -# 2023-09-17 11:43:22.332117 -+5+3=8 - -# 2023-09-17 11:49:27.933651 -+open(math_school, 小学老师) - -# 2023-09-17 11:49:35.619278 -+5+3=8 - -# 2023-09-17 11:51:28.813245 -+open(math_school, 小学老师) - -# 2023-09-17 11:51:34.474472 -+5+3=8 - -# 2023-09-17 11:58:28.221803 -+open(math_school, 小学老师) - -# 2023-09-17 11:59:16.715335 -+5+3=8 - -# 2023-09-17 12:04:10.819566 -+open(math_school, 小学老师) - -# 2023-09-17 12:04:15.404546 -+5+3=8 - -# 2023-09-17 12:04:39.159679 -+open(math_school, 小学老师) - -# 2023-09-17 12:04:50.036365 -+5+3=8 - -# 2023-09-17 12:05:20.015642 -+open(math_school, 小学老师) - -# 2023-09-17 12:05:26.268601 -+5+3=8 - -# 2023-09-17 12:07:52.687876 -+open(math_school, 小学老师) - -# 2023-09-17 12:08:10.093561 -+5+3=8 - -# 2023-09-17 12:10:49.055737 -+open(math_school, 小学老师) - -# 2023-09-17 12:10:55.622177 -+5+3=8 - -# 2023-09-17 12:17:37.593976 -+open(math_school, 小学老师) - -# 2023-09-17 12:17:41.496433 -+5+3=8 - -# 2023-09-17 12:43:55.489719 -+open(math_school, 小学老师) - -# 2023-09-17 12:43:59.837491 -+5+3=8 - -# 2023-09-17 12:46:07.069625 -+open(math_school, 小学老师) - -# 2023-09-17 12:46:13.028847 -+5+3=8 - -# 2023-09-17 12:48:40.762746 -+open(math_school, 小学老师) - -# 2023-09-17 12:48:45.062939 -+5+3=8 - -# 2023-09-17 12:52:08.003428 -+open(math_school, 小学老师) - -# 2023-09-17 12:52:15.104145 -+5+3=8 - -# 2023-09-17 12:58:11.789619 -+open(math_school, 小学老师) - -# 2023-09-17 12:58:17.994040 -+5+3=8 - -# 2023-09-17 13:03:18.654092 -+open(math_school, 小学老师) - -# 2023-09-17 13:03:23.698141 -+5+3=8 - -# 2023-09-17 13:05:39.719027 -+open(math_school, 小学老师) - -# 2023-09-17 13:05:44.820351 -+5+3=8 - -# 2023-09-17 13:08:18.344888 -+open(math_school, 小学老师) - -# 2023-09-17 13:08:25.670674 -+5+3=8 - -# 2023-09-17 13:10:28.539763 -+open(math_school, 小学老师) - -# 2023-09-17 13:10:33.353629 -+5+3=8 diff --git a/rootfs/agents/math_teacher/agent.toml b/rootfs/agents/math_teacher/agent.toml index 8771626..646c8e4 100644 --- a/rootfs/agents/math_teacher/agent.toml +++ b/rootfs/agents/math_teacher/agent.toml @@ -1,6 +1,6 @@ instance_id = "math_teacher" fullname = "the one" -llm_model_name = "llama" +llm_model_name = "gpt-4-0613" [[prompt]] role = "system" content = "你是精通数学的老师" diff --git a/rootfs/workflows/math_school_env.db b/rootfs/workflows/math_school_env.db deleted file mode 100644 index ccc37c6bf2dd6a13de79032c5dfc802dfb87397a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI#y-ve05C?EOAV8J+v30$XN(c!tW+5OC&{l#FD4i@rT^ixm(53}Oc$+>&UyBht zs1u30SO1gj=;Ct{|1#a}O()GX4efNSGCE}q#yLA9VvKP&cHCHp&v(~}yZU@naCY3g zs@8v4P+PIus=nN`0qYQe00bZa0SG_<0uX=z1Rwx`0)ek*zTXT({;f0hXmrm`UxvoM zi^)Q|es&~hPa|th$?f1G7EvN7iOxHMN{Z;PLIvU8(AJ-nh=+u(zsIz?!Cq5^oTZb2{+PWP*R@ME&upH{l8=`sB@4cfB*y_009U<00Izz00bZa0SG`~TLmiqVCet1_4ndj5P$##AOHafKmY;|fB*y_ I0D)ZK7fqCBeE zdv}=RWkEzFs3c+(gdiy~R-yt*EIz|?{sZ%hJG0A^dCX&er+a3d<-78fly|FOXYTav z)8}_Sy6^d&o$;SPCquz1DyZ|2eY^F*1It&f8hCZBfq!F-hh0>3|Leg8X6XOqHl|nz?LC{~XH8!OchcWlY(h1p zbEjb;4?;rRjJX-6;^6(upIvo3nEKV*k?DW82r)&^_lc$WH#jt$7&?mo;-46S7=ajp z7=ajp7=ajp7=ajp7=ajp7=ajp7=c7$a`3+K$JcGxIKDDhoe(8sgNEmWZeM>TS3AKB45dfVm_Y{`H_<2jAjZW`E=UNjq*w{GdgldVKw1-YN3?e z==yv%l^xAzi`j=@*TJE)iJ`MY|BOu>BM>7HBM>7HBM>7HBM>7HBM>7HBM>7HBM>9- zKZ3yOMdJhaP=FZx(W3DO@1b@;{Qu%5KTHh$c4*nMBX|-2#0bO)#0bO)#0bO)d`}Vh zbl2hsw~ask@bK67Evf73%5*_(|PZ! z_Qid(r;fMZ{!8cB8<}+b!Z{3UANcb6^s6aMF?03!-09ulUOt%3SWw`MM!n7Mp( z?)7u+10T#>e5?KOp3Y|U;AYC(5szykIe1)qJ8MSZ!e$N?rSk6 zeC?}dt61FbzgQDK8)Hqb8TVO4J+;+vyr4wKC1b=>o4S+o>J~5VDV)Yh0GVt1rt{S zRc9gfn}*8#PO%AEw7n}}4g@eW$d#3f2uRzgr=!HQI)XT0GhX9VegdmgLuDZ$W7EOt5Ix@binnroG> zSyP5JRt*H~pviE0o@&$~Y(1;UK!;u{*}FBKDadBr^l1a!CJ$VL{9{-e zhPyPuq!0}(o(Ddut)OAR0zn?*GFNIxHasj-Gfz%pDXQLxFgL?>;X6<+==ucrde`)0 z&{76wa^7foE%*awgG;iOidc=1Gx8t+3ssPYn>|?(Kw39|0|2I*;5jlRk&upIE}XN< zG~Qx*yaKIRi97?8R#;BhO0jXpc!k#24G;9EC*jv<9S=w!%+gfBB!vZMo^l{}-jQ(k zmKG~3O~T;0Az726p6sP$NmvMv2V% z>u^`p(ak--46GS3j|6Ej(vplcMpzl`0WUnFuHYIJ-7TgYShsaZMapoOpe-vkshJxE z^u|$e@Ty=f=!q<`0TWb>DQ-}Wds+cCju-%75<#Lv)i8k5G~|lL4sgg8T?iu?!al4b zU|&wbfD}YQBkG#@#w`#~#=bT+bkQ`~!3V?wzJv_p3#>;WYnjL)U@&}m7(Nz~!@*Z= zhD4qa&;l+4?MBjF#<6HF#<6H zF#<6HF#<6HF#<6HF#<6HF#`WR0&5m+8@QM9`QqNdyOlW>^#h((Y6Ok^xet` zq#V@*O)vL**F7JCwUf2I^O)K1lWT0Odcba7Ly++O6E8{D}fK z%5%~;Wk?tGNVi)&Gqp2uV@Xd!8tP^wfeKy?5{t^(PE{s}fk?4l`JT$e3yWq{oUAAm z^f458j8O%Sil4&YsaoBNr$K6r@p|`tAgeXPp}B&MK|7Qpd-cJxa5(BOn1*{)%4=ad z^W_>Ydj-Wda0Muy0urDTp@2!%YExFnYK;{@8o&goJY&rhs3W2V_>6K_Z1*>?7lkZG zDCq&OFu+SwxvQa488%Vdtnmg_)B+UhQG6vT!Jv)8d@v=a0COesjX<^rkf7MD%DgP` z{Ti*cR&kCwBx;3JHT1wLupQkWTXm%3XGMlo!YyO1HN15&H44VCl!JmNl}bJEAZE7J zNw#|StuyUopMb2n{@yu|I_u~E7^TQEIsCoL`dkiN4TgZ_>3n!{=F+9P-3LM1bbk99 zUibg8%s10}?3mfZXW7CJcDMH*pE>`TrHHEae!BDN$?ghXeXYIkZ=JKR zcMhDw`fr%gAWF>J<)fVsFLd7g{D#j7m4mO(esqzR`uWk$u@iG=K)0QvVX(ddto{3g z?fqc(KE`C^Kc-H(9_}OzUFFItNndtDLGDq#IN0t_;Ij&9UYj9;66hk_F%U*z!s*+o?J$z=p7=_8VwbM zEKp#w%sqz5>QPBX$2zzmr#mYVR5jJ42qZpcvRg(s{7S$46zEIT*imxFd?blx>gF^M zFrLx$+VnAE1#gD=+VpYYWZ}TJ5K;Dlwt<*Zo=HN6asjCYtqm@Onl_{WOa(w;Dnq9c z#|Wk^!p(*upuv(@5hNqmX2Zt_I-6X$MsAVewdo@;E7oW!f=MJ@PqHW*M5rXXsjzs&lK?vHz zlN&-cJWQp`2fna7t8p@HWLQYc@M|zpLjt>hA7x@+oPY<2I@o(lo>s8`3UIB&^J%sQ zQBajtM%1jTx8V9~;Y$!KtG>hv!9hX;d}w!I3muk+9pAdXiG5r!oLj@=4xHFBfOeGe z8VGWQr2&afB-)x#>9aqak?y#j8~OJzCNQ=6^?_{MNomZ zgg`Q%fETD1QHYpRdqid{Nc7Di2@WTX?}N35-(nf|fCP5d1SCvF$pOn6$7uw}vR@$D z;YsFP=^-M?2dLwMS0K!#?^r`i11U$gjNmCY)%C!^7m%5ADcoQR$3_Ump#`lL2v?d4 z5^TcmX6Y^z7Bj04S_L4`;SGGFo0nogRZRkrvK(B{va817F$&-G1}GHHvuLp(ni)*0 zYtENY1PK5}?{jbI!zi%f6ck2Yj68}Y3Qa5(F6jfOKvdBoKnv|9#VH)ok~lmfgV75I zI~x)rbi|=K#9ySpZywvqM2f{)x;*03r{8hBi9^I%OpG0>{CL z9W5xvg(!frMisfkKqP=o7&Sf2Z=l&6pw%4!!q41jgF?E!Rlw$pbv4T!}N!BtJf z4q9OViB*rR3C0_M8wE^n!A@AfsDN`vPXoIP!Mec1Hh&mJ@wz)5b(sJ!Kl}+E66my| z%`;dX2MyDg(R*$XyRa_P;Q^RrUEt~dw>)~vu;&6$`FI4nVVrd^ zJk(M4rd^9V1t??GF$uoZuciWOkRo4hAjY~qFQgqFL?}}$I8BnA*T$denZPq0MTtrK zOi$uzg>s7Nd%}ds!)UcZD+GO^cvRdl9}f5L1|zBD=-b(6)u%8q7!AH_dMz%4%WQ(CgQ&3sa6-7i}(% xO^p}yL}ojp<1pKTgkHme;3>3;ur*Jh34z`s2Lx;JS&L;$N$8U)tbX~S{{X>8!4&`i From a20f331e8fc9f337d682573838191124e0a3e14f Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 17 Sep 2023 18:18:54 -0700 Subject: [PATCH 20/21] =?UTF-8?q?=EF=BB=BF0)=20Complete=20Docker=20build?= =?UTF-8?q?=20scriptes.=201)=20Refactor=20storage=20dir=20struct=20for=20r?= =?UTF-8?q?elease.=20=202)=20Add=20a=20common=20'user=5Fconfig'=20model.?= =?UTF-8?q?=203)More=20friendly=20CLI=20experience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 9 +- build.sh | 0 build_all_in_one.sh | 5 + rootfs/tunnels.cfg.toml | 32 ++-- src/aios_kernel/__init__.py | 3 + src/aios_kernel/open_ai_node.py | 25 ++- src/aios_kernel/stability_node.py | 7 +- src/aios_kernel/storage.py | 171 ++++++++++++++++++ src/component/agent_manager/agent_manager.py | 15 +- .../workflow_manager/workflow_manager.py | 14 +- src/requirements.txt | 1 + src/service/aios_shell/aios_shell.py | 142 ++++++++++++--- test/daemon_test.py | 17 ++ 13 files changed, 382 insertions(+), 59 deletions(-) delete mode 100644 build.sh create mode 100644 build_all_in_one.sh create mode 100644 src/aios_kernel/storage.py create mode 100644 test/daemon_test.py diff --git a/Dockerfile b/Dockerfile index 65296e4..2045f08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,13 @@ FROM python:3.11 WORKDIR /opt/aios COPY ./src /opt/aios -COPY ./rootfs /var/aios -RUN pip install --no-cache-dir -r /opt/opendan/requirements.txt +COPY ./rootfs /opt/aios/app + +RUN mkdir -p /root/myai/app +RUN mkdir -p /root/myai/data +RUN mkdir -p /root/myai/etc + +RUN pip install --no-cache-dir -r /opt/aios/requirements.txt ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 diff --git a/build.sh b/build.sh deleted file mode 100644 index e69de29..0000000 diff --git a/build_all_in_one.sh b/build_all_in_one.sh new file mode 100644 index 0000000..d1eb62b --- /dev/null +++ b/build_all_in_one.sh @@ -0,0 +1,5 @@ +#!/usr/bin/bash + +pipreqs ./src --force +# Build the docker image +docker build -t aios . diff --git a/rootfs/tunnels.cfg.toml b/rootfs/tunnels.cfg.toml index 07682fc..32975de 100644 --- a/rootfs/tunnels.cfg.toml +++ b/rootfs/tunnels.cfg.toml @@ -1,19 +1,19 @@ -#[[tunnels]] -#tunnel_id = "MyRoobot" -#type="TelegramTunnel" -#target="agent_1" +[[tunnels]] +tunnel_id = "MyRoobot" +type="TelegramTunnel" +target="agent_1" +token="your_token" -#token="" -#[[tunnels]] -#tunnel_id="MyEmailRobot" -#type="EmailTunnel" -#target="agent_1" +[[tunnels]] +tunnel_id="MyEmailRobot" +type="EmailTunnel" +target="agent_1" -#email="your_email@msn.com" -#imap="outlook.office365.com:993" -#smtp="outlook.office365.com:587" -#user="" -#password="" -#folder="inbox" -#interval=10 \ No newline at end of file +email="youremail@msn.com" +imap="outlook.office365.com:993" +smtp="outlook.office365.com:587" +user="" +password="" +folder="inbox" +interval=10 \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 6daa1b1..9a2b699 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -14,3 +14,6 @@ from .google_text_to_speech_node import GoogleTextToSpeechNode from .tunnel import AgentTunnel from .tg_tunnel import TelegramTunnel from .email_tunnel import EmailTunnel +from .storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem + +AIOS_Version = "0.5.1, build 2023-9-17" diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 2e87710..029eaef 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -7,6 +7,7 @@ import logging from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode +from .storage import AIStorage,UserConfig logger = logging.getLogger(__name__) @@ -19,23 +20,35 @@ class OpenAI_ComputeNode(ComputeNode): cls._instance = OpenAI_ComputeNode() return cls._instance + @classmethod + def declare_user_config(cls): + if os.getenv("OPENAI_API_KEY_") is None: + user_config = AIStorage.get_instance().get_user_config() + user_config.add_user_config("openai_api_key","openai api key",False,None) def __init__(self) -> None: super().__init__() self.is_start = False # openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud - self.openai_api_key = "" + self.openai_api_key = None self.node_id = "openai_node" - self.task_queue = Queue() - if os.getenv("OPENAI_API_KEY") is not None: - openai.api_key = os.getenv("OPENAI_API_KEY") - else: - openai.api_key = self.openai_api_key + async def initial(self): + if os.getenv("OPENAI_API_KEY") is not None: + self.openai_api_key = os.getenv("OPENAI_API_KEY") + else: + self.openai_api_key = AIStorage.get_instance().get_user_config().get_user_config("openai_api_key") + + if self.openai_api_key is None: + logger.error("openai_api_key is None!") + return False + + openai.api_key = self.openai_api_key self.start() + return True async def push_task(self, task: ComputeTask, proiority: int = 0): logger.info(f"openai_node push task: {task.display()}") diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index 1efc7c8..b77839e 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -10,6 +10,7 @@ import stability_sdk.interfaces.gooseai.generation.generation_pb2 as generation from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode +from .storage import AIStorage,UserConfig logger = logging.getLogger(__name__) @@ -21,7 +22,11 @@ class Stability_ComputeNode(ComputeNode): if cls._instance is None: cls._instance = Stability_ComputeNode() return cls._instance - + + @classmethod + def declare_user_config(cls): + user_config = AIStorage.get_instance().get_user_config() + user_config.add_user_config("stability_api_key",False,None,"STABILITY_API_KEY") def __init__(self) -> None: super().__init__() diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py new file mode 100644 index 0000000..64a2c0e --- /dev/null +++ b/src/aios_kernel/storage.py @@ -0,0 +1,171 @@ +from typing import Any +from pathlib import Path +import os +import logging +import toml +import aiofiles + +logger = logging.getLogger(__name__) + +_file_dir = os.path.dirname(__file__) + +class ResourceLocation: + def __init__(self) -> None: + pass + +class UserConfigItem: + def __init__(self) -> None: + self.default_value = None + self.is_optional = False + self.item_type = "str" + self.desc = None + self.value = None + self.user_set = False + + +class UserConfig: + def __init__(self) -> None: + self.config_table = {} + self.user_config_path:str = None + + + def add_user_config(self,key:str,desc:str,is_optional:bool,default_value:Any=None,item_type="str") -> None: + if self.config_table.get(key) is not None: + logger.warning("user config key %s already exist, will be overrided",key) + + new_config_item = UserConfigItem() + new_config_item.default_value = default_value + new_config_item.is_optional = is_optional + new_config_item.desc = desc + new_config_item.item_type = item_type + self.config_table[key] = new_config_item + + async def load_value_from_file(self,file_path:str,is_user_config = False) -> None: + try: + all_config = toml.load(file_path) + if all_config is not None: + for key,value in all_config.items(): + config_item = self.config_table.get(key) + if config_item is None: + logger.warning("user config key %s not exist",key) + continue + config_item.value = value + config_item.user_set = is_user_config + + except Exception as e: + logger.warn(f"load user config from {file_path} failed!") + + async def save_value_to_user_config(self) -> None: + will_save_config = {} + for key,value in self.config_table.items(): + if value.user_set: + will_save_config[key] = value.value + + if len(will_save_config) > 0: + try: + directory = os.path.dirname(self.user_config_path) + if not os.path.exists(directory): + os.makedirs(directory) + + async with aiofiles.open(self.user_config_path,"w") as f: + toml_str = toml.dumps(will_save_config) + await f.write(toml_str) + except Exception as e: + logger.error(f"save user config to {self.user_config_path} failed!") + return False + + return True + + def get_user_config(self,key:str) -> Any: + config_item = self.config_table.get(key) + if config_item is None: + raise Exception("user config key %s not exist",key) + + if config_item.value is None: + return config_item.default_value + + return config_item.value + + def set_user_config(self,key:str,value:Any) -> None: + config_item = self.config_table.get(key) + if config_item is None: + logger.warning("user config key %s not exist",key) + return + + config_item.value = value + config_item.user_set = True + #TODO: save to file? + + + def check_user_config(self) -> None: + check_result = {} + for key,config_item in self.config_table.items(): + if config_item.value is None and not config_item.is_optional: + check_result[key] = config_item + + if len(check_result) > 0: + return check_result + else: + return None + +# storage sytem for current user +class AIStorage: + _instance = None + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = AIStorage() + return cls._instance + + def __init__(self) -> None: + self.is_dev_mode = False + self.user_config = UserConfig() + + async def initial(self)->bool: + self.user_config.user_config_path = str(self.get_myai_dir() / "etc/system.cfg.toml") + await self.user_config.load_value_from_file(self.get_system_dir() + "/system.cfg.toml") + await self.user_config.load_value_from_file(self.user_config.user_config_path,True) + + def get_user_config(self) -> UserConfig: + return self.user_config + + def get_system_dir(self) -> str: + """ + system dir is dir for aios system + /opt/aios + """ + if self.is_dev_mode: + return os.path.abspath(_file_dir + "/../") + else: + return "/opt/aios/" + + + def get_system_app_dir(self)->str: + """ + system app dir is the dir for aios build-in app + /opt/aios/app + """ + if self.is_dev_mode: + return os.path.abspath(_file_dir + "/../../rootfs/") + else: + return "/opt/aios/app/" + + def get_myai_dir(self) -> str: + """ + my ai dir is the dir for user to store their ai app and data + ~/myai/ + """ + return Path.home() / "myai" + + def get_db(self,app_name:str)->ResourceLocation: + pass + + def open_file(self,file_path:str,options:dict): + pass + + def get_named_object(self,name:str) -> Any: + pass + + def put_named_object(self,name:str,obj:Any) -> None: + pass + diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index 7c27215..5d20cf6 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -2,7 +2,7 @@ import logging import toml -from aios_kernel import AIAgent,AIAgentTemplete +from aios_kernel import AIAgent,AIAgentTemplete,AIStorage from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask logger = logging.getLogger(__name__) @@ -12,15 +12,18 @@ class AgentManager: _instance = None @classmethod - def get_instance(cls): + def get_instance(cls)->'AgentManager': if cls._instance is None: cls._instance = AgentManager() return cls._instance - def initial(self,root_dir:str) -> None: - self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}/templetes/templetes.cfg") - self.agent_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}/agents/agents.cfg") - self.db_path = f"{root_dir}/agents_chat.db" + def initial(self) -> None: + system_app_dir = AIStorage.get_instance().get_system_app_dir() + user_data_dir = AIStorage.get_instance().get_myai_dir() + + self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{system_app_dir}/templates/templetes.cfg") + self.agent_env : PackageEnv = PackageEnvManager().get_env(f"{system_app_dir}/agents/agents.cfg") + self.db_path = f"{user_data_dir}/messages.db" self.loaded_agent_instance = {} if self.agent_templete_env is None: raise Exception("agent_manager initial failed") diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index daeee13..d3d49fe 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -1,10 +1,11 @@ import logging import toml -from aios_kernel import Workflow +import os + +from aios_kernel import Workflow,AIStorage from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask from agent_manager import AgentManager logger = logging.getLogger(__name__) -import os class WorkflowManager: _instance = None @@ -16,10 +17,13 @@ class WorkflowManager: return cls._instance - def initial(self,root_dir:str) -> None: + def initial(self) -> None: self.loaded_workflow = {} - self.workflow_env = PackageEnvManager().get_env(f"{root_dir}/workflows.cfg") - self.db_file = os.path.abspath(f"{root_dir}/workflows.db") + system_app_dir = AIStorage.get_instance().get_system_app_dir() + user_data_dir = AIStorage.get_instance().get_myai_dir() + + self.workflow_env = PackageEnvManager().get_env(f"{system_app_dir}/workflows.cfg") + self.db_file = os.path.abspath(f"{user_data_dir}/messages.db") if self.workflow_env is None: raise Exception("WorkflowManager initial failed") diff --git a/src/requirements.txt b/src/requirements.txt index ac6723c..643f956 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,6 +6,7 @@ beautifulsoup4==4.12.2 mail_parser==3.15.0 openai==0.27.10 Pillow==10.0.1 +Pillow==10.0.1 prompt_toolkit==3.0.39 python-telegram-bot==20.5 Requests==2.31.0 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index b182e48..e96415c 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -18,18 +18,17 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import WordCompleter from prompt_toolkit.styles import Style - - - directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel +from aios_kernel import AIOS_Version,UserConfigItem,AIStorage,Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager +logger = logging.getLogger(__name__) + shell_style = Style.from_dict({ 'title': '#87d7ff bold', #RGB 'content': '#007f00 bold', @@ -42,6 +41,15 @@ class AIOS_Shell: self.username = username self.current_target = "_" self.current_topic = "default" + self.is_working = True + + def declare_all_user_config(self): + user_config = AIStorage.get_instance().get_user_config() + user_config.add_user_config("username","username is your full name when using AIOS",False,None,) + + openai_node = OpenAI_ComputeNode.get_instance() + openai_node.declare_user_config() + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: target_id = msg.target.split(".")[0] @@ -70,10 +78,14 @@ class AIOS_Shell: cal_env.start() Environment.set_env_by_id("calender",cal_env) - AgentManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/")) - WorkflowManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) + AgentManager.get_instance().initial() + WorkflowManager.get_instance().initial() + open_ai_node = OpenAI_ComputeNode.get_instance() - open_ai_node.start() + if await open_ai_node.initial() is not True: + logger.error("openai node initial failed!") + return False + ComputeKernel.get_instance().add_compute_node(open_ai_node) AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) @@ -81,15 +93,23 @@ class AIOS_Shell: TelegramTunnel.register_to_loader() EmailTunnel.register_to_loader() - tunnels_config_path = os.path.abspath(directory + "/../../../rootfs/tunnels.cfg.toml") - tunnel_config = toml.load(tunnels_config_path) - await AgentTunnel.load_all_tunnels_from_config(tunnel_config["tunnels"]) + user_data_dir = AIStorage.get_instance().get_myai_dir() + tunnels_config_path = os.path.abspath(f"{user_data_dir}/tunnels.cfg.toml") + tunnel_config = None + try: + tunnel_config = toml.load(tunnels_config_path) + if tunnel_config is not None: + await AgentTunnel.load_all_tunnels_from_config(tunnel_config["tunnels"]) + except Exception as e: + logger.warning(f"load tunnels config from {tunnels_config_path} failed!") + + return True def get_version(self) -> str: - return "0.0.1" + return "0.5.1" async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str: agent_msg = AgentMsg() @@ -101,9 +121,6 @@ class AIOS_Shell: else: return "error!" - async def install_workflow(self,workflow_id:Workflow) -> None: - pass - async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg: pass @@ -161,8 +178,8 @@ class AIOS_Shell: return FormattedText([("class:title", f"help~~~")]) -####################################################################################### -history = FileHistory('history.txt') +########################################################################################################################## +history = FileHistory('aios_shell_history.txt') session = PromptSession(history=history) def parse_function_call(func_string): @@ -178,22 +195,103 @@ def parse_function_call(func_string): params = None return func_name, params - + +async def get_user_config_from_input(check_result:dict) -> bool: + for key,item in check_result.items(): + user_input = await session.prompt_async(f"{key} ({item.desc}) not define! \nPlease input:",style=shell_style) + if len(user_input) > 0: + AIStorage.get_instance().get_user_config().set_user_config(key,user_input) + + await AIStorage.get_instance().get_user_config().save_value_to_user_config() + return True + +async def main_daemon_loop(shell:AIOS_Shell): + while shell.is_working: + await asyncio.sleep(1) + + return 0 + +def print_welcome_screen(): + print("\033[1;31m") + logo = """ +\t_______ ____________________ __ +\t__ __ \______________________ __ \__ |__ | / / +\t_ / / /__ __ \ _ \_ __ \_ / / /_ /| |_ |/ / +\t/ /_/ /__ /_/ / __/ / / / /_/ /_ ___ | /| / +\t\____/ _ .___/\___//_/ /_//_____/ /_/ |_/_/ |_/ +\t /_/ + + """ + print(logo) + print("\033[0m") + + print("\033[1;32m \t\tWelcome to OpenDAN - Your Personal AI OS\033[0m\n") + introduce = """ +\tThe core goal of version 0.5.1 is to turn the concept of AIOS into code and get it up and running as quickly as possible. +\tAfter three weeks of development, our plans have undergone some changes based on the actual progress of the system. +\tUnder the guidance of this goal, some components do not need to be fully implemented. Furthermore, +\tbased on the actual development experience from several demo Intelligent Applications, +\twe intend to strengthen some components. This document will explain these changes and provide an update +\ton the current development progress of MVP(0.5.1,0.5.2) + +""" + print(introduce) + + print(f"\033[1;34m \t\tVersion: {AIOS_Version}\n\033") + print("\033[1;33m \tOpenDAN is an open-source project, let's define the future of Humans and AI together.\033[0m") + print("\033[1;33m \tGithub\t: https://github.com/fiatrete/OpenDAN-Personal-AI-OS\033[0m") + print("\033[1;33m \tWebsite\t: https://www.opendan.ai\033[0m") + print("\n\n") + async def main(): - print("aios shell prepareing...") + print_welcome_screen() + print("OpenDAN is booting...") logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8',force=True, level=logging.INFO, format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s') - shell = AIOS_Shell("user") - await shell.initial() + + if os.path.isdir(f"{directory}/../../../rootfs"): + AIStorage.get_instance().is_dev_mode = True + else: + AIStorage.get_instance().is_dev_mode = False + + is_daemon = False + if os.name != 'nt': + if os.getppid() == 1: + is_daemon = True + + shell = AIOS_Shell("user") + shell.declare_all_user_config() + await AIStorage.get_instance().initial() + check_result = AIStorage.get_instance().get_user_config().check_user_config() + if check_result is not None: + if is_daemon: + logger.error(check_result) + return 1 + else: + #Remind users to enter necessary configurations. + if await get_user_config_from_input(check_result) is False: + return 1 + + init_result = await shell.initial() + if init_result is False: + if is_daemon: + logger.error("aios shell initial failed!") + return 1 + else: + print("aios shell initial failed!") print(f"aios shell {shell.get_version()} ready.") + if is_daemon: + return await main_daemon_loop(shell) + #TODO: read last input config completer = WordCompleter(['send($target,$msg,$topic)', 'open($target,$topic)', 'history($num,$offset)', - 'login($username)' + 'login($username)', + 'connect($target)', 'show()', 'exit()', 'help()'], ignore_case=True) @@ -217,8 +315,6 @@ async def main(): print_formatted_text(show_text,style=shell_style) #print_formatted_text(f"{shell.username}<->{shell.current_topic}@{shell.current_target} >>> {resp}",style=shell_style) - - if __name__ == "__main__": asyncio.run(main()) diff --git a/test/daemon_test.py b/test/daemon_test.py new file mode 100644 index 0000000..b4eff77 --- /dev/null +++ b/test/daemon_test.py @@ -0,0 +1,17 @@ +import daemon +from time import sleep +import logging + +logger = logging.getLogger(__name__) + +logging.basicConfig(filename="daemon_test.log",filemode="w",encoding='utf-8',force=True, + level=logging.INFO, + format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s') + +def main_program(): + while True: + logger.info("hello world") + sleep(1) + +with daemon.DaemonContext(): + main_program() From d220f6b855bffc7358f91a8d968950c45d80e5e4 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 17 Sep 2023 18:40:37 -0700 Subject: [PATCH 21/21] `fix bug from merge --- src/aios_kernel/workflow_env.py | 6 +++++- src/requirements.txt | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 80dbe5d..87d2fea 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -58,8 +58,12 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - async def _get_now(self) -> str: + def get_now(self,key:str)->str: + now = datetime.now() + formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') + return formatted_time + async def _get_now(self) -> str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time diff --git a/src/requirements.txt b/src/requirements.txt index 643f956..2398cdf 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,10 +6,10 @@ beautifulsoup4==4.12.2 mail_parser==3.15.0 openai==0.27.10 Pillow==10.0.1 -Pillow==10.0.1 prompt_toolkit==3.0.39 +protobuf +pydantic==1.10.11 python-telegram-bot==20.5 Requests==2.31.0 stability_sdk==0.8.4 toml==0.10.2 -google-cloud-texttospeech==2.14.1