Add basic enviroment support. Can use env values in prompts right now.

This commit is contained in:
Liu Zhicong
2023-09-04 22:36:59 -07:00
parent 25bba0742a
commit 1bbdf2e722
8 changed files with 340 additions and 23 deletions
+2 -1
View File
@@ -7,4 +7,5 @@ from .compute_node import ComputeNode,LocalComputeNode
from .open_ai_node import OpenAI_ComputeNode
from .role import AIRole,AIRoleGroup
from .workflow import Workflow
from .bus import AIBus
from .bus import AIBus
from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent
+96 -4
View File
@@ -2,20 +2,112 @@
# 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
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]]= {}
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
@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.warn(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(key)
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
+55 -14
View File
@@ -2,6 +2,7 @@
import logging
import asyncio
import json
import os
from asyncio import Queue
from typing import Optional,Tuple
from abc import ABC, abstractmethod
@@ -14,6 +15,7 @@ from .role import AIRole,AIRoleGroup
from .ai_function import CallChain
from .compute_kernel import ComputeKernel
from .bus import AIBus
from .workflow_env import WorkflowEnvironment
logger = logging.getLogger(__name__)
@@ -56,6 +58,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()
@@ -100,6 +104,27 @@ class Workflow:
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:
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:
@@ -107,10 +132,12 @@ 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()
@@ -296,6 +323,13 @@ class Workflow:
logger.info(f"{the_role.role_id} post call {call[0]} with args {call[1]}")
return
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)
async def role_process_msg(self,msg:AgentMsg,the_role:AIRole):
session_topic = f"{msg.sender}#{msg.topic}"
@@ -316,8 +350,9 @@ class Workflow:
msg_prompt = AgentPrompt()
msg_prompt.messages = [{"role":"user","content":msg.body}]
prompt.append(msg_prompt)
self._format_msg_by_env_value(prompt)
async def _do_process_msg():
#TODO: send msg to agent might be better?
result_str = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size())
@@ -392,16 +427,22 @@ 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)
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
else:
logger.warn(f"environment {env.get_id()} already connected!")
+136
View File
@@ -0,0 +1,136 @@
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
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.event_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
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
# 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}")
View File