1.workflow has reached a state of basic functionality.

2. implemented AI message bus infrastructure,
This commit is contained in:
Liu Zhicong
2023-08-30 12:30:41 -07:00
parent ccbef2104b
commit 05d2e4a9cf
21 changed files with 1146 additions and 297 deletions
+5 -2
View File
@@ -1,7 +1,10 @@
from .environment import Environment,EnvironmentEvent
from .agent import AgentMsg,AIAgent,AIAgentTemplete,AgentMsgState,AgentPrompt,AgentMsgState
from .agent_message import AgentMsg,AgentMsgState
from .chatsession import AIChatSession
from .agent import AIAgent,AIAgentTemplete,AgentPrompt
from .compute_kernel import ComputeKernel,ComputeTask
from .compute_node import ComputeNode,LocalComputeNode
from .open_ai_node import OpenAI_ComputeNode
from .role import AIRole,AIRoleGroup
from .workflow import Workflow
from .workflow import Workflow
from .bus import AIBus
+31 -106
View File
@@ -1,51 +1,17 @@
from typing import Optional
from enum import Enum
from asyncio import Queue
import asyncio
import logging
import uuid
import time
from .agent_message import AgentMsg
from .chatsession import AIChatSession
logger = logging.getLogger(__name__)
class AgentMsgState(Enum):
RESPONSED = 0
INIT = 1
SENDING = 2
PROCESSING = 3
ERROR = 4
class AgentMsg:
def __init__(self) -> None:
self.create_time = 0
self.sender:str = None
self.target:str = None
self.body:str = None
self.state = AgentMsgState.INIT
self.resp_msg = None
def set(self,sender:str,target:str,body:str) -> None:
self.sender = sender
self.target = target
self.body = body
self.create_time = time.time()
def get_msg_id(self) -> str:
pass
def get_sender(self) -> str:
return self.sender
def get_target(self) -> str:
return self.target
# return workflow_name, role_name, session_id
def parser_target(self,target:str) -> None:
pass
class AgentPrompt:
def __init__(self) -> None:
self.messages = []
@@ -59,6 +25,9 @@ class AgentPrompt:
return result_str
def append(self,prompt):
if prompt is None:
return
self.messages.extend(prompt.messages)
def load_from_config(self,config:list) -> bool:
@@ -69,28 +38,6 @@ class AgentPrompt:
self.messages = config
return True
# chat session store the chat history between owner and agent
# chat session might be large, so can read / write at stream mode.
class AIChatSession:
def __init__(self,owner_id) -> None:
self.owner_id = owner_id
def get_owner_id(self) -> str:
return self.owner_id
def append_post(self,msg:AgentMsg) -> None:
"""append msg to session, msg is post from session (owner => msg.target)"""
pass
def append_recv(self,msg:AgentMsg) -> None:
"""append msg to session, msg is recv from msg'sender (msg.sender => owner)"""
pass
def attach_event_handler(self,handler) -> None:
"""chat session changed event handler"""
pass
#TODO : add iterator interface for read chat history
class AIAgentTemplete:
def __init__(self) -> None:
@@ -121,14 +68,14 @@ class AIAgent:
def __init__(self) -> None:
self.prompt:AgentPrompt = None
self.llm_model_name:str = None
self.max_token_size:int = 0
self.max_token_size:int = 3600
self.instance_id:str = None
self.template_id:str = None
self.fullname:str = None
self.powerby = None
self.enable = True
self.chat_sessions = {}
self.chat_db = None
self.unread_msg = Queue() # msg from other agent
@classmethod
@@ -170,27 +117,6 @@ class AIAgent:
return True
def post_msg(self,msg:AgentMsg) -> None:
# TODO: drop same msg already processed
msg.state = AgentMsgState.SENDING
self.unread_msg.put_nowait(msg)
def start(self) -> None:
async def _process_msg_loop():
while True:
msg = await self.unread_msg.get()
if msg is None:
continue
msg.state = AgentMsgState.PROCESSING
resp_msg = await self._process_msg(msg)
if resp_msg is None:
msg.state = AgentMsgState.ERROR
continue
else:
msg.state = AgentMsgState.RESPONSED
msg.resp_msg = resp_msg
asyncio.create_task(_process_msg_loop())
def _get_llm_result_type(self,result:str) -> str:
if result == "ignore":
@@ -200,16 +126,19 @@ class AIAgent:
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)
prompt = AgentPrompt()
prompt.append(self.prompt)
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":msg.sender,"content":msg.body}]
prompt.append(msg_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,the_role.get_name())) # chat context
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)
@@ -257,12 +186,13 @@ class AIAgent:
if is_ignore is not True:
# TODO : how to get inner chat session?
chatsession = self.get_chat_session(msg.sender)
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(final_result)
chatsession.append_post(resp_msg)
return resp_msg
@@ -277,25 +207,20 @@ class AIAgent:
def get_template_id(self) -> str:
return self.template_id
def get_chat_session_for_msg(self,msg:AgentMsg) -> AIChatSession:
pass
def get_chat_session(self,remote:str,topic_name:str=None) -> AIChatSession:
if topic_name is None:
topic_name = "_"
result_session = self.chat_sessions.get(topic_name + "@" + remote)
if result_session is not None:
return result_session
result_session = AIChatSession(self)
self.chat_sessions[topic_name + "@" + remote] = result_session
return result_session
def get_llm_model_name(self) -> str:
return self.llm_model_name
def get_max_token_size(self) -> int:
return self.max_token_size
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})
return result_prompt
+39
View File
@@ -0,0 +1,39 @@
from enum import Enum
import uuid
import time
class AgentMsgState(Enum):
RESPONSED = 0
INIT = 1
SENDING = 2
PROCESSING = 3
ERROR = 4
class AgentMsg:
def __init__(self) -> None:
self.create_time = 0
self.sender:str = None
self.target:str = None
self.body:str = None
self.topic:str = "T#" + uuid.uuid4().hex
#self.msg_type = 0
self.state = AgentMsgState.INIT
self.resp_msg = None
def set(self,sender:str,target:str,body:str,topic:str=None) -> None:
self.id = "msg#" + uuid.uuid4().hex
self.sender = sender
self.target = target
self.body = body
self.create_time = time.time()
if topic:
self.topic = topic
def get_msg_id(self) -> str:
return self.id
def get_sender(self) -> str:
return self.sender
def get_target(self) -> str:
return self.target
+129
View File
@@ -0,0 +1,129 @@
from typing import Any
from .agent_message import AgentMsg,AgentMsgState
import asyncio
from asyncio import Queue
import logging
logger = logging.getLogger(__name__)
class AIBusHandler:
def __init__(self,handler:Any) -> None:
self.handler = handler
self.working_task = None
self.results = {}
self.queue:Queue = Queue()
async def handle_message(self,msg:AgentMsg) -> Any:
if self.handler is None:
return None
return await self.handler(msg)
class AIBus:
_instance = None
@classmethod
def get_default_bus(cls):
if cls._instance is None:
cls._instance = 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:
handler = self.handlers.get(target_id)
if handler:
handler.queue.put_nowait(msg)
self.start_process(target_id)
return True
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)
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 get_message_resp(self,name:str,msg_id:str) -> AgentMsg:
handler = self.handlers.get(name)
if handler is None:
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)
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)
if resp is not None:
msg.resp_msg = resp
msg.state = AgentMsgState.RESPONSED
return resp
await asyncio.sleep(0.2)
retry_times += 1
if retry_times > 100:
msg.state = AgentMsgState.ERROR
return None
return None
def register_unhandle_message_handler(self,handler:Any) -> Queue:
self.unhandle_handler = handler
# means sub
def register_message_handler(self,handler_name:str,handler:Any) -> Queue:
handler_node = AIBusHandler(handler)
self.handlers[handler_name] = handler_node
return handler_node.queue
async def process_queue(self, handler:AIBusHandler):
while True:
# Wait for a message
message = await handler.queue.get()
try:
# Try to handle the message
result = await handler.handle_message(message)
handler.results[message.id] = result
except Exception as e:
# If an error occurs, put the message back into the queue
logger.error(f"handle message {message.id} failed! {e}")
#self.queues[name].put_nowait(message)
return
def start_process(self,target_name):
handler = self.handlers.get(target_name)
if handler is None:
logger.error(f"handler {target_name} not found!")
return
if handler.handler is None:
return
if handler.working_task is not None:
logger.warn(f"handler {target_name} is already working!")
return
handler.working_task = asyncio.create_task(self.process_queue(handler))
+253
View File
@@ -0,0 +1,253 @@
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 logging
import threading
import datetime
import uuid
from .agent_message import AgentMsg
class ChatSessionDB:
def __init__(self, db_file):
""" initialize db connection """
self.local = threading.local()
self.db_file = db_file
self._get_conn()
def _get_conn(self):
""" get db connection """
if not hasattr(self.local, 'conn'):
self.local.conn = self._create_connection(self.db_file)
return self.local.conn
def _create_connection(self, db_file):
""" create a database connection to a SQLite database """
conn = None
try:
conn = sqlite3.connect(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("""
CREATE TABLE IF NOT EXISTS ChatSessions (
SessionID TEXT PRIMARY KEY,
SessionOwner TEXT,
SessionTopic TEXT,
StartTime TEXT
);
""")
# create messages table
conn.execute("""
CREATE TABLE IF NOT EXISTS Messages (
MessageID TEXT PRIMARY KEY,
SessionID TEXT,
SenderID TEXT,
ReceiverID TEXT,
Timestamp TEXT,
Content TEXT,
Status INTEGER
);
""")
conn.commit()
except Error as e:
logging.error("Error occurred while creating tables: %s", e)
def insert_chatsession(self, session_id, session_owner,session_topic, start_time):
""" insert a new session into the ChatSessions table """
try:
conn = self._get_conn()
conn.execute("""
INSERT INTO ChatSessions (SessionID, SessionOwner,SessionTopic, StartTime)
VALUES (?,?, ?, ?)
""", (session_id, session_owner,session_topic, start_time))
conn.commit()
return 0 # return 0 if successful
except Error as e:
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):
""" insert a new message into the Messages table """
try:
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))
conn.commit()
return 0 # return 0 if successful
except Error as e:
logging.error("Error occurred while inserting message: %s", e)
return -1 # return -1 if an error occurs
def get_chatsession_by_id(self, session_id):
"""Get a message by its ID"""
conn = self._get_conn()
c = conn.cursor()
c.execute("SELECT * FROM ChatSessions WHERE SessionID = ?", (session_id,))
chatsession = c.fetchone()
return chatsession
def get_chatsession_by_owner_topic(self, owner_id, topic):
"""Get a chatsession by its owner and topic"""
conn = self._get_conn()
c = conn.cursor()
c.execute("SELECT * FROM ChatSessions WHERE SessionOwner = ? AND SessionTopic = ?", (owner_id,topic))
chatsession = c.fetchone()
return chatsession
def get_chatsessions(self, limit, offset):
""" retrieve sessions with pagination """
try:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM ChatSessions
ORDER BY StartTime DESC
LIMIT ? OFFSET ?
""", (limit, offset))
results = cursor.fetchall()
#self.close()
return results # return 0 and the result if successful
except Error as e:
logging.error("Error occurred while getting sessions: %s", e)
return -1, None # return -1 and None if an error occurs
def get_message_by_id(self, message_id):
"""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,))
message = c.fetchone()
return message
def get_messages(self, session_id, limit, offset):
""" retrieve messages of a session with pagination """
try:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute("""
SELECT MessageID,SessionID,SenderID,ReceiverID,Timestamp,Content,Status FROM Messages
WHERE SessionID = ?
ORDER BY Timestamp DESC
LIMIT ? OFFSET ?
""", (session_id, limit, offset))
results = cursor.fetchall()
#self.close()
return results # return 0 and the result if successful
except Error as e:
logging.error("Error occurred while getting messages: %s", e)
return -1, None # return -1 and None if an error occurs
def update_message_status(self, message_id, status):
""" update the status of a message """
try:
conn = self._get_conn()
conn.execute("""
UPDATE Messages
SET Status = ?
WHERE MessageID = ?
""", (status, message_id))
conn.commit()
return 0 # return 0 if successful
except Error as e:
logging.error("Error occurred while updating message status: %s", e)
return -1 # return -1 if an error occurs
# chat session store the chat history between owner and agent
# chat session might be large, so can read / write at stream mode.
class AIChatSession:
_dbs = {}
#@classmethod
#async def get_session_by_id(cls,session_id:str,db_path:str):
# db = cls._dbs.get(db_path)
# if db is None:
# db = ChatSessionDB(db_path)
# cls._dbs[db_path] = db
# db.get_chatsession_by_id(session_id)
# #result = AIChatSession()
@classmethod
def get_session(cls,owner_id:str,session_topic:str,db_path:str,auto_create = True) -> str:
db = cls._dbs.get(db_path)
if db is None:
db = ChatSessionDB(db_path)
cls._dbs[db_path] = db
result = None
session = db.get_chatsession_by_owner_topic(owner_id,session_topic)
if session is None:
if auto_create:
session_id = "CS#" + uuid.uuid4().hex
db.insert_chatsession(session_id,owner_id,session_topic,datetime.datetime.now())
result = AIChatSession(owner_id,session_id,db)
else:
result = AIChatSession(owner_id,session[0],db)
result.topic = session_topic
return result
def __init__(self,owner_id:str, session_id:str, db:ChatSessionDB) -> None:
self.owner_id :str = owner_id
self.session_id : str = session_id
self.db : ChatSessionDB = db
self.topic : str = None
self.start_time : str = None
def get_owner_id(self) -> str:
return self.owner_id
def read_history(self, number:int=10,offset=0) -> [AgentMsg]:
msgs = self.db.get_messages(self.session_id, number, offset)
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]
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)
#def attach_event_handler(self,handler) -> None:
# """chat session changed event handler"""
# pass
#TODO : add iterator interface for read chat history
+1 -4
View File
@@ -19,16 +19,13 @@ class ComputeKernel:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.is_start = False
else:
print("ComputeKernel is already created!")
return cls._instance
def __init__(self) -> None:
if self.is_start is True:
print("ComputeKernel is already start!")
return
print("init ComputeKernel!!!")
self.is_start = True
self.task_queue = Queue()
self.is_start = False
+2 -2
View File
@@ -54,7 +54,7 @@ class OpenAI_ComputeNode(ComputeNode):
logger.info(f"call openai {mode_name} prompts: {prompts}")
resp = openai.ChatCompletion.create(model=mode_name,
messages=prompts,
max_tokens=2000,
max_tokens=4000,
temperature=1.2)
logger.info(f"openai response: {resp}")
@@ -69,7 +69,7 @@ class OpenAI_ComputeNode(ComputeNode):
result.worker_id = self.node_id
result.result_str = resp["choices"][0]["message"]["content"]
result.result = resp["choices"][0]["message"]
return result
def start(self):
+46 -5
View File
@@ -1,25 +1,66 @@
from .agent import AIAgent
import logging
from .agent import AIAgent,AgentPrompt
class AIRole:
def __init__(self) -> None:
self.agent_instance_id : str = None
self.role_name : str = None
self.agent : AIAgent = None
self.fullname : str = None
self.agent_name : str = None
self.prompt : AgentPrompt = None
self.introduce : str = None
self.agent = None
def load_from_config(self,config:dict) -> bool:
pass
name_node = config.get("name")
if name_node is None:
logging.error("role name is not found!")
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!")
return False
self.agent_name = agent_id_node
prompt_node = config.get("prompt")
if prompt_node:
self.prompt = AgentPrompt()
if self.prompt.load_from_config(prompt_node) is False:
logging.error("load prompt failed!")
return False
intro_node = config.get("intro")
if intro_node is not None:
self.introduce = intro_node
def get_intro(self) -> str:
return self.introduce
def get_name(self) -> str:
return self.role_name
def get_prompt(self) -> AgentPrompt:
return self.prompt
class AIRoleGroup:
def __init__(self) -> None:
self.roles : dict[str,str]= None
pass
self.roles : dict[str,AIRole] = {}
def load_from_config(self,config:dict) -> bool:
for k,v in config.items():
role = AIRole()
if role.load_from_config(v) is False:
logging.error(f"load role {k} failed!")
return False
self.roles[k] = role
return True
def get(self,role_name:str) -> AIRole:
return self.roles.get(role_name)
+230 -120
View File
@@ -6,146 +6,217 @@ from typing import Optional,Tuple
from abc import ABC, abstractmethod
from .environment import Environment,EnvironmentEvent
from .agent import AgentPrompt,AgentMsg,AIChatSession
from .role import AIRole
from .agent_message import AgentMsg,AgentMsgState
from .agent import AgentPrompt,AgentMsg
from .chatsession import AIChatSession
from .role import AIRole,AIRoleGroup
from .ai_function import CallChain
from .compute_kernel import ComputeKernel
from .bus import AIBus
logger = logging.getLogger(__name__)
class MessageFilter:
def __init__(self) -> None:
pass
def select(self,msg:AgentMsg) -> AIRole:
pass
self.filters = {}
def select(self,msg:AgentMsg) -> str:
star_target = self.filters.get("*")
if star_target is not None:
return star_target
# TODO: add more filter
return None
def load_from_config(self,config:dict) -> bool:
self.filters = config
return True
class Workflow:
def __init__(self) -> None:
self.workflow_name : str = None
self.rule_prompt : AgentPrompt = None
self.workflow_config = None
self.role_group = None
self.role_group : dict = None
self.input_filter : MessageFilter= None
self.msg_queue = Queue()
self.connected_environment = {}
self.sub_workflows = {}
self.owner_workflow = None
self.db_file = None
self.is_start = False
self.msg_queue = Queue()
def get_bus(self) -> AIBus:
return AIBus.get_default_bus()
def set_owner(self,owner):
self.owner_workflow = owner
def load_from_config(self,config:dict) -> bool:
if config is None:
return False
def load_from_disk(self,config_path:str,context_dir_path) -> int:
pass
if config.get("name") is None:
logger.error("workflow config must have name")
return False
self.workflow_name = config.get("name")
#workflow is asynchronous.
# When processing one message, it can process another message at the same time.
# chatsession is synchronous, it has to wait for the previous message to finish processing before it can process the next message.
# Therefore, post a message needs to specify the session_id explicitly, if not specified it will be automatically created by workflow.
def post_msg(self,msg:AgentMsg) -> None:
self.msg_queue.put_nowait(msg)
return
async def send_msg(self,msg:AgentMsg) -> str:
pass
async def run(self):
# TODO add tracking design of msg processing
while True:
the_msg = await self._pop_msg()
chatsession:AIChatSession = self._get_chat_session_for_msg(the_msg)
if chatsession is None:
logger.error(f"get_chat_session_for_msg return None for :{the_msg}")
continue
#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("roles") is None:
logger.error("workflow config must have roles")
return False
self.role_group = AIRoleGroup()
if self.role_group.load_from_config(config.get("roles")) is False:
logger.error("Workflow load role_group failed")
return False
chatsession.append_recv(the_msg)
if config.get("input_filter") is not None:
self.input_filter = MessageFilter()
if self.input_filter.load_from_config(config.get("input_filter")) is False:
logger.error("Workflow load input_filter failed")
return False
sub_workflows = config.get("sub_workflows")
if sub_workflows is not None:
if self._load_sub_workflows(sub_workflows) is False:
logger.error("Workflow load sub workflows failed")
return False
#TODO: load env
async def _process_msg(msg:AgentMsg,the_role) -> None:
# prompt generat progress is most important part of workflow(app) develope
prompt = AgentPrompt()
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,the_role.get_name())) # chat context
return True
result = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
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
async def _process_msg(self,msg:AgentMsg):
final_result = None
chatsession = None
if self.input_filter is not None:
select_role_id = self.input_filter.select(msg)
if select_role_id is not None:
select_role = self.role_group.get(select_role_id)
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
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
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())
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
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)
# 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 final_result
case "ignore":
is_ignore = True
if is_ignore is not True:
# TODO : how to get inner chat session?
inner_chat_session = the_role.agent.get_chat_session_for_msg(msg)
if inner_chat_session is not None:
inner_chat_session.append_input(msg)
inner_chat_session.append_result(final_result)
return result
async def _workflow_process_msg(msg:AgentMsg) -> None:
final_result = None
if self.input_filter is not None:
select_role = self.input_filter.select(msg)
if select_role is not None:
result = await _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:
results = {}
for this_role in self.role_group.roles:
a_result = asyncio.create_task(_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)
if final_result is not None:
# TODO post message to source
pass
asyncio.create_task(_workflow_process_msg(the_msg))
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!")
return None
# prompt generat progress is most important part of workflow(app) develope
prompt = AgentPrompt()
prompt.append(the_role.agent.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
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":"user","content":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
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)
case "ignore":
is_ignore = True
if is_ignore:
return None
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
@@ -153,14 +224,25 @@ class Workflow:
def _get_chat_session_for_msg(self,msg:AgentMsg) -> AIChatSession:
pass
async def _get_prompt_from_session(self,chatsession:AIChatSession,role_name:str) -> AgentPrompt:
pass
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})
return result_prompt
def _get_msg_queue(self,session_id:str):
pass
def _merge_msg_result(self,results:dict) -> AgentMsg:
pass
# 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
@@ -168,20 +250,48 @@ class Workflow:
def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt:
pass
def _get_resp_prompt(self,resp:str,msg:AgentMsg,role:AIRole,prompt:AgentPrompt,chatsession:AIChatSession) -> AgentPrompt:
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:
pass
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:
pass
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"""