Merge branch 'MVP' into MVP

This commit is contained in:
Liu Zhicong
2023-09-18 00:37:41 -07:00
committed by GitHub
48 changed files with 3596 additions and 615 deletions
+13 -2
View File
@@ -1,5 +1,5 @@
from .environment import Environment,EnvironmentEvent
from .agent_message import AgentMsg,AgentMsgState
from .agent_message import AgentMsg,AgentMsgStatus
from .chatsession import AIChatSession
from .agent import AIAgent,AIAgentTemplete,AgentPrompt
from .compute_kernel import ComputeKernel,ComputeTask
@@ -8,4 +8,15 @@ from .open_ai_node import OpenAI_ComputeNode
from .knowledge_base import KnowledgeBase
from .role import AIRole,AIRoleGroup
from .workflow import Workflow
from .bus import AIBus
from .bus import AIBus
from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent
from .local_llama_compute_node import LocalLlama_ComputeNode
from .whisper_node import WhisperComputeNode
from .google_text_to_speech_node import GoogleTextToSpeechNode
from .tunnel import AgentTunnel
from .tg_tunnel import TelegramTunnel
from .email_tunnel import EmailTunnel
from .storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem
AIOS_Version = "0.5.1, build 2023-9-17"
+85 -60
View File
@@ -5,9 +5,13 @@ import asyncio
import logging
import uuid
import time
import json
from .agent_message import AgentMsg
from .agent_message import AgentMsg, AgentMsgStatus, AgentMsgType
from .chatsession import AIChatSession
from .compute_task import ComputeTaskResult
from .ai_function import AIFunction
from .environment import Environment
logger = logging.getLogger(__name__)
@@ -69,7 +73,7 @@ class AIAgent:
self.prompt:AgentPrompt = None
self.llm_model_name:str = None
self.max_token_size:int = 3600
self.instance_id:str = None
self.agent_id:str = None
self.template_id:str = None
self.fullname:str = None
self.powerby = None
@@ -77,6 +81,8 @@ class AIAgent:
self.chat_db = None
self.unread_msg = Queue() # msg from other agent
self.owner_env : Environment = None
self.owenr_bus = None
@classmethod
def create_from_templete(cls,templete:AIAgentTemplete, fullname:str):
@@ -85,7 +91,7 @@ class AIAgent:
result_agent.llm_model_name = templete.llm_model_name
result_agent.max_token_size = templete.max_token_size
result_agent.template_id = templete.template_id
result_agent.instance_id = "agent#" + uuid.uuid4().hex
result_agent.agent_id = "agent#" + uuid.uuid4().hex
result_agent.fullname = fullname
result_agent.powerby = templete.author
result_agent.prompt = templete.prompt
@@ -95,10 +101,10 @@ class AIAgent:
if config.get("instance_id") is None:
logger.error("agent instance_id is None!")
return False
self.instance_id = config["instance_id"]
self.agent_id = config["instance_id"]
if config.get("fullname") is None:
logger.error(f"agent {self.instance_id} fullname is None!")
logger.error(f"agent {self.agent_id} fullname is None!")
return False
self.fullname = config["fullname"]
@@ -123,83 +129,101 @@ class AIAgent:
return "ignore"
return "text"
def _get_inner_functions(self) -> dict:
if self.owner_env is None:
return None
all_inner_function = self.owner_env.get_all_ai_functions()
if all_inner_function is None:
return None
result_func = []
for inner_func in all_inner_function:
this_func = {}
this_func["name"] = inner_func.get_name()
this_func["description"] = inner_func.get_description()
this_func["parameters"] = inner_func.get_parameters()
result_func.append(this_func)
return result_func
async def _execute_func(self,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg) -> str:
from .compute_kernel import ComputeKernel
func_name = inenr_func_call_node.get("name")
arguments = json.loads(inenr_func_call_node.get("arguments"))
func_node : AIFunction = self.owner_env.get_ai_function(func_name)
if func_node is None:
return "execute failed,function not found"
ineternal_call_record = AgentMsg.create_internal_call_msg(func_name,arguments,org_msg.get_msg_id(),org_msg.target)
result_str:str = await func_node.execute(**arguments)
inner_functions = self._get_inner_functions()
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)
ineternal_call_record.result_str = task_result.result_str
ineternal_call_record.done_time = time.time()
org_msg.inner_call_chain.append(ineternal_call_record)
inner_func_call_node = task_result.result_message.get("function_call")
if inner_func_call_node:
return await self._execute_func(inner_func_call_node,prompt,org_msg)
else:
return task_result.result_str
async def _process_msg(self,msg:AgentMsg) -> AgentMsg:
from .compute_kernel import ComputeKernel
session_topic = msg.get_sender() + "#" + msg.topic
chatsession = AIChatSession.get_session(self.instance_id,session_topic,self.chat_db)
chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
if msg.mentions is not None:
if not self.agent_id in msg.mentions:
chatsession.append(msg)
logger.info(f"agent {self.agent_id} recv a group chat message from {msg.sender},but is not mentioned,ignore!")
return None
prompt = AgentPrompt()
prompt.append(self.prompt)
# prompt.append(self._get_function_prompt(the_role.get_name()))
# prompt.append(self._get_knowlege_prompt(the_role.get_name()))
prompt.append(await self._get_prompt_from_session(chatsession)) # chat context
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":"user","content":msg.body}]
prompt.append(msg_prompt)
result = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size)
final_result = result
result_type : str = self._get_llm_result_type(result)
inner_functions = self._get_inner_functions()
task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions)
final_result = task_result.result_str
inner_func_call_node = task_result.result_message.get("function_call")
if inner_func_call_node:
#TODO to save more token ,can i use msg_prompt?
final_result = await self._execute_func(inner_func_call_node,prompt,msg)
result_type : str = self._get_llm_result_type(final_result)
is_ignore = False
match result_type:
# case "function":
# callchain:CallChain = self._parse_function_call_chain(result)
# resp = await callchain.exec()
# if callchain.have_result():
# # generator proc resp prompt with WAITING state
# proc_resp_prompt:AgentPrompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession)
# final_result = await ComputeKernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
# return final_result
# case "send_message":
# # send message to other / sub workflow
# next_msg:AgentMsg = self._parse_to_msg(result)
# if next_msg is not None:
# # TODO: Next Target can be another role in workflow
# next_workflow:Workflow = self.get_workflow(next_msg.get_target())
# inner_chat_session = the_role.agent.get_chat_session(next_msg.get_target(),next_msg.get_session_id())
# inner_chat_session.append_post(next_msg)
# resp = await next_workflow.send_msg(next_msg)
# inner_chat_session.append_recv(resp)
# # generator proc resp prompt with WAITING state
# proc_resp_prompt:AgentPrompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession)
# final_result = await ComputeKernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
# return final_result
#case "post_message":
# # post message to other / sub workflow
# next_msg:AgentMsg = self._parse_to_msg(result)
# if next_msg is not None:
# next_workflow:Workflow = self.get_workflow(next_msg.get_target())
# inner_chat_session = the_role.agent.get_chat_session(next_msg.get_target(),next_msg.get_session_id())
# inner_chat_session.append_post(next_msg)
# next_workflow.post_msg(next_msg)
case "ignore":
is_ignore = True
if is_ignore is not True:
# TODO : how to get inner chat session?
resp_msg = AgentMsg()
resp_msg.set(self.instance_id,msg.sender,final_result)
resp_msg.topic = msg.topic
if chatsession is not None:
chatsession.append_recv(msg)
chatsession.append_post(resp_msg)
resp_msg = msg.create_resp_msg(final_result)
chatsession.append(msg)
chatsession.append(resp_msg)
return resp_msg
return None
def get_id(self) -> str:
return self.instance_id
return self.agent_id
def get_fullname(self) -> str:
return self.fullname
@@ -213,13 +237,14 @@ class AIAgent:
def get_max_token_size(self) -> int:
return self.max_token_size
async def _get_prompt_from_session(self,chatsession:AIChatSession) -> AgentPrompt:
async def _get_prompt_from_session(self,chatsession:AIChatSession,is_groupchat=False) -> AgentPrompt:
# TODO: get prompt from group chat is different from single chat
messages = chatsession.read_history() # read last 10 message
result_prompt = AgentPrompt()
for msg in reversed(messages):
if msg.target == chatsession.owner_id:
result_prompt.messages.append({"role":"user","content":f"{msg.sender}:{msg.body}"})
if msg.sender == chatsession.owner_id:
result_prompt.messages.append({"role":"user","content":msg.body})
elif msg.sender == chatsession.owner_id:
result_prompt.messages.append({"role":"assistant","content":msg.body})
return result_prompt
+108 -8
View File
@@ -1,27 +1,102 @@
from enum import Enum
import uuid
import time
import re
class AgentMsgState(Enum):
class AgentMsgType(Enum):
TYPE_MSG = 0
TYPE_INTERNAL_CALL = 1
TYPE_ACTION = 2
TYPE_EVENT = 3
class AgentMsgStatus(Enum):
RESPONSED = 0
INIT = 1
SENDING = 2
PROCESSING = 3
ERROR = 4
RECVED = 5
EXECUTED = 6
# msg is a msg / msg resp
# msg body可以有内容类型(MIME标签),text, image, voice, video, file,以及富文本(html)
# msg is a inner function call with result
# msg is a Action with result
# qutoe Msg
# forword msg
# reply msg
# 逻辑上的同一个Message在同一个session中看到的msgid相同
# 在不同的session中看到的msgid不同
class AgentMsg:
def __init__(self) -> None:
def __init__(self,msg_type=AgentMsgType.TYPE_MSG) -> None:
self.msg_id = ""
self.msg_type:AgentMsgType = msg_type
self.prev_msg_id:str = None
self.quote_msg_id:str = None
self.rely_msg_id:str = None # if not none means this is a respone msg
self.session_id:str = None
self.create_time = 0
self.sender:str = None
self.done_time = 0
self.topic:str = None # topic is use to find session, not store in db
self.sender:str = None # obj_id.sub_objid@tunnel_id
self.target:str = None
self.mentions:[] = None #use in group chat only
#self.title:str = None
self.body:str = None
self.topic:str = "T#" + uuid.uuid4().hex
#self.msg_type = 0
self.state = AgentMsgState.INIT
self.body_mime:str = None #//default is "text/plain",encode is utf8
#type is call / action
self.func_name = None
self.args = None
self.result_str = None
#type is event
self.event_name = None
self.event_args = None
self.status = AgentMsgStatus.INIT
self.inner_call_chain = []
self.resp_msg = None
@classmethod
def create_internal_call_msg(self,func_name:str,args:dict,prev_msg_id:str,caller:str):
msg = AgentMsg(AgentMsgType.TYPE_INTERNAL_CALL)
msg.func_name = func_name
msg.args = args
msg.prev_msg_id = prev_msg_id
msg.sender = caller
return msg
def create_action_msg(self,action_name:str,args:dict,caller:str):
msg = AgentMsg(AgentMsgType.TYPE_ACTION)
msg.func_name = action_name
msg.args = args
msg.prev_msg_id = self.msg_id
msg.topic = self.topic
msg.sender = caller
return msg
def create_resp_msg(self,resp_body):
resp_msg = AgentMsg()
resp_msg.msg_id = "msg#" + uuid.uuid4().hex
self.create_time = time.time()
resp_msg.rely_msg_id = self.msg_id
resp_msg.sender = self.target
resp_msg.target = self.sender
resp_msg.body = resp_body
resp_msg.topic = self.topic
return resp_msg
def set(self,sender:str,target:str,body:str,topic:str=None) -> None:
self.id = "msg#" + uuid.uuid4().hex
self.msg_id = "msg#" + uuid.uuid4().hex
self.sender = sender
self.target = target
self.body = body
@@ -30,10 +105,35 @@ class AgentMsg:
self.topic = topic
def get_msg_id(self) -> str:
return self.id
return self.msg_id
def get_sender(self) -> str:
return self.sender
def get_target(self) -> str:
return self.target
def get_prev_msg_id(self) -> str:
return self.prev_msg_id
def get_quote_msg_id(self) -> str:
return self.quote_msg_id
@classmethod
def parse_function_call(cls,func_string:str):
match = re.search(r'\s*(\w+)\s*\(\s*(.*)\s*\)\s*', func_string)
if not match:
return None
func_name = match.group(1)
if func_name is None:
return None
if len(func_name) < 2:
return None
params_string = match.group(2).strip()
params = re.split(r'\s*,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)', params_string)
params = [param.strip('"') for param in params]
return func_name, params
+90 -10
View File
@@ -1,25 +1,74 @@
from abc import ABC, abstractmethod
from typing import Dict,Coroutine,Callable
class AIFunction:
def __init__(self) -> None:
self.intro : str = None
self.description : str = None
def load_from_config(self,config:dict) -> bool:
@abstractmethod
def get_name(self) -> str:
"""
return the name of the function (should be snake case)
"""
pass
@abstractmethod
def get_description(self) -> str:
"""
return a detailed description of what the function does
"""
return self.description
@abstractmethod
def get_parameters(self) -> Dict:
"""
Return the list of parameters to execute this function in the form of
JSON schema as specified in the OpenAI documentation:
https://platform.openai.com/docs/api-reference/chat/create#chat/create-parameters
str = run_code(code:str)
parameters = {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python code which needs to be executed"
}
}
}
"""
pass
@abstractmethod
async def execute(self, **kwargs) -> str:
"""
Execute the function and return a JSON serializable dict.
The parameters are passed in the form of kwargs
"""
pass
@abstractmethod
def is_local(self) -> bool:
"""
is this function call need network?
"""
pass
@abstractmethod
def is_in_zone(self) -> bool:
pass
def is_readyonly(self) -> bool:
"""
is this function call in Lan?
"""
pass
def get_intro(self) -> str:
return self.intro
async def execute(self):
@abstractmethod
def is_ready_only(self) -> bool:
pass
#def load_from_config(self,config:dict) -> bool:
# pass
# call chain is a combination of ai_function,group of ai_function.
class CallChain:
def __init__(self) -> None:
@@ -29,4 +78,35 @@ class CallChain:
pass
async def execute(self):
pass
pass
class SimpleAIFunction(AIFunction):
def __init__(self,func_id:str,description:str,func_handler:Coroutine,parameters:Dict = None) -> None:
self.func_id = func_id
self.description = description
self.func_handler = func_handler
self.parameters = parameters
def get_name(self) -> str:
return self.func_id
def get_parameters(self) -> Dict:
if self.parameters is not None:
return self.parameters
return {"type": "object", "properties": {}}
async def execute(self,**kwargs) -> str:
if self.func_handler is None:
return "error: function not implemented"
return await self.func_handler(**kwargs)
def is_local(self) -> bool:
return True
def is_in_zone(self) -> bool:
return True
def is_ready_only(self) -> bool:
return False
+46 -39
View File
@@ -1,5 +1,5 @@
from typing import Any
from .agent_message import AgentMsg,AgentMsgState
from typing import Coroutine,Dict,Any
from .agent_message import AgentMsg,AgentMsgStatus
import asyncio
from asyncio import Queue
@@ -8,19 +8,32 @@ import logging
logger = logging.getLogger(__name__)
class AIBusHandler:
def __init__(self,handler:Any) -> None:
def __init__(self,handler:Coroutine,owner_bus,enable_defualt_proc=True) -> None:
self.handler = handler
self.working_task = None
self.results = {}
self.results = {} # recv resps
self.queue:Queue = Queue()
self.enable_defualt_proc = enable_defualt_proc
self.owner_bus = owner_bus
async def handle_message(self,msg:AgentMsg) -> Any:
if self.handler is None:
return None
return await self.handler(msg)
if self.enable_defualt_proc:
# do default process
if msg.rely_msg_id is not None:
self.results[msg.rely_msg_id] = msg
return None
resp_msg = await self.handler(msg)
if self.enable_defualt_proc:
if resp_msg is not None:
await self.owner_bus.post_message(resp_msg,False)
return resp_msg
class AIBus:
_instance = None
@classmethod
@@ -30,10 +43,13 @@ class AIBus:
return cls._instance
def __init__(self) -> None:
self.handlers = {}
self.unhandle_handler = None
async def post_message(self,target_id,msg:AgentMsg,use_unhandle=True) -> bool:
self.handlers:Dict[AIBusHandler] = {}
self.unhandle_handler:Coroutine = None
async def post_message(self,msg:AgentMsg,use_unhandle=True) -> bool:
target_id = msg.target.split(".")[0]
handler = self.handlers.get(target_id)
if handler:
handler.queue.put_nowait(msg)
@@ -43,45 +59,39 @@ class AIBus:
if use_unhandle:
if self.unhandle_handler is not None:
if await self.unhandle_handler(self,msg):
return await self.post_message(target_id,msg,False)
return await self.post_message(msg,False)
logger.warn(f"post message to {msg.target} failed!,target not found")
return False
def resp_message(self,my_id:str,org_msg_id:str,resp:AgentMsg) -> None:
handler = self.handlers.get(my_id)
if handler is None:
return None
handler.results[org_msg_id] = resp
async def resp_message(self,org_msg_id:str,resp:AgentMsg) -> None:
assert resp.rely_msg_id == org_msg_id
return await self.post_message(resp)
async def get_message_resp(self,name:str,msg_id:str) -> AgentMsg:
handler = self.handlers.get(name)
if handler is None:
async def send_message(self,msg:AgentMsg) -> AgentMsg:
sender_id = msg.sender.split(".")[0]
sender_handler = self.handlers.get(sender_id) # sender already register on bus
if sender_handler is None:
logger.warn(f"sender {sender_id} not register on AI_BUS!")
return None
return handler.results.get(msg_id)
async def send_message(self,target_id:str,msg:AgentMsg) -> AgentMsg:
post_result = await self.post_message(target_id,msg)
post_result = await self.post_message(msg)
if post_result is False:
return None
handler = self.handlers.get(target_id)
if handler is None:
return None
retry_times = 0
while True:
resp = handler.results.get(msg.id)
resp = sender_handler.results.get(msg.msg_id)
if resp is not None:
msg.resp_msg = resp
msg.state = AgentMsgState.RESPONSED
msg.status = AgentMsgStatus.RESPONSED
del sender_handler.results[msg.msg_id]
return resp
await asyncio.sleep(0.2)
retry_times += 1
if retry_times > 100:
msg.state = AgentMsgState.ERROR
msg.status = AgentMsgStatus.ERROR
return None
return None
@@ -91,7 +101,7 @@ class AIBus:
# means sub
def register_message_handler(self,handler_name:str,handler:Any) -> Queue:
handler_node = AIBusHandler(handler)
handler_node = AIBusHandler(handler,self)
self.handlers[handler_name] = handler_node
return handler_node.queue
@@ -100,13 +110,12 @@ class AIBus:
# Wait for a message
message = await handler.queue.get()
try:
#try:
# Try to handle the message
result = await handler.handle_message(message)
handler.results[message.id] = result
except Exception as e:
await handler.handle_message(message)
#except Exception as e:
# If an error occurs, put the message back into the queue
logger.error(f"handle message {message.id} failed! {e}")
# logger.error(f"handle message {message.msg_id} failed! {e}")
#self.queues[name].put_nowait(message)
return
@@ -124,6 +133,4 @@ class AIBus:
logger.warn(f"handler {target_name} is already working!")
return
handler.working_task = asyncio.create_task(self.process_queue(handler))
handler.working_task = asyncio.create_task(self.process_queue(handler))
+74 -26
View File
@@ -5,8 +5,9 @@ import logging
import threading
import datetime
import uuid
import json
from .agent_message import AgentMsg
from .agent_message import AgentMsgType, AgentMsg, AgentMsgStatus
class ChatSessionDB:
def __init__(self, db_file):
@@ -54,14 +55,31 @@ class ChatSessionDB:
""")
# create messages table
# reciver_id could be None
conn.execute("""
CREATE TABLE IF NOT EXISTS Messages (
MessageID TEXT PRIMARY KEY,
SessionID TEXT,
SenderID TEXT,
MsgType INTEGER,
PrevMsgID TEXT,
QuoteMsgID TEXT,
RelyMsgID TEXT,
SenderID TEXT,
ReceiverID TEXT,
Timestamp TEXT,
Topic TEXT,
Mentions TEXT,
ContentMIME TEXT,
Content TEXT,
ActionName TEXT,
ActionParams TEXT,
ActionResult TEXT,
DoneTime TEXT,
Status INTEGER
);
""")
@@ -83,15 +101,43 @@ class ChatSessionDB:
logging.error("Error occurred while inserting session: %s", e)
return -1 # return -1 if an error occurs
def insert_message(self, message_id, session_id, sender_id, receiver_id, timestamp, content, status):
def insert_message(self, msg:AgentMsg):
""" insert a new message into the Messages table """
try:
action_name = None
action_params = None
action_result = None
mentions = None
if msg.mentions:
mentions = json.dumps(msg.mentions)
match msg.msg_type:
case AgentMsgType.TYPE_MSG:
pass
case AgentMsgType.TYPE_ACTION:
action_name = msg.func_name
action_params = json.dumps(msg.args)
action_result = msg.result_str
case AgentMsgType.TYPE_INTERNAL_CALL:
action_name = msg.func_name
action_params = json.dumps(msg.args)
action_result = msg.result_str
case AgentMsgType.TYPE_EVENT:
action_name = msg.event_name
action_params = json.dumps(msg.event_args)
conn = self._get_conn()
conn.execute("""
INSERT INTO Messages (MessageID, SessionID, SenderID, ReceiverID, Timestamp, Content, Status)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (message_id, session_id, sender_id, receiver_id, timestamp, content, status))
INSERT INTO Messages (MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status)
VALUES (?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (msg.msg_id, msg.session_id, msg.msg_type.value, msg.prev_msg_id, msg.sender, msg.target, msg.create_time, msg.topic,mentions,msg.body_mime,msg.body,action_name,action_params,action_result,msg.done_time,msg.status.value))
conn.commit()
if msg.inner_call_chain:
for inner_call in msg.inner_call_chain:
self.insert_message(inner_call)
return 0 # return 0 if successful
except Error as e:
logging.error("Error occurred while inserting message: %s", e)
@@ -134,7 +180,7 @@ class ChatSessionDB:
"""Get a message by its ID"""
conn =self._get_conn()
c = conn.cursor()
c.execute("SELECT MessageID,SessionID,SenderID,ReceiverID,Timestamp,Content,Status FROM Messages WHERE MessageID = ?", (message_id,))
c.execute("SELECT MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status FROM Messages WHERE MessageID = ?", (message_id,))
message = c.fetchone()
return message
@@ -144,7 +190,7 @@ class ChatSessionDB:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute("""
SELECT MessageID,SessionID,SenderID,ReceiverID,Timestamp,Content,Status FROM Messages
SELECT MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status FROM Messages
WHERE SessionID = ?
ORDER BY Timestamp DESC
LIMIT ? OFFSET ?
@@ -222,29 +268,31 @@ class AIChatSession:
result = []
for msg in msgs:
agent_msg = AgentMsg()
agent_msg.id = msg[0]
agent_msg.sender = msg[2]
agent_msg.target = msg[3]
agent_msg.create_time = msg[4]
agent_msg.body = msg[5]
# agent_msg.state = msg[6]
agent_msg.msg_id = msg[0]
agent_msg.session_id = msg[1]
agent_msg.msg_type = AgentMsgType(msg[2])
agent_msg.prev_msg_id = msg[3]
agent_msg.sender = msg[4]
agent_msg.target = msg[5]
agent_msg.create_time = msg[6]
agent_msg.topic = msg[7]
if msg[8] is not None:
agent_msg.mentions = json.loads(msg[8])
agent_msg.body_mime = msg[9]
agent_msg.body = msg[10]
agent_msg.func_name = msg[11]
if msg[12] is not None:
agent_msg.args = json.loads(msg[12])
agent_msg.result_str = msg[13]
agent_msg.done_time = msg[14]
agent_msg.status = AgentMsgStatus(msg[15])
result.append(agent_msg)
return result
def append(self,msg:AgentMsg) -> None:
self.db.insert_message(msg.id,self.session_id,msg.sender,msg.target,msg.create_time,msg.body,0)
def append_post(self,msg:AgentMsg) -> None:
"""append msg to session, msg is post from session (owner => msg.target)"""
assert msg.sender == self.owner_id,"post message means msg.sender == self.owner_id"
self.append(msg)
def append_recv(self,msg:AgentMsg) -> None:
"""append msg to session, msg is recv from msg'sender (msg.sender => owner)"""
assert msg.target == self.owner_id,"recv message means msg.target == self.owner_id"
self.append(msg)
msg.session_id = self.session_id
self.db.insert_message(msg)
#def attach_event_handler(self,handler) -> None:
# """chat session changed event handler"""
+39 -39
View File
@@ -6,97 +6,96 @@ from asyncio import Queue
from .agent import AgentPrompt
from .compute_node import ComputeNode
from .compute_task import ComputeTask,ComputeTaskState,ComputeTaskResult
from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskResult
logger = logging.getLogger(__name__)
# How to dispatch different computing tasks (some tasks may contain a large amount of state for correct execution)
# to suitable computing nodes, achieving a balance of speed, cost, and power consumption,
# to suitable computing nodes, achieving a balance of speed, cost, and power consumption,
# is the CORE GOAL of the entire computing task schedule system (aios_kernel).
class ComputeKernel:
_instance = None
def __new__(cls):
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.is_start = False
cls._instance = ComputeKernel()
return cls._instance
def __init__(self) -> None:
if self.is_start is True:
return
self.is_start = True
self.is_start = False
self.task_queue = Queue()
self.is_start = False
self.compute_nodes = {}
self.start()
def run(self,task:ComputeTask) -> None:
def run(self, task: ComputeTask) -> None:
# check there is compute node can support this task
if self.is_task_support(task) is False:
logger.error(f"task {task.display()} is not support by any compute node")
logger.error(
f"task {task.display()} is not support by any compute node")
return
# add task to working_queue
self.task_queue.put_nowait(task)
def start(self):
if self.is_start is True:
logger.warn("compute_kernel is already start")
return
self.is_start = True
async def _run_task_loop():
while True:
logger.info("compute_kernel is waiting for task...")
task = await self.task_queue.get()
logger.info(f"compute_kernel get task: {task.display()}")
c_node:ComputeNode = self._schedule(task)
c_node: ComputeNode = self._schedule(task)
await c_node.push_task(task)
logger.warn("compute_kernel is stoped!")
asyncio.create_task(_run_task_loop())
def _schedule(self,task) -> ComputeNode:
logger.warn("compute_kernel is stoped!")
asyncio.create_task(_run_task_loop())
def _schedule(self, task) -> ComputeNode:
for node in self.compute_nodes.values():
if node.is_support(task) is True:
return node
logger.warning(f"task {task.display()} is not support by any compute node")
logger.warning(
f"task {task.display()} is not support by any compute node")
return None
def add_compute_node(self,node:ComputeNode):
def add_compute_node(self, node: ComputeNode):
if self.compute_nodes.get(node.node_id) is not None:
logger.warn(f"compute_node {node.display()} already in compute_kernel")
logger.warn(
f"compute_node {node.display()} already in compute_kernel")
return
self.compute_nodes[node.node_id] = node
logger.info(f"add compute_node {node.display()} to compute_kernel")
def disable_compute_node(self,node_id:str):
def disable_compute_node(self, node_id: str):
node = self.compute_nodes.get(node_id)
if node is None:
logger.warn(f"compute_node {node_id} not in compute_kernel")
return
node.enable = False
def is_task_support(self,task:ComputeTask) -> bool:
def is_task_support(self, task: ComputeTask) -> bool:
return True
# friendly interface for use:
def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0):
def llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0,inner_functions = None):
# craete a llm_work_task ,push on queue's end
# then task_schedule would run this task.(might schedule some work_task to another host)
task_req = ComputeTask()
task_req.set_llm_params(prompt,mode_name,max_token)
task_req.set_llm_params(prompt, mode_name, max_token,inner_functions)
self.run(task_req)
return task_req
async def do_llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0) -> str:
task_req = self.llm_completion(prompt,mode_name,max_token)
async def do_llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0, inner_functions = None) -> str:
task_req = self.llm_completion(prompt, mode_name, max_token,inner_functions)
async def check_timer():
check_times = 0
while True:
@@ -106,19 +105,19 @@ class ComputeKernel:
if task_req.state == ComputeTaskState.ERROR:
break
if check_times >= 20:
if check_times >= 20:
task_req.state = ComputeTaskState.ERROR
break
await asyncio.sleep(0.5)
check_times += 1
await asyncio.create_task(check_timer())
if task_req.state == ComputeTaskState.DONE:
return task_req.result.result_str
return task_req.result
return "error!"
def text_embedding(self,input:str,model_name:Optional[str] = None):
task_req = ComputeTask()
task_req.set_text_embedding_params(input,model_name)
@@ -148,4 +147,5 @@ class ComputeKernel:
return task_req.result.result
return "error!"
+10 -14
View File
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from .compute_task import ComputeTask
from .compute_task import ComputeTask, ComputeTaskType
class ComputeNode(ABC):
@@ -8,15 +8,15 @@ class ComputeNode(ABC):
self.enable = True
@abstractmethod
async def push_task(self,task:ComputeTask,proiority:int = 0):
pass
@abstractmethod
async def remove_task(self,task_id:str):
async def push_task(self, task: ComputeTask, proiority: int = 0):
pass
@abstractmethod
def get_task_state(self,task_id:str):
async def remove_task(self, task_id: str):
pass
@abstractmethod
def get_task_state(self, task_id: str):
pass
@abstractmethod
@@ -37,17 +37,13 @@ class ComputeNode(ABC):
def is_trusted(self) -> bool:
return True
def get_fee_type(self) -> str:
return "free"
class LocalComputeNode(ComputeNode):
def display(self) -> str:
return super().display()
def is_local(self) -> bool:
return True
def is_local(self) -> bool:
return True
+34 -20
View File
@@ -3,6 +3,7 @@ from enum import Enum
import uuid
import time
class ComputeTaskState(Enum):
DONE = 0
INIT = 1
@@ -10,33 +11,47 @@ class ComputeTaskState(Enum):
ERROR = 3
PENDING = 4
class ComputeTaskType(Enum):
NONE = -1
LLM_COMPLETION = 0
TEXT_2_IMAGE = 1
IMAGE_2_IMAGE = 2
VOICE_2_TEXT = 3
TEXT_2_VOICE = 4
class ComputeTask:
def __init__(self) -> None:
self.task_type = "llm_completion"
self.task_type = ComputeTaskType.NONE
self.create_time = None
self.task_id:str = None
self.callchain_id:str = None
self.params:dict = {}
self.refers:dict = None
self.pading_data:bytearray = None
self.task_id: str = None
self.callchain_id: str = None
self.params: dict = {}
self.refers: dict = None
self.pading_data: bytearray = None
self.state = ComputeTaskState.INIT
self.result = None
self.error_str = None
def set_llm_params(self,prompts,model_name,max_token_size,callchain_id = None):
self.task_type = "llm_completion"
def set_llm_params(self, prompts, model_name, max_token_size, inner_functions = None, callchain_id=None):
self.task_type = ComputeTaskType.LLM_COMPLETION
self.create_time = time.time()
self.task_id = uuid.uuid4().hex
self.callchain_id = callchain_id
self.params["prompts"] = prompts.messages
if model_name is not None:
self.params["model_name"] = model_name
else:
else:
self.params["model_name"] = "gpt-4-0613"
self.params["max_token_size"] = max_token_size
if max_token_size is None:
self.params["max_token_size"] = 4000
else:
self.params["max_token_size"] = max_token_size
if inner_functions is not None:
self.params["inner_functions"] = inner_functions
def set_text_embedding_params(self, input, model_name=None, callchain_id = None):
self.task_type = "text_embedding"
@@ -56,16 +71,15 @@ class ComputeTask:
class ComputeTaskResult:
def __init__(self) -> None:
self.create_time = None
self.task_id:str = None
self.callchain_id:str = None
self.worker_id:str = None
self.result_code:int = 0
self.result_str:str = None
self.task_id: str = None
self.callchain_id: str = None
self.worker_id: str = None
self.result_code: int = 0
self.result_str: str = None # easy to use,can read from result
self.result_message: dict = {}
self.result_refers: dict = None
self.pading_data: bytearray = None
self.result:dict = {}
self.result_refers:dict = None
self.pading_data:bytearray = None
def set_from_task(self,task:ComputeTask):
def set_from_task(self, task: ComputeTask):
self.task_id = task.task_id
self.callchain_id = task.callchain_id
+34
View File
@@ -0,0 +1,34 @@
from typing import List
class Contact:
def __init__(self,name:str) -> None:
self.name = name
self.tags = []
def is_zone_owner(self,zone_id=None) -> bool:
return True
def get_tags(self)->List[str]:
return self.tags
def get_name(self)->str:
return self.name
class ContactManager:
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = ContactManager()
return cls._instance
def __init__(self) -> None:
self.contacts = {}
self.contacts["liuzhicong"] = Contact("liuzhicong")
#def get_by_addr(self,addr:str) -> Contact:
# pass
def get_by_name(self,name:str) -> Contact:
return self.contacts.get(name)
+143
View File
@@ -0,0 +1,143 @@
import asyncio
import aiosmtplib
import aioimaplib
import email
from email.header import decode_header
import mailparser
import logging
import time
import datetime
from .tunnel import AgentTunnel
from .agent_message import AgentMsg
from email.message import EmailMessage
logger = logging.getLogger(__name__)
class EmailTunnel(AgentTunnel):
@classmethod
def register_to_loader(cls):
async def load_email_tunnel(config:dict) -> AgentTunnel:
result_tunnel = EmailTunnel()
if await result_tunnel.load_from_config(config):
return result_tunnel
else:
return None
AgentTunnel.register_loader("EmailTunnel",load_email_tunnel)
async def load_from_config(self,config:dict)->bool:
self.target_id = config["target"]
self.tunnel_id = config["tunnel_id"]
self.type = "TelegramTunnel"
self.email = config["email"]
self.imap_server = config["imap"]
s = self.imap_server.split(":")
if len(s) == 2:
self.imap_server = s[0]
self.imap_port = int(s[1])
self.smtp_server = config["smtp"]
s = self.smtp_server.split(":")
if len(s) == 2:
self.smtp_server = s[0]
self.smtp_port = int(s[1])
self.login_user = config["user"]
self.login_password = config["password"]
self.folder = config["folder"]
self.check_interval = config["interval"]
return True
def __init__(self) -> None:
super().__init__()
self.is_start = False
self.read_email = {}
async def on_new_email(self,mail:mailparser.MailParser) -> None:
remote_email_addr = mail.from_[0][1]
remote_user_name = remote_email_addr.split("@")[0]
agent_msg = self.conver_mail_to_agent_msg(mail)
agent_msg.sender = remote_user_name
agent_msg.target = self.target_id
self.ai_bus.register_message_handler(remote_user_name, self._process_message)
resp_msg = await self.ai_bus.send_message(agent_msg)
if resp_msg is None:
await self.reply_email(remote_email_addr,"Sorry, I can't understand your message","")
else:
if resp_msg.body_mime is None:
await self.reply_email(remote_email_addr,"result",resp_msg.body)
async def reply_email(self,target_email:str,title:str,msg:str) -> None:
email_msg = EmailMessage()
email_msg['Subject'] = f"Reply: {title}"
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()
msg.set("",self.target_id,mail.text_plain[0])
msg.topic = "email"
return msg
async def check_email(self) -> None:
self.last_check_num = 0
self.last_check_time = datetime.datetime.now()
while True:
if self.is_start == False:
return
await asyncio.sleep(self.check_interval)
imap_client = aioimaplib.IMAP4_SSL(host=self.imap_server,port=self.imap_port)
await imap_client.wait_hello_from_server()
await imap_client.login(self.login_user, self.login_password)
date_since = self.last_check_time.strftime("%d-%b-%Y")
await imap_client.select(self.folder)
status, messages = await imap_client.search('UNSEEN',charset='US-ASCII')
self.last_check_time = datetime.datetime.now()
if status == "OK":
message_numbers = messages[0].split()
for num in message_numbers:
num = int(num)
if self.read_email.get(num) is not None:
continue
status, email_data = await imap_client.fetch(str(num), "(RFC822)")
if status == "OK":
#r = email.message_from_bytes(email_data[1])
mail = mailparser.parse_from_bytes(email_data[1])
self.read_email[num] = mail
await self.on_new_email(mail)
await imap_client.logout()
async def start(self) -> bool:
if self.is_start:
logger.warning(f"tunnel {self.tunnel_id} is already started")
return False
self.is_start = True
asyncio.create_task(self.check_email())
return True
async def close(self) -> None:
self.is_start = False
async def _process_message(self, msg: AgentMsg) -> None:
logger.warn(f"process message {msg.msg_id} from {msg.sender} to {msg.target}")
+118 -4
View File
@@ -2,20 +2,134 @@
# we have some built-in environment: Calender(include timer),Home(connect to IoT device in your home), ,KnwoledgeBase,FileSystem,
from abc import ABC, abstractmethod
from typing import Callable
from typing import Any, Callable, Optional,Dict,Awaitable,List
import logging
from .ai_function import AIFunction
logger = logging.getLogger(__name__)
class EnvironmentEvent(ABC):
@abstractmethod
def display(self) -> str:
pass
EnvironmentEventHandler = Callable[[str,EnvironmentEvent],Awaitable[Any]]
class Environment:
def __init__(self) -> None:
pass
_all_env = {}
@classmethod
def get_env_by_id(cls,env_id:str):
return cls._all_env.get(env_id)
@classmethod
def set_env_by_id(cls,id,env):
assert id == env.get_id()
cls._all_env[env.get_id()] = env
def __init__(self,env_id:str) -> None:
self.env_id = env_id
self.values:Dict[str,str] = {}
self.get_handlers:Dict[str,Callable] = {}
self.owner_env:Dict[str,Environment] = {}
# self.valid_keys:Dict[str,bool] = None
self.event_handlers:Dict[str,List[EnvironmentEventHandler]]= {}
self.functions : Dict[str,AIFunction] = {}
def get_id(self) -> str:
return self.env_id
def add_owner_env(self,env) -> None:
self.owner_env[env.get_id()] = env
#@abstractmethod
#TODO: how to use env? different env has different prompt
#def get_env_prompt(self) -> str:
# pass
def add_ai_function(self,func:AIFunction) -> None:
if self.functions.get(func.get_name()) is not None:
logger.warn(f"add ai_function {func.get_name()} in env {self.env_id}:function already exist")
self.functions[func.get_name()] = func
def get_ai_function(self,func_name:str) -> AIFunction:
return self.functions.get(func_name)
#def enable_ai_function(self,func_name:str) -> None:
# pass
#def disable_ai_function(self,func_name:str) -> None:
# pass
def get_all_ai_functions(self) -> List[AIFunction]:
return self.functions.values()
@abstractmethod
def _do_get_value(self,key:str) -> Optional[str]:
pass
def register_get_handler(self,key:str,handler:Callable) -> None:
h = self.get_handlers.get(key)
if h is not None:
logger.warn(f"register get_handler {key} in env {self.env_id}:handler already exist")
self.get_handlers[key] = handler
def attach_event_handler(self,event_id:str,handler:Callable) -> None:
pass
handler_list = self.event_handlers.get(event_id)
if handler_list is None:
handler_list = []
self.event_handlers[event_id] = handler_list
handler_list.append(handler)
def remove_event_handler(self,event_id:str,handler:Callable) -> None:
handler_list = self.event_handlers.get(event_id)
if handler is not None:
handler_list.remove(handler)
return
logger.warn(f"remove event_handler {event_id} in env {self.env_id}:handler not found")
async def fire_event(self,event_id:str,event:EnvironmentEvent) -> None:
handler_list = self.event_handlers.get(event_id)
if handler_list is not None:
for handler in handler_list:
await handler(self.env_id,event)
else:
logger.debug(f"fire event {event_id} in env {self.env_id}:handler not found")
return
def __getitem__(self, key):
return self.get_value(key)
def get_value(self,key:str) -> Optional[str]:
handler = self.get_handlers.get(key)
if handler is not None:
return handler()
s = self.values.get(key)
if isinstance(s,str):
return s
else:
logger.warn(f"get value {key} in env {self.env_id} failed!,type is not str")
s = self._do_get_value(key)
if s is not None:
return s
if self.owner_env is not None:
for env in self.owner_env.values():
s = env.get_value(key)
if s is not None:
return s
logger.warn(f"get value {key} in env {self.env_id} failed!,not found")
return None
def set_value(self, key: str, str_value: str,is_storage:bool = True):
logger.info(f"set value {key} in env {self.env_id} to {str_value}")
self.values[key] = str_value
@@ -0,0 +1,104 @@
import os
import asyncio
from asyncio import Queue
import logging
from google.cloud import texttospeech
from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType
from .compute_node import ComputeNode
logger = logging.getLogger(__name__)
"""
You need to set the GOOGLE_APPLICATION_CREDENTIALS environment variable when using it.
see:https://cloud.google.com/text-to-speech/docs/before-you-begin
"""
class GoogleTextToSpeechNode(ComputeNode):
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(GoogleTextToSpeechNode, cls).__new__(cls)
cls._instance.is_start = False
return cls._instance
def __init__(self):
super().__init__()
if self.is_start is True:
logger.warn("GoogleTextToSpeechNode is already start")
return
self.is_start = True
self.node_id = "google_text_to_speech_node"
self.task_queue = Queue()
self.client = texttospeech.TextToSpeechClient()
self.start()
def start(self):
async def _run_task_loop():
while True:
task = await self.task_queue.get()
try:
result = self._run_task(task)
if result is not None:
task.state = ComputeTaskState.DONE
task.result = result
except Exception as e:
logger.error(f"google_text_to_speech_node run task error: {e}")
task.state = ComputeTaskState.ERROR
task.result = ComputeTaskResult()
task.result.set_from_task(task)
task.result.worker_id = self.node_id
task.result.result_str = str(e)
asyncio.create_task(_run_task_loop())
def _run_task(self, task: ComputeTask):
task.state = ComputeTaskState.RUNNING
language_code = task.params["language_code"]
text = task.params["text"]
synthesis_input = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(language_code=language_code,
ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL)
audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3)
response = self.client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config)
result = ComputeTaskResult()
result.set_from_task(task)
result.worker_id = self.node_id
result.result = response.audio_content
return result
async def push_task(self, task: ComputeTask, proiority: int = 0):
logger.info(f"google_text_to_speech_node push task: {task.display()}")
self.task_queue.put_nowait(task)
async def remove_task(self, task_id: str):
pass
def get_task_state(self, task_id: str):
pass
def display(self) -> str:
return f"GoogleTextToSpeechNode: {self.node_id}"
def get_capacity(self):
return 0
def is_support(self, task_type: ComputeTaskType) -> bool:
if task_type == ComputeTaskType.TEXT_2_VOICE:
return True
return False
def is_local(self) -> bool:
return False
@@ -0,0 +1,91 @@
import logging
import requests
from typing import Optional, List
from pydantic import BaseModel
from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskType
from .queue_compute_node import Queue_ComputeNode
logger = logging.getLogger(__name__)
"""
This is a custom implementation, it should be redesigned.
"""
class LocalLlama_ComputeNode(Queue_ComputeNode):
async def execute_task(self, task: ComputeTask) -> {
"content": str,
"message": str,
"state": ComputeTaskState,
"error": {
"code": int,
"message": str,
}
}:
class GenerateResponse(BaseModel):
error: Optional[int]
msg: Optional[str]
results: Optional[List[str]]
try:
prompt_msgs = []
for prompt in task.params["prompts"]:
prompt_msgs.append(prompt["content"])
body = {
"prompts": prompt_msgs
}
response = requests.post("http://aigc:7880/generate", json = body, verify=False, headers={"Content-Type": "application/json"})
response.close()
logger.info(f"LocalLlama_ComputeNode task responsed, request: {body}, status-code: {response.status_code}, headers: {response.headers}, content: {response.content}")
if response.status_code != 200:
return {
"state": ComputeTaskState.ERROR,
"error": {
"code": response.status_code,
"message": "http request failed: " + response.status_code
}
}
else:
resp = response.json()
if "error" in resp:
return {
"state": ComputeTaskState.ERROR,
"error": {
"code": resp["error"],
"message": "local llama failed:" + resp["msg"]
}
}
else:
return {
"state": ComputeTaskState.DONE,
"content": str(resp["results"]),
"message": str(resp["results"])
}
except Exception as err:
import traceback
logger.error(f"{traceback.format_exc()}, error: {err}")
return {
"state": ComputeTaskState.ERROR,
"error": {
"code": -1,
"message": "unknown exception: " + str(err)
}
}
def display(self) -> str:
return f"LocalLlama_ComputeNode: {self.node_id}"
def get_capacity(self):
pass
def is_support(self, task: ComputeTask) -> bool:
return task.task_type == ComputeTaskType.LLM_COMPLETION and (not task.params["model_name"] or task.params["model_name"] == "llama")
def is_local(self) -> bool:
return True
+92 -65
View File
@@ -5,78 +5,60 @@ import asyncio
from asyncio import Queue
import logging
from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState
from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType
from .compute_node import ComputeNode
from .storage import AIStorage,UserConfig
logger = logging.getLogger(__name__)
class OpenAI_ComputeNode(ComputeNode):
_instance = None
def __new__(cls):
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = super(OpenAI_ComputeNode, cls).__new__(cls)
cls._instance.is_start = False
cls._instance = OpenAI_ComputeNode()
return cls._instance
@classmethod
def declare_user_config(cls):
if os.getenv("OPENAI_API_KEY_") is None:
user_config = AIStorage.get_instance().get_user_config()
user_config.add_user_config("openai_api_key","openai api key",False,None)
def __init__(self) -> None:
super().__init__()
if self.is_start is True:
logger.warn("OpenAI_ComputeNode is already start")
return
self.is_start = True
#openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud
self.openai_api_key = ""
self.node_id = "openai_node"
self.is_start = False
# openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud
self.openai_api_key = None
self.node_id = "openai_node"
self.task_queue = Queue()
if os.getenv("OPENAI_API_KEY") is not None:
openai.api_key = os.getenv("OPENAI_API_KEY")
async def initial(self):
if os.getenv("OPENAI_API_KEY") is not None:
self.openai_api_key = os.getenv("OPENAI_API_KEY")
else:
openai.api_key = self.openai_api_key
self.openai_api_key = AIStorage.get_instance().get_user_config().get_user_config("openai_api_key")
if self.openai_api_key is None:
logger.error("openai_api_key is None!")
return False
openai.api_key = self.openai_api_key
self.start()
async def push_task(self,task:ComputeTask,proiority:int = 0):
return True
async def push_task(self, task: ComputeTask, proiority: int = 0):
logger.info(f"openai_node push task: {task.display()}")
self.task_queue.put_nowait(task)
async def remove_task(self,task_id:str):
async def remove_task(self, task_id: str):
pass
def _run_task(self,task:ComputeTask):
def _run_task(self, task: ComputeTask):
task.state = ComputeTaskState.RUNNING
# switch tsak type
if task.task_type == "llm_completion":
mode_name = task.params["model_name"]
# max_token_size = task.params["max_token_size"]
prompts = task.params["prompts"]
mode_name = task.params["model_name"]
# max_token_size = task.params["max_token_size"]
prompts = task.params["prompts"]
logger.info(f"call openai {mode_name} prompts: {prompts}")
resp = openai.ChatCompletion.create(model=mode_name,
messages=prompts,
max_tokens=4000,
temperature=1.2)
logger.info(f"openai response: {resp}")
status_code = resp["choices"][0]["finish_reason"]
if status_code != "stop":
task.state = ComputeTaskState.ERROR
task.error_str =f"The status code was {status_code}."
return None
result = ComputeTaskResult()
result.set_from_task(task)
result.worker_id = self.node_id
result.result_str = resp["choices"][0]["message"]["content"]
result.result = resp["choices"][0]["message"]
return result
if task.task_type == "text_embedding":
model_name = task.params["model_name"]
input = task.params["input"]
@@ -108,34 +90,85 @@ class OpenAI_ComputeNode(ComputeNode):
result.result = resp["data"][0]["embedding"]
return result
if task.task_type == "llm_completion":
mode_name = task.params["model_name"]
# max_token_size = task.params["max_token_size"]
prompts = task.params["prompts"]
mode_name = task.params["model_name"]
# max_token_size = task.params["max_token_size"]
prompts = task.params["prompts"]
logger.info(f"call openai {mode_name} prompts: {prompts}")
if task.params.get("inner_functions") is None:
resp = openai.ChatCompletion.create(model=mode_name,
messages=prompts,
max_tokens=task.params["max_token_size"],
temperature=0.7)
else:
resp = openai.ChatCompletion.create(model=mode_name,
messages=prompts,
functions=task.params["inner_functions"],
max_tokens=task.params["max_token_size"],
temperature=0.7) # TODO: add temperature to task params?
logger.info(f"openai response: {resp}")
result = ComputeTaskResult()
result.set_from_task(task)
status_code = resp["choices"][0]["finish_reason"]
match status_code:
case "function_call":
task.state = ComputeTaskState.DONE
case "stop":
task.state = ComputeTaskState.DONE
case _:
task.state = ComputeTaskState.ERROR
task.error_str = f"The status code was {status_code}."
return None
result.worker_id = self.node_id
result.result_str = resp["choices"][0]["message"]["content"]
result.result_message = resp["choices"][0]["message"]
return result
def start(self):
if self.is_start is True:
return
self.is_start = True
async def _run_task_loop():
while True:
logger.info("openai_node is waiting for task...")
task = await self.task_queue.get()
logger.info(f"openai_node get task: {task.display()}")
result = self._run_task(task)
if result is not None:
task.state = ComputeTaskState.DONE
task.result = result
asyncio.create_task(_run_task_loop())
def display(self) -> str:
return f"OpenAI_ComputeNode: {self.node_id}"
def get_task_state(self,task_id:str):
pass
def get_task_state(self, task_id: str):
pass
def get_capacity(self):
pass
def is_support(self, task: ComputeTask) -> bool:
if task.task_type == "llm_completion":
return True
if task.task_type == ComputeTaskType.LLM_COMPLETION:
if (not task.params["model_name"] or task.params["model_name"] == "gpt-4-0613")
return True
if task.task_type == "text_embedding":
if task.params["model_name"] == "text-embedding-ada-002":
return True
@@ -144,9 +177,3 @@ class OpenAI_ComputeNode(ComputeNode):
def is_local(self) -> bool:
return False
+69
View File
@@ -0,0 +1,69 @@
import asyncio
from asyncio import Queue
import logging
from abc import abstractmethod
from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType
from .compute_node import ComputeNode
logger = logging.getLogger(__name__)
class Queue_ComputeNode(ComputeNode):
def __init__(self):
super().__init__()
self.task_queue = Queue()
@abstractmethod
async def execute_task(self, task: ComputeTask) -> {
"content": str,
"message": str,
"state": ComputeTaskState,
"error": {
"code": int,
"message": str,
}
}:
pass
async def push_task(self, task: ComputeTask, proiority: int = 0):
logger.info(f"{self.display()} push task: {task.display()}")
self.task_queue.put_nowait(task)
async def remove_task(self, task_id: str):
pass
async def _run_task(self, task: ComputeTask):
task.state = ComputeTaskState.RUNNING
resp = await self.execute_task(task)
result = ComputeTaskResult()
result.set_from_task(task)
task.state = resp["state"]
if task.state == ComputeTaskState.ERROR:
task.error_str = resp["error"]["message"]
result.worker_id = self.node_id
result.result_str = resp["content"]
result.result_message = resp["message"]
return result
def start(self):
async def _run_task_loop():
while True:
task = await self.task_queue.get()
logger.info(f"{self.display()} get task: {task.display()}")
result = await self._run_task(task)
if result is not None:
task.result = result
asyncio.create_task(_run_task_loop())
def get_task_state(self, task_id: str):
pass
+8 -2
View File
@@ -6,6 +6,7 @@ class AIRole:
def __init__(self) -> None:
self.agent_instance_id : str = None
self.role_name : str = None
self.role_id :str = None # $workflow_id.$sub_workflow_id.$role_name
self.fullname : str = None
self.agent_name : str = None
self.prompt : AgentPrompt = None
@@ -19,6 +20,7 @@ class AIRole:
return False
self.role_name = name_node
agent_id_node = config.get("agent")
if agent_id_node is None:
logging.error("agent id is not found!")
@@ -35,7 +37,10 @@ class AIRole:
intro_node = config.get("intro")
if intro_node is not None:
self.introduce = intro_node
def get_role_id(self) -> str:
return self.role_id
def get_intro(self) -> str:
return self.introduce
@@ -48,6 +53,7 @@ class AIRole:
class AIRoleGroup:
def __init__(self) -> None:
self.roles : dict[str,AIRole] = {}
self.owner_name : str = None
def load_from_config(self,config:dict) -> bool:
for k,v in config.items():
@@ -55,7 +61,7 @@ class AIRoleGroup:
if role.load_from_config(v) is False:
logging.error(f"load role {k} failed!")
return False
role.role_id = self.owner_name + "." + k
self.roles[k] = role
return True
+141
View File
@@ -0,0 +1,141 @@
import os
import io
import asyncio
from asyncio import Queue
import logging
from PIL import Image
from stability_sdk import client
import stability_sdk.interfaces.gooseai.generation.generation_pb2 as generation
from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType
from .compute_node import ComputeNode
from .storage import AIStorage,UserConfig
logger = logging.getLogger(__name__)
class Stability_ComputeNode(ComputeNode):
_instanace = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = Stability_ComputeNode()
return cls._instance
@classmethod
def declare_user_config(cls):
user_config = AIStorage.get_instance().get_user_config()
user_config.add_user_config("stability_api_key",False,None,"STABILITY_API_KEY")
def __init__(self) -> None:
super().__init__()
self.is_start = False
self.node_id = "stability_node"
self.api_key = ""
self.engine = "stable-diffusion-512-v2-1"
self.task_queue = Queue()
if os.getenv("STABILITY_API_KEY") is not None:
self.api_key = os.getenv("STABILITY_API_KEY")
# Check out the following link for a list of available engines: https://platform.stability.ai/docs/features/api-parameters#engine
if os.getenv("STABILITY_ENGINE") is not None:
self.engine = os.getenv("STABILITY_ENGINE")
self.client = client.StabilityInference(
key=self.api_key,
verbose=True, # Print debug messages.
engine=self.engine,
)
self.start()
async def push_task(self, task: ComputeTask, proiority: int = 0):
logger.info(f"stability_node push task: {task.display()}")
self.task_queue.put_nowait(task)
async def remove_task(self, task_id: str):
pass
def _run_task(self, task: ComputeTask):
task.state = ComputeTaskState.RUNNING
# model_name && max_token_size not used here
prompts = task.params["prompts"]
logging.info(f"call stability {self.engine} prompts: {prompts}")
answers = self.client.generate(
prompt=prompts,
# If a seed is provided, the resulting generated image will be deterministic.
seed=0,
# What this means is that as long as all generation parameters remain the same, you can always recall the same image simply by generating it again.
# Note: This isn't quite the case for Clip Guided generations, which we'll tackle in a future example notebook.
# Amount of inference steps performed on image generation. Defaults to 30.
steps=30,
# Influences how strongly your generation is guided to match your prompt.
cfg_scale=7.0,
# Setting this value higher increases the strength in which it tries to match your prompt.
# Defaults to 7.0 if not specified.
width=512, # Generation width, defaults to 512 if not included.
height=512, # Generation height, defaults to 512 if not included.
# Number of images to generate, defaults to 1 if not included.
samples=1,
# Choose which sampler we want to denoise our generation with.
sampler=generation.SAMPLER_K_DPMPP_2M
# Defaults to k_dpmpp_2m if not specified. Clip Guidance only supports ancestral samplers.
# (Available Samplers: ddim, plms, k_euler, k_euler_ancestral, k_heun, k_dpm_2, k_dpm_2_ancestral, k_dpmpp_2s_ancestral, k_lms, k_dpmpp_2m, k_dpmpp_sde)
)
for resp in answers:
for artifact in resp.artifacts:
logger.info(f"artifact:{artifact.id},{artifact.type},{artifact.finish_reason}")
if artifact.finish_reason == generation.FILTER:
logging.warn("request activated the API's safety filters")
if artifact.type == generation.ARTIFACT_IMAGE:
img = Image.open(io.BytesIO(artifact.binary))
# Save our generated images with the task_id as the filename.
file_name = task.task_id + ".png" # which dir to save?
img.save(file_name)
result = ComputeTaskResult()
result.set_from_task(task)
result.worker_id = self.node_id
result.result = {"file": file_name}
return result
return None
def start(self):
if self.is_start:
return
self.is_start = True
async def _run_task_loop():
while True:
logger.info("stability_node is waiting for task...")
task = await self.task_queue.get()
logger.info(f"stability_node get task: {task.display()}")
result = self._run_task(task)
if result is not None:
task.state = ComputeTaskState.DONE
task.result = result
asyncio.create_task(_run_task_loop())
def display(self) -> str:
return f"Stability_ComputeNode: {self.node_id}"
def get_task_state(self, task_id: str):
pass
def get_capacity(self):
pass
def is_support(self, task: ComputeTask) -> bool:
return task.task_type == ComputeTaskType.TEXT_2_IMAGE
def is_local(self) -> bool:
return False
+171
View File
@@ -0,0 +1,171 @@
from typing import Any
from pathlib import Path
import os
import logging
import toml
import aiofiles
logger = logging.getLogger(__name__)
_file_dir = os.path.dirname(__file__)
class ResourceLocation:
def __init__(self) -> None:
pass
class UserConfigItem:
def __init__(self) -> None:
self.default_value = None
self.is_optional = False
self.item_type = "str"
self.desc = None
self.value = None
self.user_set = False
class UserConfig:
def __init__(self) -> None:
self.config_table = {}
self.user_config_path:str = None
def add_user_config(self,key:str,desc:str,is_optional:bool,default_value:Any=None,item_type="str") -> None:
if self.config_table.get(key) is not None:
logger.warning("user config key %s already exist, will be overrided",key)
new_config_item = UserConfigItem()
new_config_item.default_value = default_value
new_config_item.is_optional = is_optional
new_config_item.desc = desc
new_config_item.item_type = item_type
self.config_table[key] = new_config_item
async def load_value_from_file(self,file_path:str,is_user_config = False) -> None:
try:
all_config = toml.load(file_path)
if all_config is not None:
for key,value in all_config.items():
config_item = self.config_table.get(key)
if config_item is None:
logger.warning("user config key %s not exist",key)
continue
config_item.value = value
config_item.user_set = is_user_config
except Exception as e:
logger.warn(f"load user config from {file_path} failed!")
async def save_value_to_user_config(self) -> None:
will_save_config = {}
for key,value in self.config_table.items():
if value.user_set:
will_save_config[key] = value.value
if len(will_save_config) > 0:
try:
directory = os.path.dirname(self.user_config_path)
if not os.path.exists(directory):
os.makedirs(directory)
async with aiofiles.open(self.user_config_path,"w") as f:
toml_str = toml.dumps(will_save_config)
await f.write(toml_str)
except Exception as e:
logger.error(f"save user config to {self.user_config_path} failed!")
return False
return True
def get_user_config(self,key:str) -> Any:
config_item = self.config_table.get(key)
if config_item is None:
raise Exception("user config key %s not exist",key)
if config_item.value is None:
return config_item.default_value
return config_item.value
def set_user_config(self,key:str,value:Any) -> None:
config_item = self.config_table.get(key)
if config_item is None:
logger.warning("user config key %s not exist",key)
return
config_item.value = value
config_item.user_set = True
#TODO: save to file?
def check_user_config(self) -> None:
check_result = {}
for key,config_item in self.config_table.items():
if config_item.value is None and not config_item.is_optional:
check_result[key] = config_item
if len(check_result) > 0:
return check_result
else:
return None
# storage sytem for current user
class AIStorage:
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = AIStorage()
return cls._instance
def __init__(self) -> None:
self.is_dev_mode = False
self.user_config = UserConfig()
async def initial(self)->bool:
self.user_config.user_config_path = str(self.get_myai_dir() / "etc/system.cfg.toml")
await self.user_config.load_value_from_file(self.get_system_dir() + "/system.cfg.toml")
await self.user_config.load_value_from_file(self.user_config.user_config_path,True)
def get_user_config(self) -> UserConfig:
return self.user_config
def get_system_dir(self) -> str:
"""
system dir is dir for aios system
/opt/aios
"""
if self.is_dev_mode:
return os.path.abspath(_file_dir + "/../")
else:
return "/opt/aios/"
def get_system_app_dir(self)->str:
"""
system app dir is the dir for aios build-in app
/opt/aios/app
"""
if self.is_dev_mode:
return os.path.abspath(_file_dir + "/../../rootfs/")
else:
return "/opt/aios/app/"
def get_myai_dir(self) -> str:
"""
my ai dir is the dir for user to store their ai app and data
~/myai/
"""
return Path.home() / "myai"
def get_db(self,app_name:str)->ResourceLocation:
pass
def open_file(self,file_path:str,options:dict):
pass
def get_named_object(self,name:str) -> Any:
pass
def put_named_object(self,name:str,obj:Any) -> None:
pass
+116
View File
@@ -0,0 +1,116 @@
import logging
import threading
import asyncio
import uuid
from typing import Callable
from telegram import ForceReply, Update
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
from .tunnel import AgentTunnel
from .contact_manager import ContactManager
from .agent_message import AgentMsg
logger = logging.getLogger(__name__)
class TelegramTunnel(AgentTunnel):
@classmethod
def register_to_loader(cls):
async def load_tg_tunnel(config:dict) -> AgentTunnel:
result_tunnel = TelegramTunnel("")
if await result_tunnel.load_from_config(config):
return result_tunnel
else:
return None
AgentTunnel.register_loader("TelegramTunnel",load_tg_tunnel)
async def load_from_config(self,config:dict)->bool:
self.tg_token = config["token"]
self.target_id = config["target"]
self.tunnel_id = config["tunnel_id"]
self.type = "TelegramTunnel"
return True
def dump_to_config(self) -> dict:
pass
def __init__(self,tg_token:str) -> None:
super().__init__()
self.is_start = False
self.tg_token = tg_token
#self.tunnel_id = "tg_tunnel#" + self.app.bot.id
async def start(self) -> bool:
if self.is_start:
logger.warning(f"tunnel {self.tunnel_id} is already started")
return False
self.is_start = True
self.app:Application = Application.builder().token(self.tg_token).build()
self.app.add_handler(MessageHandler(filters.TEXT, self.on_message))
def _run_app():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.app.run_polling(allowed_updates=Update.ALL_TYPES)
self.poll_thread = threading.Thread(target=_run_app)
self.poll_thread.start()
return True
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 conver_tg_msg_to_agent_msg(self,update:Update) -> AgentMsg:
agent_msg = AgentMsg()
agent_msg.topic = "_telegram"
agent_msg.msg_id = "tg_msg#" + str(update.message.message_id) + "#" + uuid.uuid4().hex
agent_msg.target = self.target_id
agent_msg.body = update.message.text
agent_msg.create_time = update.message.date.timestamp()
#if update.message.photo is not None:
# agent_msg.body_mime = "image"
# agent_msg.body = update.message.photo[-1].get_file().download()
return agent_msg
async def on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
cm = ContactManager.get_instance()
reomte_user_name = f"{update.effective_user.id}@telegram"
#contact = cm.get_by_name(update.effective_user.username)
#if contact is not None:
# reomte_user_name = contact.get_name()
#if contact is None:
# update.message.reply_text(f"{self.target_id} process message error, unknown user!")
#if not contact.is_zone_owner():
# update.message.reply_text(f"{self.target_id} process message error, you are not my owner!")
agent_msg = await self.conver_tg_msg_to_agent_msg(update)
agent_msg.sender = reomte_user_name
self.ai_bus.register_message_handler(reomte_user_name, self._process_message)
resp_msg = await self.ai_bus.send_message(agent_msg)
if resp_msg is None:
await update.message.reply_text(f"{self.target_id} process message error")
else:
if resp_msg.body_mime is None:
await update.message.reply_text(resp_msg.body)
else:
if resp_msg.body_mime.startswith("image"):
photo_file = open(resp_msg.body,"rb")
if photo_file:
await update.message.reply_photo(resp_msg.body)
else:
await update.message.reply_text(resp_msg.body)
else:
await update.message.reply_text(resp_msg.body)
+63
View File
@@ -0,0 +1,63 @@
from abc import ABC, abstractmethod
import logging
from typing import Coroutine
from .agent_message import AgentMsg
from .bus import AIBus
logger = logging.getLogger(__name__)
class AgentTunnel(ABC):
_all_loader = {}
_all_tunnels = {}
@classmethod
def register_loader(cls,tunnel_type:str,loader:Coroutine) -> None:
cls._all_loader[tunnel_type] = loader
@classmethod
async def load_all_tunnels_from_config(cls,config:dict) -> None:
for tunnel_config in config:
loader = cls._all_loader.get(tunnel_config["type"])
if loader is not None:
tunnel = await loader(tunnel_config)
if tunnel is not None:
cls._all_tunnels[tunnel.tunnel_id] = tunnel
tunnel.connect_to(AIBus.get_default_bus(),tunnel.target_id)
await tunnel.start()
else:
logger.error(f"load tunnel {tunnel_config['tunnel_id']} failed")
else:
logger.error(f"load tunnel {tunnel_config['type']} failed,loader not found")
def __init__(self) -> None:
super().__init__()
self.tunnel_id = None
self.target_id = None
self.target_type = None
self.ai_bus = None
self.is_connected = False
def connect_to(self, ai_bus:AIBus,target_id: str) -> None:
"""
Connect to the agent with the given id
"""
if self.is_connected:
logger.warning(f"tunnel {self.tunnel_id} is already connected to {self.target_id}")
return
self.target_id = target_id
self.target_type = "agent"
self.ai_bus = ai_bus
self.is_connected = True
@abstractmethod
async def start(self) -> bool:
pass
@abstractmethod
async def close(self) -> None:
pass
@abstractmethod
async def _process_message(self, msg: AgentMsg) -> None:
pass
+111
View File
@@ -0,0 +1,111 @@
from asyncio import Queue
import asyncio
import openai
import os
import logging
from .compute_node import ComputeNode
from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType
logger = logging.getLogger(__name__)
class WhisperComputeNode(ComputeNode):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.is_start = False
return cls._instance
def __init__(self) -> None:
super().__init__()
if self.is_start is True:
logger.warn("WhisperComputeNode is already start")
return
self.is_start = True
self.node_id = "whisper_node"
self.enable = True
self.task_queue = Queue()
self.open_api_key = None
if self.open_api_key is None and os.getenv("OPENAI_API_KEY") is not None:
self.open_api_key = os.getenv("OPENAI_API_KEY")
if self.open_api_key is None:
raise Exception("WhisperComputeNode open_api_key is None")
self.start()
def start(self):
async def _run_task_loop():
while True:
task = await self.task_queue.get()
try:
result = self._run_task(task)
if result is not None:
task.state = ComputeTaskState.DONE
task.result = result
except Exception as e:
logger.error(f"whisper_node run task error: {e}")
task.state = ComputeTaskState.ERROR
task.result = ComputeTaskResult()
task.result.set_from_task(task)
task.result.worker_id = self.node_id
task.result.result_str = str(e)
asyncio.create_task(_run_task_loop())
def _run_task(self, task: ComputeTask):
task.state = ComputeTaskState.RUNNING
prompt = task.params["prompt"]
response_format = None
if "response_format" in task.params:
response_format = task.params["response_format"]
temperature = None
if "temperature" in task.params:
temperature = task.params["temperature"]
language = None
if "language" in task.params:
language = task.params["language"]
file = task.params["file"]
resp = openai.Audio.transcribe("whisper-1",
file,
self.open_api_key,
prompt=prompt,
response_format=response_format,
temperature=temperature,
language=language)
result = ComputeTaskResult()
result.set_from_task(task)
result.worker_id = self.node_id
result.result_str = resp["text"]
result.result = resp
return result
async def push_task(self, task: ComputeTask, proiority: int = 0):
logger.info(f"whisper_node push task: {task.display()}")
self.task_queue.put_nowait(task)
async def remove_task(self, task_id: str):
pass
def get_task_state(self, task_id: str):
pass
def display(self) -> str:
return f"WhisperComputeNode: {self.node_id}"
def get_capacity(self):
return 0
def is_support(self, task_type: ComputeTaskType) -> bool:
if task_type == ComputeTaskType.VOICE_2_TEXT:
return True
return False
def is_local(self) -> bool:
return False
+410 -173
View File
@@ -1,18 +1,23 @@
import logging
import asyncio
import json
import os
import time
from asyncio import Queue
from typing import Optional,Tuple
from abc import ABC, abstractmethod
from .environment import Environment,EnvironmentEvent
from .agent_message import AgentMsg,AgentMsgState
from .agent_message import AgentMsg,AgentMsgStatus
from .agent import AgentPrompt,AgentMsg
from .chatsession import AIChatSession
from .role import AIRole,AIRoleGroup
from .ai_function import CallChain
from .ai_function import AIFunction
from .compute_kernel import ComputeKernel
from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState
from .bus import AIBus
from .workflow_env import WorkflowEnvironment
logger = logging.getLogger(__name__)
@@ -33,9 +38,20 @@ class MessageFilter:
return True
class LLMResult:
def __init__(self) -> None:
self.state : str = "ignore"
self.resp : str = ""
self.post_msgs = []
self.send_msgs = []
self.calls = []
self.post_calls = []
class Workflow:
def __init__(self) -> None:
self.workflow_name : str = None
self.workflow_id : str = None
self.rule_prompt : AgentPrompt = None
self.workflow_config = None
self.role_group : dict = None
@@ -44,6 +60,8 @@ class Workflow:
self.sub_workflows = {}
self.owner_workflow = None
self.db_file = None
self.env_db_file = None
self.workflow_env:WorkflowEnvironment = None
self.is_start = False
self.msg_queue = Queue()
@@ -62,27 +80,57 @@ class Workflow:
logger.error("workflow config must have name")
return False
self.workflow_name = config.get("name")
if self.owner_workflow is None:
self.workflow_id = self.workflow_name
else:
self.workflow_id = self.owner_workflow.workflow_id + "." + self.workflow_name
self.db_file = self.owner_workflow.db_file
#if config.get("rule_prompt") is None:
# logger.error("workflow config must have rule_prompt")
# return False
#self.rule_prompt = AgentPrompt()
#if self.rule_prompt.load_from_config(config.get("rule_prompt")) is False:
# logger.error("Workflow load rule_prompt failed")
# return False
if config.get("prompt") is not None:
self.rule_prompt = AgentPrompt()
if self.rule_prompt.load_from_config(config.get("prompt")) is False:
logger.error("Workflow load prompt failed")
return False
if config.get("roles") is None:
logger.error("workflow config must have roles")
return False
self.role_group = AIRoleGroup()
self.role_group.owner_name = self.workflow_id
if self.role_group.load_from_config(config.get("roles")) is False:
logger.error("Workflow load role_group failed")
return False
if config.get("input_filter") is not None:
if config.get("filter") is not None:
self.input_filter = MessageFilter()
if self.input_filter.load_from_config(config.get("input_filter")) is False:
if self.input_filter.load_from_config(config.get("filter")) is False:
logger.error("Workflow load input_filter failed")
return False
if self.owner_workflow is None:
self.env_db_file = os.path.dirname(self.db_file) + "/" + self.workflow_id + "_env.db"
else:
self.env_db_file = self.owner_workflow.env_db_file
self.workflow_env = WorkflowEnvironment(self.workflow_id,self.env_db_file)
env_ndoe = config.get("enviroment")
if env_ndoe is not None:
if self._load_env_from_config(env_ndoe) is False:
logger.error("Workflow load env failed")
return False
connected_env_ndoe = config.get("connected_env")
if connected_env_ndoe is not None:
for _node in connected_env_ndoe:
env_id = _node.get("env_id")
if env_id is None:
continue
remote_env = Environment.get_env_by_id(_node.get(env_id))
if remote_env is None:
logger.error(f"Workflow load connected_env failed, env {env_id} not found!")
return False
self.connect_to_environment(remote_env,_node.get("event2msg"))
sub_workflows = config.get("sub_workflows")
if sub_workflows is not None:
@@ -90,23 +138,95 @@ class Workflow:
logger.error("Workflow load sub workflows failed")
return False
#TODO: load env
return True
def _load_env_from_config(self,config:dict) -> bool:
for k,v in config.items():
self.workflow_env.set_value(k,v,False)
def _load_sub_workflows(self,config:dict) -> bool:
for k,v in config.items():
sub_workflow = Workflow()
sub_workflow.set_owner(self)
if sub_workflow.load_from_config(v) is False:
logger.error(f"load sub workflow {k} failed!")
return False
self.sub_workflows[k] = sub_workflow
return True
def _parse_msg_target(self,s:str)->list[str]:
return s.split(".")
async def _forword_msg(self,inner_obj_id,msg):
i : int = 1
current_workflow = self
while i < len(inner_obj_id):
if i == len(inner_obj_id) - 1:
the_role : AIRole = current_workflow.role_group.get(inner_obj_id[i])
current_workflow_chatsession = AIChatSession.get_session(current_workflow.workflow_id,msg.sender + "#" + msg.topic,current_workflow.db_file)
if the_role is not None:
return await current_workflow.role_process_msg(msg,the_role,current_workflow_chatsession)
sub_workflow = current_workflow.sub_workflows.get(inner_obj_id[i])
if sub_workflow is not None:
return await sub_workflow._process_msg(msg)
logger.error(f"{msg.target} not found! forword message failed!")
return None
else:
current_workflow = current_workflow.sub_workflows.get(inner_obj_id[i])
if current_workflow is None:
logger.error(f"sub workflow {inner_obj_id[i]} not found!")
return None
i += 1
logger.error(f"{msg.target} not found! forword message failed!")
return None
def get_workflow_id_from_target(self,target:str) -> str:
target_list = target.split(".")
if len(target_list) == 0:
return target
else:
result_str = ""
p = 0
for s in target_list:
p = p + 1
result_str += s
if p < len(target_list)-1:
result_str += "."
else:
return result_str
async def _process_msg(self,msg:AgentMsg):
real_target = msg.target.split(".")[0]
targets = self._parse_msg_target(msg.target)
if len(targets) > 1:
return await self._forword_msg(targets,msg)
#0 we don't support workflow join a group right now, this cloud be a feture in future
if msg.mentions is not None:
logger.warn(f"workflow {self.workflow_id} recv a group chat message,not support ignore!")
return None
#1. workflow start process message
final_result = None
chatsession = None
# this is workflow's group_chat session
session_topic = msg.sender + "#" + msg.topic
chatsesssion = AIChatSession.get_session(self.workflow_id,session_topic,self.db_file)
#2. find role by msg.mentions or workflow's selector logic
if msg.mentions is not None:
if not self.workflow_id in msg.mentions:
chatsesssion.append(msg)
logger.info(f"workflow {self.workflow_id} recv a group chat message from {msg.sender},but is not mentioned,ignore!")
return None
for mention in msg.mentions:
this_role = self.role_group.get(mention)
if this_role is not None:
return await self.role_process_msg(msg,this_role,chatsesssion)
if self.input_filter is not None:
select_role_id = self.input_filter.select(msg)
if select_role_id is not None:
@@ -114,189 +234,302 @@ class Workflow:
if select_role is None:
logger.error(f"input_filter return invalid role id:{select_role_id}, role not found in role_group")
return None
result = await self._role_process_msg(msg,select_role)
if result is None:
logger.error(f"_process_msg return None for :{msg}")
return
if chatsession is not None:
chatsession.append_post(result)
final_result = result
else:
logger.error(f"input_filter return None for :{msg}")
return
else:
results = {}
for this_role in self.role_group.roles.values():
# TODO : we would do this in parallel
a_result = await self._role_process_msg(msg,this_role)
results[this_role.get_name()] = a_result
# merge result from all roles
# TODO: one input msg can have multiple result msg, at this while ,we only support one result msg
final_result:AgentMsg = self._merge_msg_result(results)
if chatsession is not None:
chatsession.append_post(final_result)
return await self.role_process_msg(msg,select_role,chatsesssion)
else:
logger.error(f"input_filter return None for :{msg.body}")
return None
logger.error(f"{self.workflow_id}:no role can process this msg:{msg.body}")
return final_result
@classmethod
def prase_llm_result(cls,llm_result_str:str)->LLMResult:
r = LLMResult()
if llm_result_str is None:
r.state = "ignore"
return r
if llm_result_str == "ignore":
r.state = "ignore"
return r
lines = llm_result_str.splitlines()
is_need_wait = False
for line in lines:
func_call = AgentMsg.parse_function_call(line)
if func_call:
func_args = func_call[1]
match func_call[0]:
case "sendmsg":# sendmsg($target_id,$msg_content)
if len(func_args) != 2:
logger.error(f"parse sendmsg failed! {func_call}")
continue
new_msg = AgentMsg()
target_id = func_args[0]
msg_content = func_args[1]
new_msg.set("_",target_id,msg_content)
r.send_msgs.append(new_msg)
is_need_wait = True
continue
case "postmsg":# postmsg($target_id,$msg_content)
if len(func_args) != 2:
logger.error(f"parse postmsg failed! {func_call}")
continue
new_msg = AgentMsg()
target_id = func_args[0]
msg_content = func_args[1]
new_msg.set("_",target_id,msg_content)
r.post_msgs.append(new_msg)
continue
case "call":# call($func_name,$args_str)
r.calls.append(func_call)
is_need_wait = True
continue
case "post_call": # post_call($func_name,$args_str)
r.post_calls.append(func_call)
continue
r.resp += line + "\n"
else:
r.resp += line + "\n"
if is_need_wait:
r.state = "waiting"
else:
r.state = "reponsed"
return r
async def role_post_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession):
msg.sender = the_role.get_role_id()
target_role = self.role_group.get(msg.target)
if target_role:
msg.target = target_role.get_role_id()
logger.info(f"{msg.sender} post message {msg.msg_id} to inner role: {msg.target}")
asyncio.create_task(self.role_process_msg(msg,target_role,workflow_chat_session))
return
target_workflow = self.sub_workflows.get(msg.target)
if target_workflow:
msg.target = target_workflow.workflow_id
logger.info(f"{msg.sender} post message {msg.msg_id} to sub workflow: {msg.target}")
asyncio.create_task(target_workflow._process_msg(msg))
logger.info(f"{msg.sender} post message {msg.msg_id} to AIBus: {msg.target}")
await self.get_bus().post_message(msg.target,msg)
return
async def role_send_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession):
msg.sender = the_role.get_role_id()
target_role = self.role_group.get(msg.target)
if target_role:
# msg.target = target_role.get_role_id()
logger.info(f"{msg.sender} send message {msg.msg_id} to inner role: {msg.target}")
return await self.role_process_msg(msg,target_role,workflow_chat_session)
async def _role_process_msg(self,msg:AgentMsg,the_role:AIRole) -> None:
# TODO : we just record role's chatsession, but in future, we would record workflow's chatsession(like a groupo chat)
session_topic = f"{the_role.get_name()}#{msg.sender}#{msg.topic}"
chatsession = AIChatSession.get_session(self.workflow_name,session_topic,self.db_file)
if chatsession is None:
logger.error(f"get session {session_topic}@{self.workflow_name} failed!")
target_workflow = self.sub_workflows.get(msg.target)
if target_workflow:
# msg.target = target_workflow.workflow_id
logger.info(f"{msg.sender} send message {msg.msg_id} to sub workflow: {msg.target}")
return await target_workflow._process_msg(msg)
logger.info(f"{msg.sender} post message {msg.msg_id} to AIBus: {msg.target}")
return await self.get_bus().send_message(msg)
async def role_call(self,call:tuple,the_role:AIRole):
logger.info(f"{the_role.role_id} call {call[0]} with args {call[1]}")
func_name = call[0]
arguments = call[1]
func_node : AIFunction = self.workflow_env.get_ai_function(func_name)
if func_node is None:
return "execute failed,function not found"
result_str:str = await func_node.execute(**arguments)
return result_str
async def role_post_call(self,call:tuple,the_role:AIRole):
logger.info(f"{the_role.role_id} post call {call[0]} with args {call[1]}")
return await self.role_call(call,the_role)
def _format_msg_by_env_value(self,prompt:AgentPrompt):
if self.workflow_env is None:
return
for msg in prompt.messages:
old_content = msg.get("content")
msg["content"] = old_content.format_map(self.workflow_env)
def _get_inner_functions(self) -> dict:
all_inner_function = self.workflow_env.get_all_ai_functions()
if all_inner_function is None:
return None
# prompt generat progress is most important part of workflow(app) develope
result_func = []
for inner_func in all_inner_function:
this_func = {}
this_func["name"] = inner_func.get_name()
this_func["description"] = inner_func.get_description()
this_func["parameters"] = inner_func.get_parameters()
result_func.append(this_func)
if len(result_func) > 0:
return result_func
return None
async def _role_execute_func(self,the_role:AIRole,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg) -> str:
from .compute_kernel import ComputeKernel
func_name = inenr_func_call_node.get("name")
arguments = json.loads(inenr_func_call_node.get("arguments"))
func_node : AIFunction = self.workflow_env.get_ai_function(func_name)
if func_node is None:
return "execute failed,function not found"
ineternal_call_record = AgentMsg.create_internal_call_msg(func_name,arguments,org_msg.get_msg_id(),org_msg.target)
result_str:str = await func_node.execute(**arguments)
inner_functions = self._get_inner_functions()
prompt.messages.append({"role":"function","content":result_str,"name":func_name})
task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,
the_role.agent.llm_model_name,the_role.agent.max_token_size,
inner_functions)
ineternal_call_record.result_str = task_result.result_str
ineternal_call_record.done_time = time.time()
org_msg.inner_call_chain.append(ineternal_call_record)
inner_func_call_node = task_result.result_message.get("function_call")
if inner_func_call_node:
return await self._role_execute_func(the_role,inner_func_call_node,prompt,org_msg)
else:
return task_result.result_str
def _is_in_same_workflow(self,msg) -> bool:
pass
async def role_process_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession):
msg.target = the_role.get_role_id()
prompt = AgentPrompt()
prompt.append(the_role.agent.prompt)
prompt.append(self.get_workflow_rule_prompt())
prompt.append(the_role.get_prompt())
# prompt.append(self.get_workflow_rule_prompt())
# prompt.append(self._get_function_prompt(the_role.get_name()))
# prompt.append(self._get_knowlege_prompt(the_role.get_name()))
prompt.append(await self._get_prompt_from_session(chatsession))
#prompt.append(await self._get_prompt_from_session(chatsession,the_role.get_name())) # chat context
#support group chat, user content include sender name!
prompt.append(await self._get_prompt_from_session(workflow_chat_session))
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":"user","content":msg.body}]
msg_prompt.messages = [{"role":"user","content":f"{msg.sender}:{msg.body}"}]
prompt.append(msg_prompt)
result = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
chatsession.append_recv(msg)
final_result = result
result_type : str = self._get_llm_result_type(result)
is_ignore = False
match result_type:
case "function":
callchain:CallChain = self._parse_function_call_chain(result)
resp = await callchain.exec()
if callchain.have_result():
# generator proc resp prompt with WAITING state
#proc_resp_prompt:AgentPrompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession)
final_result = await ComputeKernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
return final_result
self._format_msg_by_env_value(prompt)
inner_functions = self._get_inner_functions()
async def _do_process_msg():
#TODO: send msg to agent might be better?
task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size(),inner_functions)
result_str = task_result.result_str
logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}")
case "send_message":
# send message to other / sub workflow
next_msg:AgentMsg = self._parse_to_msg(result)
if next_msg is not None:
next_msg.sender = self.workflow_name
logger.info(f"W#{self.workflow_name} send message to {next_msg.get_target()}")
resp_msg = await self.get_bus().send_message(next_msg.get_target(),next_msg)
if resp_msg is not None:
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":"assistant","content":result},{"role":"user","content":f"{next_msg.get_target()}:{resp_msg.body}"}]
final_result = await ComputeKernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
case "post_message":
# post message to other / sub workflow
next_msg:AgentMsg = self._parse_to_msg(result)
if next_msg is not None:
next_msg.sender = self.workflow_name
logger.info(f"W#{self.workflow_name} post message to {next_msg.get_target()}")
self.get_bus().post_message(next_msg.get_target(),next_msg)
inner_func_call_node = task_result.result_message.get("function_call")
if inner_func_call_node:
#TODO to save more token ,can i use msg_prompt?
result_str = await self._role_execute_func(the_role,inner_func_call_node,prompt,msg)
case "ignore":
is_ignore = True
result = Workflow.prase_llm_result(result_str)
for postmsg in result.post_msgs:
postmsg.prev_msg_id = msg.get_msg_id()
# might be craete a new msg.topic for this postmsg
postmsg.topic = msg.topic
if is_ignore:
return None
await self.role_post_msg(postmsg,the_role,workflow_chat_session)
if not self._is_in_same_workflow(postmsg):
role_sesion = AIChatSession.get_session(the_role.get_role_id(),f"{postmsg.target}#{msg.topic}",self.db_file)
role_sesion.append(postmsg)
else:
# message will be saved in role.process_message
pass
for post_call in result.post_calls:
action_msg = msg.create_action_msg(post_call[0],post_call[1],the_role.get_role_id())
workflow_chat_session.append(action_msg)
await self.role_post_call(post_call,the_role)
#save post_call
result_prompt_str = ""
match result.state:
case "ignore":
return None
case "reponsed":
resp_msg = msg.create_resp_msg(result.resp)
resp_msg.sender = the_role.get_role_id()
# It is always the person handling the messages who puts them into the session.
workflow_chat_session.append(msg)
workflow_chat_session.append(resp_msg)
#await self.get_bus().resp_message(resp_msg)
return resp_msg
case "waiting":
# TODO: Use role:"function" would be better
for sendmsg in result.send_msgs:
target = sendmsg.target
sendmsg.topic = msg.topic
sendmsg.prev_msg_id = msg.get_msg_id()
send_resp = await self.role_send_msg(sendmsg,the_role,workflow_chat_session)
if send_resp is not None:
result_prompt_str += f"\n{target} response is :{send_resp.body}"
if not self._is_in_same_workflow(sendmsg):
role_sesion = AIChatSession.get_session(the_role.get_role_id(),f"{sendmsg.target}#{sendmsg.topic}",self.db_file)
role_sesion.append(sendmsg)
role_sesion.append(send_resp)
else:
# message will be saved in role.process_message
pass
for call in result.calls:
action_msg = msg.create_action_msg(call[0],call[1],call_result,the_role.get_role_id)
call_result = await self.role_call(call,the_role)
if call_result is not None:
result_prompt_str += f"\ncall {call[0]} result is :{call_result}"
#save action
action_msg.result_str = call_result
workflow_chat_session.append(action_msg)
result_prompt = AgentPrompt()
result_prompt.messages = [{"role":"user","content":result_prompt_str}]
prompt.append(result_prompt)
r = await _do_process_msg()
return r
resp_msg = AgentMsg()
resp_msg.set(self.workflow_name,msg.sender,final_result)
chatsession.append_post(resp_msg)
return resp_msg
async def _pop_msg(self) -> AgentMsg:
pass
def _get_chat_session_for_msg(self,msg:AgentMsg) -> AIChatSession:
pass
return await _do_process_msg()
async def _get_prompt_from_session(self,chatsession:AIChatSession) -> AgentPrompt:
messages = chatsession.read_history() # read last 10 message
result_prompt = AgentPrompt()
for msg in reversed(messages):
if msg.target == chatsession.owner_id:
result_prompt.messages.append({"role":"user","content":f"{msg.sender}:{msg.body}"})
if msg.sender == chatsession.owner_id:
result_prompt.messages.append({"role":"assistant","content":msg.body})
else:
result_prompt.messages.append({"role":"user","content":f"{msg.body}"})
return result_prompt
def _get_msg_queue(self,session_id:str):
pass
def _merge_msg_result(self,results:dict) -> AgentMsg:
# TODO: one input msg can have multiple result msg, at this while ,we only support one result msg
for k,v in results.items():
if v is not None:
return v
def _get_function_prompt(self,role_name:str) -> AgentPrompt:
pass
def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt:
pass
def _get_resp_prompt(self,resp:str,msg:AgentMsg,role:AIRole,prompt:AgentPrompt) -> AgentPrompt:
pass
def get_workflow_rule_prompt(self) -> AgentPrompt:
return self.rule_prompt
def _get_llm_result_type(self,llm_resp_str:str) -> str:
if llm_resp_str == "ignore":
return "ignore"
if llm_resp_str.find("sendmsg(") != -1:
return "send_message"
if llm_resp_str.find("postmsg(") != -1:
return "post_message"
if llm_resp_str.find("call(") != -1:
return "function"
return "text"
def _parse_function_call_chain(self,llm_resp_str) -> CallChain:
pass
def _parse_to_msg(self,llm_resp_str) -> AgentMsg:
lines = llm_resp_str.splitlines()
for line in lines:
if line.startswith("sendmsg("):
line = line[8:]
_index = line.find(",")
msg = AgentMsg()
msg.set("",line[:_index],line[_index+1:])
return msg
if line.startswith("postmsg("):
line = line[8:]
_index = line.find(",")
msg = AgentMsg()
msg.set("",line[:_index],line[_index+1:])
return msg
return None
def get_workflow(self,workflow_name:str):
"""get workflow from known workflow list or sub workflow list"""
pass
def _env_event_to_msg(self,env_event:EnvironmentEvent) -> AgentMsg:
pass
@@ -304,16 +537,20 @@ class Workflow:
def get_inner_environment(self,env_id:str) -> Environment:
pass
def connect_to_environment(self,env:Environment) -> None:
the_env = self.connected_environment.get(env.get_id())
if the_env is None:
self.connected_environment[env.get_id()] = env
def _env_msg_handler(env_event:EnvironmentEvent) -> None:
the_msg:AgentMsg= self._env_event_to_msg(env_event)
self.post_msg(the_msg)
# register all event handler
the_env.attach_event_handler(None,_env_msg_handler)
else:
logger.warn(f"environment {env.get_id()} already connected!")
def connect_to_environment(self,the_env:Environment,conn_info:dict) -> None:
if the_env is not None:
self.workflow_env.add_owner_env(the_env)
#for event2msg in conn_info:
# for k,v in event2msg:
# if k == "role":
# continue
# else:
#
# def _env_msg_handler(env_event:EnvironmentEvent) -> None:
# the_msg:AgentMsg= self._env_event_to_msg(env_event)
# self.role_post_msg
# the_env.attach_event_handler(k,_env_msg_handler)
# break
+151
View File
@@ -0,0 +1,151 @@
from datetime import datetime
import asyncio
import sqlite3 # Because sqlite3 IO operation is small, so we can use sqlite3 directly.(so we don't need to use async sqlite3 now)
from sqlite3 import Error
import threading
import logging
from typing import Optional
from .environment import Environment,EnvironmentEvent
from .ai_function import SimpleAIFunction
logger = logging.getLogger(__name__)
class CalenderEvent(EnvironmentEvent):
def __init__(self,data) -> None:
super().__init__()
self.event_name = "timer"
self.data = data
def display(self) -> str:
return f"#event timer:{self.data}"
# AI Calender GOAL: Let user use "create notify after 2 days" to create a timer event
class CalenderEnvironment(Environment):
def __init__(self, env_id: str) -> None:
super().__init__(env_id)
self.is_run = False
self.add_ai_function(SimpleAIFunction("get_time",
"get current time",
self._get_now))
def _do_get_value(self,key:str) -> Optional[str]:
return None
def start(self) -> None:
if self.is_run:
return
self.is_run = True
self.register_get_handler("now",self.get_now)
async def timer_loop():
while True:
if self.is_run == False:
break
await asyncio.sleep(1.0)
now = datetime.now()
formatted_time = now.strftime('%Y-%m-%d %H:%M:%S')
env_event:CalenderEvent = CalenderEvent(formatted_time)
await self.fire_event("timer",env_event)
return
asyncio.create_task(timer_loop())
def stop(self):
self.is_run = False
def get_now(self,key:str)->str:
now = datetime.now()
formatted_time = now.strftime('%Y-%m-%d %H:%M:%S')
return formatted_time
async def _get_now(self) -> str:
now = datetime.now()
formatted_time = now.strftime('%Y-%m-%d %H:%M:%S')
return formatted_time
# Default Workflow Environment(Context)
class WorkflowEnvironment(Environment):
def __init__(self, env_id: str,db_file:str) -> None:
super().__init__(env_id)
self.db_file = db_file
self.local = threading.local()
self.table_name = "WorkflowEnv_" + env_id
def _get_conn(self):
""" get db connection """
if not hasattr(self.local, 'conn'):
self.local.conn = self._create_connection()
return self.local.conn
def _create_connection(self):
""" create a database connection to a SQLite database """
conn = None
try:
conn = sqlite3.connect(self.db_file)
except Error as e:
logging.error("Error occurred while connecting to database: %s", e)
return None
if conn:
self._create_table(conn)
return conn
def close(self):
if not hasattr(self.local, 'conn'):
return
self.local.conn.close()
def _create_table(self, conn):
""" create table """
try:
# create sessions table
conn.execute(f"""
CREATE TABLE IF NOT EXISTS """ + self.table_name + """ (
EnvKey TEXT PRIMARY KEY,
EnvValue TEXT,
UpdateTime TEXT
);
""")
conn.commit()
except Error as e:
logging.error("Error occurred while creating tables: %s", e)
def _do_get_value(self, key: str) -> str | None:
try:
conn = self._get_conn()
c = conn.cursor()
c.execute("SELECT EnvValue FROM " + self.table_name +" WHERE EnvKey = ?", (key,))
value = c.fetchone()
if value is None:
return None
return value[0]
except Error as e:
logging.error(f"Error occurred while _do_get_value{key}: {e}")
return None
def set_value(self, key: str, str_value: str, is_storage:bool=True):
super().set_value(key,str_value)
if is_storage is False:
return
try:
conn = self._get_conn()
conn.execute("""
INSERT OR REPLACE INTO """ + self.table_name+ """ (EnvKey, EnvValue, UpdateTime)
VALUES (?, ?, ?)
""", (key, str_value, datetime.now()))
conn.commit()
return 0 # return 0 if successful
except Error as e:
logging.error(f"Error occurred while update env{self.env_id}.{key} ,error:{e}")
def get_functions(self):
pass
View File
+10 -7
View File
@@ -2,7 +2,7 @@
import logging
import toml
from aios_kernel import AIAgent,AIAgentTemplete
from aios_kernel import AIAgent,AIAgentTemplete,AIStorage
from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask
logger = logging.getLogger(__name__)
@@ -11,16 +11,19 @@ logger = logging.getLogger(__name__)
class AgentManager:
_instance = None
def __new__(cls):
@classmethod
def get_instance(cls)->'AgentManager':
if cls._instance is None:
cls._instance = super(AgentManager, cls).__new__(cls)
cls._instance = AgentManager()
return cls._instance
def initial(self) -> None:
system_app_dir = AIStorage.get_instance().get_system_app_dir()
user_data_dir = AIStorage.get_instance().get_myai_dir()
def initial(self,root_dir:str) -> None:
self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}/templetes/templetes.cfg")
self.agent_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}/agents/agents.cfg")
self.db_path = f"{root_dir}/agents_chat.db"
self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{system_app_dir}/templates/templetes.cfg")
self.agent_env : PackageEnv = PackageEnvManager().get_env(f"{system_app_dir}/agents/agents.cfg")
self.db_path = f"{user_data_dir}/messages.db"
self.loaded_agent_instance = {}
if self.agent_templete_env is None:
raise Exception("agent_manager initial failed")
+4 -5
View File
@@ -134,16 +134,15 @@ class PackageEnv:
class PackageEnvManager:
_instance = None
def __new__(cls):
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = super(PackageEnvManager, cls).__new__(cls)
cls._instance = PackageEnvManager()
return cls._instance
def __init__(self) -> None:
self._pkg_envs = {}
pass
def get_env(self,cfg_path:str) -> PackageEnv:
if cfg_path in self._pkg_envs:
return self._pkg_envs[cfg_path]
@@ -1,30 +1,49 @@
import logging
import toml
from aios_kernel import Workflow
import os
from aios_kernel import Workflow,AIStorage
from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask
from agent_manager import AgentManager
logger = logging.getLogger(__name__)
import os
class WorkflowManager:
_instance = None
def __new__(cls):
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = super(WorkflowManager, cls).__new__(cls)
cls._instance = WorkflowManager()
return cls._instance
def initial(self,root_dir:str) -> None:
def initial(self) -> None:
self.loaded_workflow = {}
self.workflow_env = PackageEnvManager().get_env(f"{root_dir}/workflows.cfg")
self.db_file = os.path.abspath(f"{root_dir}/workflows.db")
system_app_dir = AIStorage.get_instance().get_system_app_dir()
user_data_dir = AIStorage.get_instance().get_myai_dir()
self.workflow_env = PackageEnvManager().get_env(f"{system_app_dir}/workflows.cfg")
self.db_file = os.path.abspath(f"{user_data_dir}/messages.db")
if self.workflow_env is None:
raise Exception("WorkflowManager initial failed")
async def get_agent_default_workflow(self,agent_id:str) -> Workflow:
pass
async def _load_workflow_agents(self,workflow:Workflow) -> bool:
for v in workflow.role_group.roles.values():
agent = await AgentManager().get(v.agent_name)
if agent is None:
logger.error(f"load agent {v.agent_name} failed!")
return False
v.agent = agent
for sub_workflow in workflow.sub_workflows.values():
if await self._load_workflow_agents(sub_workflow) is False:
return False
return True
async def get_workflow(self,workflow_id:str) -> Workflow:
the_workflow : Workflow = self.loaded_workflow.get(workflow_id)
if the_workflow:
@@ -38,15 +57,11 @@ class WorkflowManager:
the_workflow = await self._load_workflow_from_media(workflow_media_info)
if the_workflow is None:
logger.warn(f"load workflow {workflow_id} from media failed!")
return None
for v in the_workflow.role_group.roles.values():
agent = await AgentManager().get(v.agent_name)
if agent is None:
logger.error(f"load agent {v.agent_name} failed!")
return None
v.agent = agent
if await self._load_workflow_agents(the_workflow) is False:
return None
return the_workflow
async def _load_workflow_from_media(self,workflow_media:PackageMediaInfo) -> Workflow:
@@ -64,10 +79,12 @@ class WorkflowManager:
config_data = await config_file.read()
config = toml.loads(config_data)
result_workflow = Workflow()
result_workflow.db_file = self.db_file
if result_workflow.load_from_config(config) is False:
logger.error(f"load workflow from {workflow_media} failed!")
return None
result_workflow.db_file = self.db_file
return result_workflow
except Exception as e:
logger.error(f"read workflow.toml cfg from {workflow_media} failed! unexpected error occurred: {str(e)}")
+18 -2
View File
@@ -1,7 +1,23 @@
chromadb==0.4
openai==0.28
toml==0.10
Pillow==10.0
moviepy==1.0
base58==2.1
base36==0.1
base36==0.1
aiofiles==23.2.1
aiohttp==3.7.0
aioimaplib==1.0.1
aiosmtplib==2.0.2
beautifulsoup4==4.12.2
mail_parser==3.15.0
openai==0.27.10
Pillow
prompt_toolkit==3.0.39
protobuf
pydantic==1.10.11
python-telegram-bot==20.5
Requests==2.31.0
stability_sdk
toml==0.10.2
+186 -65
View File
@@ -4,6 +4,7 @@ import sys
import os
import logging
import re
import toml
from typing import Any, Optional, TypeVar, Tuple, Sequence
import argparse
@@ -17,6 +18,19 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.styles import Style
directory = os.path.dirname(__file__)
sys.path.append(directory + '/../../')
from aios_kernel import AIOS_Version,UserConfigItem,AIStorage,Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel,LocalLlama_ComputeNode
sys.path.append(directory + '/../../component/')
from agent_manager import AgentManager
from workflow_manager import WorkflowManager
logger = logging.getLogger(__name__)
shell_style = Style.from_dict({
'title': '#87d7ff bold', #RGB
'content': '#007f00 bold',
@@ -24,68 +38,97 @@ shell_style = Style.from_dict({
})
directory = os.path.dirname(__file__)
sys.path.append(directory + '/../../')
from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession
sys.path.append(directory + '/../../component/')
from agent_manager import AgentManager
from workflow_manager import WorkflowManager
class AIOS_Shell:
def __init__(self,username:str) -> None:
self.username = username
self.current_target = "_"
self.current_topic = "default"
self.is_working = True
def declare_all_user_config(self):
user_config = AIStorage.get_instance().get_user_config()
user_config.add_user_config("username","username is your full name when using AIOS",False,None,)
openai_node = OpenAI_ComputeNode.get_instance()
openai_node.declare_user_config()
async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool:
agent : AIAgent = await AgentManager().get(msg.target)
target_id = msg.target.split(".")[0]
agent : AIAgent = await AgentManager.get_instance().get(target_id)
if agent is not None:
bus.register_message_handler(msg.target,agent._process_msg)
agent.owner_env = Environment.get_env_by_id("calender")
bus.register_message_handler(target_id,agent._process_msg)
return True
a_workflow = await WorkflowManager().get_workflow(msg.target)
a_workflow = await WorkflowManager.get_instance().get_workflow(target_id)
if a_workflow is not None:
bus.register_message_handler(msg.target,a_workflow._process_msg)
for subflow in a_workflow.sub_workflows.values():
bus.register_message_handler(subflow.workflow_name,subflow._process_msg)
bus.register_message_handler(target_id,a_workflow._process_msg)
return True
return False
async def is_agent(self,target_id:str) -> bool:
agent : AIAgent = await AgentManager().get(target_id)
agent : AIAgent = await AgentManager.get_instance().get(target_id)
if agent is not None:
return True
else:
return False
async def initial(self) -> bool:
AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/"))
WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/"))
open_ai_node = OpenAI_ComputeNode()
open_ai_node.start()
ComputeKernel().add_compute_node(open_ai_node)
cal_env = CalenderEnvironment("calender")
cal_env.start()
Environment.set_env_by_id("calender",cal_env)
AgentManager.get_instance().initial()
WorkflowManager.get_instance().initial()
open_ai_node = OpenAI_ComputeNode.get_instance()
if await open_ai_node.initial() is not True:
logger.error("openai node initial failed!")
return False
ComputeKernel.get_instance().add_compute_node(open_ai_node)
llama_ai_node = LocalLlama_ComputeNode()
llama_ai_node.start()
ComputeKernel().add_compute_node(llama_ai_node)
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)
TelegramTunnel.register_to_loader()
EmailTunnel.register_to_loader()
user_data_dir = AIStorage.get_instance().get_myai_dir()
tunnels_config_path = os.path.abspath(f"{user_data_dir}/tunnels.cfg.toml")
tunnel_config = None
try:
tunnel_config = toml.load(tunnels_config_path)
if tunnel_config is not None:
await AgentTunnel.load_all_tunnels_from_config(tunnel_config["tunnels"])
except Exception as e:
logger.warning(f"load tunnels config from {tunnels_config_path} failed!")
return True
def get_version(self) -> str:
return "0.0.1"
return "0.5.1"
async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str:
agent_msg = AgentMsg()
agent_msg.set(sender,target_id,msg)
agent_msg.topic = topic
resp = await AIBus().get_default_bus().send_message(target_id,agent_msg)
resp = await AIBus.get_default_bus().send_message(agent_msg)
if resp is not None:
return resp.body
else:
return "error!"
async def install_workflow(self,workflow_id:Workflow) -> None:
async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg:
pass
async def call_func(self,func_name, args):
@@ -108,24 +151,30 @@ class AIOS_Shell:
self.current_topic = topic
show_text = FormattedText([("class:title", f"current session switch to {topic}@{target_id}")])
return show_text
case 'login':
if len(args) >= 1:
self.username = args[0]
AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg)
return self.username + " login success!"
case 'history':
num = 10
offset = 0
if len(args) >= 1:
num = args[0]
if len(args) >= 2:
offset = args[1]
if args is not None:
if len(args) >= 1:
num = args[0]
if len(args) >= 2:
offset = args[1]
db_path = ""
if await self.is_agent(self.current_target):
db_path = AgentManager().db_path
db_path = AgentManager.get_instance().db_path
else:
db_path = WorkflowManager().db_file
db_path = WorkflowManager.get_instance().db_file
chatsession:AIChatSession = AIChatSession.get_session(self.current_target,f"{self.username}#{self.current_topic}",db_path,False)
if chatsession is not None:
msgs = chatsession.read_history(num,offset)
format_texts = []
for msg in reversed(msgs):
for msg in msgs:
format_texts.append(("class:content",f"{msg.sender} >>> {msg.body}"))
format_texts.append(("",f"\n-------------------\n"))
return FormattedText(format_texts)
@@ -136,46 +185,120 @@ class AIOS_Shell:
return FormattedText([("class:title", f"help~~~")])
#######################################################################################
history = FileHistory('history.txt')
##########################################################################################################################
history = FileHistory('aios_shell_history.txt')
session = PromptSession(history=history)
def parse_function_call(s):
match = re.match(r'(\w+)\((.*)\)$', s)
if match:
func_name = match.group(1)
args_str = match.group(2)
args = []
buffer = ''
quote_count = 0 # Count of single or double quotes
for char in args_str:
if char in ['"', "'"]:
quote_count += 1
if char == ',' and quote_count % 2 == 0: # ',' is outside of quotes
args.append(buffer.strip())
buffer = ''
else:
buffer += char
if buffer:
args.append(buffer.strip())
return func_name, args
else:
def parse_function_call(func_string):
match = re.search(r'\s*(\w+)\s*\(\s*(.*)\s*\)\s*', func_string)
if not match:
return None
func_name = match.group(1)
params_string = match.group(2).strip()
params = re.split(r'\s*,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)', params_string)
params = [param.strip('"') for param in params]
if len(params[0]) == 0:
params = None
return func_name, params
async def get_user_config_from_input(check_result:dict) -> bool:
for key,item in check_result.items():
user_input = await session.prompt_async(f"{key} ({item.desc}) not define! \nPlease input:",style=shell_style)
if len(user_input) > 0:
AIStorage.get_instance().get_user_config().set_user_config(key,user_input)
await AIStorage.get_instance().get_user_config().save_value_to_user_config()
return True
async def main_daemon_loop(shell:AIOS_Shell):
while shell.is_working:
await asyncio.sleep(1)
return 0
def print_welcome_screen():
print("\033[1;31m")
logo = """
\t_______ ____________________ __
\t__ __ \______________________ __ \__ |__ | / /
\t_ / / /__ __ \ _ \_ __ \_ / / /_ /| |_ |/ /
\t/ /_/ /__ /_/ / __/ / / / /_/ /_ ___ | /| /
\t\____/ _ .___/\___//_/ /_//_____/ /_/ |_/_/ |_/
\t /_/
"""
print(logo)
print("\033[0m")
print("\033[1;32m \t\tWelcome to OpenDAN - Your Personal AI OS\033[0m\n")
introduce = """
\tThe core goal of version 0.5.1 is to turn the concept of AIOS into code and get it up and running as quickly as possible.
\tAfter three weeks of development, our plans have undergone some changes based on the actual progress of the system.
\tUnder the guidance of this goal, some components do not need to be fully implemented. Furthermore,
\tbased on the actual development experience from several demo Intelligent Applications,
\twe intend to strengthen some components. This document will explain these changes and provide an update
\ton the current development progress of MVP(0.5.1,0.5.2)
"""
print(introduce)
print(f"\033[1;34m \t\tVersion: {AIOS_Version}\n\033")
print("\033[1;33m \tOpenDAN is an open-source project, let's define the future of Humans and AI together.\033[0m")
print("\033[1;33m \tGithub\t: https://github.com/fiatrete/OpenDAN-Personal-AI-OS\033[0m")
print("\033[1;33m \tWebsite\t: https://www.opendan.ai\033[0m")
print("\n\n")
async def main():
print("aios shell prepareing...")
logging.basicConfig(filename="aios_shell.log",filemode="w",level=logging.INFO,format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s')
shell = AIOS_Shell("user")
await shell.initial()
print(f"aios shell {shell.get_version()} ready.")
print_welcome_screen()
print("OpenDAN is booting...")
logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8',force=True,
level=logging.INFO,
format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s')
if os.path.isdir(f"{directory}/../../../rootfs"):
AIStorage.get_instance().is_dev_mode = True
else:
AIStorage.get_instance().is_dev_mode = False
is_daemon = False
if os.name != 'nt':
if os.getppid() == 1:
is_daemon = True
shell = AIOS_Shell("user")
shell.declare_all_user_config()
await AIStorage.get_instance().initial()
check_result = AIStorage.get_instance().get_user_config().check_user_config()
if check_result is not None:
if is_daemon:
logger.error(check_result)
return 1
else:
#Remind users to enter necessary configurations.
if await get_user_config_from_input(check_result) is False:
return 1
init_result = await shell.initial()
if init_result is False:
if is_daemon:
logger.error("aios shell initial failed!")
return 1
else:
print("aios shell initial failed!")
print(f"aios shell {shell.get_version()} ready.")
if is_daemon:
return await main_daemon_loop(shell)
#TODO: read last input config
completer = WordCompleter(['send($target,$msg,$topic)',
'open($target,$topic)',
'history($num,$offset)',
'login($username)',
'connect($target)',
'show()',
'exit()',
'help()'], ignore_case=True)
@@ -199,8 +322,6 @@ async def main():
print_formatted_text(show_text,style=shell_style)
#print_formatted_text(f"{shell.username}<->{shell.current_topic}@{shell.current_target} >>> {resp}",style=shell_style)
if __name__ == "__main__":
asyncio.run(main())
+171
View File
@@ -0,0 +1,171 @@
"""
Capture your email locally, and parse out the pictures in the email body and the pictures, videos and other files in the attachment. Subsequently, it supports vectorized analysis of your personal data and serves as a knowledge base to enable large language model answers. Better results.
An example of a local file is as follows:
├── data
│ └── alex0072@gmail.com
│ └── 5de3e52f3a6b90cabe6cbdd4ae3a5c5b
│ ├── email.txt
│ ├── meta.json
│ ├── image
│ │ ├── 0648B869@99C03070.DB94B354.jpg
│ └── body_image
│ ├── 11044884873.jpg
│ ├── 282985198265470.gif
│ └── dd-login-service-min.png
"""
import imaplib
import os
import toml
import logging
import mailparser
import hashlib
import json
import base64
from bs4 import BeautifulSoup
import requests
class EmailSpider:
def __init__(self):
# logger config
self.logger = logging.getLogger('email spider')
self.logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s [%(name)s] [%(levelname)s] %(message)s')
ch.setFormatter(formatter)
self.logger.addHandler(ch)
# read config from toml file
# and read from config config.local.toml if exists (config.local.toml is ignored by git)
self.config = toml.load('./rootfs/email/config.toml')
if os.path.exists('./rootfs/email/config.local.toml'):
self.config = toml.load('./rootfs/email/config.local.toml')
self.client = self.email_client()
def email_client(self) -> imaplib.IMAP4_SSL:
self.logger.info(f"read email config from {self.config.get('EMAIL_IMAP_SERVER')}")
client = imaplib.IMAP4_SSL(
host=self.config.get('EMAIL_IMAP_SERVER'),
port=self.config.get('EMAIL_IMAP_PORT')
)
client.login(self.config.get('EMAIL_ADDRESS'), self.config.get('EMAIL_PASSWORD'))
return client
def list_box(self):
_, mailbox_list = self.client.list()
for mailbox in mailbox_list:
print(mailbox.decode())
def read_emails(self, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"):
self.client.select(folder)
_, data = self.client.uid('search', None, imap_keyword)
# get email uid list
email_list = data[0].split()
self.logger.info(f"got {len(email_list)} emails")
email_list.reverse()
for uid in email_list:
if self.check_email_saved(uid):
self.logger.info(f"email uid {uid} already saved")
else:
self.read_and_save_email(uid)
self.logger.info(f"email uid {uid} saved")
def read_and_save_email(self, uid: str):
message_parts = "(BODY.PEEK[])"
_, email_data = self.client.uid('fetch', uid, message_parts)
mail = mailparser.parse_from_bytes(email_data[0][1])
self.logger.info(f"got email subject [{mail.subject}]")
self.save_email(mail)
def get_local_dir_name(self, mail: mailparser.MailParser) -> str:
dir = f"{self.config.get('LOCAL_DIR')}/{self.config.get('EMAIL_ADDRESS')}"
name = f"{mail.subject}__{mail.date}"
name = hashlib.md5(name.encode('utf-8')).hexdigest()
return f"{dir}/{name}"
def check_email_saved(self, uid: str):
message_parts = "(BODY[HEADER])"
_, email_data = self.client.uid('fetch', uid, message_parts)
mail = mailparser.parse_from_bytes(email_data[0][1])
self.logger.info(f"[{uid}]check email subject [{mail.subject}]")
dir = self.get_local_dir_name(mail)
self.logger.info(f"check email saved {dir}")
file = f"{dir}/email.txt"
if os.path.exists(file):
return False
return False
# save email attachment(images)
def save_email_attachment(self, mail: mailparser.MailParser, email_dir: str):
for attachment in mail.attachments:
if attachment['mail_content_type'] in ['image/png', 'image/jpeg', 'image/gif']:
print('current mail have image attachment')
img_dir = f"{email_dir}/image"
if not os.path.exists(img_dir):
os.makedirs(img_dir)
filename = attachment['filename']
filefullname = f"{img_dir}/{filename}"
image_data = attachment['payload']
try:
image_data = base64.b64decode(image_data)
except base64.binascii.Error:
image_data = image_data.encode()
with open(filefullname, 'wb') as f:
f.write(image_data)
self.logger.info(f"save email image {filename} success")
# save email body images(html content)
def save_body_images(self, html_content: str, email_dir: str):
# get all image urls
soup = BeautifulSoup(html_content, 'html.parser')
img_tags = soup.find_all('img')
img_urls = [img['src'] for img in img_tags if 'src' in img.attrs]
self.logger.info(f'Found {len(img_urls)} images in email body')
if not os.path.exists(email_dir):
os.makedirs(email_dir)
for img_url in img_urls:
# keep the original image filename(last of url)
img_filename = os.path.join(email_dir, img_url.split('/')[-1])
# download image
response = requests.get(img_url, stream=True)
if response.status_code == 200:
with open(img_filename, 'wb') as img_file:
for chunk in response.iter_content(1024):
img_file.write(chunk)
self.logger.info(f'Downloaded {img_url} to {img_filename}')
else:
self.logger.info(f'Failed to download {img_url}')
# save email content to local dir
def save_email(self, mail: mailparser.MailParser):
dir = f"{self.config.get('LOCAL_DIR')}/{self.config.get('EMAIL_ADDRESS')}"
if not os.path.exists(dir):
os.makedirs(dir)
email_dir = self.get_local_dir_name(mail)
self.logger.info(f"save email to {email_dir}")
if not os.path.exists(email_dir):
os.makedirs(email_dir)
with open(f"{email_dir}/email.txt", "w") as f:
f.write(mail.body)
with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f:
mail_dict = json.loads(mail.mail_json)
if 'body' in mail_dict:
del mail_dict['body']
json.dump(mail_dict, f, ensure_ascii=False, indent=4)
self.logger.info(f"save email meta info {f.name}")
self.save_email_attachment(mail, email_dir)
self.save_body_images(mail.body, f"{email_dir}/body_image")
if __name__ == "__main__":
spider = EmailSpider()
folder = 'INBOX'
imap_keyword = "ALL"
spider.read_emails(folder, imap_keyword)