Refactor the tunnel-related code to make the logic of the Agent sending messages to the Human more reasonable.

This commit is contained in:
Liu Zhicong
2023-11-08 22:13:18 -08:00
parent 7fd3749a40
commit de685da38b
15 changed files with 652 additions and 232 deletions
+1 -1
View File
@@ -24,5 +24,5 @@ from .stability_node import Stability_ComputeNode
from .local_st_compute_node import LocalSentenceTransformer_Text_ComputeNode,LocalSentenceTransformer_Image_ComputeNode
from .compute_node_config import ComputeNodeConfig
from .ai_function import SimpleAIFunction
AIOS_Version = "0.5.1, build 2023-9-28"
AIOS_Version = "0.5.2, build 2023-11-1"
+153 -46
View File
@@ -12,7 +12,7 @@ import datetime
import copy
import sys
from .agent_base import AgentMsg, AgentMsgStatus, AgentMsgType,FunctionItem,LLMResult,AgentPrompt,AgentReport,AgentTodo,AgentGoal,AgentTodoResult,AgentWorkLog
from .agent_base import AgentMsg, AgentMsgStatus, AgentMsgType,FunctionItem,LLMResult,AgentPrompt,AgentReport,AgentTodo,AgentTodoResult,AgentWorkLog
from .chatsession import AIChatSession
from .compute_task import ComputeTaskResult,ComputeTaskResultCode
from .ai_function import AIFunction
@@ -100,6 +100,8 @@ class AIAgent:
self.agent_energy = 15
self.agent_task = None
self.last_recover_time = time.time()
self.enable_thread = False
self.can_do_unassigned_task = True
self.agent_id:str = None
@@ -158,6 +160,9 @@ class AIAgent:
return False
self.fullname = config["fullname"]
if config.get("enable_thread") is not None:
self.enable_thread = bool(config["enable_thread"])
if config.get("prompt") is not None:
self.agent_prompt = AgentPrompt()
self.agent_prompt.load_from_config(config["prompt"])
@@ -304,7 +309,7 @@ class AIAgent:
prompt.messages.append({"role":"function","content":result_str,"name":func_name})
task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions)
if task_result.result_code != ComputeTaskResultCode.OK:
logger.error(f"llm compute error:{task_result.error_str}")
logger.error(f"_execute_func llm compute error:{task_result.error_str}")
return task_result
ineternal_call_record.result_str = task_result.result_str
@@ -425,6 +430,9 @@ class AIAgent:
def need_session_summmary(self,msg:AgentMsg,session:AIChatSession) -> bool:
return False
async def _create_openai_thread(self) -> str:
return None
async def _process_msg(self,msg:AgentMsg,workspace = None) -> AgentMsg:
msg_prompt = AgentPrompt()
@@ -447,6 +455,19 @@ class AIAgent:
msg_prompt.messages = [{"role":"user","content":msg.body}]
session_topic = msg.get_sender() + "#" + msg.topic
chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
if self.enable_thread:
need_create_thread = False
if chatsession.openai_thread_id is not None:
if len(chatsession.openai_thread_id) < 1:
need_create_thread = True
else:
need_create_thread = True
if need_create_thread:
openai_thread_id = await self._create_openai_thread()
if openai_thread_id is not None:
chatsession.update_openai_thread_id(openai_thread_id)
workspace = self.get_workspace_by_msg(msg)
@@ -544,6 +565,7 @@ class AIAgent:
async def _get_history_prompt_for_think(self,chatsession:AIChatSession,summary:str,system_token_len:int,pos:int)->(AgentPrompt,int):
history_len = (self.max_token_size * 0.7) - system_token_len
messages = chatsession.read_history(self.history_len,pos,"natural") # read
@@ -663,43 +685,65 @@ class AIAgent:
workspace : WorkspaceEnvironment = self.get_workspace_by_msg(None)
logger.info(f"agent {self.agent_id} do my work start!")
# review todo能更整体的思考一次todo的优先级
if await self.need_review_todos():
await self._llm_review_todos(workspace)
# review todolist
#if await self.need_review_todolist():
# await self._llm_review_todolist(workspace)
todo_list = await workspace.get_todo_list(self.agent_id)
check_count = 0
do_count = 0
for todo in todo_list:
if self.agent_energy <= 0:
break
if await self.can_do(todo,workspace) is False:
continue
if todo.retry_count < 2:
need_think_todo_from_goal = False
await self._llm_do(todo,workspace)
if await self.need_review_todo(todo,workspace):
review_result = await self._llm_review_todo(todo,workspace)
todo.last_review_time = datetime.datetime.now().timestamp()
elif await self.can_check(todo,workspace):
check_result : AgentTodoResult = await self._llm_check_todo(todo,workspace)
todo.last_check_time = datetime.datetime.now().timestamp()
match check_result.result_code:
case AgentTodoResult.TODO_RESULT_CODE_LLM_ERROR:
continue
case AgentTodoResult.TODO_RESULT_CODE_OK:
await workspace.update_todo(todo.todo_id,AgentTodo.TODO_STATE_DONE)
case AgentTodoResult.TODO_RESULT_CODE_EXEC_OP_ERROR:
await workspace.update_todo(todo.todo_id,AgentTodo.TDDO_STATE_CHECKFAILED)
await workspace.append_worklog(todo,check_result)
self.agent_energy -= 1
if todo.state == "done":
await self._llm_check_todo(todo,workspace)
self.agent_energy -= 1
logger.info(f"agent {self.agent_id} do my work done!")
check_count += 1
elif await self.can_do(todo,workspace):
do_result : AgentTodoResult = await self._llm_do(todo,workspace)
todo.last_do_time = datetime.datetime.now().timestamp()
todo.retry_count += 1
def get_review_todo_prompt(self) -> AgentPrompt:
match do_result.result_code:
case AgentTodoResult.TODO_RESULT_CODE_LLM_ERROR:
continue
case AgentTodoResult.TODO_RESULT_CODE_OK:
await workspace.update_todo(todo.todo_id,AgentTodo.TODO_STATE_WAITING_CHECK)
case AgentTodoResult.TODO_RESULT_CODE_EXEC_OP_ERROR:
await workspace.update_todo(todo.todo_id,AgentTodo.TODO_STATE_EXEC_FAILED)
await workspace.append_worklog(todo,do_result)
self.agent_energy -= 2
do_count += 1
logger.info(f"agent {self.agent_id} ,check:{check_count} todo,do:{do_count} todo.")
def get_review_todo_prompt(self,todo:AgentTodo) -> AgentPrompt:
return self.review_todo_prompt
async def need_review_todos(self) -> bool:
if self.get_review_todo_prompt() is None:
return False
return True
async def _llm_review_todos(self,workspace:WorkspaceEnvironment):
async def _llm_review_todo(self,todo:AgentTodo,workspace:WorkspaceEnvironment):
prompt = AgentPrompt()
prompt.append(workspace.get_prompt())
prompt.append(workspace.get_role_prompt(self.agent_id))
prompt.append(self.get_review_todo_prompt())
prompt.append(self.get_review_todo_prompt(todo))
todo_tree = workspace.get_todo_tree("/")
prompt.append(AgentPrompt(todo_tree))
@@ -712,49 +756,105 @@ class AIAgent:
return
def get_do_prompt(self,todo_type:str=None) -> AgentPrompt:
def get_do_prompt(self,todo:AgentTodo) -> AgentPrompt:
return self.do_prompt
def get_prompt_from_todo(self,todo:AgentTodo) -> AgentPrompt:
json_str = json.dumps(todo.raw_obj)
return AgentPrompt(json_str)
async def need_review_todo(self,todo:AgentTodo,workspace:WorkspaceEnvironment) -> bool:
return False
async def can_check(self,todo:AgentTodo,workspace:WorkspaceEnvironment) -> bool:
if self.get_check_prompt(todo) is None:
return False
if todo.can_check() is False:
return False
if todo.checker is not None:
if todo.checker != self.agent_id:
return False
else:
if self.can_do_unassigned_task is False:
return False
else:
todo.checker = self.agent_id
return True
async def can_do(self,todo:AgentTodo,workspace:WorkspaceEnvironment) -> bool:
return todo.can_do()
if todo.can_do() is False:
return False
if todo.worker is not None:
if todo.worker != self.agent_id:
return False
else:
if self.can_do_unassigned_task is False:
return False
else:
todo.worker = self.agent_id
return True
async def _llm_do(self,todo:AgentTodo,workspace:WorkspaceEnvironment) -> AgentTodoResult:
result = AgentTodoResult()
prompt : AgentPrompt = AgentPrompt()
#prompt.append(self.agent_prompt)
prompt.append(workspace.get_role_prompt(self.agent_id))
do_prompt = workspace.get_do_prompt()
do_prompt = workspace.get_do_prompt(todo)
if do_prompt is None:
do_prompt = self.get_do_prompt()
do_prompt = self.get_do_prompt(todo)
prompt.append(do_prompt)
# 有通用的todo执行方法,也有定制的,针对特定类型TODO更高效的执行方法
# 根据经验,Agent可以自主掌握/整理更多类型的TODO的执行方法
# There are general methods for executing todos, as well as customized ones that are more efficient for specific types of TODOS.
# Based on experience, an Agent can autonomously master/organize execution methods for a greater variety of TODO types.
#prompt.append(do_log_prompt)
#prompt.append(work_log_prompt)
prompt.append(self.get_prompt_from_todo(todo))
task_result:ComputeTaskResult = await self._do_llm_complection(prompt)
if task_result.error_str is not None:
logger.error(f"_llm_do compute error:{task_result.error_str}")
result.result_code = AgentTodoResult.TODO_RESULT_CODE_LLM_ERROR
result.error_str = task_result.error_str
return result
llm_result = LLMResult.from_str(task_result.result_str)
# result_str is the explain of how to do this todo
result.result_str = llm_result.resp
result.op_list = llm_result.op_list
if llm_result.post_msgs is not None:
for msg in llm_result.post_msgs:
msg.sender = self.agent_id
msg.topic = f"{todo.title}##{todo.todo_id}"
#msg.prev_msg_id = todo.todo_id
chatsession = AIChatSession.get_session(self.agent_id,f"{msg.target}#{msg.topic}",self.chat_db)
chatsession.append(msg)
resp = await AIBus.get_default_bus().post_message(msg)
logging.info(f"agent {self.agent_id} send msg to {msg.target} result:{resp}")
await workspace.exec_op_list(llm_result.op_list,self.agent_id)
await workspace.append_do_result(self.agent_id,llm_result)
return task_result
op_errors,have_error = await workspace.exec_op_list(llm_result.op_list,self.agent_id)
if have_error:
result.result_code = AgentTodoResult.TODO_RESULT_CODE_EXEC_OP_ERROR
#result.error_str = error_str
return result
return result
def get_check_prompt(self) -> AgentPrompt:
async def append_toddo_result(self,todo,worksapce,llm_result,result_str):
pass
def get_check_prompt(self,todo:AgentTodo) -> AgentPrompt:
return self.check_prompt
async def _llm_check_todo(self, todo:AgentTodo,workspace:WorkspaceEnvironment) -> bool:
async def _llm_check_todo(self, todo:AgentTodo,workspace:WorkspaceEnvironment) :
if self.get_check_prompt(todo) is None:
return True
return None
prompt : AgentPrompt = AgentPrompt()
prompt.append(self.agent_prompt)
@@ -920,7 +1020,7 @@ class AIAgent:
#3) llm summarize chat history
task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,None)
if task_result.result_code != ComputeTaskResultCode.OK:
logger.error(f"llm compute error:{task_result.error_str}")
logger.error(f"think_chatsession llm compute error:{task_result.error_str}")
break
else:
new_summary= task_result.result_str
@@ -930,6 +1030,8 @@ class AIAgent:
async def get_prompt_from_session(self,chatsession:AIChatSession,system_token_len,input_token_len) -> AgentPrompt:
# TODO: get prompt from group chat is different from single chat
if self.enable_thread:
return None
history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len
messages = chatsession.read_history(self.history_len) # read
@@ -971,7 +1073,7 @@ class AIAgent:
#logger.debug(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len}, totoal prompt:{system_prompt_len + function_token_len + history_token_len} ")
task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions)
if task_result.result_code != ComputeTaskResultCode.OK:
logger.error(f"llm compute error:{task_result.error_str}")
logger.error(f"_do_llm_complection llm compute error:{task_result.error_str}")
#error_resp = msg.create_error_resp(task_result.error_str)
return task_result
@@ -987,7 +1089,15 @@ class AIAgent:
return task_result
def need_work(self) -> bool:
return True
if self.do_prompt is not None:
return True
if self.check_prompt is not None:
return True
if self.agent_energy > 2:
return True
return False
def need_self_think(self) -> bool:
return False
@@ -1014,28 +1124,25 @@ class AIAgent:
self.agent_energy += (now - self.last_recover_time) / 60
self.last_recover_time = now
if self.agent_energy <= 1:
continue
# complete todo
# complete & check todo
if self.need_work():
await self.do_my_work()
# review other's todo
# self.review_other_works()
# do work summary
if self.need_self_think():
await self.do_self_think()
#
if self.need_self_learn():
await self.do_self_learn()
except Exception as e:
tb_str = traceback.format_exc()
logger.error(f"agent {self.agent_id} on timer error:{tb_str},{e}")
logger.error(f"agent {self.agent_id} on timer error:{e},{tb_str}")
continue
+118 -29
View File
@@ -258,8 +258,16 @@ class LLMResult:
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")
post_msgs = llm_json.get("post_msg")
r.post_msgs = []
if post_msgs:
for msg in post_msgs:
new_msg = AgentMsg()
target_id = msg.get("target")
msg_content = msg.get("content")
new_msg.set("",target_id,msg_content)
r.post_msgs.append(new_msg)
#new_msg.msg_type = AgentMsgType.TYPE_MSG
r.calls = llm_json.get("calls")
r.post_calls = llm_json.get("post_calls")
@@ -352,23 +360,44 @@ class LLMResult:
return r
class AgentGoal:
def __init__(self) -> None:
self.description = None
class AgentReport:
def __init__(self):
pass
class AgentTodoResult:
TODO_RESULT_CODE_OK = 0,
TODO_RESULT_CODE_LLM_ERROR = 1,
TODO_RESULT_CODE_EXEC_OP_ERROR = 2
def __init__(self) -> None:
self.result_state = "error"
self.result_code = AgentTodoResult.TODO_RESULT_CODE_OK
self.result_str = None
self.error_str = None
self.op_list = None
def to_dict(self) -> dict:
result = {}
result["result_code"] = self.result_code
result["result_str"] = self.result_str
result["error_str"] = self.error_str
result["op_list"] = self.op_list
return result
class AgentTodo:
TODO_STATE_WAIT_ASSIGN = "wait_assign"
TODO_STATE_INIT = "init"
TODO_STATE_PENDING = "pending"
TODO_STATE_WAITING_CHECK = "wait_check"
TODO_STATE_EXEC_FAILED = "exec_failed"
TDDO_STATE_CHECKFAILED = "check_failed"
TODO_STATE_CASNCEL = "cancel"
TODO_STATE_DONE = "done"
TODO_STATE_EXPIRED = "expired"
def __init__(self):
self.todo_id = "todo#" + uuid.uuid4().hex
self.title = None
@@ -376,20 +405,23 @@ class AgentTodo:
self.todo_path = None # get parent todo,sub todo by path
#self.parent = None
self.create_time = time.time()
self.due_date = time.time() + 3600 * 24 * 2
self.state = "pending"
self.depend_todo_ids = []
self.sub_todos = {}
self.need_check = True
self.result : ComputeTaskResult = None
self.last_check_result = None
self.state = "wait_assign"
self.worker = None
self.checker = None
self.createor = None
self.need_check = True
self.due_date = time.time() + 3600 * 24 * 2
self.last_do_time = None
self.last_check_time = None
self.last_review_time = None
self.depend_todo_ids = []
self.sub_todos = {}
self.result : AgentTodoResult = None
self.last_check_result = None
self.retry_count = 0
self.raw_obj = None
@@ -400,14 +432,26 @@ class AgentTodo:
todo.todo_id = json_obj.get("id")
todo.title = json_obj.get("title")
todo.state = json_obj.get("state")
create_time = json_obj.get("create_time")
if create_time:
todo.create_time = datetime.fromisoformat(create_time).timestamp()
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")
last_do_time = json_obj.get("last_do_time")
if last_do_time:
todo.last_do_time = datetime.fromisoformat(last_do_time).timestamp()
last_check_time = json_obj.get("last_check_time")
if last_check_time:
todo.last_check_time = datetime.fromisoformat(last_check_time).timestamp()
last_review_time = json_obj.get("last_review_time")
if last_review_time:
todo.last_review_time = datetime.fromisoformat(last_review_time).timestamp()
todo.depend_todo_ids = json_obj.get("depend_todo_ids")
todo.need_check = json_obj.get("need_check")
#todo.result = json_obj.get("result")
@@ -423,14 +467,21 @@ class AgentTodo:
return todo
def to_dict(self) -> dict:
result = {}
if self.raw_obj:
result = self.raw_obj
else:
result = {}
result["id"] = self.todo_id
result["todo_path"] = self.todo_path
#result["parent_id"] = self.parent_id
result["title"] = self.title
result["state"] = self.state
result["create_time"] = datetime.fromtimestamp(self.create_time).isoformat()
result["detail"] = self.detail
result["due_date"] = datetime.fromtimestamp(self.due_date).isoformat()
result["last_do_time"] = datetime.fromtimestamp(self.last_do_time).isoformat() if self.last_do_time else None
result["last_check_time"] = datetime.fromtimestamp(self.last_check_time).isoformat() if self.last_check_time else None
result["last_review_time"] = datetime.fromtimestamp(self.last_review_time).isoformat() if self.last_review_time else None
result["depend_todo_ids"] = self.depend_todo_ids
result["need_check"] = self.need_check
result["worker"] = self.worker
@@ -439,19 +490,57 @@ class AgentTodo:
result["retry_count"] = self.retry_count
return result
def can_do(self) -> bool:
for sub_todo in self.sub_todos.values():
if sub_todo.state:
if sub_todo.state != "done":
return False
def can_check(self)->bool:
if self.state != AgentTodo.TODO_STATE_WAITING_CHECK:
return False
now = datetime.now().timestamp()
time_diff = now - self.due_date
if time_diff > 7*24*3600:
return False
if self.last_check_time:
time_diff = now - self.last_check_time
if time_diff < 60*15:
logger.info(f"todo {self.title} is already checked, ignore")
return False
return True
def can_do(self) -> bool:
match self.state:
case AgentTodo.TODO_STATE_DONE:
logger.info(f"todo {self.title} is done, ignore")
return False
case AgentTodo.TODO_STATE_CASNCEL:
logger.info(f"todo {self.title} is cancel, ignore")
return False
case AgentTodo.TODO_STATE_EXPIRED:
logger.info(f"todo {self.title} is expired, ignore")
return False
case AgentTodo.TODO_STATE_EXEC_FAILED:
if self.retry_count > 3:
logger.info(f"todo {self.title} retry count ({self.retry_count}) is too many, ignore")
return False
now = datetime.now().timestamp()
time_diff = self.due_date - now
if time_diff < 0:
logger.info(f"todo {self.title} is expired, ignore")
self.state = AgentTodo.TODO_STATE_EXPIRED
return False
if time_diff > 7*24*3600:
logger.info(f"todo {self.title} is far before due date, ignore")
return False
if self.last_do_time:
time_diff = now - self.last_do_time
if time_diff < 60*15:
logger.info(f"todo {self.title} is already do ignore")
return False
logger.info(f"todo {self.title} can do.")
return True
class AgentWorkLog:
def __init__(self) -> None:
pass
+3
View File
@@ -109,6 +109,9 @@ class AIBus:
# means sub
def register_message_handler(self,handler_name:str,handler:Any) -> Queue:
handler_node = AIBusHandler(handler,self)
if self.handlers.get(handler_name) is not None:
logger.warn(f"handler {handler_name} already register on AI_BUS!")
self.handlers[handler_name] = handler_node
return handler_node.queue
+28 -5
View File
@@ -52,7 +52,8 @@ class ChatSessionDB:
SessionTopic TEXT,
StartTime TEXT,
SummarizePos INTEGER,
Summary TEXT
Summary TEXT,
ThreadID TEXT
);
""")
@@ -89,14 +90,14 @@ class ChatSessionDB:
except Error as e:
logging.error("Error occurred while creating tables: %s", e)
def insert_chatsession(self, session_id, session_owner,session_topic, start_time):
def insert_chatsession(self, session_id, session_owner,session_topic, start_time,thread_id = ""):
""" insert a new session into the ChatSessions table """
try:
conn = self._get_conn()
conn.execute("""
INSERT INTO ChatSessions (SessionID, SessionOwner,SessionTopic, StartTime,SummarizePos,Summary)
VALUES (?,?, ?, ?,0,"")
""", (session_id, session_owner,session_topic, start_time))
INSERT INTO ChatSessions (SessionID, SessionOwner,SessionTopic, StartTime,SummarizePos,Summary,ThreadID)
VALUES (?,?, ?, ?,0,"",?)
""", (session_id, session_owner,session_topic, start_time,thread_id))
conn.commit()
return 0 # return 0 if successful
except Error as e:
@@ -253,6 +254,21 @@ class ChatSessionDB:
except Error as e:
logging.error("Error occurred while updating session summary: %s", e)
return -1
def update_session_thread_id(self, session_id, thread_id):
""" update the threadid of a session """
try:
conn = self._get_conn()
conn.execute("""
UPDATE ChatSessions
SET ThreadID = ?
WHERE SessionID = ?
""", (thread_id, session_id))
conn.commit()
return 0 # return 0 if successful
except Error as e:
logging.error("Error occurred while updating session threadid: %s", e)
return -1
# chat session store the chat history between owner and agent
# chat session might be large, so can read / write at stream mode.
@@ -286,6 +302,7 @@ class AIChatSession:
result.topic = session_topic
result.summarize_pos = session[4]
result.summary = session[5]
result.openai_thread_id = session[6]
return result
@@ -305,6 +322,7 @@ class AIChatSession:
result.topic = session[2]
result.summarize_pos = session[4]
result.summary = session[5]
result.openai_thread_id = session[6]
return result
@@ -331,6 +349,7 @@ class AIChatSession:
self.start_time : str = None
self.summarize_pos : int = 0
self.summary = None
self.openai_thread_id = None
def get_owner_id(self) -> str:
return self.owner_id
@@ -376,6 +395,10 @@ class AIChatSession:
self.summarize_pos = progress
self.summary = new_summary
def update_openai_thread_id(self,thread_id:str) -> None:
self.db.update_session_thread_id(self.session_id,thread_id)
self.openai_thread_id = thread_id
#def attach_event_handler(self,handler) -> None:
# """chat session changed event handler"""
# pass
+84
View File
@@ -0,0 +1,84 @@
from typing import List
import logging
from datetime import datetime
from .agent_base import AgentMsg
from .tunnel import AgentTunnel
logger = logging.getLogger(__name__)
class Contact:
def __init__(self, name, phone=None, email=None, telegram=None,added_by=None, tags=[], notes=""):
self.name = name
self.phone = phone
self.email = email
self.telegram = telegram
self.added_by = added_by
self.tags = tags
self.notes = notes
self.is_family_member = False
self.active_tunnels = {}
def to_dict(self):
return {
"name": self.name,
"phone": self.phone,
"email": self.email,
"telegram" : self.telegram,
"added_by": self.added_by,
"tags": self.tags,
"notes": self.notes,
"now" : datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
async def _process_msg(self,msg:AgentMsg):
tunnel : AgentTunnel = self.get_active_tunnel(msg.sender)
if tunnel is not None:
await tunnel.post_message(msg)
return None
else:
tunnel = await self.create_default_tunnel(msg.sender)
if tunnel is not None:
self.active_tunnels[msg.sender] = tunnel
await tunnel.post_message(msg)
return None
logger.warn(f"contact {self.name} cann't get tunnel,post message failed!")
def get_active_tunnel(self,agent_id) -> AgentTunnel:
tunnel = self.active_tunnels.get(agent_id)
return tunnel
def set_active_tunnel(self,agent_id,tunnel:AgentTunnel):
self.active_tunnels[agent_id] = tunnel
async def create_default_tunnel(self,agent_id:str) -> AgentTunnel:
from .email_tunnel import EmailTunnel
result_tunnels = AgentTunnel.get_tunnel_by_agentid(agent_id)
for tunnel in result_tunnels:
if isinstance(tunnel,EmailTunnel):
return tunnel
return None
@classmethod
def from_dict(cls, data):
return Contact(data.get("name"), data.get("phone"), data.get("email"), data.get("telegram"),data.get("added_by"), data.get("tags"), data.get("notes"))
class FamilyMember(Contact):
def __init__(self, name, relationship,phone=None, email=None,telegram=None):
super().__init__(name, phone, email, telegram)
self.name = name
self.relationship = relationship
self.is_family_member = True
def to_dict(self):
result = super().to_dict()
result["relationship"] = self.relationship
return result
@classmethod
def from_dict(cls, data):
return FamilyMember(data.get("name"),data.get("relationship"),data.get("phone"), data.get("email"),data.get("telegram"))
+8 -41
View File
@@ -2,49 +2,13 @@ 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
self.phone = phone
self.email = email
self.telegram = telegram
self.added_by = added_by
self.tags = tags
self.notes = notes
self.is_family_member = False
from .agent_base import AgentMsg
from .tunnel import AgentTunnel
from .contact import Contact,FamilyMember
import logging
def to_dict(self):
return {
"name": self.name,
"phone": self.phone,
"email": self.email,
"telegram" : self.telegram,
logger = logging.getLogger(__name__)
"added_by": self.added_by,
"tags": self.tags,
"notes": self.notes,
"now" : datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
@classmethod
def from_dict(cls, data):
return Contact(data.get("name"), data.get("phone"), data.get("email"), data.get("telegram"),data.get("added_by"), data.get("tags"), data.get("notes"))
class FamilyMember(Contact):
def __init__(self, name, relationship,phone=None, email=None,telegram=None):
super().__init__(name, phone, email, telegram)
self.name = name
self.relationship = relationship
self.is_family_member = True
def to_dict(self):
result = super().to_dict()
result["relationship"] = self.relationship
return result
@classmethod
def from_dict(cls, data):
return FamilyMember(data.get("name"),data.get("relationship"),data.get("phone"), data.get("email"),data.get("telegram"))
class ContactManager:
_instance = None
@@ -154,6 +118,9 @@ class ContactManager:
def list_family_members(self):
return self.family_members
#def register_to_ai_bus(self, ai_bus:AIBus):
# ai_bus.register_message_handler("contact_manager", self.process_msg)
#async def process_msg(self,msg:AgentMsg):
# # forword message to contact
+34 -3
View File
@@ -9,6 +9,7 @@ import time
import datetime
from .tunnel import AgentTunnel
from .agent_base import AgentMsg
from .contact_manager import ContactManager
from email.message import EmailMessage
@@ -30,7 +31,7 @@ class EmailTunnel(AgentTunnel):
self.target_id = config["target"]
self.tunnel_id = config["tunnel_id"]
self.type = "TelegramTunnel"
self.type = "EmailTunnel"
self.email = config["email"]
self.imap_server = config["imap"]
s = self.imap_server.split(":")
@@ -46,8 +47,10 @@ class EmailTunnel(AgentTunnel):
self.login_user = config["user"]
self.login_password = config["password"]
self.folder = config["folder"]
self.check_interval = config["interval"]
if config.get("folder") is not None:
self.folder = config["folder"]
if config.get("interval") is not None:
self.check_interval = config["interval"]
return True
@@ -55,6 +58,8 @@ class EmailTunnel(AgentTunnel):
super().__init__()
self.is_start = False
self.read_email = {}
self.folder = "INBOX"
self.check_interval = 60
async def on_new_email(self,mail:mailparser.MailParser) -> None:
remote_email_addr = mail.from_[0][1]
@@ -86,7 +91,32 @@ class EmailTunnel(AgentTunnel):
password=self.login_password,
)
async def post_message(self, msg: AgentMsg) -> None:
cm = ContactManager.get_instance()
contact = cm.find_contact_by_name(msg.target)
if contact is None:
logger.error(f"can't find contact {msg.target} , post message through email_tunnel failed!")
return
target_email = contact.email
if target_email is None:
logger.error(f"contact {msg.target} has no email, post message through email_tunnel failed!")
return
email_msg = EmailMessage()
email_msg['Subject'] = f"{msg.topic},From AIAgent {msg.sender}"
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()
@@ -141,4 +171,5 @@ class EmailTunnel(AgentTunnel):
async def _process_message(self, msg: AgentMsg) -> None:
logger.warn(f"process message {msg.msg_id} from {msg.sender} to {msg.target}")
+47 -5
View File
@@ -21,7 +21,8 @@ from .agent_base import AgentMsg,AgentMsgType
logger = logging.getLogger(__name__)
class TelegramTunnel(AgentTunnel):
all_bots = {}
default_chatid = {}
@classmethod
def register_to_loader(cls):
async def load_tg_tunnel(config:dict) -> AgentTunnel:
@@ -55,6 +56,7 @@ class TelegramTunnel(AgentTunnel):
self.update_queue = None
self.allow_group = "contact"
self.in_process_tg_msg = {}
self.chatid_record = {}
async def _do_process_raw_message(self,bot: Bot, update_id: int) -> int:
# Request updates after the last update_id
@@ -81,6 +83,7 @@ class TelegramTunnel(AgentTunnel):
self.update_queue = asyncio.Queue()
self.bot_updater = Updater(self.bot,update_queue=self.update_queue)
TelegramTunnel.all_bots[self.target_id] = self.bot
async def _run_app():
try:
@@ -110,8 +113,41 @@ class TelegramTunnel(AgentTunnel):
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 _process_message(self, msg: AgentMsg) -> bool:
logger.warn(f"tg_tunnel process message {msg.msg_id} from agent {msg.sender} to human {msg.target}")
# async def _process_message(self, msg: AgentMsg) -> bool:
# logger.info(f"tg_tunnel process message {msg.msg_id} from agent {msg.sender} to human {msg.target}")
# cm = ContactManager.get_instance()
# contact = cm.find_contact_by_name(msg.target)
# bot = TelegramTunnel.all_bots.get(msg.sender)
# chatid_index = f"{self.target_id}#{msg.target}"
# chatid = TelegramTunnel.default_chatid.get(chatid_index)
# if chatid is None:
# logger.warning(f"tg_tunnel process message {msg.msg_id} from agent {msg.sender} to human {msg.target} failed! chatid not found!")
# return None
# if bot is None:
# logger.warning(f"tg_tunnel process message {msg.msg_id} from agent {msg.sender} to human {msg.target} failed! bot not found!")
# return None
# if contact:
# if contact.telegram:
# await bot.send_message(chat_id=chatid,text=msg.body)
# logging.info(f"tg_tunnel send message {msg.msg_id} from agent {msg.sender} to human {msg.target} @ chatid:{chatid}success!")
# return None
# logger.warning(f"tg_tunnel process message {msg.msg_id} from agent {msg.sender} to human {msg.target} failed! contact not found!")
# return None
async def post_message(self, msg: AgentMsg) -> None:
chatid = self.chatid_record.get(msg.target)
if chatid:
# TODO: support image and audio
await self.bot.send_message(chat_id=chatid,text=msg.body)
logging.info(f"tg_tunnel send message {msg.msg_id} from agent {msg.sender} to human {msg.target} @ chatid:{chatid}success!")
else:
logger.warning(f"tg_tunnel process message {msg.msg_id} from agent {msg.sender} to human {msg.target} failed! chatid not found!")
async def conver_tg_msg_to_agent_msg(self,message:Message) -> AgentMsg:
@@ -189,6 +225,8 @@ class TelegramTunnel(AgentTunnel):
if contact is not None:
reomte_user_name = contact.name
#TelegramTunnel.default_chatid[f"{self.target_id}#{reomte_user_name}"] = update.effective_chat.id
if not contact.is_family_member:
if self.allow_group != "contact" and self.allow_group !="guest":
await update.message.reply_text(f"You're not supposed to talk to me! Please contact my father~")
@@ -209,7 +247,11 @@ class TelegramTunnel(AgentTunnel):
contact.added_by = self.target_id
cm.add_contact(contact.name, contact)
reomte_user_name = contact.name
if contact is not None:
contact.set_active_tunnel(self.target_id,self)
self.chatid_record[reomte_user_name] = update.effective_chat.id
self.ai_bus.register_message_handler(reomte_user_name,contact._process_msg)
agent_msg.sender = reomte_user_name
logger.info(f"process message {agent_msg.msg_id} from {agent_msg.sender} to {agent_msg.target}")
@@ -217,7 +259,7 @@ class TelegramTunnel(AgentTunnel):
self.ai_bus.register_message_handler(agent_msg.target, self._process_message)
resp_msg = await self.ai_bus.send_message(agent_msg,self.target_id,agent_msg.target)
else:
self.ai_bus.register_message_handler(reomte_user_name, self._process_message)
#self.ai_bus.register_message_handler(reomte_user_name, self._process_message)
resp_msg = await self.ai_bus.send_message(agent_msg)
#await bot.send_chat_action(chat_id=update.effective_chat.id, action="typing")
+11
View File
@@ -54,6 +54,14 @@ class AgentTunnel(ABC):
return False
@classmethod
async def get_tunnel_by_agentid(cls,agent_id:str):
result = []
for tunnel in cls._all_tunnels.values():
if tunnel.target_id == agent_id:
result.append(tunnel)
return result
def __init__(self) -> None:
super().__init__()
@@ -75,6 +83,9 @@ class AgentTunnel(ABC):
self.ai_bus = ai_bus
self.is_connected = True
@abstractmethod
def post_message(self, msg: AgentMsg) -> None:
pass
@abstractmethod
+135 -90
View File
@@ -17,7 +17,7 @@ from typing import Any,List
import os
import chardet
from .agent_base import AgentMsg,AgentTodo,AgentPrompt
from .agent_base import AgentMsg,AgentTodo,AgentPrompt,AgentTodoResult
from .environment import Environment,EnvironmentEvent
from .ai_function import AIFunction,SimpleAIFunction
from .storage import AIStorage,ResourceLocation
@@ -47,40 +47,53 @@ class WorkspaceEnvironment(Environment):
def get_knowledge_base(self) -> str:
pass
def get_do_prompt(self,todo_type:str=None)->AgentPrompt:
def get_do_prompt(self,todo:AgentTodo=None)->AgentPrompt:
return None
async def exec_op_list(self,oplist:List,agent_id:str)->None:
# result mean: list[op_error_str],have_error
async def exec_op_list(self,oplist:List,agent_id:str)->tuple[List[str],bool]:
result_str = "op list is none"
if oplist is None:
return
return None,False
result_str = []
have_error = False
for op in oplist:
if op["op"] == "create":
return await self.create(op["path"],op["content"])
await self.create(op["path"],op["content"])
elif op["op"] == "write_file":
is_append = op.get("is_append")
if is_append is None:
is_append = False
return await self.write(op["path"],op["content"],is_append)
error_str = await self.write(op["path"],op["content"],is_append)
elif op["op"] == "delete":
return await self.delete(op["path"])
error_str = await self.delete(op["path"])
elif op["op"] == "rename":
return await self.rename(op["path"],op["new_name"])
error_str = await self.rename(op["path"],op["new_name"])
elif op["op"] == "mkdir":
return await self.mkdir(op["path"])
error_str = await self.mkdir(op["path"])
elif op["op"] == "create_todo":
todoObj = AgentTodo.from_dict(op["todo"])
todoObj.worker = agent_id
todoObj.createor = agent_id
parent_id = op.get("parent")
await self.create_todo(parent_id,todoObj)
error_str = await self.create_todo(parent_id,todoObj)
elif op["op"] == "update_todo":
todo_id = op["id"]
new_stat = op["state"]
await self.update_todo(todo_id,new_stat)
error_str = await self.update_todo(todo_id,new_stat)
else:
logger.error(f"execute op list failed: unknown op:{op['op']}")
error_str = f"execute op list failed: unknown op:{op['op']}"
if error_str:
have_error = True
result_str.append(error_str)
else:
result_str.append(f"execute success!")
return result_str,have_error
async def list(self,path:str,only_dir:bool=False) -> str:
directory_path = self.root_path + path
@@ -112,13 +125,16 @@ class WorkspaceEnvironment(Environment):
async def write(self,path:str,content:str,is_append:bool=False) -> str:
file_path = self.root_path + path
if is_append:
async with aiofiles.open(file_path, mode='a', encoding="utf-8") as f:
await f.write(content)
else:
async with aiofiles.open(file_path, mode='w', encoding="utf-8") as f:
await f.write(content)
return "success"
try:
if is_append:
async with aiofiles.open(file_path, mode='a', encoding="utf-8") as f:
await f.write(content)
else:
async with aiofiles.open(file_path, mode='w', encoding="utf-8") as f:
await f.write(content)
except Exception as e:
return str(e)
return None
async def create(self,path:str,content:str=None) -> bool:
if content is None:
@@ -132,21 +148,29 @@ class WorkspaceEnvironment(Environment):
await f.write(content)
return True
async def delete(self,path:str) -> bool:
file_path = self.root_path + path
os.remove(file_path)
return True
async def delete(self,path:str) -> str:
try:
file_path = self.root_path + path
os.remove(file_path)
except Exception as e:
return str(e)
return None
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 rename(self,path:str,new_name:str) -> str:
try:
file_path = self.root_path + path
new_path = self.root_path + new_name
os.rename(file_path,new_path)
except Exception as e:
return str(e)
return None
async def get_todo_tree(self,path:str = None,deep:int = 4):
if path:
@@ -205,17 +229,17 @@ class WorkspaceEnvironment(Environment):
continue
todo = await self.get_todo_by_fullpath(entry.path)
if todo.worker != agent_id:
continue
todo.rank = int(todo.create_time)>>deep
if todo:
if todo.worker:
if todo.worker != agent_id:
continue
if parent:
parent.sub_todos[todo.todo_id] = todo
result_list.append(todo)
await scan_dir(entry.path,deep + 1,todo)
todo.rank = int(todo.create_time)>>deep
await scan_dir(entry.path,deep + 1,todo)
return
@@ -226,68 +250,89 @@ class WorkspaceEnvironment(Environment):
return result_list
async def get_todo_by_fullpath(self,path:str) -> AgentTodo:
#logger.info("get_todo_by_fullpath:%s",path)
detail_path = path + "/detail"
logger.info("get_todo_by_fullpath:%s",path)
async with aiofiles.open(detail_path, mode='r', encoding="utf-8") as f:
content = await f.read(4096)
logger.debug("get_todo_by_fullpath:%s,content:%s",path,content)
todo_dict = json.loads(content)
result_todo = AgentTodo.from_dict(todo_dict)
if result_todo:
relative_path = os.path.relpath(path, self.root_path + "/todos/")
if not relative_path.startswith('/'):
relative_path = '/' + relative_path
result_todo.todo_path = relative_path
self.known_todo[result_todo.todo_id] = result_todo
else:
logger.error("get_todo_by_path:%s,parse failed!",path)
return result_todo
detail_path = path + "/detail"
try:
async with aiofiles.open(detail_path, mode='r', encoding="utf-8") as f:
content = await f.read(4096)
logger.debug("get_todo_by_fullpath:%s,content:%s",path,content)
todo_dict = json.loads(content)
result_todo = AgentTodo.from_dict(todo_dict)
if result_todo:
relative_path = os.path.relpath(path, self.root_path + "/todos/")
if not relative_path.startswith('/'):
relative_path = '/' + relative_path
result_todo.todo_path = relative_path
self.known_todo[result_todo.todo_id] = result_todo
else:
logger.error("get_todo_by_path:%s,parse failed!",path)
return result_todo
except Exception as e:
logger.error("get_todo_by_path:%s,failed:%s",path,e)
return None
async def get_todo(self,id:str) -> AgentTodo:
return self.known_todo.get(id)
async def create_todo(self,parent_id:str,todo:AgentTodo) -> None:
if parent_id:
if parent_id not in self.known_todo:
logger.error("create_todo failed: parent_id not found!")
return False
parent_path = self.known_todo.get(parent_id).todo_path
todo_path = f"{parent_path}/{todo.title}"
else:
todo_path = todo.title
async def create_todo(self,parent_id:str,todo:AgentTodo) -> str:
try:
if parent_id:
if parent_id not in self.known_todo:
logger.error("create_todo failed: parent_id not found!")
return False
parent_path = self.known_todo.get(parent_id).todo_path
todo_path = f"{parent_path}/{todo.title}"
else:
todo_path = todo.title
dir_path = f"{self.root_path}/todos/{todo_path}"
os.makedirs(dir_path)
detail_path = f"{dir_path}/detail"
if todo.todo_path is None:
todo.todo_path = todo_path
logger.info("create_todo %s",detail_path)
async with aiofiles.open(detail_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(todo.to_dict()))
self.known_todo[todo.todo_id] = todo
return True
async def update_todo(self,todo_id:str,new_stat:str)->bool:
todo : AgentTodo = self.known_todo.get(todo_id)
if todo:
todo.status = new_stat
if todo.raw_obj is None:
todo.raw_obj = todo.to_dict()
todo.raw_obj["status"] = new_stat
detail_path = f"{self.root_path}/todos/{todo.todo_path}/detail"
async with aiofiles.open(detail_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(todo.raw_obj))
return True
return False
dir_path = f"{self.root_path}/todos/{todo_path}"
async def append_do_result(self,todo:AgentTodo,result):
return True
os.makedirs(dir_path)
detail_path = f"{dir_path}/detail"
if todo.todo_path is None:
todo.todo_path = todo_path
logger.info("create_todo %s",detail_path)
async with aiofiles.open(detail_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(todo.to_dict()))
self.known_todo[todo.todo_id] = todo
except Exception as e:
logger.error("create_todo failed:%s",e)
return str(e)
return None
async def update_todo(self,todo_id:str,new_stat:str)->str:
try:
todo : AgentTodo = self.known_todo.get(todo_id)
if todo:
todo.state = new_stat
detail_path = f"{self.root_path}/todos/{todo.todo_path}/detail"
async with aiofiles.open(detail_path, mode='w', encoding="utf-8") as f:
await f.write(json.dumps(todo.to_dict()))
return None
else:
return "todo not found."
except Exception as e:
return str(e)
async def append_worklog(self,todo:AgentTodo,result:AgentTodoResult):
worklog = f"{self.root_path}/todos/{todo.todo_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(result.to_dict())
json_obj["logs"] = logs
await f.write(json.dumps(json_obj))
class CodeInterpreter:
def __init__(self, language, debug_mode):
+23 -4
View File
@@ -92,6 +92,11 @@ class AIOS_Shell:
if a_workflow is not None:
bus.register_message_handler(target_id,a_workflow._process_msg)
return True
a_contact = await ContactManager.get_instance().get_contact(target_id)
if a_contact is not None:
bus.register_message_handler(target_id,a_contact._process_msg)
return True
return False
@@ -184,7 +189,7 @@ class AIOS_Shell:
await ComputeKernel.get_instance().start()
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)
#AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg)
pipelines = KnowledgePipelineManager.initial(os.path.join(AIStorage().get_instance().get_myai_dir(), "knowledge/pipelines"))
@@ -212,6 +217,7 @@ class AIOS_Shell:
return "0.5.1"
async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str:
#AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg)
agent_msg = AgentMsg()
agent_msg.set(sender,target_id,msg)
agent_msg.topic = topic
@@ -241,6 +247,11 @@ class AIOS_Shell:
input_table["allow"] = UserConfigItem("allow group (default is member,you can choose contact or guest)")
case "email":
tunnel_config["type"] = "EmailTunnel"
input_table["email"] = UserConfigItem("email address agent will use \n")
input_table["imap"] = UserConfigItem("imap server address,like hostname:port")
input_table["smtp"] = UserConfigItem("smtp server address,like hostname:port")
input_table["user"] = UserConfigItem("mail server login user name")
input_table["password"] = UserConfigItem("main server login password")
case _:
error_text = FormattedText([("class:error", f"tunnel type {tunnel_type}not support!")])
print_formatted_text(error_text,style=shell_style)
@@ -406,13 +417,21 @@ class AIOS_Shell:
match func_name:
case 'send':
show_text = FormattedText([("class:error", f'send args error,/send Tracy "Hello! It is a good day!" default')])
sender = None
if len(args) == 3:
target_id = args[0]
msg_content = args[1]
topic = args[2]
resp = await self.send_msg(msg_content,target_id,topic,self.username)
show_text = FormattedText([("class:title", f"{self.current_topic}@{self.current_target} >>> "),
("class:content", resp)])
sender = self.username
elif len(args) == 4:
target_id = args[0]
msg_content = args[1]
topic = args[2]
sender = args[3]
resp = await self.send_msg(msg_content,target_id,topic,sender)
show_text = FormattedText([("class:title", f"{self.current_topic}@{self.current_target} >>> "),
("class:content", resp)])
return show_text
case 'set_config':
show_text = FormattedText([("class:error", f"set config args error,/set_config $config_item! ")])