Add implement of Agent Workspace (include a taskmanager system)

This commit is contained in:
Liu Zhicong
2023-12-10 21:42:23 -08:00
parent 662aee7560
commit 3d00095650
10 changed files with 813 additions and 68 deletions
+15 -11
View File
@@ -135,16 +135,20 @@ class AIAgent(BaseAIAgent):
self.owenr_bus = None
self.enable_function_list = None
self.llm_process:Dict[str,BaseLLMProcess] = {}
self.memory : AgentMemory = None
self.prviate_workspace : AgentWorkspace = None
self.behaviors:Dict[str,BaseLLMProcess] = {}
async def initial(self,params:Dict = None):
self.memory = AgentMemory(self.agent_id,self.chat_db)
self.prviate_workspace = AgentWorkspace(self.agent_id)
init_params = {}
init_params["memory"] = self.memory
for process_name in self.llm_process.keys():
init_result = await self.llm_process[process_name].initial(init_params)
init_params["workspace"] = self.prviate_workspace
for process_name in self.behaviors.keys():
init_result = await self.behaviors[process_name].initial(init_params)
if init_result is False:
logger.error(f"llm process {process_name} initial failed! initial return False")
return False
@@ -222,16 +226,16 @@ class AIAgent(BaseAIAgent):
self.history_len = int(config.get("history_len"))
#load all LLMProcess
self.llm_process = {}
LLMProcess = config.get("LLMProcess")
for process_config_name in LLMProcess.keys():
process_config = LLMProcess[process_config_name]
self.behaviors = {}
behaviors = config.get("behavior")
for process_config_name in behaviors.keys():
process_config = behaviors[process_config_name]
real_config = {}
real_config.update(config)
real_config.update(process_config)
load_result = await LLMProcessLoader.get_instance().load_from_config(real_config)
if load_result:
self.llm_process[process_config_name] = load_result
self.behaviors[process_config_name] = load_result
else:
logger.error(f"load LLMProcess {process_config_name} failed!")
return False
@@ -337,7 +341,7 @@ class AIAgent(BaseAIAgent):
input_parms = {
"msg":msg
}
msg_process = self.llm_process.get("message")
msg_process = self.behaviors.get("on_message")
llm_result : LLMResult = await msg_process.process(input_parms)
if llm_result.state == LLMResultStates.ERROR:
error_resp = msg.create_error_resp(llm_result.error_str)
@@ -602,7 +606,7 @@ class AIAgent(BaseAIAgent):
async def _llm_review_unassigned_todos(self,workspace:WorkspaceEnvironment):
pass
async def _llm_read_report(self,report:AgentReport,worksapce:WorkspaceEnvironment):
async def _llm_read_report(self,report,worksapce:WorkspaceEnvironment):
work_summary = worksapce.get_work_summary(self.agent_id)
prompt : LLMPrompt = LLMPrompt()
prompt.append(self.agent_prompt)
+67 -26
View File
@@ -15,6 +15,7 @@ from ..proto.ai_function import *
from .agent_base import *
from .agent_memory import *
from .workspace import *
from ..frame.compute_kernel import *
from ..environment.environment import *
@@ -45,6 +46,19 @@ class BaseLLMProcess(ABC):
self.envs : Dict[str,BaseEnvironment] = []
self.env : CompositeEnvironment = None
def aifunction_to_inner_function(self,all_inner_function:List[AIFunction]) -> List[Dict]:
result_func = []
result_len = 0
for inner_func in all_inner_function:
func_name = inner_func.get_name()
this_func = {}
this_func["name"] = func_name
this_func["description"] = inner_func.get_description()
this_func["parameters"] = inner_func.get_parameters()
result_len += len(json.dumps(this_func)) / 4
result_func.append(this_func)
return result_func
@abstractmethod
async def prepare_prompt(self,input:Dict) -> LLMPrompt:
pass
@@ -54,7 +68,7 @@ class BaseLLMProcess(ABC):
pass
@abstractmethod
async def exec_actions(self,actions:List[ActionItem],input:Dict,llm_result:LLMResult) -> bool:
async def post_llm_process(self,actions:List[ActionItem],input:Dict,llm_result:LLMResult) -> bool:
pass
@abstractmethod
@@ -87,8 +101,9 @@ class BaseLLMProcess(ABC):
def _format_content_by_env_value(self,content:str,env)->str:
return content.format_map(env)
async def _execute_inner_func(self,inner_func_call_node,prompt: LLMPrompt,stack_limit = 5) -> ComputeTaskResult:
async def _execute_inner_func(self,inner_func_call_node,prompt: LLMPrompt,stack_limit = 1) -> ComputeTaskResult:
arguments = None
stack_limit = stack_limit - 1
try:
func_name = inner_func_call_node.get("name")
arguments = json.loads(inner_func_call_node.get("arguments"))
@@ -117,13 +132,18 @@ class BaseLLMProcess(ABC):
task_result.result_code = ComputeTaskResultCode.ERROR
task_result.error_str = f"prompt too long,can not predict"
return task_result
if stack_limit > 0:
inner_functions=prompt.inner_functions
else:
inner_functions = None
task_result: ComputeTaskResult = await (ComputeKernel.get_instance().do_llm_completion(
prompt,
resp_mode=resp_mode,
mode_name=self.model_name,
max_token=max_result_token,
inner_functions=prompt.inner_functions, #NOTICE: inner_function in prompt can be a subset of get_inner_function
inner_functions=inner_functions, #NOTICE: inner_function in prompt can be a subset of get_inner_function
timeout=self.timeout))
if task_result.result_code != ComputeTaskResultCode.OK:
@@ -131,19 +151,15 @@ class BaseLLMProcess(ABC):
return task_result
inner_func_call_node = None
if stack_limit > 0:
result_message : dict = task_result.result.get("message")
if result_message:
inner_func_call_node = result_message.get("function_call")
if inner_func_call_node:
func_msg = copy.deepcopy(result_message)
del func_msg["tool_calls"]#TODO: support tool_calls?
prompt.messages.append(func_msg)
else:
logger.error(f"inner function call stack limit reached")
task_result.result_code = ComputeTaskResultCode.ERROR
task_result.error_str = "inner function call stack limit reached"
return task_result
result_message : dict = task_result.result.get("message")
if result_message:
inner_func_call_node = result_message.get("function_call")
if inner_func_call_node:
func_msg = copy.deepcopy(result_message)
del func_msg["tool_calls"]#TODO: support tool_calls?
prompt.messages.append(func_msg)
if inner_func_call_node:
return await self._execute_inner_func(inner_func_call_node,prompt,stack_limit-1)
@@ -194,7 +210,7 @@ class BaseLLMProcess(ABC):
# use action to save history?
if llm_result.action_list or len(llm_result.action_list) > 0:
await self.exec_actions(llm_result.action_list,input,llm_result)
await self.post_llm_process(llm_result.action_list,input,llm_result)
return llm_result
@@ -213,7 +229,7 @@ class LLMAgentMessageProcess(BaseLLMProcess):
self.enable_inner_functions : Dict[str,bool] = None
self.enable_actions : Dict[str,AIOperation] = None
self.actions_desc : Dict[str,Dict] = None
self.workspace : WorkspaceEnvironment = None
self.workspace : AgentWorkspace = None
self.memory : AgentMemory = None
self.enable_kb = False
@@ -236,7 +252,8 @@ class LLMAgentMessageProcess(BaseLLMProcess):
if self.memory is None:
logger.error(f"LLMAgeMessageProcess initial failed! memory not found")
return False
self.workspace = params.get("workspace")
self.init_actions()
return True
@@ -370,6 +387,8 @@ class LLMAgentMessageProcess(BaseLLMProcess):
### 修改todo/task的action
### workspace提供的额外的action
system_prompt_dict["support_actions"] = await self.get_action_desc()
#prompt.append_system_message(await self.get_action_desc())
## Context (文本替换),是否应该覆盖全部消息
@@ -403,6 +422,9 @@ class LLMAgentMessageProcess(BaseLLMProcess):
#prompt.append_system_message(self.tools_tips)
prompt.inner_functions.extend(self.get_inner_function_desc_from_env())
if self.workspace:
prompt.inner_functions.extend(self.aifunction_to_inner_function(self.workspace.get_inner_function_desc()))
## 给予查询KB的权限
if self.enable_kb:
prompt.inner_functions.extend(self.get_inner_function_desc_from_kb())
@@ -415,9 +437,9 @@ class LLMAgentMessageProcess(BaseLLMProcess):
async def get_inner_function(self,func_name:str) -> AIFunction:
return None
return self.workspace.inner_functions.get(func_name)
async def exec_actions(self,actions:List[ActionItem],input:Dict,llm_result:LLMResult) -> bool:
async def post_llm_process(self,actions:List[ActionItem],input:Dict,llm_result:LLMResult) -> bool:
msg = input.get("msg")
if msg.msg_type == AgentMsgType.TYPE_GROUPMSG:
resp_msg = msg.create_group_resp_msg(self.memory.agent_id,llm_result.resp)
@@ -436,6 +458,7 @@ class LLMAgentMessageProcess(BaseLLMProcess):
action_item.parms["resp_msg"] = resp_msg
action_item.parms["llm_result"] = llm_result
action_item.parms["start_at"] = datetime.now()
action_item.parms["creator"] = self.memory.agent_id
action_item.parms["result"] = await op.execute(action_item.parms)
action_item.parms["end_at"] = datetime.now()
else:
@@ -461,7 +484,25 @@ class ReviewTaskProcess(BaseLLMProcess):
async def get_inner_function(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
async def post_llm_process(self,actions:List[ActionItem]) -> bool:
pass
class QuickReviewTaskProcess(BaseLLMProcess):
def __init__(self) -> None:
super().__init__()
async def load_from_config(self, config: dict) -> Coroutine[Any, Any, bool]:
if await super().load_from_config(config) is False:
return False
async def prepare_prompt(self) -> LLMPrompt:
prompt = LLMPrompt()
pass
async def get_inner_function(self,func_name:str) -> AIFunction:
pass
async def post_llm_process(self,actions:List[ActionItem]) -> bool:
pass
class DoTodoProcess(BaseLLMProcess):
@@ -479,7 +520,7 @@ class DoTodoProcess(BaseLLMProcess):
async def get_inner_function(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
async def post_llm_process(self,actions:List[ActionItem]) -> bool:
pass
@@ -498,7 +539,7 @@ class CheckTodoProcess(BaseLLMProcess):
async def get_inner_function(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
async def post_llm_process(self,actions:List[ActionItem]) -> bool:
pass
class SelfLearningProcess(BaseLLMProcess):
@@ -516,7 +557,7 @@ class SelfLearningProcess(BaseLLMProcess):
async def get_inner_function(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
async def post_llm_process(self,actions:List[ActionItem]) -> bool:
pass
class SelfThinkingProcess(BaseLLMProcess):
@@ -534,7 +575,7 @@ class SelfThinkingProcess(BaseLLMProcess):
async def get_inner_function(self,func_name:str) -> AIFunction:
pass
async def exec_actions(self,actions:List[ActionItem]) -> bool:
async def post_llm_process(self,actions:List[ActionItem]) -> bool:
pass
class LLMProcessLoader:
+357
View File
@@ -0,0 +1,357 @@
from ast import Dict
import json
import sqlite3
import os
from typing import List
import aiofiles
from ..proto.ai_function import *
from ..proto.agent_task import *
from ..storage.storage import *
logger = logging.getLogger(__name__)
class LocalAgentTaskManger(AgentTaskManager):
def __init__(self, owner_id):
super().__init__()
self.root_path = f"{AIStorage.get_instance().get_myai_dir()}/tasklist/{owner_id}"
#self.root_path = os.path.join(workspace, list_type)
if not os.path.exists(self.root_path):
os.makedirs(self.root_path)
self.db_path = os.path.join(self.root_path, "tasklist.db")
self.conn = None
try:
self.conn = sqlite3.connect(self.db_path)
except Exception as e:
logger.error("Error occurred while connecting to database: %s", e)
return None
cursor = self.conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS obj_list (
id TEXT,
path TEXT
)
''')
self.conn.commit()
def _get_obj_path(self,objid:str) -> str:
cursor = self.conn.cursor()
cursor.execute('''
SELECT path FROM obj_list WHERE id = ?
''',(objid,))
row = cursor.fetchone()
if row:
return row[0]
else:
return None
def _save_obj_path(self,objid:str,path:str):
cursor = self.conn.cursor()
cursor.execute('''
INSERT INTO obj_list (id,path) VALUES (?,?)
''',(objid,path))
self.conn.commit()
async def create_task(self,task:AgentTask,parent_id:str = None) -> str:
try:
#perfix = task.task_id[-5]
if parent_id:
parent_path = self._get_obj_path(parent_id)
task_path = f"{parent_path}/{task.title}"
else:
task_path = f"{task.title}"
dir_path = f"{self.root_path}/{task_path}"
os.makedirs(dir_path)
detail_path = f"{dir_path}/detail"
if task.task_path is None:
task.task_path = task_path
self._save_obj_path(task.task_id,task_path)
logger.info("create_task at %s",detail_path)
async with aiofiles.open(detail_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(task.to_dict()))
except Exception as e:
logger.error("create_task failed:%s",e)
return str(e)
return None
async def create_todos(self,owner_task_id:str,todos:List[AgentTodoTask]):
owner_task_path = self._get_obj_path(owner_task_id)
if owner_task_path is None:
return f"owner task {owner_task_id} not found"
try:
step_order = 0
for todo in todos:
todo.step_order = step_order
todo.owner_taskid = owner_task_id
todo_path = f"{self.root_path}/{owner_task_path}/#{step_order} {todo.title}.todo"
self._save_obj_path(todo.todo_id,todo_path)
async with aiofiles.open(todo_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(todo.to_dict()))
logger.info("create_todos at %s OK!",todo_path)
step_order += 1
except Exception as e:
logger.error("create_todos failed:%s",e)
return str(e)
return None
async def append_worklog(self,task:AgentTask,log:AgentWorkLog):
worklog = f"{self.root_path}/{task.task_path}/.worklog"
async with aiofiles.open(worklog, mode='w+', encoding="utf-8") as f:
content = await f.read()
if len(content) > 0:
json_obj = json.loads(content)
else:
json_obj = {}
logs = json_obj.get("logs")
if logs is None:
logs = []
logs.append(log.to_dict())
json_obj["logs"] = logs
await f.write(json.dumps(json_obj))
async def get_worklog(self,obj_id:str)->List[AgentWorkLog]:
obj_path = self._get_obj_path(obj_id)
if obj_path is None:
return []
if obj_path.endswith(".todo"):
dir_path = os.path.dirname(obj_path)
worklog_path = f"{self.root_path}/{dir_path}/.worklog"
else:
worklog_path = f"{self.root_path}/{obj_path}/.worklog"
async with aiofiles.open(worklog_path, mode='r', encoding="utf-8") as f:
content = await f.read()
if len(content) > 0:
json_obj = json.loads(content)
else:
json_obj = {}
logs = json_obj.get("logs")
return logs
async def get_task(self,task_id:str) -> AgentTask:
task_path = self._get_obj_path(task_id)
if task_path is None:
logger.error("get_task:%s,not found!",task_id)
return None
return await self.get_task_by_path(task_path)
async def _get_task_by_fullpath(self,task_fullpath) -> AgentTask:
detail_path = f"{task_fullpath}/detail"
try:
with open(detail_path, mode='r', encoding="utf-8") as f:
task_dict = json.load(f)
result_task:AgentTask = AgentTask.from_dict(task_dict)
if result_task:
relative_path = os.path.relpath(task_fullpath, self.root_path)
result_task.task_path = relative_path
else:
logger.error("_get_task_by_fullpath:%s,parse failed!",detail_path)
return result_task
except Exception as e:
logger.error("_get_task_by_fullpath:%s,failed:%s",task_fullpath,e)
return None
async def get_task_by_path(self,task_path:str) -> AgentTask:
full_path = f"{self.root_path}/{task_path}"
return await self._get_task_by_fullpath(full_path)
async def get_todo(self,todo_id:str) -> AgentTodoTask:
todo_path = self._get_obj_path(todo_id)
if todo_path is None:
logger.error("get_todo:%s,not found!",todo_id)
return None
try:
with open(todo_path, mode='r', encoding="utf-8") as f:
todo_dict = json.load(f)
result_todo:AgentTodoTask = AgentTodoTask.from_dict(todo_dict)
if result_todo:
result_todo.todo_path = todo_path
else:
logger.error("get_todo:%s,parse failed!",todo_path)
return result_todo
except Exception as e:
logger.error("get_todo:%s,failed:%s",todo_path,e)
return None
async def get_sub_tasks(self,task_id:str) -> List[AgentTask]:
task_path = self._get_obj_path(task_id)
if task_path is None:
return []
sub_tasks = []
for sub_item in os.listdir(task_path):
if sub_item.startswith("."):
continue
if sub_item == "workspace":
continue
full_path = os.path.join(task_path, sub_item)
if os.path.isdir(full_path):
sub_task = await self.get_task_by_path(f"{task_path}/{sub_item}")
if sub_task:
sub_tasks.append(sub_task)
pass
async def get_sub_todos(self,task_id:str) -> List[AgentTodoTask]:
task_path = self._get_obj_path(task_id)
if task_path is None:
return []
sub_todos = []
for sub_item in os.listdir(task_path):
if sub_item.startswith("."):
continue
if sub_item == "workspace":
continue
full_path = os.path.join(task_path, sub_item)
if os.path.isfile(full_path) and sub_item.endswith(".todo"):
sub_todo = await self.get_todo_by_path(f"{task_path}/{sub_item}")
if sub_todo:
sub_todos.append(sub_todo)
return sub_todos
#async def get_task_depends(self,task_id:str) -> List[AgentTask]:
# pass
async def list_task(self,filter:dict) -> List[AgentTask]:
directory_path = self.root_path
result_list:List[AgentTask] = []
for entry in os.scandir(directory_path):
if not entry.is_dir():
continue
if entry.name.startswith("."):
continue
if entry.name == "workspace":
continue
task_item = await self.get_task_by_path(entry.path)
if task_item:
if not task_item.is_finish():
result_list.append(task_item)
return result_list
async def update_task(self,task:AgentTask):
detail_path = f"{self.root_path}/{task.task_path}/detail"
try:
async with aiofiles.open(detail_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(task.to_dict()))
except Exception as e:
logger.error("update_task failed:%s",e)
return str(e)
return None
async def update_todo(self,todo:AgentTodoTask):
todo_path = self._get_obj_path(todo.todo_id)
if todo_path is None:
return f"todo {todo.todo_id} not found"
try:
async with aiofiles.open(todo_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(todo.to_dict()))
except Exception as e:
logger.error("update_todo failed:%s",e)
return str(e)
return None
#async def update_task_state(self,task_id,state:str):
# pass
#async def update_todo_state(self,task_id,state:str):
# pass
#todo共享其所在task的文件夹
async def get_task_file(self,task_id:str,path:str)->str:
#return fileid
pass
async def set_task_file(self,task_id:str,path:str,fileid:str):
pass
async def list_task_file(self,task_id:str,path:str):
pass
async def remove_task_file(self,task_id:str,path:str):
pass
class AgentWorkspace:
def __init__(self,owner_agent_id:str) -> None:
self.agent_id : str = owner_agent_id
self.task_mgr : AgentTaskManager = LocalAgentTaskManger(owner_agent_id)
self.actions : Dict[str,ActionItem] = {}
self.inner_functions : Dict[str,AIFunction] = {}
self.init_actions()
self.init_inner_functions()
def init_actions(self):
async def create_task(params):
taskObj = AgentTask.create_by_dict(params)
parent_id = params.get("parent")
return await self.task_mgr.create_task(taskObj,parent_id)
create_task_action = SimpleAIOperation(
"create_task",
"Create a task in the task system, the supported parameters are: title, detail (simple task can not be filled), tags,due_date",
create_task,
)
self.actions[create_task_action.get_name()] = create_task_action
def get_actions(self) -> Dict:
return self.actions
def init_inner_functions(self):
async def list_tasks():
result = {}
fitler = {}
task_list = await self.task_mgr.list_task(fitler)
for task_item in task_list:
result[task_item.task_id] = task_item.title
return json.dumps(result)
self.inner_functions["list_tasks"] = SimpleAIFunction("list_tasks",
"list all tasks in json format like {{$task_id:$task_title}...}",
list_tasks)
def get_inner_function_desc(self) -> List[AIFunction]:
func_list = []
func_list.extend(self.inner_functions.values())
return func_list