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'
}
}]
}
"""
+46 -24
View File
@@ -130,6 +130,7 @@ class AIAgent:
self.owenr_bus = None
self.enable_function_list = None
@classmethod
def create_from_templete(cls,templete:AIAgentTemplete, fullname:str):
# Agent just inherit from templete on craete,if template changed,agent will not change
@@ -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)
final_result = llm_result.resp
# 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)
await workspace.exec_op_list(llm_result.op_list)
is_ignore = False
result_prompt_str = ""
@@ -899,30 +919,25 @@ 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
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)
@@ -930,7 +945,11 @@ class AIAgent:
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
+80 -4
View File
@@ -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,10 +224,13 @@ 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:
@@ -227,11 +238,35 @@ class LLMResult:
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 = []
+4 -2
View File
@@ -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
+66 -20
View File
@@ -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
@@ -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