Agent can create todo by op_list

This commit is contained in:
Liu Zhicong
2023-11-03 01:16:32 -07:00
parent 5eced91432
commit 1fbe5ae1ea
6 changed files with 237 additions and 53 deletions
+37
View File
@@ -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'
}
}]
}
"""
+48 -26
View File
@@ -129,6 +129,7 @@ class AIAgent:
self.owner_env : Environment = None self.owner_env : Environment = None
self.owenr_bus = None self.owenr_bus = None
self.enable_function_list = None self.enable_function_list = None
@classmethod @classmethod
def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): def create_from_templete(cls,templete:AIAgentTemplete, fullname:str):
@@ -148,6 +149,7 @@ class AIAgent:
logger.error("agent instance_id is None!") logger.error("agent instance_id is None!")
return False return False
self.agent_id = config["instance_id"] self.agent_id = config["instance_id"]
self.agent_workspace = WorkspaceEnvironment(self.agent_id)
if config.get("fullname") is None: if config.get("fullname") is None:
logger.error(f"agent {self.agent_id} fullname is None!") logger.error(f"agent {self.agent_id} fullname is None!")
@@ -412,7 +414,7 @@ class AIAgent:
# return None # return None
def get_workspace_by_msg(self,msg:AgentMsg) -> WorkspaceEnvironment: 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: def need_session_summmary(self,msg:AgentMsg,session:AIChatSession) -> bool:
return False return False
@@ -455,14 +457,26 @@ class AIAgent:
summary = self.llm_select_session_summary(msg,chatsession) summary = self.llm_select_session_summary(msg,chatsession)
prompt.append(AgentPrompt(summary)) 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() inner_functions,function_token_len = self._get_inner_functions()
system_prompt_len = prompt.get_prompt_token_len() system_prompt_len = prompt.get_prompt_token_len()
input_len = len(msg.body) input_len = len(msg.body)
if msg.msg_type == AgentMsgType.TYPE_GROUPMSG: 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: else:
history_prmpt,history_token_len = await self.get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len) history_str,history_token_len = await self.get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len)
prompt.append(history_prmpt) # chat context 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) prompt.append(msg_prompt)
@@ -475,12 +489,18 @@ class AIAgent:
return error_resp return error_resp
final_result = task_result.result_str 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) final_result = llm_result.resp
# extra_info include the operation about workspace await workspace.exec_op_list(llm_result.op_list)
if llm_result.extra_info is not None:
await workspace.update_state_by_msg(msg,llm_result.extra_info)
is_ignore = False is_ignore = False
result_prompt_str = "" result_prompt_str = ""
@@ -899,38 +919,37 @@ class AIAgent:
history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len
messages = chatsession.read_history(self.history_len) # read messages = chatsession.read_history(self.history_len) # read
result_token_len = 0 result_token_len = 0
result_prompt = AgentPrompt()
read_history_msg = 0 read_history_msg = 0
have_known_info = False
known_info = ""
if chatsession.summary is not None: if chatsession.summary is not None:
if len(chatsession.summary) > 1: 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) result_token_len -= len(chatsession.summary)
have_known_info = True
histroy_str = ""
for msg in reversed(messages): for msg in reversed(messages):
read_history_msg += 1 read_history_msg += 1
dt = datetime.datetime.fromtimestamp(float(msg.create_time)) dt = datetime.datetime.fromtimestamp(float(msg.create_time))
formatted_time = dt.strftime('%y-%m-%d %H:%M:%S') formatted_time = dt.strftime('%y-%m-%d %H:%M:%S')
record_str = f"{msg.sender},[{formatted_time}]\n{msg.body}\n"
if msg.sender == self.agent_id: have_known_info = True
if self.enable_timestamp: histroy_str = histroy_str + record_str
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})
history_len -= len(msg.body) history_len -= len(msg.body)
result_token_len += len(msg.body) result_token_len += len(msg.body)
if history_len < 0: if history_len < 0:
logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.") logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.")
break 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: async def _do_llm_complection(self,prompt:AgentPrompt,inner_functions:dict=None,org_msg:AgentMsg=None) -> ComputeTaskResult:
from .compute_kernel import ComputeKernel from .compute_kernel import ComputeKernel
@@ -952,6 +971,9 @@ class AIAgent:
return task_result return task_result
async def execute_op_list(self,oplist:list,workspace:WorkspaceEnvironment):
pass
def need_work(self) -> bool: def need_work(self) -> bool:
return True return True
+81 -5
View File
@@ -1,10 +1,12 @@
import copy import copy
from datetime import datetime
import logging import logging
from enum import Enum from enum import Enum
import uuid import uuid
import time import time
import re import re
import shlex import shlex
import json
from typing import List from typing import List
from .ai_function import FunctionItem from .ai_function import FunctionItem
from .compute_task import ComputeTaskResult from .compute_task import ComputeTaskResult
@@ -78,6 +80,12 @@ class AgentMsg:
self.inner_call_chain = [] self.inner_call_chain = []
self.resp_msg = None self.resp_msg = None
@classmethod
def from_json(cls,json_obj:dict) -> 'AgentMsg':
msg = AgentMsg()
return msg
@classmethod @classmethod
def create_internal_call_msg(self,func_name:str,args:dict,prev_msg_id:str,caller:str): def create_internal_call_msg(self,func_name:str,args:dict,prev_msg_id:str,caller:str):
msg = AgentMsg(AgentMsgType.TYPE_INTERNAL_CALL) msg = AgentMsg(AgentMsgType.TYPE_INTERNAL_CALL)
@@ -216,22 +224,49 @@ class AgentPrompt:
return False return False
self.messages = [] self.messages = []
for msg in config: for msg in config:
if msg.get("role") == "system": if msg.get("content"):
self.system_message = msg if msg.get("role") == "system":
self.system_message = msg
else:
self.messages.append(msg)
else: else:
self.messages.append(msg) logger.error("prompt message has no content!")
return True return True
class LLMResult: class LLMResult:
def __init__(self) -> None: def __init__(self) -> None:
self.state : str = "ignore" self.state : str = "ignore"
self.resp : str = "" self.resp : str = ""
self.paragraphs : dict[str,FunctionItem] = [] self.paragraphs : dict[str,FunctionItem] = []
self.post_msgs : List[AgentMsg] = [] self.post_msgs : List[AgentMsg] = []
self.send_msgs : List[AgentMsg] = [] self.send_msgs : List[AgentMsg] = []
self.calls : List[FunctionItem] = [] self.calls : List[FunctionItem] = []
self.post_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 @classmethod
def from_str(self,llm_result_str:str,valid_func:List[str]=None) -> 'LLMResult': def from_str(self,llm_result_str:str,valid_func:List[str]=None) -> 'LLMResult':
@@ -328,11 +363,52 @@ class AgentTodoResult:
self.result_state = "error" self.result_state = "error"
class AgentTodo: 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): def __init__(self):
self.todo_id = "todo#" + uuid.uuid4().hex self.todo_id = "todo#" + uuid.uuid4().hex
self.title = None self.title = None
self.detail = None self.detail = None
self.todo_path = None # get parent todo,sub todo by path 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 = [] self.depend_todo_ids = []
+4 -2
View File
@@ -1,6 +1,7 @@
from typing import List from typing import List
import toml import toml
import time
from datetime import datetime
class Contact: class Contact:
def __init__(self, name, phone=None, email=None, telegram=None,added_by=None, tags=[], notes=""): def __init__(self, name, phone=None, email=None, telegram=None,added_by=None, tags=[], notes=""):
self.name = name self.name = name
@@ -21,7 +22,8 @@ class Contact:
"added_by": self.added_by, "added_by": self.added_by,
"tags": self.tags, "tags": self.tags,
"notes": self.notes "notes": self.notes,
"now" : datetime.now().strftime('%Y-%m-%d %H:%M:%S')
} }
@classmethod @classmethod
+66 -20
View File
@@ -12,37 +12,58 @@ import sys
import os import os
import re import re
import asyncio import asyncio
import aiofiles
from typing import Any,List from typing import Any,List
import aiofiles.os import os
import chardet import chardet
from .agent_base import AgentMsg,AgentTodo from .agent_base import AgentMsg,AgentTodo
from .environment import Environment,EnvironmentEvent from .environment import Environment,EnvironmentEvent
from .ai_function import AIFunction,SimpleAIFunction from .ai_function import AIFunction,SimpleAIFunction
from .storage import AIStorage,ResourceLocation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WorkspaceEnvironment(Environment): class WorkspaceEnvironment(Environment):
def __init__(self, env_id: str) -> None: def __init__(self, env_id: str) -> None:
super().__init__(env_id) 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): def set_root_path(self,path:str):
self.root_path = path 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: def get_knowledge_base(self) -> str:
pass 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: for op in oplist:
if op["op"] == "create": if op["op"] == "create":
self.create(op["path"],op["content"]) return await self.create(op["path"],op["content"])
elif op["op"] == "write": 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": elif op["op"] == "delete":
self.delete(op["path"]) return await self.delete(op["path"])
elif op["op"] == "rename": 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: else:
logger.error(f"execute op list failed: unknown op:{op['op']}") logger.error(f"execute op list failed: unknown op:{op['op']}")
@@ -98,26 +119,44 @@ class WorkspaceEnvironment(Environment):
os.remove(file_path) os.remove(file_path)
return True 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: async def rename(self,path:str,new_name:str) -> bool:
file_path = self.root_path + path file_path = self.root_path + path
new_path = self.root_path + new_name new_path = self.root_path + new_name
os.rename(file_path,new_path) os.rename(file_path,new_path)
return True 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 str_result:str = "/todos\n"
async def update_state_by_msg(self, msg: AgentMsg,extra_info:dict) -> None: todo_count:int = 0
# add todo
# update todo status
pass
async def update_todos(self,oplist:list) -> None: async def scan_dir(directory_path:str,deep:int):
# add todo nonlocal str_result
# update todo status nonlocal todo_count
pass 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: async def get_todo_by_path(self,path:str) -> AgentTodo:
pass pass
@@ -125,8 +164,15 @@ class WorkspaceEnvironment(Environment):
async def get_todo(self,id:str) -> AgentTodo: async def get_todo(self,id:str) -> AgentTodo:
pass pass
async def save_new_todo(self,path:str,todo:AgentTodo) -> None: async def create_todo(self,path:str,todo:AgentTodo) -> None:
pass 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: async def update_todo(self,path:str,todo:AgentTodo)->None:
pass pass
@@ -69,6 +69,7 @@ class AgentManager:
the_agent : AIAgent = await self._load_agent_from_media(agent_media_info) the_agent : AIAgent = await self._load_agent_from_media(agent_media_info)
if the_agent is None: if the_agent is None:
logger.warn(f"load agent {agent_id} from media failed!") logger.warn(f"load agent {agent_id} from media failed!")
return None
the_agent.chat_db = self.db_path the_agent.chat_db = self.db_path
return the_agent return the_agent