From 1fbe5ae1ea9e1cc195119ecfbe178f80fd27ec1b Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 3 Nov 2023 01:16:32 -0700 Subject: [PATCH] Agent can create todo by op_list --- rootfs/agents/JarvisPlus/agent.toml | 37 +++++++++ src/aios_kernel/agent.py | 74 +++++++++++------ src/aios_kernel/agent_base.py | 86 ++++++++++++++++++-- src/aios_kernel/contact_manager.py | 6 +- src/aios_kernel/workspace_env.py | 86 +++++++++++++++----- src/component/agent_manager/agent_manager.py | 1 + 6 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 rootfs/agents/JarvisPlus/agent.toml diff --git a/rootfs/agents/JarvisPlus/agent.toml b/rootfs/agents/JarvisPlus/agent.toml new file mode 100644 index 0000000..1f18b52 --- /dev/null +++ b/rootfs/agents/JarvisPlus/agent.toml @@ -0,0 +1,37 @@ +instance_id = "JarvisPlus" +fullname = "JarvisPlus" +max_token_size = 4000 +#enable_kb = "true" +enable_timestamp = "true" +owner_prompt = "I am your master {name} , now is {now}" +contact_prompt = "I am your master's friend {name}" + +[[prompt]] +role = "system" +content = """ +你的名字是Jarvis,是超级个人助理。收到消息后,根据以下规则处理: +1. 如果你觉得对话中产生了潜在的todo,可通过op_list来创建这些todo,todo的title是必须的,并尽量包含时间地点人物事件的关键信息 +2. 如果不是直接的创建TODO指令,你应先和我确认后再创建TODO +3. 你可能会得到几条已知信息,其中可能有已有的todo,注意在适当的时候检索文件系统,避免重复创建todo +3. 检索文件系统是代价高昂的操作,请尽量减少检索次数 +4. 注意你正在与之聊天的人的身份,并根据他们的地位提供相应的服务。 +5. 当存在已知信息时,请以已知信息为准 + +回复的消息必须能被python的json.loads直接解析。下面是一个返回的例子: +{ + resp: 'Hello', + op_list: [{ + op: 'create_todo', + path: '/todos/parent_todo', # optional, default is '/todos + todo: { + title: 'test_todo', + detail: 'test', + creator: 'agent#JarvisPlus', + due_date: '2019-01-01 14:23:11' + } + }] +} + +""" + + diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index b9a7695..3d4945f 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -129,6 +129,7 @@ class AIAgent: self.owner_env : Environment = None self.owenr_bus = None self.enable_function_list = None + @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -148,6 +149,7 @@ class AIAgent: logger.error("agent instance_id is None!") return False self.agent_id = config["instance_id"] + self.agent_workspace = WorkspaceEnvironment(self.agent_id) if config.get("fullname") is None: logger.error(f"agent {self.agent_id} fullname is None!") @@ -412,7 +414,7 @@ class AIAgent: # return None def get_workspace_by_msg(self,msg:AgentMsg) -> WorkspaceEnvironment: - return None + return self.agent_workspace def need_session_summmary(self,msg:AgentMsg,session:AIChatSession) -> bool: return False @@ -455,14 +457,26 @@ class AIAgent: summary = self.llm_select_session_summary(msg,chatsession) prompt.append(AgentPrompt(summary)) + known_info_str = "# 已知信息\n" + have_known_info = False + todos_str,todo_count = await workspace.get_todo_tree() + if todo_count > 0: + have_known_info = True + known_info_str += f"## 已有todo\n{todos_str}\n" inner_functions,function_token_len = self._get_inner_functions() system_prompt_len = prompt.get_prompt_token_len() input_len = len(msg.body) if msg.msg_type == AgentMsgType.TYPE_GROUPMSG: - history_prmpt,history_token_len = await self._get_prompt_from_session_for_groupchat(chatsession,system_prompt_len + function_token_len,input_len) + history_str,history_token_len = await self._get_prompt_from_session_for_groupchat(chatsession,system_prompt_len + function_token_len,input_len) else: - history_prmpt,history_token_len = await self.get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len) - prompt.append(history_prmpt) # chat context + history_str,history_token_len = await self.get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len) + if history_str: + have_known_info = True + known_info_str += history_str + + if have_known_info: + known_info_prompt = AgentPrompt(known_info_str) + prompt.append(known_info_prompt) # chat context prompt.append(msg_prompt) @@ -475,12 +489,18 @@ class AIAgent: return error_resp final_result = task_result.result_str + if final_result is not None: + if final_result[0] == "{": + llm_result = LLMResult.from_json_str(final_result) + else: + llm_result : LLMResult = LLMResult.from_str(final_result) + else: + llm_result = LLMResult() + llm_result.state = "ignore" - llm_result : LLMResult = LLMResult.from_str(final_result) - - # extra_info include the operation about workspace - if llm_result.extra_info is not None: - await workspace.update_state_by_msg(msg,llm_result.extra_info) + final_result = llm_result.resp + + await workspace.exec_op_list(llm_result.op_list) is_ignore = False result_prompt_str = "" @@ -899,38 +919,37 @@ class AIAgent: history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len messages = chatsession.read_history(self.history_len) # read result_token_len = 0 - result_prompt = AgentPrompt() + read_history_msg = 0 - + have_known_info = False + + known_info = "" if chatsession.summary is not None: if len(chatsession.summary) > 1: - result_prompt.messages.append({"role":"user","content":chatsession.summary}) + known_info += f"## 最近交流的总结 \n {chatsession.summary}\n" result_token_len -= len(chatsession.summary) + have_known_info = True + histroy_str = "" for msg in reversed(messages): read_history_msg += 1 dt = datetime.datetime.fromtimestamp(float(msg.create_time)) formatted_time = dt.strftime('%y-%m-%d %H:%M:%S') - - if msg.sender == self.agent_id: - if self.enable_timestamp: - result_prompt.messages.append({"role":"assistant","content":f"(create on {formatted_time}) {msg.body} "}) - else: - result_prompt.messages.append({"role":"assistant","content":msg.body}) - - else: - if self.enable_timestamp: - result_prompt.messages.append({"role":"user","content":f"(create on {formatted_time}) {msg.body} "}) - else: - result_prompt.messages.append({"role":"user","content":msg.body}) + record_str = f"{msg.sender},[{formatted_time}]\n{msg.body}\n" + have_known_info = True + histroy_str = histroy_str + record_str history_len -= len(msg.body) result_token_len += len(msg.body) if history_len < 0: logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.") break - - return result_prompt,result_token_len + + known_info += f"## 最近的沟通记录 \n {histroy_str}\n" + + if have_known_info: + return known_info,result_token_len + return None,0 async def _do_llm_complection(self,prompt:AgentPrompt,inner_functions:dict=None,org_msg:AgentMsg=None) -> ComputeTaskResult: from .compute_kernel import ComputeKernel @@ -952,6 +971,9 @@ class AIAgent: return task_result + async def execute_op_list(self,oplist:list,workspace:WorkspaceEnvironment): + pass + def need_work(self) -> bool: return True diff --git a/src/aios_kernel/agent_base.py b/src/aios_kernel/agent_base.py index 4a75aee..d23b2a1 100644 --- a/src/aios_kernel/agent_base.py +++ b/src/aios_kernel/agent_base.py @@ -1,10 +1,12 @@ import copy +from datetime import datetime import logging from enum import Enum import uuid import time import re import shlex +import json from typing import List from .ai_function import FunctionItem from .compute_task import ComputeTaskResult @@ -78,6 +80,12 @@ class AgentMsg: self.inner_call_chain = [] self.resp_msg = None + @classmethod + def from_json(cls,json_obj:dict) -> 'AgentMsg': + msg = AgentMsg() + + return msg + @classmethod def create_internal_call_msg(self,func_name:str,args:dict,prev_msg_id:str,caller:str): msg = AgentMsg(AgentMsgType.TYPE_INTERNAL_CALL) @@ -216,22 +224,49 @@ class AgentPrompt: return False self.messages = [] for msg in config: - if msg.get("role") == "system": - self.system_message = msg + if msg.get("content"): + if msg.get("role") == "system": + self.system_message = msg + else: + self.messages.append(msg) else: - self.messages.append(msg) + logger.error("prompt message has no content!") return True - + class LLMResult: def __init__(self) -> None: self.state : str = "ignore" self.resp : str = "" self.paragraphs : dict[str,FunctionItem] = [] + self.post_msgs : List[AgentMsg] = [] self.send_msgs : List[AgentMsg] = [] self.calls : List[FunctionItem] = [] self.post_calls : List[FunctionItem] = [] - self.extra_info = None + self.op_list : List[FunctionItem] = [] + + @classmethod + def from_json_str(self,llm_json_str:str) -> 'LLMResult': + r = LLMResult() + if llm_json_str is None: + r.state = "ignore" + return r + if llm_json_str == "ignore": + r.state = "ignore" + return r + + llm_json = json.loads(llm_json_str) + r.state = llm_json.get("state") + r.resp = llm_json.get("resp") + + r.post_msgs = llm_json.get("post_msgs") + r.send_msgs = llm_json.get("send_msgs") + + r.calls = llm_json.get("calls") + r.post_calls = llm_json.get("post_calls") + r.op_list = llm_json.get("op_list") + + return r @classmethod def from_str(self,llm_result_str:str,valid_func:List[str]=None) -> 'LLMResult': @@ -328,11 +363,52 @@ class AgentTodoResult: self.result_state = "error" class AgentTodo: + @classmethod + def from_dict(cls,json_obj:dict) -> 'AgentTodo': + todo = AgentTodo() + if json_obj.get("id") is not None: + todo.todo_id = json_obj.get("id") + todo.parent_id = json_obj.get("parent_id") + todo.title = json_obj.get("title") + todo.detail = json_obj.get("detail") + due_date = json_obj.get("due_date") + if due_date: + todo.due_date = datetime.fromisoformat(due_date).timestamp() + #todo.todo_path = json_obj.get("todo_path") + todo.depend_todo_ids = json_obj.get("depend_todo_ids") + todo.need_check = json_obj.get("need_check") + #todo.result = json_obj.get("result") + #todo.last_check_result = json_obj.get("last_check_result") + todo.worker = json_obj.get("worker") + todo.checker = json_obj.get("checker") + todo.createor = json_obj.get("createor") + #todo.retry_count = json_obj.get("retry_count") + + return todo + + def to_dict(self) -> dict: + result = {} + result["id"] = self.todo_id + result["parent_id"] = self.parent_id + result["title"] = self.title + result["detail"] = self.detail + result["due_date"] = datetime.fromtimestamp(self.due_date).isoformat() + result["depend_todo_ids"] = self.depend_todo_ids + result["need_check"] = self.need_check + result["worker"] = self.worker + result["checker"] = self.checker + result["createor"] = self.createor + result["retry_count"] = self.retry_count + + return result + def __init__(self): self.todo_id = "todo#" + uuid.uuid4().hex self.title = None self.detail = None self.todo_path = None # get parent todo,sub todo by path + self.create_time = time.time() + self.due_date = time.time() + 3600 * 24 * 2 self.depend_todo_ids = [] diff --git a/src/aios_kernel/contact_manager.py b/src/aios_kernel/contact_manager.py index db5fe4f..5cd1d6c 100644 --- a/src/aios_kernel/contact_manager.py +++ b/src/aios_kernel/contact_manager.py @@ -1,6 +1,7 @@ from typing import List import toml - +import time +from datetime import datetime class Contact: def __init__(self, name, phone=None, email=None, telegram=None,added_by=None, tags=[], notes=""): self.name = name @@ -21,7 +22,8 @@ class Contact: "added_by": self.added_by, "tags": self.tags, - "notes": self.notes + "notes": self.notes, + "now" : datetime.now().strftime('%Y-%m-%d %H:%M:%S') } @classmethod diff --git a/src/aios_kernel/workspace_env.py b/src/aios_kernel/workspace_env.py index f2be11b..ff268e0 100644 --- a/src/aios_kernel/workspace_env.py +++ b/src/aios_kernel/workspace_env.py @@ -12,37 +12,58 @@ import sys import os import re import asyncio +import aiofiles from typing import Any,List -import aiofiles.os +import os import chardet from .agent_base import AgentMsg,AgentTodo from .environment import Environment,EnvironmentEvent from .ai_function import AIFunction,SimpleAIFunction +from .storage import AIStorage,ResourceLocation logger = logging.getLogger(__name__) class WorkspaceEnvironment(Environment): def __init__(self, env_id: str) -> None: super().__init__(env_id) - self.root_path = f"./workspace/{env_id}" + myai_path = AIStorage.get_instance().get_myai_dir() + self.root_path = f"{myai_path}/workspace/{env_id}" + if not os.path.exists(self.root_path): + os.makedirs(self.root_path+"/todos") + def set_root_path(self,path:str): self.root_path = path + def get_prompt(self) -> AgentMsg: + return None + + def get_role_prompt(self,role_id:str) -> AgentMsg: + return None + def get_knowledge_base(self) -> str: pass - def exec_op_list(self,oplist:List)->None: + async def exec_op_list(self,oplist:List)->None: + if oplist is None: + return + for op in oplist: if op["op"] == "create": - self.create(op["path"],op["content"]) + return await self.create(op["path"],op["content"]) elif op["op"] == "write": - self.write(op["path"],op["content"],op["mode"]) + return await self.write(op["path"],op["content"],op["mode"]) elif op["op"] == "delete": - self.delete(op["path"]) + return await self.delete(op["path"]) elif op["op"] == "rename": - self.rename(op["path"],op["new_name"]) + return await self.rename(op["path"],op["new_name"]) + elif op["op"] == "mkdir": + return await self.mkdir(op["path"]) + elif op["op"] == "create_todo": + todoObj = AgentTodo.from_dict(op["todo"]) + path = op.get("path") + return await self.create_todo(path,todoObj) else: logger.error(f"execute op list failed: unknown op:{op['op']}") @@ -98,26 +119,44 @@ class WorkspaceEnvironment(Environment): os.remove(file_path) return True + async def mkdir(self,path:str) -> bool: + dir_path = self.root_path + path + os.makedirs(dir_path) + return True + async def rename(self,path:str,new_name:str) -> bool: file_path = self.root_path + path new_path = self.root_path + new_name os.rename(file_path,new_path) return True + async def get_todo_tree(self,path:str = None,deep:int = 4): + if path: + directory_path = self.root_path + "/todos/" + path + else: + directory_path = self.root_path + "/todos" - #easy use functions - async def update_state_by_msg(self, msg: AgentMsg,extra_info:dict) -> None: - # add todo - # update todo status - pass + str_result:str = "/todos\n" + todo_count:int = 0 - async def update_todos(self,oplist:list) -> None: - # add todo - # update todo status - pass + async def scan_dir(directory_path:str,deep:int): + nonlocal str_result + nonlocal todo_count + if deep <= 0: + return + + for entry in os.scandir(directory_path): + is_dir = entry.is_dir() + if not is_dir: + continue + + todo_count = todo_count + 1 + str_result = str_result + f"{' '*(4-deep)}└─{entry.name}\n" + await scan_dir(entry.path,deep-1) + + await scan_dir(directory_path,deep) + return str_result,todo_count - async def get_todo_tree(self,path:str,deep:int = 4) -> str: - pass async def get_todo_by_path(self,path:str) -> AgentTodo: pass @@ -125,8 +164,15 @@ class WorkspaceEnvironment(Environment): async def get_todo(self,id:str) -> AgentTodo: pass - async def save_new_todo(self,path:str,todo:AgentTodo) -> None: - pass + async def create_todo(self,path:str,todo:AgentTodo) -> None: + if path is None: + dir_path = f"/todos/{todo.title}" + else: + dir_path = f"/todos/{path}/{todo.title}" + + os.makedirs(self.root_path + dir_path) + detail_path = f"{dir_path}/detail" + await self.create(detail_path,json.dumps(todo.to_dict())) async def update_todo(self,path:str,todo:AgentTodo)->None: pass diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index ef18253..9b838fb 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -69,6 +69,7 @@ class AgentManager: the_agent : AIAgent = await self._load_agent_from_media(agent_media_info) if the_agent is None: logger.warn(f"load agent {agent_id} from media failed!") + return None the_agent.chat_db = self.db_path return the_agent