From 9814889d34eac302856e91001ab5fc7d3f91e70a Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 10 Aug 2023 23:57:02 -0700 Subject: [PATCH 001/177] Initial MVP project structs. --- build.sh | 0 buildin/agents/english-teacher/README | 1 + buildin/services/spiders/email-spider/README | 1 + doc/mvp/mvp plan.md | 169 +++++++++++++++++++ doc/mvp/package_manager.md | 13 ++ src/component/package_manager/README | 1 + src/kernel/README | 1 + src/service/app_manager/README | 1 + 8 files changed, 187 insertions(+) create mode 100644 build.sh create mode 100644 buildin/agents/english-teacher/README create mode 100644 buildin/services/spiders/email-spider/README create mode 100644 doc/mvp/mvp plan.md create mode 100644 doc/mvp/package_manager.md create mode 100644 src/component/package_manager/README create mode 100644 src/kernel/README create mode 100644 src/service/app_manager/README diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..e69de29 diff --git a/buildin/agents/english-teacher/README b/buildin/agents/english-teacher/README new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/buildin/agents/english-teacher/README @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/buildin/services/spiders/email-spider/README b/buildin/services/spiders/email-spider/README new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/buildin/services/spiders/email-spider/README @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/doc/mvp/mvp plan.md b/doc/mvp/mvp plan.md new file mode 100644 index 0000000..357e592 --- /dev/null +++ b/doc/mvp/mvp plan.md @@ -0,0 +1,169 @@ +# OpenDAN Basic Planning of MVP +0.5.1 Implement data capture into Knownlege-Base(KB) via Spider, followed by access by AI Agent (35%) +0.5.2 Build a Personal-Center based on the KB and associate the AI Agent with accessible telgram accounts (30%) +0.5.3 Release for waitlist (5%) +0.5.4 First public release (10%) +0.5.5 Incorporate modifications after the first public version, workload depends on feedback (15%) +0.5.6 Official version of MVP (5%) + +# Basic Architecture +(TODO) + +# R&D Process Management +Based on the project management module provided by SourceDAO, explore a new open source R&D process of "open source is mining". +0. Confirming Version Leader based on committee election. +1. Module (task division): Divide the system's futures into independent modules as much as possible, the development workload should be at the level of 1 person for 2-3 weeks. +2. Discussion: Discuss whether the module division is reasonable, and design it's *BUDGET* based on the difficulty of the module and its importance to the current version (most important step) +3. Recruit module PM. The module PM is responsible for the module's test delivery: completing the set functions, constructing the basic tests, and passing the self-tests. Testing should retain at least 30% of development resources. +4. For the completed module, PM should write and publicize the Project Proposal. It contains more detailed about module goals + design ideas, participating teams (if any, there should also be a preliminary division of work within the team and calculation of contribution value), and acceptance plan design. +5. The PM completes development and self-testing. Mark the module is *DONE*. +6. Version Leader organizes the acceptance of the module (a dedicated acceptor can be appointed). +7. Version Leader organizes integration testing according to the completion situation, and the module PM fixes BUGs. The test results can be used in the nightly-channel of OpenDAN. +8. After the test passes, the Version Leader announces the version release, anyone can use this from release-chanel. +9. The committee accepts the version after the release effect. After acceptance, all participants can extract contribution rewards. + +Difficulty is expressed in the mode of requirement engineer level * time (in weeks, less than 1 week is counted as 1 week). 1 week of work time is calculated as 20 hours. + + +**Below is the module division design for version 0.5.1.** + +# Package Management (Architect*2) +The package management system provides basic package definition and local lookup functionality. + +The types of packages are +1. Agent +2. Function +3. Service +4. App +5. Models (various AI models) + +The system itself is upgraded on this basis + +# Agent Management (Senior*1) +Define Agent package, and give it to the package management system for publishing/download/installation/running + +## Some pure prompt word engineering Agent (ordinary*1) +Can refer to the existing implementation of ChatGPT-Next-Web, mainly to let the system run as soon as possible + +# Agent (AI) Runtime (Architecture*2) +Implementing the Agent as a person to run is an important basic component +The Agent is an instance of a person, and the contents of all its sessions are interconnected. We can clone a new person based on an existing Agent, and this new Agent runs in a new container inherited from the old agent. +- Wrap LLM Kernel +- Manage chat-session +- Manage memory (one of the difficulties, different from Knowlege-Base) +- Manage identity and permissions +- Manage logs, for the convenience of development and debugging, we can see all the details of the key operation and self-thinking of the Agent. + +# Agent Work Flow (Architecture*2) +A team of AI Agents completes a complex task +Understanding this goal can better design the Agent running container +*Next version* + +# LLM Kernel Packaging (Architecture*1) +Encapsulate LLM, support Agent to use different LLM cores. We can support the GPT4 first. + +## Local LLM Integration (Senior*2) +If the integration is convenient, we can also integrate a local LLM core to see the effect as soon as possible. + +# Function Management (Architecture*1) +Define Function, Function itself is a stateless passive calculation. Easy to be recognized and called by Agent. +Function management needs to design the basic structure of Function, implement cloth/download/installation/running based on package management + +## Some basic standard Functions (Senior*2) +Can refer to some existing work, the main work in the early stage is standardization, as much as possible in information reading, the write operation that needs to be authorized by users needs to be promoted for consideration +Get the physical information of the specified location: time, temperature, humidity + +# Compute Runtime (Architecture*1) +Except for the Agent, all components run in isolated running containers +The Agent also runs in a container, and its isolation conditions may be stricter than the running container. +Built based on docker, + +# Compute Task Manager (Architecture*2) +Manage computational tasks that require more computational resources. +Compute tasks can be completed locally or on other devices. +Insufficient computing resources is the norm, and scheduling work should be done here. Normal queue, give way to urgent short tasks when necessary. + +# Spiders +## Spider Basic Architecture (Architecture*1 ) +## Local Storage Format Based on NamedObject Theory (Architecture*1 ) +## Email-Spider Senior*1 +No threshold at all, mainly to run the framework +## Twitter-Spider Senior*2 +For the convenience of ordinary users to install, do not use twitter api +But if it is really impossible to complete, then use twitter api (difficulty drops to ordinary*2) +## Facebook-Spider Senior*2 +For the convenience of ordinary users to install, do not use facebook api +But if it is really impossible to complete, then use facebook api (difficulty drops to ordinary*2) + +# Embedding Pipelines +## Piplene Basic Architecture (Architecture*3) +NamedObject+RawData => Vector DB +## Image-Embedding pipeline (Senior*3) +Implement Image vectorization pipeline (focus) +## Doc-Embedding pipeline (Senior*3) +Implement Text vectorization pipeline +## Code-Embedding pipeline (Senior*2) +## Video-Embedding pipeline (Senior*3) +## Audio-Embedding pipeline (Senior*3) + +# Knowledge Base (Architecture*2) +Provide a unified interface, support switching vector database kernel +Integrate open source vector database (pay attention to Lience selection) + +When designing the interface, prepare for future access control + +# 0.5.1 Integration Test (Senior*3) +Can be divided into two parts +1.Spider -> Pipline -> Knowledge Base +2.AI Agent <- Functions <- Knowledge Base +3.WebUI (not necessary) + +## Basic OS System Construction (Architecture*1) +The MVP version will not build a very serious system, the main goal is to set up an integrated test environment. + +# AI Gateway(Network OS BUS) +Obtain data by recognizing network data +The Gateway also provides an external access entrance for the entire system, and access control can be unified. +Provides the bus abstraction in the network operating system (the network cable is the bus), devices within the Zone are recognized by the system as plug-and-play devices, and can be called by applications/Agents +*Next version* + +# AI Browser +By cooperating with the browser, save the web pages that users have seen to capture data +*Next version* + +# IoT Functions +On the one hand, define standard Functions according to device types +On the other hand, it is necessary to crack some existing IoT devices so that they can be connected to the AI Gateway and can be controlled by the Agent +*Next version* + +# Personal Lora Model Building +*Next version* + +## Personal Image Lora +## Personal Voice Lora + +# Permission and Privacy Management +*Next version* + +# AI Capability (Function) Integration +*Next version* + +## Text2Image +Stable Diffusion core +Stable Diffusion model management +## Text2Voice + + +# Telegram API Integration +*Next version* + +# Web Version Personal Center +*Next version* + +# Built-in Agents +*Next version* + +# Built-in APPs +*Next version* + + diff --git a/doc/mvp/package_manager.md b/doc/mvp/package_manager.md new file mode 100644 index 0000000..c1084d4 --- /dev/null +++ b/doc/mvp/package_manager.md @@ -0,0 +1,13 @@ + +# Package Management System Introduction +The Package Manager is a fundamental component of the system for managing Packages. +The design of this system references Git - the distinction between client and server is not so significant, it is more about synchronization. + +# Problems to Solve +TODO + +# Design +TODO + +# Testing +TODO diff --git a/src/component/package_manager/README b/src/component/package_manager/README new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/src/component/package_manager/README @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/src/kernel/README b/src/kernel/README new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/src/kernel/README @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/src/service/app_manager/README b/src/service/app_manager/README new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/src/service/app_manager/README @@ -0,0 +1 @@ +TODO \ No newline at end of file From 3d8f6fd5c8f5cddb2b607fee54f6cf4a839ddb77 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 14 Aug 2023 12:44:19 -0700 Subject: [PATCH 002/177] Temporary commit --- buildin/agents/english-teacher/agent.cfg | 0 .../projectA/.pkg/pkg_nameB.zip | Bin 0 -> 22 bytes doc/mvp/pkg_system_demo/projectA/pkg.cfg.toml | 0 doc/mvp/pkg_system_demo/projectA/pkg.lock | 0 .../pkg_system_demo/projectA/pkg_index.toml | 0 src/component/agent_manager/__init__.py | 3 + src/component/agent_manager/agent.py | 3 + src/component/agent_manager/agent_manager.py | 40 ++++++ src/component/agent_manager/templete.py | 3 + src/component/ndn_client/__init__.py | 1 + src/component/ndn_client/cid.py | 8 ++ src/component/package_manager/__init__.py | 0 src/component/package_manager/env.py | 124 ++++++++++++++++++ src/component/package_manager/index_db.py | 0 src/component/package_manager/index_syncer.py | 1 + src/component/package_manager/installer.py | 102 ++++++++++++++ src/component/package_manager/media_reader.py | 0 src/component/package_manager/pkg.py | 22 ++++ test/agent_test.py | 35 +++++ test/pkg.cfg.toml | 15 +++ test/spider_test.py | 23 ++++ 21 files changed, 380 insertions(+) create mode 100644 buildin/agents/english-teacher/agent.cfg create mode 100644 doc/mvp/pkg_system_demo/projectA/.pkg/pkg_nameB.zip create mode 100644 doc/mvp/pkg_system_demo/projectA/pkg.cfg.toml create mode 100644 doc/mvp/pkg_system_demo/projectA/pkg.lock create mode 100644 doc/mvp/pkg_system_demo/projectA/pkg_index.toml create mode 100644 src/component/agent_manager/__init__.py create mode 100644 src/component/agent_manager/agent.py create mode 100644 src/component/agent_manager/agent_manager.py create mode 100644 src/component/agent_manager/templete.py create mode 100644 src/component/ndn_client/__init__.py create mode 100644 src/component/ndn_client/cid.py create mode 100644 src/component/package_manager/__init__.py create mode 100644 src/component/package_manager/env.py create mode 100644 src/component/package_manager/index_db.py create mode 100644 src/component/package_manager/index_syncer.py create mode 100644 src/component/package_manager/installer.py create mode 100644 src/component/package_manager/media_reader.py create mode 100644 src/component/package_manager/pkg.py create mode 100644 test/agent_test.py create mode 100644 test/pkg.cfg.toml create mode 100644 test/spider_test.py diff --git a/buildin/agents/english-teacher/agent.cfg b/buildin/agents/english-teacher/agent.cfg new file mode 100644 index 0000000..e69de29 diff --git a/doc/mvp/pkg_system_demo/projectA/.pkg/pkg_nameB.zip b/doc/mvp/pkg_system_demo/projectA/.pkg/pkg_nameB.zip new file mode 100644 index 0000000000000000000000000000000000000000..15cb0ecb3e219d1701294bfdf0fe3f5cb5d208e7 GIT binary patch literal 22 NcmWIWW@Tf*000g10H*)| literal 0 HcmV?d00001 diff --git a/doc/mvp/pkg_system_demo/projectA/pkg.cfg.toml b/doc/mvp/pkg_system_demo/projectA/pkg.cfg.toml new file mode 100644 index 0000000..e69de29 diff --git a/doc/mvp/pkg_system_demo/projectA/pkg.lock b/doc/mvp/pkg_system_demo/projectA/pkg.lock new file mode 100644 index 0000000..e69de29 diff --git a/doc/mvp/pkg_system_demo/projectA/pkg_index.toml b/doc/mvp/pkg_system_demo/projectA/pkg_index.toml new file mode 100644 index 0000000..e69de29 diff --git a/src/component/agent_manager/__init__.py b/src/component/agent_manager/__init__.py new file mode 100644 index 0000000..863c4d1 --- /dev/null +++ b/src/component/agent_manager/__init__.py @@ -0,0 +1,3 @@ +from .agent_manager import agent_manager,agent_manager_client +from .agent import ai_agent +from .templete import ai_agent_templete \ No newline at end of file diff --git a/src/component/agent_manager/agent.py b/src/component/agent_manager/agent.py new file mode 100644 index 0000000..26f8b71 --- /dev/null +++ b/src/component/agent_manager/agent.py @@ -0,0 +1,3 @@ +class ai_agent: + def __init__(self) -> None: + pass \ No newline at end of file diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py new file mode 100644 index 0000000..d837175 --- /dev/null +++ b/src/component/agent_manager/agent_manager.py @@ -0,0 +1,40 @@ + +from .agent import ai_agent +from .templete import ai_agent_templete + +class agent_manager: + _instance = None + + def __init__(self) -> None: + pass + + def __new__(cls): + if cls._instance is None: + cls._instance = super(agent_manager, cls).__new__(cls) + return cls._instance + + + def initial(self,root_dir:str) -> None: + pass + + + def get(self,agent_id) -> ai_agent: + pass + + + def get_templete(self,templete_id) -> ai_agent_templete: + pass + + def install(self,templete_id) -> int: + + installer = None + #installer.install(templete_id) + pass + + def create(self,template,agent_name,agent_last_name,agent_introduce) -> ai_agent: + pass + + +class agent_manager_client: + def __init__(self) -> None: + pass \ No newline at end of file diff --git a/src/component/agent_manager/templete.py b/src/component/agent_manager/templete.py new file mode 100644 index 0000000..87540bf --- /dev/null +++ b/src/component/agent_manager/templete.py @@ -0,0 +1,3 @@ +class ai_agent_templete: + def __init__(self) -> None: + pass \ No newline at end of file diff --git a/src/component/ndn_client/__init__.py b/src/component/ndn_client/__init__.py new file mode 100644 index 0000000..6b4585c --- /dev/null +++ b/src/component/ndn_client/__init__.py @@ -0,0 +1 @@ +from .cid import content_id \ No newline at end of file diff --git a/src/component/ndn_client/cid.py b/src/component/ndn_client/cid.py new file mode 100644 index 0000000..981df3b --- /dev/null +++ b/src/component/ndn_client/cid.py @@ -0,0 +1,8 @@ + + +class content_id: + def __init__(self) -> None: + pass + + def create_from_str(cid_str:str): + pass \ No newline at end of file diff --git a/src/component/package_manager/__init__.py b/src/component/package_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py new file mode 100644 index 0000000..a5a463b --- /dev/null +++ b/src/component/package_manager/env.py @@ -0,0 +1,124 @@ + +import logging +import toml + +from .pkg import pkg_info,pkg_media_info +from .installer import pkg_installer + +logger = logging.getLogger(__name__) + +#pkg_env_mgr,以cfg_path为key管理多个pkg_env,是个单件 +class pkg_env_mgr: + _instance = None + def __new__(cls): + if cls._instance is None: + cls._instance = super(pkg_env_mgr, cls).__new__(cls) + return cls._instance + + def __init__(self) -> None: + self._pkg_envs = {} + pass + + def get_env(self,cfg_path:str) -> pkg_env: + if cfg_path in self._pkg_envs: + return self._pkg_envs[cfg_path] + else: + pkg_env = pkg_env(cfg_path) + self._pkg_envs[cfg_path] = pkg_env + return pkg_env + + def get_user_env(self) -> pkg_env: + pass + + def get_system_env(self) -> pkg_env: + pass + +class pkg_env: + def __init__(self,cfg_path:str,inheritance = True) -> None: + self.prefixs : list[str]= [] + self.is_strict : bool = True + self.parent_envs : list[pkg_env] = [pkg_env_mgr().get_user_env()] + self.index_dbs = None + + self.cfg_path = cfg_path + self._load_pkg_cfg(cfg_path) + pass + + def load(self,pkg_name:str,cid:None) -> pkg_media_info: + if cid is None: + #lookup pkg's cid from indexDb + pkg_id,verion_str,cid = pkg_info.parse_pkg_name(pkg_name) + if cid is None: + #lookup include pkg-index-db search + pkg_info = self.lookup(pkg_id,verion_str) + if pkg_info is not None: + return self.load(pkg_id,pkg_info.cid) + + if self.is_strict is False: + #load pkg not in index-db, easy for debug + for prefix in self.prefixs: + fullpath = prefix + pkg_name + logger.debug("try load {pkg_name} at {fullpath} ...") + if pkg_env.is_valied_media(fullpath): + return self.load_pkg_media_info(fullpath) + + logger.warn(f"load {pkg_name} failed: pkg not found in index-db and pkg_dirs") + return None + else: + #got cid here~ + return self.load(pkg_id,cid) + else: + #search pkg_id#cid and load pkg_media_info + for prefix in self.prefixs: + fullpath =f"{prefix}{pkg_id}#{cid}" + logger.debug(f"try load {pkg_name} from {fullpath}") + if pkg_env.is_valied_media(fullpath): + media_info = self.load_pkg_media_info(fullpath) + return media_info + + logger.warn(f"load {pkg_id}#{cid} error,not found") + + + def get_pkg_media_info(self,pkg_name:str)->pkg_media_info: + pass + + def load_pkg_media_info(self,pkg_full_path:str) -> pkg_media_info: + pass + + + def lookup(self,pkg_id:str,version_str:str) -> pkg_info: + + pass + + def get_installer(self) -> pkg_installer: + pass + + @classmethod + def is_valied_media(pkg_full_path:str) -> bool: + pass + + + def _load_pkg_cfg(self,cfg_path:str): + if cfg_path is None: + return + + cfg = None + if len(cfg_path) < 1: + return + try: + cfg = toml.load(cfg_path) + except Exception as e: + logger.error(f"read pkg cfg from {cfg_path} failed! unexpected error occurred: {str(e)}") + return + + if cfg: + if cfg.env: + if cfg.env.is_strict is not None: + self.is_strict = cfg.env.is_strict + + if cfg.env.prefixs is not None: + self.prefixs = self._preprocess_prefixs(cfg.env.prefixs) + + + def _preprocess_prefixs(self,prefixs): + pass \ No newline at end of file diff --git a/src/component/package_manager/index_db.py b/src/component/package_manager/index_db.py new file mode 100644 index 0000000..e69de29 diff --git a/src/component/package_manager/index_syncer.py b/src/component/package_manager/index_syncer.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/component/package_manager/index_syncer.py @@ -0,0 +1 @@ + diff --git a/src/component/package_manager/installer.py b/src/component/package_manager/installer.py new file mode 100644 index 0000000..83ed220 --- /dev/null +++ b/src/component/package_manager/installer.py @@ -0,0 +1,102 @@ +# installer download pkg by cid, than install it to target dir +import logging +import asyncio +import aiohttp +import aiofiles + +from threading import RLock + +from ndn_client import content_id +from .pkg import pkg_info +from .env import pkg_env + +logger = logging.getLogger(__name__) + +INSTALL_TASK_STATE_DONE = 0 +INSTALL_TASK_STATE_CHECK_DEPENDENCY = 1 +INSTALL_TASK_STATE_INSTALL_DEPENDENCY = 2 +INSTALL_TASK_STATE_DOWNLOADING = 3 +INSTALL_TASK_STATE_INSTALLING = 4 +INSTALL_TAKS_STATE_ERROR = 5 + +class install_task: + def __init__(self,owner:pkg_env) -> None: + self.owner = owner + self.state = INSTALL_TASK_STATE_CHECK_DEPENDENCY + self.working_task = None + self.error_str = None + + + +class pkg_installer: + def __init__(self,owner_env:pkg_env) -> None: + self.all_tasks = {} + self.owner_env = owner_env + + def install(self,pkg_name:str, + install_from_dependency = False, can_upgrade = True,options = None)->Tuple[install_task,str]: + media_info = self.owner_env.get_media_info(pkg_name) + is_upgrade = False + if media_info is not None: + is_upgrade = True + if can_upgrade is False: + error_str = f"pkg:{pkg_name} already installed and can't upgrade" + logger.error(error_str) + return None,error_str + + pkg_id,version_str,content_id = pkg_info.parse_pkg_name(pkg_name) + + the_pkg_info = self.owner_env.lookup(pkg_id,version_str,content_id) + if the_pkg_info is None: + error_str = f"pkg:{pkg_name} ,content_id:{content_id} not found in index db" + logger.error(error_str) + return None,error_str + + result_task = self.all_tasks.get(the_pkg_info.cid) + if result_task is not None: + return result_task,None + + result_task = install_task() + self.all_tasks[the_pkg_info.cid] = result_task + async def install_pkg(): + # check dependency + if await self.check_dependency(the_pkg_info) is False: + error_str = f"pkg:{pkg_name} check dependency failed" + logger.error(error_str) + result_task.error_str = error_str + result_task.state = INSTALL_TAKS_STATE_ERROR + return + + # install dependency + result_task.state = INSTALL_TASK_STATE_INSTALL_DEPENDENCY + for depend_pkg_name in the_pkg_info.depends: + # TODO:这里可能需要用安装队列,直接等待安装完成可能会太慢,也可能会导致循环等待 + sub_task,err_str = self.install(depend_pkg_name,True,can_upgrade,options) + + + result_task.working_task = asyncio.create_task(install_pkg()) + return result_task,None + + + def uninstall(self): + pass + + async def check_dependency(self,pkg:pkg_info) -> bool: + #TODO:这里可能会有循环依赖导致的递归无法退出的问题,需要记录已经检查过的pkg + for depend_pkg_name in pkg.depends: + depend_pkg_info = self.owner_env.lookup(depend_pkg_name) + if depend_pkg_info is None: + logger.warn(f"{pkg.name}'s depend pkg {depend_pkg_name} not found in index db") + return False + + if await self.check_dependency(depend_pkg_info) is False: + return False + + return True + + + + + + + diff --git a/src/component/package_manager/media_reader.py b/src/component/package_manager/media_reader.py new file mode 100644 index 0000000..e69de29 diff --git a/src/component/package_manager/pkg.py b/src/component/package_manager/pkg.py new file mode 100644 index 0000000..abaeb25 --- /dev/null +++ b/src/component/package_manager/pkg.py @@ -0,0 +1,22 @@ + +class pkg_info: + def __init__(self) -> None: + self.name = "" + self.cid = None + self.depends : list[str] = None + pass + + def parse_pkg_name(pkg_name:str) -> Tuple[str, str, str]: + #return pkg_id,version_str,cid + pass + + @property + def cid(self) -> str: + return self.cid + +class pkg_media_info: + def __init__(self) -> None: + pass + + + diff --git a/test/agent_test.py b/test/agent_test.py new file mode 100644 index 0000000..e0fd3e3 --- /dev/null +++ b/test/agent_test.py @@ -0,0 +1,35 @@ +import sys +sys.path.append('../src/component/') + +from agent_manager import agent_manager + +def clean_agent(): + print("clean_agent") + +def clean_agent_templete(): + print("clean_agent_templte") + +def test_agent(): + am = agent_manager() + am.initial("root_dir") + agent = am.get("english_teacher") + if agent is None: + agent_templete = am.get_templete("english_teacher") + + if agent_templete is None : + op = am.install("english_teacher") + #wait install done + + agent = am.create(agent_templete,"Tracy","Wang","Tracy Wang is my english teacher") + + print("Agent Tracy Wang load success!"); + + #print(agent.get_introduce()); + + #chat_session = agent.get_default_chat_session("master"); + #chat_session.chat("给我讲一个英文笑话!"); + #chat_session.wait_response(); + #print(chat_session.last_msg()); + +if __name__ == "__main__": + test_agent() \ No newline at end of file diff --git a/test/pkg.cfg.toml b/test/pkg.cfg.toml new file mode 100644 index 0000000..46acf0a --- /dev/null +++ b/test/pkg.cfg.toml @@ -0,0 +1,15 @@ + +[authors] +"buckyos.org" = {public="aaaa",name="BuckyOS Core Dev Team"} +[authors."cyfs.com"] +public="bbbb" +name="CYFS Core Dev Team" + +[env] +is_strict = false +prefixs = ["default","../m/s","abc"] +obj2 = {k1="bb",k2=300} +[env.obj] +k1="aa" +k2=200 + diff --git a/test/spider_test.py b/test/spider_test.py new file mode 100644 index 0000000..296f382 --- /dev/null +++ b/test/spider_test.py @@ -0,0 +1,23 @@ + + +def test_main(): + sm = service_manager() + + knownlege_base_service = sm.get('knowlege_base') + knownlege_base_service.start() + + email_spider = sm.get('email_spider') + email_spider.start() + + doc_embeding_service = sm.get('doc_embeding_service') + doc_embeding_service.start() + + ia = agents_manager().get("ai_info_assistor") + chat_session = ia.get_default_chat_session("master"); + chat_session.chat("Who responded to my issue last week"); + chat_session.wait_response(); + #print(chat_session.last_msg()); + +if __name__ == '__main__': + test_main() + \ No newline at end of file From 6b39379d7cb7829278865a2ef7216ecc9d705261 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 16 Aug 2023 20:06:09 -0700 Subject: [PATCH 003/177] complete pkg_system basic design and impl --- doc/mvp/pkg_install_procedure.drawio | 377 ++++++++++++++++++ .../{projectA => demo1}/pkg.cfg.toml | 0 .../{projectA => demo1}/pkg.lock | 0 doc/mvp/pkg_system_demo/demo1/pkg_index.toml | 3 + .../.pkg => demo1/pkgs}/pkg_nameB.zip | Bin .../pkg_system_demo/projectA/pkg_index.toml | 0 src/component/ndn_client/ndn_client.py | 13 + src/component/package_manager/env.py | 88 ++-- src/component/package_manager/installer.py | 142 +++++-- src/component/package_manager/pkg.py | 7 +- 10 files changed, 554 insertions(+), 76 deletions(-) create mode 100644 doc/mvp/pkg_install_procedure.drawio rename doc/mvp/pkg_system_demo/{projectA => demo1}/pkg.cfg.toml (100%) rename doc/mvp/pkg_system_demo/{projectA => demo1}/pkg.lock (100%) create mode 100644 doc/mvp/pkg_system_demo/demo1/pkg_index.toml rename doc/mvp/pkg_system_demo/{projectA/.pkg => demo1/pkgs}/pkg_nameB.zip (100%) delete mode 100644 doc/mvp/pkg_system_demo/projectA/pkg_index.toml create mode 100644 src/component/ndn_client/ndn_client.py diff --git a/doc/mvp/pkg_install_procedure.drawio b/doc/mvp/pkg_install_procedure.drawio new file mode 100644 index 0000000..b77bb77 --- /dev/null +++ b/doc/mvp/pkg_install_procedure.drawio @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/mvp/pkg_system_demo/projectA/pkg.cfg.toml b/doc/mvp/pkg_system_demo/demo1/pkg.cfg.toml similarity index 100% rename from doc/mvp/pkg_system_demo/projectA/pkg.cfg.toml rename to doc/mvp/pkg_system_demo/demo1/pkg.cfg.toml diff --git a/doc/mvp/pkg_system_demo/projectA/pkg.lock b/doc/mvp/pkg_system_demo/demo1/pkg.lock similarity index 100% rename from doc/mvp/pkg_system_demo/projectA/pkg.lock rename to doc/mvp/pkg_system_demo/demo1/pkg.lock diff --git a/doc/mvp/pkg_system_demo/demo1/pkg_index.toml b/doc/mvp/pkg_system_demo/demo1/pkg_index.toml new file mode 100644 index 0000000..f72d8a4 --- /dev/null +++ b/doc/mvp/pkg_system_demo/demo1/pkg_index.toml @@ -0,0 +1,3 @@ +[authors] + + diff --git a/doc/mvp/pkg_system_demo/projectA/.pkg/pkg_nameB.zip b/doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameB.zip similarity index 100% rename from doc/mvp/pkg_system_demo/projectA/.pkg/pkg_nameB.zip rename to doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameB.zip diff --git a/doc/mvp/pkg_system_demo/projectA/pkg_index.toml b/doc/mvp/pkg_system_demo/projectA/pkg_index.toml deleted file mode 100644 index e69de29..0000000 diff --git a/src/component/ndn_client/ndn_client.py b/src/component/ndn_client/ndn_client.py new file mode 100644 index 0000000..c507fb1 --- /dev/null +++ b/src/component/ndn_client/ndn_client.py @@ -0,0 +1,13 @@ + +from .cid import content_id + +class ndn_get_task: + def __init__(self) -> None: + pass + +class ndn_client: + def __init__(self): + pass + + def get_file(cid:content_id,target_path:str,urls:{}=None,options:{}=None): + pass \ No newline at end of file diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py index a5a463b..85c6b2a 100644 --- a/src/component/package_manager/env.py +++ b/src/component/package_manager/env.py @@ -7,7 +7,6 @@ from .installer import pkg_installer logger = logging.getLogger(__name__) -#pkg_env_mgr,以cfg_path为key管理多个pkg_env,是个单件 class pkg_env_mgr: _instance = None def __new__(cls): @@ -17,6 +16,7 @@ class pkg_env_mgr: def __init__(self) -> None: self._pkg_envs = {} + pass def get_env(self,cfg_path:str) -> pkg_env: @@ -34,60 +34,70 @@ class pkg_env_mgr: pass class pkg_env: - def __init__(self,cfg_path:str,inheritance = True) -> None: - self.prefixs : list[str]= [] + def __init__(self,cfg_path:str) -> None: + self.pkg_dir : str = "" + self.pkg_obj_dir : str = "" self.is_strict : bool = True - self.parent_envs : list[pkg_env] = [pkg_env_mgr().get_user_env()] + self.parent_envs : list[pkg_env] = None self.index_dbs = None - + self.cfg_path = cfg_path self._load_pkg_cfg(cfg_path) pass - def load(self,pkg_name:str,cid:None) -> pkg_media_info: + def load(self,pkg_name:str,search_parent=True) -> pkg_media_info: + pkg_path = None + pkg_id,verion_str,cid = pkg_info.parse_pkg_name(pkg_name) + if cid is None: - #lookup pkg's cid from indexDb - pkg_id,verion_str,cid = pkg_info.parse_pkg_name(pkg_name) - if cid is None: - #lookup include pkg-index-db search - pkg_info = self.lookup(pkg_id,verion_str) - if pkg_info is not None: - return self.load(pkg_id,pkg_info.cid) - - if self.is_strict is False: - #load pkg not in index-db, easy for debug - for prefix in self.prefixs: - fullpath = prefix + pkg_name - logger.debug("try load {pkg_name} at {fullpath} ...") - if pkg_env.is_valied_media(fullpath): - return self.load_pkg_media_info(fullpath) - - logger.warn(f"load {pkg_name} failed: pkg not found in index-db and pkg_dirs") - return None + if verion_str is None: + channel = self.get_pkg_channel_from_version(verion_str) + if channel is None: + pkg_path = f"{self.pkg_dir}{pkg_id}" + else: + pkg_path = f"{self.pkg_dir}{pkg_id}#{channel}" else: - #got cid here~ - return self.load(pkg_id,cid) + channel = self.get_pkg_channel_from_version(verion_str) + the_version = self.get_exact_version_from_installed(verion_str) + if the_version is None: + logger.warn(f"load {pkg_name} failed: no match version from {verion_str}") + return None + if channel is None: + pkg_path = f"{self.pkg_dir}{pkg_id}#{the_version}" + else: + pkg_path = f"{self.pkg_dir}{pkg_id}#{channel}#{the_version}" else: - #search pkg_id#cid and load pkg_media_info - for prefix in self.prefixs: - fullpath =f"{prefix}{pkg_id}#{cid}" - logger.debug(f"try load {pkg_name} from {fullpath}") - if pkg_env.is_valied_media(fullpath): - media_info = self.load_pkg_media_info(fullpath) - return media_info - - logger.warn(f"load {pkg_id}#{cid} error,not found") + pkg_path = f"{self.pkg_obj_dir}.{pkg_id}/{cid}" + + media_info = self.try_load_pkg_media_info(pkg_id,pkg_path) + if media_info is None: + if search_parent: + for parent_env in self.parent_envs: + media_info = parent_env.load(pkg_id,cid,False) + if media_info is not None: + return media_info + + logger.warn(f"load {pkg_id}#{cid} error,not found ,search_parent={search_parent}") + return None + def get_exact_version_from_installed(self,verion_str:str) -> str: + pass + + def get_pkg_channel_from_version(self,pkg_version:str) -> str: + pass def get_pkg_media_info(self,pkg_name:str)->pkg_media_info: pass - def load_pkg_media_info(self,pkg_full_path:str) -> pkg_media_info: + def try_load_pkg_media_info(self,pkg_full_path:str) -> pkg_media_info: pass + def get_installed_pkg_info(self,pkg_name:str) -> pkg_info: + pass + def lookup(self,pkg_id:str,version_str:str) -> pkg_info: - + # to make sure pkg.cid is correct, we MUST verfiy eveything here pass def get_installer(self) -> pkg_installer: @@ -96,7 +106,9 @@ class pkg_env: @classmethod def is_valied_media(pkg_full_path:str) -> bool: pass - + + def do_pkg_media_trans(self,pkg_info:pkg_info,source_path:str,target_path:str) -> bool: + pass def _load_pkg_cfg(self,cfg_path:str): if cfg_path is None: diff --git a/src/component/package_manager/installer.py b/src/component/package_manager/installer.py index 83ed220..894de8f 100644 --- a/src/component/package_manager/installer.py +++ b/src/component/package_manager/installer.py @@ -3,13 +3,13 @@ import logging import asyncio import aiohttp import aiofiles +import os -from threading import RLock - -from ndn_client import content_id -from .pkg import pkg_info +from ndn_client import content_id,ndn_client +from .pkg import pkg_info,pkg_media_info from .env import pkg_env + logger = logging.getLogger(__name__) INSTALL_TASK_STATE_DONE = 0 @@ -23,73 +23,141 @@ class install_task: def __init__(self,owner:pkg_env) -> None: self.owner = owner self.state = INSTALL_TASK_STATE_CHECK_DEPENDENCY + + self.pkg_media_info = None self.working_task = None + self.dependency_tasks = None self.error_str = None - - class pkg_installer: def __init__(self,owner_env:pkg_env) -> None: self.all_tasks = {} self.owner_env = owner_env def install(self,pkg_name:str, - install_from_dependency = False, can_upgrade = True,options = None)->Tuple[install_task,str]: - media_info = self.owner_env.get_media_info(pkg_name) - is_upgrade = False - if media_info is not None: - is_upgrade = True - if can_upgrade is False: - error_str = f"pkg:{pkg_name} already installed and can't upgrade" - logger.error(error_str) - return None,error_str - - pkg_id,version_str,content_id = pkg_info.parse_pkg_name(pkg_name) + install_from_dependency = False, can_upgrade = True,skip_depends = False,options = None)->Tuple[install_task,str]: + + the_pkg_info : pkg_info = None + is_upgrade : bool = False + need_backup : bool = False + + pkg_id,version_str,cid = pkg_info.parse_pkg_name(pkg_name) + media_info : pkg_media_info = self.owner_env.get_media_info(pkg_name) # must use index-db? + if media_info is not None: + if cid is not None: + if can_upgrade: + is_upgrade = True + else: + error_str = f"{pkg_name},{cid} already installed!" + logger.error(error_str) + return None,error_str + else: + the_pkg_info = self.owner_env.lookup(pkg_id,version_str,None) + if the_pkg_info is None: + error_str = f"{pkg_name} old version exist in local but not found in index db!" + logger.error(error_str) + return None,error_str + else: + is_upgrade = True + need_backup = True - the_pkg_info = self.owner_env.lookup(pkg_id,version_str,content_id) if the_pkg_info is None: - error_str = f"pkg:{pkg_name} ,content_id:{content_id} not found in index db" + the_pkg_info = self.owner_env.lookup(pkg_id,version_str,cid) + + if the_pkg_info is None: + error_str = f"{pkg_name} ,cid:{cid} not found in index db" logger.error(error_str) return None,error_str result_task = self.all_tasks.get(the_pkg_info.cid) if result_task is not None: - return result_task,None - + return result_task,"already installing" + + logger.info(f"start download&install {pkg_name},install_from_dependency={install_from_dependency},upgrade={is_upgrade},backup={need_backup},target_pkg_info={the_pkg_info}") result_task = install_task() self.all_tasks[the_pkg_info.cid] = result_task - async def install_pkg(): + async def download_and_install_pkg()->int: # check dependency - if await self.check_dependency(the_pkg_info) is False: - error_str = f"pkg:{pkg_name} check dependency failed" - logger.error(error_str) - result_task.error_str = error_str - result_task.state = INSTALL_TAKS_STATE_ERROR - return + if skip_depends is False: + result_task.dependency_tasks = {} + self.get_dependency_tasks(the_pkg_info,result_task.dependency_tasks) + result_task.state = INSTALL_TASK_STATE_INSTALL_DEPENDENCY + for depend_pkg_name in result_task.dependency_tasks: + # check pkg in local? + # install miss pkg + pass - # install dependency - result_task.state = INSTALL_TASK_STATE_INSTALL_DEPENDENCY - for depend_pkg_name in the_pkg_info.depends: - # TODO:这里可能需要用安装队列,直接等待安装完成可能会太慢,也可能会导致循环等待 - sub_task,err_str = self.install(depend_pkg_name,True,can_upgrade,options) + result_task.state = INSTALL_TASK_STATE_DOWNLOADING + install_full_path = "" + target_full_path = "" + old_package_full_path = "" + is_download_directy = False + + if the_pkg_info.target_media_type == the_pkg_info.source_media_type: + is_download_directy = True + if is_upgrade: + target_full_path = "" + else: + target_full_path = "" + else: + pass + + urls = self.owner_env.get_pkg_urls(the_pkg_info) + #download + client = ndn_client() # set watch + download_result = await client.get_file(the_pkg_info.cid,urls,target_full_path,options) + if download_result !=0: + result_task.state = INSTALL_TAKS_STATE_ERROR + return result_task.state + + result_task.state = INSTALL_TASK_STATE_INSTALLING + if is_download_directy is False: + install_media_result = False + install_media_result = await self.owner_env.do_pkg_media_trans(the_pkg_info,target_full_path,install_full_path) + if install_media_result is False: + result_task.state = INSTALL_TAKS_STATE_ERROR + result_task.error_str = "install media error,from {target_full_path} to {install_full_path}" + return result_task.state + + # last step,save install flag : install by manual or install by dependency + ## save cid dir + if is_upgrade: + os.rename(old_package_full_path, old_package_full_path + ".old" ) + os.rename(target_full_path,install_full_path) + ## update/create version link + + ## update pkg state + ## remove old version + + result_task.state = INSTALL_TASK_STATE_DONE + return result_task.state - result_task.working_task = asyncio.create_task(install_pkg()) + result_task.working_task = asyncio.create_task(download_and_install_pkg()) return result_task,None def uninstall(self): pass + + def get_dependency_tasks(self,pkg:pkg_info,dependency_tasks): + pass - async def check_dependency(self,pkg:pkg_info) -> bool: - #TODO:这里可能会有循环依赖导致的递归无法退出的问题,需要记录已经检查过的pkg + async def check_dependency(self,pkg:pkg_info,task_list:{}) -> bool: for depend_pkg_name in pkg.depends: + depend_task = task_list.get(depend_pkg_name) + if depend_task is not None: + logger.debug(f"{pkg.name}'s depend pkg {depend_pkg_name} already in task list") + continue + depend_task = install_task() + task_list[depend_pkg_name] = depend_task + depend_pkg_info = self.owner_env.lookup(depend_pkg_name) if depend_pkg_info is None: logger.warn(f"{pkg.name}'s depend pkg {depend_pkg_name} not found in index db") return False - if await self.check_dependency(depend_pkg_info) is False: + if await self.check_dependency(depend_pkg_info,task_list) is False: return False return True diff --git a/src/component/package_manager/pkg.py b/src/component/package_manager/pkg.py index abaeb25..ebc9194 100644 --- a/src/component/package_manager/pkg.py +++ b/src/component/package_manager/pkg.py @@ -4,7 +4,12 @@ class pkg_info: self.name = "" self.cid = None self.depends : list[str] = None - pass + self.author = None + self.remote_urls = None + self.target_media_type = "dir" + self.source_media_type = "7z" + + def parse_pkg_name(pkg_name:str) -> Tuple[str, str, str]: #return pkg_id,version_str,cid From 814f5cf481feb86d61b68e6684b22727b72a131c Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 20 Aug 2023 22:53:35 -0700 Subject: [PATCH 004/177] complete ai kerne frame code --- doc/mvp/compute_task.drawio | 76 ++++ doc/mvp/workflow.drawio | 368 ++++++++++++++++++ src/{kernel => aios_kernel}/README | 0 src/aios_kernel/__init__.py | 0 src/aios_kernel/agent.py | 58 +++ src/aios_kernel/agent_group.py | 20 + src/aios_kernel/aios_shell.py | 6 + src/aios_kernel/compute_kernel.py | 29 ++ src/aios_kernel/compute_node.py | 13 + src/aios_kernel/environment.py | 12 + src/aios_kernel/llm_kernel.py | 14 + src/aios_kernel/open_ai_node.py | 6 + src/aios_kernel/workflow.py | 38 ++ src/component/agent_manager/agent.py | 3 - src/component/agent_manager/agent_manager.py | 55 ++- src/component/package_manager/__init__.py | 3 + src/component/package_manager/installer.py | 8 +- src/component/package_manager/media_reader.py | 9 + src/component/package_manager/pkg.py | 9 +- test/compute_task_test.py | 12 + test/workflow_test.py | 6 + 21 files changed, 725 insertions(+), 20 deletions(-) create mode 100644 doc/mvp/compute_task.drawio create mode 100644 doc/mvp/workflow.drawio rename src/{kernel => aios_kernel}/README (100%) create mode 100644 src/aios_kernel/__init__.py create mode 100644 src/aios_kernel/agent.py create mode 100644 src/aios_kernel/agent_group.py create mode 100644 src/aios_kernel/aios_shell.py create mode 100644 src/aios_kernel/compute_kernel.py create mode 100644 src/aios_kernel/compute_node.py create mode 100644 src/aios_kernel/environment.py create mode 100644 src/aios_kernel/llm_kernel.py create mode 100644 src/aios_kernel/open_ai_node.py create mode 100644 src/aios_kernel/workflow.py delete mode 100644 src/component/agent_manager/agent.py create mode 100644 test/compute_task_test.py create mode 100644 test/workflow_test.py diff --git a/doc/mvp/compute_task.drawio b/doc/mvp/compute_task.drawio new file mode 100644 index 0000000..981d7a7 --- /dev/null +++ b/doc/mvp/compute_task.drawio @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio new file mode 100644 index 0000000..cc154e7 --- /dev/null +++ b/doc/mvp/workflow.drawio @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/kernel/README b/src/aios_kernel/README similarity index 100% rename from src/kernel/README rename to src/aios_kernel/README diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py new file mode 100644 index 0000000..a8c15b9 --- /dev/null +++ b/src/aios_kernel/agent.py @@ -0,0 +1,58 @@ +from typing import Optional +import logging +import llm_kernel,llm_work_task + +logger = logging.getLogger(__name__) + + + +class agent_msg: + def __init__(self) -> None: + pass + +class agent_prompt: + def __init__(self) -> None: + pass + + def as_str()->str: + pass + +class agent_chat_session: + def __init__(self) -> None: + self.llm_model_name = None + self.llm_instance = None + self.max_token_size = 0 + self.chat_msg_list = None + self.enable_function = True + + pass + + def chat(self,message:str) -> None: + pass + # Key functions, let the AI Agent try to run. + def completion(self)->llm_work_task: + if self.llm_instance is None: + self.llm_instance = llm_kernel.craete(self.llm_model_name) + if self.llm_instance is None: + logger.fatal(f"cann't get llm_kerenel : {self.llm_model_name}") + return + + llm_work_task = self.llm_instance.completion(self._get_prompt(),self.max_token_size) + return llm_work_task + + def _get_prompt(str) -> str: + pass + +class ai_agent: + def __init__(self) -> None: + pass + + def get_chat_session(self,chat_user_name:str,session_id:Optional[str]) -> agent_chat_session: + pass + + + + #chat_session = agent.get_default_chat_session("master"); + #chat_session.chat("给我讲一个英文笑话!"); + #chat_session.completion(); + #print(chat_session.last_msg()); \ No newline at end of file diff --git a/src/aios_kernel/agent_group.py b/src/aios_kernel/agent_group.py new file mode 100644 index 0000000..edb031d --- /dev/null +++ b/src/aios_kernel/agent_group.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod +from typing import Any +from .agent import agent,agent_msg + +class ai_role: + def __init__(self) -> None: + pass + +class agent_group: + def __init__(self) -> None: + self.roles = None + pass + + def add_role(self,role_name:str,agent_id:str) -> None: + pass + + def send_msg(self,role_name:str,msg:agent_msg) -> None: + pass + + \ No newline at end of file diff --git a/src/aios_kernel/aios_shell.py b/src/aios_kernel/aios_shell.py new file mode 100644 index 0000000..c6e1890 --- /dev/null +++ b/src/aios_kernel/aios_shell.py @@ -0,0 +1,6 @@ +from .workflow import ai_workflow +from agent import agent_msg +class aios_shell: + def send_msg(self,msg:agent_msg,target:ai_workflow) -> None: + pass + \ No newline at end of file diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py new file mode 100644 index 0000000..be21341 --- /dev/null +++ b/src/aios_kernel/compute_kernel.py @@ -0,0 +1,29 @@ +from compute_node import compute_node +from abc import ABC, abstractmethod + +# 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, +# is the CORE GOAL of the entire computing task schedule system (aios_kernel). +class compute_task(ABC): + @abstractmethod + def display(self) -> str: + pass + + +class compute_kernel: + def __init__(self) -> None: + pass + + def run(self,task:compute_task) -> None: + # check there is compute node can support this task + # add task to working_queue + pass + + def start(self): + pass + + def add_compute_node(self,node:compute_node): + pass + + def disable_compute_node(self,): + pass \ No newline at end of file diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py new file mode 100644 index 0000000..ef3c16c --- /dev/null +++ b/src/aios_kernel/compute_node.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + + +class compute_node(ABC): + @abstractmethod + def display(self) -> str: + pass + +class local_compute_node(compute_node): + def display(self) -> str: + return super().display() + + diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py new file mode 100644 index 0000000..2524c36 --- /dev/null +++ b/src/aios_kernel/environment.py @@ -0,0 +1,12 @@ +class environment_event(ABC): + @abstractmethod + def display(self) -> str: + pass + + +class environment: + def __init__(self) -> None: + pass + + def event_to_msg(self,) -> environment_event: + pass \ No newline at end of file diff --git a/src/aios_kernel/llm_kernel.py b/src/aios_kernel/llm_kernel.py new file mode 100644 index 0000000..992db7f --- /dev/null +++ b/src/aios_kernel/llm_kernel.py @@ -0,0 +1,14 @@ + +class llm_work_task: + def __init__(self) -> None: + pass + + +class llm_kernel: + def __init__(self) -> None: + pass + def completion(self,prompt:str,max_token:int) -> llm_work_task: + # 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) + pass + \ No newline at end of file diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py new file mode 100644 index 0000000..17f3a83 --- /dev/null +++ b/src/aios_kernel/open_ai_node.py @@ -0,0 +1,6 @@ +from compute_node import compute_node + +class open_ai_compute_node(compute_node): + def display(self) -> str: + return super().display() + \ No newline at end of file diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py new file mode 100644 index 0000000..3a514ac --- /dev/null +++ b/src/aios_kernel/workflow.py @@ -0,0 +1,38 @@ +import environment +import agent_prompt,agent_msg + +class ai_workflow: + def __init__(self) -> None: + self.rule_prompt : agent_prompt = None + self.workflow_config = None + self.context = None + + def load_from_disk(self,config_path:str,context_dir_path) -> int: + pass + + def send_msg(self,msg:agent_msg,target_group:str = None) -> None: + if target_group is None: + target_group = self.get_default_group() + pass + + def run(self): + + pass + + def _pop_msg(self) -> Tuple[agent_msg,str]: + pass + + def get_default_group(self) -> agent_group: + pass + + def get_group(self,group_name:str) -> agent_group: + pass + + def get_workflow_rule_prompt(self) -> agent_prompt: + return self.rule_prompt + + def get_inner_environment(self) -> environment: + pass + + def connect_to_environment(self,env:environment) -> None: + pass \ No newline at end of file diff --git a/src/component/agent_manager/agent.py b/src/component/agent_manager/agent.py deleted file mode 100644 index 26f8b71..0000000 --- a/src/component/agent_manager/agent.py +++ /dev/null @@ -1,3 +0,0 @@ -class ai_agent: - def __init__(self) -> None: - pass \ No newline at end of file diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index d837175..b6173b6 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -1,11 +1,19 @@ +import logging +import toml + from .agent import ai_agent from .templete import ai_agent_templete +from package_manager import pkg_env,pkg_env_mgr,pkg_media_info,media_reader,pkg_install_task + +logger = logging.getLogger(__name__) + class agent_manager: _instance = None def __init__(self) -> None: + self.loaded_agent_instance = {} pass def __new__(cls): @@ -15,22 +23,51 @@ class agent_manager: def initial(self,root_dir:str) -> None: - pass + self.agent_templete_env : pkg_env = pkg_env_mgr().get_env(f"{root_dir}templetes/agent_templetes.cfg") + self.agent_env : pkg_env = pkg_env_mgr().get_env(f"{root_dir}agents/agents.cfg") + if self.agent_templete_env is None: + raise Exception("agent_manager initial failed") + - - def get(self,agent_id) -> ai_agent: - pass + def get(self,agent_id:str) -> ai_agent: + the_agent = self.loaded_agent_instance.get(agent_id) + if the_agent: + return the_agent + + # try load from disk + agent_media_info = self.agent_env.load(agent_id) + if agent_media_info is None: + return None + + the_agent = self._load_agent_from_media(agent_media_info) + if the_agent is None: + logger.warn(f"load agent {agent_id} from media failed!") + + return the_agent + def remove(self,agent_id:str)->int: + pass + def get_templete(self,templete_id) -> ai_agent_templete: + template_media_info = self.agent_templete_env.get(templete_id) + if template_media_info is None: + return None + return self._load_templete_from_media(template_media_info) + + def install(self,templete_id) -> pkg_install_task: + installer = self.agent_templete_env.get_installer() + return installer.install(templete_id) + + def uninstall(self,templete_id) -> int: + pass + + def _load_templete_from_media(self,templete_media:pkg_media_info) -> ai_agent_templete: pass - def install(self,templete_id) -> int: - - installer = None - #installer.install(templete_id) + def _load_agent_from_media(self,agent_media:pkg_media_info) -> ai_agent: pass - + def create(self,template,agent_name,agent_last_name,agent_introduce) -> ai_agent: pass diff --git a/src/component/package_manager/__init__.py b/src/component/package_manager/__init__.py index e69de29..b603579 100644 --- a/src/component/package_manager/__init__.py +++ b/src/component/package_manager/__init__.py @@ -0,0 +1,3 @@ +from .env import pkg_env_mgr,pkg_env +from .pkg import pkg_info,pkg_media_info +from .installer import pkg_install_task \ No newline at end of file diff --git a/src/component/package_manager/installer.py b/src/component/package_manager/installer.py index 894de8f..e388de3 100644 --- a/src/component/package_manager/installer.py +++ b/src/component/package_manager/installer.py @@ -19,7 +19,7 @@ INSTALL_TASK_STATE_DOWNLOADING = 3 INSTALL_TASK_STATE_INSTALLING = 4 INSTALL_TAKS_STATE_ERROR = 5 -class install_task: +class pkg_install_task: def __init__(self,owner:pkg_env) -> None: self.owner = owner self.state = INSTALL_TASK_STATE_CHECK_DEPENDENCY @@ -35,7 +35,7 @@ class pkg_installer: self.owner_env = owner_env def install(self,pkg_name:str, - install_from_dependency = False, can_upgrade = True,skip_depends = False,options = None)->Tuple[install_task,str]: + install_from_dependency = False, can_upgrade = True,skip_depends = False,options = None)->Tuple[pkg_install_task,str]: the_pkg_info : pkg_info = None is_upgrade : bool = False @@ -74,7 +74,7 @@ class pkg_installer: return result_task,"already installing" logger.info(f"start download&install {pkg_name},install_from_dependency={install_from_dependency},upgrade={is_upgrade},backup={need_backup},target_pkg_info={the_pkg_info}") - result_task = install_task() + result_task = pkg_install_task() self.all_tasks[the_pkg_info.cid] = result_task async def download_and_install_pkg()->int: # check dependency @@ -149,7 +149,7 @@ class pkg_installer: if depend_task is not None: logger.debug(f"{pkg.name}'s depend pkg {depend_pkg_name} already in task list") continue - depend_task = install_task() + depend_task = pkg_install_task() task_list[depend_pkg_name] = depend_task depend_pkg_info = self.owner_env.lookup(depend_pkg_name) diff --git a/src/component/package_manager/media_reader.py b/src/component/package_manager/media_reader.py index e69de29..dd17e68 100644 --- a/src/component/package_manager/media_reader.py +++ b/src/component/package_manager/media_reader.py @@ -0,0 +1,9 @@ +class media_reader: + def __init__(self): + pass + + def load_from_media(self,media_info:str) -> int: + pass + + def read(self, inner_path:str): + raise NotImplementedError \ No newline at end of file diff --git a/src/component/package_manager/pkg.py b/src/component/package_manager/pkg.py index ebc9194..d501eec 100644 --- a/src/component/package_manager/pkg.py +++ b/src/component/package_manager/pkg.py @@ -9,12 +9,13 @@ class pkg_info: self.target_media_type = "dir" self.source_media_type = "7z" - - - def parse_pkg_name(pkg_name:str) -> Tuple[str, str, str]: - #return pkg_id,version_str,cid + @classmethod + def parse_pkg_name(cls,pkg_name:str) -> Tuple[str, str, str]: + """parse pkg name like test-pkg#nightly#>0.2.31#sha1:323423423 to test-pkg,nightly#>0.2.31,sha1:323423423""" pass + + @property def cid(self) -> str: return self.cid diff --git a/test/compute_task_test.py b/test/compute_task_test.py new file mode 100644 index 0000000..a980b21 --- /dev/null +++ b/test/compute_task_test.py @@ -0,0 +1,12 @@ +import asyncio + +async def test_llm_completion_task(): + # compute task have engouh meta info to make sure compute_kernel can run it in most suitable compute container + test_task = llm_completion_task() + # add tset_task to compute_kernel's execute queue + compute_kernel.run(test_task) + # wait for test_task + + +if __name__ == "__main__": + asyncio.run(test_llm_completion_task()) \ No newline at end of file diff --git a/test/workflow_test.py b/test/workflow_test.py new file mode 100644 index 0000000..daa4fd1 --- /dev/null +++ b/test/workflow_test.py @@ -0,0 +1,6 @@ + + +def test_workflow(): + + + pass \ No newline at end of file From 760b0871cd66c202edea324b31e13c8fd7daf9db Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 22 Aug 2023 17:11:20 -0700 Subject: [PATCH 005/177] The framework design of the aios kernel has been basically completed, as well as the key logic code centered on workflow. --- doc/mvp/pkg_install_procedure.drawio | 4 +- doc/mvp/workflow.drawio | 8 +- src/aios_kernel/README | 4 +- src/aios_kernel/__init__.py | 7 + src/aios_kernel/agent.py | 88 +++++++---- src/aios_kernel/agent_group.py | 20 --- src/aios_kernel/ai_function.py | 32 ++++ src/aios_kernel/aios_shell.py | 12 +- src/aios_kernel/compute_kernel.py | 54 ++++++- src/aios_kernel/compute_node.py | 37 ++++- src/aios_kernel/environment.py | 17 +- src/aios_kernel/llm_kernel.py | 14 -- src/aios_kernel/open_ai_node.py | 4 +- src/aios_kernel/role.py | 25 +++ src/aios_kernel/workflow.py | 196 ++++++++++++++++++++++-- src/component/agent_manager/templete.py | 3 - test/agent_test.py | 3 +- test/async_test.py | 66 ++++++++ 18 files changed, 496 insertions(+), 98 deletions(-) delete mode 100644 src/aios_kernel/agent_group.py create mode 100644 src/aios_kernel/ai_function.py delete mode 100644 src/aios_kernel/llm_kernel.py create mode 100644 src/aios_kernel/role.py delete mode 100644 src/component/agent_manager/templete.py create mode 100644 test/async_test.py diff --git a/doc/mvp/pkg_install_procedure.drawio b/doc/mvp/pkg_install_procedure.drawio index b77bb77..7c2b49d 100644 --- a/doc/mvp/pkg_install_procedure.drawio +++ b/doc/mvp/pkg_install_procedure.drawio @@ -1,4 +1,4 @@ - + @@ -241,7 +241,7 @@ - + diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index cc154e7..dfdce99 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + - + @@ -211,7 +211,7 @@ - + @@ -251,7 +251,7 @@ - + diff --git a/src/aios_kernel/README b/src/aios_kernel/README index 30404ce..eee6354 100644 --- a/src/aios_kernel/README +++ b/src/aios_kernel/README @@ -1 +1,3 @@ -TODO \ No newline at end of file +TODO: +Embading Pipline +Knowlege Base \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index e69de29..515431f 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -0,0 +1,7 @@ +from .environment import environment,environment_event +from .agent import agent_msg,ai_agent,ai_agent_templete +from .compute_kernel import compute_kernel,compute_task +from .compute_node import compute_node,local_compute_node +from .open_ai_node import open_ai_compute_node +from .role import ai_role,ai_role_group +from .workflow import ai_workflow \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index a8c15b9..b4738a9 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -1,58 +1,88 @@ from typing import Optional + import logging -import llm_kernel,llm_work_task logger = logging.getLogger(__name__) - - class agent_msg: def __init__(self) -> None: + self.sender = None + self.target = None + self.body = None + + 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 agent_prompt: def __init__(self) -> None: pass - def as_str()->str: + def as_str(self)->str: pass -class agent_chat_session: + def append(self,prompt) -> None: + pass + +# chat session store the chat history between owner and agent +# chat session might be large, so can read / write at stream mode. +class ai_chat_session: def __init__(self) -> None: - self.llm_model_name = None - self.llm_instance = None - self.max_token_size = 0 - self.chat_msg_list = None - self.enable_function = True - pass - def chat(self,message:str) -> None: + def get_owner_id(self) -> str: pass - # Key functions, let the AI Agent try to run. - def completion(self)->llm_work_task: - if self.llm_instance is None: - self.llm_instance = llm_kernel.craete(self.llm_model_name) - if self.llm_instance is None: - logger.fatal(f"cann't get llm_kerenel : {self.llm_model_name}") - return - - llm_work_task = self.llm_instance.completion(self._get_prompt(),self.max_token_size) - return llm_work_task - def _get_prompt(str) -> str: + def append_post(self,msg:agent_msg) -> None: + """append msg to session, msg is post from session (owner => msg.target)""" + pass + + def append_recv(self,msg:agent_msg) -> 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 ai_agent_templete: + def __init__(self) -> None: + pass + class ai_agent: def __init__(self) -> None: - pass + self.chat_sessions = None + self.llm_model_name = None + self.max_token_size = 0 + self.instance_id = None + self.template_id = None - def get_chat_session(self,chat_user_name:str,session_id:Optional[str]) -> agent_chat_session: + def get_id(self) -> str: + return self.instance_id + + def get_template_id(self) -> str: + return self.template_id + + def get_chat_session_for_msg(self,msg:agent_msg) -> ai_chat_session: pass + def get_chat_session(self,sender:str,session_id:str) -> ai_chat_session: + pass + def get_llm_model_name(self) -> str: + return self.llm_model_name + + def get_max_token_size(self) -> int: + return self.max_token_size - #chat_session = agent.get_default_chat_session("master"); - #chat_session.chat("给我讲一个英文笑话!"); - #chat_session.completion(); - #print(chat_session.last_msg()); \ No newline at end of file diff --git a/src/aios_kernel/agent_group.py b/src/aios_kernel/agent_group.py deleted file mode 100644 index edb031d..0000000 --- a/src/aios_kernel/agent_group.py +++ /dev/null @@ -1,20 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any -from .agent import agent,agent_msg - -class ai_role: - def __init__(self) -> None: - pass - -class agent_group: - def __init__(self) -> None: - self.roles = None - pass - - def add_role(self,role_name:str,agent_id:str) -> None: - pass - - def send_msg(self,role_name:str,msg:agent_msg) -> None: - pass - - \ No newline at end of file diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py new file mode 100644 index 0000000..b3367fb --- /dev/null +++ b/src/aios_kernel/ai_function.py @@ -0,0 +1,32 @@ +class ai_function: + def __init__(self) -> None: + self.intro : str = None + + def load_from_config(self,config:dict) -> bool: + pass + + def is_local(self) -> bool: + pass + + def is_in_zone(self) -> bool: + pass + + def is_readyonly(self) -> bool: + pass + + def get_intro(self) -> str: + return self.intro + + async def execute(self): + pass + +# call chain is a combination of ai_function,group of ai_function. +class call_chain: + def __init__(self) -> None: + pass + + def load_from_config(self,config:dict) -> bool: + pass + + async def execute(self): + pass \ No newline at end of file diff --git a/src/aios_kernel/aios_shell.py b/src/aios_kernel/aios_shell.py index c6e1890..5599573 100644 --- a/src/aios_kernel/aios_shell.py +++ b/src/aios_kernel/aios_shell.py @@ -1,6 +1,14 @@ +# aiso shell like bash of linux from .workflow import ai_workflow -from agent import agent_msg + + class aios_shell: - def send_msg(self,msg:agent_msg,target:ai_workflow) -> None: + def __init__(self,username:str) -> None: + pass + + async def send_msg(self,msg:str,target_workflow:str) -> str: + pass + + async def install_workflow(self,workflow_id:ai_workflow) -> None: pass \ No newline at end of file diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index be21341..07dedde 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -1,5 +1,12 @@ -from compute_node import compute_node from abc import ABC, abstractmethod +from typing import Optional +import logging +import asyncio + +from .agent import agent_prompt +from .compute_node import compute_node + +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, @@ -11,19 +18,60 @@ class compute_task(ABC): class compute_kernel: + _instance = None + def __new__(cls): + if cls._instance is None: + cls._instance = super(compute_kernel, cls).__new__(cls) + return cls._instance + def __init__(self) -> None: + self.task_queue = [] + self.is_start = False pass def run(self,task:compute_task) -> 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") + return # add task to working_queue - pass + self.task_queue.append(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: + task = self.task_queue.pop(0) + c_node:compute_node= await self._schedule(task) + c_node.push_task(task) + + asyncio.create_task(_run_task_loop()) + + + async def _schedule(self,task) -> compute_node: pass def add_compute_node(self,node:compute_node): pass def disable_compute_node(self,): - pass \ No newline at end of file + pass + + def is_task_support(self,task:compute_task) -> bool: + pass + + + # friendly interface for use: + def llm_completion(self,prompt:agent_prompt,mode_name:Optional[str] = None,max_token:int = 0) -> compute_task: + # 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) + pass + + async def do_llm_completion(self,prompt:agent_prompt,mode_name:Optional[str] = None,max_token:int = 0) -> str: + pass + \ No newline at end of file diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index ef3c16c..2e05fca 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -1,13 +1,48 @@ from abc import ABC, abstractmethod - +from .compute_kernel import compute_task class compute_node(ABC): + @abstractmethod + async def push_task(self,task:compute_task,proiority:int = 0): + pass + + async def remove_task(self,task_id:str): + pass + + @abstractmethod + def get_task_state(self,task_id:str): + pass + @abstractmethod def display(self) -> str: pass + @abstractmethod + def get_capacity(self): + pass + + @abstractmethod + def is_support(self,task_type:str) -> bool: + pass + + @abstractmethod + def is_local(self) -> bool: + pass + + @abstractmethod + def is_trusted(self) -> bool: + return True + + def get_fee_type(self) -> str: + return "free" + + + class local_compute_node(compute_node): def display(self) -> str: return super().display() + def is_local(self) -> bool: + return True + diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 2524c36..60b2625 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -1,12 +1,23 @@ +# basic environment class +# 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 .agent import agent_msg + class environment_event(ABC): @abstractmethod def display(self) -> str: pass - class environment: def __init__(self) -> None: pass - def event_to_msg(self,) -> environment_event: - pass \ No newline at end of file + def get_id(self) -> str: + pass + + def attach_event_handler(self,event_id:str,handler:Callable) -> None: + pass + diff --git a/src/aios_kernel/llm_kernel.py b/src/aios_kernel/llm_kernel.py deleted file mode 100644 index 992db7f..0000000 --- a/src/aios_kernel/llm_kernel.py +++ /dev/null @@ -1,14 +0,0 @@ - -class llm_work_task: - def __init__(self) -> None: - pass - - -class llm_kernel: - def __init__(self) -> None: - pass - def completion(self,prompt:str,max_token:int) -> llm_work_task: - # 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) - pass - \ No newline at end of file diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 17f3a83..f362ed8 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -1,6 +1,8 @@ -from compute_node import compute_node +from .compute_node import compute_node class open_ai_compute_node(compute_node): def display(self) -> str: return super().display() + + \ No newline at end of file diff --git a/src/aios_kernel/role.py b/src/aios_kernel/role.py new file mode 100644 index 0000000..2be34f5 --- /dev/null +++ b/src/aios_kernel/role.py @@ -0,0 +1,25 @@ +from .agent import ai_agent + +class ai_role: + def __init__(self) -> None: + self.agent_instance_id : str = None + self.role_name : str = None + self.agent : ai_agent = None + self.introduce : str = None + + def load_from_config(self,config:dict) -> bool: + pass + + def get_intro(self) -> str: + return self.introduce + + def get_name(self) -> str: + return self.role_name + +class ai_role_group: + def __init__(self) -> None: + self.roles : dict[str,str]= None + pass + + + \ No newline at end of file diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 3a514ac..05e1faa 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -1,38 +1,206 @@ -import environment -import agent_prompt,agent_msg + +import logging +import asyncio +from typing import Optional,Tuple + +from .environment import environment,environment_event +from .agent import agent_prompt,agent_msg,ai_chat_session +from .role import ai_role +from .ai_function import call_chain +from .compute_kernel import compute_kernel + +logger = logging.getLogger(__name__) + +class ai_message_filter: + def __init__(self) -> None: + pass + def select(self,msg:agent_msg) -> ai_role: + pass class ai_workflow: def __init__(self) -> None: self.rule_prompt : agent_prompt = None self.workflow_config = None - self.context = None - + self.role_group = None + self.input_filter : ai_message_filter= None + self.msg_queue = [] + self.connected_environment = {} + def load_from_disk(self,config_path:str,context_dir_path) -> int: pass - def send_msg(self,msg:agent_msg,target_group:str = None) -> None: - if target_group is None: - target_group = self.get_default_group() + #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:agent_msg) -> None: + self.msg_queue.append(msg) + return + + async def send_msg(self,msg:agent_msg) -> str: + pass + + async def run(self): + # TODO add tracking design of msg processing + while True: + the_msg = await self._pop_msg() + chatsession:ai_chat_session = 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 + + chatsession.append_recv(the_msg) + + async def _process_msg(msg:agent_msg,the_role) -> None: + # prompt generat progress is most important part of workflow(app) develope + prompt = 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,the_role.get_name())) # chat context + + result = await compute_kernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + final_result = result + result_type : str = self._get_llm_result_type(result) + is_ignore = False + match result_type: + case "function": + callchain:call_chain = self._parse_function_call_chain(result) + resp = await callchain.exec() + if callchain.have_result(): + # generator proc resp prompt with WAITING state + proc_resp_prompt:agent_prompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession) + final_result = await compute_kernel().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:agent_msg = self._parse_to_msg(result) + if next_msg is not None: + # TODO: Next Target can be another role in workflow + next_workflow:ai_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:agent_prompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession) + final_result = await compute_kernel().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:agent_msg = self._parse_to_msg(result) + if next_msg is not None: + next_workflow:ai_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? + 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:agent_msg) -> 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:agent_msg = 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 _pop_msg(self) -> agent_msg: pass - def run(self): - + def _get_chat_session_for_msg(self,msg:agent_msg) -> ai_chat_session: pass - def _pop_msg(self) -> Tuple[agent_msg,str]: + async def _get_prompt_from_session(self,chatsession:ai_chat_session,role_name:str) -> agent_prompt: + pass + + def _get_msg_queue(self,session_id:str): pass - def get_default_group(self) -> agent_group: + def _merge_msg_result(self,results:dict) -> agent_msg: pass - def get_group(self,group_name:str) -> agent_group: + def _get_function_prompt(self,role_name:str) -> agent_prompt: + pass + + def _get_knowlege_prompt(self,role_name:str) -> agent_prompt: + pass + + def _get_resp_prompt(self,resp:str,msg:agent_msg,role:ai_role,prompt:agent_prompt,chatsession:ai_chat_session) -> agent_prompt: pass def get_workflow_rule_prompt(self) -> agent_prompt: return self.rule_prompt + + def _get_llm_result_type(self,llm_resp_str:str) -> str: + pass - def get_inner_environment(self) -> environment: + def _parse_function_call_chain(self,llm_resp_str) -> call_chain: + pass + + def _parse_to_msg(self,llm_resp_str) -> agent_msg: + pass + + def get_workflow(self,workflow_name:str) -> ai_workflow: + """get workflow from known workflow list or sub workflow list""" + pass + + + def _env_event_to_msg(self,env_event:environment_event) -> agent_msg: + pass + + def get_inner_environment(self,env_id:str) -> environment: pass def connect_to_environment(self,env:environment) -> None: - pass \ No newline at end of file + 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:environment_event) -> None: + the_msg:agent_msg= 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!") + diff --git a/src/component/agent_manager/templete.py b/src/component/agent_manager/templete.py deleted file mode 100644 index 87540bf..0000000 --- a/src/component/agent_manager/templete.py +++ /dev/null @@ -1,3 +0,0 @@ -class ai_agent_templete: - def __init__(self) -> None: - pass \ No newline at end of file diff --git a/test/agent_test.py b/test/agent_test.py index e0fd3e3..78941d4 100644 --- a/test/agent_test.py +++ b/test/agent_test.py @@ -23,8 +23,9 @@ def test_agent(): agent = am.create(agent_templete,"Tracy","Wang","Tracy Wang is my english teacher") print("Agent Tracy Wang load success!"); + print(agent.get_introduce()); - #print(agent.get_introduce()); + #chat_session = agent.get_default_chat_session("master"); #chat_session.chat("给我讲一个英文笑话!"); diff --git a/test/async_test.py b/test/async_test.py new file mode 100644 index 0000000..64cda58 --- /dev/null +++ b/test/async_test.py @@ -0,0 +1,66 @@ +import asyncio +import aiohttp +import aiofiles +STATE_DONE = 0 +STATE_DOWNLOADING = 1 +class install_task: + def __init__(self) -> None: + self.download_task = None + self.state = STATE_DOWNLOADING + self.recv_bytes = 0 + self.total_bytes = 0 + +class install_task_mgr: + def __init__(self) -> None: + self.all_tasks = {} + + def create_install_task(self,url:str,target:str): + owner = self + this_task = self.all_tasks.get(url) + if this_task is not None: + return this_task + + this_task = install_task() + self.all_tasks[url] = this_task + async def down_and_write(): + async with aiofiles.open(target, 'wb') as file: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + while True: + chunk = await response.content.read(1024*1024*16) + this_task.recv_bytes += len(chunk) + if not chunk: + break + await file.write(chunk) + print(f"download task {url} done!") + this_task.state = STATE_DONE + del owner.all_tasks[url] + + this_task.download_task = asyncio.create_task(down_and_write()) + return this_task + + + +async def test_wait_download(mgr): + this_task = mgr.create_install_task("https://www.cyfs.com/download/beta/cyberchat/android/latest","test.pkg") + await this_task.download_task + +def test_timer_download(mgr): + this_task = mgr.create_install_task("https://www.cyfs.com/download/beta/cyberchat/android/latest","test.pkg") + # start timer + async def check_timer(): + while this_task.state == STATE_DOWNLOADING: + await r = asyncio.sleep(1) + print(f"download bytes:{this_task.recv_bytes}") + print("download complete!") + + asyncio.create_task(check_timer()) + +async def test_main(): + mgr = install_task_mgr() + test_timer_download(mgr) + await test_wait_download(mgr) + await asyncio.sleep(1) + +if __name__ == "__main__": + asyncio.run(test_main()) \ No newline at end of file From 5454009e7b09bca6bc5d822687c03c53fbe9954f Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 23 Aug 2023 11:19:16 -0700 Subject: [PATCH 006/177] Refactor the code to make it comply with PEP-8 standards:Convert all class definitions to CamelCase style. (issue 37) --- doc/mvp/compute_task.drawio | 152 ++-- doc/mvp/workflow.drawio | 736 +++++++++---------- src/aios_kernel/__init__.py | 14 +- src/aios_kernel/agent.py | 18 +- src/aios_kernel/ai_function.py | 4 +- src/aios_kernel/aios_shell.py | 6 +- src/aios_kernel/compute_kernel.py | 24 +- src/aios_kernel/compute_node.py | 8 +- src/aios_kernel/environment.py | 6 +- src/aios_kernel/open_ai_node.py | 4 +- src/aios_kernel/role.py | 10 +- src/aios_kernel/workflow.py | 86 +-- src/component/agent_manager/__init__.py | 2 +- src/component/agent_manager/agent_manager.py | 31 +- src/component/ndn_client/__init__.py | 3 +- src/component/ndn_client/cid.py | 3 +- src/component/ndn_client/ndn_client.py | 8 +- src/component/package_manager/__init__.py | 6 +- src/component/package_manager/env.py | 44 +- src/component/package_manager/installer.py | 33 +- src/component/package_manager/pkg.py | 6 +- 21 files changed, 601 insertions(+), 603 deletions(-) diff --git a/doc/mvp/compute_task.drawio b/doc/mvp/compute_task.drawio index 981d7a7..ede6cf1 100644 --- a/doc/mvp/compute_task.drawio +++ b/doc/mvp/compute_task.drawio @@ -1,76 +1,76 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index dfdce99..88b61cc 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,368 +1,368 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 515431f..5eff80e 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -1,7 +1,7 @@ -from .environment import environment,environment_event -from .agent import agent_msg,ai_agent,ai_agent_templete -from .compute_kernel import compute_kernel,compute_task -from .compute_node import compute_node,local_compute_node -from .open_ai_node import open_ai_compute_node -from .role import ai_role,ai_role_group -from .workflow import ai_workflow \ No newline at end of file +from .environment import Environment,EnvironmentEvent +from .agent import AgentMsg,AIAgent,AIAgentTemplete +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 \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index b4738a9..6b0e5fc 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -4,7 +4,7 @@ import logging logger = logging.getLogger(__name__) -class agent_msg: +class AgentMsg: def __init__(self) -> None: self.sender = None self.target = None @@ -23,7 +23,7 @@ class agent_msg: def parser_target(self,target:str) -> None: pass -class agent_prompt: +class AgentPrompt: def __init__(self) -> None: pass @@ -35,18 +35,18 @@ class agent_prompt: # chat session store the chat history between owner and agent # chat session might be large, so can read / write at stream mode. -class ai_chat_session: +class AIChatSession: def __init__(self) -> None: pass def get_owner_id(self) -> str: pass - def append_post(self,msg:agent_msg) -> None: + 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:agent_msg) -> None: + def append_recv(self,msg:AgentMsg) -> None: """append msg to session, msg is recv from msg'sender (msg.sender => owner)""" pass @@ -56,11 +56,11 @@ class ai_chat_session: #TODO : add iterator interface for read chat history -class ai_agent_templete: +class AIAgentTemplete: def __init__(self) -> None: pass -class ai_agent: +class AIAgent: def __init__(self) -> None: self.chat_sessions = None self.llm_model_name = None @@ -74,10 +74,10 @@ class ai_agent: def get_template_id(self) -> str: return self.template_id - def get_chat_session_for_msg(self,msg:agent_msg) -> ai_chat_session: + def get_chat_session_for_msg(self,msg:AgentMsg) -> AIChatSession: pass - def get_chat_session(self,sender:str,session_id:str) -> ai_chat_session: + def get_chat_session(self,sender:str,session_id:str) -> AIChatSession: pass def get_llm_model_name(self) -> str: diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py index b3367fb..74cbdcc 100644 --- a/src/aios_kernel/ai_function.py +++ b/src/aios_kernel/ai_function.py @@ -1,4 +1,4 @@ -class ai_function: +class AIFunction: def __init__(self) -> None: self.intro : str = None @@ -21,7 +21,7 @@ class ai_function: pass # call chain is a combination of ai_function,group of ai_function. -class call_chain: +class CallChain: def __init__(self) -> None: pass diff --git a/src/aios_kernel/aios_shell.py b/src/aios_kernel/aios_shell.py index 5599573..610d5a9 100644 --- a/src/aios_kernel/aios_shell.py +++ b/src/aios_kernel/aios_shell.py @@ -1,14 +1,14 @@ # aiso shell like bash of linux -from .workflow import ai_workflow +from .workflow import Workflow -class aios_shell: +class AIOS_Shell: def __init__(self,username:str) -> None: pass async def send_msg(self,msg:str,target_workflow:str) -> str: pass - async def install_workflow(self,workflow_id:ai_workflow) -> None: + async def install_workflow(self,workflow_id:Workflow) -> None: pass \ No newline at end of file diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 07dedde..d05bd02 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -3,25 +3,25 @@ from typing import Optional import logging import asyncio -from .agent import agent_prompt -from .compute_node import compute_node +from .agent import AgentPrompt +from .compute_node import ComputeNode 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, # is the CORE GOAL of the entire computing task schedule system (aios_kernel). -class compute_task(ABC): +class ComputeTask(ABC): @abstractmethod def display(self) -> str: pass -class compute_kernel: +class ComputeKernel: _instance = None def __new__(cls): if cls._instance is None: - cls._instance = super(compute_kernel, cls).__new__(cls) + cls._instance = super(ComputeKernel, cls).__new__(cls) return cls._instance def __init__(self) -> None: @@ -29,7 +29,7 @@ class compute_kernel: self.is_start = False pass - def run(self,task:compute_task) -> 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") @@ -47,31 +47,31 @@ class compute_kernel: async def _run_task_loop(): while True: task = self.task_queue.pop(0) - c_node:compute_node= await self._schedule(task) + c_node:ComputeNode= await self._schedule(task) c_node.push_task(task) asyncio.create_task(_run_task_loop()) - async def _schedule(self,task) -> compute_node: + async def _schedule(self,task) -> ComputeNode: pass - def add_compute_node(self,node:compute_node): + def add_compute_node(self,node:ComputeNode): pass def disable_compute_node(self,): pass - def is_task_support(self,task:compute_task) -> bool: + def is_task_support(self,task:ComputeTask) -> bool: pass # friendly interface for use: - def llm_completion(self,prompt:agent_prompt,mode_name:Optional[str] = None,max_token:int = 0) -> compute_task: + def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0) -> ComputeTask: # 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) pass - async def do_llm_completion(self,prompt:agent_prompt,mode_name:Optional[str] = None,max_token:int = 0) -> str: + async def do_llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0) -> str: pass \ No newline at end of file diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 2e05fca..acf32a6 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod -from .compute_kernel import compute_task +from .compute_kernel import ComputeTask -class compute_node(ABC): +class ComputeNode(ABC): @abstractmethod - async def push_task(self,task:compute_task,proiority:int = 0): + async def push_task(self,task:ComputeTask,proiority:int = 0): pass async def remove_task(self,task_id:str): @@ -38,7 +38,7 @@ class compute_node(ABC): -class local_compute_node(compute_node): +class LocalComputeNode(ComputeNode): def display(self) -> str: return super().display() diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 60b2625..29de5b8 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -4,14 +4,14 @@ from abc import ABC, abstractmethod from typing import Callable -from .agent import agent_msg +from .agent import AgentMsg -class environment_event(ABC): +class EnvironmentEvent(ABC): @abstractmethod def display(self) -> str: pass -class environment: +class Environment: def __init__(self) -> None: pass diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index f362ed8..69279d9 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -1,6 +1,6 @@ -from .compute_node import compute_node +from .compute_node import ComputeNode -class open_ai_compute_node(compute_node): +class OpenAI_ComputeNode(ComputeNode): def display(self) -> str: return super().display() diff --git a/src/aios_kernel/role.py b/src/aios_kernel/role.py index 2be34f5..0b22a95 100644 --- a/src/aios_kernel/role.py +++ b/src/aios_kernel/role.py @@ -1,22 +1,22 @@ -from .agent import ai_agent +from .agent import AIAgent -class ai_role: +class AIRole: def __init__(self) -> None: self.agent_instance_id : str = None self.role_name : str = None - self.agent : ai_agent = None + self.agent : AIAgent = None self.introduce : str = None def load_from_config(self,config:dict) -> bool: pass - + def get_intro(self) -> str: return self.introduce def get_name(self) -> str: return self.role_name -class ai_role_group: +class AIRoleGroup: def __init__(self) -> None: self.roles : dict[str,str]= None pass diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 05e1faa..2192b09 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -3,26 +3,26 @@ import logging import asyncio from typing import Optional,Tuple -from .environment import environment,environment_event -from .agent import agent_prompt,agent_msg,ai_chat_session -from .role import ai_role -from .ai_function import call_chain -from .compute_kernel import compute_kernel +from .environment import Environment,EnvironmentEvent +from .agent import AgentPrompt,AgentMsg,AIChatSession +from .role import AIRole +from .ai_function import CallChain +from .compute_kernel import ComputeKernel logger = logging.getLogger(__name__) -class ai_message_filter: +class MessageFilter: def __init__(self) -> None: pass - def select(self,msg:agent_msg) -> ai_role: + def select(self,msg:AgentMsg) -> AIRole: pass -class ai_workflow: +class Workflow: def __init__(self) -> None: - self.rule_prompt : agent_prompt = None + self.rule_prompt : AgentPrompt = None self.workflow_config = None self.role_group = None - self.input_filter : ai_message_filter= None + self.input_filter : MessageFilter= None self.msg_queue = [] self.connected_environment = {} @@ -33,70 +33,70 @@ class ai_workflow: # 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:agent_msg) -> None: + def post_msg(self,msg:AgentMsg) -> None: self.msg_queue.append(msg) return - async def send_msg(self,msg:agent_msg) -> str: + 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:ai_chat_session = self._get_chat_session_for_msg(the_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 chatsession.append_recv(the_msg) - async def _process_msg(msg:agent_msg,the_role) -> None: + async def _process_msg(msg:AgentMsg,the_role) -> None: # prompt generat progress is most important part of workflow(app) develope - prompt = agent_prompt() + 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 - result = await compute_kernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + result = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) final_result = result result_type : str = self._get_llm_result_type(result) is_ignore = False match result_type: case "function": - callchain:call_chain = self._parse_function_call_chain(result) + 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:agent_prompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession) - final_result = await compute_kernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + 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:agent_msg = self._parse_to_msg(result) + 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:ai_workflow = self.get_workflow(next_msg.get_target()) + 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:agent_prompt = self._get_resp_prompt(resp,msg,the_role,prompt,chatsession) - final_result = await compute_kernel().do_llm_completion(proc_resp_prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + 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:agent_msg = self._parse_to_msg(result) + next_msg:AgentMsg = self._parse_to_msg(result) if next_msg is not None: - next_workflow:ai_workflow = self.get_workflow(next_msg.get_target()) + 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) @@ -113,7 +113,7 @@ class ai_workflow: return result - async def _workflow_process_msg(msg:agent_msg) -> None: + 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) @@ -134,7 +134,7 @@ class ai_workflow: # 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:agent_msg = self._merge_msg_result(results) + final_result:AgentMsg = self._merge_msg_result(results) if chatsession is not None: chatsession.append_post(final_result) @@ -144,59 +144,59 @@ class ai_workflow: asyncio.create_task(_workflow_process_msg(the_msg)) - async def _pop_msg(self) -> agent_msg: + async def _pop_msg(self) -> AgentMsg: pass - def _get_chat_session_for_msg(self,msg:agent_msg) -> ai_chat_session: + def _get_chat_session_for_msg(self,msg:AgentMsg) -> AIChatSession: pass - async def _get_prompt_from_session(self,chatsession:ai_chat_session,role_name:str) -> agent_prompt: + async def _get_prompt_from_session(self,chatsession:AIChatSession,role_name:str) -> AgentPrompt: pass def _get_msg_queue(self,session_id:str): pass - def _merge_msg_result(self,results:dict) -> agent_msg: + def _merge_msg_result(self,results:dict) -> AgentMsg: pass - def _get_function_prompt(self,role_name:str) -> agent_prompt: + def _get_function_prompt(self,role_name:str) -> AgentPrompt: pass - def _get_knowlege_prompt(self,role_name:str) -> agent_prompt: + def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt: pass - def _get_resp_prompt(self,resp:str,msg:agent_msg,role:ai_role,prompt:agent_prompt,chatsession:ai_chat_session) -> agent_prompt: + def _get_resp_prompt(self,resp:str,msg:AgentMsg,role:AIRole,prompt:AgentPrompt,chatsession:AIChatSession) -> AgentPrompt: pass - def get_workflow_rule_prompt(self) -> agent_prompt: + def get_workflow_rule_prompt(self) -> AgentPrompt: return self.rule_prompt def _get_llm_result_type(self,llm_resp_str:str) -> str: pass - def _parse_function_call_chain(self,llm_resp_str) -> call_chain: + def _parse_function_call_chain(self,llm_resp_str) -> CallChain: pass - def _parse_to_msg(self,llm_resp_str) -> agent_msg: + def _parse_to_msg(self,llm_resp_str) -> AgentMsg: pass - def get_workflow(self,workflow_name:str) -> ai_workflow: + def get_workflow(self,workflow_name:str) -> Workflow: """get workflow from known workflow list or sub workflow list""" pass - def _env_event_to_msg(self,env_event:environment_event) -> agent_msg: + def _env_event_to_msg(self,env_event:EnvironmentEvent) -> AgentMsg: pass - def get_inner_environment(self,env_id:str) -> environment: + def get_inner_environment(self,env_id:str) -> Environment: pass - def connect_to_environment(self,env:environment) -> None: + 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:environment_event) -> None: - the_msg:agent_msg= self._env_event_to_msg(env_event) + 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 diff --git a/src/component/agent_manager/__init__.py b/src/component/agent_manager/__init__.py index 863c4d1..7313a5a 100644 --- a/src/component/agent_manager/__init__.py +++ b/src/component/agent_manager/__init__.py @@ -1,3 +1,3 @@ -from .agent_manager import agent_manager,agent_manager_client +from .agent_manager import AgentManager,AgentManagerClient from .agent import ai_agent from .templete import ai_agent_templete \ No newline at end of file diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index b6173b6..af39c88 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -2,14 +2,13 @@ import logging import toml -from .agent import ai_agent -from .templete import ai_agent_templete -from package_manager import pkg_env,pkg_env_mgr,pkg_media_info,media_reader,pkg_install_task +from aios_kernel import AIAgent,AIAgentTemplete +from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask logger = logging.getLogger(__name__) -class agent_manager: +class AgentManager: _instance = None def __init__(self) -> None: @@ -18,18 +17,18 @@ class agent_manager: def __new__(cls): if cls._instance is None: - cls._instance = super(agent_manager, cls).__new__(cls) + cls._instance = super(AgentManager, cls).__new__(cls) return cls._instance def initial(self,root_dir:str) -> None: - self.agent_templete_env : pkg_env = pkg_env_mgr().get_env(f"{root_dir}templetes/agent_templetes.cfg") - self.agent_env : pkg_env = pkg_env_mgr().get_env(f"{root_dir}agents/agents.cfg") + self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}templetes/agent_templetes.cfg") + self.agent_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}agents/agents.cfg") if self.agent_templete_env is None: raise Exception("agent_manager initial failed") - def get(self,agent_id:str) -> ai_agent: + def get(self,agent_id:str) -> AIAgent: the_agent = self.loaded_agent_instance.get(agent_id) if the_agent: return the_agent @@ -39,7 +38,7 @@ class agent_manager: if agent_media_info is None: return None - the_agent = self._load_agent_from_media(agent_media_info) + the_agent : AIAgent = self._load_agent_from_media(agent_media_info) if the_agent is None: logger.warn(f"load agent {agent_id} from media failed!") @@ -49,29 +48,25 @@ class agent_manager: def remove(self,agent_id:str)->int: pass - def get_templete(self,templete_id) -> ai_agent_templete: + def get_templete(self,templete_id) -> AIAgentTemplete: template_media_info = self.agent_templete_env.get(templete_id) if template_media_info is None: return None return self._load_templete_from_media(template_media_info) - def install(self,templete_id) -> pkg_install_task: + def install(self,templete_id) -> PackageInstallTask: installer = self.agent_templete_env.get_installer() return installer.install(templete_id) def uninstall(self,templete_id) -> int: pass - def _load_templete_from_media(self,templete_media:pkg_media_info) -> ai_agent_templete: + def _load_templete_from_media(self,templete_media:PackageMediaInfo) -> AIAgentTemplete: pass - def _load_agent_from_media(self,agent_media:pkg_media_info) -> ai_agent: + def _load_agent_from_media(self,agent_media:PackageMediaInfo) -> AIAgent: pass - def create(self,template,agent_name,agent_last_name,agent_introduce) -> ai_agent: + def create(self,template,agent_name,agent_last_name,agent_introduce) -> AIAgent: pass - -class agent_manager_client: - def __init__(self) -> None: - pass \ No newline at end of file diff --git a/src/component/ndn_client/__init__.py b/src/component/ndn_client/__init__.py index 6b4585c..23e2329 100644 --- a/src/component/ndn_client/__init__.py +++ b/src/component/ndn_client/__init__.py @@ -1 +1,2 @@ -from .cid import content_id \ No newline at end of file +from .cid import ContentId +from .ndn_client import NDN_Client \ No newline at end of file diff --git a/src/component/ndn_client/cid.py b/src/component/ndn_client/cid.py index 981df3b..da89ed4 100644 --- a/src/component/ndn_client/cid.py +++ b/src/component/ndn_client/cid.py @@ -1,8 +1,9 @@ -class content_id: +class ContentId: def __init__(self) -> None: pass + @staticmethod def create_from_str(cid_str:str): pass \ No newline at end of file diff --git a/src/component/ndn_client/ndn_client.py b/src/component/ndn_client/ndn_client.py index c507fb1..fe46f6b 100644 --- a/src/component/ndn_client/ndn_client.py +++ b/src/component/ndn_client/ndn_client.py @@ -1,13 +1,13 @@ -from .cid import content_id +from .cid import ContentId -class ndn_get_task: +class NDN_GetTask: def __init__(self) -> None: pass -class ndn_client: +class NDN_Client: def __init__(self): pass - def get_file(cid:content_id,target_path:str,urls:{}=None,options:{}=None): + def get_file(self,cid:ContentId,target_path:str,urls:{}=None,options:{}=None): pass \ No newline at end of file diff --git a/src/component/package_manager/__init__.py b/src/component/package_manager/__init__.py index b603579..0af5eb5 100644 --- a/src/component/package_manager/__init__.py +++ b/src/component/package_manager/__init__.py @@ -1,3 +1,3 @@ -from .env import pkg_env_mgr,pkg_env -from .pkg import pkg_info,pkg_media_info -from .installer import pkg_install_task \ No newline at end of file +from .env import PackageEnvManager,PackageEnv +from .pkg import PackageInfo,PackageMediaInfo +from .installer import PackageInstallTask \ No newline at end of file diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py index 85c6b2a..b4cac9c 100644 --- a/src/component/package_manager/env.py +++ b/src/component/package_manager/env.py @@ -2,16 +2,16 @@ import logging import toml -from .pkg import pkg_info,pkg_media_info -from .installer import pkg_installer +from .pkg import PackageInfo,PackageMediaInfo +from .installer import PackageInstaller logger = logging.getLogger(__name__) -class pkg_env_mgr: +class PackageEnvManager: _instance = None def __new__(cls): if cls._instance is None: - cls._instance = super(pkg_env_mgr, cls).__new__(cls) + cls._instance = super(PackageEnvManager, cls).__new__(cls) return cls._instance def __init__(self) -> None: @@ -19,46 +19,46 @@ class pkg_env_mgr: pass - def get_env(self,cfg_path:str) -> pkg_env: + def get_env(self,cfg_path:str) -> PackageEnv: if cfg_path in self._pkg_envs: return self._pkg_envs[cfg_path] else: - pkg_env = pkg_env(cfg_path) + pkg_env = PackageEnv(cfg_path) self._pkg_envs[cfg_path] = pkg_env return pkg_env - def get_user_env(self) -> pkg_env: + def get_user_env(self) -> PackageEnv: pass - def get_system_env(self) -> pkg_env: + def get_system_env(self) -> PackageEnv: pass -class pkg_env: +class PackageEnv: def __init__(self,cfg_path:str) -> None: self.pkg_dir : str = "" self.pkg_obj_dir : str = "" self.is_strict : bool = True - self.parent_envs : list[pkg_env] = None + self.parent_envs : list[PackageEnv] = None self.index_dbs = None self.cfg_path = cfg_path self._load_pkg_cfg(cfg_path) pass - def load(self,pkg_name:str,search_parent=True) -> pkg_media_info: + def load(self,pkg_name:str,search_parent=True) -> PackageMediaInfo: pkg_path = None - pkg_id,verion_str,cid = pkg_info.parse_pkg_name(pkg_name) + pkg_id,verion_str,cid = PackageInfo.parse_pkg_name(pkg_name) if cid is None: if verion_str is None: - channel = self.get_pkg_channel_from_version(verion_str) + channel:str = self.get_pkg_channel_from_version(verion_str) if channel is None: pkg_path = f"{self.pkg_dir}{pkg_id}" else: pkg_path = f"{self.pkg_dir}{pkg_id}#{channel}" else: - channel = self.get_pkg_channel_from_version(verion_str) - the_version = self.get_exact_version_from_installed(verion_str) + channel:str = self.get_pkg_channel_from_version(verion_str) + the_version:str = self.get_exact_version_from_installed(verion_str) if the_version is None: logger.warn(f"load {pkg_name} failed: no match version from {verion_str}") return None @@ -69,7 +69,7 @@ class pkg_env: else: pkg_path = f"{self.pkg_obj_dir}.{pkg_id}/{cid}" - media_info = self.try_load_pkg_media_info(pkg_id,pkg_path) + media_info:PackageMediaInfo = self.try_load_pkg_media_info(pkg_path) if media_info is None: if search_parent: for parent_env in self.parent_envs: @@ -86,28 +86,28 @@ class pkg_env: def get_pkg_channel_from_version(self,pkg_version:str) -> str: pass - def get_pkg_media_info(self,pkg_name:str)->pkg_media_info: + def get_pkg_media_info(self,pkg_name:str)->PackageMediaInfo: pass - def try_load_pkg_media_info(self,pkg_full_path:str) -> pkg_media_info: + def try_load_pkg_media_info(self,pkg_full_path:str) -> PackageMediaInfo: pass - def get_installed_pkg_info(self,pkg_name:str) -> pkg_info: + def get_installed_pkg_info(self,pkg_name:str) -> PackageInfo: pass - def lookup(self,pkg_id:str,version_str:str) -> pkg_info: + def lookup(self,pkg_id:str,version_str:str) -> PackageInfo: # to make sure pkg.cid is correct, we MUST verfiy eveything here pass - def get_installer(self) -> pkg_installer: + def get_installer(self) -> PackageInstaller: pass @classmethod def is_valied_media(pkg_full_path:str) -> bool: pass - def do_pkg_media_trans(self,pkg_info:pkg_info,source_path:str,target_path:str) -> bool: + def do_pkg_media_trans(self,pkg_info:PackageInfo,source_path:str,target_path:str) -> bool: pass def _load_pkg_cfg(self,cfg_path:str): diff --git a/src/component/package_manager/installer.py b/src/component/package_manager/installer.py index e388de3..7055f0c 100644 --- a/src/component/package_manager/installer.py +++ b/src/component/package_manager/installer.py @@ -5,10 +5,9 @@ import aiohttp import aiofiles import os -from ndn_client import content_id,ndn_client -from .pkg import pkg_info,pkg_media_info -from .env import pkg_env - +from ndn_client import ContentId,NDN_Client +from .pkg import PackageInfo,PackageMediaInfo +from .env import PackageEnv logger = logging.getLogger(__name__) @@ -19,8 +18,8 @@ INSTALL_TASK_STATE_DOWNLOADING = 3 INSTALL_TASK_STATE_INSTALLING = 4 INSTALL_TAKS_STATE_ERROR = 5 -class pkg_install_task: - def __init__(self,owner:pkg_env) -> None: +class PackageInstallTask: + def __init__(self,owner:PackageEnv) -> None: self.owner = owner self.state = INSTALL_TASK_STATE_CHECK_DEPENDENCY @@ -29,20 +28,20 @@ class pkg_install_task: self.dependency_tasks = None self.error_str = None -class pkg_installer: - def __init__(self,owner_env:pkg_env) -> None: +class PackageInstaller: + def __init__(self,owner_env:PackageEnv) -> None: self.all_tasks = {} self.owner_env = owner_env def install(self,pkg_name:str, - install_from_dependency = False, can_upgrade = True,skip_depends = False,options = None)->Tuple[pkg_install_task,str]: + install_from_dependency = False, can_upgrade = True,skip_depends = False,options = None)->Tuple[PackageInstallTask,str]: - the_pkg_info : pkg_info = None + the_pkg_info : PackageInfo = None is_upgrade : bool = False need_backup : bool = False - pkg_id,version_str,cid = pkg_info.parse_pkg_name(pkg_name) - media_info : pkg_media_info = self.owner_env.get_media_info(pkg_name) # must use index-db? + pkg_id,version_str,cid = PackageInfo.parse_pkg_name(pkg_name) + media_info : PackageMediaInfo = self.owner_env.get_media_info(pkg_name) # must use index-db? if media_info is not None: if cid is not None: if can_upgrade: @@ -74,7 +73,7 @@ class pkg_installer: return result_task,"already installing" logger.info(f"start download&install {pkg_name},install_from_dependency={install_from_dependency},upgrade={is_upgrade},backup={need_backup},target_pkg_info={the_pkg_info}") - result_task = pkg_install_task() + result_task = PackageInstallTask(self.owner_env) self.all_tasks[the_pkg_info.cid] = result_task async def download_and_install_pkg()->int: # check dependency @@ -104,7 +103,7 @@ class pkg_installer: urls = self.owner_env.get_pkg_urls(the_pkg_info) #download - client = ndn_client() # set watch + client = NDN_Client() # set watch download_result = await client.get_file(the_pkg_info.cid,urls,target_full_path,options) if download_result !=0: result_task.state = INSTALL_TAKS_STATE_ERROR @@ -140,16 +139,16 @@ class pkg_installer: def uninstall(self): pass - def get_dependency_tasks(self,pkg:pkg_info,dependency_tasks): + def get_dependency_tasks(self,pkg:PackageInfo,dependency_tasks): pass - async def check_dependency(self,pkg:pkg_info,task_list:{}) -> bool: + async def check_dependency(self,pkg:PackageInfo,task_list:{}) -> bool: for depend_pkg_name in pkg.depends: depend_task = task_list.get(depend_pkg_name) if depend_task is not None: logger.debug(f"{pkg.name}'s depend pkg {depend_pkg_name} already in task list") continue - depend_task = pkg_install_task() + depend_task = PackageInstallTask(self.owner_env) task_list[depend_pkg_name] = depend_task depend_pkg_info = self.owner_env.lookup(depend_pkg_name) diff --git a/src/component/package_manager/pkg.py b/src/component/package_manager/pkg.py index d501eec..9944103 100644 --- a/src/component/package_manager/pkg.py +++ b/src/component/package_manager/pkg.py @@ -1,5 +1,7 @@ +from typing import Tuple -class pkg_info: + +class PackageInfo: def __init__(self) -> None: self.name = "" self.cid = None @@ -20,7 +22,7 @@ class pkg_info: def cid(self) -> str: return self.cid -class pkg_media_info: +class PackageMediaInfo: def __init__(self) -> None: pass From 1a6cf1ad7a752992f145da6d2246f695fccdb70a Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 23 Aug 2023 13:29:02 -0700 Subject: [PATCH 007/177] complete document of sub system package_manager. --- .gitignore | 1 + doc/mvp/aios_kernel.md | 9 + doc/mvp/install_package.png | Bin 0 -> 169448 bytes doc/mvp/load_package.png | Bin 0 -> 98767 bytes doc/mvp/package_manager.md | 127 +++++- doc/mvp/pkg_install_procedure.drawio | 377 ------------------ doc/mvp/pkg_procedure.drawio | 377 ++++++++++++++++++ .../file.txt | 1 + ...df120b430021c36c232c99ef8d926aea2acd6b.zip | Bin 0 -> 117 bytes .../{pkg_index.toml => .pkgs/index-db.toml} | 0 doc/mvp/pkg_system_demo/demo1/pkg.cfg.toml | 1 + doc/mvp/pkg_system_demo/demo1/pkg.lock | 1 + .../demo1/pkgs/pkg_nameA#1.0.3/file.txt | 1 + .../demo1/pkgs/pkg_nameA/file.txt | 1 + doc/mvp/pkg_tree.png | Bin 0 -> 34043 bytes src/aios_kernel/README | 2 +- src/component/package_manager/env.py | 5 +- src/component/package_manager/installer.py | 1 + src/component/package_manager/media_reader.py | 2 +- test/test_toml.py | 6 + 20 files changed, 524 insertions(+), 388 deletions(-) create mode 100644 .gitignore create mode 100644 doc/mvp/aios_kernel.md create mode 100644 doc/mvp/install_package.png create mode 100644 doc/mvp/load_package.png delete mode 100644 doc/mvp/pkg_install_procedure.drawio create mode 100644 doc/mvp/pkg_procedure.drawio create mode 100644 doc/mvp/pkg_system_demo/demo1/.pkgs/.pkg_nameA/pkg_nameA#sha1-2aae6c35c94fcfb415dbe95f408b9ce91ee846ed/file.txt create mode 100644 doc/mvp/pkg_system_demo/demo1/.pkgs/.pkg_nameB/pkg_nameB#sha1-addf120b430021c36c232c99ef8d926aea2acd6b.zip rename doc/mvp/pkg_system_demo/demo1/{pkg_index.toml => .pkgs/index-db.toml} (100%) create mode 100644 doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA#1.0.3/file.txt create mode 100644 doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA/file.txt create mode 100644 doc/mvp/pkg_tree.png create mode 100644 test/test_toml.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/doc/mvp/aios_kernel.md b/doc/mvp/aios_kernel.md new file mode 100644 index 0000000..57a957b --- /dev/null +++ b/doc/mvp/aios_kernel.md @@ -0,0 +1,9 @@ + +# aios kernel部分的核心介绍 + +## 核心理论 + + +## 以 workflow为核心组织ai agent +定义了未来工程师使用LLM构造应用的方法 + diff --git a/doc/mvp/install_package.png b/doc/mvp/install_package.png new file mode 100644 index 0000000000000000000000000000000000000000..599313afeb5919d8adee04a9f15454357b433ba9 GIT binary patch literal 169448 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%0Dc9ku@z{reI`Zr`X9E6Z$A-wCU9>XQ09si_-sX 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 Xz!Hb&odPT3?Zkwog>nVnef|Fc07IZD literal 0 HcmV?d00001 diff --git a/doc/mvp/load_package.png b/doc/mvp/load_package.png new file mode 100644 index 0000000000000000000000000000000000000000..f503c13b067dfea7c8fdced41ca76226e1074cf4 GIT binary patch literal 98767 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 R?HlmN&emaT%HIzD_U~(zt;PTV literal 0 HcmV?d00001 diff --git a/doc/mvp/package_manager.md b/doc/mvp/package_manager.md index c1084d4..787d259 100644 --- a/doc/mvp/package_manager.md +++ b/doc/mvp/package_manager.md @@ -1,13 +1,126 @@ -# Package Management System Introduction -The Package Manager is a fundamental component of the system for managing Packages. -The design of this system references Git - the distinction between client and server is not so significant, it is more about synchronization. # Problems to Solve -TODO +The Package Manager is a fundamental component of the system for managing Packages. +The sub system provides fundamental support for packaging, publishing, downloading, verifying, installing, and loading folders containing required packages under different scenarios. Based on relevant modules, it's easy to build a package management system similar to `apt/pip/npm`. + +The system design has deeply referenced Git and NDN networks. The distinction between client and server is not that important. Through cryptography, it achieves decentralized trustworthy verification. Any client can become an effective repo server through simple configuration. + # Design -TODO +Let's start by introducing the two important processes. +## Load Package +[![](./load_package.png)](pkg_procedure.drawio) -# Testing -TODO +## Install Package +[![](./install_package.png)](pkg_procedure.drawio) + +注意安装的依赖检测流程,按照流程允许将确实的package安装到当前env + +# Some Basic Concepts +- ***env***:A target environment consisting of a series of configuration files, where packages can be loaded/installed. +- ***pkg***:A Package(pkg) is either a folder or a file that serves the same purpose as a folder (such as zip, iso, etc.). +- ***pkg_name***:A unique string used to label a package. It's usually a readable package name, but can also include the version number or even the ContentId. +- ***version_id***:A complete version number is made up of channel_name and version. channel_name is generally not specified and is configured uniformly by the env (e.g., all use the `nightly` channel or all use the `release` channel). Version_ids are divided into exact versions and conditional versions. +- ***mediainfo***:A recognizable file or folder format. Successfully loading a package means obtaining a confirmed MediaInfo, from which the package's content can be further read. +- ***Author***: The package's author, which includes the author's friendly name (unique) and public key. This system cannot determine the trustworthiness of the relationship between the friendly name and the public key; upper layers need to extend this based on product design. Verification through DNS systems or smart contracts is common. +- ***Distributor***:The distributor is responsible for maintaining index_db. +- ***index_db***:A database containing a series of package information (pkg_info), maintained by the distributor. `index_db` is a complete file saved within the env. It is updated through the index_db update operation in the env from the distributor. Since -pkg_info contains the package's cid info and the corresponding author's signature, the distributor can only select the version released by the author and cannot release packages on behalf of the Author. +- ***repo_server***:Includes pkg_server and index_db_server, which can be deployed separately. +- ***ndn_client***: A library for trustworthy downloading of packages through the package's ContentId. + +# Package Env Directory Structure +[![](./pkg_tree.png)]() + +The diagram represents a typical pkg_env directory structure, where: + +- ***pkg.cfg.toml*** The root directory contains a pkg.cfg.toml file, which is the main configuration file for the environment. +- ***index-db*** Inside pkg.cfg.toml, there are two external files included: an external pkg.lock (local version locking) and .pkgs/index-db.toml (independently distributed package index by the distributor). +- ***.pkgs*** Similar to .git, this folder contains a series of files and directories not directly used by users but supporting package management. It stores all different versions of packages using the naming convention $.pkg_name/pkg_name#cid. +- ***pkgs*** This directory is user-facing, structured according to successful package installations. Installed packages are soft-linked to the actual files/directories under the .pkgs folder. This minimizes redundant file copying and makes it convenient for users to view and modify. In this example, pkg_nameA has two versions (the default version and 1.0.3), both pointing to the actual folders with CIDs in .pkgs. + + +During the local testing phase, users can easily place their own packages in the pkgs directory for successful loading. Any local changes will not affect the content of the index-db, nor will it impact testing. Cryptographic verification only occurs during the download and installation process. + +The above environment isolation design also provides a fairly standard solution for common dependency conflicts. + +# Test + + +## Load Package Test +Load testing some times not depend on index-db. +### Loading Using pkg_id +This is the simplest mode for users. +Not specifying a version number usually means using the default version. When an index exists, the default version is fetched from the index. +In the absence of an index, the default version will prioritize links without suffixes; otherwise, it will use the link with the highest version. +```python +load("english-dict") +``` +Actual load: +``` +./pkgs/english-dict/ +./pkgs/english-dict#0.1.5/ +``` + +When there's an index-db, it will determine the default version based on the index-db information and load using the directory name with the version: +``` +./pkgs/english-dict#0.1.3/ +``` +Note that even when there's an index-db with a cid, the system still primarily loads by symbol. This gives system administrators more flexibility. Try to avoid modifying directories named with cids. + + +### Loading Using pkg_id + cid +```python +load("english-dict#sha256:1234567890") +``` + +This is the simplest method and doesn't rely on index-db. The system can precisely locate and load the package, which is stored in: +``` +./.pkgs/english-dict/sha256:1234567890/ +``` + +Verification of media information does not occur before loading; it only takes place after the download is complete. + +### +The channel is part of the version. If it's not specified, the default channel name will be read from the environment. +If the version number is fixed, the directory is directly constructed for loading. If the version number is conditional, it depends on the locally installed version list to first determine the version, and then constructs the directory for loading. +```python +load("english-dict#>0.1.4") +``` +The package will be loaded based on the actual version installed locally: +``` +./pkgs/english-dict#0.1.5/ +``` +Automatic local repair logic for loading using an exact version number: +At this point, if that directory does not exist, but it can be seen from the index-db that the cid corresponding to version 0.1.5 is already installed locally, loading will fail by default (simply deleting the version link effectively blocks a version). + +Only when the option to automatically repair links during loading is enabled (which requires permissions), will it automatically create a link to that cid directory and successfully load. +#### Version Control +Support Only 4 Comparison Operators: >, <, >=, <= + +- ***>0.1.2***: Any version greater than 0.1.2 +- ***>0.1.2<0.1.5***: Any version greater than 0.1.2 and less than 0.1.5 +- ***<0.1.2***: Any version less than 0.1.2 + +The logic for version selection during load is as follows: + +1. Retrieve all locally installed versions. +2. Based on the version selection criteria, choose one version. + +Note that during installation, the version chosen based on dependency information has its selection set from all versions in index-db, and it is not related to the versions already installed locally. + + +## Package Installation Testing (To Be Completed) +Installation testing depends on index-db. + +### Installing Using pkg_id +Check if the installed result matches the current version specified in index-db. + +### Installing Using pkg_id + cid +Verify that the installation process correctly validates the cid. After successful installation, making simple changes to the files on the server should result in a download verification failure. + +### Installing Using pkg_id + Version Constraints +Check if the installed result matches the correct version specified in index-db. + +### Installation with Local Upgrades +After installing using the pkg_id method, make changes to the package content, then reinstall. At this point, the locally modified version should be backed up, and the current version should be reinstalled. \ No newline at end of file diff --git a/doc/mvp/pkg_install_procedure.drawio b/doc/mvp/pkg_install_procedure.drawio deleted file mode 100644 index 7c2b49d..0000000 --- a/doc/mvp/pkg_install_procedure.drawio +++ /dev/null @@ -1,377 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/mvp/pkg_procedure.drawio b/doc/mvp/pkg_procedure.drawio new file mode 100644 index 0000000..7f97bd4 --- /dev/null +++ b/doc/mvp/pkg_procedure.drawio @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/mvp/pkg_system_demo/demo1/.pkgs/.pkg_nameA/pkg_nameA#sha1-2aae6c35c94fcfb415dbe95f408b9ce91ee846ed/file.txt b/doc/mvp/pkg_system_demo/demo1/.pkgs/.pkg_nameA/pkg_nameA#sha1-2aae6c35c94fcfb415dbe95f408b9ce91ee846ed/file.txt new file mode 100644 index 0000000..2fbd84d --- /dev/null +++ b/doc/mvp/pkg_system_demo/demo1/.pkgs/.pkg_nameA/pkg_nameA#sha1-2aae6c35c94fcfb415dbe95f408b9ce91ee846ed/file.txt @@ -0,0 +1 @@ + AA \ No newline at end of file diff --git a/doc/mvp/pkg_system_demo/demo1/.pkgs/.pkg_nameB/pkg_nameB#sha1-addf120b430021c36c232c99ef8d926aea2acd6b.zip b/doc/mvp/pkg_system_demo/demo1/.pkgs/.pkg_nameB/pkg_nameB#sha1-addf120b430021c36c232c99ef8d926aea2acd6b.zip new file mode 100644 index 0000000000000000000000000000000000000000..7e4e9482345740bda87d06b90b1aadb42e3a1184 GIT binary patch literal 117 zcmWIWW@Zs#0D;-b;^7-6Y}IE5vO$;wh|@B2QuRtIN)#L&1H2iTL>O=@1*&FXWKaN6 WKmgMi;LXYg;xhuF8IaZlaToxA`4Ha# literal 0 HcmV?d00001 diff --git a/doc/mvp/pkg_system_demo/demo1/pkg_index.toml b/doc/mvp/pkg_system_demo/demo1/.pkgs/index-db.toml similarity index 100% rename from doc/mvp/pkg_system_demo/demo1/pkg_index.toml rename to doc/mvp/pkg_system_demo/demo1/.pkgs/index-db.toml diff --git a/doc/mvp/pkg_system_demo/demo1/pkg.cfg.toml b/doc/mvp/pkg_system_demo/demo1/pkg.cfg.toml index e69de29..30404ce 100644 --- a/doc/mvp/pkg_system_demo/demo1/pkg.cfg.toml +++ b/doc/mvp/pkg_system_demo/demo1/pkg.cfg.toml @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/doc/mvp/pkg_system_demo/demo1/pkg.lock b/doc/mvp/pkg_system_demo/demo1/pkg.lock index e69de29..30404ce 100644 --- a/doc/mvp/pkg_system_demo/demo1/pkg.lock +++ b/doc/mvp/pkg_system_demo/demo1/pkg.lock @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA#1.0.3/file.txt b/doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA#1.0.3/file.txt new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA#1.0.3/file.txt @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA/file.txt b/doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA/file.txt new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/doc/mvp/pkg_system_demo/demo1/pkgs/pkg_nameA/file.txt @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/doc/mvp/pkg_tree.png b/doc/mvp/pkg_tree.png new file mode 100644 index 0000000000000000000000000000000000000000..e64bb25524efdf715baa8587dc13a7a0488da123 GIT binary patch literal 34043 zcmcG$XH-*L+b#^-jsl7l=^{lS6p<=b>AeI9B@{t=ReHy*qI3ijP^v&EfrOqQEeMJ< z={*rpdJ8>t^eph%@3YVOetl<*!x$NathMI6dztqYqV#puC@GjI$jHbjHPn^CWMt=A z$;il;F8>95^2lP)2KYtp4OUYmEA3%f0{%Gf_(X4>}Gk7<_=xR%!=pTs{tw&^w>LBx2NKfPL4wi-pbJG9re^E=8EWU^+l znIv^bu2r+JIL`jP9d1NJd8GGyNr42(CxiOG>TFj_U^7E( zHeX`jkpLf;{U$ZZI#DP!LUQN>`&uI|HZ$zxKmsjdQn7H;j`i^0!NP~zr_226h4Av% zu;uw|&+7=g^zTCt9J50mxNk5XwfA{ch88xit6cbfSoYh+HFUF;!KykMQsbABoD&17 z(L`ms!zbrWhb~GSFX@GYHSkSAgwol;JcL6S1Z(K@#f?NmR0}qU_YTd32ODZH2YVW} zhM$!E9_Ca{;3R(Ex=k9Oa&C!Cls-po`N8AxMT$qJs!oFPT)Udg8wmze)6_{i3`t|5 zmnKI0XtZdxe&6#qvFV_aip}6dV$C_(#CWP0GnwR-BagXdBmeo5zL)eyyAu?y@$r)s zGrYSWVq(IN+rq1pXh_`4v=y~GBl~m0;m&iv!YG=KCf2KMGY&h02Cj$}J&1QR=x!ff z-|O6@Ev{-Py3nZB))&UW)(;-<;biJH4Q=Sj5jFkR>m{*E#a4~LFemOo#{pEK;sKE&1;HOFR=Z(5CJpJ`clX*(tCUxTi=N<&!wB|61nKyG& zI;fs3D2_W+R|+hCtz<(lyi=#0Mg)%rukl9B*a2$Xio8FhPKW=L$gb0Dv4FwaYDqH($icYv`Yp4dx<;*ja6N{UB`5)C-xhwXcHrA>sg${S;i=bRb-uUS zp4CE5#XmbS1!4^EKD4K<8~KX_(Jq3Ha|zHh+B|3M*wLEk=d{0rhhImlT$xIV{`C zI^$uq>zl;vRLj_fwPkJI{wb1UeP?;-ldC-o0{#|1KxAa`%=Ml4Oz7P8Hc~5EumYDn zvBs_%X5}Z6RbM2!Mi*IzJJejr$o_r25G=Luu1u=8>ie_5%Ydj&M)us@Ii%HJzR4IF z!x_vE#C+gqmmGH*_Q}Yw7p%$8-Vk2=b9b}J*$1Dnr~3jwkD(iMXK%^{_fNA28QB

{(Jw%cZvTC;+d@b=k&r42LTZ~hI`-R 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 VCc00*IuisnLn>(~7RZ}C{a=Yy`(OY7 literal 0 HcmV?d00001 diff --git a/src/aios_kernel/README b/src/aios_kernel/README index eee6354..1182ca5 100644 --- a/src/aios_kernel/README +++ b/src/aios_kernel/README @@ -1,3 +1,3 @@ TODO: Embading Pipline -Knowlege Base \ No newline at end of file +Knowlege-Base diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py index b4cac9c..b71813d 100644 --- a/src/component/package_manager/env.py +++ b/src/component/package_manager/env.py @@ -2,8 +2,9 @@ import logging import toml -from .pkg import PackageInfo,PackageMediaInfo -from .installer import PackageInstaller +from .pkg import PackageInfo,PackageMediaInfo, +from .env import PackageEnv +from .installer import PackageInstaller,PackageInstallTask logger = logging.getLogger(__name__) diff --git a/src/component/package_manager/installer.py b/src/component/package_manager/installer.py index 7055f0c..8ab8f5f 100644 --- a/src/component/package_manager/installer.py +++ b/src/component/package_manager/installer.py @@ -4,6 +4,7 @@ import asyncio import aiohttp import aiofiles import os +from typing import Tuple from ndn_client import ContentId,NDN_Client from .pkg import PackageInfo,PackageMediaInfo diff --git a/src/component/package_manager/media_reader.py b/src/component/package_manager/media_reader.py index dd17e68..d1b941d 100644 --- a/src/component/package_manager/media_reader.py +++ b/src/component/package_manager/media_reader.py @@ -1,4 +1,4 @@ -class media_reader: +class MediaReader: def __init__(self): pass diff --git a/test/test_toml.py b/test/test_toml.py new file mode 100644 index 0000000..1e19f1c --- /dev/null +++ b/test/test_toml.py @@ -0,0 +1,6 @@ +import toml +import os; + +directory = os.path.dirname(__file__) +cfg = toml.load(directory + "\pkg.cfg.toml") +print(cfg) \ No newline at end of file From 4a8d81ce160dcdef2bc6f3a292c4ba8b70f42842 Mon Sep 17 00:00:00 2001 From: Si Changjun Date: Thu, 24 Aug 2023 15:54:52 +0800 Subject: [PATCH 008/177] knowleadge.drawio --- doc/未命名绘图.drawio | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/未命名绘图.drawio diff --git a/doc/未命名绘图.drawio b/doc/未命名绘图.drawio new file mode 100644 index 0000000..f8db6cf --- /dev/null +++ b/doc/未命名绘图.drawio @@ -0,0 +1,10 @@ + + + + + + + + + + From ccbef2104b0c1aa2324072244075da060cea8a88 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 27 Aug 2023 18:07:33 -0700 Subject: [PATCH 009/177] framework code has been completed basicly. Through the use of aios_shell, we are now able to get agents run able (at openai compute node) --- doc/mvp/demo_workflow/workflow.toml | 81 ++++++ doc/mvp/package_manager.md | 2 +- doc/mvp/workflow.drawio | 4 +- rootfs/agents/agent_1/agent.toml | 5 + rootfs/agents/agents.cfg | 2 + rootfs/templetes/templetes.cfg | 2 + src/aios_kernel/__init__.py | 2 +- src/aios_kernel/agent.py | 251 ++++++++++++++++-- src/aios_kernel/aios_shell.py | 14 - src/aios_kernel/compute_kernel.py | 91 +++++-- src/aios_kernel/compute_node.py | 9 +- src/aios_kernel/compute_task.py | 60 +++++ src/aios_kernel/environment.py | 2 - src/aios_kernel/open_ai_node.py | 104 +++++++- src/aios_kernel/workflow.py | 9 +- src/component/agent_manager/__init__.py | 4 +- src/component/agent_manager/agent_manager.py | 40 ++- src/component/ndn_client/cid.py | 3 + src/component/ndn_client/ndn_client.py | 110 +++++++- src/component/package_manager/env.py | 232 ++++++++-------- src/component/package_manager/media_reader.py | 19 +- src/component/package_manager/pkg.py | 23 +- src/component/workflow_manager/__init__.py | 1 + .../workflow_manager/workflow_manager.py | 11 + src/service/aios_shell/aios_shell.py | 128 +++++++++ 25 files changed, 1011 insertions(+), 198 deletions(-) create mode 100644 doc/mvp/demo_workflow/workflow.toml create mode 100644 rootfs/agents/agent_1/agent.toml create mode 100644 rootfs/agents/agents.cfg create mode 100644 rootfs/templetes/templetes.cfg delete mode 100644 src/aios_kernel/aios_shell.py create mode 100644 src/aios_kernel/compute_task.py create mode 100644 src/component/workflow_manager/__init__.py create mode 100644 src/component/workflow_manager/workflow_manager.py create mode 100644 src/service/aios_shell/aios_shell.py diff --git a/doc/mvp/demo_workflow/workflow.toml b/doc/mvp/demo_workflow/workflow.toml new file mode 100644 index 0000000..66a3735 --- /dev/null +++ b/doc/mvp/demo_workflow/workflow.toml @@ -0,0 +1,81 @@ +# workflow实例场景 +# 能展示sub workflow +# 能展示env的整合 +# 能展示filter的使用 +# 能展示function的调用 +# 能展示基于工作目标/KPI的sub workflow迭代流程 +# 多人场景安排 + +# 例子:举办一个团队活动 +# 方案讨论(通过交互引导的方式收集主人的需求)与确定 + +# 活动前: +# 通讯员,对接管理参加活动的人的情况 (email spider) +# 酒店预订 简单:搜索(酒店评价) 处理异常 +# 行程(票务)预订 :搜索,处理异常 +# 餐饮预订:给出方案,确定细节,预订 + +# 活动中: +# 进行统计和分析,调整设备 +# 安保,空调,音乐,拍照,录像 +# 响应紧急情况 + +# 活动结束后: +# 整理照片,视频,进行必要的二次创作,发送给相关人员 +# 对活动进行总结,提出改件意见(指导下一次活动) + +# 1. 人员 +# 主管,负责和客户沟通,并对每个环境的结果进行总结 +# 嘉宾对接 +# 酒店组 +# 行程组 +# 财务组 +# 多媒体组 + + +[filter] +"*" = "manager" + +[roles.manager] +name = "经理" +prompt = "你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是: +1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 +2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。 +3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。 +4. 当你决定要和工作小组通信时,请使用`send_message({小组名称},{内容}`)的形式。 +" +agent="agent.manager" + + +[sub_workflows] +[sub_workflows."嘉宾对接组"] +# 展现读取email和发送email与嘉宾沟通的能力 +[sub_workflows."嘉宾对接组".environments.email] +new_mail = "收到来自{event.data.from},标题为{event.data.subject}的邮件,内容为{event.data.content}的电子邮件" # 这里将new_mail事件转换为了一个来自环境的message + +[sub_workflows."嘉宾对接组".roles.leader] +name = "嘉宾对接组组长" +prompt = "你是一家活动策划公司的嘉宾对接组的组长,你的工作是基于已知信息,当前活动信息、公司经理的指令与嘉宾沟通,收集嘉宾的信息,然后将信息反馈给经理。在你看来,参加活动的多少有成员都是嘉宾,你可以通过你知道的信息给不同的成员进行分级。你的基本工作模式是: +1. 处理收到的邮件,如果邮件来自嘉宾,你会尝试从邮件的表态和内容中分享嘉宾的需要,并结合你对当前活动方案的理解判断是否需要和经理沟通,如果需要和经理沟通,你会将嘉宾的需求总结和告诉经理。不需要沟通的事项可以直接回复嘉宾。 +2. 你总是通过`call_function(get_env,'parent.topic'`的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。 +3. 当你决定要和经理通信时,请使用`send_message(manager,{内容}`)的形式,内容的长度不超过200字。 +4. 当你决定要回复嘉宾时,请使用`call_function(sendmail,{嘉宾邮件地址},{标题},{内容})的形式,内容的长度不超过500字。 +" +# 这里是孤立工作模式,组长只和经理沟通,也可以赋予其和其它组沟通的能力 +agent="agent.email.leader" + +[sub_workflows."酒店预定组"] +# 展现使用搜索引擎,并调用预订酒店的能力 +[sub_workflows."酒店预定组".environments.email] + +[sub_workflows."酒店预定组".roles.leader] +name="酒店预定组组长" + +[sub_workflows."酒店预定组".roles.research] +name="酒店搜索专家" + +[sub_workflows."行程预订组"] +# 展现处理冲突并反推 +nam3="3" + + diff --git a/doc/mvp/package_manager.md b/doc/mvp/package_manager.md index 787d259..33d9ce7 100644 --- a/doc/mvp/package_manager.md +++ b/doc/mvp/package_manager.md @@ -15,7 +15,7 @@ Let's start by introducing the two important processes. ## Install Package [![](./install_package.png)](pkg_procedure.drawio) -注意安装的依赖检测流程,按照流程允许将确实的package安装到当前env +Note that the dependency check during installation allows for the missing packages to be installed into the current environment. # Some Basic Concepts - ***env***:A target environment consisting of a series of configuration files, where packages can be loaded/installed. diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index 88b61cc..e49374a 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -220,7 +220,7 @@ - + diff --git a/rootfs/agents/agent_1/agent.toml b/rootfs/agents/agent_1/agent.toml new file mode 100644 index 0000000..9b13b5f --- /dev/null +++ b/rootfs/agents/agent_1/agent.toml @@ -0,0 +1,5 @@ +instance_id = "agent:xxxxxxabcde" +fullname = "tracy wang" +[[prompt]] +role = "system" +content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" diff --git a/rootfs/agents/agents.cfg b/rootfs/agents/agents.cfg new file mode 100644 index 0000000..c9f8d63 --- /dev/null +++ b/rootfs/agents/agents.cfg @@ -0,0 +1,2 @@ +main = "./" +cache = "./.agents" \ No newline at end of file diff --git a/rootfs/templetes/templetes.cfg b/rootfs/templetes/templetes.cfg new file mode 100644 index 0000000..9cff26f --- /dev/null +++ b/rootfs/templetes/templetes.cfg @@ -0,0 +1,2 @@ +main = "./" +cache = "./.templetes" \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 5eff80e..04d4cc5 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -1,5 +1,5 @@ from .environment import Environment,EnvironmentEvent -from .agent import AgentMsg,AIAgent,AIAgentTemplete +from .agent import AgentMsg,AIAgent,AIAgentTemplete,AgentMsgState,AgentPrompt,AgentMsgState from .compute_kernel import ComputeKernel,ComputeTask from .compute_node import ComputeNode,LocalComputeNode from .open_ai_node import OpenAI_ComputeNode diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 6b0e5fc..8346649 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -1,14 +1,37 @@ from typing import Optional - +from enum import Enum +from asyncio import Queue +import asyncio import logging +import uuid +import time + logger = logging.getLogger(__name__) + +class AgentMsgState(Enum): + RESPONSED = 0 + INIT = 1 + SENDING = 2 + PROCESSING = 3 + ERROR = 4 + class AgentMsg: def __init__(self) -> None: - self.sender = None - self.target = None - self.body = 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 @@ -25,22 +48,35 @@ class AgentMsg: class AgentPrompt: def __init__(self) -> None: - pass + self.messages = [] def as_str(self)->str: - pass + result_str = "" + if self.messages: + for msg in self.messages: + result_str += msg.get("role") + ":" + msg.get("content") + "\n" - def append(self,prompt) -> None: - pass + return result_str + + def append(self,prompt): + self.messages.extend(prompt.messages) + + def load_from_config(self,config:list) -> bool: + if isinstance(config,list) is not True: + logger.error("prompt is not list!") + return False + + 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) -> None: - pass + def __init__(self,owner_id) -> None: + self.owner_id = owner_id def get_owner_id(self) -> str: - pass + return self.owner_id def append_post(self,msg:AgentMsg) -> None: """append msg to session, msg is post from session (owner => msg.target)""" @@ -58,18 +94,185 @@ class AIChatSession: class AIAgentTemplete: def __init__(self) -> None: - pass + self.llm_model_name:str = "gpt-4-0613" + self.max_token_size:int = 0 + self.template_id:str = None + self.introduce:str = None + self.author:str = None + self.prompt:AgentPrompt = None + + def load_from_config(self,config:dict) -> bool: + if config.get("llm_model_name") is not None: + self.llm_model_name = config["llm_model_name"] + if config.get("max_token_size") is not None: + self.max_token_size = config["max_token_size"] + if config.get("template_id") is not None: + self.template_id = config["template_id"] + if config.get("prompt") is not None: + self.prompt = AgentPrompt() + if self.prompt.load_from_config(config["prompt"]) is False: + logger.error("load prompt from config failed!") + return False + + return True + class AIAgent: def __init__(self) -> None: - self.chat_sessions = None - self.llm_model_name = None - self.max_token_size = 0 - self.instance_id = None - self.template_id = None + self.prompt:AgentPrompt = None + self.llm_model_name:str = None + self.max_token_size:int = 0 + self.instance_id:str = None + self.template_id:str = None + self.fullname:str = None + self.powerby = None + self.enable = True + self.chat_sessions = {} + self.unread_msg = Queue() # msg from other agent + + @classmethod + def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): + # Agent just inherit from templete on craete,if template changed,agent will not change + result_agent = 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.fullname = fullname + result_agent.powerby = templete.author + result_agent.prompt = templete.prompt + return result_agent + + def load_from_config(self,config:dict) -> bool: + if config.get("instance_id") is None: + logger.error("agent instance_id is None!") + return False + self.instance_id = config["instance_id"] + + if config.get("fullname") is None: + logger.error(f"agent {self.instance_id} fullname is None!") + return False + self.fullname = config["fullname"] + + if config.get("prompt") is not None: + self.prompt = AgentPrompt() + self.prompt.load_from_config(config["prompt"]) + + if config.get("powerby") is not None: + self.powerby = config["powerby"] + if config.get("template_id") is not None: + self.template_id = config["template_id"] + if config.get("llm_model_name") is not None: + self.llm_model_name = config["llm_model_name"] + if config.get("max_token_size") is not None: + self.max_token_size = config["max_token_size"] + + 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": + return "ignore" + + return "text" + + async def _process_msg(self,msg:AgentMsg) -> AgentMsg: + from .compute_kernel import ComputeKernel + + 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 + + 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) + 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? + chatsession = self.get_chat_session(msg.sender) + resp_msg = AgentMsg() + resp_msg.set(self.instance_id,msg.sender,final_result) + if chatsession is not None: + chatsession.append_recv(msg) + chatsession.append_post(final_result) + + return resp_msg + + return None + def get_id(self) -> str: return self.instance_id + + def get_fullname(self) -> str: + return self.fullname def get_template_id(self) -> str: return self.template_id @@ -77,8 +280,18 @@ class AIAgent: def get_chat_session_for_msg(self,msg:AgentMsg) -> AIChatSession: pass - def get_chat_session(self,sender:str,session_id:str) -> 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 diff --git a/src/aios_kernel/aios_shell.py b/src/aios_kernel/aios_shell.py deleted file mode 100644 index 610d5a9..0000000 --- a/src/aios_kernel/aios_shell.py +++ /dev/null @@ -1,14 +0,0 @@ -# aiso shell like bash of linux -from .workflow import Workflow - - -class AIOS_Shell: - def __init__(self,username:str) -> None: - pass - - async def send_msg(self,msg:str,target_workflow:str) -> str: - pass - - async def install_workflow(self,workflow_id:Workflow) -> None: - pass - \ No newline at end of file diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index d05bd02..84351a7 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -2,32 +2,39 @@ from abc import ABC, abstractmethod from typing import Optional import logging import asyncio +from asyncio import Queue from .agent import AgentPrompt from .compute_node import ComputeNode +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, # is the CORE GOAL of the entire computing task schedule system (aios_kernel). -class ComputeTask(ABC): - @abstractmethod - def display(self) -> str: - pass - - class ComputeKernel: _instance = None def __new__(cls): if cls._instance is None: - cls._instance = super(ComputeKernel, cls).__new__(cls) + cls._instance = super().__new__(cls) + cls._instance.is_start = False + else: + print("ComputeKernel is already created!") return cls._instance def __init__(self) -> None: - self.task_queue = [] + 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 - pass + self.compute_nodes = {} + + self.start() def run(self,task:ComputeTask) -> None: # check there is compute node can support this task @@ -35,7 +42,7 @@ class ComputeKernel: logger.error(f"task {task.display()} is not support by any compute node") return # add task to working_queue - self.task_queue.append(task) + self.task_queue.put_nowait(task) def start(self): @@ -46,32 +53,72 @@ class ComputeKernel: self.is_start = True async def _run_task_loop(): while True: - task = self.task_queue.pop(0) - c_node:ComputeNode= await self._schedule(task) - c_node.push_task(task) + 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) + await c_node.push_task(task) + + logger.warn("compute_kernel is stoped!") asyncio.create_task(_run_task_loop()) - async def _schedule(self,task) -> ComputeNode: - pass + 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") + return None def add_compute_node(self,node:ComputeNode): - pass + if self.compute_nodes.get(node.node_id) is not None: + 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,): - pass + 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: - pass + return True # friendly interface for use: - def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0) -> ComputeTask: + def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0): # 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) - pass + task_req = ComputeTask() + task_req.set_llm_params(prompt,mode_name,max_token) + 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: - pass + task_req = self.llm_completion(prompt,mode_name,max_token) + async def check_timer(): + check_times = 0 + while True: + if task_req.state == ComputeTaskState.DONE: + break + + if task_req.state == ComputeTaskState.ERROR: + break + + 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 "error!" \ No newline at end of file diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index acf32a6..6fe0059 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -1,11 +1,17 @@ from abc import ABC, abstractmethod -from .compute_kernel import ComputeTask +from .compute_task import ComputeTask + class ComputeNode(ABC): + def __init__(self) -> None: + self.node_id = "default" + self.enable = True + @abstractmethod async def push_task(self,task:ComputeTask,proiority:int = 0): pass + @abstractmethod async def remove_task(self,task_id:str): pass @@ -29,7 +35,6 @@ class ComputeNode(ABC): def is_local(self) -> bool: pass - @abstractmethod def is_trusted(self) -> bool: return True diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py new file mode 100644 index 0000000..f2253e0 --- /dev/null +++ b/src/aios_kernel/compute_task.py @@ -0,0 +1,60 @@ + +from enum import Enum +import uuid +import time + +class ComputeTaskState(Enum): + DONE = 0 + INIT = 1 + RUNNING = 2 + ERROR = 3 + PENDING = 4 + + +class ComputeTask: + def __init__(self) -> None: + self.task_type = "llm_completion" + 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.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" + 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: + self.params["model_name"] = "gpt-4-0613" + self.params["max_token_size"] = max_token_size + + def display(self) -> str: + return f"ComputeTask: {self.task_id} {self.task_type} {self.state}" + + +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.result:dict = {} + self.result_refers:dict = None + self.pading_data:bytearray = None + + def set_from_task(self,task:ComputeTask): + self.task_id = task.task_id + self.callchain_id = task.callchain_id diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 29de5b8..64c6481 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -4,8 +4,6 @@ from abc import ABC, abstractmethod from typing import Callable -from .agent import AgentMsg - class EnvironmentEvent(ABC): @abstractmethod def display(self) -> str: diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 69279d9..cf88815 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -1,8 +1,110 @@ + +import openai +import os +import asyncio +from asyncio import Queue +import logging + +from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState from .compute_node import ComputeNode +logger = logging.getLogger(__name__) + class OpenAI_ComputeNode(ComputeNode): + _instance = None + def __new__(cls): + if cls._instance is None: + cls._instance = super(OpenAI_ComputeNode, cls).__new__(cls) + cls._instance.is_start = False + return cls._instance + + 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.task_queue = Queue() + + if os.getenv("OPENAI_API_KEY") is not None: + openai.api_key = os.getenv("OPENAI_API_KEY") + else: + openai.api_key = self.openai_api_key + + self.start() + + 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): + pass + + def _run_task(self,task:ComputeTask): + task.state = ComputeTaskState.RUNNING + 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=2000, + 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 + + def start(self): + 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 super().display() + return f"OpenAI_ComputeNode: {self.node_id}" + + def get_task_state(self,task_id:str): + pass + + + def get_capacity(self): + pass + + + def is_support(self,task_type:str) -> bool: + return True + + + def is_local(self) -> bool: + return False + + + \ No newline at end of file diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 2192b09..1c842b8 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -1,7 +1,9 @@ import logging import asyncio +from asyncio import Queue from typing import Optional,Tuple +from abc import ABC, abstractmethod from .environment import Environment,EnvironmentEvent from .agent import AgentPrompt,AgentMsg,AIChatSession @@ -17,13 +19,14 @@ class MessageFilter: def select(self,msg:AgentMsg) -> AIRole: pass + class Workflow: def __init__(self) -> None: self.rule_prompt : AgentPrompt = None self.workflow_config = None self.role_group = None self.input_filter : MessageFilter= None - self.msg_queue = [] + self.msg_queue = Queue() self.connected_environment = {} def load_from_disk(self,config_path:str,context_dir_path) -> int: @@ -34,7 +37,7 @@ class Workflow: # 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.append(msg) + self.msg_queue.put_nowait(msg) return async def send_msg(self,msg:AgentMsg) -> str: @@ -180,7 +183,7 @@ class Workflow: def _parse_to_msg(self,llm_resp_str) -> AgentMsg: pass - def get_workflow(self,workflow_name:str) -> Workflow: + def get_workflow(self,workflow_name:str): """get workflow from known workflow list or sub workflow list""" pass diff --git a/src/component/agent_manager/__init__.py b/src/component/agent_manager/__init__.py index 7313a5a..18058c7 100644 --- a/src/component/agent_manager/__init__.py +++ b/src/component/agent_manager/__init__.py @@ -1,3 +1 @@ -from .agent_manager import AgentManager,AgentManagerClient -from .agent import ai_agent -from .templete import ai_agent_templete \ No newline at end of file +from .agent_manager import AgentManager diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index af39c88..3d1284f 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -22,13 +22,13 @@ class AgentManager: def initial(self,root_dir:str) -> None: - self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{root_dir}templetes/agent_templetes.cfg") + 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") if self.agent_templete_env is None: raise Exception("agent_manager initial failed") - def get(self,agent_id:str) -> AIAgent: + async def get(self,agent_id:str) -> AIAgent: the_agent = self.loaded_agent_instance.get(agent_id) if the_agent: return the_agent @@ -38,17 +38,17 @@ class AgentManager: if agent_media_info is None: return None - the_agent : AIAgent = self._load_agent_from_media(agent_media_info) + the_agent : AIAgent = await self._load_agent_from_media(agent_media_info) if the_agent is None: logger.warn(f"load agent {agent_id} from media failed!") - + else: + the_agent.start() return the_agent - def remove(self,agent_id:str)->int: pass - def get_templete(self,templete_id) -> AIAgentTemplete: + async def get_templete(self,templete_id) -> AIAgentTemplete: template_media_info = self.agent_templete_env.get(templete_id) if template_media_info is None: return None @@ -61,11 +61,33 @@ class AgentManager: def uninstall(self,templete_id) -> int: pass - def _load_templete_from_media(self,templete_media:PackageMediaInfo) -> AIAgentTemplete: + async def _load_templete_from_media(self,templete_media:PackageMediaInfo) -> AIAgentTemplete: pass - def _load_agent_from_media(self,agent_media:PackageMediaInfo) -> AIAgent: - pass + async def _load_agent_from_media(self,agent_media:PackageMediaInfo) -> AIAgent: + reader = self.agent_env._create_media_loader(agent_media) + if reader is None: + logger.error(f"create media loader for {agent_media} failed!") + return None + + try: + config_file = await reader.read("agent.toml","r") + if config_file is None: + logger.error(f"read agent config from {agent_media} failed!") + return None + + config_data = await config_file.read() + config = toml.loads(config_data) + result_agent = AIAgent() + if result_agent.load_from_config(config) is False: + logger.error(f"load agent from {agent_media} failed!") + return None + return result_agent + except Exception as e: + logger.error(f"read agent.toml cfg from {agent_media} failed! unexpected error occurred: {str(e)}") + return None + + def create(self,template,agent_name,agent_last_name,agent_introduce) -> AIAgent: pass diff --git a/src/component/ndn_client/cid.py b/src/component/ndn_client/cid.py index da89ed4..8bb6764 100644 --- a/src/component/ndn_client/cid.py +++ b/src/component/ndn_client/cid.py @@ -4,6 +4,9 @@ class ContentId: def __init__(self) -> None: pass + def as_str(self) -> str: + pass + @staticmethod def create_from_str(cid_str:str): pass \ No newline at end of file diff --git a/src/component/ndn_client/ndn_client.py b/src/component/ndn_client/ndn_client.py index fe46f6b..e9c46a6 100644 --- a/src/component/ndn_client/ndn_client.py +++ b/src/component/ndn_client/ndn_client.py @@ -1,13 +1,117 @@ +import asyncio,aiofiles,aiohttp +import logging +from typing import Optional from .cid import ContentId +logger = logging.getLogger(__name__) + +NDN_GET_TASK_STATE_INIT = 0 +NDN_GET_TAKS_CONNECTING = 1 +NDN_GET_TASK_STATE_DOWNLOADING = 2 +NDN_GET_TASK_STATE_VERIFYING = 3 +NDN_GET_TASK_STATE_DONE = 4 +NDN_GET_TASK_STATE_ERROR = 5 + class NDN_GetTask: def __init__(self) -> None: + self.cid:str = None + self.target_path:str = None + self.urls:[str] = None + self.options:Optional[dict] = None + + self.working_task = None + self.state = NDN_GET_TASK_STATE_INIT + self.total_size = 0 + self.recv_bytes = 0 + self.write_bytes = 0 + self.error_str = None + self.chunk_queue = None + + self.retry_count = 0 + self.used_urls = [] + self.hash_update = None + + + def select_url(self,index:int)->str: + return self.urls[0] + + def get_chunk_for_download(self)->bytes: pass class NDN_Client: def __init__(self): - pass + self.cache_dir = "" + self.default_ndn_http_gateway = "" + self.all_task = {} + self.memory_chunk_size = 1024*1024*2 + self.chunk_queue_size = 16 - def get_file(self,cid:ContentId,target_path:str,urls:{}=None,options:{}=None): - pass \ No newline at end of file + def load_config(self,config:dict): + if config.get("cache_dir"): + self.cache_dir = config.get("cache_dir") + if config.get("dndn_gateway"): + self.default_ndn_http_gateway = config.get("ndn_gateway") + + def get_file(self,cid:ContentId,target_path:str,urls:{}=None,options:{}=None)->NDN_GetTask: + get_task = self.all_task.get(cid.as_str()) + if get_task: + return get_task + else: + get_task = NDN_GetTask() + self.all_task[cid.as_str()] = get_task + + get_task.cid = cid + get_task.target_path = target_path + get_task.urls = urls + get_task.options = options + if get_task.urls is None: + get_task.urls = [f"{self.default_ndn_http_gateway}/{cid.as_str()}"] + logger.info(f"get_file {cid.as_str()} urls is None, use {get_task.urls[0]} as default") + + + async def get_file_async(): + target_file = aiofiles.open(target, 'wb') + # if file exist, check hash first + + http_session = aiohttp.ClientSession() + resp = http_session.get(get_task.select_url(0)) + if resp.status != 200: + get_task.error_str = f"get_file {cid.as_str()} failed,http status:{resp.status}" + return + get_task.total_size = resp.content_length + + async def write_file_async(): + while True: + chunk = await get_task.chunk_queue.pop() + chunk_size = len(chunk) + if not chunk or chunk_size == 0: + break + get_task.hash_update.update(chunk) + await target_file.write(chunk) + get_task.write_bytes += chunk_size + + #verify + get_task.state = NDN_GET_TASK_STATE_VERIFYING + await target_file.close() + return + + write_task = asyncio.create_task(write_file_async()) + while True: + await get_task.chunk_queue.pop() + chunk = resp.content.read(self.memory_chunk_size) + chunk_size = len(chunk) + if not chunk or chunk_size == 0: + break + + get_task.recv_bytes += len(chunk) + get_task.chunk_queue.push(chunk) + + + get_task.state = NDN_GET_TASK_STATE_DONE + await write_task + + get_task.working_task = asyncio.create_task(get_file_async()) + return get_task + + \ No newline at end of file diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py index b71813d..0ef9cfe 100644 --- a/src/component/package_manager/env.py +++ b/src/component/package_manager/env.py @@ -1,13 +1,137 @@ import logging import toml +import os -from .pkg import PackageInfo,PackageMediaInfo, -from .env import PackageEnv -from .installer import PackageInstaller,PackageInstallTask +from .pkg import PackageInfo,PackageMediaInfo +from .media_reader import MediaReader logger = logging.getLogger(__name__) + +class PackageEnv: + def __init__(self,cfg_path:str) -> None: + self.pkg_dir : str = "./pkgs/" + self.pkg_obj_dir : str = "./.pkgs/" + + self.locked_index : str = "./pkg.lock" + self.is_strict : bool = True + self.parent_envs : list[PackageEnv] = None + self.index_dbs = None + + self.env_dir = None + self.cfg_path = cfg_path + self._load_pkg_cfg(cfg_path) + pass + + def load_from_config(self,config:dict) -> bool: + if config.get("main") is not None: + self.pkg_dir = os.path.abspath(self.env_dir + "/" + config["main"]) + + if config.get("cache") is not None: + self.pkg_obj_dir = os.path.abspath(self.env_dir + "/ " + config["cache"]) + + def load(self,pkg_name:str,search_parent=True) -> PackageMediaInfo: + pkg_path = None + pkg_id,verion_str,cid = PackageInfo.parse_pkg_name(pkg_name) + + if cid is None: + if verion_str is None: + pkg_path = f"{self.pkg_dir}/{pkg_id}" + else: + #TODO fix bug about channel here + channel:str = self.get_pkg_channel_from_version(verion_str) + the_version:str = self.get_exact_version_from_installed(verion_str) + if the_version is None: + logger.warn(f"load {pkg_name} failed: no match version from {verion_str}") + return None + if channel is None: + pkg_path = f"{self.pkg_dir}/{pkg_id}#{the_version}" + else: + pkg_path = f"{self.pkg_dir}/{pkg_id}#{channel}#{the_version}" + else: + pkg_path = f"{self.pkg_obj_dir}/.{pkg_id}/{cid}" + + media_info:PackageMediaInfo = self.try_load_pkg_media_info(pkg_path) + if media_info is None: + if search_parent is True and self.parent_envs is not None: + for parent_env in self.parent_envs: + media_info = parent_env.load(pkg_id,cid,False) + if media_info is not None: + return media_info + + if media_info is None: + logger.warn(f"pkg_load {pkg_id}, cid:{cid} error,not found ,search_parent={search_parent}") + + return media_info + + def get_exact_version_from_installed(self,verion_str:str) -> str: + pass + + def get_pkg_channel_from_version(self,pkg_version:str) -> str: + args = pkg_version.split("~") + if len(args) == 1: + return None + else: + return args[0] + + + def get_pkg_media_info(self,pkg_name:str)->PackageMediaInfo: + pass + + def try_load_pkg_media_info(self,pkg_full_path:str) -> PackageMediaInfo: + the_result : PackageMediaInfo = None + logger.debug(f"try load pkng from:{pkg_full_path}") + if os.path.isdir(pkg_full_path): + the_result = PackageMediaInfo(pkg_full_path,"dir") + + return the_result + + def _create_media_loader(self,media_info:PackageMediaInfo) -> MediaReader: + match media_info.media_type: + case "dir": + from .media_reader import FolderMediaReader + return FolderMediaReader(media_info.full_path) + + logger.error(f"create media loader for {media_info} failed!") + return None + + def get_installed_pkg_info(self,pkg_name:str) -> PackageInfo: + pass + + def lookup(self,pkg_id:str,version_str:str) -> PackageInfo: + # to make sure pkg.cid is correct, we MUST verfiy eveything here + pass + + @classmethod + def is_valied_media(pkg_full_path:str) -> bool: + pass + + def do_pkg_media_trans(self,pkg_info:PackageInfo,source_path:str,target_path:str) -> bool: + pass + + def _load_pkg_cfg(self,cfg_path:str): + if cfg_path is None: + return + + cfg = None + if len(cfg_path) < 1: + return + try: + cfg = toml.load(cfg_path) + self.env_dir = os.path.abspath(os.path.dirname(cfg_path)) + self.cfg_path = os.path.abspath(cfg_path) + except Exception as e: + logger.error(f"read pkg cfg from {cfg_path} failed! unexpected error occurred: {str(e)}") + return + + return self.load_from_config(cfg) + + + + def _preprocess_prefixs(self,prefixs): + pass + class PackageEnvManager: _instance = None def __new__(cls): @@ -33,105 +157,3 @@ class PackageEnvManager: def get_system_env(self) -> PackageEnv: pass - -class PackageEnv: - def __init__(self,cfg_path:str) -> None: - self.pkg_dir : str = "" - self.pkg_obj_dir : str = "" - self.is_strict : bool = True - self.parent_envs : list[PackageEnv] = None - self.index_dbs = None - - self.cfg_path = cfg_path - self._load_pkg_cfg(cfg_path) - pass - - def load(self,pkg_name:str,search_parent=True) -> PackageMediaInfo: - pkg_path = None - pkg_id,verion_str,cid = PackageInfo.parse_pkg_name(pkg_name) - - if cid is None: - if verion_str is None: - channel:str = self.get_pkg_channel_from_version(verion_str) - if channel is None: - pkg_path = f"{self.pkg_dir}{pkg_id}" - else: - pkg_path = f"{self.pkg_dir}{pkg_id}#{channel}" - else: - channel:str = self.get_pkg_channel_from_version(verion_str) - the_version:str = self.get_exact_version_from_installed(verion_str) - if the_version is None: - logger.warn(f"load {pkg_name} failed: no match version from {verion_str}") - return None - if channel is None: - pkg_path = f"{self.pkg_dir}{pkg_id}#{the_version}" - else: - pkg_path = f"{self.pkg_dir}{pkg_id}#{channel}#{the_version}" - else: - pkg_path = f"{self.pkg_obj_dir}.{pkg_id}/{cid}" - - media_info:PackageMediaInfo = self.try_load_pkg_media_info(pkg_path) - if media_info is None: - if search_parent: - for parent_env in self.parent_envs: - media_info = parent_env.load(pkg_id,cid,False) - if media_info is not None: - return media_info - - logger.warn(f"load {pkg_id}#{cid} error,not found ,search_parent={search_parent}") - return None - - def get_exact_version_from_installed(self,verion_str:str) -> str: - pass - - def get_pkg_channel_from_version(self,pkg_version:str) -> str: - pass - - def get_pkg_media_info(self,pkg_name:str)->PackageMediaInfo: - pass - - def try_load_pkg_media_info(self,pkg_full_path:str) -> PackageMediaInfo: - pass - - - def get_installed_pkg_info(self,pkg_name:str) -> PackageInfo: - pass - - def lookup(self,pkg_id:str,version_str:str) -> PackageInfo: - # to make sure pkg.cid is correct, we MUST verfiy eveything here - pass - - def get_installer(self) -> PackageInstaller: - pass - - @classmethod - def is_valied_media(pkg_full_path:str) -> bool: - pass - - def do_pkg_media_trans(self,pkg_info:PackageInfo,source_path:str,target_path:str) -> bool: - pass - - def _load_pkg_cfg(self,cfg_path:str): - if cfg_path is None: - return - - cfg = None - if len(cfg_path) < 1: - return - try: - cfg = toml.load(cfg_path) - except Exception as e: - logger.error(f"read pkg cfg from {cfg_path} failed! unexpected error occurred: {str(e)}") - return - - if cfg: - if cfg.env: - if cfg.env.is_strict is not None: - self.is_strict = cfg.env.is_strict - - if cfg.env.prefixs is not None: - self.prefixs = self._preprocess_prefixs(cfg.env.prefixs) - - - def _preprocess_prefixs(self,prefixs): - pass \ No newline at end of file diff --git a/src/component/package_manager/media_reader.py b/src/component/package_manager/media_reader.py index d1b941d..9c7f72e 100644 --- a/src/component/package_manager/media_reader.py +++ b/src/component/package_manager/media_reader.py @@ -1,9 +1,18 @@ -class MediaReader: - def __init__(self): +from abc import ABC, abstractmethod +import aiofiles + +class MediaReader(ABC): + @abstractmethod + async def read(self, inner_path:str,mode:str): pass - def load_from_media(self,media_info:str) -> int: + +class FolderMediaReader(MediaReader): + def __init__(self, root_dir:str) -> None: + self.root_dir = root_dir pass - def read(self, inner_path:str): - raise NotImplementedError \ No newline at end of file + async def read(self, inner_path:str,mode:str): + full_path = self.root_dir + "/" + inner_path + result_file = await aiofiles.open(full_path, mode,encoding='utf-8') + return result_file \ No newline at end of file diff --git a/src/component/package_manager/pkg.py b/src/component/package_manager/pkg.py index 9944103..7ed8431 100644 --- a/src/component/package_manager/pkg.py +++ b/src/component/package_manager/pkg.py @@ -11,10 +11,20 @@ class PackageInfo: self.target_media_type = "dir" self.source_media_type = "7z" - @classmethod - def parse_pkg_name(cls,pkg_name:str) -> Tuple[str, str, str]: - """parse pkg name like test-pkg#nightly#>0.2.31#sha1:323423423 to test-pkg,nightly#>0.2.31,sha1:323423423""" - pass + @staticmethod + def parse_pkg_name(pkg_name:str) -> Tuple[str, str, str]: + """parse pkg name like test-pkg#nightly~>0.2.31#sha1:323423423 to test-pkg,nightly#>0.2.31,sha1:323423423""" + args = pkg_name.split("#") + if len(args) == 1: + return args[0],None,None + elif len(args) == 2: + return args[0],None,arg[2] + elif len(args) == 3: + return args[0],args[1],args[2] + else: + logger.error(f"parse pkg name {pkg_name} failed!") + return None,None,None + @@ -23,8 +33,9 @@ class PackageInfo: return self.cid class PackageMediaInfo: - def __init__(self) -> None: - pass + def __init__(self,full_path,media_type) -> None: + self.media_type = media_type + self.full_path = full_path diff --git a/src/component/workflow_manager/__init__.py b/src/component/workflow_manager/__init__.py new file mode 100644 index 0000000..de62e89 --- /dev/null +++ b/src/component/workflow_manager/__init__.py @@ -0,0 +1 @@ +from .workflow_manager import WorkflowManager \ No newline at end of file diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py new file mode 100644 index 0000000..2a6a68d --- /dev/null +++ b/src/component/workflow_manager/workflow_manager.py @@ -0,0 +1,11 @@ +from aios_kernel import Workflow + +class WorkflowManager: + def __init__(self) -> None: + pass + + def initial(self,root_dir:str) -> None: + pass + + def get_workflow(self,workflow_id:str) -> Workflow: + pass \ No newline at end of file diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py new file mode 100644 index 0000000..7d989ca --- /dev/null +++ b/src/service/aios_shell/aios_shell.py @@ -0,0 +1,128 @@ +# aiso shell like bash for linux +import asyncio +import sys +import os +import logging + +from typing import Any, Optional, TypeVar, Tuple, Sequence +import argparse + +from prompt_toolkit.formatted_text.base import AnyFormattedText +from prompt_toolkit import Application, PromptSession, prompt +from prompt_toolkit.selection import SelectionState +from prompt_toolkit.history import FileHistory +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit.completion import WordCompleter + + +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../../') +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode + +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.user_chatsession = {} + + async def initial(self) -> bool: + AgentManager().initial(directory + "/../../../rootfs/") + WorkflowManager().initial(directory + "/../../../../rootfs/workflows/workflows.cfg") + open_ai_node = OpenAI_ComputeNode() + open_ai_node.start() + ComputeKernel().add_compute_node(open_ai_node) + return True + + + def get_version(self) -> str: + return "0.0.1" + + async def send_msg(self,msg:str,target_id:str,sender:str = None) -> str: + agent_msg = AgentMsg() + agent_msg.set(sender,target_id,msg) + + agent : AIAgent = await AgentManager().get(target_id) + if agent is not None: + agent.post_msg(agent_msg) + + a_workflow = WorkflowManager().get_workflow(target_id) + if a_workflow is not None: + a_workflow.post_msg(agent_msg) + + + async def check_timer(): + check_times = 0 + while True: + if agent_msg.state == AgentMsgState.RESPONSED: + break + + if agent_msg.state == AgentMsgState.ERROR: + break + + if check_times >= 20: + agent_msg.state = AgentMsgState.ERROR + break + + await asyncio.sleep(0.5) + check_times += 1 + + await asyncio.create_task(check_timer()) + if agent_msg.state == AgentMsgState.RESPONSED: + return agent_msg.resp_msg.body + + return "error!" + + + async def install_workflow(self,workflow_id:Workflow) -> None: + pass + + +####################################################################################### +def proc_input_by_agent(): + pass + +def show_help(): + print("this is help") + + +async def main(): + print("aios shell prepareing...") + logging.basicConfig(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.") + + + completer = WordCompleter(['list agent', 'list workflow', 'exit', 'help'], ignore_case=True) + #history = FileHistory('history.txt') + while True: + user_input = await PromptSession().prompt_async('>>> ',completer=completer) + match user_input: + case "list agent": + print(AgentManager().list_agent()) + case "list workflow": + print(WorkflowManager().list_workflow()) + case "help": + show_help() + case "exit": + break + + + if user_input.startswith("send"): + args = user_input.split(" ") + if len(args) < 3: + print("send msg failed, usage: send target_id msg_content") + continue + target_id = args[1] + msg_content = args[2] + resp = await shell.send_msg(msg_content,target_id,shell.username) + print(f"<<< {resp}") + +if __name__ == "__main__": + asyncio.run(main()) + From 076eeaf977d015ef57c095d13cef374f2ecbe149 Mon Sep 17 00:00:00 2001 From: Si Changjun Date: Thu, 24 Aug 2023 15:54:52 +0800 Subject: [PATCH 010/177] knowleadge.drawio --- doc/未命名绘图.drawio | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/未命名绘图.drawio diff --git a/doc/未命名绘图.drawio b/doc/未命名绘图.drawio new file mode 100644 index 0000000..f8db6cf --- /dev/null +++ b/doc/未命名绘图.drawio @@ -0,0 +1,10 @@ + + + + + + + + + + From 67677ba40fcd11300c3a86cdb47e1682e8f35d30 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Tue, 29 Aug 2023 16:45:05 +0800 Subject: [PATCH 011/177] objected knowledge base design --- doc/mvp/knowleadge.drawio | 1478 +++++++++++++++++++++++++++++++++++++ doc/mvp/knowleage.md | 20 + doc/未命名绘图.drawio | 10 - 3 files changed, 1498 insertions(+), 10 deletions(-) create mode 100644 doc/mvp/knowleadge.drawio create mode 100644 doc/mvp/knowleage.md delete mode 100644 doc/未命名绘图.drawio diff --git a/doc/mvp/knowleadge.drawio b/doc/mvp/knowleadge.drawio new file mode 100644 index 0000000..bdb5534 --- /dev/null +++ b/doc/mvp/knowleadge.drawio @@ -0,0 +1,1478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/mvp/knowleage.md b/doc/mvp/knowleage.md new file mode 100644 index 0000000..7b48cac --- /dev/null +++ b/doc/mvp/knowleage.md @@ -0,0 +1,20 @@ +# Objected knowleadge base, a specialized implemention for emails +## Vectorized Knowledge +Large language models are trained on general corpora and without fine-tuning on user-specific data, they struggle to utilize user-related context effectively. + +Users accumulate a vast amount of content that reflects their personality during their regular internet usage. This includes personal photos, tweets, Facebook posts, emails, etc. While it's possible to include all this content in the prompt during each interaction with the large language model, this approach is costly and can easily reach the token limit. + +A common solution is to generate feature vectors from this content using word embedding techniques and store them in a vector database. During an interaction, the vector that is most relevant to the prompt is retrieved from the database, merged with the prompt, and then passed to the large language model. + +We refer to this vectorized content as "knowledge". + +## Objected knownleadge base +In a personal AI system, to build a user's own knowledge base, we first need to implement various spider programs to crawl and retrieve all user-related data. Modern web content is typically rich text, including text, images, videos, hyperlinks, etc. Organizing this rich text in a tree-like structure similar to HTML is necessary, hence the need to introduce an object structure to represent this content. + +Different parts of this content cannot be vectorized using the same embedding model. For instance, text and images, as well as the content of an image and its EXIF information, need separate embeddings. This means that in the vector database, the same content may have multiple vector values, and a row can represent a whole content item or just a part of it. + +We need a comprehensive object structure to represent the hierarchy and relationships of content, as well as to implement the indexing and storage of objects. In the Minimum Viable Product (MVP) version, we'll implement a specialized solution for email content. In future versions, we can generalize this to handle other types of content, such as Facebook posts, tweets, etc. + +## Agent with knowleadge base +At the same time, we also need to explore the paradigm of using the knowledge base in Agents and workflows, so that the agent can better complete tasks in interaction with users through the context provided by the knowledge base. + diff --git a/doc/未命名绘图.drawio b/doc/未命名绘图.drawio deleted file mode 100644 index f8db6cf..0000000 --- a/doc/未命名绘图.drawio +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - From a7960861d12fd34411ce18af1b58d77e7630d826 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Wed, 30 Aug 2023 17:16:37 +0800 Subject: [PATCH 012/177] objected knowledge base implement for email code structure --- doc/mvp/workflow.drawio | 6 +- doc/未命名绘图.drawio | 10 ---- src/aios_kernel/knowledge/__init__.py | 2 + src/aios_kernel/knowledge/knowledge_base.py | 10 ++++ src/aios_kernel/knowledge/object.py | 65 +++++++++++++++++++++ src/aios_kernel/knowledge/object_storage.py | 33 +++++++++++ src/aios_kernel/knowledge/vector_base.py | 14 +++++ src/service/email_spider/converter.py | 24 ++++++++ src/service/email_spider/main.py | 12 ++++ src/service/email_spider/spider.py | 17 ++++++ 10 files changed, 180 insertions(+), 13 deletions(-) delete mode 100644 doc/未命名绘图.drawio create mode 100644 src/aios_kernel/knowledge/__init__.py create mode 100644 src/aios_kernel/knowledge/knowledge_base.py create mode 100644 src/aios_kernel/knowledge/object.py create mode 100644 src/aios_kernel/knowledge/object_storage.py create mode 100644 src/aios_kernel/knowledge/vector_base.py create mode 100644 src/service/email_spider/converter.py create mode 100644 src/service/email_spider/main.py create mode 100644 src/service/email_spider/spider.py diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index e49374a..c6659c1 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -123,7 +123,7 @@ - + @@ -220,7 +220,7 @@ - + diff --git a/doc/未命名绘图.drawio b/doc/未命名绘图.drawio deleted file mode 100644 index f8db6cf..0000000 --- a/doc/未命名绘图.drawio +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/aios_kernel/knowledge/__init__.py b/src/aios_kernel/knowledge/__init__.py new file mode 100644 index 0000000..13bb147 --- /dev/null +++ b/src/aios_kernel/knowledge/__init__.py @@ -0,0 +1,2 @@ +from .object import EmailObject, ImageObject, TextChunkObject, ObjectID +from .knowledge_base import KnowledgeBase diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py new file mode 100644 index 0000000..59e4f25 --- /dev/null +++ b/src/aios_kernel/knowledge/knowledge_base.py @@ -0,0 +1,10 @@ +# define a knowledge base class +from src.aios_kernel.agent import AgentPrompt +from .object import KnowledgeObject + +class KnowledgeBase: + async def insert(self, object: KnowledgeObject): + pass + + async def query(self, prompt: AgentPrompt) -> AgentPrompt: + pass \ No newline at end of file diff --git a/src/aios_kernel/knowledge/object.py b/src/aios_kernel/knowledge/object.py new file mode 100644 index 0000000..320cd8f --- /dev/null +++ b/src/aios_kernel/knowledge/object.py @@ -0,0 +1,65 @@ + +# define a object type enum +from abc import ABC + + +class ObjectType(Enum): + TextChunk = 1 + Image = 2 + Email = 101 + + +# define a object ID class to identify a object +class ObjectID: # pylint: disable=too-few-public-methods + def __init__(self, object_type, digist): + self.object_type = object_type + self.digist = digist + + def __str__(self): + return f"{self.object_type.name}:{self.digist}" + + +# define a object class +class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods + def __init__(self, object_type): + self.object_type = object_type + + @abstractmethod + def get_id(self) -> ObjectID: + pass + + # define a to binary method to convert object to binary + @abstractmethod + def to_binary(self) -> bytes: + pass + + # define a from binary method to convert binary to object + @abstractmethod + def from_binary(self, binary: bytes): + pass + + +# define a text chunk class +class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, text): + super().__init__(ObjectType.TextChunk) + self.text = text + + +# define a image class +class ImageObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta, path): + super().__init__(ObjectType.Image) + self.meta = meta + self.path = path + + +# define a email class +class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta): + super().__init__(ObjectType.Email) + self.meta = meta + self.text = [] + self.images = [] + + diff --git a/src/aios_kernel/knowledge/object_storage.py b/src/aios_kernel/knowledge/object_storage.py new file mode 100644 index 0000000..1a6f85e --- /dev/null +++ b/src/aios_kernel/knowledge/object_storage.py @@ -0,0 +1,33 @@ +# import RDB LargeBinary +from sqlalchemy import Column, String, LargeBinary, create_engine, sessionmaker, pickle +from .object import KnowledgeObject + +# implement object storage with RDB +# define object storage table +class ObjectStorageTable(Base): + __tablename__ = 'object_storage' + id = Column(String, primary_key=True) + parent = Column(String, nullable=True) + object = Column(LargeBinary, nullable=False) + + def __init__(self, id, parent, object): # pylint: disable=redefined-builtin + self.id = id + self.parent = parent + self.object = object + +# define object storage class +class ObjectStorage: + async def __init__(self, db_url): + self.engine = create_engine(db_url) + self.session = sessionmaker(bind=self.engine)() # pylint: disable=not-callable + + async def get(self, id) -> [KnowledgeObject, KnowledgeObject]: + obj = self.session.query(ObjectStorageTable).filter(ObjectStorageTable.id == id).first() + if obj is None: + return None + return pickle.loads(obj.object) + + # define insert method + async def insert(self, object, parent): # pylint: disable=redefined-builtin + obj = ObjectStorageTable(id, parent, pickle.dumps(object)) + \ No newline at end of file diff --git a/src/aios_kernel/knowledge/vector_base.py b/src/aios_kernel/knowledge/vector_base.py new file mode 100644 index 0000000..b2cf442 --- /dev/null +++ b/src/aios_kernel/knowledge/vector_base.py @@ -0,0 +1,14 @@ +# import the ObjectID class +from .object import ObjectID + +# define a vector base class +class VectorBase: + def __init__(self, db_url, model_name) -> None: + self.db_url = db_url + self.model_name = model_name + + async def insert(self, vector: [float], id: ObjectID): + pass + + async def query(self, vector: [float], top_k: int) -> [ObjectID]: + pass \ No newline at end of file diff --git a/src/service/email_spider/converter.py b/src/service/email_spider/converter.py new file mode 100644 index 0000000..193b220 --- /dev/null +++ b/src/service/email_spider/converter.py @@ -0,0 +1,24 @@ +from aios_kernel.knowledge import KnowledgeBase, EmailObject + +# define a email converter class + +class EmailConverter: + # define init method + def __init__(self, local_dir, knowledge_base: KnowledgeBase) -> None: + pass + + async def run(self): + # convert the email to knowledge object + for email_dir in self._next(): + # convert the email to knowledge object + knowledge_object = self._convert(email_dir) + # insert the knowledge object to knowledge base + await self.knowledge_base.insert(knowledge_object) + + def _next(self) -> str: + pass + + def _convert(self, email_dir) -> EmailObject: + pass + + \ No newline at end of file diff --git a/src/service/email_spider/main.py b/src/service/email_spider/main.py new file mode 100644 index 0000000..cb75abd --- /dev/null +++ b/src/service/email_spider/main.py @@ -0,0 +1,12 @@ +import asyncio +from .spider import EmailSpider, EmailConverter + + +if __name__ == "__main__": + spider = EmailSpider("smtp.163.com","user","pwd","./email") + asyncio.run(spider.run()) + + converter = EmailConverter("./email",KnowledgeBase()) + asyncio.run(converter.run()) + + diff --git a/src/service/email_spider/spider.py b/src/service/email_spider/spider.py new file mode 100644 index 0000000..0b5a212 --- /dev/null +++ b/src/service/email_spider/spider.py @@ -0,0 +1,17 @@ +# define a email spider class + +class EmailSpider: + def __init__(self, address, account, pwd, local_dir) -> None: + pass + + async def run(self): + # spide the email from the email server + for email_link in self._next(): + # save the email to local directory + self._save(email_link) + + def _next(self): + pass + + def _save(self, email_link) -> str: + pass \ No newline at end of file From 05d2e4a9cf8ad0df59c0a4cc96dd35ba3605c90e Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 30 Aug 2023 12:30:41 -0700 Subject: [PATCH 013/177] 1.workflow has reached a state of basic functionality. 2. implemented AI message bus infrastructure, --- .gitignore | 1 + doc/mvp/process model.drawio | 136 +++++++ doc/mvp/workflow.drawio | 4 +- rootfs/agents/agent_1/agent.toml | 2 +- rootfs/agents/email_reader/agent.toml | 5 + rootfs/agents/manager/agent.toml | 6 + rootfs/workflows/plan_maker/workflow.toml | 74 ++++ rootfs/workflows/workflows.cfg | 2 + src/aios_kernel/__init__.py | 7 +- src/aios_kernel/agent.py | 137 ++----- src/aios_kernel/agent_message.py | 39 ++ src/aios_kernel/bus.py | 129 +++++++ src/aios_kernel/chatsession.py | 253 +++++++++++++ src/aios_kernel/compute_kernel.py | 5 +- src/aios_kernel/open_ai_node.py | 4 +- src/aios_kernel/role.py | 51 ++- src/aios_kernel/workflow.py | 350 ++++++++++++------ src/component/agent_manager/agent_manager.py | 14 +- .../workflow_manager/workflow_manager.py | 73 +++- src/service/aios_shell/aios_shell.py | 76 ++-- test/chatsession_test.py | 75 ++++ 21 files changed, 1146 insertions(+), 297 deletions(-) create mode 100644 doc/mvp/process model.drawio create mode 100644 rootfs/agents/email_reader/agent.toml create mode 100644 rootfs/agents/manager/agent.toml create mode 100644 rootfs/workflows/plan_maker/workflow.toml create mode 100644 rootfs/workflows/workflows.cfg create mode 100644 src/aios_kernel/agent_message.py create mode 100644 src/aios_kernel/bus.py create mode 100644 src/aios_kernel/chatsession.py create mode 100644 test/chatsession_test.py diff --git a/.gitignore b/.gitignore index 1d74e21..4ef429f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .vscode/ +*.pyc diff --git a/doc/mvp/process model.drawio b/doc/mvp/process model.drawio new file mode 100644 index 0000000..86d56e7 --- /dev/null +++ b/doc/mvp/process model.drawio @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index e49374a..2b3e239 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -220,7 +220,7 @@ - + diff --git a/rootfs/agents/agent_1/agent.toml b/rootfs/agents/agent_1/agent.toml index 9b13b5f..81966ab 100644 --- a/rootfs/agents/agent_1/agent.toml +++ b/rootfs/agents/agent_1/agent.toml @@ -1,4 +1,4 @@ -instance_id = "agent:xxxxxxabcde" +instance_id = "agent_1" fullname = "tracy wang" [[prompt]] role = "system" diff --git a/rootfs/agents/email_reader/agent.toml b/rootfs/agents/email_reader/agent.toml new file mode 100644 index 0000000..a6baba9 --- /dev/null +++ b/rootfs/agents/email_reader/agent.toml @@ -0,0 +1,5 @@ +instance_id = "agent:xxxxxxabcde" +fullname = "jackma" +[[prompt]] +role = "system" +content = "你擅长处理商务电子邮件" diff --git a/rootfs/agents/manager/agent.toml b/rootfs/agents/manager/agent.toml new file mode 100644 index 0000000..5bb2ba6 --- /dev/null +++ b/rootfs/agents/manager/agent.toml @@ -0,0 +1,6 @@ +instance_id = "agent:xxxxxxabcde" +fullname = "musk" +[[prompt]] +role = "system" +content = "你有丰富的管理技能,擅长将复杂工作拆解成简单的任务,让团队成员高效协作。" + diff --git a/rootfs/workflows/plan_maker/workflow.toml b/rootfs/workflows/plan_maker/workflow.toml new file mode 100644 index 0000000..e5b150f --- /dev/null +++ b/rootfs/workflows/plan_maker/workflow.toml @@ -0,0 +1,74 @@ +# workflow实例场景 +# 能展示sub workflow +# 能展示env的整合 +# 能展示filter的使用 +# 能展示function的调用 +# 能展示基于工作目标/KPI的sub workflow迭代流程 +# 多人场景安排 + +# 例子:举办一个团队活动 +# 方案讨论(通过交互引导的方式收集主人的需求)与确定 + +# 活动前: +# 通讯员,对接管理参加活动的人的情况 (email spider) +# 酒店预订 简单:搜索(酒店评价) 处理异常 +# 行程(票务)预订 :搜索,处理异常 +# 餐饮预订:给出方案,确定细节,预订 + +# 活动中: +# 进行统计和分析,调整设备 +# 安保,空调,音乐,拍照,录像 +# 响应紧急情况 + +# 活动结束后: +# 整理照片,视频,进行必要的二次创作,发送给相关人员 +# 对活动进行总结,提出改件意见(指导下一次活动) + +# 1. 人员 +# 主管,负责和客户沟通,并对每个环境的结果进行总结 +# 嘉宾对接 +# 酒店组 +# 行程组 +# 财务组 +# 多媒体组 +name = "plan_maker" + +[filter] +"*" = "manager" + +[roles.manager] +name = "manager" +fullname = "经理" +agent="manager" +[[roles.manager.prompt]] +role="system" +content="""你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是: +1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 +2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。 +3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。 +4. 当你决定要和工作小组通信时,请使用`sendmsg({小组名称},{内容}`)的形式。""" + + + + +[sub_workflows] +[sub_workflows."嘉宾对接组"] +name="嘉宾对接组" +filter = {"*" = "leader"} + +# 展现读取email和发送email与嘉宾沟通的能力 +[sub_workflows."嘉宾对接组".environments.email] +new_mail = "收到来自{event.data.from},标题为{event.data.subject}的邮件,内容为{event.data.content}的电子邮件" # 这里将new_mail事件转换为了一个来自环境的message + +[sub_workflows."嘉宾对接组".roles.leader] +name = "leader" +fullname = "嘉宾对接组组长" +agent="email_reader" +[[sub_workflows."嘉宾对接组".roles.leader.prompt]] +role="system" +content="""你是一家活动策划公司的嘉宾对接组的组长,你的工作是基于已知信息,当前活动信息、公司经理的指令与嘉宾沟通,收集嘉宾的信息,然后将信息反馈给经理。在你看来,参加活动的多少有成员都是嘉宾,你可以通过你知道的信息给不同的成员进行分级。你的基本工作模式是: +1. 处理收到的邮件,如果邮件来自嘉宾,你会尝试从邮件的表态和内容中分享嘉宾的需要,并结合你对当前活动方案的理解判断是否需要和经理沟通,如果需要和经理沟通,你会将嘉宾的需求总结和告诉经理。不需要沟通的事项可以直接回复嘉宾。 +2. 你总是通过`call_function(get_env,'parent.topic'`的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。 +3. 当你决定要和经理通信时,请使用`send_msg(manager,{内容}`)的形式,内容的长度不超过200字。 +4. 当你决定要回复嘉宾时,请使用`call_function(sendmail,{嘉宾邮件地址},{标题},{内容})的形式,内容的长度不超过500字。""" +# 这里是孤立工作模式,组长只和经理沟通,也可以赋予其和其它组沟通的能力 diff --git a/rootfs/workflows/workflows.cfg b/rootfs/workflows/workflows.cfg new file mode 100644 index 0000000..f8a61a0 --- /dev/null +++ b/rootfs/workflows/workflows.cfg @@ -0,0 +1,2 @@ +main = "./" +cache = "./.workflows" \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 04d4cc5..bd1434b 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -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 \ No newline at end of file +from .workflow import Workflow +from .bus import AIBus \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 8346649..2680695 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -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 diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py new file mode 100644 index 0000000..3aa05c1 --- /dev/null +++ b/src/aios_kernel/agent_message.py @@ -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 diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py new file mode 100644 index 0000000..6c92a8a --- /dev/null +++ b/src/aios_kernel/bus.py @@ -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)) + + \ No newline at end of file diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py new file mode 100644 index 0000000..90890ed --- /dev/null +++ b/src/aios_kernel/chatsession.py @@ -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 diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 84351a7..1d05afd 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -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 diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index cf88815..7f5c8e6 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -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): diff --git a/src/aios_kernel/role.py b/src/aios_kernel/role.py index 0b22a95..c4d97ab 100644 --- a/src/aios_kernel/role.py +++ b/src/aios_kernel/role.py @@ -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) \ No newline at end of file diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 1c842b8..b3c3127 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -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""" diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index 3d1284f..9b05b62 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -10,10 +10,6 @@ logger = logging.getLogger(__name__) class AgentManager: _instance = None - - def __init__(self) -> None: - self.loaded_agent_instance = {} - pass def __new__(cls): if cls._instance is None: @@ -22,8 +18,10 @@ class AgentManager: 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.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.loaded_agent_instance = {} if self.agent_templete_env is None: raise Exception("agent_manager initial failed") @@ -41,8 +39,8 @@ class AgentManager: the_agent : AIAgent = await self._load_agent_from_media(agent_media_info) if the_agent is None: logger.warn(f"load agent {agent_id} from media failed!") - else: - the_agent.start() + + the_agent.chat_db = self.db_path return the_agent def remove(self,agent_id:str)->int: diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index 2a6a68d..28479dd 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -1,11 +1,74 @@ +import logging +import toml from aios_kernel import Workflow +from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask +from agent_manager import AgentManager +logger = logging.getLogger(__name__) +import os class WorkflowManager: - def __init__(self) -> None: - pass - + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(WorkflowManager, cls).__new__(cls) + return cls._instance + + def initial(self,root_dir:str) -> 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") + if self.workflow_env is None: + raise Exception("WorkflowManager initial failed") + + async def get_agent_default_workflow(self,agent_id:str) -> Workflow: pass - def get_workflow(self,workflow_id:str) -> Workflow: - pass \ No newline at end of file + async def get_workflow(self,workflow_id:str) -> Workflow: + the_workflow : Workflow = self.loaded_workflow.get(workflow_id) + if the_workflow: + return the_workflow + + # try load from disk + workflow_media_info = self.workflow_env.load(workflow_id) + if workflow_media_info is None: + return None + + 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!") + + 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 + + + return the_workflow + + async def _load_workflow_from_media(self,workflow_media:PackageMediaInfo) -> Workflow: + reader = self.workflow_env._create_media_loader(workflow_media) + if reader is None: + logger.error(f"create media loader for {workflow_media} failed!") + return None + + try: + config_file = await reader.read("workflow.toml","r") + if config_file is None: + logger.error(f"read workflow config from {workflow_media} failed!") + return None + + config_data = await config_file.read() + config = toml.loads(config_data) + result_workflow = Workflow() + 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)}") + return None \ No newline at end of file diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 7d989ca..fa48bdd 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -17,23 +17,42 @@ from prompt_toolkit.completion import WordCompleter directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus 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.user_chatsession = {} + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: + agent : AIAgent = await AgentManager().get(msg.target) + if agent is not None: + bus.register_message_handler(msg.target,agent._process_msg) + return True + + a_workflow = await WorkflowManager().get_workflow(msg.target) + 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) + return True + + return False + + async def initial(self) -> bool: - AgentManager().initial(directory + "/../../../rootfs/") - WorkflowManager().initial(directory + "/../../../../rootfs/workflows/workflows.cfg") + 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) + AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) return True @@ -43,38 +62,12 @@ class AIOS_Shell: async def send_msg(self,msg:str,target_id:str,sender:str = None) -> str: agent_msg = AgentMsg() agent_msg.set(sender,target_id,msg) - - agent : AIAgent = await AgentManager().get(target_id) - if agent is not None: - agent.post_msg(agent_msg) - - a_workflow = WorkflowManager().get_workflow(target_id) - if a_workflow is not None: - a_workflow.post_msg(agent_msg) - - - async def check_timer(): - check_times = 0 - while True: - if agent_msg.state == AgentMsgState.RESPONSED: - break - - if agent_msg.state == AgentMsgState.ERROR: - break - - if check_times >= 20: - agent_msg.state = AgentMsgState.ERROR - break - - await asyncio.sleep(0.5) - check_times += 1 - - await asyncio.create_task(check_timer()) - if agent_msg.state == AgentMsgState.RESPONSED: - return agent_msg.resp_msg.body - - return "error!" - + agent_msg.topic = "default" + resp = await AIBus().get_default_bus().send_message(target_id,agent_msg) + if resp is not None: + return resp.body + else: + return "error!" async def install_workflow(self,workflow_id:Workflow) -> None: pass @@ -87,21 +80,20 @@ def proc_input_by_agent(): def show_help(): print("this is help") +history = FileHistory('history.txt') +session = PromptSession(history=history) async def main(): print("aios shell prepareing...") - logging.basicConfig(level=logging.INFO, - format='%(asctime)s %(name)s %(levelname)s %(message)s') - + logging.basicConfig(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.") - completer = WordCompleter(['list agent', 'list workflow', 'exit', 'help'], ignore_case=True) #history = FileHistory('history.txt') while True: - user_input = await PromptSession().prompt_async('>>> ',completer=completer) + user_input = await session.prompt_async('>>> ',completer=completer) match user_input: case "list agent": print(AgentManager().list_agent()) @@ -121,8 +113,8 @@ async def main(): target_id = args[1] msg_content = args[2] resp = await shell.send_msg(msg_content,target_id,shell.username) - print(f"<<< {resp}") + print(f"<<<{target_id} : {resp}") -if __name__ == "__main__": +if __name__ == "__main__": asyncio.run(main()) diff --git a/test/chatsession_test.py b/test/chatsession_test.py new file mode 100644 index 0000000..c528556 --- /dev/null +++ b/test/chatsession_test.py @@ -0,0 +1,75 @@ +import unittest +import sys +import os +import sqlite3 +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../src') + +from aios_kernel import ChatSessionDB + + +class TestChatDatabase(unittest.TestCase): + + def setUp(self): + """Function to setup the test case""" + self.db_file = 'test_chat.db' + self.chat_db = ChatSessionDB(self.db_file) + + def tearDown(self): + """Function to cleanup after the test case""" + self.chat_db.close() + os.remove(self.db_file) + + def test_database_creation(self): + """Test if the database is created""" + self.assertTrue(os.path.exists(self.db_file)) + + def test_table_creation(self): + """Test if the tables are created in the database""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + # Check if ChatSessions table exists + cursor.execute(""" + SELECT count(name) FROM sqlite_master WHERE type='table' AND name='ChatSessions' + """) + self.assertEqual(cursor.fetchone()[0], 1) + + # Check if Messages table exists + cursor.execute(""" + SELECT count(name) FROM sqlite_master WHERE type='table' AND name='Messages' + """) + self.assertEqual(cursor.fetchone()[0], 1) + + conn.close() + + def test_insert_and_get_session(self): + """Test if we can insert and retrieve a session""" + session_id = "session1" + session_owner = "user1" + session_topic = "topic1" + start_time = "2023-08-28 12:00:00" + + self.chat_db.insert_chatsession(session_id,session_owner, session_topic, start_time) + session = self.chat_db.get_chatsession_by_id(session_id) + + self.assertEqual(session, (session_id,session_owner,session_topic, start_time)) + + def test_insert_and_get_message(self): + """Test if we can insert and retrieve a message""" + message_id = "message1" + session_id = "session1" + sender_id = "user1" + receiver_id = "user2" + timestamp = "2023-08-28 12:30:00" + content = "Hello, world!" + status = 0 + + self.chat_db.insert_message(message_id, session_id, sender_id, receiver_id, timestamp, content, status) + message = self.chat_db.get_message_by_id(message_id) + + self.assertEqual(message, (message_id, session_id, sender_id, receiver_id, timestamp, content, status)) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 93b6b8e7f2916b8820d5bcbd688f96765de30a21 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 30 Aug 2023 23:01:44 -0700 Subject: [PATCH 014/177] Improving aios_shell: We now have a basic, expandable framework, along with some basic features that will facilitate future testing. --- src/aios_kernel/workflow.py | 2 +- src/service/aios_shell/aios_shell.py | 162 ++++++++++++++++++++------- 2 files changed, 125 insertions(+), 39 deletions(-) diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index b3c3127..88a13ed 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -180,7 +180,7 @@ class Workflow: 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) + #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 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index fa48bdd..cded7d0 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -3,32 +3,42 @@ import asyncio import sys import os import logging +import re from typing import Any, Optional, TypeVar, Tuple, Sequence import argparse -from prompt_toolkit.formatted_text.base import AnyFormattedText -from prompt_toolkit import Application, PromptSession, prompt + +from prompt_toolkit import HTML, PromptSession, prompt,print_formatted_text +from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.selection import SelectionState from prompt_toolkit.history import FileHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import WordCompleter +from prompt_toolkit.styles import Style + +shell_style = Style.from_dict({ + 'title': '#87d7ff bold', #RGB + 'content': '#007f00 bold', + 'prompt': '#00FF00', +}) directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus +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.user_chatsession = {} + self.current_target = "_" + self.current_topic = "default" async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: agent : AIAgent = await AgentManager().get(msg.target) @@ -45,9 +55,15 @@ class AIOS_Shell: return False + async def is_agent(self,target_id:str) -> bool: + agent : AIAgent = await AgentManager().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/")) + AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) open_ai_node = OpenAI_ComputeNode() open_ai_node.start() @@ -59,10 +75,10 @@ class AIOS_Shell: def get_version(self) -> str: return "0.0.1" - async def send_msg(self,msg:str,target_id:str,sender:str = None) -> str: + 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 = "default" + agent_msg.topic = topic resp = await AIBus().get_default_bus().send_message(target_id,agent_msg) if resp is not None: return resp.body @@ -71,49 +87,119 @@ class AIOS_Shell: async def install_workflow(self,workflow_id:Workflow) -> None: pass - + + async def call_func(self,func_name, args): + match func_name: + case 'send': + target_id = args[0] + msg_content = args[1] + topic = args[2] + resp = await self.send_msg(msg_content,target_id,topic,self.username) + show_text = FormattedText([("class:title", f"{self.current_topic}@{self.current_target} >>> "), + ("class:content", resp)]) + return show_text + case 'open': + if len(args) >= 1: + target_id = args[0] + if len(args) >= 2: + topic = args[1] + + self.current_target = target_id + self.current_topic = topic + show_text = FormattedText([("class:title", f"current session switch to {topic}@{target_id}")]) + return show_text + case 'history': + num = 10 + offset = 0 + 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 + else: + db_path = WorkflowManager().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): + format_texts.append(("class:content",f"{msg.sender} >>> {msg.body}")) + format_texts.append(("",f"\n-------------------\n")) + return FormattedText(format_texts) + return FormattedText([("class:title", f"chatsession not found")]) + case 'exit': + os._exit(0) + case 'help': + return FormattedText([("class:title", f"help~~~")]) + ####################################################################################### -def proc_input_by_agent(): - pass - -def show_help(): - print("this is help") - history = FileHistory('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: + return None + + async def main(): print("aios shell prepareing...") - logging.basicConfig(level=logging.INFO,format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s') + 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.") - completer = WordCompleter(['list agent', 'list workflow', 'exit', 'help'], ignore_case=True) - #history = FileHistory('history.txt') + completer = WordCompleter(['send($target,$msg,$topic)', + 'open($target,$topic)', + 'history($num,$offset)', + 'show()', + 'exit()', + 'help()'], ignore_case=True) + while True: - user_input = await session.prompt_async('>>> ',completer=completer) - match user_input: - case "list agent": - print(AgentManager().list_agent()) - case "list workflow": - print(WorkflowManager().list_workflow()) - case "help": - show_help() - case "exit": - break - + user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$",completer=completer,style=shell_style) + if len(user_input) <= 1: + continue - if user_input.startswith("send"): - args = user_input.split(" ") - if len(args) < 3: - print("send msg failed, usage: send target_id msg_content") - continue - target_id = args[1] - msg_content = args[2] - resp = await shell.send_msg(msg_content,target_id,shell.username) - print(f"<<<{target_id} : {resp}") + func_call = parse_function_call(user_input) + show_text = None + if func_call: + show_text = await shell.call_func(func_call[0], func_call[1]) + else: + resp = await shell.send_msg(user_input,shell.current_target,shell.current_topic,shell.username) + show_text = FormattedText([ + ("class:title", f"{shell.current_topic}@{shell.current_target} >>> "), + ("class:content", resp) + ]) + + 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()) From fc0ce3b381f3884d1a1064a36a694f913de3faff Mon Sep 17 00:00:00 2001 From: Si Changjun Date: Thu, 24 Aug 2023 15:54:52 +0800 Subject: [PATCH 015/177] knowleadge.drawio --- doc/未命名绘图.drawio | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/未命名绘图.drawio diff --git a/doc/未命名绘图.drawio b/doc/未命名绘图.drawio new file mode 100644 index 0000000..f8db6cf --- /dev/null +++ b/doc/未命名绘图.drawio @@ -0,0 +1,10 @@ + + + + + + + + + + From aa05a3a003dd423ce4d6256f1d7cce494fdf4de7 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Tue, 29 Aug 2023 16:45:05 +0800 Subject: [PATCH 016/177] objected knowledge base design --- doc/mvp/knowleadge.drawio | 1478 +++++++++++++++++++++++++++++++++++++ doc/mvp/knowleage.md | 20 + doc/未命名绘图.drawio | 10 - 3 files changed, 1498 insertions(+), 10 deletions(-) create mode 100644 doc/mvp/knowleadge.drawio create mode 100644 doc/mvp/knowleage.md delete mode 100644 doc/未命名绘图.drawio diff --git a/doc/mvp/knowleadge.drawio b/doc/mvp/knowleadge.drawio new file mode 100644 index 0000000..bdb5534 --- /dev/null +++ b/doc/mvp/knowleadge.drawio @@ -0,0 +1,1478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/mvp/knowleage.md b/doc/mvp/knowleage.md new file mode 100644 index 0000000..7b48cac --- /dev/null +++ b/doc/mvp/knowleage.md @@ -0,0 +1,20 @@ +# Objected knowleadge base, a specialized implemention for emails +## Vectorized Knowledge +Large language models are trained on general corpora and without fine-tuning on user-specific data, they struggle to utilize user-related context effectively. + +Users accumulate a vast amount of content that reflects their personality during their regular internet usage. This includes personal photos, tweets, Facebook posts, emails, etc. While it's possible to include all this content in the prompt during each interaction with the large language model, this approach is costly and can easily reach the token limit. + +A common solution is to generate feature vectors from this content using word embedding techniques and store them in a vector database. During an interaction, the vector that is most relevant to the prompt is retrieved from the database, merged with the prompt, and then passed to the large language model. + +We refer to this vectorized content as "knowledge". + +## Objected knownleadge base +In a personal AI system, to build a user's own knowledge base, we first need to implement various spider programs to crawl and retrieve all user-related data. Modern web content is typically rich text, including text, images, videos, hyperlinks, etc. Organizing this rich text in a tree-like structure similar to HTML is necessary, hence the need to introduce an object structure to represent this content. + +Different parts of this content cannot be vectorized using the same embedding model. For instance, text and images, as well as the content of an image and its EXIF information, need separate embeddings. This means that in the vector database, the same content may have multiple vector values, and a row can represent a whole content item or just a part of it. + +We need a comprehensive object structure to represent the hierarchy and relationships of content, as well as to implement the indexing and storage of objects. In the Minimum Viable Product (MVP) version, we'll implement a specialized solution for email content. In future versions, we can generalize this to handle other types of content, such as Facebook posts, tweets, etc. + +## Agent with knowleadge base +At the same time, we also need to explore the paradigm of using the knowledge base in Agents and workflows, so that the agent can better complete tasks in interaction with users through the context provided by the knowledge base. + diff --git a/doc/未命名绘图.drawio b/doc/未命名绘图.drawio deleted file mode 100644 index f8db6cf..0000000 --- a/doc/未命名绘图.drawio +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - From 39eb96f9e330f5e7556a88f41624b525e6ecd29b Mon Sep 17 00:00:00 2001 From: tsukasa Date: Wed, 30 Aug 2023 17:16:37 +0800 Subject: [PATCH 017/177] objected knowledge base implement for email code structure --- doc/mvp/workflow.drawio | 6 +- doc/未命名绘图.drawio | 10 ---- src/aios_kernel/knowledge/__init__.py | 2 + src/aios_kernel/knowledge/knowledge_base.py | 10 ++++ src/aios_kernel/knowledge/object.py | 65 +++++++++++++++++++++ src/aios_kernel/knowledge/object_storage.py | 33 +++++++++++ src/aios_kernel/knowledge/vector_base.py | 14 +++++ src/service/email_spider/converter.py | 24 ++++++++ src/service/email_spider/main.py | 12 ++++ src/service/email_spider/spider.py | 17 ++++++ 10 files changed, 180 insertions(+), 13 deletions(-) delete mode 100644 doc/未命名绘图.drawio create mode 100644 src/aios_kernel/knowledge/__init__.py create mode 100644 src/aios_kernel/knowledge/knowledge_base.py create mode 100644 src/aios_kernel/knowledge/object.py create mode 100644 src/aios_kernel/knowledge/object_storage.py create mode 100644 src/aios_kernel/knowledge/vector_base.py create mode 100644 src/service/email_spider/converter.py create mode 100644 src/service/email_spider/main.py create mode 100644 src/service/email_spider/spider.py diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index 2b3e239..c6659c1 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -123,7 +123,7 @@ - + @@ -220,7 +220,7 @@ - + diff --git a/doc/未命名绘图.drawio b/doc/未命名绘图.drawio deleted file mode 100644 index f8db6cf..0000000 --- a/doc/未命名绘图.drawio +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/aios_kernel/knowledge/__init__.py b/src/aios_kernel/knowledge/__init__.py new file mode 100644 index 0000000..13bb147 --- /dev/null +++ b/src/aios_kernel/knowledge/__init__.py @@ -0,0 +1,2 @@ +from .object import EmailObject, ImageObject, TextChunkObject, ObjectID +from .knowledge_base import KnowledgeBase diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py new file mode 100644 index 0000000..59e4f25 --- /dev/null +++ b/src/aios_kernel/knowledge/knowledge_base.py @@ -0,0 +1,10 @@ +# define a knowledge base class +from src.aios_kernel.agent import AgentPrompt +from .object import KnowledgeObject + +class KnowledgeBase: + async def insert(self, object: KnowledgeObject): + pass + + async def query(self, prompt: AgentPrompt) -> AgentPrompt: + pass \ No newline at end of file diff --git a/src/aios_kernel/knowledge/object.py b/src/aios_kernel/knowledge/object.py new file mode 100644 index 0000000..320cd8f --- /dev/null +++ b/src/aios_kernel/knowledge/object.py @@ -0,0 +1,65 @@ + +# define a object type enum +from abc import ABC + + +class ObjectType(Enum): + TextChunk = 1 + Image = 2 + Email = 101 + + +# define a object ID class to identify a object +class ObjectID: # pylint: disable=too-few-public-methods + def __init__(self, object_type, digist): + self.object_type = object_type + self.digist = digist + + def __str__(self): + return f"{self.object_type.name}:{self.digist}" + + +# define a object class +class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods + def __init__(self, object_type): + self.object_type = object_type + + @abstractmethod + def get_id(self) -> ObjectID: + pass + + # define a to binary method to convert object to binary + @abstractmethod + def to_binary(self) -> bytes: + pass + + # define a from binary method to convert binary to object + @abstractmethod + def from_binary(self, binary: bytes): + pass + + +# define a text chunk class +class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, text): + super().__init__(ObjectType.TextChunk) + self.text = text + + +# define a image class +class ImageObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta, path): + super().__init__(ObjectType.Image) + self.meta = meta + self.path = path + + +# define a email class +class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta): + super().__init__(ObjectType.Email) + self.meta = meta + self.text = [] + self.images = [] + + diff --git a/src/aios_kernel/knowledge/object_storage.py b/src/aios_kernel/knowledge/object_storage.py new file mode 100644 index 0000000..1a6f85e --- /dev/null +++ b/src/aios_kernel/knowledge/object_storage.py @@ -0,0 +1,33 @@ +# import RDB LargeBinary +from sqlalchemy import Column, String, LargeBinary, create_engine, sessionmaker, pickle +from .object import KnowledgeObject + +# implement object storage with RDB +# define object storage table +class ObjectStorageTable(Base): + __tablename__ = 'object_storage' + id = Column(String, primary_key=True) + parent = Column(String, nullable=True) + object = Column(LargeBinary, nullable=False) + + def __init__(self, id, parent, object): # pylint: disable=redefined-builtin + self.id = id + self.parent = parent + self.object = object + +# define object storage class +class ObjectStorage: + async def __init__(self, db_url): + self.engine = create_engine(db_url) + self.session = sessionmaker(bind=self.engine)() # pylint: disable=not-callable + + async def get(self, id) -> [KnowledgeObject, KnowledgeObject]: + obj = self.session.query(ObjectStorageTable).filter(ObjectStorageTable.id == id).first() + if obj is None: + return None + return pickle.loads(obj.object) + + # define insert method + async def insert(self, object, parent): # pylint: disable=redefined-builtin + obj = ObjectStorageTable(id, parent, pickle.dumps(object)) + \ No newline at end of file diff --git a/src/aios_kernel/knowledge/vector_base.py b/src/aios_kernel/knowledge/vector_base.py new file mode 100644 index 0000000..b2cf442 --- /dev/null +++ b/src/aios_kernel/knowledge/vector_base.py @@ -0,0 +1,14 @@ +# import the ObjectID class +from .object import ObjectID + +# define a vector base class +class VectorBase: + def __init__(self, db_url, model_name) -> None: + self.db_url = db_url + self.model_name = model_name + + async def insert(self, vector: [float], id: ObjectID): + pass + + async def query(self, vector: [float], top_k: int) -> [ObjectID]: + pass \ No newline at end of file diff --git a/src/service/email_spider/converter.py b/src/service/email_spider/converter.py new file mode 100644 index 0000000..193b220 --- /dev/null +++ b/src/service/email_spider/converter.py @@ -0,0 +1,24 @@ +from aios_kernel.knowledge import KnowledgeBase, EmailObject + +# define a email converter class + +class EmailConverter: + # define init method + def __init__(self, local_dir, knowledge_base: KnowledgeBase) -> None: + pass + + async def run(self): + # convert the email to knowledge object + for email_dir in self._next(): + # convert the email to knowledge object + knowledge_object = self._convert(email_dir) + # insert the knowledge object to knowledge base + await self.knowledge_base.insert(knowledge_object) + + def _next(self) -> str: + pass + + def _convert(self, email_dir) -> EmailObject: + pass + + \ No newline at end of file diff --git a/src/service/email_spider/main.py b/src/service/email_spider/main.py new file mode 100644 index 0000000..cb75abd --- /dev/null +++ b/src/service/email_spider/main.py @@ -0,0 +1,12 @@ +import asyncio +from .spider import EmailSpider, EmailConverter + + +if __name__ == "__main__": + spider = EmailSpider("smtp.163.com","user","pwd","./email") + asyncio.run(spider.run()) + + converter = EmailConverter("./email",KnowledgeBase()) + asyncio.run(converter.run()) + + diff --git a/src/service/email_spider/spider.py b/src/service/email_spider/spider.py new file mode 100644 index 0000000..0b5a212 --- /dev/null +++ b/src/service/email_spider/spider.py @@ -0,0 +1,17 @@ +# define a email spider class + +class EmailSpider: + def __init__(self, address, account, pwd, local_dir) -> None: + pass + + async def run(self): + # spide the email from the email server + for email_link in self._next(): + # save the email to local directory + self._save(email_link) + + def _next(self): + pass + + def _save(self, email_link) -> str: + pass \ No newline at end of file From febe28eecc3dc9e4e7c339956dfb87597d47ef85 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 31 Aug 2023 15:45:02 +0800 Subject: [PATCH 018/177] Add embeding compute task --- doc/mvp/workflow.drawio | 2 +- src/aios_kernel/compute_task.py | 8 +++++ src/aios_kernel/open_ai_node.py | 54 +++++++++++++++++++-------------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index c6659c1..e011651 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -123,7 +123,7 @@ - + diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index f2253e0..ea1d7dc 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -38,6 +38,14 @@ class ComputeTask: self.params["model_name"] = "gpt-4-0613" self.params["max_token_size"] = max_token_size + def set_embeding_params(self, model_name, input, callchain_id = None): + self.task_type = "embeding" + self.create_time = time.time() + self.task_id = uuid.uuid4().hex + self.callchain_id = callchain_id + self.params["model_name"] = model_name + self.params["input"] = input + def display(self) -> str: return f"ComputeTask: {self.task_id} {self.task_type} {self.state}" diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 7f5c8e6..be0d184 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -47,31 +47,39 @@ class OpenAI_ComputeNode(ComputeNode): def _run_task(self,task:ComputeTask): task.state = ComputeTaskState.RUNNING - mode_name = task.params["model_name"] - # max_token_size = task.params["max_token_size"] - prompts = task.params["prompts"] + # 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"] - 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 + 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 == "embeding": + pass + def start(self): async def _run_task_loop(): while True: From a6f03ff62a2a7db1a01850ba5672e8492aca8a33 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Thu, 31 Aug 2023 16:14:13 +0800 Subject: [PATCH 019/177] Add a service: email spider #43 --- .gitignore | 2 + rootfs/email/config.toml | 7 ++ src/service/spider/email_spider.py | 109 +++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 rootfs/email/config.toml create mode 100644 src/service/spider/email_spider.py diff --git a/.gitignore b/.gitignore index 4ef429f..84349bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .vscode/ *.pyc +rootfs/email/config.local.toml +rootfs/data diff --git a/rootfs/email/config.toml b/rootfs/email/config.toml new file mode 100644 index 0000000..1bf4bca --- /dev/null +++ b/rootfs/email/config.toml @@ -0,0 +1,7 @@ + + +EMAIL_IMAP_SERVER = "imap.gmail.com" +EMAIL_ADDRESS = '<>' +EMAIL_PASSWORD = '<>' +EMAIL_IMAP_PORT = 993 +LOCAL_DIR = 'rootfs/data' \ No newline at end of file diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py new file mode 100644 index 0000000..ae6e484 --- /dev/null +++ b/src/service/spider/email_spider.py @@ -0,0 +1,109 @@ +import imaplib +import os +import toml +import logging +import mailparser +import hashlib +import json + +# logger config +logger = logging.getLogger('email spider') +logger.setLevel(logging.DEBUG) +ch = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s [%(name)s] [%(levelname)s] %(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) + +# read config from toml file +# and read from config config.local.toml if exists (config.local.toml is ignored by git) +config = toml.load('./rootfs/email/config.toml') +if os.path.exists('./rootfs/email/config.local.toml'): + config = toml.load('./rootfs/email/config.local.toml') + + +# create email client +def email_client() -> imaplib.IMAP4_SSL: + logger.info(f"read email config from {config.get('EMAIL_IMAP_SERVER')}") + client = imaplib.IMAP4_SSL(config.get('EMAIL_IMAP_SERVER')) + client.login(config.get('EMAIL_ADDRESS'), config.get('EMAIL_PASSWORD')) + return client + +def list_box(mail: imaplib.IMAP4_SSL): + _, mailbox_list = mail.list() + for mailbox in mailbox_list: + print(mailbox.decode()) + +def read_emails(client: imaplib.IMAP4_SSL, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"): + client.select(folder) + _, data = client.uid('search', None, imap_keyword) + # get email uid list + email_list = data[0].split() + logger.info(f"got {len(email_list)} emails") + for uid in email_list: + logger.info(f"read email uid {uid}") + if check_email_saved(client, uid): + logger.info(f"email uid {uid} already saved") + continue + else: + read_and_save_email(client, uid) + logger.info(f"email uid {uid} saved") + + +def read_and_save_email(client: imaplib.IMAP4_SSL, uid: str): + message_parts = "(BODY.PEEK[])" + _, email_data = client.uid('fetch', uid, message_parts) + mail = mailparser.parse_from_bytes(email_data[0][1]) + logger.info(f"got email subject [{mail.subject}]") + save_email(mail) + +def get_local_dir_name(mail: mailparser.MailParser) -> str: + dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" + name = f"{mail.subject}__{mail.date}" + name = hashlib.md5(name.encode('utf-8')).hexdigest() + return f"{dir}/{name}" + + +# check only need to check email header, not need to download email body +def check_email_saved(client: imaplib.IMAP4_SSL, uid: str): + message_parts = "(BODY[HEADER])" + _, email_data = client.uid('fetch', uid, message_parts) + mail = mailparser.parse_from_bytes(email_data[0][1]) + logger.info(f"check email subject [{mail.subject}]") + dir = get_local_dir_name(mail) + logger.info(f"check email saved {dir}") + file = f"{dir}/email.txt" + if os.path.exists(file): + return True + return False + + +# save email to local file by each folder +def save_email(mail: mailparser.MailParser): + # create email account dir + dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" + if not os.path.exists(dir): + os.makedirs(dir) + # create email local dir + email_dir = get_local_dir_name(mail) + logger.info(f"save email to {email_dir}") + if not os.path.exists(email_dir): + os.makedirs(email_dir) + + # save email content and meta info + 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) + logger.info(f"save email meta info {f.name}") + + + +if __name__ == "__main__": + mail = email_client() + folder = 'INBOX' + # imap_keyword = "UNSEEN" + imap_keyword = "ALL" + read_emails(mail, folder, imap_keyword) From 5bd9775e4f31e32258b262878f846ec9a2360caf Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 31 Aug 2023 16:32:20 +0800 Subject: [PATCH 020/177] Add openai text embedding compute task --- src/aios_kernel/compute_kernel.py | 30 +++++++++ src/aios_kernel/compute_node.py | 2 +- src/aios_kernel/compute_task.py | 9 ++- src/aios_kernel/knowledge/knowledge_base.py | 4 +- src/aios_kernel/open_ai_node.py | 70 +++++++++++++-------- 5 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 1d05afd..5829bbc 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -118,4 +118,34 @@ class ComputeKernel: return task_req.result.result_str return "error!" + + def text_embedding(self,input:str,model_name:Optional[str] = None): + task_req = ComputeTask() + task_req.set_text_embeding_params(input,model_name) + self.run(task_req) + return task_req + + async def do_text_embedding(self,input:str,model_name:Optional[str] = None) -> [float] + task_req = self.text_embedding(input,model_name) + async def check_timer(): + check_times = 0 + while True: + if task_req.state == ComputeTaskState.DONE: + break + + if task_req.state == ComputeTaskState.ERROR: + break + + 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 + + return "error!" \ No newline at end of file diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 6fe0059..ef8f933 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -28,7 +28,7 @@ class ComputeNode(ABC): pass @abstractmethod - def is_support(self,task_type:str) -> bool: + def is_support(self, task: ComputeTask) -> bool: pass @abstractmethod diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index ea1d7dc..35a1afd 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -38,12 +38,15 @@ class ComputeTask: self.params["model_name"] = "gpt-4-0613" self.params["max_token_size"] = max_token_size - def set_embeding_params(self, model_name, input, callchain_id = None): - self.task_type = "embeding" + def set_text_embeding_params(self, input, model_name=None, callchain_id = None): + self.task_type = "text_embedding" self.create_time = time.time() self.task_id = uuid.uuid4().hex self.callchain_id = callchain_id - self.params["model_name"] = model_name + if model_name is not None: + self.params["model_name"] = model_name + else: + self.params["model_name"] = "text-embedding-ada-002" self.params["input"] = input def display(self) -> str: diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py index 59e4f25..a576280 100644 --- a/src/aios_kernel/knowledge/knowledge_base.py +++ b/src/aios_kernel/knowledge/knowledge_base.py @@ -7,4 +7,6 @@ class KnowledgeBase: pass async def query(self, prompt: AgentPrompt) -> AgentPrompt: - pass \ No newline at end of file + pass + + \ No newline at end of file diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index be0d184..27177f0 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -53,32 +53,45 @@ class OpenAI_ComputeNode(ComputeNode): # max_token_size = task.params["max_token_size"] prompts = task.params["prompts"] - mode_name = task.params["model_name"] + mode_name = task.params["model_name"] # max_token_size = task.params["max_token_size"] - prompts = task.params["prompts"] + 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 == "embeding": - pass + 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"] + logger.info(f"call openai {model_name} input: {input}") + + resp = openai.Embeding.create(model=model_name, + input=input) + logger.info(f"openai response: {resp}") + + result = ComputeTaskResult() + result.set_from_task(task) + result.worker_id = self.node_id + result.result = resp["data"][0]["embedding"] + + return result def start(self): async def _run_task_loop(): @@ -104,8 +117,13 @@ class OpenAI_ComputeNode(ComputeNode): pass - def is_support(self,task_type:str) -> bool: - return True + def is_support(self, task: ComputeTask) -> bool: + if task.task_type == "llm_completion": + return True + if task.task_type == "text_embedding": + if task.params["model_name"] == "text-embedding-ada-002": + return True + return False def is_local(self) -> bool: From cff34c6aba53cde3f9aaa235e9ba6dde5d81b1f9 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 31 Aug 2023 20:13:41 +0800 Subject: [PATCH 021/177] knowledge base basic flow --- src/aios_kernel/compute_kernel.py | 2 +- src/aios_kernel/knowledge/knowledge_base.py | 46 +++++++++++++++++++-- src/aios_kernel/knowledge/object.py | 8 ++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 5829bbc..f626578 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -125,7 +125,7 @@ class ComputeKernel: self.run(task_req) return task_req - async def do_text_embedding(self,input:str,model_name:Optional[str] = None) -> [float] + async def do_text_embedding(self,input:str,model_name:Optional[str] = None) -> [float]: task_req = self.text_embedding(input,model_name) async def check_timer(): check_times = 0 diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py index a576280..bb06401 100644 --- a/src/aios_kernel/knowledge/knowledge_base.py +++ b/src/aios_kernel/knowledge/knowledge_base.py @@ -1,12 +1,50 @@ # define a knowledge base class -from src.aios_kernel.agent import AgentPrompt -from .object import KnowledgeObject +from ..agent import AgentPrompt +from ..compute_kernel import ComputeKernel +from .object import KnowledgeObject, ObjectType, EmailObject, TextChunkObject, ImageObject +from .object_storage import ObjectStorage +from .vector_base import VectorBase class KnowledgeBase: + def __init__(self) -> None: + self.object_store = ObjectStorage() + self.vector_base = VectorBase() + self.compute_kernel = ComputeKernel() + async def insert(self, object: KnowledgeObject): - pass + if object.object_type == ObjectType.Email: + email: EmailObject = object + for text_id in email.text: + [text, _] = self.object_store.get(text_id) + text: TextChunkObject = text + vector = await self.compute_kernel.do_text_embedding(text.text) + self.vector_base.insert(vector, text_id) + + for image_id in email.images: + [image, _] = self.object_store.get(image_id) + image: ImageObject = image + vector = await self.compute_kernel.do_text_embedding(image.meta) + self.vector_base.insert(vector, image_id) + + vector = await self.compute_kernel.do_text_embedding(email.meta) + self.vector_base.insert(vector, email.get_id()) + else: + pass async def query(self, prompt: AgentPrompt) -> AgentPrompt: - pass + for msg in prompt.messages: + if msg.role == "user": + vector = await self.compute_kernel.do_text_embedding(msg.content) + object_ids = self.vector_base.query(vector, 10) + for object_id in object_ids: + if object_id.object_type == ObjectType.Email: + + [object, email] = self.object_store.get(object_id) + if object.object_type == ObjectType.Email: + email: EmailObject = object + prompt.append(AgentPrompt()) + prompt + + \ No newline at end of file diff --git a/src/aios_kernel/knowledge/object.py b/src/aios_kernel/knowledge/object.py index 320cd8f..815d4e2 100644 --- a/src/aios_kernel/knowledge/object.py +++ b/src/aios_kernel/knowledge/object.py @@ -21,7 +21,7 @@ class ObjectID: # pylint: disable=too-few-public-methods # define a object class class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods - def __init__(self, object_type): + def __init__(self, object_type: ObjectType): self.object_type = object_type @abstractmethod @@ -41,7 +41,7 @@ class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods # define a text chunk class class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, text): + def __init__(self, text: str): super().__init__(ObjectType.TextChunk) self.text = text @@ -59,7 +59,7 @@ class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods def __init__(self, meta): super().__init__(ObjectType.Email) self.meta = meta - self.text = [] - self.images = [] + self.text = [ObjectID] + self.images = [ObjectID] From b4990e4c57c1b7bac160ce370882467fc8585771 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 1 Sep 2023 00:39:36 -0700 Subject: [PATCH 022/177] Workflow is almost done. --- doc/mvp/process model.drawio | 80 ++--- doc/mvp/workflow.drawio | 4 +- rootfs/workflows/plan_maker/workflow.toml | 8 +- src/aios_kernel/agent_message.py | 20 ++ src/aios_kernel/bus.py | 12 +- src/aios_kernel/chatsession.py | 4 +- src/aios_kernel/role.py | 10 +- src/aios_kernel/workflow.py | 309 ++++++++++++++---- .../workflow_manager/workflow_manager.py | 29 +- src/service/aios_shell/aios_shell.py | 76 +++-- test/pkg.cfg.toml | 92 +++++- 11 files changed, 469 insertions(+), 175 deletions(-) diff --git a/doc/mvp/process model.drawio b/doc/mvp/process model.drawio index 86d56e7..d78b1db 100644 --- a/doc/mvp/process model.drawio +++ b/doc/mvp/process model.drawio @@ -1,132 +1,132 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index 2b3e239..54e724c 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -220,7 +220,7 @@ - + diff --git a/rootfs/workflows/plan_maker/workflow.toml b/rootfs/workflows/plan_maker/workflow.toml index e5b150f..1dc80bf 100644 --- a/rootfs/workflows/plan_maker/workflow.toml +++ b/rootfs/workflows/plan_maker/workflow.toml @@ -46,7 +46,7 @@ content="""你是一个活动策划公司的经理,与客户对接并向团队 1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。 3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。 -4. 当你决定要和工作小组通信时,请使用`sendmsg({小组名称},{内容}`)的形式。""" +4. 当你决定要和工作小组通信时,请使用 sendmsg(小组名称,内容) 的形式。""" @@ -68,7 +68,7 @@ agent="email_reader" role="system" content="""你是一家活动策划公司的嘉宾对接组的组长,你的工作是基于已知信息,当前活动信息、公司经理的指令与嘉宾沟通,收集嘉宾的信息,然后将信息反馈给经理。在你看来,参加活动的多少有成员都是嘉宾,你可以通过你知道的信息给不同的成员进行分级。你的基本工作模式是: 1. 处理收到的邮件,如果邮件来自嘉宾,你会尝试从邮件的表态和内容中分享嘉宾的需要,并结合你对当前活动方案的理解判断是否需要和经理沟通,如果需要和经理沟通,你会将嘉宾的需求总结和告诉经理。不需要沟通的事项可以直接回复嘉宾。 -2. 你总是通过`call_function(get_env,'parent.topic'`的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。 -3. 当你决定要和经理通信时,请使用`send_msg(manager,{内容}`)的形式,内容的长度不超过200字。 -4. 当你决定要回复嘉宾时,请使用`call_function(sendmail,{嘉宾邮件地址},{标题},{内容})的形式,内容的长度不超过500字。""" +2. 你总是通过 call_function(get_env,'parent.topic')的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。 +3. 当你决定要和经理通信时,请使用 sendmsg(manager,内容)的形式,内容的长度不超过200字。 +4. 当你决定要回复嘉宾时,请使用 call(sendmail,嘉宾邮件地址,邮件标题,内容) 的形式,内容的长度不超过500字。""" # 这里是孤立工作模式,组长只和经理沟通,也可以赋予其和其它组沟通的能力 diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 3aa05c1..242cc66 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -1,6 +1,7 @@ from enum import Enum import uuid import time +import re class AgentMsgState(Enum): RESPONSED = 0 @@ -37,3 +38,22 @@ class AgentMsg: def get_target(self) -> str: return self.target + + @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 + diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py index 6c92a8a..ae1853e 100644 --- a/src/aios_kernel/bus.py +++ b/src/aios_kernel/bus.py @@ -34,6 +34,7 @@ class AIBus: self.unhandle_handler = None async def post_message(self,target_id,msg:AgentMsg,use_unhandle=True) -> bool: + target_id = target_id.split(".")[0] handler = self.handlers.get(target_id) if handler: handler.queue.put_nowait(msg) @@ -62,6 +63,7 @@ class AIBus: return handler.results.get(msg_id) async def send_message(self,target_id:str,msg:AgentMsg) -> AgentMsg: + target_id = target_id.split(".")[0] post_result = await self.post_message(target_id,msg) if post_result is False: return None @@ -126,4 +128,12 @@ class AIBus: handler.working_task = asyncio.create_task(self.process_queue(handler)) - \ No newline at end of file + + +#send message to target logic: +# find target handler: +# process_message(msg): +# session = get_session(msg.sender,msg.target) + + +# history: open(sender,target,topic) \ No newline at end of file diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py index 90890ed..ecd90a3 100644 --- a/src/aios_kernel/chatsession.py +++ b/src/aios_kernel/chatsession.py @@ -237,13 +237,13 @@ class AIChatSession: 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" + #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" + #assert msg.target == self.owner_id,"recv message means msg.target == self.owner_id" self.append(msg) #def attach_event_handler(self,handler) -> None: diff --git a/src/aios_kernel/role.py b/src/aios_kernel/role.py index c4d97ab..1d63e61 100644 --- a/src/aios_kernel/role.py +++ b/src/aios_kernel/role.py @@ -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 diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 88a13ed..9c90056 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -33,9 +33,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 @@ -62,6 +73,11 @@ 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") @@ -74,13 +90,14 @@ class Workflow: 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 @@ -98,15 +115,52 @@ class Workflow: 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]) + if the_role is not None: + return await current_workflow.role_process_msg(msg,the_role) + 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 + async def _process_msg(self,msg:AgentMsg): + # workflow can be a message handler, but never be a message sender + # all message forword to roles or sub workflow + # workflow no chatsession record, but role have + final_result = None - chatsession = None + + targets = self._parse_msg_target(msg.target) + if len(targets) > 1: + return await self._forword_msg(targets,msg) + if self.input_filter is not None: select_role_id = self.input_filter.select(msg) if select_role_id is not None: @@ -115,35 +169,206 @@ class Workflow: 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) + 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: + # no input filter, we would process all message, slowly,not suggest to use results = {} + final_result:AgentMsg = None 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) + 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) + final_result = a_result 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[1] + msg_content = func_args[2] + 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): + 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.id} to inner role: {msg.target}") + asyncio.create_task(self.role_process_msg(msg,target_role)) + 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.id} to sub workflow: {msg.target}") + asyncio.create_task(target_workflow._process_msg(msg)) + + logger.info(f"{msg.sender} post message {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): + 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.id} to inner role: {msg.target}") + return await self.role_process_msg(msg,target_role) + + 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.id} to sub workflow: {msg.target}") + return await target_workflow._process_msg(msg) + + logger.info(f"{msg.sender} post message {msg.id} to AIBus: {msg.target}") + return await self.get_bus().send_message(msg.target,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]}") + return """{result:"timeout"}""" + + 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 + + + async def role_process_msg(self,msg:AgentMsg,the_role:AIRole): + session_topic = f"{msg.sender}#{msg.topic}" + session_owner = the_role.get_role_id() + chatsession : AIChatSession = AIChatSession.get_session(session_owner,session_topic,self.db_file) + if chatsession is None: + logger.error(f"get session {session_topic}@{session_owner} failed!") + return None + + 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)) + + msg_prompt = AgentPrompt() + msg_prompt.messages = [{"role":"user","content":msg.body}] + prompt.append(msg_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()) + result = Workflow.prase_llm_result(result_str) + + + for postmsg in result.post_msgs: + postmsg.topic = msg.topic + await self.role_post_msg(the_role,postmsg) + + for post_call in result.post_calls: + await self.role_post_call(post_call,the_role) + + + result_prompt_str = "" + match result.state: + case "ignore": + return None + case "reponsed": + resp_msg = AgentMsg() + resp_msg.topic = msg.topic + resp_msg.set(session_owner,msg.sender,result.resp) + + chatsession.append_recv(msg) + chatsession.append_post(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 + send_resp = await self.role_send_msg(sendmsg,the_role) + if send_resp is not None: + result_prompt_str += f"\n{target} response is :{send_resp.body}" + + for call in result.calls: + 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}" + + result_prompt = AgentPrompt() + result_prompt.messages = [{"role":"user","content":result_prompt_str}] + prompt.append(result_prompt) + r = await _do_process_msg() + return r + + return await _do_process_msg() + + + #obsolete 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}" @@ -162,7 +387,6 @@ class Workflow: # 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}] @@ -180,7 +404,7 @@ class Workflow: 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) + 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 @@ -218,31 +442,18 @@ class Workflow: 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 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 @@ -250,48 +461,10 @@ 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) -> 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""" diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index 28479dd..adb844a 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -25,6 +25,20 @@ class WorkflowManager: 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: @@ -39,14 +53,9 @@ class WorkflowManager: if the_workflow is None: logger.warn(f"load workflow {workflow_id} from media failed!") - 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 +73,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)}") diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index cded7d0..3789957 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -41,16 +41,15 @@ class AIOS_Shell: self.current_topic = "default" 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(target_id) if agent is not None: - bus.register_message_handler(msg.target,agent._process_msg) + bus.register_message_handler(target_id,agent._process_msg) return True - a_workflow = await WorkflowManager().get_workflow(msg.target) + a_workflow = await WorkflowManager().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 @@ -79,7 +78,7 @@ class AIOS_Shell: 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(target_id,agent_msg) if resp is not None: return resp.body else: @@ -111,10 +110,11 @@ class AIOS_Shell: 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): @@ -140,37 +140,43 @@ class AIOS_Shell: history = FileHistory('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 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') + logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8', + level=logging.INFO, + format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s') shell = AIOS_Shell("user") await shell.initial() + + s = """了解。首先,我们需要进行方案讨论,定出一致的活动策略。我将任务分解如下: +财务组: 分析可见的预算,提供一份合理并被执行的财务方案。 +行程预订组: 硅谷自然迷人的地方众多,寻找适合11人秋游的地点,以及一个可行的周末日期。策划一个一天或两天的安排,包括选择有南瓜园的地方,采摘苹果,参观当地的酗酒作坊等秋游活动。 +嘉宾对接组: 把个人的食品饮料或过敏食物的需求事先了解并计入行程内。 +酒店预订组:根据行程预订组的日期安排,活动时间在1天还是2天,在这个之内找一个合适的住宿,试着保持让住宿在预算边界以内。 +活动摄像组: 准备活动拍摄与剪辑方案。\n\n然后,我将把这些拆分后的任务发来小组。 +sendmsg(财务组,分析预算并提供一份财务方案.) +sendmsg(行程预订组,找出适合秋游的地方和日期.) +sendmsg(嘉宾对接组,了解个人的饮食需求.) +sendmsg(酒店预订组,查询并预订住宿.) +sendmsg(活动摄像组,提供活动拍摄方案.) +经过一两天的准备,一切就绪之后,我将向工作人员发送最后的行程计划, + """ + r = Workflow.prase_llm_result(s) print(f"aios shell {shell.get_version()} ready.") completer = WordCompleter(['send($target,$msg,$topic)', diff --git a/test/pkg.cfg.toml b/test/pkg.cfg.toml index 46acf0a..68c46d5 100644 --- a/test/pkg.cfg.toml +++ b/test/pkg.cfg.toml @@ -1,15 +1,83 @@ +# workflow实例场景 +# 能展示sub workflow +# 能展示env的整合 +# 能展示filter的使用 +# 能展示function的调用 +# 能展示基于工作目标/KPI的sub workflow迭代流程 +# 多人场景安排 -[authors] -"buckyos.org" = {public="aaaa",name="BuckyOS Core Dev Team"} -[authors."cyfs.com"] -public="bbbb" -name="CYFS Core Dev Team" +# 例子:举办一个团队活动 +# 方案讨论(通过交互引导的方式收集主人的需求)与确定 -[env] -is_strict = false -prefixs = ["default","../m/s","abc"] -obj2 = {k1="bb",k2=300} -[env.obj] -k1="aa" -k2=200 +# 活动前: +# 通讯员,对接管理参加活动的人的情况 (email spider) +# 酒店预订 简单:搜索(酒店评价) 处理异常 +# 行程(票务)预订 :搜索,处理异常 +# 餐饮预订:给出方案,确定细节,预订 + +# 活动中: +# 进行统计和分析,调整设备 +# 安保,空调,音乐,拍照,录像 +# 响应紧急情况 +# 活动结束后: +# 整理照片,视频,进行必要的二次创作,发送给相关人员 +# 对活动进行总结,提出改件意见(指导下一次活动) + +# 1. 人员 +# 主管,负责和客户沟通,并对每个环境的结果进行总结 +# 嘉宾对接 +# 酒店组 +# 行程组 +# 财务组 +# 多媒体组 + + +[filter] +"*" = "manager" + +[roles.manager] +fullname = "经理" +agent="manager" +[[roles.manager.prompt]] +role="system" +content="""你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是:\ +1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 \ +2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。\ +3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。\ +4. 当你决定要和工作小组通信时,请使用`send_message({小组名称},{内容}`)的形式。"""}] + + + + +[sub_workflows] +[sub_workflows."嘉宾对接组"] +# 展现读取email和发送email与嘉宾沟通的能力 +[sub_workflows."嘉宾对接组".environments.email] +new_mail = "收到来自{event.data.from},标题为{event.data.subject}的邮件,内容为{event.data.content}的电子邮件" # 这里将new_mail事件转换为了一个来自环境的message + +[sub_workflows."嘉宾对接组".roles.leader] +name = "嘉宾对接组组长" +[[sub_workflows."嘉宾对接组".roles.leader.prompt]] +role="system" +content="""你是一家活动策划公司的嘉宾对接组的组长,你的工作是基于已知信息,当前活动信息、公司经理的指令与嘉宾沟通,收集嘉宾的信息,然后将信息反馈给经理。在你看来,参加活动的多少有成员都是嘉宾,你可以通过你知道的信息给不同的成员进行分级。你的基本工作模式是:\ +1. 处理收到的邮件,如果邮件来自嘉宾,你会尝试从邮件的表态和内容中分享嘉宾的需要,并结合你对当前活动方案的理解判断是否需要和经理沟通,如果需要和经理沟通,你会将嘉宾的需求总结和告诉经理。不需要沟通的事项可以直接回复嘉宾。\ +2. 你总是通过`call_function(get_env,'parent.topic'`的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。\ +3. 当你决定要和经理通信时,请使用`send_message(manager,{内容}`)的形式,内容的长度不超过200字。\ +4. 当你决定要回复嘉宾时,请使用`call_function(sendmail,{嘉宾邮件地址},{标题},{内容})的形式,内容的长度不超过500字。""" +# 这里是孤立工作模式,组长只和经理沟通,也可以赋予其和其它组沟通的能力 + +[sub_workflows."酒店预定组"] +# 展现使用搜索引擎,并调用预订酒店的能力 +[sub_workflows."酒店预定组".environments.email] + +[sub_workflows."酒店预定组".roles.leader] +name="酒店预定组组长" +prompt = [{role="xxx",content="yyy"}] + +[sub_workflows."酒店预定组".roles.research] +name="酒店搜索专家" + +[sub_workflows."行程预订组"] +# 展现处理冲突并反推 +nam3="3" From 25bba0742a2d16b930cf8ecafd1609de5fd61233 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 1 Sep 2023 12:05:03 -0700 Subject: [PATCH 023/177] 1. Build a more simple workflow:math_school 2. Fix some bug --- rootfs/agents/math_teacher/agent.toml | 5 + rootfs/workflows/math_school/workflow.toml | 50 ++++++++ src/aios_kernel/workflow.py | 109 ++---------------- .../workflow_manager/workflow_manager.py | 1 + src/service/aios_shell/aios_shell.py | 22 +--- 5 files changed, 74 insertions(+), 113 deletions(-) create mode 100644 rootfs/agents/math_teacher/agent.toml create mode 100644 rootfs/workflows/math_school/workflow.toml diff --git a/rootfs/agents/math_teacher/agent.toml b/rootfs/agents/math_teacher/agent.toml new file mode 100644 index 0000000..e24792c --- /dev/null +++ b/rootfs/agents/math_teacher/agent.toml @@ -0,0 +1,5 @@ +instance_id = "math_teacher" +fullname = "the one" +[[prompt]] +role = "system" +content = "你是精通数学的老师" diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml new file mode 100644 index 0000000..9f5af27 --- /dev/null +++ b/rootfs/workflows/math_school/workflow.toml @@ -0,0 +1,50 @@ +name = "math_school" + +[filter] +"*" = "小学老师" + +[roles."小学老师"] +name = "小学老师" +fullname = "Ada Zhang" +agent="math_teacher" +[[roles."小学老师".prompt]] +role="system" +content="""你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 +当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 +当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 +你会根据教导处主任的指示,定期调整教学方法""" + + +[roles."初中老师"] +name = "初中老师" +fullname = "Mark Wang" +agent="math_teacher" +[[roles."初中老师".prompt]] +role="system" +content="""你在学校任职,担任初中老师。 +当你发现学生的水平不是初中生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 +当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 +你会根据教导处主任的指示,定期调整教学方法""" + +[roles."高中老师"] +name = "高中老师" +fullname = "Hong Sun" +agent="math_teacher" + +[[roles."高中老师".prompt]] +role="system" +content="""你在学校任职,担任高中老师。 +当你发现学生的水平不是高中生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 +当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 +你会根据教导处主任的指示,定期调整教学方法""" + +[roles."教导处主任"] +name = "教导处主任" +fullname = "Green King" +agent="math_teacher" + +[[roles."教导处主任".prompt]] +role="system" +content="""你在学校任职,担任教导处主任。 +你收到老师发来的信息时,如果是类似 学生名_作业分数 的结果,会在合适的情况下根据学生作业的整体情况,对老师的教学方法进行必要的调整。""" + diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 9c90056..f7c13e8 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -1,6 +1,7 @@ import logging import asyncio +import json from asyncio import Queue from typing import Optional,Tuple from abc import ABC, abstractmethod @@ -79,13 +80,12 @@ class Workflow: 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 @@ -225,8 +225,8 @@ class Workflow: logger.error(f"parse postmsg failed! {func_call}") continue new_msg = AgentMsg() - target_id = func_args[1] - msg_content = func_args[2] + target_id = func_args[0] + msg_content = func_args[1] new_msg.set("_",target_id,msg_content) r.post_msgs.append(new_msg) continue @@ -307,9 +307,8 @@ class Workflow: 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)) @@ -323,16 +322,14 @@ class Workflow: #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()) result = Workflow.prase_llm_result(result_str) - - + logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}") for postmsg in result.post_msgs: postmsg.topic = msg.topic - await self.role_post_msg(the_role,postmsg) + await self.role_post_msg(postmsg,the_role) for post_call in result.post_calls: await self.role_post_call(post_call,the_role) - result_prompt_str = "" match result.state: case "ignore": @@ -367,82 +364,6 @@ class Workflow: return await _do_process_msg() - - #obsolete - 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)) - - 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 _get_prompt_from_session(self,chatsession:AIChatSession) -> AgentPrompt: messages = chatsession.read_history() # read last 10 message result_prompt = AgentPrompt() @@ -465,12 +386,6 @@ class Workflow: def get_workflow_rule_prompt(self) -> AgentPrompt: return self.rule_prompt - - 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 diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index adb844a..5603745 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -52,6 +52,7 @@ 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 if await self._load_workflow_agents(the_workflow) is False: return None diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 3789957..5236e7e 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -107,6 +107,10 @@ 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] + return self.username + " login success!" case 'history': num = 10 offset = 0 @@ -157,31 +161,17 @@ def parse_function_call(func_string): async def main(): print("aios shell prepareing...") - logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8', + 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') shell = AIOS_Shell("user") await shell.initial() - - s = """了解。首先,我们需要进行方案讨论,定出一致的活动策略。我将任务分解如下: -财务组: 分析可见的预算,提供一份合理并被执行的财务方案。 -行程预订组: 硅谷自然迷人的地方众多,寻找适合11人秋游的地点,以及一个可行的周末日期。策划一个一天或两天的安排,包括选择有南瓜园的地方,采摘苹果,参观当地的酗酒作坊等秋游活动。 -嘉宾对接组: 把个人的食品饮料或过敏食物的需求事先了解并计入行程内。 -酒店预订组:根据行程预订组的日期安排,活动时间在1天还是2天,在这个之内找一个合适的住宿,试着保持让住宿在预算边界以内。 -活动摄像组: 准备活动拍摄与剪辑方案。\n\n然后,我将把这些拆分后的任务发来小组。 -sendmsg(财务组,分析预算并提供一份财务方案.) -sendmsg(行程预订组,找出适合秋游的地方和日期.) -sendmsg(嘉宾对接组,了解个人的饮食需求.) -sendmsg(酒店预订组,查询并预订住宿.) -sendmsg(活动摄像组,提供活动拍摄方案.) -经过一两天的准备,一切就绪之后,我将向工作人员发送最后的行程计划, - """ - r = Workflow.prase_llm_result(s) print(f"aios shell {shell.get_version()} ready.") completer = WordCompleter(['send($target,$msg,$topic)', 'open($target,$topic)', 'history($num,$offset)', + 'login($username)' 'show()', 'exit()', 'help()'], ignore_case=True) From 1bbdf2e7225662666a8efd159c4066bd9cb19acc Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 4 Sep 2023 22:36:59 -0700 Subject: [PATCH 024/177] Add basic enviroment support. Can use env values in prompts right now. --- rootfs/workflows/math_school/workflow.toml | 17 ++- src/aios_kernel/__init__.py | 3 +- src/aios_kernel/environment.py | 100 ++++++++++++++- src/aios_kernel/workflow.py | 69 ++++++++--- src/aios_kernel/workflow_env.py | 136 +++++++++++++++++++++ src/aios_kernel/worksapce_env.py | 0 src/service/aios_shell/aios_shell.py | 6 +- test/env_test.py | 32 +++++ 8 files changed, 340 insertions(+), 23 deletions(-) create mode 100644 src/aios_kernel/workflow_env.py create mode 100644 src/aios_kernel/worksapce_env.py create mode 100644 test/env_test.py diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml index 9f5af27..48c9831 100644 --- a/rootfs/workflows/math_school/workflow.toml +++ b/rootfs/workflows/math_school/workflow.toml @@ -1,5 +1,15 @@ name = "math_school" +[enviroment] +GOAL="成为最好的学校" + + +[[connected_env]] +env_id = "calender" +[[connected_env.event2msg]] +timer = "现在是{data}" +role = "教导处主任" + [filter] "*" = "小学老师" @@ -9,7 +19,7 @@ fullname = "Ada Zhang" agent="math_teacher" [[roles."小学老师".prompt]] role="system" -content="""你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 +content="""现在时间是:{now}你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 你会根据教导处主任的指示,定期调整教学方法""" @@ -45,6 +55,7 @@ agent="math_teacher" [[roles."教导处主任".prompt]] role="system" -content="""你在学校任职,担任教导处主任。 -你收到老师发来的信息时,如果是类似 学生名_作业分数 的结果,会在合适的情况下根据学生作业的整体情况,对老师的教学方法进行必要的调整。""" +content="""你在学校任职,担任教导处主任。你的目标是{GOAL} +你收到老师发来的信息时,如果是类似 学生名_作业分数 的结果,会在合适的情况下根据学生作业的整体情况,对老师的教学方法进行必要的调整。 +当收到非老师发来的时间信息时,回复那一天学生的平均分。""" diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bd1434b..bef117f 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -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 \ No newline at end of file +from .bus import AIBus +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 64c6481..9979e2c 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -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 diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index f7c13e8..69f39c3 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -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!") diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py new file mode 100644 index 0000000..9ef00cb --- /dev/null +++ b/src/aios_kernel/workflow_env.py @@ -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}") + diff --git a/src/aios_kernel/worksapce_env.py b/src/aios_kernel/worksapce_env.py new file mode 100644 index 0000000..e69de29 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 5236e7e..053c5ec 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -31,7 +31,7 @@ from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,Op sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager - +from aios_kernel import CalenderEnvironment,Environment class AIOS_Shell: @@ -62,6 +62,10 @@ class AIOS_Shell: return False async def initial(self) -> bool: + cal_env = CalenderEnvironment("calender") + cal_env.start() + Environment.set_env_by_id("calender",cal_env) + AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) open_ai_node = OpenAI_ComputeNode() diff --git a/test/env_test.py b/test/env_test.py new file mode 100644 index 0000000..694acbb --- /dev/null +++ b/test/env_test.py @@ -0,0 +1,32 @@ +import asyncio +import os +import sys + +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../src') + +from aios_kernel import CalenderEnvironment,WorkflowEnvironment + + +async def test_buildin_envs(): + c_env = CalenderEnvironment("calender") + c_env.start() + print(c_env.get_value("now")) + async def show_event(eventid,event): + print(event.data) + c_env.attach_event_handler("timer",show_event) + + w_env = WorkflowEnvironment("workflow",os.path.abspath(directory + "/../rootfs/workflow_env.db")) + w_env.set_value("test","test_aaaa") + print(w_env.get_value("test")) + + await asyncio.sleep(10) + + +if __name__ == "__main__": + #test_rstr = "abc is {abc}" + #values = {"abc":"123"} + #new_str = test_rstr.format_map(values) + #print(new_str) + + asyncio.run(test_buildin_envs()) \ No newline at end of file From 3eac598c972c7a8c3a1ecc01cae76202118f898d Mon Sep 17 00:00:00 2001 From: Song Date: Thu, 7 Sep 2023 12:50:13 +0800 Subject: [PATCH 025/177] Add stability node (Text2Img) --- src/aios_kernel/compute_node.py | 25 +++--- src/aios_kernel/compute_task.py | 42 +++++---- src/aios_kernel/open_ai_node.py | 55 +++++------- src/aios_kernel/stability_node.py | 140 ++++++++++++++++++++++++++++++ src/requirements.txt | 8 ++ 5 files changed, 209 insertions(+), 61 deletions(-) create mode 100644 src/aios_kernel/stability_node.py create mode 100644 src/requirements.txt diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 6fe0059..7e159f7 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -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 @@ -28,7 +28,7 @@ class ComputeNode(ABC): pass @abstractmethod - def is_support(self,task_type:str) -> bool: + def is_support(self, task_type: ComputeTaskType) -> bool: pass @abstractmethod @@ -37,17 +37,14 @@ 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 - - diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index f2253e0..7895283 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -3,6 +3,7 @@ from enum import Enum import uuid import time + class ComputeTaskState(Enum): DONE = 0 INIT = 1 @@ -11,22 +12,31 @@ class ComputeTaskState(Enum): 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.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): + def set_llm_params(self, prompts, model_name, max_token_size, callchain_id=None): self.task_type = "llm_completion" self.create_time = time.time() self.task_id = uuid.uuid4().hex @@ -34,7 +44,7 @@ class ComputeTask: 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 @@ -45,16 +55,16 @@ 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 - self.result: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 diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 7f5c8e6..06e0f09 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -5,47 +5,49 @@ 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 logger = logging.getLogger(__name__) + class OpenAI_ComputeNode(ComputeNode): _instance = None + def __new__(cls): if cls._instance is None: cls._instance = super(OpenAI_ComputeNode, cls).__new__(cls) cls._instance.is_start = False return cls._instance - + 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 + # openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud self.openai_api_key = "" self.node_id = "openai_node" self.task_queue = Queue() - if os.getenv("OPENAI_API_KEY") is not None: + if os.getenv("OPENAI_API_KEY") is not None: openai.api_key = os.getenv("OPENAI_API_KEY") else: openai.api_key = self.openai_api_key - + self.start() - - async def push_task(self,task:ComputeTask,proiority:int = 0): + + 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 mode_name = task.params["model_name"] # max_token_size = task.params["max_token_size"] @@ -57,19 +59,19 @@ class OpenAI_ComputeNode(ComputeNode): 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}." + task.error_str = f"The status code was {status_code}." return None - - result = ComputeTaskResult() + + 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 def start(self): @@ -82,29 +84,20 @@ class OpenAI_ComputeNode(ComputeNode): 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_type:str) -> bool: - return True - + def is_support(self, task_type: ComputeTaskType) -> bool: + return task_type == ComputeTaskType.LLM_COMPLETION def is_local(self) -> bool: return False - - - - - - \ No newline at end of file diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py new file mode 100644 index 0000000..a5ed4e0 --- /dev/null +++ b/src/aios_kernel/stability_node.py @@ -0,0 +1,140 @@ +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 + +logger = logging.getLogger(__name__) + + +class Stability_ComputeNode(ComputeNode): + _instanace = None + + def __new__(cls): + if cls._instanace is None: + cls._instanace = super(Stability_ComputeNode, cls).__new__(cls) + cls._instanace.is_start = False + return cls._instanace + + def __init__(self) -> None: + super().__init__() + if self.is_start is True: + logger.warn("Stability_ComputeNode is already start") + return + + self.is_start = True + self.node_id = "stability_node" + self.api_key = "" # "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" + self.engine = "" # stable-diffusion-512-v2-0 + + self.task_queue = Queue() + + if os.getenv("STABILITY_API_KEY") is not None: + self.api_key = os.getenv("STABILITY_API_KEY") + else: + self.api_key = "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" + + # 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") + else: + self.engine = "stable-diffusion-512-v2-1" + + 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("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): + 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_type: ComputeTaskType) -> bool: + return task_type == ComputeTaskType.TEXT_2_IMAGE + + def is_local(self) -> bool: + return False diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..605dc72 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,8 @@ +aiofiles==23.2.1 +aiohttp==3.8.5 +openai==0.28.0 +Pillow==10.0.0 +Pillow==10.0.0 +prompt_toolkit==3.0.39 +stability_sdk==0.8.4 +toml==0.10.2 From 188595beb623c81350ecd07de9fbfd01603b005a Mon Sep 17 00:00:00 2001 From: liyaxing Date: Tue, 5 Sep 2023 20:42:22 +0800 Subject: [PATCH 026/177] Add chromadb based vector store impl and test case --- .../{knowledge => }/knowledge_base.py | 20 ++++---- src/{aios_kernel => }/knowledge/__init__.py | 2 +- src/{aios_kernel => }/knowledge/object.py | 4 +- .../knowledge/object_storage.py | 0 src/knowledge/vector/__init__.py | 2 + src/knowledge/vector/chroma_store.py | 49 +++++++++++++++++++ .../vector}/vector_base.py | 5 +- test/test_vector_storage.py | 29 +++++++++++ 8 files changed, 96 insertions(+), 15 deletions(-) rename src/aios_kernel/{knowledge => }/knowledge_base.py (74%) rename src/{aios_kernel => }/knowledge/__init__.py (63%) rename src/{aios_kernel => }/knowledge/object.py (96%) rename src/{aios_kernel => }/knowledge/object_storage.py (100%) create mode 100644 src/knowledge/vector/__init__.py create mode 100644 src/knowledge/vector/chroma_store.py rename src/{aios_kernel/knowledge => knowledge/vector}/vector_base.py (76%) create mode 100644 test/test_vector_storage.py diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge_base.py similarity index 74% rename from src/aios_kernel/knowledge/knowledge_base.py rename to src/aios_kernel/knowledge_base.py index bb06401..a882ba7 100644 --- a/src/aios_kernel/knowledge/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,9 +1,8 @@ # define a knowledge base class -from ..agent import AgentPrompt -from ..compute_kernel import ComputeKernel -from .object import KnowledgeObject, ObjectType, EmailObject, TextChunkObject, ImageObject -from .object_storage import ObjectStorage -from .vector_base import VectorBase +from . import AgentPrompt, ComputeKernel +from ..knowledge.object import KnowledgeObject, ObjectType, EmailObject, TextChunkObject, ImageObject +from ..knowledge.object_storage import ObjectStorage +from ..knowledge.vector.vector_base import VectorBase class KnowledgeBase: def __init__(self) -> None: @@ -38,12 +37,11 @@ class KnowledgeBase: object_ids = self.vector_base.query(vector, 10) for object_id in object_ids: if object_id.object_type == ObjectType.Email: - - [object, email] = self.object_store.get(object_id) - if object.object_type == ObjectType.Email: - email: EmailObject = object - prompt.append(AgentPrompt()) - prompt + [object, email] = self.object_store.get(object_id) + if object.object_type == ObjectType.Email: + email: EmailObject = object + prompt.append(AgentPrompt()) + prompt diff --git a/src/aios_kernel/knowledge/__init__.py b/src/knowledge/__init__.py similarity index 63% rename from src/aios_kernel/knowledge/__init__.py rename to src/knowledge/__init__.py index 13bb147..c879518 100644 --- a/src/aios_kernel/knowledge/__init__.py +++ b/src/knowledge/__init__.py @@ -1,2 +1,2 @@ from .object import EmailObject, ImageObject, TextChunkObject, ObjectID -from .knowledge_base import KnowledgeBase +from .vector import * \ No newline at end of file diff --git a/src/aios_kernel/knowledge/object.py b/src/knowledge/object.py similarity index 96% rename from src/aios_kernel/knowledge/object.py rename to src/knowledge/object.py index 815d4e2..f7a127e 100644 --- a/src/aios_kernel/knowledge/object.py +++ b/src/knowledge/object.py @@ -1,7 +1,7 @@ # define a object type enum -from abc import ABC - +from abc import ABC, abstractmethod +from enum import Enum class ObjectType(Enum): TextChunk = 1 diff --git a/src/aios_kernel/knowledge/object_storage.py b/src/knowledge/object_storage.py similarity index 100% rename from src/aios_kernel/knowledge/object_storage.py rename to src/knowledge/object_storage.py diff --git a/src/knowledge/vector/__init__.py b/src/knowledge/vector/__init__.py new file mode 100644 index 0000000..b3f0d78 --- /dev/null +++ b/src/knowledge/vector/__init__.py @@ -0,0 +1,2 @@ +from .vector_base import VectorBase +from .chroma_store import ChromaVectorStore diff --git a/src/knowledge/vector/chroma_store.py b/src/knowledge/vector/chroma_store.py new file mode 100644 index 0000000..626cd4f --- /dev/null +++ b/src/knowledge/vector/chroma_store.py @@ -0,0 +1,49 @@ +from .vector_base import VectorBase +from ..object import ObjectID +import chromadb +import logging +import os + + +class ChromaVectorStore(VectorBase): + def __init__(self, db_url, model_name: str) -> None: + super().__init__(db_url, model_name) + + logging.info( + "will init chroma vector store, db={}, model={}".format(db_url, model_name) + ) + + directory = os.path.join( + os.path.dirname(__file__), "../../../rootfs/data/vector" + ) + logging.info("will use vector store: {}".format(directory)) + + client = chromadb.PersistentClient( + path=directory, settings=chromadb.Settings(anonymized_telemetry=False) + ) + # client = chromadb.Client() + + collection_name = "coll_{}".format(model_name) + logging.info("will init chroma colletion: %s", collection_name) + + collection = client.get_or_create_collection(collection_name) + self.collection = collection + + async def insert(self, vector: [float], id: ObjectID): + self.collection.add( + embeddings=vector, + ids=id, + ) + + async def query(self, vector: [float], top_k: int) -> [ObjectID]: + ret = self.collection.query( + query_embeddings=vector, + n_results=top_k, + ) + + return ret["ids"] + + async def delete(self, id: ObjectID): + self.collection.delete( + ids=id, + ) diff --git a/src/aios_kernel/knowledge/vector_base.py b/src/knowledge/vector/vector_base.py similarity index 76% rename from src/aios_kernel/knowledge/vector_base.py rename to src/knowledge/vector/vector_base.py index b2cf442..354c21e 100644 --- a/src/aios_kernel/knowledge/vector_base.py +++ b/src/knowledge/vector/vector_base.py @@ -1,5 +1,5 @@ # import the ObjectID class -from .object import ObjectID +from ..object import ObjectID # define a vector base class class VectorBase: @@ -11,4 +11,7 @@ class VectorBase: pass async def query(self, vector: [float], top_k: int) -> [ObjectID]: + pass + + async def delete(self, id: ObjectID): pass \ No newline at end of file diff --git a/test/test_vector_storage.py b/test/test_vector_storage.py new file mode 100644 index 0000000..7357d65 --- /dev/null +++ b/test/test_vector_storage.py @@ -0,0 +1,29 @@ +import sys +import os + +dir_path = os.path.dirname(os.path.realpath(__file__)) +print(dir_path) + +sys.path.append("{}/../src/".format(dir_path)) +print(sys.path) + +from knowledge import ChromaVectorStore + + +import asyncio +import unittest + + +async def test_vector(): + storage = ChromaVectorStore("", "test") + await storage.insert([1, 2, 3], "test") + ids = await storage.query([1, 2, 3], 10) + print(ids) + +class TestVectorSTorage(unittest.TestCase): + def test_run(self): + asyncio.run(test_vector()) + + +if __name__ == "__main__": + unittest.main() From 1e5b9afd0f2fdd36bee255c75be1c45c8847e83f Mon Sep 17 00:00:00 2001 From: liyaxing Date: Thu, 7 Sep 2023 20:19:18 +0800 Subject: [PATCH 027/177] Add chunk relate impl and test case --- src/knowledge/__init__.py | 5 ++- src/knowledge/data/__init__.py | 2 + src/knowledge/data/chunk.py | 43 +++++++++++++++++++ src/knowledge/data/tracker.py | 77 ++++++++++++++++++++++++++++++++++ test/test_chunk.py | 36 ++++++++++++++++ 5 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/knowledge/data/__init__.py create mode 100644 src/knowledge/data/chunk.py create mode 100644 src/knowledge/data/tracker.py create mode 100644 test/test_chunk.py diff --git a/src/knowledge/__init__.py b/src/knowledge/__init__.py index c879518..6edb83e 100644 --- a/src/knowledge/__init__.py +++ b/src/knowledge/__init__.py @@ -1,2 +1,3 @@ -from .object import EmailObject, ImageObject, TextChunkObject, ObjectID -from .vector import * \ No newline at end of file +from .object import * +from .vector import * +from .data import * \ No newline at end of file diff --git a/src/knowledge/data/__init__.py b/src/knowledge/data/__init__.py new file mode 100644 index 0000000..81c672a --- /dev/null +++ b/src/knowledge/data/__init__.py @@ -0,0 +1,2 @@ +from .chunk import ChunkID, PositionType, PositionFileRange +from .tracker import ChunkTracker \ No newline at end of file diff --git a/src/knowledge/data/chunk.py b/src/knowledge/data/chunk.py new file mode 100644 index 0000000..ff355bd --- /dev/null +++ b/src/knowledge/data/chunk.py @@ -0,0 +1,43 @@ +from enum import Enum +from ..object import ObjectID + +ChunkID = ObjectID + +class PositionType(Enum): + Unknown = 1 + Device = 2 + File = 3 + FileRange = 4 + ChunkManager = 5 + + +class PositionFileRange: + def __init__(self, path: str, range_begin: int, range_end: int): + self.path = path + self.range_begin = range_begin + self.range_end = range_end + + def encode(self): + return f"{self.range_begin}:{self.range_end}:{self.path}" + + @staticmethod + def decode(value: str): + parts = value.split(":") + if len(parts) < 3: + raise ValueError("Invalid input string") + + try: + range_begin = int(parts[0]) + range_end = int(parts[1]) + except ValueError as e: + raise ValueError("Invalid range_begin or range_end string") from e + + path = ":".join(parts[2:]) + return PositionFileRange(path, range_begin, range_end) + + def __str__(self): + return self.encode() + + @staticmethod + def from_string(value: str): + return PositionFileRange.decode(value) diff --git a/src/knowledge/data/tracker.py b/src/knowledge/data/tracker.py new file mode 100644 index 0000000..efe4ede --- /dev/null +++ b/src/knowledge/data/tracker.py @@ -0,0 +1,77 @@ +import sqlite3 +import time +import logging +import os +from .chunk import ChunkID, PositionType, PositionFileRange + + +class ChunkTracker: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + directory = os.path.join( + os.path.dirname(__file__), "../../../rootfs/data/chunk/" + ) + + if not os.path.exists(directory): + os.mkdir(directory) + file = os.path.join(directory, "chunk_tracker.db") + logging.info(f"will init chunk tracker, db={file}") + cls._instance.__singleton_init__(file) + + return cls._instance + + def __singleton_init__(self, db_path: str): + logging.info(f"will init chunk tracker, db={db_path}") + + self.conn = sqlite3.connect(db_path) + self.cursor = self.conn.cursor() + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS chunks ( + id TEXT NOT NULL, + pos TEXT NOT NULL, + pos_type TINYINT NOT NULL, + insert_time UNSIGNED BIG INT NOT NULL, + update_time UNSIGNED BIG INT NOT NULL, + flags INTEGER DEFAULT 0, + PRIMARY KEY(id, pos, pos_type) + ) + """ + ) + self.conn.commit() + + def add_position(self, chunk_id: ChunkID, position: str, position_type: PositionType): + logging.debug(f"add chunk position: {chunk_id}, {position}, {position_type}") + + insert_time = update_time = int(time.time()) + self.cursor.execute( + """ + INSERT OR REPLACE INTO chunks (id, pos, pos_type, insert_time, update_time) + VALUES (?, ?, ?, ?, ?) + """, + (str(chunk_id), position, position_type.value, insert_time, update_time,), + ) + self.conn.commit() + + def remove_position(self, chunk_id: ChunkID): + logging.info(f"remove chunk position: {chunk_id}") + + self.cursor.execute( + """ + DELETE FROM chunks WHERE id = ? + """, + (str(chunk_id),), + ) + self.conn.commit() + + def get_position(self, chunk_id: ChunkID) -> (str, PositionType): + self.cursor.execute( + """ + SELECT pos, pos_type FROM chunks WHERE id = ? + """, + (str(chunk_id),), + ) + return self.cursor.fetchone() diff --git a/test/test_chunk.py b/test/test_chunk.py new file mode 100644 index 0000000..40a7aeb --- /dev/null +++ b/test/test_chunk.py @@ -0,0 +1,36 @@ +import sys +import os + +dir_path = os.path.dirname(os.path.realpath(__file__)) +print(dir_path) + +sys.path.append("{}/../src/".format(dir_path)) +print(sys.path) + +from knowledge import ChunkTracker, ChunkID, HashValue, PositionType + + +import asyncio +import unittest + + +class TestChunk(unittest.TestCase): + def test_chunk_tracker(self): + tracker = ChunkTracker() + + hash = HashValue.hash_data("1234567890".encode("utf-8")); + cid = ChunkID.new_chunk_id(hash) + print(cid) + + tracker.add_position(cid, "/tmp/1", PositionType.File) + ret = tracker.get_position(cid) + print(ret[0]) + + tracker.remove_position(cid) + ret = tracker.get_position(cid) + self.assertEqual(ret, None) + + + +if __name__ == "__main__": + unittest.main() From e8e3812d287eeae5b4275883b0927928932280bd Mon Sep 17 00:00:00 2001 From: liyaxing Date: Fri, 8 Sep 2023 15:52:05 +0800 Subject: [PATCH 028/177] Add chunk store and chunk list helper --- src/knowledge/data/__init__.py | 4 +- src/knowledge/data/chunk.py | 2 +- src/knowledge/data/chunk_store.py | 26 +++++++++ src/knowledge/data/gen.py | 94 +++++++++++++++++++++++++++++++ src/knowledge/data/tracker.py | 40 ++++++------- test/test_chunk.py | 26 ++++++++- 6 files changed, 164 insertions(+), 28 deletions(-) create mode 100644 src/knowledge/data/chunk_store.py create mode 100644 src/knowledge/data/gen.py diff --git a/src/knowledge/data/__init__.py b/src/knowledge/data/__init__.py index 81c672a..f8e69aa 100644 --- a/src/knowledge/data/__init__.py +++ b/src/knowledge/data/__init__.py @@ -1,2 +1,4 @@ from .chunk import ChunkID, PositionType, PositionFileRange -from .tracker import ChunkTracker \ No newline at end of file +from .tracker import ChunkTracker +from .chunk_store import ChunkStore +from .gen import ChunkListGenerator \ No newline at end of file diff --git a/src/knowledge/data/chunk.py b/src/knowledge/data/chunk.py index ff355bd..5267dc3 100644 --- a/src/knowledge/data/chunk.py +++ b/src/knowledge/data/chunk.py @@ -8,7 +8,7 @@ class PositionType(Enum): Device = 2 File = 3 FileRange = 4 - ChunkManager = 5 + ChunkStore = 5 class PositionFileRange: diff --git a/src/knowledge/data/chunk_store.py b/src/knowledge/data/chunk_store.py new file mode 100644 index 0000000..d06eb38 --- /dev/null +++ b/src/knowledge/data/chunk_store.py @@ -0,0 +1,26 @@ +import os +import logging +from ..object import FileBlobStorage +from .chunk import ChunkID + + +class ChunkStore: + def __init__(self, root_dir: str): + logging.info(f"will init chunk store, root_dir={root_dir}") + + if not os.path.exists(root_dir): + os.makedirs(root_dir) + + self.root = root_dir + self.blob = FileBlobStorage(root_dir) + + def put_chunk(self, chunk_id: ChunkID, contents: bytes): + self.blob.put(chunk_id, contents) + + def get_chunk(self, chunk_id: ChunkID) -> bytes: + return self.blob.get(chunk_id) + + def delete_chunk(self, chunk_id: ChunkID): + self.blob.delete(chunk_id) + + \ No newline at end of file diff --git a/src/knowledge/data/gen.py b/src/knowledge/data/gen.py new file mode 100644 index 0000000..a6d9425 --- /dev/null +++ b/src/knowledge/data/gen.py @@ -0,0 +1,94 @@ +import os +import hashlib +import re +from typing import Tuple, List +from .chunk_store import ChunkStore +from .chunk import ChunkID, PositionFileRange, PositionType +from ..object import HashValue +from .tracker import ChunkTracker + + +class ChunkListGenerator: + def __init__(self, chunk_store: ChunkStore, chunk_tracker: ChunkTracker): + self.chunk_store = chunk_store + self.chunk_tracker = chunk_tracker + + def create_chunk_list_from_file( + self, file_path: str, chunk_size: int, restore: bool + ) -> Tuple[List[ChunkID], HashValue]: + assert ( + chunk_size % (1024 * 1024) == 0 + ), "chunk size should be an integral multiple of 1MB" + chunk_list = [] + hash_obj = hashlib.sha256() + + with open(file_path, "rb") as file: + while True: + chunk = file.read(chunk_size) + if not chunk: + break + chunk_id = ChunkID.hash_data(chunk) + chunk_list.append(chunk_id) + + hash_obj.update(chunk) + + if restore: + self.chunk_tracker.add_position( + chunk_id, file_path, PositionType.ChunkStore + ) + self.chunk_store.put_chunk(chunk_id, chunk) + else: + file_range = PositionFileRange( + file_path, file.tell() - chunk_size, chunk_size + ) + self.chunk_tracker.add_position( + chunk_id, file_range, PositionType.FileRange + ) + + file_hash = HashValue(hash_obj.digest()) + print(f"calc file hash: {file_path}, {file_hash}") + return chunk_list, file_hash + + def create_chunk_list_from_text( + self, text: str, chunk_max_words: int, separator_chars: str = ".," + ) -> Tuple[List[ChunkID], HashValue]: + text_list = self._split_text_list(text, chunk_max_words, separator_chars) + chunk_list = [] + hash_obj = hashlib.sha256() + + for text in text_list: + chunk_bytes = text.encode("utf-8") + hash_obj.update(chunk_bytes) + + chunk_id = ChunkID.hash_data(chunk_bytes) + chunk_list.append(chunk_id) + self.chunk_tracker.add_position( + chunk_id, "", PositionType.ChunkStore + ) + self.chunk_store.put_chunk(chunk_id, chunk_bytes) + + hash = HashValue(hash_obj.digest()) + return chunk_list, hash + + @staticmethod + def _split_text_list( + text: str, chunk_max_words: int, separator_chars: str = ".," + ) -> List[str]: + sentences = re.split(f"[{separator_chars}]", text) + chunk_list = [] + chunk = [] + word_count = 0 + for sentence in sentences: + words = sentence.split() + for word in words: + if word_count < chunk_max_words: + chunk.append(word) + word_count += 1 + else: + chunk_list.append(" ".join(chunk)) + chunk = [word] + word_count = 1 + if chunk: + chunk_list.append(" ".join(chunk)) + return chunk_list + diff --git a/src/knowledge/data/tracker.py b/src/knowledge/data/tracker.py index efe4ede..2905d11 100644 --- a/src/knowledge/data/tracker.py +++ b/src/knowledge/data/tracker.py @@ -2,31 +2,17 @@ import sqlite3 import time import logging import os -from .chunk import ChunkID, PositionType, PositionFileRange +from .chunk import ChunkID, PositionType, PositionFileRange class ChunkTracker: - _instance = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - directory = os.path.join( - os.path.dirname(__file__), "../../../rootfs/data/chunk/" - ) - - if not os.path.exists(directory): - os.mkdir(directory) - file = os.path.join(directory, "chunk_tracker.db") - logging.info(f"will init chunk tracker, db={file}") - cls._instance.__singleton_init__(file) - - return cls._instance + def __init__(self, root_dir: str): + if not os.path.exists(root_dir): + os.makedirs(root_dir) + file = os.path.join(root_dir, "chunk_tracker.db") + logging.info(f"will init chunk tracker, db={file}") - def __singleton_init__(self, db_path: str): - logging.info(f"will init chunk tracker, db={db_path}") - - self.conn = sqlite3.connect(db_path) + self.conn = sqlite3.connect(file) self.cursor = self.conn.cursor() self.cursor.execute( """ @@ -43,7 +29,9 @@ class ChunkTracker: ) self.conn.commit() - def add_position(self, chunk_id: ChunkID, position: str, position_type: PositionType): + def add_position( + self, chunk_id: ChunkID, position: str, position_type: PositionType + ): logging.debug(f"add chunk position: {chunk_id}, {position}, {position_type}") insert_time = update_time = int(time.time()) @@ -52,7 +40,13 @@ class ChunkTracker: INSERT OR REPLACE INTO chunks (id, pos, pos_type, insert_time, update_time) VALUES (?, ?, ?, ?, ?) """, - (str(chunk_id), position, position_type.value, insert_time, update_time,), + ( + str(chunk_id), + position, + position_type.value, + insert_time, + update_time, + ), ) self.conn.commit() diff --git a/test/test_chunk.py b/test/test_chunk.py index 40a7aeb..b2f2e7e 100644 --- a/test/test_chunk.py +++ b/test/test_chunk.py @@ -7,16 +7,27 @@ print(dir_path) sys.path.append("{}/../src/".format(dir_path)) print(sys.path) -from knowledge import ChunkTracker, ChunkID, HashValue, PositionType + +import logging +import sys + +root = logging.getLogger() +root.setLevel(logging.DEBUG) +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +root.addHandler(handler) +from knowledge import ChunkTracker, ChunkID, HashValue, PositionType, KnowledgeStore, ChunkListGenerator import asyncio import unittest class TestChunk(unittest.TestCase): def test_chunk_tracker(self): - tracker = ChunkTracker() + tracker = KnowledgeStore().get_chunk_tracker() hash = HashValue.hash_data("1234567890".encode("utf-8")); cid = ChunkID.new_chunk_id(hash) @@ -29,8 +40,17 @@ class TestChunk(unittest.TestCase): tracker.remove_position(cid) ret = tracker.get_position(cid) self.assertEqual(ret, None) - + + def test_chunk(self): + gen = ChunkListGenerator(KnowledgeStore().get_chunk_store(), KnowledgeStore().get_chunk_tracker()) + gen.create_chunk_list_from_file("H:/test", 1024 * 1024, True) + + # Read the file + text_file = "H:/test.txt" + with open(text_file, 'r', encoding = "utf-8") as file: + text = file.read() + gen.create_chunk_list_from_text(text, 1024) if __name__ == "__main__": unittest.main() From 0d793bd12d4fd5e24b13d9a829a81ad91adac3b4 Mon Sep 17 00:00:00 2001 From: Song Date: Sat, 9 Sep 2023 22:07:31 +0800 Subject: [PATCH 029/177] Remove some test information --- src/aios_kernel/compute_task.py | 4 ++-- src/aios_kernel/stability_node.py | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index 7895283..086d946 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -23,7 +23,7 @@ class ComputeTaskType(Enum): 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 @@ -37,7 +37,7 @@ class ComputeTask: self.error_str = None def set_llm_params(self, prompts, model_name, max_token_size, callchain_id=None): - self.task_type = "llm_completion" + self.task_type = ComputeTaskType.LLM_COMPLETION self.create_time = time.time() self.task_id = uuid.uuid4().hex self.callchain_id = callchain_id diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index a5ed4e0..8d40878 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -31,21 +31,17 @@ class Stability_ComputeNode(ComputeNode): self.is_start = True self.node_id = "stability_node" - self.api_key = "" # "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" - self.engine = "" # stable-diffusion-512-v2-0 + 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") - else: - self.api_key = "sk-RQDlJtBFQg6I3IueeGCGZTPhWPYAl3bgRdvFDMkcEXsKbUc0" # 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") - else: - self.engine = "stable-diffusion-512-v2-1" self.client = client.StabilityInference( key=self.api_key, From 27ed3e20e6cf448739a61da1ef97283be2c8b557 Mon Sep 17 00:00:00 2001 From: Song Date: Sat, 9 Sep 2023 22:08:39 +0800 Subject: [PATCH 030/177] The argument for is_support should be task_type --- src/aios_kernel/compute_kernel.py | 62 ++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 1d05afd..2501ad6 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -6,15 +6,18 @@ 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): if cls._instance is None: cls._instance = super().__new__(cls) @@ -25,7 +28,7 @@ class ComputeKernel: def __init__(self) -> None: if self.is_start is True: return - + self.is_start = True self.task_queue = Queue() self.is_start = False @@ -33,70 +36,72 @@ class ComputeKernel: 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: + if node.is_support(task.task_type) 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): # 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) 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) -> str: + task_req = self.llm_completion(prompt, mode_name, max_token) + async def check_timer(): check_times = 0 while True: @@ -106,16 +111,15 @@ 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 "error!" - \ No newline at end of file From 36476eb5b4078e8fd4f5edfdd4aee90887b515ef Mon Sep 17 00:00:00 2001 From: liyaxing Date: Mon, 11 Sep 2023 00:46:56 +0800 Subject: [PATCH 031/177] Add object and chunk base impl and test cases --- src/aios_kernel/knowledge_base.py | 2 +- src/knowledge/__init__.py | 3 +- src/knowledge/data/__init__.py | 3 +- src/knowledge/data/chunk_list.py | 14 +++++ src/knowledge/data/{gen.py => writer.py} | 26 ++++---- src/knowledge/object.py | 65 ------------------- src/knowledge/object/__init__.py | 5 ++ src/knowledge/object/blob.py | 54 ++++++++++++++++ src/knowledge/object/hash.py | 42 +++++++++++++ src/knowledge/object/object.py | 79 ++++++++++++++++++++++++ src/knowledge/object/object_id.py | 53 ++++++++++++++++ src/knowledge/object/object_store.py | 24 +++++++ src/knowledge/object_storage.py | 33 ---------- src/knowledge/store.py | 46 ++++++++++++++ test/test_chunk.py | 30 ++++++--- test/test_object.py | 31 ++++++++++ 16 files changed, 385 insertions(+), 125 deletions(-) create mode 100644 src/knowledge/data/chunk_list.py rename src/knowledge/data/{gen.py => writer.py} (89%) delete mode 100644 src/knowledge/object.py create mode 100644 src/knowledge/object/__init__.py create mode 100644 src/knowledge/object/blob.py create mode 100644 src/knowledge/object/hash.py create mode 100644 src/knowledge/object/object.py create mode 100644 src/knowledge/object/object_id.py create mode 100644 src/knowledge/object/object_store.py delete mode 100644 src/knowledge/object_storage.py create mode 100644 src/knowledge/store.py create mode 100644 test/test_object.py diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index a882ba7..4f4ba4f 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,7 +1,7 @@ # define a knowledge base class from . import AgentPrompt, ComputeKernel from ..knowledge.object import KnowledgeObject, ObjectType, EmailObject, TextChunkObject, ImageObject -from ..knowledge.object_storage import ObjectStorage +from ..knowledge.store import ObjectStorage from ..knowledge.vector.vector_base import VectorBase class KnowledgeBase: diff --git a/src/knowledge/__init__.py b/src/knowledge/__init__.py index 6edb83e..e2cdeb2 100644 --- a/src/knowledge/__init__.py +++ b/src/knowledge/__init__.py @@ -1,3 +1,4 @@ from .object import * from .vector import * -from .data import * \ No newline at end of file +from .data import * +from .store import KnowledgeStore \ No newline at end of file diff --git a/src/knowledge/data/__init__.py b/src/knowledge/data/__init__.py index f8e69aa..2b391c9 100644 --- a/src/knowledge/data/__init__.py +++ b/src/knowledge/data/__init__.py @@ -1,4 +1,5 @@ from .chunk import ChunkID, PositionType, PositionFileRange from .tracker import ChunkTracker from .chunk_store import ChunkStore -from .gen import ChunkListGenerator \ No newline at end of file +from .writer import ChunkListWriter +from .chunk_list import ChunkList diff --git a/src/knowledge/data/chunk_list.py b/src/knowledge/data/chunk_list.py new file mode 100644 index 0000000..969b03f --- /dev/null +++ b/src/knowledge/data/chunk_list.py @@ -0,0 +1,14 @@ +from ..object import HashValue +from .chunk import ChunkID +from typing import List + +class ChunkList: + def __init__(self, chunk_list: List[ChunkID], hash: HashValue): + self.chunk_list = chunk_list + self.hash = hash + + def __str__(self): + return self.hash.to_base58() + + def __repr__(self): + return f"chunk_list: {self.chunk_list}, hash: {self.hash}" \ No newline at end of file diff --git a/src/knowledge/data/gen.py b/src/knowledge/data/writer.py similarity index 89% rename from src/knowledge/data/gen.py rename to src/knowledge/data/writer.py index a6d9425..7f79447 100644 --- a/src/knowledge/data/gen.py +++ b/src/knowledge/data/writer.py @@ -6,16 +6,16 @@ from .chunk_store import ChunkStore from .chunk import ChunkID, PositionFileRange, PositionType from ..object import HashValue from .tracker import ChunkTracker +from .chunk_list import ChunkList - -class ChunkListGenerator: +class ChunkListWriter: def __init__(self, chunk_store: ChunkStore, chunk_tracker: ChunkTracker): self.chunk_store = chunk_store self.chunk_tracker = chunk_tracker def create_chunk_list_from_file( self, file_path: str, chunk_size: int, restore: bool - ) -> Tuple[List[ChunkID], HashValue]: + ) -> ChunkList: assert ( chunk_size % (1024 * 1024) == 0 ), "chunk size should be an integral multiple of 1MB" @@ -47,7 +47,8 @@ class ChunkListGenerator: file_hash = HashValue(hash_obj.digest()) print(f"calc file hash: {file_path}, {file_hash}") - return chunk_list, file_hash + + return ChunkList(chunk_list, file_hash) def create_chunk_list_from_text( self, text: str, chunk_max_words: int, separator_chars: str = ".," @@ -55,20 +56,18 @@ class ChunkListGenerator: text_list = self._split_text_list(text, chunk_max_words, separator_chars) chunk_list = [] hash_obj = hashlib.sha256() - + for text in text_list: - chunk_bytes = text.encode("utf-8") + chunk_bytes = text.encode("utf-8") hash_obj.update(chunk_bytes) - + chunk_id = ChunkID.hash_data(chunk_bytes) chunk_list.append(chunk_id) - self.chunk_tracker.add_position( - chunk_id, "", PositionType.ChunkStore - ) + self.chunk_tracker.add_position(chunk_id, "", PositionType.ChunkStore) self.chunk_store.put_chunk(chunk_id, chunk_bytes) - + hash = HashValue(hash_obj.digest()) - return chunk_list, hash + return ChunkList(chunk_list, hash) @staticmethod def _split_text_list( @@ -90,5 +89,4 @@ class ChunkListGenerator: word_count = 1 if chunk: chunk_list.append(" ".join(chunk)) - return chunk_list - + return chunk_list \ No newline at end of file diff --git a/src/knowledge/object.py b/src/knowledge/object.py deleted file mode 100644 index f7a127e..0000000 --- a/src/knowledge/object.py +++ /dev/null @@ -1,65 +0,0 @@ - -# define a object type enum -from abc import ABC, abstractmethod -from enum import Enum - -class ObjectType(Enum): - TextChunk = 1 - Image = 2 - Email = 101 - - -# define a object ID class to identify a object -class ObjectID: # pylint: disable=too-few-public-methods - def __init__(self, object_type, digist): - self.object_type = object_type - self.digist = digist - - def __str__(self): - return f"{self.object_type.name}:{self.digist}" - - -# define a object class -class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods - def __init__(self, object_type: ObjectType): - self.object_type = object_type - - @abstractmethod - def get_id(self) -> ObjectID: - pass - - # define a to binary method to convert object to binary - @abstractmethod - def to_binary(self) -> bytes: - pass - - # define a from binary method to convert binary to object - @abstractmethod - def from_binary(self, binary: bytes): - pass - - -# define a text chunk class -class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, text: str): - super().__init__(ObjectType.TextChunk) - self.text = text - - -# define a image class -class ImageObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, meta, path): - super().__init__(ObjectType.Image) - self.meta = meta - self.path = path - - -# define a email class -class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, meta): - super().__init__(ObjectType.Email) - self.meta = meta - self.text = [ObjectID] - self.images = [ObjectID] - - diff --git a/src/knowledge/object/__init__.py b/src/knowledge/object/__init__.py new file mode 100644 index 0000000..ef1db5c --- /dev/null +++ b/src/knowledge/object/__init__.py @@ -0,0 +1,5 @@ +from .object import EmailObject, ImageObject, TextChunkObject, ObjectID +from .blob import FileBlobStorage +from .hash import HashValue, hash_data +from .object_store import ObjectStore +from .object_id import ObjectID, ObjectType diff --git a/src/knowledge/object/blob.py b/src/knowledge/object/blob.py new file mode 100644 index 0000000..b005e62 --- /dev/null +++ b/src/knowledge/object/blob.py @@ -0,0 +1,54 @@ +import os +import shutil +from .object import ObjectID + + +class FileBlobStorage: + def __init__(self, root): + self.root = root + + def get_full_path(self, object_id: ObjectID, auto_create: bool = True): + if os.name == "nt": # Windows + hash_str = object_id.to_base36() + len = 3 + else: + hash_str = str(object_id) + len = 2 + + tmp, first = hash_str[:-len], hash_str[-len:] + second = tmp[-len:] + + if os.name == "nt": # Windows + if second in ["con", "aux", "nul", "prn"]: + second = tmp[-(len + 1) :] + if first in ["con", "aux", "nul", "prn"]: + first = f"{first}_" + + path = os.path.join(self.root, first, second) + if auto_create and not os.path.exists(path): + os.makedirs(path) + + path = os.path.join(path, hash_str) + + return path + + def write_sync(self, path: str, contents: bytes): + with open(path, "wb") as f: + f.write(contents) + + def put(self, object_id: ObjectID, contents: bytes): + full_path = self.get_full_path(object_id) + self.write_sync(full_path, contents) + + def get(self, object_id: ObjectID) -> bytes: + full_path = self.get_full_path(object_id) + with open(full_path, "rb") as f: + return f.read() + + def delete(self, object_id: ObjectID): + full_path = self.get_full_path(object_id) + os.remove(full_path) + + def exists(self, object_id: ObjectID) -> bool: + full_path = self.get_full_path(object_id) + return os.path.exists(full_path) diff --git a/src/knowledge/object/hash.py b/src/knowledge/object/hash.py new file mode 100644 index 0000000..5063814 --- /dev/null +++ b/src/knowledge/object/hash.py @@ -0,0 +1,42 @@ +import hashlib +import base58 +import base36 + +class HashValue: + def __init__(self, value: bytes): + assert len(value) == 32, "HashValue must be 32 bytes long" + self.value = value + + def __str__(self) -> str: + return self.to_base58() + + @staticmethod + def hash_data(data): + return hash_data(data) + + def to_base58(self): + return base58.b58encode(self.value).decode() + + @staticmethod + def from_base58(s): + return HashValue(base58.b58decode(s)) + + def to_base36(self): + # Convert the bytes to int before encoding + num = int.from_bytes(self.value, 'big') + return base36.dumps(num) + + @staticmethod + def from_base36(s): + # Decode to int and then convert to bytes + num = base36.loads(s) + return HashValue(num.to_bytes((num.bit_length() + 7) // 8, 'big')) + + +HASH_VALUE_LEN = 32 + + +def hash_data(data: bytes): + sha256 = hashlib.sha256() + sha256.update(data) + return HashValue(sha256.digest()) diff --git a/src/knowledge/object/object.py b/src/knowledge/object/object.py new file mode 100644 index 0000000..0eff535 --- /dev/null +++ b/src/knowledge/object/object.py @@ -0,0 +1,79 @@ + +# define a object type enum +from abc import ABC, abstractmethod +from enum import Enum +from .object_id import ObjectID +import hashlib +import json +import pickle + +class KnowledgeObject(ABC): + def __init__(self, object_type: int, desc: dict = {}, body: dict = {}): + self.desc = desc + self.body = body + self.object_type = object_type + + def get_object_type(self): + return self.object_type + + def object_id(self) -> ObjectID: + return self.calculate_id() + + def set_desc_with_key_value(self, key, value): + self.desc[key] = value + + def get_desc_with_key(self, key): + return self.desc.get(key) + + def get_desc(self) -> dict: + return self.desc + + def set_body_with_key_value(self, key, value): + self.body[key] = value + + def get_body_with_key(self, key): + return self.body.get(key) + + def get_body(self) -> dict: + return self.body + + def calculate_id(self): + # Convert the object_type and desc to string and compute the SHA256 hash + data = json.dumps({"object_type": self.object_type, "desc": self.desc}) + sha256 = hashlib.sha256() + sha256.update(data.encode()) + return ObjectID(sha256.digest()) + + def encode(self) -> bytes: + return pickle.dumps(self) + + @staticmethod + def decode(data: bytes): + return pickle.loads(data) + + + +# define a text chunk class +class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, text: str): + super().__init__(ObjectType.TextChunk) + self.text = text + + +# define a image class +class ImageObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta, path): + super().__init__(ObjectType.Image) + self.meta = meta + self.path = path + + +# define a email class +class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta): + super().__init__(ObjectType.Email) + self.meta = meta + self.text = [ObjectID] + self.images = [ObjectID] + + diff --git a/src/knowledge/object/object_id.py b/src/knowledge/object/object_id.py new file mode 100644 index 0000000..27c0063 --- /dev/null +++ b/src/knowledge/object/object_id.py @@ -0,0 +1,53 @@ +# define a object type enum +from abc import ABC, abstractmethod +from enum import Enum +from .hash import HashValue +import base58 +import base36 + + +class ObjectType(Enum): + Chunk = 7 + TextChunk = 100 + Image = 101 + Email = 102 + + +# define a object ID class to identify a object +class ObjectID: # pylint: disable=too-few-public-methods + def __init__(self, object_type: ObjectType, value: bytes): + assert len(value) == 32, "ObjectID must be 32 bytes long" + self.object_type = object_type + self.value = value + + def __str__(self): + return self.to_base58() + + def get_object_type(self): + return self.object_type + + def to_base58(self): + return base58.b58encode(self.value).decode() + + @staticmethod + def from_base58(s): + return ObjectID(base58.b58decode(s)) + + def to_base36(self): + # Convert the bytes to int before encoding + num = int.from_bytes(self.value, "big") + return base36.dumps(num) + + @staticmethod + def from_base36(s): + # Decode to int and then convert to bytes + num = base36.loads(s) + return ObjectID(num.to_bytes((num.bit_length() + 7) // 8, "big")) + + @staticmethod + def new_chunk_id(chunk_hash: HashValue): + return ObjectID(ObjectType.Chunk, chunk_hash.value) + + @staticmethod + def hash_data(data: bytes): + return ObjectID.new_chunk_id(HashValue.hash_data(data)) \ No newline at end of file diff --git a/src/knowledge/object/object_store.py b/src/knowledge/object/object_store.py new file mode 100644 index 0000000..d903b8f --- /dev/null +++ b/src/knowledge/object/object_store.py @@ -0,0 +1,24 @@ +import os +import logging +from .blob import FileBlobStorage +from .object_id import ObjectID + + +class ObjectStore: + def __init__(self, root_dir: str): + logging.info(f"will init object blob store, root_dir={root_dir}") + + blob_dir = os.path.join(root_dir, "blob") + if not os.path.exists(blob_dir): + logging.info(f"will create blob dir: {blob_dir}") + os.makedirs(blob_dir) + self.blob = FileBlobStorage(blob_dir) + + def put_object(self, object_id: ObjectID, contents: bytes): + self.blob.put(object_id, contents) + + def get_object(self, object_id: ObjectID) -> bytes: + return self.blob.get(object_id) + + def delete_object(self, object_id: ObjectID): + self.blob.delete(object_id) diff --git a/src/knowledge/object_storage.py b/src/knowledge/object_storage.py deleted file mode 100644 index 1a6f85e..0000000 --- a/src/knowledge/object_storage.py +++ /dev/null @@ -1,33 +0,0 @@ -# import RDB LargeBinary -from sqlalchemy import Column, String, LargeBinary, create_engine, sessionmaker, pickle -from .object import KnowledgeObject - -# implement object storage with RDB -# define object storage table -class ObjectStorageTable(Base): - __tablename__ = 'object_storage' - id = Column(String, primary_key=True) - parent = Column(String, nullable=True) - object = Column(LargeBinary, nullable=False) - - def __init__(self, id, parent, object): # pylint: disable=redefined-builtin - self.id = id - self.parent = parent - self.object = object - -# define object storage class -class ObjectStorage: - async def __init__(self, db_url): - self.engine = create_engine(db_url) - self.session = sessionmaker(bind=self.engine)() # pylint: disable=not-callable - - async def get(self, id) -> [KnowledgeObject, KnowledgeObject]: - obj = self.session.query(ObjectStorageTable).filter(ObjectStorageTable.id == id).first() - if obj is None: - return None - return pickle.loads(obj.object) - - # define insert method - async def insert(self, object, parent): # pylint: disable=redefined-builtin - obj = ObjectStorageTable(id, parent, pickle.dumps(object)) - \ No newline at end of file diff --git a/src/knowledge/store.py b/src/knowledge/store.py new file mode 100644 index 0000000..a4a0af6 --- /dev/null +++ b/src/knowledge/store.py @@ -0,0 +1,46 @@ +import os +from .object import ObjectStore +from .data import ChunkStore, ChunkTracker +import logging + + +# KnowledgeStore class, 聚合ChunkStore,ChunkTracker, ObjectStore,并且是一个全局单例,可以方便的使用这三个内置的store示例 +class KnowledgeStore: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + directory = os.path.join( + os.path.dirname(__file__), "../../rootfs/data/" + ) + directory = os.path.normpath(directory) + print(directory) + + if not os.path.exists(directory): + os.makedirs(directory) + + cls._instance.__singleton_init__(directory) + + return cls._instance + + def __singleton_init__(self, root_dir: str): + logging.info(f"will init knowledge store, root_dir={root_dir}") + + self.root = root_dir + + object_store_dir = os.path.join(root_dir, "object") + self.object_store = ObjectStore(object_store_dir) + + chunk_store_dir = os.path.join(root_dir, "chunk") + self.chunk_store = ChunkStore(chunk_store_dir) + self.chunk_tracker = ChunkTracker(chunk_store_dir) + + def get_object_store(self) -> ObjectStore: + return self.object_store + + def get_chunk_store(self) -> ChunkStore: + return self.chunk_store + + def get_chunk_tracker(self) -> ChunkTracker: + return self.chunk_tracker diff --git a/test/test_chunk.py b/test/test_chunk.py index b2f2e7e..f1f496b 100644 --- a/test/test_chunk.py +++ b/test/test_chunk.py @@ -15,12 +15,19 @@ root = logging.getLogger() root.setLevel(logging.DEBUG) handler = logging.StreamHandler(sys.stdout) handler.setLevel(logging.DEBUG) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) root.addHandler(handler) -from knowledge import ChunkTracker, ChunkID, HashValue, PositionType, KnowledgeStore, ChunkListGenerator +from knowledge import ( + ChunkTracker, + ChunkID, + HashValue, + PositionType, + KnowledgeStore, + ChunkListWriter, +) import asyncio import unittest @@ -28,29 +35,32 @@ import unittest class TestChunk(unittest.TestCase): def test_chunk_tracker(self): tracker = KnowledgeStore().get_chunk_tracker() - - hash = HashValue.hash_data("1234567890".encode("utf-8")); + + hash = HashValue.hash_data("1234567890".encode("utf-8")) cid = ChunkID.new_chunk_id(hash) print(cid) - + tracker.add_position(cid, "/tmp/1", PositionType.File) ret = tracker.get_position(cid) print(ret[0]) - + tracker.remove_position(cid) ret = tracker.get_position(cid) self.assertEqual(ret, None) - + def test_chunk(self): - gen = ChunkListGenerator(KnowledgeStore().get_chunk_store(), KnowledgeStore().get_chunk_tracker()) + gen = ChunkListWriter( + KnowledgeStore().get_chunk_store(), KnowledgeStore().get_chunk_tracker() + ) gen.create_chunk_list_from_file("H:/test", 1024 * 1024, True) - + # Read the file text_file = "H:/test.txt" - with open(text_file, 'r', encoding = "utf-8") as file: + with open(text_file, "r", encoding="utf-8") as file: text = file.read() gen.create_chunk_list_from_text(text, 1024) + if __name__ == "__main__": unittest.main() diff --git a/test/test_object.py b/test/test_object.py new file mode 100644 index 0000000..6a603fd --- /dev/null +++ b/test/test_object.py @@ -0,0 +1,31 @@ +import sys +import os + +dir_path = os.path.dirname(os.path.realpath(__file__)) +print(dir_path) + +sys.path.append("{}/../src/".format(dir_path)) +print(sys.path) + +from knowledge import ObjectID, HashValue + + +import asyncio +import unittest + + +class TestVectorSTorage(unittest.TestCase): + def test_object(self): + data = HashValue.hash_data("1233".encode("utf-8")); + print(data.to_base58()) + print(data.to_base36()) + + data2 = HashValue.from_base58(data.to_base58()) + self.assertEqual(data.to_base36(), data2.to_base36()) + + data2 = HashValue.from_base36(data.to_base36()) + self.assertEqual(data.to_base58(), data2.to_base58()) + + +if __name__ == "__main__": + unittest.main() From 2761883ae81a2d8bb3048baef665c85b22b70039 Mon Sep 17 00:00:00 2001 From: liyaxing Date: Mon, 11 Sep 2023 02:05:24 +0800 Subject: [PATCH 032/177] Add some core objects impl --- src/knowledge/__init__.py | 3 +- src/knowledge/core_object/__init__.py | 4 + src/knowledge/core_object/document_object.py | 57 ++++++++++++ src/knowledge/core_object/email_object.py | 47 ++++++++++ src/knowledge/core_object/image_object.py | 89 +++++++++++++++++++ src/knowledge/core_object/rich_text_object.py | 80 +++++++++++++++++ src/knowledge/core_object/video_object.py | 84 +++++++++++++++++ src/knowledge/object/__init__.py | 2 +- src/knowledge/object/object.py | 31 +------ src/knowledge/object/object_id.py | 5 +- src/knowledge/store.py | 2 +- 11 files changed, 372 insertions(+), 32 deletions(-) create mode 100644 src/knowledge/core_object/__init__.py create mode 100644 src/knowledge/core_object/document_object.py create mode 100644 src/knowledge/core_object/email_object.py create mode 100644 src/knowledge/core_object/image_object.py create mode 100644 src/knowledge/core_object/rich_text_object.py create mode 100644 src/knowledge/core_object/video_object.py diff --git a/src/knowledge/__init__.py b/src/knowledge/__init__.py index e2cdeb2..680c632 100644 --- a/src/knowledge/__init__.py +++ b/src/knowledge/__init__.py @@ -1,4 +1,5 @@ from .object import * from .vector import * from .data import * -from .store import KnowledgeStore \ No newline at end of file +from .store import KnowledgeStore +from .core_object import * \ No newline at end of file diff --git a/src/knowledge/core_object/__init__.py b/src/knowledge/core_object/__init__.py new file mode 100644 index 0000000..065de74 --- /dev/null +++ b/src/knowledge/core_object/__init__.py @@ -0,0 +1,4 @@ +from .document_object import DocumentObject, DocumentObjectBuilder +from .image_object import ImageObject, ImageObjectBuilder +from .video_object import VideoObject, VideoObjectBuilder +from .email_object import EmailObject, EmailObjectBuilder \ No newline at end of file diff --git a/src/knowledge/core_object/document_object.py b/src/knowledge/core_object/document_object.py new file mode 100644 index 0000000..dd9bb8b --- /dev/null +++ b/src/knowledge/core_object/document_object.py @@ -0,0 +1,57 @@ +from ..object import KnowledgeObject +from ..data import ChunkList, ChunkListWriter +from ..object import ObjectType + + +# desc +# meta +# hash: "file-hash", +# tags: {} +# body +# chunk_list: [chunk_id, chunk_id, ...] + + +class DocumentObject(KnowledgeObject): + def __init__(self, meta: dict, tags: dict, chunk_list: ChunkList): + desc = dict() + body = dict() + desc["meta"] = meta + desc["tags"] = tags + desc["hash"] = chunk_list.hash.to_base58() + body["chunk_list"] = chunk_list.chunk_list + + super().__init__(ObjectType.Document, desc, body) + + def get_meta(self): + return self.desc["meta"] + + def get_tags(self): + return self.desc["tags"] + + def get_hash(self): + return self.desc["hash"] + + def get_chunk_list(self): + return self.body["chunk_list"] + + +class DocumentObjectBuilder: + def __init__(self, meta: dict, tags: dict, text: str): + self.meta = meta + self.tags = tags + self.text = text + + def set_meta(self, meta: dict): + self.meta = meta + return self + + def set_text(self, text: str): + self.text = text + return self + + def build(self) -> DocumentObject: + chunk_list = ChunkListWriter.create_chunk_list_from_text( + self.text, + 1024 * 4, + ) + return DocumentObject(self.meta, self.tags, chunk_list) diff --git a/src/knowledge/core_object/email_object.py b/src/knowledge/core_object/email_object.py new file mode 100644 index 0000000..fe5e0d4 --- /dev/null +++ b/src/knowledge/core_object/email_object.py @@ -0,0 +1,47 @@ +from .rich_text_object import RichTextObject, RichTextObjectBuilder +from ..object import ObjectID, ObjectType, KnowledgeObject + + +class EmailObject(KnowledgeObject): + def __init__(self, meta: dict, tags: dict, rich_text: RichTextObject): + desc = dict() + body = dict() + desc["meta"] = meta + desc["tags"] = tags + + # FIXME rich text content store in desc or body? which one is better? + body["content"] = rich_text + + super().__init__(ObjectType.Email, desc, body) + + def get_meta(self): + return self.desc["meta"] + + def get_tags(self): + return self.desc["tags"] + + def get_rich_text(self): + return self.body["rich_text"] + + +class EmailObjectBuilder: + def __init__(self, meta: dict, tags: dict, folder: str): + self.meta = meta + self.tags = tags + self.folder = folder + + def set_meta(self, meta: dict): + self.meta = meta + return self + + def set_tags(self, tags: dict): + self.tags = tags + return self + + def set_folder(self, folder: str): + self.folder = folder + return self + + def build(self) -> EmailObject: + content = RichTextObjectBuilder(self.folder).build() + return EmailObject(self.meta, self.tags, content) diff --git a/src/knowledge/core_object/image_object.py b/src/knowledge/core_object/image_object.py new file mode 100644 index 0000000..d6a0cde --- /dev/null +++ b/src/knowledge/core_object/image_object.py @@ -0,0 +1,89 @@ +from ..object import KnowledgeObject +from ..data import ChunkList, ChunkListWriter +from ..object import ObjectType + + +# desc +# meta +# tags +# hash: "file-hash", +# exif: {} +# body +# chunk_list: [chunk_id, chunk_id, ...] + + +class ImageObject(KnowledgeObject): + def __init__(self, meta: dict, tags: dict, exif: dict, chunk_list: ChunkList): + desc = dict() + body = dict() + desc["meta"] = meta + desc["exif"] = exif + desc + desc["hash"] = chunk_list.hash.to_base58() + body["chunk_list"] = chunk_list.chunk_list + + super().__init__(ObjectType.Image, desc, body) + + def get_meta(self): + return self.desc["meta"] + + def get_exif(self): + return self.desc["exif"] + + def get_tags(self): + return self.desc["tags"] + + def get_hash(self): + return self.desc["hash"] + + def get_chunk_list(self): + return self.body["chunk_list"] + + +from PIL import Image +from PIL.ExifTags import TAGS + + +def get_exif_data(image_path: str): + with Image.open(image_path) as image: + exif_data = image._getexif() + + if exif_data is not None: + return { + TAGS.get(key): exif_data[key] + for key in exif_data.keys() + if key in TAGS and isinstance(exif_data[key], (bytes, str)) + } + else: + return {} + + +class ImageObjectBuilder: + def __init__(self, meta: dict, tags: dict, image_file: str): + self.meta = meta + self.tags = tags + self.image_file = image_file + self.restore_file = False + + def set_meta(self, meta: dict): + self.meta = meta + return self + + def set_tags(self, tags: dict): + self.tags = tags + return self + + def set_image_file(self, image_file: str): + self.image_file = image_file + return self + + def set_restore_file(self, restore_file: bool): + self.restore_file = restore_file + return self + + def build(self) -> ImageObject: + chunk_list = ChunkListWriter.create_chunk_list_from_file( + self.image_file, 1024 * 1024 * 4, self.restore_file + ) + exif = get_exif_data(self.image_file) + return ImageObject(self.meta, self.tags, exif, chunk_list) diff --git a/src/knowledge/core_object/rich_text_object.py b/src/knowledge/core_object/rich_text_object.py new file mode 100644 index 0000000..ac3fcbe --- /dev/null +++ b/src/knowledge/core_object/rich_text_object.py @@ -0,0 +1,80 @@ +from knowledge.object.object_id import ObjectType +from ..object import KnowledgeObject +from ..data import ChunkList, ChunkListWriter +from ..object import ObjectType +from .video_object import VideoObjectBuilder, VideoObject +from .image_object import ImageObjectBuilder, ImageObject +from .document_object import DocumentObjectBuilder, DocumentObject + +class RichTextObject(KnowledgeObject): + def __init__(self, images: dict = {}, videos: dict = {}, documents: dict = {}, rich_texts: dict = {}}): + desc = dict() + desc.images = dict() + desc.videos = dict() + desc.documents = dict() + desc.rich_texts = dict() + + super().__init__(ObjectType.RichText, desc) + + + def add_image_with_key(self, key, image_object: ImageObject): + assert self.desc.images[key] == None + self.desc.images[key] = image_object + + def add_image(self, image_object: ImageObject): + self.desc.images[image_object.object_id()] = image_object + + def get_image_with_key(self, key) -> ImageObject: + return self.desc.images[key] + + def get_images(self) -> dict: + return self.desc.images + + def add_video_with_key(self, key, video_object: VideoObject): + assert self.desc.videos[key] == None + self.desc.videos[key] = video_object + + def add_video(self, video_object: VideoObject): + self.desc.videos[video_object.object_id()] = video_object + + def get_video_with_key(self, key) -> VideoObject: + return self.desc.videos[key] + + def get_videos(self) -> dict: + return self.desc.videos + + + def add_document_with_key(self, key, document_object: DocumentObject): + assert self.desc.documents[key] == None + self.desc.documents[key] = document_object + + def add_document(self, document_object: DocumentObject): + self.desc.documents[document_object.object_id()] = document_object + + def get_document_with_key(self, key) -> DocumentObject: + return self.desc.documents[key] + + def get_documents(self) -> dict: + return self.desc.documents + + def add_rich_text_with_key(self, key, rich_text_object): + assert self.desc.rich_texts[key] == None + self.desc.rich_texts[key] = rich_text_object + + def add_rich_text(self, rich_text_object): + self.desc.rich_texts[rich_text_object.object_id()] = rich_text_object + + def get_rich_text_with_key(self, key): + return self.desc.rich_texts[key] + + def get_rich_texts(self) -> dict: + return self.desc.rich_texts + + +class RichTextObjectBuilder: + def __init__(self, folder: str): + self.folder = folder + + def build(self) -> RichTextObject: + # TODO + return RichTextObject() \ No newline at end of file diff --git a/src/knowledge/core_object/video_object.py b/src/knowledge/core_object/video_object.py new file mode 100644 index 0000000..ab4c262 --- /dev/null +++ b/src/knowledge/core_object/video_object.py @@ -0,0 +1,84 @@ +from ..object import KnowledgeObject +from ..data import ChunkList, ChunkListWriter +from ..object import ObjectType + + +# desc +# meta +# tags +# hash: "file-hash", +# info: {} +# body +# chunk_list: [chunk_id, chunk_id, ...] + + +class VideoObject(KnowledgeObject): + def __init__(self, meta: dict, tags: dict, info: dict, chunk_list: ChunkList): + desc = dict() + body = dict() + desc["meta"] = meta + desc["tags"] = tags + desc["info"] = info + desc["hash"] = chunk_list.hash.to_base58() + body["chunk_list"] = chunk_list.chunk_list + + super().__init__(ObjectType.Video, desc, body) + + def get_meta(self): + return self.desc["meta"] + + def get_tags(self): + return self.desc["tags"] + + def get_info(self): + return self.desc["info"] + + def get_hash(self): + return self.desc["hash"] + + def get_chunk_list(self): + return self.body["chunk_list"] + + +from moviepy.editor import VideoFileClip + + +def get_video_info(video_path: str) -> dict: + clip = VideoFileClip(video_path) + return { + "duration": clip.duration, # Duration in seconds + "fps": clip.fps, # Frames per second + "nframes": clip.reader.nframes, # Total number of frames + "size": clip.size, # Size of the frames (width, height) + } + + +class VideoObjectBuilder: + def __init__(self, meta: dict, tags: dict, video_file: str): + self.meta = meta + self.tags = tags + self.video_file = video_file + self.restore_file = False + + def set_meta(self, meta: dict): + self.meta = meta + return self + + def set_tags(self, tags: dict): + self.tags = tags + return self + + def set_video_file(self, video_file: str): + self.video_file = video_file + return self + + def set_restore_file(self, restore_file: bool): + self.restore_file = restore_file + return self + + def build(self) -> VideoObject: + chunk_list = ChunkListWriter.create_chunk_list_from_file( + self.video_file, 1024 * 1024 * 4, self.restore_file + ) + info = get_video_info(self.video_file) + return VideoObject(self.meta, self.tags, info, chunk_list) diff --git a/src/knowledge/object/__init__.py b/src/knowledge/object/__init__.py index ef1db5c..7886d7a 100644 --- a/src/knowledge/object/__init__.py +++ b/src/knowledge/object/__init__.py @@ -1,4 +1,4 @@ -from .object import EmailObject, ImageObject, TextChunkObject, ObjectID +from .object import KnowledgeObject from .blob import FileBlobStorage from .hash import HashValue, hash_data from .object_store import ObjectStore diff --git a/src/knowledge/object/object.py b/src/knowledge/object/object.py index 0eff535..b3841ed 100644 --- a/src/knowledge/object/object.py +++ b/src/knowledge/object/object.py @@ -2,18 +2,18 @@ # define a object type enum from abc import ABC, abstractmethod from enum import Enum -from .object_id import ObjectID +from .object_id import ObjectID, ObjectType import hashlib import json import pickle class KnowledgeObject(ABC): - def __init__(self, object_type: int, desc: dict = {}, body: dict = {}): + def __init__(self, object_type: ObjectType, desc: dict = {}, body: dict = {}): self.desc = desc self.body = body self.object_type = object_type - def get_object_type(self): + def get_object_type(self) -> ObjectType: return self.object_type def object_id(self) -> ObjectID: @@ -50,30 +50,5 @@ class KnowledgeObject(ABC): @staticmethod def decode(data: bytes): return pickle.loads(data) - - - -# define a text chunk class -class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, text: str): - super().__init__(ObjectType.TextChunk) - self.text = text - - -# define a image class -class ImageObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, meta, path): - super().__init__(ObjectType.Image) - self.meta = meta - self.path = path - - -# define a email class -class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, meta): - super().__init__(ObjectType.Email) - self.meta = meta - self.text = [ObjectID] - self.images = [ObjectID] diff --git a/src/knowledge/object/object_id.py b/src/knowledge/object/object_id.py index 27c0063..3c3ac24 100644 --- a/src/knowledge/object/object_id.py +++ b/src/knowledge/object/object_id.py @@ -10,7 +10,10 @@ class ObjectType(Enum): Chunk = 7 TextChunk = 100 Image = 101 - Email = 102 + Video = 102 + Document = 103 + RichText = 104 + Email = 105 # define a object ID class to identify a object diff --git a/src/knowledge/store.py b/src/knowledge/store.py index a4a0af6..ef697fa 100644 --- a/src/knowledge/store.py +++ b/src/knowledge/store.py @@ -4,7 +4,7 @@ from .data import ChunkStore, ChunkTracker import logging -# KnowledgeStore class, 聚合ChunkStore,ChunkTracker, ObjectStore,并且是一个全局单例,可以方便的使用这三个内置的store示例 +# KnowledgeStore class, which aggregates ChunkStore, ChunkTracker, and ObjectStore, and is a global singleton that makes it easy to use these three built-in store examples class KnowledgeStore: _instance = None From f3cc3f60435880d1e451e8bb6d31780e10f9a245 Mon Sep 17 00:00:00 2001 From: liyaxing Date: Mon, 11 Sep 2023 02:07:01 +0800 Subject: [PATCH 033/177] Update requirements --- .gitignore | 2 ++ src/requirements.txt | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 src/requirements.txt diff --git a/.gitignore b/.gitignore index 4ef429f..948ea70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .vscode/ *.pyc +rootfs/data +*.log diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..811e8e1 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,5 @@ +chromadb==0.4 +openai==0.28 +toml==0.10 +Pillow==10.0 +moviepy==1.0 \ No newline at end of file From 6b45cb95701b570611965472804229d57f7c2016 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 10 Sep 2023 12:01:45 -0700 Subject: [PATCH 034/177] 1.Can use aios_shell from new jarvis telgram bot right now 2.Update MVP plan2.md 3.Refactor function support --- doc/mvp/compute_kernel.png | Bin 0 -> 139279 bytes doc/mvp/mvp plan 2.md | 500 ++++++++++++++++++++++++ src/aios_kernel/ai_function.py | 53 ++- src/aios_kernel/environment.py | 8 +- src/aios_kernel/workflow.py | 6 +- src/aios_kernel/workflow_env.py | 3 + src/service/aios_shell/tg_aios_shell.py | 223 +++++++++++ 7 files changed, 782 insertions(+), 11 deletions(-) create mode 100644 doc/mvp/compute_kernel.png create mode 100644 doc/mvp/mvp plan 2.md create mode 100644 src/service/aios_shell/tg_aios_shell.py diff --git a/doc/mvp/compute_kernel.png b/doc/mvp/compute_kernel.png new file mode 100644 index 0000000000000000000000000000000000000000..f0ddbf180a909a39a41efcd01f1575a2b0cb9ed5 GIT binary patch literal 139279 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%0APw!S$VdE$?Zj0_j3{_vz8#X7^6Om7Y&@B)ty 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 z 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.5.2),A8 +- [ ] Frame Services(0.5.2) + - [ ] Kernel Service + - [ ] System-Call Interface,A2 + - [ ] Name Service,A4 + - [ ] Node Daemon,A2 + - [ ] ACL Control,A4 + - [ ] Contact Manager,A2 + - [ ] Runtime Context (0.5.2),A4 + - [ ] Package System,@waterflier, A2+S4 +- [x] AI Compute System,@waterflier, A2 + - [ ] Scheduler,@streetycat, A2 + - [x] LLM + - [x] GPT4 (Cloud),@waterflier, S1 + - [ ] LLaMa2,@streetycat, A2 + - [ ] Claude2, S2 + - [ ] Falcon2, S2 + - [ ] MPT-7B, S2 + - [ ] Vicuna, S2 + - [ ] Embeding,@photosssa,@lurenpluto , A4 + - [ ] Txt2img,@glen0125,A4 + - [ ] Img2txt(0.5.2),A3 + - [ ] Txt2voice,A3 + - [ ] Voice2txt, @wugren,A3 + - [ ] Language Translate (Pending) +- [x] Storage System + - [ ] DFS (0.5.2),A4 + - [ ] Object Storage, @lurenpluto ,A2 + - [ ] D-RDB (0.5.2),A2 + - [ ] D-VDB,@lurenpluto , A4 +- [ ] Embeding Piplines,@photosssa, A2 +- [ ] Network Gateway,A6 + - [x] NDN Client, @waterflier, A1 +- [ ] Build-in Service + - [x] Spider,@alexsunxl, A2 + - [x] E-mail Spider,@alexsunxl, S2 + - [ ] Telegram Spider,S2 + - [ ] Twitter Spider (0.5.2) + - [ ] Facebook Spider (0.5.2) + - [ ] Agent Message Tunnel (0.5.2) + - [ ] E-mail Tunnel,A2 + - [ ] Telegram Tunnel,S2 + - [ ] Discord Tunnel,S2 + - [ ] Home IoT Environment (0.5.2), A4 + - [] Compatible Home Assistant (0.5.2), A4 +- [ ] Build-in Agents/Apps + - [ ] Agent: Personal Information Assistant,@photosssa,@lurenpluto , A2 + - [ ] Agent: Bulter Jarvis,@waterflier, A2 + - [ ] App: Personal Station (0.5.2),A4+S4 +- [ ] UI + - [x] CLI UI (aios_shell),@waterflier,S2 + - [ ] Web UI (0.5.2),A4+S4 +- [ ] 0.5.1 Integration Test (Senior*3) + - [x] Workflow -> AI Agent -> AI Agent,@waterflier,S1 + - [ ] Spider -> Pipline -> Knowledge Base,@photosssa,S2 + - [ ] AI Agent <- Functions <- Knowledge Base,@lurenpluto,S2 +- [ ] SDK + - [x] Workflow SDK,@waterflier, A2 + - [ ] AI Environments SDK (0.5.2), A2 + - [ ] Compute Kernel SDK (0.5.2), A2 +- [ ] Document (>0.5.2) + - [ ] System design document, including the design document of each subsystem + - [ ] Installation/use document for end users + - [ ] SDK document for developers + + +The following is the introduction of the adjustment of each component after the current implementation. + +# AIOS Kernel + +Define some of the important basic concepts of Intelligent Applications running on OpenDAN + +## Agent + +Agent is the core concept of the system, created through appropriate LLM, prompt words, and memory. Agents support our vision of a new relationship between humans and computation in the future: +``` +Human <-> Agent <-> Compute +``` +Agents form the basis of future intelligent applications. From the user's perspective, the strength of AIOS is primarily determined by "how many agents with different capabilities it has." +The above process has now been implemented. In practice, I found a key issue is that we need to continuously seek the optimal solution. This issue directly relates to how application developers of OpenDAN build intelligent applications, so I think it has a high priority. + +### Optimization of system prompts + +The goal is to allow Agents to communicate with other Agents (forming a team), call Functions at the right time, and read/write status through the environment at the right time, using prompt words. The existing implementation is usually: + +``` +When you decide to communicate with a work group, please use : sendmsg(group_name, content). +``` + +Our optimization direction is: + +1. To allow Agents to initiate calls accurately +2. To use as few precious prompt word resources as possible. + +If there are already systematic studies in this field, introductions are also welcome! + +## Workflow + +Workflow has realized the concept of allowing multiple Agents to play different roles and collaboratively solve complex problems within an organization. It is also the main form of intelligent applications on OpenDAN. Compared to a single Agent, building a team composed of Agents can effectively solve three inherent problems of LLM: + +1. The prompt word window will grow, but it will remain limited for a long time. +2. Like humans, Agents trained on different corpora and algorithms will have different personalities and will excel in different roles. +3. The inference results of LLM are uncontrollable, so accuracy cannot be guaranteed. Just like humans make mistakes, the collaboration of multiple Agents is needed to improve accuracy. + +The basic framework of Workflow has been completed (which is also the core of version 0.5.1). Following the subsequent SDK documentation, we now have a basic framework for third-party developers to develop applications on OpenDAN. + +## AI Environments + +Environments provide an abstraction for AI Agents to access the real world. + +Environments include properties, events, methods (Env.Function), and come with natural language descriptions that Agents can understand. This allows AI Agents to understand the current environment and when to access it. For example, an Agent planning a trip needs to understand the real weather conditions at the destination in the future to make the right decisions. This weather condition needs to be provided to the Agent through Environments. + +The events in Environments also provide logic for the autonomous work of Agents. For example, an Agent can track changes in the user's schedule and date, automatically helping the user plan and track the specific itinerary for the day. + +### Celender Environment + +The system's default environment, which can access the current time, the user's schedule, and the weather information at a specific location. It also contains some important and basic user information, including home address and office address. + +#### Compatible with common Celender services. (0.5.2) + +- Microsoft Outlook Calendar +- Google Calendar +- Apple Calendar + +### Workspace Environment (0.5.2) + +A file system-based workspace environment that allows the Agent to read/write files at appropriate times. + +## AI Functions + +Function is a core concept of AIOS, providing the Agent with descriptions of suitable callable Functions, allowing the Agent to invoke Functions at the right time. Through Functions, the Agent gains "execution power," rather than just being an advisor that only provides suggestions. The Function framework allows third-party developers to develop and publish Functions, supporting Agents and Workflows to have a list of available Functions, through which they can build appropriate prompt words, enabling the Agent to invoke Functions at the right time. + +**Under development.** + +### Basic AI Functions (0.5.2) + +There are already a plethora of basic services in the world, such as querying the weather at a specific location at a specific time, checking hotel prices, or booking plane tickets. The system should separate the definition and implementation of Basic (generic) Functions, allowing Agent developers to implement common scenarios with generic logic. The definition of generic Functions is undoubtedly similar to standard setting work. + + I know that many other projects have done a lot of work in this field, and ChatGPT also has dedicated function support. What we need to do is to find the open standards that are closest to our goals and then integrate them. + +## AI BUS + +The AI BUS connects various conceptual entities of OpenDAN. For example, if Agent A wants to send a message to another Agent B and wait for the processing result of the message, it can simply use the AI BUS: + +```python +resp = await AIBus.send_msg(agentA,agentB,msg) +``` + +The abstraction of AI BUS allows different Agents to choose suitable physical hosts to run according to the system's needs. This is also why we define AIOS as a "Network OS". All entities registered on the AI BUS can be accessed via the AI BUS interface. As needed, we will also persist the messages in the AI BUS, so that when a distributed system experiences regular failures, it can continue to work after being pulled up again. + +The concept of AI BUS has many similarities with traditional MessageQueues. + +## Chatsession + +Intuitively, ChatSession saves the "chat history". The chat history is currently the natural source of Agent Memory capability. +Determining a ChatSession has three key attributes: Owner, Topic, and Remote. An operation where A sends a message to B and gets B's reply will generate two messages, and save them in two different ChatSessions. + +Currently, ChatSession is saved based on sqlite. After the Zone-level D-RDB is set up in the future, it will be migrated to RD-DB. + +## Knowlege Base + +Provide a unified interface, support switching vector database kernel +Integrate open source vector database (pay attention to Lience selection) +When designing the interface, prepare for future access control + +**Under development.** + +## Personal Models (>0.5.2) + +The goal of this subsystem is to support users in training models based on their own data, including subsequent usage, management, deployment, and other operations of the model. In the early stages, invoking this module and adding new models should be operations performed by advanced users. + +It is still uncertain whether this module will be actively used in intelligent applications. + +# Frame Services + +The implementation offers a range of fundamental services for traditional Network OS. It connects users' devices to the same Zone via the network and provides a unified abstraction for application access. This component serves as a basic framework and computing resource for the operation of intelligent applications on the upper layer. On the lower layer, it connects various types of hardware through different protocols, integrates resources, and offers a unified abstraction for intelligent applications to access. + +## Kernel Service (0.5.2) + +The Kernel Service implements the System Calls for OpenDAN and provides a "kernel mode" abstraction. In version 0.5.1, since this component is not yet implemented, all code—whether system services or application code—runs in kernel mode. + +In the future, we plan to maintain the system running in this mode for an extended period, as it facilitates debugging. + +The Kernel Service is mainly composed of the following component: + +### System-Call Interface + +Centralizes the provision and management of system call interfaces. + +### Name Service + +It is the most crucial foundational state service in a cluster (Zone) comprised of all the user's devices. As the core service of the Zone, it provides the most basic guarantee for the availability and reliability of all services within the Zone. When a user needs to restore the Zone from a backup, the Name Service is the first service to be restored. + +Its functionality is similar to that of `etcd` but includes a on-chain component. From a deployment standpoint, it needs to be operationally optimized for small clusters made up of consumer-level user devices. + +### Node Daemon + +It is a foundational service that runs on all devices that join the Zone, responding to essential kernel scheduling commands. It adjusts the services and data running on that particular device. + +### ACL Control (>0.5.2) + +Another essential foundational service of the kernel, it is responsible for the overall management of permissions related to users, applications, and data. The Runtime Context reads the relevant information and implements proper isolation. + +### Contact Manager + +From the perspectives of permission control and some early application scenarios, understanding the user's basic interpersonal relationships is an important component of OpenDAN's intelligent permission system. Therefore, we provide a contact management component at the system kernel layer. This component can be considered an upgraded version of the traditional operating system's "User Group" module. + +## Runtime Context (0.5.2) + +It serves as the runtime container for user-mode code, offering isolation guarantees for user-mode code. + + Depending on the type of service, we offer three different Runtime Contexts. The most commonly used is Docker, followed by virtual machines, and finally, entire physical machines. + +## Package System + +The Package Manager is a fundamental component of the system for managing Packages. The sub system provides fundamental support for packaging, publishing, downloading, verifying, installing, and loading folders containing required packages under different scenarios. Based on relevant modules, it's easy to build a package management system similar to apt/pip/npm. + +The system design has deeply referenced Git and NDN networks. The distinction between client and server is not that important. Through cryptography, it achieves decentralized trustworthy verification. Any client can become an effective repo server through simple configuration. + +Based on the Package System, we can implement the publishing, downloading, and installation of extendable foundational entities such as Agents, Functions, and Environments. This enables the creation of an app store on OpenDAN. + +**Under development.** + +# AI Compute System + +The purpose of designing Compute System is to enable our users to use their computational resources more efficiently. These computational resources can come from devices they own (such as their workstations and gaming laptops), as well as from cloud computing and decentralized computing networks. + +[![compute kernel design](./compute_kernel.png)](compute_task.drawio) + +The interface of this component is designed from the perspective of the model user rather than the model trainer. The basic form of its interface is: + +```python +compute_kernel.do_compute(function_name, model_name,args) +``` + +## Scheduler + +The goal of the Scheduler component is to select an appropriate ComputeNode to run tasks based on the tasks in the task queue and the known status of all ComputeNodes (which may be delayed). In the current version (0.5.1), the implementation of the Scheduler is only to get the system up and running. In the next version (0.5.2), the overall framework for computing resource scheduling needs to be established. + +## LLM + +LLM support is the system's most core functionality. OpenDAN requires that there be at least one available LLM computing node in the system. The supported interfaces are as follows: +``` +def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0): +``` +In the current era, many teams are working hard to develop new LLMs . We will also actively integrate these LLMs into OpenDAN. + +- [x] GPT4 (Cloud) +- [ ] LLaMa2 **Under development.** +- [ ] Claude2 +- [ ] Falcon2 +- [ ] MPT-7B +- [ ] Vicuna + + +## Embeding + +Provides computational support for the vectorization of different types of user data. The specific algorithms supported depend on the requirements of the entire pipeline. + +***Under development.*** + +## Txt2img + +Generate images based on text descriptions. According to the implementation mode, we can interface with a cloud-based implementation and a local implementation. + +The local implementation will definitely use Stable Diffusion. + +***Under development.*** + +## Img2txt (>0.5.2) + +Generate appropriate text descriptions for the specified images. + +## Txt2voice + +Generate voice based on specified text, using a selected model (the focus is on personal models), and guided by certain emotional cue words. + +***To be developed*** + +## Voice2txt + +Extract text information from a segment of audio (or video) through speech recognition. + +***To be developed*** + +## Language Translate + +Translate a segment of text into a specified target language. + +Since LLM itself is developed based on the foundation of translation, I am currently considering whether it is necessary to provide a text translation interface within the computing kernel. Following the principle of not adding entities if they are not needed, it can be postponed from development. +***pending*** + +# Storage System + +The file system (state storage) has always been a critical part of operating systems. Its implementation directly impacts the system's reliability and performance. The challenge of this section is how to transfer key technologies that are already mature in traditional cloud computing to clusters composed of consumer-level electronic devices with low operational maintenance, while still maintaining sufficient reliability and performance. The implementation of the subsystems in this section is of limited stability. Therefore, I believe the focus of OpenDAN in the early stages for this section should be on establishing stable interfaces to get the system running as quickly as possible, with independent improvements to be made in the future. + +From the standpoint of trade-offs, our priorities are: + +- Abandoning continuous consistency guarantees, the system only provides strong assurance for reliability up to "backup points." This means we allow the loss of some newly added data if the system experiences a failure. + +- Allowing downtime, considering the consumer-level power supply, a short period of unavailability of the system itself will not have a significant impact. We can stop the service for backup/migration when necessary. + +## DFS + +Distributed file system, combining the public storage space on all devices to form a highly reliable, highly available file system. + +## Object Storage + +Distributed object storage, and based on MapObject, it implements trustworthy RootState management. + +(MapObject and RootState is a concept from CYFS) + +**Under development.** + +## D-RDB + +Distributed relational database, providing highly reliable and highly available relational database services (mainly used for OLTP - Online Transaction Processing). We do not encourage application developers to use RDB on a large scale; the main reason for offering this component is for compatibility considerations. + +***Pending.*** + +## D-VDB + +Distributed vector database, which currently appears to be the core foundational component of the Knowledge Base library. + +***Under development.*** + +# Embeding Piplines + +Read appropriate Raw Files and Meta Data from the specified location in the Storage System. After passing through a series of Embedding Pipelines, save the results to the Vector Database as defined by the Knowledge Base. + +***Under development.*** + +# Network Gateway (0.5.2) + +Obtain user data by recognizing network data. +The Gateway also provides an external access entrance for the entire system, and access control can be unified. +Provides the bus abstraction in the network operating system (the network cable is the bus), devices within the Zone are recognized by the system as plug-and-play devices, and can be called by applications/Agents + +## NDN Client + +AI-related models are all quite large, so we offer a download tool based on NDN (Named Data Networking) theory to replace curl. The NDN Client will continue to support new Content-Based protocols in the future, allowing OpenDAN developers to publish large packages more quickly, at lower costs, and more conveniently. + +# Build-in Service + +The basic functions of the system implemented by "user mode" can be regarded as pre -installed applications of the system.Let the system have basic availability without installing any intelligent applications. +We should build built-in applications for 1,2 early preset scenarios, rather than all possible scenarios.This allows us to run the system faster and allow us to discover the shortcomings of the system faster, so as to improve the system faster. + +## Spider + +A series of reptiles are provided to help users import their data into the system. + +### E-mail Spider + +The most basic spider is used to capture user mail data.The main purpose of this is to determine the general data format(include text,image,contact) and location to save the grabbed data. + +### Telegram Spider + +Allow users to capture their own Telegram chat records and save them in the Knowlege Base + +**To be developed.** + +### Twitter Spider (0.5.2) + +Allows users to scrape their own Twitter data and save it in the Knowledge Base. + +### Facebook Spider (0.5.2) + +Allows users to scrape their own Facebook data and save it in the Knowledge Base. + +## Agent Message Tunnel (0.5.2) + +The original ROBOT module, after considering its actual function, was renamed the Agent Message Tunnel. +This is the default function supported by the system. It supports users to configure different message channels for different Agent/Workflow, so that users can interact with Agent/Worflow through existing software/services.From the perspective of product, the goal of this module can use the core function of OpenDAN without installing any new software on the one hand. On the other hand, it also creates a stronger mental model for users: My Agent can registered social account, so that Agent has his own identity in the virtual world. + +### E-mail Tunnel + +Let Agent have its own email account. After registration, users can interact with Agent through mail. + +### Telegram Tunnel + +Let Agent have his own Telegram account. After registration, users can interact with Agent through Telegram. + +### Discord Tunnel + +Let Agent have its own discord account. After registration, users can interact with agent through Discord. + +## Home IoT Environment (0.5.2) + +We've implemented a significant built-in environment: the Home Environment. Through this environment, the AI Agent can access real-time status of the home via installed IoT devices, including reading temperature and humidity information, accessing security camera data, and controlling smart devices in the home. This allows users to better manage a large number of smart IoT devices through AI technology. For instance, a user can simply tell the Agent, "Richard is coming over to watch a movie this afternoon," and the AI Agent will automatically read the security camera data, recognize Richard upon arrival, turn on the home projector, close the curtains, and turn on the wall lights. + +Thanks to LLM's powerful natural language understanding, all we need to do is connect a smart microphone to the Home Environment and configure a simple voice-to-text feature. This makes it easy to implement a privately deployed and very intelligent version of Alexa. + +In terms of system design, we use the Home Environment as an intermediary layer, freeing OpenDAN from having to spend energy on dealing with compatibility issues with various existing, complex IoT protocols. This keeps the system simple and makes it easier to expand. + +### Compatible Home Assistant + +Home Assistant is a well-known, open-source IoT system. We could consider implementing the Home Environment based on the Home Assistant's API. + +# Build-in Agents/Apps + +Once users have installed OpenDAN, it should have some basic functionalities, even without the installation of any third-party smart applications. These basic functions are provided via built-in Agents/Applications. Built-in applications have two important implications for OpenDAN: + +1. They provide a developer's perspective to scrutinize whether our design is reasonable and the application development process is smooth. +2. Through one or two scenarios, OpenDAN can be quickly put into use by real users in a production environment, and these scenarios can serve as a basis for driving system improvements in OpenDAN. + +## Agent: Personal Information Assistant + +Through interacting with this Agent, users can use natural language to query information that has already been saved in the Knowledge-Base. For example, "Please show me the photos from my meeting with Richard last week." They can also find their information more accurately based on some interactive questions. + +***To be developed.*** + +## Agent: Bulter Jarvis (0.5.2) + +The Butler Agent Jarvis can recognize certain special commands. Through these commands, it can communicate with other Agents in the system, check the system's status, and use all the system's functionalities. It can be seen as another entry point to AIOS_Shell. + +Another important function of the Jarvis is to create sessions. When a user has many workflows/agents installed on their OpenDAN, they might not know which workflow/agent to talk to in order to solve a problem. I envision the future mode to be: "If you don't know who to turn to, ask the Jarvis." The Jarvis will create or find a suitable session based on a brief conversation with the user, and then guide the user into this session. + +Based on these two functions, the Jarvis might be the only "special Agent" that requires custom development among all Agents, and it is a part of the system. + +## App: Personal Station (0.5.2) + +The Personal Station is a built-in application that provides a graphical user interface for users to interact with the system. It is a web application that can be accessed through a browser. It is also the first application that users will see after installing OpenDAN. It provides a simple interface for users to interact with the system, and it also provides a way for users to install new applications. + +The main functions of Personal Station include: + +1. Library, with the help of Personal Information Assistant, you can better manage your own photos, videos, music, documents, etc., and share them with friends more effectively. (For example, ask the assistant to share photos from an event, selecting from those you've starred, and distribute them to friends based on the people appearing in the photos.) +2. HomePage, with functions similar to Facebook/Twitter, where you can post content you want to share. You can also open your Agent to friends and family, allowing them to interact with your Agent, discuss schedule arrangements, and query your KnowledgeBase for open content. + +Home Station is a mobile-first WebApp. + +# UI + +## CLI UI (aios_shell) + +The system provides the command line UI interface priority, facing developers and early senior users. + +## Web UI (0.5.2) + +Web UI interface for end users + +# 0.5.1 Integration Test (Senior*3) + +Can be divided into 3 parts +1.Workflow -> AI Agent -> AI Agent +2.Spider -> Pipline -> Knowledge Base +3.AI Agent <- Functions <- Knowledge Base + + +# SDK + +## Workflow SDK + +The SDK allows developers to expand the new workflow/agent to the system. +At present, the SDK has completed the most original version. In ROOTFS/, the .tmol file is written according to the directory structure, and a new workflow/ agent can be added to the system. + +## AI Environments SDK (>0.5.2) + +The SDK allows developers to expand the system that can be called by AI, including +- Expand the new environment +- Expand the new function + +## Compute Kernel SDK (>0.5.2) + +This SDK allows developers to expand more core capabilities to the system + +# Document (>0.5.2) + +When we release 0.5.3, we must complete at least 3 documents: + +1. OpenDan's complete system design document, including the design document of each subsystem. +2. Installation/use document for end users. +3. SDK document for developers. diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py index 74cbdcc..e830672 100644 --- a/src/aios_kernel/ai_function.py +++ b/src/aios_kernel/ai_function.py @@ -1,25 +1,62 @@ +from abc import ABC, abstractmethod +from typing import Dict + class AIFunction: def __init__(self) -> None: self.intro : 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 + """ + pass + + @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 + """ + pass + + @abstractmethod + def execute(self, **kwargs) -> Dict: + """ + 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: diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 9979e2c..4e064bf 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -48,6 +48,12 @@ class Environment: def _do_get_value(self,key:str) -> Optional[str]: pass + def get_functions(self): + # system functions + # env functions + # user install functions + pass + def register_get_handler(self,key:str,handler:Callable) -> None: h = self.get_handlers.get(key) if h is not None: @@ -78,7 +84,7 @@ class Environment: 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") + logger.debug(f"fire event {event_id} in env {self.env_id}:handler not found") return def __getitem__(self, key): diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 69f39c3..2a8e823 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -1,4 +1,3 @@ - import logging import asyncio import json @@ -412,7 +411,10 @@ class Workflow: def _get_function_prompt(self,role_name:str) -> AgentPrompt: - pass + # system functions + # env functions + # user install functions + pass def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt: pass diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 9ef00cb..864f4fb 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -134,3 +134,6 @@ class WorkflowEnvironment(Environment): except Error as e: logging.error(f"Error occurred while update env{self.env_id}.{key} ,error:{e}") + def get_functions(self): + pass + diff --git a/src/service/aios_shell/tg_aios_shell.py b/src/service/aios_shell/tg_aios_shell.py new file mode 100644 index 0000000..98844ae --- /dev/null +++ b/src/service/aios_shell/tg_aios_shell.py @@ -0,0 +1,223 @@ + +import asyncio +import sys +import os +import logging +import re + +from typing import Any, Optional, TypeVar, Tuple, Sequence +import argparse + +from telegram import ForceReply, Update +from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters + + +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 +from aios_kernel import CalenderEnvironment,Environment + +import logging + +# Enable logging +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO +) +# set higher logging level for httpx to avoid all GET and POST requests being logged +logging.getLogger("httpx").setLevel(logging.WARNING) + +logger = logging.getLogger(__name__) + +class AIOS_Shell: + def __init__(self,username:str) -> None: + self.username = username + self.current_target = "_" + self.current_topic = "default" + + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: + target_id = msg.target.split(".")[0] + agent : AIAgent = await AgentManager().get(target_id) + if agent is not None: + bus.register_message_handler(target_id,agent._process_msg) + return True + + a_workflow = await WorkflowManager().get_workflow(target_id) + if a_workflow is not None: + 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) + if agent is not None: + return True + else: + return False + + async def initial(self) -> bool: + cal_env = CalenderEnvironment("calender") + cal_env.start() + Environment.set_env_by_id("calender",cal_env) + + 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) + AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) + return True + + + def get_version(self) -> str: + return "0.0.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) + if resp is not None: + return resp.body + else: + return "error!" + + async def install_workflow(self,workflow_id:Workflow) -> None: + pass + + async def call_func(self,func_name, args): + match func_name: + case 'send': + target_id = args[0] + msg_content = args[1] + topic = args[2] + resp = await self.send_msg(msg_content,target_id,topic,self.username) + + + return resp + case 'open': + if len(args) >= 1: + target_id = args[0] + if len(args) >= 2: + topic = args[1] + + self.current_target = target_id + self.current_topic = topic + show_text = f"current session switch to {topic}@{target_id}" + return show_text + + + case 'history': + num = 10 + offset = 0 + 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 + else: + db_path = WorkflowManager().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) + result_str = "" + for msg in reversed(msgs): + result_str += f"{msg.sender} >>> {msg.body}" + result_str += f"\n-------------------\n" + return result_str + return f"chatsession not found" + case 'help': + return "help info " + +shell:AIOS_Shell = None + +async def _init_shell(): + global shell + if shell is None: + shell = AIOS_Shell("user") + await shell.initial() + return + +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 start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Send a message when the command /start is issued.""" + await _init_shell() + user = update.effective_user + await update.message.reply_html( + rf"Hi {user.mention_html()}!", + reply_markup=ForceReply(selective=True), + ) + + +async def on_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await _init_shell() + cmd_msg = update.message.text[1:] + logger.info("command:" + cmd_msg) + func_call = parse_function_call(cmd_msg) + if func_call: + shell.username = update.effective_user + show_text = await shell.call_func(func_call[0], func_call[1]) + await update.message.reply_text(show_text) + else: + await update.message.reply_text("command not found!") + +async def on_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + await _init_shell() + shell.username = update.effective_user + resp = await shell.send_msg(update.message.text,shell.current_target,shell.current_topic,shell.username) + await update.message.reply_text(resp) + + + + +def main() -> None: + """Start the bot.""" + # Create the Application and pass it your bot's token. + tg_robot_token = os.getenv("TG_ROBOT_TOKEN") + if tg_robot_token is None: + print("TG_ROBOT_TOKEN not found!") + tg_robot_token = input("input your telegram robot token:") + + + if tg_robot_token is None: + return + + application = Application.builder().token(tg_robot_token).build() + + # on different commands - answer in Telegram + application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("help", on_command)) + application.add_handler(CommandHandler("send", on_command)) + application.add_handler(CommandHandler("open", on_command)) + application.add_handler(CommandHandler("history", on_command)) + + # on non command i.e message - echo the message on Telegram + application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_message)) + + # Run the bot until the user presses Ctrl-C + application.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main() \ No newline at end of file From 7e1f7173b43a963d27ee65dbf7c0defafd10e75f Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 10 Sep 2023 20:50:37 -0700 Subject: [PATCH 035/177] Complete AI Function support for Agent. --- doc/mvp/mvp plan 2.md | 4 +- src/aios_kernel/agent.py | 62 +++++++++++++++++++++++++--- src/aios_kernel/ai_function.py | 53 +++++++++++++++++++++--- src/aios_kernel/compute_kernel.py | 11 +++-- src/aios_kernel/compute_task.py | 16 ++++--- src/aios_kernel/environment.py | 22 ++++++++++ src/aios_kernel/open_ai_node.py | 29 ++++++++----- src/aios_kernel/workflow.py | 4 +- src/aios_kernel/workflow_env.py | 10 ++++- src/service/aios_shell/aios_shell.py | 1 + 10 files changed, 173 insertions(+), 39 deletions(-) diff --git a/doc/mvp/mvp plan 2.md b/doc/mvp/mvp plan 2.md index af01fd9..2623120 100644 --- a/doc/mvp/mvp plan 2.md +++ b/doc/mvp/mvp plan 2.md @@ -46,7 +46,7 @@ The previous plan, please see here: [MVP Plan](./mvp%20plan.md) - [ ] MPT-7B, S2 - [ ] Vicuna, S2 - [ ] Embeding,@photosssa,@lurenpluto , A4 - - [ ] Txt2img,@glen0125,A4 + - [x] Txt2img,@glen0125,A4 - [ ] Img2txt(0.5.2),A3 - [ ] Txt2voice,A3 - [ ] Voice2txt, @wugren,A3 @@ -70,7 +70,7 @@ The previous plan, please see here: [MVP Plan](./mvp%20plan.md) - [ ] Telegram Tunnel,S2 - [ ] Discord Tunnel,S2 - [ ] Home IoT Environment (0.5.2), A4 - - [] Compatible Home Assistant (0.5.2), A4 + - [ ] Compatible Home Assistant (0.5.2), A4 - [ ] Build-in Agents/Apps - [ ] Agent: Personal Information Assistant,@photosssa,@lurenpluto , A2 - [ ] Agent: Bulter Jarvis,@waterflier, A2 diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 2680695..0a3f8b0 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -5,9 +5,13 @@ import asyncio import logging import uuid import time +import json from .agent_message import AgentMsg from .chatsession import AIChatSession +from .compute_task import ComputeTaskResult +from .ai_function import AIFunction +from .environment import Environment logger = logging.getLogger(__name__) @@ -77,6 +81,7 @@ class AIAgent: self.chat_db = None self.unread_msg = Queue() # msg from other agent + self.owner_env : Environment = None @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -123,25 +128,70 @@ 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,msg_prompt:AgentPrompt) -> 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" + + result_str:str = await func_node.execute(**arguments) + inner_functions = self._get_inner_functions() + msg_prompt.messages.append({"role":"function","content":result_str,"name":func_name}) + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(msg_prompt,self.llm_model_name,self.max_token_size,inner_functions) + + 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,msg_prompt) + 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) 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().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: + final_result = await self._execute_func(inner_func_call_node,msg_prompt) + + result_type : str = self._get_llm_result_type(final_result) is_ignore = False match result_type: diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py index e830672..08738ab 100644 --- a/src/aios_kernel/ai_function.py +++ b/src/aios_kernel/ai_function.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod -from typing import Dict +from typing import Dict,Coroutine,Callable class AIFunction: def __init__(self) -> None: - self.intro : str = None + self.description : str = None @abstractmethod def get_name(self) -> str: @@ -17,7 +17,7 @@ class AIFunction: """ return a detailed description of what the function does """ - pass + return self.description @abstractmethod def get_parameters(self) -> Dict: @@ -25,11 +25,23 @@ class AIFunction: 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 - def execute(self, **kwargs) -> Dict: + async def execute(self, **kwargs) -> str: """ Execute the function and return a JSON serializable dict. The parameters are passed in the form of kwargs @@ -66,4 +78,35 @@ class CallChain: pass async def execute(self): - pass \ No newline at end of file + 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 + diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 2501ad6..5fea958 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -54,7 +54,6 @@ class ComputeKernel: 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) @@ -91,16 +90,16 @@ class ComputeKernel: 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 @@ -120,6 +119,6 @@ class ComputeKernel: await asyncio.create_task(check_timer()) if task_req.state == ComputeTaskState.DONE: - return task_req.result.result_str + return task_req.result return "error!" diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index 086d946..0edd3cf 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -11,7 +11,6 @@ class ComputeTaskState(Enum): ERROR = 3 PENDING = 4 - class ComputeTaskType(Enum): NONE = -1 LLM_COMPLETION = 0 @@ -36,7 +35,7 @@ class ComputeTask: self.result = None self.error_str = None - def set_llm_params(self, prompts, model_name, max_token_size, callchain_id=None): + 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 @@ -46,7 +45,13 @@ class ComputeTask: self.params["model_name"] = model_name 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 display(self) -> str: return f"ComputeTask: {self.task_id} {self.task_type} {self.state}" @@ -59,9 +64,8 @@ class ComputeTaskResult: self.callchain_id: str = None self.worker_id: str = None self.result_code: int = 0 - self.result_str: str = None - - self.result: dict = {} + 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 diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 4e064bf..3a11d3b 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -5,6 +5,8 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Optional,Dict,Awaitable,List import logging +from .ai_function import AIFunction + logger = logging.getLogger(__name__) class EnvironmentEvent(ABC): @@ -33,6 +35,8 @@ class 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 @@ -44,6 +48,24 @@ class Environment: #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 diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 06e0f09..1be47fd 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -56,28 +56,35 @@ class OpenAI_ComputeNode(ComputeNode): logger.info(f"call openai {mode_name} prompts: {prompts}") resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, - max_tokens=4000, - temperature=1.2) + 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}") - - 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) + + 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 = resp["choices"][0]["message"] + result.result_message = resp["choices"][0]["message"] return result def start(self): 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) diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 2a8e823..0a01c29 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -13,6 +13,7 @@ from .chatsession import AIChatSession from .role import AIRole,AIRoleGroup from .ai_function import CallChain from .compute_kernel import ComputeKernel +from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState from .bus import AIBus from .workflow_env import WorkflowEnvironment @@ -354,7 +355,8 @@ class Workflow: 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()) + task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + result_str = task_result.result_str result = Workflow.prase_llm_result(result_str) logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}") for postmsg in result.post_msgs: diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 864f4fb..9ca2ee3 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -7,9 +7,11 @@ 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__() @@ -17,7 +19,7 @@ class CalenderEvent(EnvironmentEvent): self.data = data def display(self) -> str: - return f"#event timer:{self.event_data}" + 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): @@ -25,6 +27,10 @@ class CalenderEnvironment(Environment): 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 @@ -52,7 +58,7 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - def get_now(self,key:str) -> str: + async def get_now(self) -> str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 053c5ec..27f87e4 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -44,6 +44,7 @@ class AIOS_Shell: target_id = msg.target.split(".")[0] agent : AIAgent = await AgentManager().get(target_id) if agent is not None: + agent.owner_env = Environment.get_env_by_id("calender") bus.register_message_handler(target_id,agent._process_msg) return True From 58c0d74ca131c8c505aed52dc73102b4fa166370 Mon Sep 17 00:00:00 2001 From: liyaxing Date: Mon, 11 Sep 2023 17:30:06 +0800 Subject: [PATCH 036/177] Add chunk reader impl --- src/knowledge/data/__init__.py | 1 + src/knowledge/data/chunk_store.py | 4 +- src/knowledge/data/reader.py | 85 +++++++++++++++++++++++++++++++ src/knowledge/data/tracker.py | 6 +-- src/requirements.txt | 4 +- 5 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/knowledge/data/reader.py diff --git a/src/knowledge/data/__init__.py b/src/knowledge/data/__init__.py index 2b391c9..3f470f0 100644 --- a/src/knowledge/data/__init__.py +++ b/src/knowledge/data/__init__.py @@ -3,3 +3,4 @@ from .tracker import ChunkTracker from .chunk_store import ChunkStore from .writer import ChunkListWriter from .chunk_list import ChunkList +from .reader import ChunkReader, Chunk \ No newline at end of file diff --git a/src/knowledge/data/chunk_store.py b/src/knowledge/data/chunk_store.py index d06eb38..666b027 100644 --- a/src/knowledge/data/chunk_store.py +++ b/src/knowledge/data/chunk_store.py @@ -22,5 +22,7 @@ class ChunkStore: def delete_chunk(self, chunk_id: ChunkID): self.blob.delete(chunk_id) - + + def get_chunk_file_path(self, chunk_id: ChunkID) -> str: + return self.blob.get_full_path(chunk_id, False) \ No newline at end of file diff --git a/src/knowledge/data/reader.py b/src/knowledge/data/reader.py new file mode 100644 index 0000000..c4cbb96 --- /dev/null +++ b/src/knowledge/data/reader.py @@ -0,0 +1,85 @@ +from .chunk import ChunkID, PositionType, PositionFileRange +from .chunk_store import ChunkStore +from .tracker import ChunkTracker +from ..object import HashValue +import logging +from typing import List +import hashlib + +class Chunk: + def __init__(self, file_path: str, range_start: int, size: int = -1): + self.file_path = file_path + self.range_start = range_start + self.size = size + + def read(self): + with open(self.file_path, 'rb') as f: + f.seek(self.range_start) + return f.read(self.size) + + + +class ChunkReader: + def __init__(self, chunk_store: ChunkStore, chunk_tracker: ChunkTracker): + self.chunk_store = chunk_store + self.chunk_tracker = chunk_tracker + + def get_chunk(self, chunk_id: ChunkID) -> Chunk: + positions = self.chunk_tracker.get_position(chunk_id) + if positions is None: + logging.warning(f"chunk not found: {chunk_id}") + return None + + if len(positions) == 0: + logging.warning(f"chunk not found: {chunk_id}") + return None + + for pos in positions: + [position, position_type] = pos + logging.info(f"chunk position: {chunk_id}, {position}, {position_type}") + if position_type == PositionType.ChunkStore: + file_path = self.chunk_store.get_chunk_file_path(chunk_id) + return Chunk(file_path, 0, -1) + elif position_type == PositionType.File: + return Chunk(position, 0, -1) + elif position_type == PositionType.FileRange: + file_range = PositionFileRange.decode(position) + return Chunk(file_range.path, file_range.range_begin, file_range.range_end - file_range.range_begin) + else: + raise ValueError(f"invalid position type: {position_type}") + + logging.error(f"chunk not found: {chunk_id}") + return None + + def get_chunk_list(self, chunk_list: List[ChunkID]) -> List[Chunk]: + return [self.get_chunk(chunk_id) for chunk_id in chunk_list] + + def read_chunk_list(self, chunk_ids: List[ChunkID]): + for chunk_id in chunk_ids: + chunk = self.get_chunk(chunk_id) + if chunk is None: + raise ValueError(f"chunk not found: {chunk_id}") + + yield from chunk.read() + + def read_text_chunk_list(self, chunk_ids: List[ChunkID]): + for chunk_id in chunk_ids: + chunk = self.get_chunk(chunk_id) + if chunk is None: + raise ValueError(f"text chunk not found: {chunk_id}") + + yield chunk.read().decode("utf-8") + + def calc_file_hash(self, file_path: str) -> HashValue: + hash_obj = hashlib.sha256() + with open(file_path, "rb") as file: + while True: + chunk = file.read(1024 * 1024) + if not chunk: + break + hash_obj.update(chunk) + return HashValue(hash_obj.digest()) + + def calc_text_hash(self, text: str) -> HashValue: + hash_obj = hashlib.sha256() + hash_obj.update(text.encode("utf-8")) \ No newline at end of file diff --git a/src/knowledge/data/tracker.py b/src/knowledge/data/tracker.py index 2905d11..57daa38 100644 --- a/src/knowledge/data/tracker.py +++ b/src/knowledge/data/tracker.py @@ -3,7 +3,7 @@ import time import logging import os from .chunk import ChunkID, PositionType, PositionFileRange - +from typing import List class ChunkTracker: def __init__(self, root_dir: str): @@ -61,11 +61,11 @@ class ChunkTracker: ) self.conn.commit() - def get_position(self, chunk_id: ChunkID) -> (str, PositionType): + def get_position(self, chunk_id: ChunkID) -> List[(str, PositionType)]: self.cursor.execute( """ SELECT pos, pos_type FROM chunks WHERE id = ? """, (str(chunk_id),), ) - return self.cursor.fetchone() + return self.cursor.fetchmany() diff --git a/src/requirements.txt b/src/requirements.txt index 811e8e1..1b90399 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -2,4 +2,6 @@ chromadb==0.4 openai==0.28 toml==0.10 Pillow==10.0 -moviepy==1.0 \ No newline at end of file +moviepy==1.0 +base58==2.1 +base36==0.1 \ No newline at end of file From 8ffc61f9c5358668962da8d63d8186c25cf376ef Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Mon, 11 Sep 2023 18:49:56 +0800 Subject: [PATCH 037/177] Save email attachment files in folders --- src/service/spider/email_spider.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py index ae6e484..9f744ad 100644 --- a/src/service/spider/email_spider.py +++ b/src/service/spider/email_spider.py @@ -77,6 +77,29 @@ def check_email_saved(client: imaplib.IMAP4_SSL, uid: str): return False +def save_email_attachment(mail: mailparser.MailParser, email_dir: str): + # Traverse all attachments in the email + 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) + # get original image name + filename = attachment['filename'] + filefullname = f"{img_dir}/{filename}" + # image bytes data + image_data = attachment['payload'] + try: + # decode base64 image data + 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) + logger.info(f"save email image {filename} success") + + # save email to local file by each folder def save_email(mail: mailparser.MailParser): # create email account dir @@ -98,6 +121,8 @@ def save_email(mail: mailparser.MailParser): del mail_dict['body'] json.dump(mail_dict, f, ensure_ascii=False, indent=4) logger.info(f"save email meta info {f.name}") + # save email attachment( like image) + save_email_attachment(mail, email_dir) From c6e18f96a906802feefe95d64b449c7f284da605 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Mon, 11 Sep 2023 19:58:16 +0800 Subject: [PATCH 038/177] Change the code style to the class style --- src/service/spider/email_spider.py | 212 ++++++++++++++--------------- 1 file changed, 100 insertions(+), 112 deletions(-) diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py index 9f744ad..a2a1154 100644 --- a/src/service/spider/email_spider.py +++ b/src/service/spider/email_spider.py @@ -5,130 +5,118 @@ import logging import mailparser import hashlib import json +import base64 -# logger config -logger = logging.getLogger('email spider') -logger.setLevel(logging.DEBUG) -ch = logging.StreamHandler() -formatter = logging.Formatter('%(asctime)s [%(name)s] [%(levelname)s] %(message)s') -ch.setFormatter(formatter) -logger.addHandler(ch) +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) -config = toml.load('./rootfs/email/config.toml') -if os.path.exists('./rootfs/email/config.local.toml'): - config = toml.load('./rootfs/email/config.local.toml') + # 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() -# create email client -def email_client() -> imaplib.IMAP4_SSL: - logger.info(f"read email config from {config.get('EMAIL_IMAP_SERVER')}") - client = imaplib.IMAP4_SSL(config.get('EMAIL_IMAP_SERVER')) - client.login(config.get('EMAIL_ADDRESS'), config.get('EMAIL_PASSWORD')) - return 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(mail: imaplib.IMAP4_SSL): - _, mailbox_list = mail.list() - for mailbox in mailbox_list: - print(mailbox.decode()) - -def read_emails(client: imaplib.IMAP4_SSL, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"): - client.select(folder) - _, data = client.uid('search', None, imap_keyword) - # get email uid list - email_list = data[0].split() - logger.info(f"got {len(email_list)} emails") - for uid in email_list: - logger.info(f"read email uid {uid}") - if check_email_saved(client, uid): - logger.info(f"email uid {uid} already saved") - continue - else: - read_and_save_email(client, uid) - logger.info(f"email uid {uid} saved") + 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") + 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(client: imaplib.IMAP4_SSL, uid: str): - message_parts = "(BODY.PEEK[])" - _, email_data = client.uid('fetch', uid, message_parts) - mail = mailparser.parse_from_bytes(email_data[0][1]) - logger.info(f"got email subject [{mail.subject}]") - save_email(mail) + 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(mail: mailparser.MailParser) -> str: - dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" - name = f"{mail.subject}__{mail.date}" - name = hashlib.md5(name.encode('utf-8')).hexdigest() - return f"{dir}/{name}" + 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 -# check only need to check email header, not need to download email body -def check_email_saved(client: imaplib.IMAP4_SSL, uid: str): - message_parts = "(BODY[HEADER])" - _, email_data = client.uid('fetch', uid, message_parts) - mail = mailparser.parse_from_bytes(email_data[0][1]) - logger.info(f"check email subject [{mail.subject}]") - dir = get_local_dir_name(mail) - logger.info(f"check email saved {dir}") - file = f"{dir}/email.txt" - if os.path.exists(file): - return True - return False - - -def save_email_attachment(mail: mailparser.MailParser, email_dir: str): - # Traverse all attachments in the email - 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) - # get original image name - filename = attachment['filename'] - filefullname = f"{img_dir}/{filename}" - # image bytes data - image_data = attachment['payload'] - try: - # decode base64 image data - 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) - logger.info(f"save email image {filename} success") - - -# save email to local file by each folder -def save_email(mail: mailparser.MailParser): - # create email account dir - dir = f"{config.get('LOCAL_DIR')}/{config.get('EMAIL_ADDRESS')}" - if not os.path.exists(dir): - os.makedirs(dir) - # create email local dir - email_dir = get_local_dir_name(mail) - logger.info(f"save email to {email_dir}") - if not os.path.exists(email_dir): - os.makedirs(email_dir) - - # save email content and meta info - 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) - logger.info(f"save email meta info {f.name}") - # save email attachment( like image) - save_email_attachment(mail, email_dir) - + # 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") + 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) if __name__ == "__main__": - mail = email_client() + spider = EmailSpider() folder = 'INBOX' - # imap_keyword = "UNSEEN" imap_keyword = "ALL" - read_emails(mail, folder, imap_keyword) + spider.read_emails(folder, imap_keyword) \ No newline at end of file From ddc103af38c79c562a94e895babcb6a357df3b48 Mon Sep 17 00:00:00 2001 From: liyaxing Date: Mon, 11 Sep 2023 20:32:52 +0800 Subject: [PATCH 039/177] Add EmailObjectBuilder impl and fix some bugs --- src/knowledge/core_object/document_object.py | 4 +- src/knowledge/core_object/email_object.py | 102 ++++++++++++++++-- src/knowledge/core_object/image_object.py | 4 +- src/knowledge/core_object/rich_text_object.py | 50 ++++----- src/knowledge/core_object/video_object.py | 4 +- src/knowledge/data/tracker.py | 4 +- src/knowledge/data/writer.py | 2 +- src/knowledge/store.py | 11 +- test/test_object.py | 14 ++- 9 files changed, 150 insertions(+), 45 deletions(-) diff --git a/src/knowledge/core_object/document_object.py b/src/knowledge/core_object/document_object.py index dd9bb8b..6d6270e 100644 --- a/src/knowledge/core_object/document_object.py +++ b/src/knowledge/core_object/document_object.py @@ -1,7 +1,7 @@ from ..object import KnowledgeObject from ..data import ChunkList, ChunkListWriter from ..object import ObjectType - +from .. import KnowledgeStore # desc # meta @@ -50,7 +50,7 @@ class DocumentObjectBuilder: return self def build(self) -> DocumentObject: - chunk_list = ChunkListWriter.create_chunk_list_from_text( + chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text( self.text, 1024 * 4, ) diff --git a/src/knowledge/core_object/email_object.py b/src/knowledge/core_object/email_object.py index fe5e0d4..8b8efb5 100644 --- a/src/knowledge/core_object/email_object.py +++ b/src/knowledge/core_object/email_object.py @@ -1,5 +1,12 @@ from .rich_text_object import RichTextObject, RichTextObjectBuilder from ..object import ObjectID, ObjectType, KnowledgeObject +from .document_object import DocumentObjectBuilder +from .image_object import ImageObjectBuilder +from .video_object import VideoObjectBuilder +from .rich_text_object import RichTextObjectBuilder +import os +import json +import logging class EmailObject(KnowledgeObject): @@ -24,16 +31,35 @@ class EmailObject(KnowledgeObject): return self.body["rich_text"] +""" +EmailObject folder structure: +. +├── email.txt +└── meta.json + ├── image + │ ├── image1.jpg + │ ├── image2.jpg + │ └── ... + ├── video + │ ├── video1.mp4 + │ ├── video2.mv + │ └── ... + └── audio + ├── audio1.m4a + ├── audio2.flac + └── ... +EmailObjectBuilder will read the target folder and build the EmailObject +Store meta.json to meta in EmailObject +Store email.txt to DocumentObject and RichTextObject in EmailObject +Store very image file in image folder to ImageObject and RichTextObject in EmailObject, etc +""" + + class EmailObjectBuilder: - def __init__(self, meta: dict, tags: dict, folder: str): - self.meta = meta + def __init__(self, tags: dict, folder: str): self.tags = tags self.folder = folder - def set_meta(self, meta: dict): - self.meta = meta - return self - def set_tags(self, tags: dict): self.tags = tags return self @@ -43,5 +69,65 @@ class EmailObjectBuilder: return self def build(self) -> EmailObject: - content = RichTextObjectBuilder(self.folder).build() - return EmailObject(self.meta, self.tags, content) + # Read meta.json + meta = {} + meta_file = os.path.join(self.folder, "meta.json") + if os.path.exists(meta_file): + logging.info(f"Will read meta.json {meta_file}") + with open(meta_file, "r", encoding="utf-8") as f: + meta = json.load(f) + else: + logging.info(f"Meta file missing! {meta_file}") + + # Read email.txt + documents = {} + content_file = os.path.join(self.folder, "email.txt") + if os.path.exists(content_file): + logging.info(f"Will read email.txt {content_file}") + + try: + with open(content_file, "r", encoding="utf-8") as f: + text = f.read() + + document = DocumentObjectBuilder({}, {}, text).build() + documents = {"email.txt": document} + except Exception as e: + logging.error(f"Failed to read email.txt {content_file} {e}") + else: + logging.info(f"Content file missing! {content_file}") + + # Process image files + images = {} + image_dir = os.path.join(self.folder, "image") + if os.path.exists(image_dir): + for image_file in os.listdir(image_dir): + image_path = os.path.join(image_dir, image_file) + logging.info(f"Will read image file {image_path}") + + try: + image = ImageObjectBuilder({}, {}, image_path).build() + images[image_file] = image + except Exception as e: + logging.error(f"Failed to read image file {image_path} {e}") + continue + + # Process video files + videos = {} + video_dir = os.path.join(self.folder, "video") + if os.path.exists(video_dir): + for video_file in os.listdir(video_dir): + video_path = os.path.join(video_dir, video_file) + logging.info(f"Will read video file {video_path}") + + try: + video = VideoObjectBuilder({}, {}, video_path).build() + videos[video_file] = video + except Exception as e: + logging.error(f"Failed to read video file {video_path} {e}") + continue + + # Create RichTextObject + rich_text = RichTextObject(images, videos, documents) + + # Create EmailObject + return EmailObject(meta, {}, rich_text) diff --git a/src/knowledge/core_object/image_object.py b/src/knowledge/core_object/image_object.py index d6a0cde..83ab434 100644 --- a/src/knowledge/core_object/image_object.py +++ b/src/knowledge/core_object/image_object.py @@ -1,7 +1,7 @@ from ..object import KnowledgeObject from ..data import ChunkList, ChunkListWriter from ..object import ObjectType - +from .. import KnowledgeStore # desc # meta @@ -82,7 +82,7 @@ class ImageObjectBuilder: return self def build(self) -> ImageObject: - chunk_list = ChunkListWriter.create_chunk_list_from_file( + chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_file( self.image_file, 1024 * 1024 * 4, self.restore_file ) exif = get_exif_data(self.image_file) diff --git a/src/knowledge/core_object/rich_text_object.py b/src/knowledge/core_object/rich_text_object.py index ac3fcbe..7791689 100644 --- a/src/knowledge/core_object/rich_text_object.py +++ b/src/knowledge/core_object/rich_text_object.py @@ -7,68 +7,68 @@ from .image_object import ImageObjectBuilder, ImageObject from .document_object import DocumentObjectBuilder, DocumentObject class RichTextObject(KnowledgeObject): - def __init__(self, images: dict = {}, videos: dict = {}, documents: dict = {}, rich_texts: dict = {}}): + def __init__(self, images: dict = {}, videos: dict = {}, documents: dict = {}, rich_texts: dict = {}): desc = dict() - desc.images = dict() - desc.videos = dict() - desc.documents = dict() - desc.rich_texts = dict() + desc["images"] = images + desc["videos"] = videos + desc["documents"] = documents + desc["rich_texts"] = rich_texts super().__init__(ObjectType.RichText, desc) def add_image_with_key(self, key, image_object: ImageObject): - assert self.desc.images[key] == None - self.desc.images[key] = image_object + assert self.desc["images"][key] == None + self.desc["images"][key] = image_object def add_image(self, image_object: ImageObject): - self.desc.images[image_object.object_id()] = image_object + self.desc["images"][image_object.object_id()] = image_object def get_image_with_key(self, key) -> ImageObject: - return self.desc.images[key] + return self.desc["images"][key] def get_images(self) -> dict: - return self.desc.images + return self.desc["images"] def add_video_with_key(self, key, video_object: VideoObject): - assert self.desc.videos[key] == None - self.desc.videos[key] = video_object + assert self.desc["videos"][key] == None + self.desc["videos"][key] = video_object def add_video(self, video_object: VideoObject): - self.desc.videos[video_object.object_id()] = video_object + self.desc["videos"][video_object.object_id()] = video_object def get_video_with_key(self, key) -> VideoObject: - return self.desc.videos[key] + return self.desc["videos"][key] def get_videos(self) -> dict: - return self.desc.videos + return self.desc["videos"] def add_document_with_key(self, key, document_object: DocumentObject): - assert self.desc.documents[key] == None - self.desc.documents[key] = document_object + assert self.desc["documents"][key] == None + self.desc["documents"][key] = document_object def add_document(self, document_object: DocumentObject): - self.desc.documents[document_object.object_id()] = document_object + self.desc["documents"][document_object.object_id()] = document_object def get_document_with_key(self, key) -> DocumentObject: - return self.desc.documents[key] + return self.desc["documents"][key] def get_documents(self) -> dict: - return self.desc.documents + return self.desc["documents"] def add_rich_text_with_key(self, key, rich_text_object): - assert self.desc.rich_texts[key] == None - self.desc.rich_texts[key] = rich_text_object + assert self.desc["rich_texts"][key] == None + self.desc["rich_texts"][key] = rich_text_object def add_rich_text(self, rich_text_object): - self.desc.rich_texts[rich_text_object.object_id()] = rich_text_object + self.desc["rich_texts"][rich_text_object.object_id()] = rich_text_object def get_rich_text_with_key(self, key): - return self.desc.rich_texts[key] + return self.desc["rich_texts"][key] def get_rich_texts(self) -> dict: - return self.desc.rich_texts + return self.desc["rich_texts"] class RichTextObjectBuilder: diff --git a/src/knowledge/core_object/video_object.py b/src/knowledge/core_object/video_object.py index ab4c262..56e9829 100644 --- a/src/knowledge/core_object/video_object.py +++ b/src/knowledge/core_object/video_object.py @@ -1,7 +1,7 @@ from ..object import KnowledgeObject from ..data import ChunkList, ChunkListWriter from ..object import ObjectType - +from .. import KnowledgeStore # desc # meta @@ -77,7 +77,7 @@ class VideoObjectBuilder: return self def build(self) -> VideoObject: - chunk_list = ChunkListWriter.create_chunk_list_from_file( + chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_file( self.video_file, 1024 * 1024 * 4, self.restore_file ) info = get_video_info(self.video_file) diff --git a/src/knowledge/data/tracker.py b/src/knowledge/data/tracker.py index 57daa38..66024c9 100644 --- a/src/knowledge/data/tracker.py +++ b/src/knowledge/data/tracker.py @@ -3,7 +3,7 @@ import time import logging import os from .chunk import ChunkID, PositionType, PositionFileRange -from typing import List +from typing import List, Tuple class ChunkTracker: def __init__(self, root_dir: str): @@ -61,7 +61,7 @@ class ChunkTracker: ) self.conn.commit() - def get_position(self, chunk_id: ChunkID) -> List[(str, PositionType)]: + def get_position(self, chunk_id: ChunkID) -> List[Tuple[str, PositionType]]: self.cursor.execute( """ SELECT pos, pos_type FROM chunks WHERE id = ? diff --git a/src/knowledge/data/writer.py b/src/knowledge/data/writer.py index 7f79447..7d38e9d 100644 --- a/src/knowledge/data/writer.py +++ b/src/knowledge/data/writer.py @@ -42,7 +42,7 @@ class ChunkListWriter: file_path, file.tell() - chunk_size, chunk_size ) self.chunk_tracker.add_position( - chunk_id, file_range, PositionType.FileRange + chunk_id, str(file_range), PositionType.FileRange ) file_hash = HashValue(hash_obj.digest()) diff --git a/src/knowledge/store.py b/src/knowledge/store.py index ef697fa..dc6b17a 100644 --- a/src/knowledge/store.py +++ b/src/knowledge/store.py @@ -1,6 +1,6 @@ import os from .object import ObjectStore -from .data import ChunkStore, ChunkTracker +from .data import ChunkStore, ChunkTracker, ChunkListWriter, ChunkReader import logging @@ -35,6 +35,9 @@ class KnowledgeStore: chunk_store_dir = os.path.join(root_dir, "chunk") self.chunk_store = ChunkStore(chunk_store_dir) self.chunk_tracker = ChunkTracker(chunk_store_dir) + self.chunk_list_writer = ChunkListWriter(self.chunk_store, self.chunk_tracker) + self.chunk_reader = ChunkReader(self.chunk_store, self.chunk_tracker) + def get_object_store(self) -> ObjectStore: return self.object_store @@ -44,3 +47,9 @@ class KnowledgeStore: def get_chunk_tracker(self) -> ChunkTracker: return self.chunk_tracker + + def get_chunk_list_writer(self) -> ChunkListWriter: + return self.chunk_list_writer + + def get_chunk_reader(self) -> ChunkReader: + return self.chunk_reader diff --git a/test/test_object.py b/test/test_object.py index 6a603fd..1e1bc56 100644 --- a/test/test_object.py +++ b/test/test_object.py @@ -1,5 +1,6 @@ import sys import os +import logging dir_path = os.path.dirname(os.path.realpath(__file__)) print(dir_path) @@ -7,9 +8,16 @@ print(dir_path) sys.path.append("{}/../src/".format(dir_path)) print(sys.path) -from knowledge import ObjectID, HashValue +root = logging.getLogger() +root.setLevel(logging.DEBUG) +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +root.addHandler(handler) +from knowledge import ObjectID, HashValue, EmailObjectBuilder import asyncio import unittest @@ -26,6 +34,8 @@ class TestVectorSTorage(unittest.TestCase): data2 = HashValue.from_base36(data.to_base36()) self.assertEqual(data.to_base58(), data2.to_base58()) - + email_folder = "F:\\system\\Downloads\\8081ffdb80925f5bff9c6ab9c4756c7d" + email_object = EmailObjectBuilder({}, email_folder).build() + if __name__ == "__main__": unittest.main() From 8ac6065332dc34de4da67191d6be1dae5ae15826 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Tue, 12 Sep 2023 15:28:59 +0800 Subject: [PATCH 040/177] embedding email object --- src/aios_kernel/__init__.py | 3 +- src/aios_kernel/knowledge_base.py | 113 ++++++++++++++++------ src/knowledge/core_object/__init__.py | 1 + src/knowledge/core_object/image_object.py | 2 +- src/knowledge/object/object_id.py | 1 - src/knowledge/store.py | 7 ++ src/knowledge/vector/chroma_store.py | 6 +- src/knowledge/vector/vector_base.py | 3 +- test/test_knowledge_base.py | 57 +++++++++++ test/test_vector_storage.py | 4 +- 10 files changed, 155 insertions(+), 42 deletions(-) create mode 100644 test/test_knowledge_base.py diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bd1434b..9817b76 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -4,7 +4,8 @@ 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 .open_ai_node import OpenAI_ComputeNode +from .knowledge_base import KnowledgeBase from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus \ No newline at end of file diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index 4f4ba4f..52b2943 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,47 +1,96 @@ # define a knowledge base class +import json from . import AgentPrompt, ComputeKernel -from ..knowledge.object import KnowledgeObject, ObjectType, EmailObject, TextChunkObject, ImageObject -from ..knowledge.store import ObjectStorage -from ..knowledge.vector.vector_base import VectorBase +from ..knowledge import * + class KnowledgeBase: - def __init__(self) -> None: - self.object_store = ObjectStorage() - self.vector_base = VectorBase() + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.__singleton_init__() + + return cls._instance + + def __singleton_init__(self) -> None: + self.store = KnowledgeStore() self.compute_kernel = ComputeKernel() - async def insert(self, object: KnowledgeObject): - if object.object_type == ObjectType.Email: - email: EmailObject = object - for text_id in email.text: - [text, _] = self.object_store.get(text_id) - text: TextChunkObject = text - vector = await self.compute_kernel.do_text_embedding(text.text) - self.vector_base.insert(vector, text_id) - - for image_id in email.images: - [image, _] = self.object_store.get(image_id) - image: ImageObject = image - vector = await self.compute_kernel.do_text_embedding(image.meta) - self.vector_base.insert(vector, image_id) - - vector = await self.compute_kernel.do_text_embedding(email.meta) - self.vector_base.insert(vector, email.get_id()) + async def __embedding_document(self, document: DocumentObject): + for chunk_id in document.get_chunk_list(): + chunk = self.store.get_chunk_reader().get_chunk(chunk_id) + if chunk is None: + raise ValueError(f"text chunk not found: {chunk_id}") + + text = chunk.read().decode("utf-8") + vector = await self.compute_kernel.do_text_embedding(text) + self.store.get_vector_store("default").insert(vector, chunk_id) + + async def __embedding_image(self, image: ImageObject): + desc = {} + if not image.get_meta(): + desc["meta"] = image.get_meta() + if not image.get_exif(): + desc["exif"] = image.get_exif() + if not image.get_tags(): + desc["tags"] = image.get_tags() + vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) + self.store.get_vector_store("default").insert(vector, image.calculate_id()) + + async def __embedding_vedio(self, vedio: VideoObject): + desc = {} + if not vedio.get_meta(): + desc["meta"] = vedio.get_meta() + if not vedio.get_info(): + desc["info"] = vedio.get_info() + if not vedio.get_tags(): + desc["tags"] = vedio.get_tags() + vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) + self.store.get_vector_store("default").insert(vector, vedio.calculate_id()) + + async def __embedding_rich_text(self, rich_text: RichTextObject): + for document in rich_text.get_documents().values(): + await self.__embedding_document(document) + for image in rich_text.get_images().values(): + await self.__embedding_image(image) + for vedio in rich_text.get_videos().values(): + await self.__embedding_vedio(vedio) + for rich_text in rich_text.get_rich_texts().values(): + await self.__embedding_rich_text(rich_text) + + async def __embedding_email(self, email: EmailObject): + vector = await self.compute_kernel.do_text_embedding(json.dumps(email.get_desc())) + self.store.get_vector_store("default").insert(vector, email.calculate_id()) + await self.__embedding_rich_text(email.get_rich_text()) + + async def do_embedding(self, object: KnowledgeObject): + if object.get_object_type() == ObjectType.Document: + await self.__embedding_document(object) + if object.get_object_type() == ObjectType.Image: + await self.__embedding_image(object) + if object.get_object_type() == ObjectType.Video: + await self.__embedding_vedio(object) + if object.get_object_type() == ObjectType.RichText: + await self.__embedding_rich_text(object) + if object.get_object_type() == ObjectType.Email: + await self.__embedding_email(object) else: pass - async def query(self, prompt: AgentPrompt) -> AgentPrompt: + async def query(self, prompt: AgentPrompt) -> [ObjectID]: + results = [] for msg in prompt.messages: if msg.role == "user": vector = await self.compute_kernel.do_text_embedding(msg.content) - object_ids = self.vector_base.query(vector, 10) - for object_id in object_ids: - if object_id.object_type == ObjectType.Email: - [object, email] = self.object_store.get(object_id) - if object.object_type == ObjectType.Email: - email: EmailObject = object - prompt.append(AgentPrompt()) - prompt + object_ids = self.store.get_vector_store("default").query(vector, 10) + results.append(object_ids) + return results + + + + diff --git a/src/knowledge/core_object/__init__.py b/src/knowledge/core_object/__init__.py index 065de74..dee13e2 100644 --- a/src/knowledge/core_object/__init__.py +++ b/src/knowledge/core_object/__init__.py @@ -1,4 +1,5 @@ from .document_object import DocumentObject, DocumentObjectBuilder from .image_object import ImageObject, ImageObjectBuilder from .video_object import VideoObject, VideoObjectBuilder +from .rich_text_object import RichTextObject, RichTextObjectBuilder from .email_object import EmailObject, EmailObjectBuilder \ No newline at end of file diff --git a/src/knowledge/core_object/image_object.py b/src/knowledge/core_object/image_object.py index 83ab434..30b0e8a 100644 --- a/src/knowledge/core_object/image_object.py +++ b/src/knowledge/core_object/image_object.py @@ -18,7 +18,7 @@ class ImageObject(KnowledgeObject): body = dict() desc["meta"] = meta desc["exif"] = exif - desc + desc["tags"] = tags desc["hash"] = chunk_list.hash.to_base58() body["chunk_list"] = chunk_list.chunk_list diff --git a/src/knowledge/object/object_id.py b/src/knowledge/object/object_id.py index 3c3ac24..6ce77ae 100644 --- a/src/knowledge/object/object_id.py +++ b/src/knowledge/object/object_id.py @@ -8,7 +8,6 @@ import base36 class ObjectType(Enum): Chunk = 7 - TextChunk = 100 Image = 101 Video = 102 Document = 103 diff --git a/src/knowledge/store.py b/src/knowledge/store.py index dc6b17a..c0cc6b5 100644 --- a/src/knowledge/store.py +++ b/src/knowledge/store.py @@ -1,6 +1,7 @@ import os from .object import ObjectStore from .data import ChunkStore, ChunkTracker, ChunkListWriter, ChunkReader +from .vector import ChromaVectorStore, VectorBase import logging @@ -37,6 +38,7 @@ class KnowledgeStore: self.chunk_tracker = ChunkTracker(chunk_store_dir) self.chunk_list_writer = ChunkListWriter(self.chunk_store, self.chunk_tracker) self.chunk_reader = ChunkReader(self.chunk_store, self.chunk_tracker) + self.vector_store = {} def get_object_store(self) -> ObjectStore: @@ -53,3 +55,8 @@ class KnowledgeStore: def get_chunk_reader(self) -> ChunkReader: return self.chunk_reader + + def get_vector_store(self, model_name: str) -> VectorBase: + if model_name not in self.vector_store: + self.vector_store[model_name] = ChromaVectorStore(model_name) + return self.vector_store[model_name] diff --git a/src/knowledge/vector/chroma_store.py b/src/knowledge/vector/chroma_store.py index 626cd4f..0056375 100644 --- a/src/knowledge/vector/chroma_store.py +++ b/src/knowledge/vector/chroma_store.py @@ -6,11 +6,11 @@ import os class ChromaVectorStore(VectorBase): - def __init__(self, db_url, model_name: str) -> None: - super().__init__(db_url, model_name) + def __init__(self, model_name: str) -> None: + super().__init__(model_name) logging.info( - "will init chroma vector store, db={}, model={}".format(db_url, model_name) + "will init chroma vector store, model={}".format(model_name) ) directory = os.path.join( diff --git a/src/knowledge/vector/vector_base.py b/src/knowledge/vector/vector_base.py index 354c21e..83276ed 100644 --- a/src/knowledge/vector/vector_base.py +++ b/src/knowledge/vector/vector_base.py @@ -3,8 +3,7 @@ from ..object import ObjectID # define a vector base class class VectorBase: - def __init__(self, db_url, model_name) -> None: - self.db_url = db_url + def __init__(self, model_name) -> None: self.model_name = model_name async def insert(self, vector: [float], id: ObjectID): diff --git a/test/test_knowledge_base.py b/test/test_knowledge_base.py new file mode 100644 index 0000000..efed1bd --- /dev/null +++ b/test/test_knowledge_base.py @@ -0,0 +1,57 @@ +import sys +import os +import logging + +dir_path = os.path.dirname(os.path.realpath(__file__)) +print(dir_path) + +sys.path.append("{}/../src/".format(dir_path)) +print(sys.path) + +root = logging.getLogger() +root.setLevel(logging.DEBUG) +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +root.addHandler(handler) + + +from knowledge import ObjectID, HashValue, EmailObjectBuilder +from aios_kernel import KnowledgeBase, AgentPrompt +import asyncio +import unittest + +async def test_embedding_email(): + data = HashValue.hash_data("1233".encode("utf-8")); + print(data.to_base58()) + print(data.to_base36()) + + data2 = HashValue.from_base58(data.to_base58()) + self.assertEqual(data.to_base36(), data2.to_base36()) + + data2 = HashValue.from_base36(data.to_base36()) + self.assertEqual(data.to_base58(), data2.to_base58()) + + email_folder = "F:\\system\\Downloads\\8081ffdb80925f5bff9c6ab9c4756c7d" + email_object = EmailObjectBuilder({}, email_folder).build() + + await KnowledgeBase().do_embedding(email_object) + + +async def test_query_email(): + msg_prompt = AgentPrompt() + msg_prompt.messages = [{"role":"user","content":"abcdef"}] + + KnowledgeBase().query(msg_prompt) + +class TestVectorSTorage(unittest.TestCase): + def test_embedding(self): + asyncio.run(test_embedding_email()) + + def test_query(self): + asyncio.run(test_query_email()) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_vector_storage.py b/test/test_vector_storage.py index 7357d65..6023ab1 100644 --- a/test/test_vector_storage.py +++ b/test/test_vector_storage.py @@ -14,8 +14,8 @@ import asyncio import unittest -async def test_vector(): - storage = ChromaVectorStore("", "test") +async def test_embedding_email(): + storage = ChromaVectorStore("test") await storage.insert([1, 2, 3], "test") ids = await storage.query([1, 2, 3], 10) print(ids) From 72257b8312196fdde7f65f82a0ace8baf6ae67c6 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Tue, 12 Sep 2023 15:51:48 +0800 Subject: [PATCH 041/177] Parse and save img tag in the email body --- .gitignore | 1 + src/service/spider/email_spider.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/.gitignore b/.gitignore index 84349bb..ae3bcfd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.pyc rootfs/email/config.local.toml rootfs/data +venv \ No newline at end of file diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py index a2a1154..cd44097 100644 --- a/src/service/spider/email_spider.py +++ b/src/service/spider/email_spider.py @@ -1,3 +1,21 @@ +""" +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 @@ -6,6 +24,8 @@ import mailparser import hashlib import json import base64 +from bs4 import BeautifulSoup +import requests class EmailSpider: def __init__(self): @@ -46,6 +66,7 @@ class EmailSpider: # 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") @@ -97,6 +118,31 @@ class EmailSpider: 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): @@ -113,7 +159,10 @@ class EmailSpider: 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() From 6e5beb4cdffb42c85eeb720ab0035b7c102ac58f Mon Sep 17 00:00:00 2001 From: tsukasa Date: Wed, 13 Sep 2023 17:55:27 +0800 Subject: [PATCH 042/177] save relationship of objects --- src/aios_kernel/knowledge_base.py | 107 +++++++++++++++++++++- src/knowledge/core_object/email_object.py | 2 +- src/knowledge/object/__init__.py | 1 + src/knowledge/object/relation.py | 16 ++++ src/knowledge/store.py | 10 +- test/test_knowledge_base.py | 2 +- test/test_vector_storage.py | 4 +- 7 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 src/knowledge/object/relation.py diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index 52b2943..ce689e6 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,5 +1,6 @@ # define a knowledge base class import json +import pickle from . import AgentPrompt, ComputeKernel from ..knowledge import * @@ -39,7 +40,7 @@ class KnowledgeBase: vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) self.store.get_vector_store("default").insert(vector, image.calculate_id()) - async def __embedding_vedio(self, vedio: VideoObject): + async def __embedding_video(self, vedio: VideoObject): desc = {} if not vedio.get_meta(): desc["meta"] = vedio.get_meta() @@ -56,7 +57,7 @@ class KnowledgeBase: for image in rich_text.get_images().values(): await self.__embedding_image(image) for vedio in rich_text.get_videos().values(): - await self.__embedding_vedio(vedio) + await self.__embedding_video(vedio) for rich_text in rich_text.get_rich_texts().values(): await self.__embedding_rich_text(rich_text) @@ -65,13 +66,14 @@ class KnowledgeBase: self.store.get_vector_store("default").insert(vector, email.calculate_id()) await self.__embedding_rich_text(email.get_rich_text()) - async def do_embedding(self, object: KnowledgeObject): + + async def __do_embedding(self, object: KnowledgeObject): if object.get_object_type() == ObjectType.Document: await self.__embedding_document(object) if object.get_object_type() == ObjectType.Image: await self.__embedding_image(object) if object.get_object_type() == ObjectType.Video: - await self.__embedding_vedio(object) + await self.__embedding_video(object) if object.get_object_type() == ObjectType.RichText: await self.__embedding_rich_text(object) if object.get_object_type() == ObjectType.Email: @@ -79,7 +81,80 @@ class KnowledgeBase: else: pass - async def query(self, prompt: AgentPrompt) -> [ObjectID]: + def __save_document(self, document: DocumentObject): + doc_id = document.calculate_id() + self.store.get_object_store().put_object(doc_id, document.encode()) + for chunk_id in document.get_chunk_list(): + self.store.get_relation_store().add_relation(chunk_id, doc_id) + + def __save_image(self, image: ImageObject): + image_id = image.calculate_id() + self.store.get_object_store().put_object(image_id, image.encode()) + + def __save_video(self, video: VideoObject): + video_id = video.calculate_id() + self.store.get_object_store().put_object(video_id, video.encode()) + + def __save_rich_text(self, rich_text: RichTextObject): + rich_text_id = rich_text.calculate_id() + rich_text_enc = dict() + rich_text_enc["desc"] = rich_text.desc + rich_text_enc["body"] = {"documents": {}, "images": {}, "videos": {}, "rich_texts": {}} + for key, document in rich_text.get_documents().items(): + self.__save_document(document) + doc_id = document.calculate_id() + self.store.get_relation_store().add_relation(doc_id, rich_text_id) + rich_text_enc["body"]["documents"][key] = doc_id + for key, image in rich_text.get_images().items(): + self.__save_image(image) + image_id = image.calculate_id() + self.store.get_relation_store().add_relation(image_id, rich_text_id) + rich_text_enc["body"]["images"][key] = image_id + for key, video in rich_text.get_videos().items(): + self.__save_video(video) + video_id = video.calculate_id() + self.store.get_relation_store().add_relation(video_id, rich_text_id) + rich_text_enc["body"]["videos"][key] = video_id + for key, rich_text in rich_text.get_rich_texts().items(): + self.__save_rich_text(rich_text) + rich_text_id = rich_text.calculate_id() + self.store.get_relation_store().add_relation(rich_text_id, rich_text_id) + rich_text_enc["body"]["rich_texts"][key] = rich_text_id + + + self.store.get_object_store().put_object(rich_text_id, pickle.dumps(rich_text_enc)) + + def __save_email(self, email: EmailObject): + email_id = email.calculate_id() + email_enc = dict() + email_enc["desc"] = email.desc + email_enc["body"] = {"content": None} + self.__save_rich_text(email.get_rich_text()) + rich_text_id = email.get_rich_text().calculate_id() + self.store.get_relation_store().add_relation(rich_text_id, email_id) + email_enc["body"]["content"] = rich_text_id + self.store.get_object_store().put_object(email_id, pickle.dumps(email_enc)) + + + def __save_object(self, object: KnowledgeObject): + if object.get_object_type() == ObjectType.Document: + self.__save_document(object) + if object.get_object_type() == ObjectType.Image: + self.__save_image(object) + if object.get_object_type() == ObjectType.Video: + self.__save_video(object) + if object.get_object_type() == ObjectType.RichText: + self.__save_rich_text(object) + if object.get_object_type() == ObjectType.Email: + self.__save_email(object) + else: + pass + + async def insert_object(self, object: KnowledgeObject): + self.__save_object(object) + self.__do_embedding(object) + + async def query_objects(self, prompt: AgentPrompt) -> [ObjectID]: results = [] for msg in prompt.messages: if msg.role == "user": @@ -89,6 +164,28 @@ class KnowledgeBase: return results + async def __prompt_from_objects(self, object_ids: [ObjectID]) -> AgentPrompt: + prompt = AgentPrompt() + for object_id in object_ids: + object = self.store.get_object_reader().get_object(object_id) + if object is None: + raise ValueError(f"object not found: {object_id}") + if object.get_object_type() == ObjectType.Document: + document = object + prompt.messages.append({"role": "agent", "content": document.get_body()}) + if object.get_object_type() == ObjectType.Image: + image = object + prompt.messages.append({"role": "agent", "content": json.dumps(image.get_desc())}) + if object.get_object_type() == ObjectType.Video: + video = object + prompt.messages.append({"role": "agent", "content": json.dumps(video.get_desc())}) + if object.get_object_type() == ObjectType.RichText: + rich_text = object + prompt.messages.append({"role": "agent", "content": json.dumps(rich_text.get_desc())}) + if object.get_object_type() == ObjectType.Email: + email = object + prompt.messages.append({"role": "agent", "content": json.dumps(email.get_desc())}) + return prompt diff --git a/src/knowledge/core_object/email_object.py b/src/knowledge/core_object/email_object.py index 8b8efb5..f563052 100644 --- a/src/knowledge/core_object/email_object.py +++ b/src/knowledge/core_object/email_object.py @@ -28,7 +28,7 @@ class EmailObject(KnowledgeObject): return self.desc["tags"] def get_rich_text(self): - return self.body["rich_text"] + return self.body["content"] """ diff --git a/src/knowledge/object/__init__.py b/src/knowledge/object/__init__.py index 7886d7a..291715f 100644 --- a/src/knowledge/object/__init__.py +++ b/src/knowledge/object/__init__.py @@ -1,5 +1,6 @@ from .object import KnowledgeObject from .blob import FileBlobStorage from .hash import HashValue, hash_data +from .relation import ObjectRelationStore from .object_store import ObjectStore from .object_id import ObjectID, ObjectType diff --git a/src/knowledge/object/relation.py b/src/knowledge/object/relation.py new file mode 100644 index 0000000..1b56e48 --- /dev/null +++ b/src/knowledge/object/relation.py @@ -0,0 +1,16 @@ +# define a relation store class +from .object_id import ObjectID + + +class ObjectRelationStore: + def __init__(self, root_dir: str): + pass + + def add_relation(self, object: ObjectID, parent: ObjectID): + pass + + def get_related_objects(self, object: ObjectID) -> [ObjectID]: + pass + + def delete_relation(self, object_id: ObjectID) -> [ObjectID]: + pass \ No newline at end of file diff --git a/src/knowledge/store.py b/src/knowledge/store.py index c0cc6b5..03e48d0 100644 --- a/src/knowledge/store.py +++ b/src/knowledge/store.py @@ -1,5 +1,6 @@ import os -from .object import ObjectStore + +from .object import ObjectStore, ObjectRelationStore from .data import ChunkStore, ChunkTracker, ChunkListWriter, ChunkReader from .vector import ChromaVectorStore, VectorBase import logging @@ -30,6 +31,9 @@ class KnowledgeStore: self.root = root_dir + relation_store_dir = os.path.join(root_dir, "relation") + self.relation_store = ObjectRelationStore(relation_store_dir) + object_store_dir = os.path.join(root_dir, "object") self.object_store = ObjectStore(object_store_dir) @@ -39,7 +43,9 @@ class KnowledgeStore: self.chunk_list_writer = ChunkListWriter(self.chunk_store, self.chunk_tracker) self.chunk_reader = ChunkReader(self.chunk_store, self.chunk_tracker) self.vector_store = {} - + + def get_relation_store(self) -> ObjectRelationStore: + return self.relation_store def get_object_store(self) -> ObjectStore: return self.object_store diff --git a/test/test_knowledge_base.py b/test/test_knowledge_base.py index efed1bd..ebda00c 100644 --- a/test/test_knowledge_base.py +++ b/test/test_knowledge_base.py @@ -45,7 +45,7 @@ async def test_query_email(): KnowledgeBase().query(msg_prompt) -class TestVectorSTorage(unittest.TestCase): +class TestKnowledgeBase(unittest.TestCase): def test_embedding(self): asyncio.run(test_embedding_email()) diff --git a/test/test_vector_storage.py b/test/test_vector_storage.py index 6023ab1..ff4dc72 100644 --- a/test/test_vector_storage.py +++ b/test/test_vector_storage.py @@ -14,13 +14,13 @@ import asyncio import unittest -async def test_embedding_email(): +async def test_vector(): storage = ChromaVectorStore("test") await storage.insert([1, 2, 3], "test") ids = await storage.query([1, 2, 3], 10) print(ids) -class TestVectorSTorage(unittest.TestCase): +class TestVectorStorage(unittest.TestCase): def test_run(self): asyncio.run(test_vector()) From dd3187fc8aba98bd69b5ff1fa48a0f41732f2688 Mon Sep 17 00:00:00 2001 From: zhangzhen Date: Thu, 14 Sep 2023 15:22:38 +0800 Subject: [PATCH 043/177] Provide the local llama service --- src/aios_kernel/compute_kernel.py | 2 +- src/aios_kernel/compute_node.py | 5 +- src/aios_kernel/local_llama_compute_node.py | 86 +++++++++++++++++++++ src/aios_kernel/open_ai_node.py | 4 +- src/aios_kernel/queue_compute_node.py | 69 +++++++++++++++++ src/aios_kernel/stability_node.py | 4 +- 6 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 src/aios_kernel/local_llama_compute_node.py create mode 100644 src/aios_kernel/queue_compute_node.py diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 5fea958..381dda9 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -65,7 +65,7 @@ class ComputeKernel: def _schedule(self, task) -> ComputeNode: for node in self.compute_nodes.values(): - if node.is_support(task.task_type) is True: + if node.is_support(task) is True: return node logger.warning( f"task {task.display()} is not support by any compute node") diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 7e159f7..873fecd 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -28,7 +28,7 @@ class ComputeNode(ABC): pass @abstractmethod - def is_support(self, task_type: ComputeTaskType) -> bool: + def is_support(self, task: ComputeTask) -> bool: pass @abstractmethod @@ -41,10 +41,9 @@ class ComputeNode(ABC): 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 + return True \ No newline at end of file diff --git a/src/aios_kernel/local_llama_compute_node.py b/src/aios_kernel/local_llama_compute_node.py new file mode 100644 index 0000000..0931f1f --- /dev/null +++ b/src/aios_kernel/local_llama_compute_node.py @@ -0,0 +1,86 @@ + +import logging +import requests +from typing import Optional +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[str] + + try: + body = { + "prompts": task.params["prompts"] + } + + response = requests.post("http://aigc:7880/generate", data = body, verify=False) + 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 = GenerateResponse.parse_raw(response.content.decode("utf-8")) + if resp.error: + return { + "state": ComputeTaskState.ERROR, + "error": { + "code": resp.error, + "message": "local llama failed:" + resp.msg + } + } + else: + return { + "content": str(resp.results), + "message": {} + } + 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 diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 1be47fd..e2ef098 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -103,8 +103,8 @@ class OpenAI_ComputeNode(ComputeNode): def get_capacity(self): pass - def is_support(self, task_type: ComputeTaskType) -> bool: - return task_type == ComputeTaskType.LLM_COMPLETION + 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"] == "gpt-4-0613") def is_local(self) -> bool: return False diff --git a/src/aios_kernel/queue_compute_node.py b/src/aios_kernel/queue_compute_node.py new file mode 100644 index 0000000..e975cd8 --- /dev/null +++ b/src/aios_kernel/queue_compute_node.py @@ -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 diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index 8d40878..b1f0da9 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -129,8 +129,8 @@ class Stability_ComputeNode(ComputeNode): def get_capacity(self): pass - def is_support(self, task_type: ComputeTaskType) -> bool: - return task_type == ComputeTaskType.TEXT_2_IMAGE + def is_support(self, task: ComputeTask) -> bool: + return task.task_type == ComputeTaskType.TEXT_2_IMAGE def is_local(self) -> bool: return False From aaf0f152289196606eb05593bbaf8caffe5670c8 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 14 Sep 2023 15:44:58 +0800 Subject: [PATCH 044/177] put email object with nested object not object id --- src/aios_kernel/knowledge_base.py | 43 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index ce689e6..ebbfca8 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -31,22 +31,22 @@ class KnowledgeBase: async def __embedding_image(self, image: ImageObject): desc = {} - if not image.get_meta(): + if not not image.get_meta(): desc["meta"] = image.get_meta() - if not image.get_exif(): + if not not image.get_exif(): desc["exif"] = image.get_exif() - if not image.get_tags(): + if not not image.get_tags(): desc["tags"] = image.get_tags() vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) self.store.get_vector_store("default").insert(vector, image.calculate_id()) async def __embedding_video(self, vedio: VideoObject): desc = {} - if not vedio.get_meta(): + if not not vedio.get_meta(): desc["meta"] = vedio.get_meta() - if not vedio.get_info(): + if not not vedio.get_info(): desc["info"] = vedio.get_info() - if not vedio.get_tags(): + if not not vedio.get_tags(): desc["tags"] = vedio.get_tags() vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) self.store.get_vector_store("default").insert(vector, vedio.calculate_id()) @@ -97,43 +97,43 @@ class KnowledgeBase: def __save_rich_text(self, rich_text: RichTextObject): rich_text_id = rich_text.calculate_id() - rich_text_enc = dict() - rich_text_enc["desc"] = rich_text.desc - rich_text_enc["body"] = {"documents": {}, "images": {}, "videos": {}, "rich_texts": {}} + # rich_text_enc = dict() + # rich_text_enc["desc"] = rich_text.desc + # rich_text_enc["body"] = {"documents": {}, "images": {}, "videos": {}, "rich_texts": {}} for key, document in rich_text.get_documents().items(): self.__save_document(document) doc_id = document.calculate_id() self.store.get_relation_store().add_relation(doc_id, rich_text_id) - rich_text_enc["body"]["documents"][key] = doc_id + # rich_text_enc["body"]["documents"][key] = doc_id for key, image in rich_text.get_images().items(): self.__save_image(image) image_id = image.calculate_id() self.store.get_relation_store().add_relation(image_id, rich_text_id) - rich_text_enc["body"]["images"][key] = image_id + # rich_text_enc["body"]["images"][key] = image_id for key, video in rich_text.get_videos().items(): self.__save_video(video) video_id = video.calculate_id() self.store.get_relation_store().add_relation(video_id, rich_text_id) - rich_text_enc["body"]["videos"][key] = video_id + # rich_text_enc["body"]["videos"][key] = video_id for key, rich_text in rich_text.get_rich_texts().items(): self.__save_rich_text(rich_text) rich_text_id = rich_text.calculate_id() self.store.get_relation_store().add_relation(rich_text_id, rich_text_id) - rich_text_enc["body"]["rich_texts"][key] = rich_text_id + # rich_text_enc["body"]["rich_texts"][key] = rich_text_id - self.store.get_object_store().put_object(rich_text_id, pickle.dumps(rich_text_enc)) + self.store.get_object_store().put_object(rich_text_id, rich_text.encode()) def __save_email(self, email: EmailObject): email_id = email.calculate_id() - email_enc = dict() - email_enc["desc"] = email.desc - email_enc["body"] = {"content": None} + # email_enc = dict() + # email_enc["desc"] = email.desc + # email_enc["body"] = {"content": None} self.__save_rich_text(email.get_rich_text()) rich_text_id = email.get_rich_text().calculate_id() self.store.get_relation_store().add_relation(rich_text_id, email_id) - email_enc["body"]["content"] = rich_text_id - self.store.get_object_store().put_object(email_id, pickle.dumps(email_enc)) + # email_enc["body"]["content"] = rich_text_id + self.store.get_object_store().put_object(email_id, email.encode()) def __save_object(self, object: KnowledgeObject): @@ -163,6 +163,11 @@ class KnowledgeBase: results.append(object_ids) return results + async def __load_object(self, object_id: ObjectID) -> KnowledgeObject: + if object_id.get_object_type() == ObjectType.Document: + return DocumentObject.decode(self.store.get_object_store().get_object(object_id)) + object = self.store.get_object_store().get_object(object_id) + async def __prompt_from_objects(self, object_ids: [ObjectID]) -> AgentPrompt: prompt = AgentPrompt() From 9cbf29e0b0b379dcf53ac71362cf099ce9e5f873 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 14 Sep 2023 16:03:56 +0800 Subject: [PATCH 045/177] implement object relation store with sqlite --- src/aios_kernel/knowledge_base.py | 1 - src/knowledge/object/relation.py | 56 ++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index ebbfca8..18d78a3 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,6 +1,5 @@ # define a knowledge base class import json -import pickle from . import AgentPrompt, ComputeKernel from ..knowledge import * diff --git a/src/knowledge/object/relation.py b/src/knowledge/object/relation.py index 1b56e48..f6eca2e 100644 --- a/src/knowledge/object/relation.py +++ b/src/knowledge/object/relation.py @@ -1,16 +1,64 @@ # define a relation store class +import sqlite3 +import os +import logging from .object_id import ObjectID class ObjectRelationStore: def __init__(self, root_dir: str): - pass + if not os.path.exists(root_dir): + os.makedirs(root_dir) + file = os.path.join(root_dir, "relationships.db") + logging.info(f"will init chunk tracker, db={file}") + + self.conn = sqlite3.connect(file) + self.cursor = self.conn.cursor() + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS relationships ( + id TEXT NOT NULL, + parent TEXT NOT NULL, + PRIMARY KEY(id, parent) + ) + """ + ) + self.conn.commit() def add_relation(self, object: ObjectID, parent: ObjectID): - pass + self.cursor.execute( + """ + INSERT INTO relationships (id, parent) + VALUES (?, ?, ?, ?, ?) + """, + ( + str(object), + str(parent), + ), + ) + self.conn.commit() def get_related_objects(self, object: ObjectID) -> [ObjectID]: - pass + parents = [] + cur = object + while True: + self.cursor.execute( + """ + SELECT id, parent FROM relationships WHERE id = ? + """, + (str(cur),), + ) + parent = self.cursor.fetchone() + if parent is None: + break + parents.append(parent) + cur = parent def delete_relation(self, object_id: ObjectID) -> [ObjectID]: - pass \ No newline at end of file + self.cursor.execute( + """ + DELETE FROM relationships WHERE id = ? + """, + (str(object_id),), + ) + self.conn.commit() \ No newline at end of file From a0cb5480df61bcba76466d8b39f868e1a527194e Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 14 Sep 2023 16:42:14 +0800 Subject: [PATCH 046/177] prompt from knowledge object --- src/aios_kernel/knowledge_base.py | 80 ++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index 18d78a3..cabf005 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -165,32 +165,68 @@ class KnowledgeBase: async def __load_object(self, object_id: ObjectID) -> KnowledgeObject: if object_id.get_object_type() == ObjectType.Document: return DocumentObject.decode(self.store.get_object_store().get_object(object_id)) - object = self.store.get_object_store().get_object(object_id) + if object_id.get_object_type() == ObjectType.Image: + return ImageObject.decode(self.store.get_object_store().get_object(object_id)) + if object_id.get_object_type() == ObjectType.Video: + return VideoObject.decode(self.store.get_object_store().get_object(object_id)) + if object_id.get_object_type() == ObjectType.RichText: + return RichTextObject.decode(self.store.get_object_store().get_object(object_id)) + if object_id.get_object_type() == ObjectType.Email: + return EmailObject.decode(self.store.get_object_store().get_object(object_id)) + else: + pass - async def __prompt_from_objects(self, object_ids: [ObjectID]) -> AgentPrompt: - prompt = AgentPrompt() + async def prompt_from_objects(self, object_ids: [ObjectID]) -> AgentPrompt: + results = dict() for object_id in object_ids: - object = self.store.get_object_reader().get_object(object_id) - if object is None: - raise ValueError(f"object not found: {object_id}") - if object.get_object_type() == ObjectType.Document: - document = object - prompt.messages.append({"role": "agent", "content": document.get_body()}) - if object.get_object_type() == ObjectType.Image: - image = object - prompt.messages.append({"role": "agent", "content": json.dumps(image.get_desc())}) - if object.get_object_type() == ObjectType.Video: - video = object - prompt.messages.append({"role": "agent", "content": json.dumps(video.get_desc())}) - if object.get_object_type() == ObjectType.RichText: - rich_text = object - prompt.messages.append({"role": "agent", "content": json.dumps(rich_text.get_desc())}) - if object.get_object_type() == ObjectType.Email: - email = object - prompt.messages.append({"role": "agent", "content": json.dumps(email.get_desc())}) - return prompt + parents = self.store.get_relation_store().get_related_objects(object_id) + # last parent is the root object + root_object_id = parents[-1] + if results[root_object_id] is None: + results[root_object_id] = [root_object_id, object_id] + else: + results[root_object_id].append(object_id) + content = "I found the following contents described with json format:\n" + result_desc = [] + for result in results.values(): + # first element in result is the root object + root_object = await self.__load_object(result[0]) + if root_object.get_object_type() == ObjectType.Email: + email = await self.__load_object(object_id) + desc = email.get_desc() + desc["type"] = "email" + desc["contents"] = [] + result_desc.append(desc) + upper_list = desc["contents"] + result = result[1:] + else: + upper_list = result_desc + + for object_id in result: + if object_id.get_object_type() == ObjectType.Chunk: + upper_list.append({"type": "text", "content": self.store.get_chunk_reader().get_chunk(object_id).read().decode("utf-8")}) + if object_id.get_object_type() == ObjectType.Image: + image = await self.__load_object(object_id) + desc = image.get_desc() + desc["type"] = "image" + upper_list.append(desc) + if object_id.get_object_type() == ObjectType.Video: + video = await self.__load_object(object_id) + desc = video.get_desc() + desc["type"] = "video" + upper_list.append(desc) + else: + pass + content += json.dumps(result_desc) + content += ".\n" + + prompt = AgentPrompt() + prompt.add_message("knowledge", content) + + return prompt + From c4d97942cb25d81d6f1cb040a115716bf34ae4eb Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 14 Sep 2023 01:50:18 -0700 Subject: [PATCH 047/177] Refactor chatsession impl, fix bug. --- doc/mvp/workflow.drawio | 28 ++- rootfs/agents/agent_1/agent.toml | 1 + rootfs/workflows/math_school/workflow.toml | 2 +- rootfs/workflows/plan_maker/workflow.toml | 6 +- src/aios_kernel/__init__.py | 2 +- src/aios_kernel/agent.py | 91 +++----- src/aios_kernel/agent_message.py | 96 +++++++- src/aios_kernel/bus.py | 95 ++++---- src/aios_kernel/chatsession.py | 100 ++++++--- src/aios_kernel/environment.py | 6 - src/aios_kernel/open_ai_node.py | 12 +- src/aios_kernel/workflow.py | 250 ++++++++++++++------- src/aios_kernel/workflow_env.py | 10 +- src/service/aios_shell/aios_shell.py | 12 +- src/service/aios_shell/tg_aios_shell.py | 2 +- 15 files changed, 477 insertions(+), 236 deletions(-) diff --git a/doc/mvp/workflow.drawio b/doc/mvp/workflow.drawio index 54e724c..1ca245b 100644 --- a/doc/mvp/workflow.drawio +++ b/doc/mvp/workflow.drawio @@ -1,6 +1,6 @@ - + @@ -365,4 +365,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rootfs/agents/agent_1/agent.toml b/rootfs/agents/agent_1/agent.toml index 81966ab..53d0605 100644 --- a/rootfs/agents/agent_1/agent.toml +++ b/rootfs/agents/agent_1/agent.toml @@ -1,5 +1,6 @@ instance_id = "agent_1" fullname = "tracy wang" + [[prompt]] role = "system" content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml index 48c9831..69b998b 100644 --- a/rootfs/workflows/math_school/workflow.toml +++ b/rootfs/workflows/math_school/workflow.toml @@ -7,7 +7,7 @@ GOAL="成为最好的学校" [[connected_env]] env_id = "calender" [[connected_env.event2msg]] -timer = "现在是{data}" +timer = "现在是{now}" role = "教导处主任" [filter] diff --git a/rootfs/workflows/plan_maker/workflow.toml b/rootfs/workflows/plan_maker/workflow.toml index 1dc80bf..3975313 100644 --- a/rootfs/workflows/plan_maker/workflow.toml +++ b/rootfs/workflows/plan_maker/workflow.toml @@ -42,11 +42,13 @@ fullname = "经理" agent="manager" [[roles.manager.prompt]] role="system" -content="""你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是: +content=""" +你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是: 1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。 3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。 -4. 当你决定要和工作小组通信时,请使用 sendmsg(小组名称,内容) 的形式。""" +4. 当你决定要和工作小组通信时,请使用 sendmsg(小组名称,内容) 的形式。 +""" diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bef117f..fb09d88 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -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 diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 0a3f8b0..0c3e106 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -7,7 +7,7 @@ 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 @@ -73,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 @@ -82,6 +82,7 @@ class AIAgent: self.chat_db = None self.unread_msg = Queue() # msg from other agent self.owner_env : Environment = None + self.owenr_bus : AIBus = None @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -90,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 @@ -100,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"] @@ -147,7 +148,7 @@ class AIAgent: return result_func - async def _execute_func(self,inenr_func_call_node:dict,msg_prompt:AgentPrompt) -> str: + 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") @@ -157,14 +158,21 @@ class AIAgent: 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() - msg_prompt.messages.append({"role":"function","content":result_str,"name":func_name}) - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(msg_prompt,self.llm_model_name,self.max_token_size,inner_functions) + inner_functions = self._get_inner_functions() + prompt.messages.append({"role":"function","content":result_str,"name":func_name}) + task_result:ComputeTaskResult = await ComputeKernel().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,msg_prompt) + return await self._execute_func(inner_func_call_node,prompt,org_msg) else: return task_result.result_str @@ -172,7 +180,13 @@ class AIAgent: 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_knowlege_prompt(the_role.get_name())) @@ -189,67 +203,27 @@ class AIAgent: inner_func_call_node = task_result.result_message.get("function_call") if inner_func_call_node: - final_result = await self._execute_func(inner_func_call_node,msg_prompt) + #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 @@ -263,7 +237,8 @@ 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): diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 242cc66..0640319 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -3,26 +3,100 @@ 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 @@ -31,7 +105,7 @@ 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 @@ -39,6 +113,12 @@ class AgentMsg: 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) diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py index ae1853e..22bf209 100644 --- a/src/aios_kernel/bus.py +++ b/src/aios_kernel/bus.py @@ -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,11 +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: - target_id = target_id.split(".")[0] + 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) @@ -44,46 +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: - target_id = target_id.split(".")[0] - 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 @@ -93,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 @@ -102,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 @@ -126,14 +133,4 @@ class AIBus: logger.warn(f"handler {target_name} is already working!") return - handler.working_task = asyncio.create_task(self.process_queue(handler)) - - - -#send message to target logic: -# find target handler: -# process_message(msg): -# session = get_session(msg.sender,msg.target) - - -# history: open(sender,target,topic) \ No newline at end of file + handler.working_task = asyncio.create_task(self.process_queue(handler)) \ No newline at end of file diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py index ecd90a3..5893683 100644 --- a/src/aios_kernel/chatsession.py +++ b/src/aios_kernel/chatsession.py @@ -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""" diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 3a11d3b..49ea72b 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -70,12 +70,6 @@ class Environment: def _do_get_value(self,key:str) -> Optional[str]: pass - def get_functions(self): - # system functions - # env functions - # user install functions - pass - def register_get_handler(self,key:str,handler:Callable) -> None: h = self.get_handlers.get(key) if h is not None: diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 1be47fd..3a665a3 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -54,11 +54,17 @@ class OpenAI_ComputeNode(ComputeNode): prompts = task.params["prompts"] logger.info(f"call openai {mode_name} prompts: {prompts}") - resp = openai.ChatCompletion.create(model=mode_name, + if task.params.get("inner_functions") is None: + 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? + 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}") diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 0a01c29..58d0538 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -2,21 +2,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__) class MessageFilter: @@ -158,11 +160,12 @@ class Workflow: 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) + 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) + return await sub_workflow._process_msg(msg) logger.error(f"{msg.target} not found! forword message failed!") return None else: @@ -176,18 +179,50 @@ class Workflow: 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): - # workflow can be a message handler, but never be a message sender - # all message forword to roles or sub workflow - # workflow no chatsession record, but role have - - final_result = None - + 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 + + # 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: @@ -195,27 +230,13 @@ 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 - - final_result = result - else: - logger.error(f"input_filter return None for :{msg}") - return - else: - # no input filter, we would process all message, slowly,not suggest to use - results = {} - final_result:AgentMsg = None - 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 - final_result = a_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 @@ -276,52 +297,59 @@ class Workflow: return r - async def role_post_msg(self,msg:AgentMsg,the_role:AIRole): + 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.id} to inner role: {msg.target}") - asyncio.create_task(self.role_process_msg(msg,target_role)) + 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.id} to sub workflow: {msg.target}") + 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.id} to AIBus: {msg.target}") + 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): + 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.id} to inner role: {msg.target}") - return await self.role_process_msg(msg,target_role) + 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) - 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.id} to sub workflow: {msg.target}") + 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.id} to AIBus: {msg.target}") - return await self.get_bus().send_message(msg.target,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]}") - return """{result:"timeout"}""" + 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 + return await self.role_call(call,the_role) def _format_msg_by_env_value(self,prompt:AgentPrompt): if self.workflow_env is None: @@ -331,66 +359,148 @@ class Workflow: 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}" - session_owner = the_role.get_role_id() - chatsession : AIChatSession = AIChatSession.get_session(session_owner,session_topic,self.db_file) - if chatsession is None: - logger.error(f"get session {session_topic}@{session_owner} failed!") + def _get_inner_functions(self) -> dict: + all_inner_function = self.workflow_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) + 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().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_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)) + + #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) 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().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size()) + task_result:ComputeTaskResult = await ComputeKernel().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 - result = Workflow.prase_llm_result(result_str) logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{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? + result_str = await self._role_execute_func(the_role,inner_func_call_node,prompt,msg) + + 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 - await self.role_post_msg(postmsg,the_role) + + 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 = AgentMsg() - resp_msg.topic = msg.topic - resp_msg.set(session_owner,msg.sender,result.resp) - - chatsession.append_recv(msg) - chatsession.append_post(resp_msg) + 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 - send_resp = await self.role_send_msg(sendmsg,the_role) + 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}] @@ -410,18 +520,10 @@ class Workflow: result_prompt.messages.append({"role":"user","content":f"{msg.body}"}) return result_prompt - - - def _get_function_prompt(self,role_name:str) -> AgentPrompt: - # system functions - # env functions - # user install functions - pass def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt: pass - def get_workflow_rule_prompt(self) -> AgentPrompt: return self.rule_prompt @@ -447,6 +549,4 @@ class Workflow: # the_env.attach_event_handler(k,_env_msg_handler) # break - else: - logger.warn(f"environment {env.get_id()} already connected!") diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 9ca2ee3..1107c8f 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -29,7 +29,7 @@ class CalenderEnvironment(Environment): self.add_ai_function(SimpleAIFunction("get_time", "get current time", - self.get_now)) + self._get_now)) def _do_get_value(self,key:str) -> Optional[str]: return None @@ -58,7 +58,12 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - async def get_now(self) -> str: + def get_now(self,key)->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 @@ -71,6 +76,7 @@ class WorkflowEnvironment(Environment): self.local = threading.local() self.table_name = "WorkflowEnv_" + env_id + def _get_conn(self): """ get db connection """ if not hasattr(self.local, 'conn'): diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 27f87e4..73d8c93 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -26,7 +26,7 @@ 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 +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession sys.path.append(directory + '/../../component/') from agent_manager import AgentManager @@ -73,6 +73,7 @@ class AIOS_Shell: open_ai_node.start() ComputeKernel().add_compute_node(open_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) return True @@ -83,7 +84,7 @@ class AIOS_Shell: 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: @@ -91,6 +92,9 @@ class AIOS_Shell: async def install_workflow(self,workflow_id:Workflow) -> None: pass + + async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg: + pass async def call_func(self,func_name, args): match func_name: @@ -115,6 +119,7 @@ class AIOS_Shell: 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 @@ -134,7 +139,7 @@ class AIOS_Shell: 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) @@ -171,6 +176,7 @@ async def main(): format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s') shell = AIOS_Shell("user") await shell.initial() + print(f"aios shell {shell.get_version()} ready.") completer = WordCompleter(['send($target,$msg,$topic)', diff --git a/src/service/aios_shell/tg_aios_shell.py b/src/service/aios_shell/tg_aios_shell.py index 98844ae..6160ec8 100644 --- a/src/service/aios_shell/tg_aios_shell.py +++ b/src/service/aios_shell/tg_aios_shell.py @@ -14,7 +14,7 @@ from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandl directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession sys.path.append(directory + '/../../component/') from agent_manager import AgentManager From 937385f83cf080912099d5888e7646e41b3b217e Mon Sep 17 00:00:00 2001 From: liyaxing Date: Thu, 14 Sep 2023 20:09:20 +0800 Subject: [PATCH 048/177] Improve ObjectID with remove object_type and __eq__ method --- src/knowledge/object/object_id.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/knowledge/object/object_id.py b/src/knowledge/object/object_id.py index 6ce77ae..8278183 100644 --- a/src/knowledge/object/object_id.py +++ b/src/knowledge/object/object_id.py @@ -17,16 +17,12 @@ class ObjectType(Enum): # define a object ID class to identify a object class ObjectID: # pylint: disable=too-few-public-methods - def __init__(self, object_type: ObjectType, value: bytes): + def __init__(self, value: bytes): assert len(value) == 32, "ObjectID must be 32 bytes long" - self.object_type = object_type self.value = value def __str__(self): return self.to_base58() - - def get_object_type(self): - return self.object_type def to_base58(self): return base58.b58encode(self.value).decode() @@ -48,8 +44,11 @@ class ObjectID: # pylint: disable=too-few-public-methods @staticmethod def new_chunk_id(chunk_hash: HashValue): - return ObjectID(ObjectType.Chunk, chunk_hash.value) + return ObjectID(chunk_hash.value) @staticmethod def hash_data(data: bytes): - return ObjectID.new_chunk_id(HashValue.hash_data(data)) \ No newline at end of file + return ObjectID.new_chunk_id(HashValue.hash_data(data)) + + def __eq__(self, other) -> bool: + return self.value == other.value \ No newline at end of file From e1bba92ec2c708ed0108448dfaf94f8bae8f7326 Mon Sep 17 00:00:00 2001 From: liyaxing Date: Thu, 14 Sep 2023 20:10:07 +0800 Subject: [PATCH 049/177] Add object relation store impl and test case --- src/knowledge/object/relation.py | 102 +++++++++++++++++++++---------- test/test_object.py | 41 +++++++++++-- 2 files changed, 107 insertions(+), 36 deletions(-) diff --git a/src/knowledge/object/relation.py b/src/knowledge/object/relation.py index f6eca2e..34c0911 100644 --- a/src/knowledge/object/relation.py +++ b/src/knowledge/object/relation.py @@ -1,64 +1,104 @@ # define a relation store class -import sqlite3 -import os -import logging from .object_id import ObjectID +import sqlite3 +from typing import List, Tuple, Optional +import logging +import os +from enum import Enum + + +class ObjectRelationType(Enum): + Parent = 1 class ObjectRelationStore: def __init__(self, root_dir: str): if not os.path.exists(root_dir): os.makedirs(root_dir) - file = os.path.join(root_dir, "relationships.db") - logging.info(f"will init chunk tracker, db={file}") + file = os.path.join(root_dir, "relation.db") + logging.info(f"will init object relation store, db={file}") self.conn = sqlite3.connect(file) self.cursor = self.conn.cursor() self.cursor.execute( """ - CREATE TABLE IF NOT EXISTS relationships ( - id TEXT NOT NULL, - parent TEXT NOT NULL, - PRIMARY KEY(id, parent) + CREATE TABLE IF NOT EXISTS relations ( + object_id TEXT, + assoc_id TEXT, + relation_type TEXT, + PRIMARY KEY (object_id, assoc_id, relation_type) ) """ ) - self.conn.commit() - def add_relation(self, object: ObjectID, parent: ObjectID): + def add_relation( + self, + object_id: ObjectID, + assoc_id: ObjectID, + relation_type: ObjectRelationType = ObjectRelationType.Parent, + ): + if relation_type == None: + relation_type = ObjectRelationType.Parent + self.cursor.execute( """ - INSERT INTO relationships (id, parent) - VALUES (?, ?, ?, ?, ?) + INSERT OR IGNORE INTO relations (object_id, assoc_id, relation_type) + VALUES (?, ?, ?) """, - ( - str(object), - str(parent), - ), + (str(object_id), str(assoc_id), relation_type.value), ) self.conn.commit() - def get_related_objects(self, object: ObjectID) -> [ObjectID]: - parents = [] - cur = object - while True: + def get_related_objects( + self, object_id: ObjectID, relation_type: Optional[ObjectRelationType] = None + ) -> List[ObjectID]: + if relation_type: self.cursor.execute( """ - SELECT id, parent FROM relationships WHERE id = ? + SELECT assoc_id FROM relations WHERE object_id = ? AND relation_type = ? """, - (str(cur),), + (str(object_id), relation_type.value), ) - parent = self.cursor.fetchone() - if parent is None: - break - parents.append(parent) - cur = parent + else: + self.cursor.execute( + """ + SELECT assoc_id FROM relations WHERE object_id = ? + """, + (str(object_id),), + ) + return [ObjectID.from_base58(row[0]) for row in self.cursor.fetchall()] - def delete_relation(self, object_id: ObjectID) -> [ObjectID]: + def get_related_root_objects( + self, object_id: ObjectID, relation_type: Optional[ObjectRelationType] = None + ) -> List[ObjectID]: + root_objects = [] + related_objects = self.get_related_objects(object_id, relation_type) + history = [] + history.append(object_id) + + while related_objects: + for obj in related_objects: + next_related_objects = self.get_related_objects(obj, relation_type) + if not next_related_objects: + if obj not in root_objects: + root_objects.append(obj) + else: + for related_object in next_related_objects: + if obj not in history: + related_objects.append(related_object) + else: + logging.warning( + f"loop detected: {obj} <-> {related_object}" + ) + related_objects = next_related_objects + + return root_objects + + def delete_relation(self, object_id: ObjectID): self.cursor.execute( """ - DELETE FROM relationships WHERE id = ? + DELETE FROM relations WHERE object_id = ? """, (str(object_id),), ) - self.conn.commit() \ No newline at end of file + self.conn.commit() diff --git a/test/test_object.py b/test/test_object.py index 1e1bc56..0f5620e 100644 --- a/test/test_object.py +++ b/test/test_object.py @@ -17,25 +17,56 @@ handler.setFormatter(formatter) root.addHandler(handler) -from knowledge import ObjectID, HashValue, EmailObjectBuilder +from knowledge import ( + ObjectID, + HashValue, + EmailObjectBuilder, + ObjectRelationStore, + KnowledgeStore, +) import asyncio import unittest class TestVectorSTorage(unittest.TestCase): def test_object(self): - data = HashValue.hash_data("1233".encode("utf-8")); + data = HashValue.hash_data("1233".encode("utf-8")) print(data.to_base58()) print(data.to_base36()) - + data2 = HashValue.from_base58(data.to_base58()) self.assertEqual(data.to_base36(), data2.to_base36()) - + data2 = HashValue.from_base36(data.to_base36()) self.assertEqual(data.to_base58(), data2.to_base58()) email_folder = "F:\\system\\Downloads\\8081ffdb80925f5bff9c6ab9c4756c7d" email_object = EmailObjectBuilder({}, email_folder).build() - + + def test_relation(self): + obj1 = ObjectID.hash_data("12345".encode("utf-8")) + obj2 = ObjectID.hash_data("67890".encode("utf-8")) + obj3 = ObjectID.hash_data("abcde".encode("utf-8")) + obj4 = ObjectID.hash_data("fghij".encode("utf-8")) + print(obj1.to_base58(), obj2.to_base58(), obj3.to_base58()) + relation_store = KnowledgeStore().get_relation_store() + relation_store.add_relation(obj1, obj2) + relation_store.add_relation(obj1, obj2) + relation_store.add_relation(obj2, obj3) + + relation_store.add_relation(obj1, obj3) + relation_store.add_relation(obj1, obj4) + + objs = relation_store.get_related_objects(obj2) + self.assertEqual(len(objs), 1) + self.assertEqual(objs[0], obj3) + + objs = relation_store.get_related_root_objects(obj1) + self.assertEqual(len(objs), 2) + self.assertEqual(obj3 in objs, True) + self.assertEqual(obj4 in objs, True) + # self.assertCountEqual(objs, [obj3, obj4]) + + if __name__ == "__main__": unittest.main() From 6ea83729a570711a36682d1b0a86976206963f2f Mon Sep 17 00:00:00 2001 From: liyaxing Date: Thu, 14 Sep 2023 20:57:53 +0800 Subject: [PATCH 050/177] Refactoring the EmailObject build process --- src/knowledge/core_object/document_object.py | 13 +++++-- src/knowledge/core_object/email_object.py | 41 ++++++++++++++++---- src/knowledge/data/writer.py | 2 +- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/knowledge/core_object/document_object.py b/src/knowledge/core_object/document_object.py index 6d6270e..02056bc 100644 --- a/src/knowledge/core_object/document_object.py +++ b/src/knowledge/core_object/document_object.py @@ -1,4 +1,4 @@ -from ..object import KnowledgeObject +from ..object import KnowledgeObject, ObjectRelationStore from ..data import ChunkList, ChunkListWriter from ..object import ObjectType from .. import KnowledgeStore @@ -49,9 +49,16 @@ class DocumentObjectBuilder: self.text = text return self - def build(self) -> DocumentObject: + def build(self, relation_store: ObjectRelationStore) -> DocumentObject: chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text( self.text, 1024 * 4, ) - return DocumentObject(self.meta, self.tags, chunk_list) + doc = DocumentObject(self.meta, self.tags, chunk_list) + doc_id = doc.calculate_id() + + # Add relation to store + for chunk_id in chunk_list.chunk_list: + relation_store.add_relation(chunk_id, doc_id) + + return doc diff --git a/src/knowledge/core_object/email_object.py b/src/knowledge/core_object/email_object.py index f563052..c86c3a2 100644 --- a/src/knowledge/core_object/email_object.py +++ b/src/knowledge/core_object/email_object.py @@ -1,9 +1,9 @@ +from .. import KnowledgeStore from .rich_text_object import RichTextObject, RichTextObjectBuilder from ..object import ObjectID, ObjectType, KnowledgeObject from .document_object import DocumentObjectBuilder from .image_object import ImageObjectBuilder from .video_object import VideoObjectBuilder -from .rich_text_object import RichTextObjectBuilder import os import json import logging @@ -69,6 +69,11 @@ class EmailObjectBuilder: return self def build(self) -> EmailObject: + + # Just get the object store and relation store from global KnowledgeStore + store = KnowledgeStore().get_object_store() + relation = KnowledgeStore().get_relation_store() + # Read meta.json meta = {} meta_file = os.path.join(self.folder, "meta.json") @@ -89,8 +94,10 @@ class EmailObjectBuilder: with open(content_file, "r", encoding="utf-8") as f: text = f.read() - document = DocumentObjectBuilder({}, {}, text).build() - documents = {"email.txt": document} + document = DocumentObjectBuilder({}, {}, text).build(relation_store=relation) + document_id = document.calculate_id() + store.put_object(document_id, document.encode()) + documents = {"email.txt": document_id} except Exception as e: logging.error(f"Failed to read email.txt {content_file} {e}") else: @@ -106,7 +113,9 @@ class EmailObjectBuilder: try: image = ImageObjectBuilder({}, {}, image_path).build() - images[image_file] = image + image_id = image.calculate_id() + store.put_object(image_id, image.encode()) + images[image_file] = image_id except Exception as e: logging.error(f"Failed to read image file {image_path} {e}") continue @@ -121,13 +130,31 @@ class EmailObjectBuilder: try: video = VideoObjectBuilder({}, {}, video_path).build() - videos[video_file] = video + video_id = video.calculate_id() + store.put_object(video_id, video.encode()) + videos[video_file] = video_id except Exception as e: logging.error(f"Failed to read video file {video_path} {e}") continue # Create RichTextObject rich_text = RichTextObject(images, videos, documents) - + rich_text_id = rich_text.calculate_id() + + # build relations with rich_text + for image_id in images.values(): + relation.add_relation(image_id, rich_text_id) + for video_id in videos.values(): + relation.add_relation(video_id, rich_text_id) + for document_id in documents.values(): + relation.add_relation(document_id, rich_text_id) + # Create EmailObject - return EmailObject(meta, {}, rich_text) + email_object = EmailObject(meta, {}, rich_text) + email_object_id = email_object.calculate_id() + store.put_object(email_object_id, email_object.encode()) + + # build relations with email_object + relation.add_relation(rich_text_id, email_object_id) + + return email_object diff --git a/src/knowledge/data/writer.py b/src/knowledge/data/writer.py index 7d38e9d..599c51b 100644 --- a/src/knowledge/data/writer.py +++ b/src/knowledge/data/writer.py @@ -52,7 +52,7 @@ class ChunkListWriter: def create_chunk_list_from_text( self, text: str, chunk_max_words: int, separator_chars: str = ".," - ) -> Tuple[List[ChunkID], HashValue]: + ) -> ChunkList: text_list = self._split_text_list(text, chunk_max_words, separator_chars) chunk_list = [] hash_obj = hashlib.sha256() From cbfff71d6bffc663f61d626879704066d358c7b6 Mon Sep 17 00:00:00 2001 From: wugren Date: Fri, 15 Sep 2023 00:33:18 +0800 Subject: [PATCH 051/177] add whisper speech to text node and google text to speech node --- src/aios_kernel/__init__.py | 6 +- src/aios_kernel/google_text_to_speech_node.py | 104 ++++++++++++++++ src/aios_kernel/whisper_node.py | 111 ++++++++++++++++++ src/requirements.txt | 1 + 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/aios_kernel/google_text_to_speech_node.py create mode 100644 src/aios_kernel/whisper_node.py diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bef117f..2a87b5c 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -4,8 +4,10 @@ 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 .open_ai_node import OpenAI_ComputeNode from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus -from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent +from .whisper_node import WhisperComputeNode +from .google_text_to_speech_node import GoogleTextToSpeechNode diff --git a/src/aios_kernel/google_text_to_speech_node.py b/src/aios_kernel/google_text_to_speech_node.py new file mode 100644 index 0000000..7709eef --- /dev/null +++ b/src/aios_kernel/google_text_to_speech_node.py @@ -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 diff --git a/src/aios_kernel/whisper_node.py b/src/aios_kernel/whisper_node.py new file mode 100644 index 0000000..b036ad5 --- /dev/null +++ b/src/aios_kernel/whisper_node.py @@ -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 diff --git a/src/requirements.txt b/src/requirements.txt index 605dc72..7de369e 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,3 +6,4 @@ Pillow==10.0.0 prompt_toolkit==3.0.39 stability_sdk==0.8.4 toml==0.10.2 +google-cloud-texttospeech==2.14.1 From b59af5d4a7cc0acf61793fe1d7de5fdefd75229b Mon Sep 17 00:00:00 2001 From: liyaxing Date: Fri, 15 Sep 2023 14:01:26 +0800 Subject: [PATCH 052/177] Fix object encode --- src/knowledge/data/chunk.py | 4 ++-- src/knowledge/object/object.py | 24 +++++++++++++++++------- src/knowledge/object/object_id.py | 4 ++-- src/knowledge/object/relation.py | 4 ++-- test/test_object.py | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/knowledge/data/chunk.py b/src/knowledge/data/chunk.py index 5267dc3..76f2058 100644 --- a/src/knowledge/data/chunk.py +++ b/src/knowledge/data/chunk.py @@ -1,9 +1,9 @@ -from enum import Enum +from enum import IntEnum from ..object import ObjectID ChunkID = ObjectID -class PositionType(Enum): +class PositionType(IntEnum): Unknown = 1 Device = 2 File = 3 diff --git a/src/knowledge/object/object.py b/src/knowledge/object/object.py index b3841ed..490555b 100644 --- a/src/knowledge/object/object.py +++ b/src/knowledge/object/object.py @@ -1,4 +1,3 @@ - # define a object type enum from abc import ABC, abstractmethod from enum import Enum @@ -6,6 +5,16 @@ from .object_id import ObjectID, ObjectType import hashlib import json import pickle +from typing import Any + + +class ObjectEnhancedJSONEncoder(json.JSONEncoder): + def default(self, o: Any) -> Any: + if isinstance(o, ObjectID): + return o.to_base58() + + return super().default(o) + class KnowledgeObject(ABC): def __init__(self, object_type: ObjectType, desc: dict = {}, body: dict = {}): @@ -18,7 +27,7 @@ class KnowledgeObject(ABC): def object_id(self) -> ObjectID: return self.calculate_id() - + def set_desc_with_key_value(self, key, value): self.desc[key] = value @@ -27,7 +36,7 @@ class KnowledgeObject(ABC): def get_desc(self) -> dict: return self.desc - + def set_body_with_key_value(self, key, value): self.body[key] = value @@ -36,10 +45,13 @@ class KnowledgeObject(ABC): def get_body(self) -> dict: return self.body - + def calculate_id(self): # Convert the object_type and desc to string and compute the SHA256 hash - data = json.dumps({"object_type": self.object_type, "desc": self.desc}) + data = json.dumps( + {"object_type": self.object_type, "desc": self.desc}, + cls=ObjectEnhancedJSONEncoder, + ) sha256 = hashlib.sha256() sha256.update(data.encode()) return ObjectID(sha256.digest()) @@ -50,5 +62,3 @@ class KnowledgeObject(ABC): @staticmethod def decode(data: bytes): return pickle.loads(data) - - diff --git a/src/knowledge/object/object_id.py b/src/knowledge/object/object_id.py index 8278183..42f313a 100644 --- a/src/knowledge/object/object_id.py +++ b/src/knowledge/object/object_id.py @@ -1,12 +1,12 @@ # define a object type enum from abc import ABC, abstractmethod -from enum import Enum +from enum import IntEnum from .hash import HashValue import base58 import base36 -class ObjectType(Enum): +class ObjectType(IntEnum): Chunk = 7 Image = 101 Video = 102 diff --git a/src/knowledge/object/relation.py b/src/knowledge/object/relation.py index 34c0911..f273279 100644 --- a/src/knowledge/object/relation.py +++ b/src/knowledge/object/relation.py @@ -4,10 +4,10 @@ import sqlite3 from typing import List, Tuple, Optional import logging import os -from enum import Enum +from enum import IntEnum -class ObjectRelationType(Enum): +class ObjectRelationType(IntEnum): Parent = 1 diff --git a/test/test_object.py b/test/test_object.py index 0f5620e..655e2a2 100644 --- a/test/test_object.py +++ b/test/test_object.py @@ -23,6 +23,7 @@ from knowledge import ( EmailObjectBuilder, ObjectRelationStore, KnowledgeStore, + EmailObject, ) import asyncio import unittest @@ -42,6 +43,20 @@ class TestVectorSTorage(unittest.TestCase): email_folder = "F:\\system\\Downloads\\8081ffdb80925f5bff9c6ab9c4756c7d" email_object = EmailObjectBuilder({}, email_folder).build() + + id = email_object.calculate_id() + print(f"got email object: {id.to_base58()}") + + # test encode & decode + ret = email_object.encode() + obj = EmailObject.decode(ret) + id2 = obj.calculate_id() + print(f"got email object: {id2.to_base58()}") + self.assertEqual(id.to_base58(), id2.to_base58()) + + ret2 = obj.encode() + self.assertEqual(ret, ret2) + def test_relation(self): obj1 = ObjectID.hash_data("12345".encode("utf-8")) From ef9ab83e63cb40249e7332a892fb6e28a4b32fa7 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 15 Sep 2023 17:35:12 -0700 Subject: [PATCH 053/177] Add tunnel support ,and implment TelegramTunnel. Now we can use telegram bot connect to installed agent/workflow. --- rootfs/tunnels.cfg.toml | 6 + src/aios_kernel/__init__.py | 4 +- src/aios_kernel/agent.py | 4 +- src/aios_kernel/contact_manager.py | 34 ++++ src/aios_kernel/tg_tunnel.py | 116 ++++++++++++ src/aios_kernel/tunnel.py | 63 +++++++ src/service/aios_shell/aios_shell.py | 12 +- src/service/aios_shell/tg_aios_shell.py | 223 ------------------------ 8 files changed, 234 insertions(+), 228 deletions(-) create mode 100644 rootfs/tunnels.cfg.toml create mode 100644 src/aios_kernel/contact_manager.py create mode 100644 src/aios_kernel/tg_tunnel.py create mode 100644 src/aios_kernel/tunnel.py delete mode 100644 src/service/aios_shell/tg_aios_shell.py diff --git a/rootfs/tunnels.cfg.toml b/rootfs/tunnels.cfg.toml new file mode 100644 index 0000000..c89e504 --- /dev/null +++ b/rootfs/tunnels.cfg.toml @@ -0,0 +1,6 @@ +[[tunnels]] +tunnel_id = "MyRoobot" +type="TelegramTunnel" +target="agent_1" +token="" + diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index fb09d88..0d582df 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -8,4 +8,6 @@ from .open_ai_node import OpenAI_ComputeNode from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus -from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent +from .tunnel import AgentTunnel +from .tg_tunnel import TelegramTunnel \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 0c3e106..fd25af2 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -243,8 +243,8 @@ class AIAgent: 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 diff --git a/src/aios_kernel/contact_manager.py b/src/aios_kernel/contact_manager.py new file mode 100644 index 0000000..cdc050a --- /dev/null +++ b/src/aios_kernel/contact_manager.py @@ -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) \ No newline at end of file diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py new file mode 100644 index 0000000..efa23fc --- /dev/null +++ b/src/aios_kernel/tg_tunnel.py @@ -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) + + + \ No newline at end of file diff --git a/src/aios_kernel/tunnel.py b/src/aios_kernel/tunnel.py new file mode 100644 index 0000000..bbd18d3 --- /dev/null +++ b/src/aios_kernel/tunnel.py @@ -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 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 73d8c93..b6364c1 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -4,6 +4,7 @@ import sys import os import logging import re +import toml from typing import Any, Optional, TypeVar, Tuple, Sequence import argparse @@ -26,12 +27,12 @@ shell_style = Style.from_dict({ directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager -from aios_kernel import CalenderEnvironment,Environment + class AIOS_Shell: @@ -74,6 +75,13 @@ class AIOS_Shell: ComputeKernel().add_compute_node(open_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() + + tunnels_config_path = os.path.abspath(directory + "/../../../rootfs/tunnels.cfg.toml") + tunnel_config = toml.load(tunnels_config_path) + await AgentTunnel.load_all_tunnels_from_config(tunnel_config["tunnels"]) + return True diff --git a/src/service/aios_shell/tg_aios_shell.py b/src/service/aios_shell/tg_aios_shell.py deleted file mode 100644 index 6160ec8..0000000 --- a/src/service/aios_shell/tg_aios_shell.py +++ /dev/null @@ -1,223 +0,0 @@ - -import asyncio -import sys -import os -import logging -import re - -from typing import Any, Optional, TypeVar, Tuple, Sequence -import argparse - -from telegram import ForceReply, Update -from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters - - -directory = os.path.dirname(__file__) -sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession - -sys.path.append(directory + '/../../component/') -from agent_manager import AgentManager -from workflow_manager import WorkflowManager -from aios_kernel import CalenderEnvironment,Environment - -import logging - -# Enable logging -logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO -) -# set higher logging level for httpx to avoid all GET and POST requests being logged -logging.getLogger("httpx").setLevel(logging.WARNING) - -logger = logging.getLogger(__name__) - -class AIOS_Shell: - def __init__(self,username:str) -> None: - self.username = username - self.current_target = "_" - self.current_topic = "default" - - async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: - target_id = msg.target.split(".")[0] - agent : AIAgent = await AgentManager().get(target_id) - if agent is not None: - bus.register_message_handler(target_id,agent._process_msg) - return True - - a_workflow = await WorkflowManager().get_workflow(target_id) - if a_workflow is not None: - 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) - if agent is not None: - return True - else: - return False - - async def initial(self) -> bool: - cal_env = CalenderEnvironment("calender") - cal_env.start() - Environment.set_env_by_id("calender",cal_env) - - 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) - AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) - return True - - - def get_version(self) -> str: - return "0.0.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) - if resp is not None: - return resp.body - else: - return "error!" - - async def install_workflow(self,workflow_id:Workflow) -> None: - pass - - async def call_func(self,func_name, args): - match func_name: - case 'send': - target_id = args[0] - msg_content = args[1] - topic = args[2] - resp = await self.send_msg(msg_content,target_id,topic,self.username) - - - return resp - case 'open': - if len(args) >= 1: - target_id = args[0] - if len(args) >= 2: - topic = args[1] - - self.current_target = target_id - self.current_topic = topic - show_text = f"current session switch to {topic}@{target_id}" - return show_text - - - case 'history': - num = 10 - offset = 0 - 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 - else: - db_path = WorkflowManager().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) - result_str = "" - for msg in reversed(msgs): - result_str += f"{msg.sender} >>> {msg.body}" - result_str += f"\n-------------------\n" - return result_str - return f"chatsession not found" - case 'help': - return "help info " - -shell:AIOS_Shell = None - -async def _init_shell(): - global shell - if shell is None: - shell = AIOS_Shell("user") - await shell.initial() - return - -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 start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Send a message when the command /start is issued.""" - await _init_shell() - user = update.effective_user - await update.message.reply_html( - rf"Hi {user.mention_html()}!", - reply_markup=ForceReply(selective=True), - ) - - -async def on_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - await _init_shell() - cmd_msg = update.message.text[1:] - logger.info("command:" + cmd_msg) - func_call = parse_function_call(cmd_msg) - if func_call: - shell.username = update.effective_user - show_text = await shell.call_func(func_call[0], func_call[1]) - await update.message.reply_text(show_text) - else: - await update.message.reply_text("command not found!") - -async def on_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - await _init_shell() - shell.username = update.effective_user - resp = await shell.send_msg(update.message.text,shell.current_target,shell.current_topic,shell.username) - await update.message.reply_text(resp) - - - - -def main() -> None: - """Start the bot.""" - # Create the Application and pass it your bot's token. - tg_robot_token = os.getenv("TG_ROBOT_TOKEN") - if tg_robot_token is None: - print("TG_ROBOT_TOKEN not found!") - tg_robot_token = input("input your telegram robot token:") - - - if tg_robot_token is None: - return - - application = Application.builder().token(tg_robot_token).build() - - # on different commands - answer in Telegram - application.add_handler(CommandHandler("start", start)) - application.add_handler(CommandHandler("help", on_command)) - application.add_handler(CommandHandler("send", on_command)) - application.add_handler(CommandHandler("open", on_command)) - application.add_handler(CommandHandler("history", on_command)) - - # on non command i.e message - echo the message on Telegram - application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_message)) - - # Run the bot until the user presses Ctrl-C - application.run_polling(allowed_updates=Update.ALL_TYPES) - - -if __name__ == "__main__": - main() \ No newline at end of file From 43c70489e791230ce35733dae6294c54021f8ecb Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sat, 16 Sep 2023 11:41:59 -0700 Subject: [PATCH 054/177] 1) Add e-mail tunnel support 2) Modify all __new__ to get_instance() 3) init docker build scripts --- .dockerignore | 27 ++++ Dockerfile | 10 ++ rootfs/tunnels.cfg.toml | 23 ++- src/aios_kernel/__init__.py | 3 +- src/aios_kernel/agent.py | 6 +- src/aios_kernel/compute_kernel.py | 15 +- src/aios_kernel/email_tunnel.py | 143 ++++++++++++++++++ src/aios_kernel/open_ai_node.py | 17 ++- src/aios_kernel/stability_node.py | 22 +-- src/aios_kernel/workflow.py | 10 +- src/component/agent_manager/agent_manager.py | 6 +- src/component/package_manager/env.py | 9 +- .../workflow_manager/workflow_manager.py | 5 +- src/requirements.txt | 13 +- src/service/aios_shell/aios_shell.py | 33 ++-- 15 files changed, 272 insertions(+), 70 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 src/aios_kernel/email_tunnel.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..58b3427 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/*venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/.db +**/.python-version diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..65296e4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11 +WORKDIR /opt/aios +COPY ./src /opt/aios +COPY ./rootfs /var/aios +RUN pip install --no-cache-dir -r /opt/opendan/requirements.txt + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +CMD ["python3","./service/aios_shell/aios_shell.py"] \ No newline at end of file diff --git a/rootfs/tunnels.cfg.toml b/rootfs/tunnels.cfg.toml index c89e504..07682fc 100644 --- a/rootfs/tunnels.cfg.toml +++ b/rootfs/tunnels.cfg.toml @@ -1,6 +1,19 @@ -[[tunnels]] -tunnel_id = "MyRoobot" -type="TelegramTunnel" -target="agent_1" -token="" +#[[tunnels]] +#tunnel_id = "MyRoobot" +#type="TelegramTunnel" +#target="agent_1" +#token="" + +#[[tunnels]] +#tunnel_id="MyEmailRobot" +#type="EmailTunnel" +#target="agent_1" + +#email="your_email@msn.com" +#imap="outlook.office365.com:993" +#smtp="outlook.office365.com:587" +#user="" +#password="" +#folder="inbox" +#interval=10 \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 0d582df..ddf63b5 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -10,4 +10,5 @@ from .workflow import Workflow from .bus import AIBus from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent from .tunnel import AgentTunnel -from .tg_tunnel import TelegramTunnel \ No newline at end of file +from .tg_tunnel import TelegramTunnel +from .email_tunnel import EmailTunnel \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index fd25af2..52dd2df 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -82,7 +82,7 @@ class AIAgent: self.chat_db = None self.unread_msg = Queue() # msg from other agent self.owner_env : Environment = None - self.owenr_bus : AIBus = None + self.owenr_bus = None @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -164,7 +164,7 @@ class AIAgent: inner_functions = self._get_inner_functions() prompt.messages.append({"role":"function","content":result_str,"name":func_name}) - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) + 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() @@ -198,7 +198,7 @@ class AIAgent: inner_functions = self._get_inner_functions() - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,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") diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 5fea958..b69b879 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -17,19 +17,14 @@ logger = logging.getLogger(__name__) 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 = {} diff --git a/src/aios_kernel/email_tunnel.py b/src/aios_kernel/email_tunnel.py new file mode 100644 index 0000000..c4f75b4 --- /dev/null +++ b/src/aios_kernel/email_tunnel.py @@ -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}") diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 3a665a3..2e87710 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -13,20 +13,17 @@ 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 + def __init__(self) -> None: super().__init__() - if self.is_start is True: - logger.warn("OpenAI_ComputeNode is already start") - return - self.is_start = True + self.is_start = False # openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud self.openai_api_key = "" self.node_id = "openai_node" @@ -89,6 +86,10 @@ class OpenAI_ComputeNode(ComputeNode): return result def start(self): + if self.is_start is True: + return + self.is_start = True + async def _run_task_loop(): while True: task = await self.task_queue.get() diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index 8d40878..1efc7c8 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -16,20 +16,17 @@ 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 - def __new__(cls): - if cls._instanace is None: - cls._instanace = super(Stability_ComputeNode, cls).__new__(cls) - cls._instanace.is_start = False - return cls._instanace def __init__(self) -> None: super().__init__() - if self.is_start is True: - logger.warn("Stability_ComputeNode is already start") - return - self.is_start = True + self.is_start = False self.node_id = "stability_node" self.api_key = "" self.engine = "stable-diffusion-512-v2-1" @@ -88,8 +85,8 @@ class Stability_ComputeNode(ComputeNode): for resp in answers: for artifact in resp.artifacts: - logger.info("artifact:", artifact.id, - artifact.type, artifact.finish_reason) + 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: @@ -108,6 +105,9 @@ class Stability_ComputeNode(ComputeNode): 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...") diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 58d0538..46b7507 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -122,7 +122,11 @@ class Workflow: 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")) + 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 @@ -391,7 +395,7 @@ class Workflow: inner_functions = self._get_inner_functions() prompt.messages.append({"role":"function","content":result_str,"name":func_name}) - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt, + 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) @@ -431,7 +435,7 @@ class Workflow: async def _do_process_msg(): #TODO: send msg to agent might be better? - task_result:ComputeTaskResult = await ComputeKernel().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size(),inner_functions) + 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}") diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index 9b05b62..7c27215 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -11,12 +11,12 @@ logger = logging.getLogger(__name__) class AgentManager: _instance = None - def __new__(cls): + @classmethod + def get_instance(cls): if cls._instance is None: - cls._instance = super(AgentManager, cls).__new__(cls) + cls._instance = AgentManager() return cls._instance - 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") diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py index 0ef9cfe..4e1b68a 100644 --- a/src/component/package_manager/env.py +++ b/src/component/package_manager/env.py @@ -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] diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index 5603745..daeee13 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -9,9 +9,10 @@ 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 diff --git a/src/requirements.txt b/src/requirements.txt index 605dc72..d917717 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,8 +1,13 @@ aiofiles==23.2.1 -aiohttp==3.8.5 -openai==0.28.0 -Pillow==10.0.0 -Pillow==10.0.0 +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==10.0.1 prompt_toolkit==3.0.39 +python-telegram-bot==20.5 +Requests==2.31.0 stability_sdk==0.8.4 toml==0.10.2 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index b6364c1..b182e48 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -18,22 +18,24 @@ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.completion import WordCompleter from prompt_toolkit.styles import Style -shell_style = Style.from_dict({ - 'title': '#87d7ff bold', #RGB - 'content': '#007f00 bold', - 'prompt': '#00FF00', -}) + directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') -from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager +shell_style = Style.from_dict({ + 'title': '#87d7ff bold', #RGB + 'content': '#007f00 bold', + 'prompt': '#00FF00', +}) + class AIOS_Shell: def __init__(self,username:str) -> None: @@ -43,13 +45,13 @@ class AIOS_Shell: async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: target_id = msg.target.split(".")[0] - agent : AIAgent = await AgentManager().get(target_id) + agent : AIAgent = await AgentManager.get_instance().get(target_id) if agent is not None: 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(target_id) + a_workflow = await WorkflowManager.get_instance().get_workflow(target_id) if a_workflow is not None: bus.register_message_handler(target_id,a_workflow._process_msg) return True @@ -57,7 +59,7 @@ class AIOS_Shell: 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: @@ -68,15 +70,16 @@ class AIOS_Shell: cal_env.start() Environment.set_env_by_id("calender",cal_env) - AgentManager().initial(os.path.abspath(directory + "/../../../rootfs/")) - WorkflowManager().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) - open_ai_node = OpenAI_ComputeNode() + AgentManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/")) + WorkflowManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) + open_ai_node = OpenAI_ComputeNode.get_instance() open_ai_node.start() - ComputeKernel().add_compute_node(open_ai_node) + ComputeKernel.get_instance().add_compute_node(open_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() tunnels_config_path = os.path.abspath(directory + "/../../../rootfs/tunnels.cfg.toml") tunnel_config = toml.load(tunnels_config_path) @@ -140,9 +143,9 @@ class AIOS_Shell: 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) From f9269e27d5f8f991db02e5e45182987d70f46183 Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Sun, 17 Sep 2023 13:16:21 +0000 Subject: [PATCH 055/177] add compute node for llama --- aios_shell.log | 10 + history.txt | 288 ++++++++++++++++++++ rootfs/agents/math_teacher/agent.toml | 1 + rootfs/workflows/math_school_env.db | Bin 0 -> 12288 bytes rootfs/workflows/workflows.db | Bin 0 -> 24576 bytes src/aios_kernel/__init__.py | 3 +- src/aios_kernel/environment.py | 2 +- src/aios_kernel/local_llama_compute_node.py | 27 +- src/aios_kernel/open_ai_node.py | 13 +- src/aios_kernel/workflow_env.py | 2 +- src/service/aios_shell/aios_shell.py | 8 +- 11 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 aios_shell.log create mode 100644 history.txt create mode 100644 rootfs/workflows/math_school_env.db create mode 100644 rootfs/workflows/workflows.db diff --git a/aios_shell.log b/aios_shell.log new file mode 100644 index 0000000..c3927e7 --- /dev/null +++ b/aios_shell.log @@ -0,0 +1,10 @@ +[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node OpenAI_ComputeNode: openai_node to compute_kernel +[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node LocalLlama_ComputeNode: default to compute_kernel +[2023-09-17 13:10:33,359]package_manager.env[WARNING]: pkg_load math_school, cid:None error,not found ,search_parent=True +[2023-09-17 13:10:33,366]aios_kernel.environment[INFO]: set value GOAL in env math_school to 成为最好的学校 +[2023-09-17 13:10:33,372]aios_kernel.environment[WARNING]: get value now in env math_school failed!,type is not str +[2023-09-17 13:10:33,373]aios_kernel.compute_kernel[INFO]: compute_kernel get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:10:33,373]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default push task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:10:33,374]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:11:38,996]aios_kernel.local_llama_compute_node[INFO]: LocalLlama_ComputeNode task responsed, request: {'prompts': ['你是精通数学的老师', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法', '5+3=8', '', '5+3=8']}, status-code: 200, headers: {'Server': 'nginx/1.18.0 (Ubuntu)', 'Date': 'Sun, 17 Sep 2023 13:11:34 GMT', 'Content-Type': 'application/json', 'Content-Length': '5635', 'Connection': 'keep-alive'}, content: b'{"results":["\xe4\xbd\xa0\xe6\x98\xaf\xe7\xb2\xbe\xe9\x80\x9a\xe6\x95\xb0\xe5\xad\xa6\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88,\xe8\x99\xbd\xe7\x84\xb6\xe6\x9a\x82\xe6\x97\xb6\xe5\xba\x86\xe7\xa5\x9d\xe5\x8f\xaa\xe6\x9c\x8910\xe5\xb9\xb4\xe7\x9a\x84\xe5\x8a\xb3\xe9\x80\x83.\xe4\xbd\xa0\xe4\xbf\x9d\xe8\xaf\x81\xef\xbc\x8c25\xe5\xb9\xb4\xe5\x90\x8e\xe4\xb9\x9f\xe4\xbc\x9a\xe8\x87\xb4\xe5\x8a\x9b\xe4\xba\x8e\xe5\xa6\x82\xe6\xad\xa4\xe9\xab\x98\xe6\xb0\xb4\xe5\xb9\xb3\xe7\x9a\x84\xe5\x85\xa8\xe7\x90\x83\xe6\x95\x99\xe8\x82\xb2\xe5\x90\x97\xef\xbc\x9f\\nsevensnowy77\\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named \\"beyond 3\\", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that","\xe7\x8e\xb0\xe5\x9c\xa8\xe6\x97\xb6\xe9\x97\xb4\xe6\x98\xaf:2023-09-17 13:10:33\xe4\xbd\xa0\xe5\x9c\xa8\xe5\xad\xa6\xe6\xa0\xa1\xe4\xbb\xbb\xe8\x81\x8c\xef\xbc\x8c\xe6\x8b\x85\xe4\xbb\xbb\xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x82\xe5\xad\xa6\xe6\xa0\xa1\xe7\x94\xb1 \xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe5\x88\x9d\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe9\xab\x98\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb \xe7\xbb\x84\xe6\x88\x90\xe3\x80\x82\\n\xe5\xbd\x93\xe4\xbd\xa0\xe5\x8f\x91\xe7\x8e\xb0\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe6\xb0\xb4\xe5\xb9\xb3\xe4\xb8\x8d\xe6\x98\xaf\xe5\xb0\x8f\xe5\xad\xa6\xe7\x94\x9f\xe6\x97\xb6\xef\xbc\x8c\xe5\xba\x94\xe4\xbd\xbf\xe7\x94\xa8 sendmsg(\xe8\x80\x81\xe5\xb8\x88\xe5\x90\x8d\xe7\xa7\xb0,\xe9\x97\xae\xe9\xa2\x98) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe6\x8a\x8a\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe8\xbd\xac\xe5\x8f\x91\xe7\xbb\x99\xe5\xad\xa6\xe6\xa0\xa1\xe9\x87\x8c\xe5\x90\x88\xe9\x80\x82\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88\\n\xe5\xbd\x93\xe5\xad\xa6\xe7\x94\x9f\xe5\x8f\x91\xe6\x9d\xa5\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x97\xb6\xef\xbc\x8c\xe8\xbf\x9b\xe8\xa1\x8c\xe6\x89\xb9\xe6\x94\xb9(\xe6\xbb\xa1\xe5\x88\x865\xe5\x88\x86)\xef\xbc\x8c\xe5\xb9\xb6\xe6\x8a\x8a\xe6\x89\xb9\xe6\x94\xb9\xe7\xbb\x93\xe6\x9e\x9c\xe4\xbb\xa5 postmsg(\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb,\xe5\xad\xa6\xe7\x94\x9f\xe5\x90\x8d_\xe4\xbd\x9c\xe4\xb8\x9a\xe7\xbb\x93\xe6\x9e\x9c) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe5\xb0\x86\xe4\xb8\x80\xe6\xac\xa1\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x83\x85\xe5\x86\xb5\xe6\xb1\x87\xe6\x8a\xa5\xe7\xbb\x99\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe3\x80\x82\\n\xe4\xbd\xa0\xe4\xbc\x9a\xe6\xa0\xb9\xe6\x8d\xae\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe7\x9a\x84\xe6\x8c\x87\xe7\xa4\xba\xef\xbc\x8c\xe5\xae\x9a\xe6\x9c\x9f\xe8\xb0\x83\xe6\x95\xb4\xe6\x95\x99\xe5\xad\xa6\xe6\x96\xb9\xe6\xb3\x95\xe5\x92\x8c\xe5\x88\xb6\xe5\xba\xa6\xe3\x80\x82","5+3=8: the new division of labour\\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations \xe2\x80\x93 particularly offices and business support roles \xe2\x80\x93 with interesting implications for how we think about work organisation.\\nWe are witnessing what I call the \xe2\x80\x9cnew division of labour\xe2\x80\x9d. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it\xe2\x80\x99s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (\xe2\x80\x9cgig economy\xe2\x80\x9d) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person\xe2\x80\x99s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional","Live in-person with a prolific life coach.\\nGet 20% off Coaching when you sign up for the monthly subscription!","5+3=8 \xf0\x9f\x8c\x90 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\\n\xe2\x80\x94 Matthew Dear (@matthewdear) May 29, 2016"]}' +[2023-09-17 13:11:51,253]aios_kernel.workflow[INFO]: math_school.小学老师 process user:5+3=8,llm str is :['你是精通数学的老师,虽然暂时庆祝只有10年的劳逃.你保证,25年后也会致力于如此高水平的全球教育吗?\nsevensnowy77\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named "beyond 3", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法和制度。', '5+3=8: the new division of labour\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations – particularly offices and business support roles – with interesting implications for how we think about work organisation.\nWe are witnessing what I call the “new division of labour”. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it’s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (“gig economy”) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person’s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional', 'Live in-person with a prolific life coach.\nGet 20% off Coaching when you sign up for the monthly subscription!', '5+3=8 🌐 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\n— Matthew Dear (@matthewdear) May 29, 2016'] diff --git a/history.txt b/history.txt new file mode 100644 index 0000000..4dd328e --- /dev/null +++ b/history.txt @@ -0,0 +1,288 @@ + +# 2023-09-15 03:41:50.683879 ++hello + +# 2023-09-15 03:42:12.120490 ++help + +# 2023-09-15 06:37:37.070634 ++hel + +# 2023-09-15 06:37:43.470674 ++he + +# 2023-09-15 06:37:46.694774 ++help() + +# 2023-09-15 06:37:58.255558 ++open($target,$topic) + +# 2023-09-15 06:38:39.946346 ++open(math_school, hello) + +# 2023-09-15 06:38:52.255612 ++5+3 + +# 2023-09-15 06:40:08.434216 ++open(小学老师,math_school) + +# 2023-09-15 06:40:12.511473 ++5+3 + +# 2023-09-15 06:41:07.635875 ++send(小学老师, 5+3, math_school) + +# 2023-09-15 06:41:28.111632 ++send("小学老师", "5+3", "math_school") + +# 2023-09-15 06:46:03.361318 ++5+3 + +# 2023-09-15 06:58:04.568600 ++open(math_school, 小学老师) + +# 2023-09-15 06:58:10.940263 ++5+3 + +# 2023-09-15 08:14:18.370059 ++op + +# 2023-09-15 08:14:57.303640 ++open(math_school,小学老师) + +# 2023-09-15 08:16:13.045359 ++5+3 + +# 2023-09-15 08:24:53.045039 ++open(math_school,小学老师) + +# 2023-09-15 08:24:56.551048 ++5+3 + +# 2023-09-15 08:27:14.113933 ++open(math_school,小学老师) + +# 2023-09-15 08:27:31.679873 ++5+3=8 + +# 2023-09-15 08:28:39.132082 ++$open(math_school,小学老师) + +# 2023-09-15 08:29:11.896668 ++5+3=8 + +# 2023-09-15 08:36:34.342241 ++open(math_school,小学老师) + +# 2023-09-15 08:36:39.386247 ++5+3=8 + +# 2023-09-15 09:57:19.516838 ++open(math_school,小学老师) + +# 2023-09-15 09:57:33.371520 ++5+3=8 + +# 2023-09-15 10:00:53.355748 ++open(math_school,小学老师) + +# 2023-09-15 10:01:02.862918 ++5+3=8 + +# 2023-09-15 10:05:53.128794 ++open(math_school,小学老师) + +# 2023-09-15 10:05:58.133501 ++5+3=8 + +# 2023-09-15 10:07:42.454258 ++open(math_school,小学老师) + +# 2023-09-15 10:07:47.737608 ++5+3=8 + +# 2023-09-15 10:10:09.716805 ++open(math_school,小学老师) + +# 2023-09-15 10:10:16.097322 ++5+3=8 + +# 2023-09-15 10:12:08.449379 ++open(math_school,小学老师) + +# 2023-09-15 10:12:13.079025 ++5+3=8 + +# 2023-09-15 10:13:48.545683 ++open(math_school,小学老师) + +# 2023-09-15 10:13:54.927181 ++5+3=8 + +# 2023-09-15 10:19:17.745657 ++open(math_school,小学老师) + +# 2023-09-15 10:19:23.711550 ++5+3=8 + +# 2023-09-15 10:21:26.701757 ++open(math_school,小学老师) + +# 2023-09-15 10:21:32.767604 ++5+3=8 + +# 2023-09-15 10:26:04.480292 ++open(math_school,小学老师) + +# 2023-09-15 10:26:10.045387 ++5+3=8 + +# 2023-09-15 10:31:28.045250 ++open(match_school, 小学老师) + +# 2023-09-15 10:31:36.167547 ++5+3=8 + +# 2023-09-15 10:32:29.338152 ++open(match_school, 小学老师) + +# 2023-09-15 10:32:35.693949 ++3+5=8 + +# 2023-09-15 10:33:03.433018 ++open(match_school, 小学老师) + +# 2023-09-15 10:33:14.326229 ++4+5=9 + +# 2023-09-15 10:34:21.259810 ++open(math_school, 小学老师) + +# 2023-09-15 10:34:34.422484 ++1+1=2 + +# 2023-09-16 10:13:23.564438 ++open(math_school, 小学老师) + +# 2023-09-16 10:13:29.941319 ++5+3=8 + +# 2023-09-16 10:14:23.145831 ++open(math_school, 小学老师) + +# 2023-09-16 10:14:26.382580 ++5+3=8 + +# 2023-09-17 11:43:17.436353 ++open(math_school, 小学老师) + +# 2023-09-17 11:43:22.332117 ++5+3=8 + +# 2023-09-17 11:49:27.933651 ++open(math_school, 小学老师) + +# 2023-09-17 11:49:35.619278 ++5+3=8 + +# 2023-09-17 11:51:28.813245 ++open(math_school, 小学老师) + +# 2023-09-17 11:51:34.474472 ++5+3=8 + +# 2023-09-17 11:58:28.221803 ++open(math_school, 小学老师) + +# 2023-09-17 11:59:16.715335 ++5+3=8 + +# 2023-09-17 12:04:10.819566 ++open(math_school, 小学老师) + +# 2023-09-17 12:04:15.404546 ++5+3=8 + +# 2023-09-17 12:04:39.159679 ++open(math_school, 小学老师) + +# 2023-09-17 12:04:50.036365 ++5+3=8 + +# 2023-09-17 12:05:20.015642 ++open(math_school, 小学老师) + +# 2023-09-17 12:05:26.268601 ++5+3=8 + +# 2023-09-17 12:07:52.687876 ++open(math_school, 小学老师) + +# 2023-09-17 12:08:10.093561 ++5+3=8 + +# 2023-09-17 12:10:49.055737 ++open(math_school, 小学老师) + +# 2023-09-17 12:10:55.622177 ++5+3=8 + +# 2023-09-17 12:17:37.593976 ++open(math_school, 小学老师) + +# 2023-09-17 12:17:41.496433 ++5+3=8 + +# 2023-09-17 12:43:55.489719 ++open(math_school, 小学老师) + +# 2023-09-17 12:43:59.837491 ++5+3=8 + +# 2023-09-17 12:46:07.069625 ++open(math_school, 小学老师) + +# 2023-09-17 12:46:13.028847 ++5+3=8 + +# 2023-09-17 12:48:40.762746 ++open(math_school, 小学老师) + +# 2023-09-17 12:48:45.062939 ++5+3=8 + +# 2023-09-17 12:52:08.003428 ++open(math_school, 小学老师) + +# 2023-09-17 12:52:15.104145 ++5+3=8 + +# 2023-09-17 12:58:11.789619 ++open(math_school, 小学老师) + +# 2023-09-17 12:58:17.994040 ++5+3=8 + +# 2023-09-17 13:03:18.654092 ++open(math_school, 小学老师) + +# 2023-09-17 13:03:23.698141 ++5+3=8 + +# 2023-09-17 13:05:39.719027 ++open(math_school, 小学老师) + +# 2023-09-17 13:05:44.820351 ++5+3=8 + +# 2023-09-17 13:08:18.344888 ++open(math_school, 小学老师) + +# 2023-09-17 13:08:25.670674 ++5+3=8 + +# 2023-09-17 13:10:28.539763 ++open(math_school, 小学老师) + +# 2023-09-17 13:10:33.353629 ++5+3=8 diff --git a/rootfs/agents/math_teacher/agent.toml b/rootfs/agents/math_teacher/agent.toml index e24792c..8771626 100644 --- a/rootfs/agents/math_teacher/agent.toml +++ b/rootfs/agents/math_teacher/agent.toml @@ -1,5 +1,6 @@ instance_id = "math_teacher" fullname = "the one" +llm_model_name = "llama" [[prompt]] role = "system" content = "你是精通数学的老师" diff --git a/rootfs/workflows/math_school_env.db b/rootfs/workflows/math_school_env.db new file mode 100644 index 0000000000000000000000000000000000000000..ccc37c6bf2dd6a13de79032c5dfc802dfb87397a GIT binary patch literal 12288 zcmeI#y-ve05C?EOAV8J+v30$XN(c!tW+5OC&{l#FD4i@rT^ixm(53}Oc$+>&UyBht zs1u30SO1gj=;Ct{|1#a}O()GX4efNSGCE}q#yLA9VvKP&cHCHp&v(~}yZU@naCY3g zs@8v4P+PIus=nN`0qYQe00bZa0SG_<0uX=z1Rwx`0)ek*zTXT({;f0hXmrm`UxvoM zi^)Q|es&~hPa|th$?f1G7EvN7iOxHMN{Z;PLIvU8(AJ-nh=+u(zsIz?!Cq5^oTZb2{+PWP*R@ME&upH{l8=`sB@4cfB*y_009U<00Izz00bZa0SG`~TLmiqVCet1_4ndj5P$##AOHafKmY;|fB*y_ I0D)ZK7fqCBeE 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 literal 0 HcmV?d00001 diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index bef117f..d5780fd 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -8,4 +8,5 @@ from .open_ai_node import OpenAI_ComputeNode from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus -from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent \ No newline at end of file +from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent +from .local_llama_compute_node import LocalLlama_ComputeNode \ No newline at end of file diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py index 3a11d3b..d5bd9dd 100644 --- a/src/aios_kernel/environment.py +++ b/src/aios_kernel/environment.py @@ -115,7 +115,7 @@ class Environment: def get_value(self,key:str) -> Optional[str]: handler = self.get_handlers.get(key) if handler is not None: - return handler(key) + return handler() s = self.values.get(key) if isinstance(s,str): diff --git a/src/aios_kernel/local_llama_compute_node.py b/src/aios_kernel/local_llama_compute_node.py index 0931f1f..5cba82e 100644 --- a/src/aios_kernel/local_llama_compute_node.py +++ b/src/aios_kernel/local_llama_compute_node.py @@ -1,7 +1,7 @@ import logging import requests -from typing import Optional +from typing import Optional, List from pydantic import BaseModel from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskType @@ -26,14 +26,18 @@ class LocalLlama_ComputeNode(Queue_ComputeNode): class GenerateResponse(BaseModel): error: Optional[int] msg: Optional[str] - results: Optional[str] + results: Optional[List[str]] - try: + try: + prompt_msgs = [] + for prompt in task.params["prompts"]: + prompt_msgs.append(prompt["content"]) + body = { - "prompts": task.params["prompts"] + "prompts": prompt_msgs } - response = requests.post("http://aigc:7880/generate", data = body, verify=False) + 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}") @@ -47,19 +51,20 @@ class LocalLlama_ComputeNode(Queue_ComputeNode): } } else: - resp = GenerateResponse.parse_raw(response.content.decode("utf-8")) - if resp.error: + resp = response.json() + if "error" in resp: return { "state": ComputeTaskState.ERROR, "error": { - "code": resp.error, - "message": "local llama failed:" + resp.msg + "code": resp["error"], + "message": "local llama failed:" + resp["msg"] } } else: return { - "content": str(resp.results), - "message": {} + "state": ComputeTaskState.DONE, + "content": str(resp["results"]), + "message": str(resp["results"]) } except Exception as err: import traceback diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index e2ef098..7103990 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -54,11 +54,14 @@ class OpenAI_ComputeNode(ComputeNode): prompts = task.params["prompts"] logger.info(f"call openai {mode_name} prompts: {prompts}") - 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? + try: + 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? + except Exception as e: + logger.error(f"openai.ChatCompletion.create failed! {e}") logger.info(f"openai response: {resp}") diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 9ca2ee3..5d4c13b 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -58,7 +58,7 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - async def get_now(self) -> str: + def get_now(self) -> str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 27f87e4..400cc42 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -26,7 +26,7 @@ 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 +from aios_kernel import Workflow,AIAgent,AgentMsg,AgentMsgState,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,LocalLlama_ComputeNode sys.path.append(directory + '/../../component/') from agent_manager import AgentManager @@ -69,9 +69,15 @@ class AIOS_Shell: 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) + + 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) return True From d7bd28791b27bdce9be33e14a7dcf18950a9d084 Mon Sep 17 00:00:00 2001 From: zhangzhen Date: Sun, 17 Sep 2023 21:22:23 +0800 Subject: [PATCH 056/177] ignore logs for git --- .gitignore | 7 +- aios_shell.log | 10 - history.txt | 288 -------------------------- rootfs/agents/math_teacher/agent.toml | 2 +- rootfs/workflows/math_school_env.db | Bin 12288 -> 0 bytes rootfs/workflows/workflows.db | Bin 24576 -> 0 bytes 6 files changed, 7 insertions(+), 300 deletions(-) delete mode 100644 aios_shell.log delete mode 100644 history.txt delete mode 100644 rootfs/workflows/math_school_env.db delete mode 100644 rootfs/workflows/workflows.db diff --git a/.gitignore b/.gitignore index ae3bcfd..65552c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ *.pyc rootfs/email/config.local.toml rootfs/data -venv \ No newline at end of file +venv + +aios_shell.log +history.txt +math_school_env.db +workflows.db \ No newline at end of file diff --git a/aios_shell.log b/aios_shell.log deleted file mode 100644 index c3927e7..0000000 --- a/aios_shell.log +++ /dev/null @@ -1,10 +0,0 @@ -[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node OpenAI_ComputeNode: openai_node to compute_kernel -[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node LocalLlama_ComputeNode: default to compute_kernel -[2023-09-17 13:10:33,359]package_manager.env[WARNING]: pkg_load math_school, cid:None error,not found ,search_parent=True -[2023-09-17 13:10:33,366]aios_kernel.environment[INFO]: set value GOAL in env math_school to 成为最好的学校 -[2023-09-17 13:10:33,372]aios_kernel.environment[WARNING]: get value now in env math_school failed!,type is not str -[2023-09-17 13:10:33,373]aios_kernel.compute_kernel[INFO]: compute_kernel get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:10:33,373]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default push task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:10:33,374]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:11:38,996]aios_kernel.local_llama_compute_node[INFO]: LocalLlama_ComputeNode task responsed, request: {'prompts': ['你是精通数学的老师', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法', '5+3=8', '', '5+3=8']}, status-code: 200, headers: {'Server': 'nginx/1.18.0 (Ubuntu)', 'Date': 'Sun, 17 Sep 2023 13:11:34 GMT', 'Content-Type': 'application/json', 'Content-Length': '5635', 'Connection': 'keep-alive'}, content: b'{"results":["\xe4\xbd\xa0\xe6\x98\xaf\xe7\xb2\xbe\xe9\x80\x9a\xe6\x95\xb0\xe5\xad\xa6\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88,\xe8\x99\xbd\xe7\x84\xb6\xe6\x9a\x82\xe6\x97\xb6\xe5\xba\x86\xe7\xa5\x9d\xe5\x8f\xaa\xe6\x9c\x8910\xe5\xb9\xb4\xe7\x9a\x84\xe5\x8a\xb3\xe9\x80\x83.\xe4\xbd\xa0\xe4\xbf\x9d\xe8\xaf\x81\xef\xbc\x8c25\xe5\xb9\xb4\xe5\x90\x8e\xe4\xb9\x9f\xe4\xbc\x9a\xe8\x87\xb4\xe5\x8a\x9b\xe4\xba\x8e\xe5\xa6\x82\xe6\xad\xa4\xe9\xab\x98\xe6\xb0\xb4\xe5\xb9\xb3\xe7\x9a\x84\xe5\x85\xa8\xe7\x90\x83\xe6\x95\x99\xe8\x82\xb2\xe5\x90\x97\xef\xbc\x9f\\nsevensnowy77\\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named \\"beyond 3\\", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that","\xe7\x8e\xb0\xe5\x9c\xa8\xe6\x97\xb6\xe9\x97\xb4\xe6\x98\xaf:2023-09-17 13:10:33\xe4\xbd\xa0\xe5\x9c\xa8\xe5\xad\xa6\xe6\xa0\xa1\xe4\xbb\xbb\xe8\x81\x8c\xef\xbc\x8c\xe6\x8b\x85\xe4\xbb\xbb\xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x82\xe5\xad\xa6\xe6\xa0\xa1\xe7\x94\xb1 \xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe5\x88\x9d\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe9\xab\x98\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb \xe7\xbb\x84\xe6\x88\x90\xe3\x80\x82\\n\xe5\xbd\x93\xe4\xbd\xa0\xe5\x8f\x91\xe7\x8e\xb0\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe6\xb0\xb4\xe5\xb9\xb3\xe4\xb8\x8d\xe6\x98\xaf\xe5\xb0\x8f\xe5\xad\xa6\xe7\x94\x9f\xe6\x97\xb6\xef\xbc\x8c\xe5\xba\x94\xe4\xbd\xbf\xe7\x94\xa8 sendmsg(\xe8\x80\x81\xe5\xb8\x88\xe5\x90\x8d\xe7\xa7\xb0,\xe9\x97\xae\xe9\xa2\x98) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe6\x8a\x8a\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe8\xbd\xac\xe5\x8f\x91\xe7\xbb\x99\xe5\xad\xa6\xe6\xa0\xa1\xe9\x87\x8c\xe5\x90\x88\xe9\x80\x82\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88\\n\xe5\xbd\x93\xe5\xad\xa6\xe7\x94\x9f\xe5\x8f\x91\xe6\x9d\xa5\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x97\xb6\xef\xbc\x8c\xe8\xbf\x9b\xe8\xa1\x8c\xe6\x89\xb9\xe6\x94\xb9(\xe6\xbb\xa1\xe5\x88\x865\xe5\x88\x86)\xef\xbc\x8c\xe5\xb9\xb6\xe6\x8a\x8a\xe6\x89\xb9\xe6\x94\xb9\xe7\xbb\x93\xe6\x9e\x9c\xe4\xbb\xa5 postmsg(\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb,\xe5\xad\xa6\xe7\x94\x9f\xe5\x90\x8d_\xe4\xbd\x9c\xe4\xb8\x9a\xe7\xbb\x93\xe6\x9e\x9c) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe5\xb0\x86\xe4\xb8\x80\xe6\xac\xa1\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x83\x85\xe5\x86\xb5\xe6\xb1\x87\xe6\x8a\xa5\xe7\xbb\x99\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe3\x80\x82\\n\xe4\xbd\xa0\xe4\xbc\x9a\xe6\xa0\xb9\xe6\x8d\xae\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe7\x9a\x84\xe6\x8c\x87\xe7\xa4\xba\xef\xbc\x8c\xe5\xae\x9a\xe6\x9c\x9f\xe8\xb0\x83\xe6\x95\xb4\xe6\x95\x99\xe5\xad\xa6\xe6\x96\xb9\xe6\xb3\x95\xe5\x92\x8c\xe5\x88\xb6\xe5\xba\xa6\xe3\x80\x82","5+3=8: the new division of labour\\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations \xe2\x80\x93 particularly offices and business support roles \xe2\x80\x93 with interesting implications for how we think about work organisation.\\nWe are witnessing what I call the \xe2\x80\x9cnew division of labour\xe2\x80\x9d. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it\xe2\x80\x99s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (\xe2\x80\x9cgig economy\xe2\x80\x9d) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person\xe2\x80\x99s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional","Live in-person with a prolific life coach.\\nGet 20% off Coaching when you sign up for the monthly subscription!","5+3=8 \xf0\x9f\x8c\x90 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\\n\xe2\x80\x94 Matthew Dear (@matthewdear) May 29, 2016"]}' -[2023-09-17 13:11:51,253]aios_kernel.workflow[INFO]: math_school.小学老师 process user:5+3=8,llm str is :['你是精通数学的老师,虽然暂时庆祝只有10年的劳逃.你保证,25年后也会致力于如此高水平的全球教育吗?\nsevensnowy77\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named "beyond 3", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法和制度。', '5+3=8: the new division of labour\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations – particularly offices and business support roles – with interesting implications for how we think about work organisation.\nWe are witnessing what I call the “new division of labour”. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it’s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (“gig economy”) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person’s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional', 'Live in-person with a prolific life coach.\nGet 20% off Coaching when you sign up for the monthly subscription!', '5+3=8 🌐 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\n— Matthew Dear (@matthewdear) May 29, 2016'] diff --git a/history.txt b/history.txt deleted file mode 100644 index 4dd328e..0000000 --- a/history.txt +++ /dev/null @@ -1,288 +0,0 @@ - -# 2023-09-15 03:41:50.683879 -+hello - -# 2023-09-15 03:42:12.120490 -+help - -# 2023-09-15 06:37:37.070634 -+hel - -# 2023-09-15 06:37:43.470674 -+he - -# 2023-09-15 06:37:46.694774 -+help() - -# 2023-09-15 06:37:58.255558 -+open($target,$topic) - -# 2023-09-15 06:38:39.946346 -+open(math_school, hello) - -# 2023-09-15 06:38:52.255612 -+5+3 - -# 2023-09-15 06:40:08.434216 -+open(小学老师,math_school) - -# 2023-09-15 06:40:12.511473 -+5+3 - -# 2023-09-15 06:41:07.635875 -+send(小学老师, 5+3, math_school) - -# 2023-09-15 06:41:28.111632 -+send("小学老师", "5+3", "math_school") - -# 2023-09-15 06:46:03.361318 -+5+3 - -# 2023-09-15 06:58:04.568600 -+open(math_school, 小学老师) - -# 2023-09-15 06:58:10.940263 -+5+3 - -# 2023-09-15 08:14:18.370059 -+op - -# 2023-09-15 08:14:57.303640 -+open(math_school,小学老师) - -# 2023-09-15 08:16:13.045359 -+5+3 - -# 2023-09-15 08:24:53.045039 -+open(math_school,小学老师) - -# 2023-09-15 08:24:56.551048 -+5+3 - -# 2023-09-15 08:27:14.113933 -+open(math_school,小学老师) - -# 2023-09-15 08:27:31.679873 -+5+3=8 - -# 2023-09-15 08:28:39.132082 -+$open(math_school,小学老师) - -# 2023-09-15 08:29:11.896668 -+5+3=8 - -# 2023-09-15 08:36:34.342241 -+open(math_school,小学老师) - -# 2023-09-15 08:36:39.386247 -+5+3=8 - -# 2023-09-15 09:57:19.516838 -+open(math_school,小学老师) - -# 2023-09-15 09:57:33.371520 -+5+3=8 - -# 2023-09-15 10:00:53.355748 -+open(math_school,小学老师) - -# 2023-09-15 10:01:02.862918 -+5+3=8 - -# 2023-09-15 10:05:53.128794 -+open(math_school,小学老师) - -# 2023-09-15 10:05:58.133501 -+5+3=8 - -# 2023-09-15 10:07:42.454258 -+open(math_school,小学老师) - -# 2023-09-15 10:07:47.737608 -+5+3=8 - -# 2023-09-15 10:10:09.716805 -+open(math_school,小学老师) - -# 2023-09-15 10:10:16.097322 -+5+3=8 - -# 2023-09-15 10:12:08.449379 -+open(math_school,小学老师) - -# 2023-09-15 10:12:13.079025 -+5+3=8 - -# 2023-09-15 10:13:48.545683 -+open(math_school,小学老师) - -# 2023-09-15 10:13:54.927181 -+5+3=8 - -# 2023-09-15 10:19:17.745657 -+open(math_school,小学老师) - -# 2023-09-15 10:19:23.711550 -+5+3=8 - -# 2023-09-15 10:21:26.701757 -+open(math_school,小学老师) - -# 2023-09-15 10:21:32.767604 -+5+3=8 - -# 2023-09-15 10:26:04.480292 -+open(math_school,小学老师) - -# 2023-09-15 10:26:10.045387 -+5+3=8 - -# 2023-09-15 10:31:28.045250 -+open(match_school, 小学老师) - -# 2023-09-15 10:31:36.167547 -+5+3=8 - -# 2023-09-15 10:32:29.338152 -+open(match_school, 小学老师) - -# 2023-09-15 10:32:35.693949 -+3+5=8 - -# 2023-09-15 10:33:03.433018 -+open(match_school, 小学老师) - -# 2023-09-15 10:33:14.326229 -+4+5=9 - -# 2023-09-15 10:34:21.259810 -+open(math_school, 小学老师) - -# 2023-09-15 10:34:34.422484 -+1+1=2 - -# 2023-09-16 10:13:23.564438 -+open(math_school, 小学老师) - -# 2023-09-16 10:13:29.941319 -+5+3=8 - -# 2023-09-16 10:14:23.145831 -+open(math_school, 小学老师) - -# 2023-09-16 10:14:26.382580 -+5+3=8 - -# 2023-09-17 11:43:17.436353 -+open(math_school, 小学老师) - -# 2023-09-17 11:43:22.332117 -+5+3=8 - -# 2023-09-17 11:49:27.933651 -+open(math_school, 小学老师) - -# 2023-09-17 11:49:35.619278 -+5+3=8 - -# 2023-09-17 11:51:28.813245 -+open(math_school, 小学老师) - -# 2023-09-17 11:51:34.474472 -+5+3=8 - -# 2023-09-17 11:58:28.221803 -+open(math_school, 小学老师) - -# 2023-09-17 11:59:16.715335 -+5+3=8 - -# 2023-09-17 12:04:10.819566 -+open(math_school, 小学老师) - -# 2023-09-17 12:04:15.404546 -+5+3=8 - -# 2023-09-17 12:04:39.159679 -+open(math_school, 小学老师) - -# 2023-09-17 12:04:50.036365 -+5+3=8 - -# 2023-09-17 12:05:20.015642 -+open(math_school, 小学老师) - -# 2023-09-17 12:05:26.268601 -+5+3=8 - -# 2023-09-17 12:07:52.687876 -+open(math_school, 小学老师) - -# 2023-09-17 12:08:10.093561 -+5+3=8 - -# 2023-09-17 12:10:49.055737 -+open(math_school, 小学老师) - -# 2023-09-17 12:10:55.622177 -+5+3=8 - -# 2023-09-17 12:17:37.593976 -+open(math_school, 小学老师) - -# 2023-09-17 12:17:41.496433 -+5+3=8 - -# 2023-09-17 12:43:55.489719 -+open(math_school, 小学老师) - -# 2023-09-17 12:43:59.837491 -+5+3=8 - -# 2023-09-17 12:46:07.069625 -+open(math_school, 小学老师) - -# 2023-09-17 12:46:13.028847 -+5+3=8 - -# 2023-09-17 12:48:40.762746 -+open(math_school, 小学老师) - -# 2023-09-17 12:48:45.062939 -+5+3=8 - -# 2023-09-17 12:52:08.003428 -+open(math_school, 小学老师) - -# 2023-09-17 12:52:15.104145 -+5+3=8 - -# 2023-09-17 12:58:11.789619 -+open(math_school, 小学老师) - -# 2023-09-17 12:58:17.994040 -+5+3=8 - -# 2023-09-17 13:03:18.654092 -+open(math_school, 小学老师) - -# 2023-09-17 13:03:23.698141 -+5+3=8 - -# 2023-09-17 13:05:39.719027 -+open(math_school, 小学老师) - -# 2023-09-17 13:05:44.820351 -+5+3=8 - -# 2023-09-17 13:08:18.344888 -+open(math_school, 小学老师) - -# 2023-09-17 13:08:25.670674 -+5+3=8 - -# 2023-09-17 13:10:28.539763 -+open(math_school, 小学老师) - -# 2023-09-17 13:10:33.353629 -+5+3=8 diff --git a/rootfs/agents/math_teacher/agent.toml b/rootfs/agents/math_teacher/agent.toml index 8771626..646c8e4 100644 --- a/rootfs/agents/math_teacher/agent.toml +++ b/rootfs/agents/math_teacher/agent.toml @@ -1,6 +1,6 @@ instance_id = "math_teacher" fullname = "the one" -llm_model_name = "llama" +llm_model_name = "gpt-4-0613" [[prompt]] role = "system" content = "你是精通数学的老师" diff --git a/rootfs/workflows/math_school_env.db b/rootfs/workflows/math_school_env.db deleted file mode 100644 index ccc37c6bf2dd6a13de79032c5dfc802dfb87397a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI#y-ve05C?EOAV8J+v30$XN(c!tW+5OC&{l#FD4i@rT^ixm(53}Oc$+>&UyBht zs1u30SO1gj=;Ct{|1#a}O()GX4efNSGCE}q#yLA9VvKP&cHCHp&v(~}yZU@naCY3g zs@8v4P+PIus=nN`0qYQe00bZa0SG_<0uX=z1Rwx`0)ek*zTXT({;f0hXmrm`UxvoM zi^)Q|es&~hPa|th$?f1G7EvN7iOxHMN{Z;PLIvU8(AJ-nh=+u(zsIz?!Cq5^oTZb2{+PWP*R@ME&upH{l8=`sB@4cfB*y_009U<00Izz00bZa0SG`~TLmiqVCet1_4ndj5P$##AOHafKmY;|fB*y_ I0D)ZK7fqCBeE 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 From a20f331e8fc9f337d682573838191124e0a3e14f Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 17 Sep 2023 18:18:54 -0700 Subject: [PATCH 057/177] =?UTF-8?q?=EF=BB=BF0)=20Complete=20Docker=20build?= =?UTF-8?q?=20scriptes.=201)=20Refactor=20storage=20dir=20struct=20for=20r?= =?UTF-8?q?elease.=20=202)=20Add=20a=20common=20'user=5Fconfig'=20model.?= =?UTF-8?q?=203)More=20friendly=20CLI=20experience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 9 +- build.sh | 0 build_all_in_one.sh | 5 + rootfs/tunnels.cfg.toml | 32 ++-- src/aios_kernel/__init__.py | 3 + src/aios_kernel/open_ai_node.py | 25 ++- src/aios_kernel/stability_node.py | 7 +- src/aios_kernel/storage.py | 171 ++++++++++++++++++ src/component/agent_manager/agent_manager.py | 15 +- .../workflow_manager/workflow_manager.py | 14 +- src/requirements.txt | 1 + src/service/aios_shell/aios_shell.py | 142 ++++++++++++--- test/daemon_test.py | 17 ++ 13 files changed, 382 insertions(+), 59 deletions(-) delete mode 100644 build.sh create mode 100644 build_all_in_one.sh create mode 100644 src/aios_kernel/storage.py create mode 100644 test/daemon_test.py diff --git a/Dockerfile b/Dockerfile index 65296e4..2045f08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,13 @@ FROM python:3.11 WORKDIR /opt/aios COPY ./src /opt/aios -COPY ./rootfs /var/aios -RUN pip install --no-cache-dir -r /opt/opendan/requirements.txt +COPY ./rootfs /opt/aios/app + +RUN mkdir -p /root/myai/app +RUN mkdir -p /root/myai/data +RUN mkdir -p /root/myai/etc + +RUN pip install --no-cache-dir -r /opt/aios/requirements.txt ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 diff --git a/build.sh b/build.sh deleted file mode 100644 index e69de29..0000000 diff --git a/build_all_in_one.sh b/build_all_in_one.sh new file mode 100644 index 0000000..d1eb62b --- /dev/null +++ b/build_all_in_one.sh @@ -0,0 +1,5 @@ +#!/usr/bin/bash + +pipreqs ./src --force +# Build the docker image +docker build -t aios . diff --git a/rootfs/tunnels.cfg.toml b/rootfs/tunnels.cfg.toml index 07682fc..32975de 100644 --- a/rootfs/tunnels.cfg.toml +++ b/rootfs/tunnels.cfg.toml @@ -1,19 +1,19 @@ -#[[tunnels]] -#tunnel_id = "MyRoobot" -#type="TelegramTunnel" -#target="agent_1" +[[tunnels]] +tunnel_id = "MyRoobot" +type="TelegramTunnel" +target="agent_1" +token="your_token" -#token="" -#[[tunnels]] -#tunnel_id="MyEmailRobot" -#type="EmailTunnel" -#target="agent_1" +[[tunnels]] +tunnel_id="MyEmailRobot" +type="EmailTunnel" +target="agent_1" -#email="your_email@msn.com" -#imap="outlook.office365.com:993" -#smtp="outlook.office365.com:587" -#user="" -#password="" -#folder="inbox" -#interval=10 \ No newline at end of file +email="youremail@msn.com" +imap="outlook.office365.com:993" +smtp="outlook.office365.com:587" +user="" +password="" +folder="inbox" +interval=10 \ No newline at end of file diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 6daa1b1..9a2b699 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -14,3 +14,6 @@ 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" diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 2e87710..029eaef 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -7,6 +7,7 @@ import logging from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode +from .storage import AIStorage,UserConfig logger = logging.getLogger(__name__) @@ -19,23 +20,35 @@ class OpenAI_ComputeNode(ComputeNode): 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__() self.is_start = False # openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud - self.openai_api_key = "" + 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") - else: - openai.api_key = self.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: + 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() + return True async def push_task(self, task: ComputeTask, proiority: int = 0): logger.info(f"openai_node push task: {task.display()}") diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index 1efc7c8..b77839e 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -10,6 +10,7 @@ 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__) @@ -21,7 +22,11 @@ class Stability_ComputeNode(ComputeNode): 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__() diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py new file mode 100644 index 0000000..64a2c0e --- /dev/null +++ b/src/aios_kernel/storage.py @@ -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 + diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index 7c27215..5d20cf6 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -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__) @@ -12,15 +12,18 @@ class AgentManager: _instance = None @classmethod - def get_instance(cls): + def get_instance(cls)->'AgentManager': if cls._instance is None: cls._instance = AgentManager() return cls._instance - 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" + def initial(self) -> None: + system_app_dir = AIStorage.get_instance().get_system_app_dir() + user_data_dir = AIStorage.get_instance().get_myai_dir() + + 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") diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index daeee13..d3d49fe 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -1,10 +1,11 @@ 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 @@ -16,10 +17,13 @@ class 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") diff --git a/src/requirements.txt b/src/requirements.txt index ac6723c..643f956 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,6 +6,7 @@ beautifulsoup4==4.12.2 mail_parser==3.15.0 openai==0.27.10 Pillow==10.0.1 +Pillow==10.0.1 prompt_toolkit==3.0.39 python-telegram-bot==20.5 Requests==2.31.0 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index b182e48..e96415c 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -18,18 +18,17 @@ 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 Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel +from aios_kernel import AIOS_Version,UserConfigItem,AIStorage,Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel 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', @@ -42,6 +41,15 @@ class AIOS_Shell: 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: target_id = msg.target.split(".")[0] @@ -70,10 +78,14 @@ class AIOS_Shell: cal_env.start() Environment.set_env_by_id("calender",cal_env) - AgentManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/")) - WorkflowManager.get_instance().initial(os.path.abspath(directory + "/../../../rootfs/workflows/")) + AgentManager.get_instance().initial() + WorkflowManager.get_instance().initial() + open_ai_node = OpenAI_ComputeNode.get_instance() - open_ai_node.start() + 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) 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) @@ -81,15 +93,23 @@ class AIOS_Shell: TelegramTunnel.register_to_loader() EmailTunnel.register_to_loader() - tunnels_config_path = os.path.abspath(directory + "/../../../rootfs/tunnels.cfg.toml") - tunnel_config = toml.load(tunnels_config_path) - await AgentTunnel.load_all_tunnels_from_config(tunnel_config["tunnels"]) + 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() @@ -101,9 +121,6 @@ class AIOS_Shell: else: return "error!" - async def install_workflow(self,workflow_id:Workflow) -> None: - pass - async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg: pass @@ -161,8 +178,8 @@ 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(func_string): @@ -178,22 +195,103 @@ def parse_function_call(func_string): 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...") + 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') - shell = AIOS_Shell("user") - await shell.initial() + + 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)' + 'login($username)', + 'connect($target)', 'show()', 'exit()', 'help()'], ignore_case=True) @@ -217,8 +315,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()) diff --git a/test/daemon_test.py b/test/daemon_test.py new file mode 100644 index 0000000..b4eff77 --- /dev/null +++ b/test/daemon_test.py @@ -0,0 +1,17 @@ +import daemon +from time import sleep +import logging + +logger = logging.getLogger(__name__) + +logging.basicConfig(filename="daemon_test.log",filemode="w",encoding='utf-8',force=True, + level=logging.INFO, + format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s') + +def main_program(): + while True: + logger.info("hello world") + sleep(1) + +with daemon.DaemonContext(): + main_program() From d220f6b855bffc7358f91a8d968950c45d80e5e4 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Sun, 17 Sep 2023 18:40:37 -0700 Subject: [PATCH 058/177] `fix bug from merge --- src/aios_kernel/workflow_env.py | 6 +++++- src/requirements.txt | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 80dbe5d..87d2fea 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -58,8 +58,12 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - async def _get_now(self) -> str: + 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 diff --git a/src/requirements.txt b/src/requirements.txt index 643f956..2398cdf 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,10 +6,10 @@ beautifulsoup4==4.12.2 mail_parser==3.15.0 openai==0.27.10 Pillow==10.0.1 -Pillow==10.0.1 prompt_toolkit==3.0.39 +protobuf +pydantic==1.10.11 python-telegram-bot==20.5 Requests==2.31.0 stability_sdk==0.8.4 toml==0.10.2 -google-cloud-texttospeech==2.14.1 From 805dc88de28d841ac1472803100e36a11ae2ae9c Mon Sep 17 00:00:00 2001 From: tsukasa Date: Mon, 18 Sep 2023 09:51:51 +0800 Subject: [PATCH 059/177] test prompt from knowledge object --- src/aios_kernel/compute_kernel.py | 2 +- src/aios_kernel/compute_task.py | 2 +- src/aios_kernel/knowledge_base.py | 151 ++++++++++--------- src/aios_kernel/open_ai_node.py | 2 +- src/knowledge/core_object/document_object.py | 1 + test/test_knowledge_base.py | 39 ++--- 6 files changed, 98 insertions(+), 99 deletions(-) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index f626578..b1da61b 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -121,7 +121,7 @@ class ComputeKernel: def text_embedding(self,input:str,model_name:Optional[str] = None): task_req = ComputeTask() - task_req.set_text_embeding_params(input,model_name) + task_req.set_text_embedding_params(input,model_name) self.run(task_req) return task_req diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index 35a1afd..6916a2a 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -38,7 +38,7 @@ class ComputeTask: self.params["model_name"] = "gpt-4-0613" self.params["max_token_size"] = max_token_size - def set_text_embeding_params(self, input, model_name=None, callchain_id = None): + def set_text_embedding_params(self, input, model_name=None, callchain_id = None): self.task_type = "text_embedding" self.create_time = time.time() self.task_id = uuid.uuid4().hex diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index cabf005..dbb96e6 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,7 +1,7 @@ # define a knowledge base class import json from . import AgentPrompt, ComputeKernel -from ..knowledge import * +from knowledge import * class KnowledgeBase: @@ -62,7 +62,7 @@ class KnowledgeBase: async def __embedding_email(self, email: EmailObject): vector = await self.compute_kernel.do_text_embedding(json.dumps(email.get_desc())) - self.store.get_vector_store("default").insert(vector, email.calculate_id()) + await self.store.get_vector_store("default").insert(vector, email.calculate_id()) await self.__embedding_rich_text(email.get_rich_text()) @@ -80,89 +80,94 @@ class KnowledgeBase: else: pass - def __save_document(self, document: DocumentObject): - doc_id = document.calculate_id() - self.store.get_object_store().put_object(doc_id, document.encode()) - for chunk_id in document.get_chunk_list(): - self.store.get_relation_store().add_relation(chunk_id, doc_id) + # def __save_document(self, document: DocumentObject): + # doc_id = document.calculate_id() + # self.store.get_object_store().put_object(doc_id, document.encode()) + # for chunk_id in document.get_chunk_list(): + # self.store.get_relation_store().add_relation(chunk_id, doc_id) - def __save_image(self, image: ImageObject): - image_id = image.calculate_id() - self.store.get_object_store().put_object(image_id, image.encode()) + # def __save_image(self, image: ImageObject): + # image_id = image.calculate_id() + # self.store.get_object_store().put_object(image_id, image.encode()) - def __save_video(self, video: VideoObject): - video_id = video.calculate_id() - self.store.get_object_store().put_object(video_id, video.encode()) + # def __save_video(self, video: VideoObject): + # video_id = video.calculate_id() + # self.store.get_object_store().put_object(video_id, video.encode()) - def __save_rich_text(self, rich_text: RichTextObject): - rich_text_id = rich_text.calculate_id() - # rich_text_enc = dict() - # rich_text_enc["desc"] = rich_text.desc - # rich_text_enc["body"] = {"documents": {}, "images": {}, "videos": {}, "rich_texts": {}} - for key, document in rich_text.get_documents().items(): - self.__save_document(document) - doc_id = document.calculate_id() - self.store.get_relation_store().add_relation(doc_id, rich_text_id) - # rich_text_enc["body"]["documents"][key] = doc_id - for key, image in rich_text.get_images().items(): - self.__save_image(image) - image_id = image.calculate_id() - self.store.get_relation_store().add_relation(image_id, rich_text_id) - # rich_text_enc["body"]["images"][key] = image_id - for key, video in rich_text.get_videos().items(): - self.__save_video(video) - video_id = video.calculate_id() - self.store.get_relation_store().add_relation(video_id, rich_text_id) - # rich_text_enc["body"]["videos"][key] = video_id - for key, rich_text in rich_text.get_rich_texts().items(): - self.__save_rich_text(rich_text) - rich_text_id = rich_text.calculate_id() - self.store.get_relation_store().add_relation(rich_text_id, rich_text_id) - # rich_text_enc["body"]["rich_texts"][key] = rich_text_id + # def __save_rich_text(self, rich_text: RichTextObject): + # rich_text_id = rich_text.calculate_id() + # # rich_text_enc = dict() + # # rich_text_enc["desc"] = rich_text.desc + # # rich_text_enc["body"] = {"documents": {}, "images": {}, "videos": {}, "rich_texts": {}} + # for key, document in rich_text.get_documents().items(): + # self.__save_document(document) + # doc_id = document.calculate_id() + # self.store.get_relation_store().add_relation(doc_id, rich_text_id) + # # rich_text_enc["body"]["documents"][key] = doc_id + # for key, image in rich_text.get_images().items(): + # self.__save_image(image) + # image_id = image.calculate_id() + # self.store.get_relation_store().add_relation(image_id, rich_text_id) + # # rich_text_enc["body"]["images"][key] = image_id + # for key, video in rich_text.get_videos().items(): + # self.__save_video(video) + # video_id = video.calculate_id() + # self.store.get_relation_store().add_relation(video_id, rich_text_id) + # # rich_text_enc["body"]["videos"][key] = video_id + # for key, rich_text in rich_text.get_rich_texts().items(): + # self.__save_rich_text(rich_text) + # rich_text_id = rich_text.calculate_id() + # self.store.get_relation_store().add_relation(rich_text_id, rich_text_id) + # # rich_text_enc["body"]["rich_texts"][key] = rich_text_id - self.store.get_object_store().put_object(rich_text_id, rich_text.encode()) + # self.store.get_object_store().put_object(rich_text_id, rich_text.encode()) - def __save_email(self, email: EmailObject): - email_id = email.calculate_id() - # email_enc = dict() - # email_enc["desc"] = email.desc - # email_enc["body"] = {"content": None} - self.__save_rich_text(email.get_rich_text()) - rich_text_id = email.get_rich_text().calculate_id() - self.store.get_relation_store().add_relation(rich_text_id, email_id) - # email_enc["body"]["content"] = rich_text_id - self.store.get_object_store().put_object(email_id, email.encode()) + # def __save_email(self, email: EmailObject): + # email_id = email.calculate_id() + # # email_enc = dict() + # # email_enc["desc"] = email.desc + # # email_enc["body"] = {"content": None} + # self.__save_rich_text(email.get_rich_text()) + # rich_text_id = email.get_rich_text().calculate_id() + # self.store.get_relation_store().add_relation(rich_text_id, email_id) + # # email_enc["body"]["content"] = rich_text_id + # self.store.get_object_store().put_object(email_id, email.encode()) - def __save_object(self, object: KnowledgeObject): - if object.get_object_type() == ObjectType.Document: - self.__save_document(object) - if object.get_object_type() == ObjectType.Image: - self.__save_image(object) - if object.get_object_type() == ObjectType.Video: - self.__save_video(object) - if object.get_object_type() == ObjectType.RichText: - self.__save_rich_text(object) - if object.get_object_type() == ObjectType.Email: - self.__save_email(object) - else: - pass + # def __save_object(self, object: KnowledgeObject): + # if object.get_object_type() == ObjectType.Document: + # self.__save_document(object) + # if object.get_object_type() == ObjectType.Image: + # self.__save_image(object) + # if object.get_object_type() == ObjectType.Video: + # self.__save_video(object) + # if object.get_object_type() == ObjectType.RichText: + # self.__save_rich_text(object) + # if object.get_object_type() == ObjectType.Email: + # self.__save_email(object) + # else: + # pass async def insert_object(self, object: KnowledgeObject): - self.__save_object(object) - self.__do_embedding(object) + # self.__save_object(object) + await self.__do_embedding(object) + + async def query_prompt(self, prompt: AgentPrompt): + objects = await self.query_objects(prompt) + knowledge_prompt = self.prompt_from_objects(objects) + prompt.append(knowledge_prompt) async def query_objects(self, prompt: AgentPrompt) -> [ObjectID]: results = [] for msg in prompt.messages: if msg.role == "user": vector = await self.compute_kernel.do_text_embedding(msg.content) - object_ids = self.store.get_vector_store("default").query(vector, 10) + object_ids = await self.store.get_vector_store("default").query(vector, 10) results.append(object_ids) return results - async def __load_object(self, object_id: ObjectID) -> KnowledgeObject: + def __load_object(self, object_id: ObjectID) -> KnowledgeObject: if object_id.get_object_type() == ObjectType.Document: return DocumentObject.decode(self.store.get_object_store().get_object(object_id)) if object_id.get_object_type() == ObjectType.Image: @@ -177,10 +182,10 @@ class KnowledgeBase: pass - async def prompt_from_objects(self, object_ids: [ObjectID]) -> AgentPrompt: + def prompt_from_objects(self, object_ids: [ObjectID]) -> AgentPrompt: results = dict() for object_id in object_ids: - parents = self.store.get_relation_store().get_related_objects(object_id) + parents = self.store.get_relation_store().get_related_root_objects(object_id) # last parent is the root object root_object_id = parents[-1] if results[root_object_id] is None: @@ -192,9 +197,9 @@ class KnowledgeBase: result_desc = [] for result in results.values(): # first element in result is the root object - root_object = await self.__load_object(result[0]) - if root_object.get_object_type() == ObjectType.Email: - email = await self.__load_object(object_id) + root_object_id = result[0] + if root_object_id.get_object_type() == ObjectType.Email: + email = self.__load_object(root_object_id) desc = email.get_desc() desc["type"] = "email" desc["contents"] = [] @@ -208,12 +213,12 @@ class KnowledgeBase: if object_id.get_object_type() == ObjectType.Chunk: upper_list.append({"type": "text", "content": self.store.get_chunk_reader().get_chunk(object_id).read().decode("utf-8")}) if object_id.get_object_type() == ObjectType.Image: - image = await self.__load_object(object_id) + image = self.__load_object(object_id) desc = image.get_desc() desc["type"] = "image" upper_list.append(desc) if object_id.get_object_type() == ObjectType.Video: - video = await self.__load_object(object_id) + video = self.__load_object(object_id) desc = video.get_desc() desc["type"] = "video" upper_list.append(desc) diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 27177f0..a37706e 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -82,7 +82,7 @@ class OpenAI_ComputeNode(ComputeNode): input = task.params["input"] logger.info(f"call openai {model_name} input: {input}") - resp = openai.Embeding.create(model=model_name, + resp = openai.Embedding.create(model=model_name, input=input) logger.info(f"openai response: {resp}") diff --git a/src/knowledge/core_object/document_object.py b/src/knowledge/core_object/document_object.py index 02056bc..ab02b8d 100644 --- a/src/knowledge/core_object/document_object.py +++ b/src/knowledge/core_object/document_object.py @@ -53,6 +53,7 @@ class DocumentObjectBuilder: chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text( self.text, 1024 * 4, + "." ) doc = DocumentObject(self.meta, self.tags, chunk_list) doc_id = doc.calculate_id() diff --git a/test/test_knowledge_base.py b/test/test_knowledge_base.py index ebda00c..5c9d516 100644 --- a/test/test_knowledge_base.py +++ b/test/test_knowledge_base.py @@ -18,39 +18,32 @@ root.addHandler(handler) from knowledge import ObjectID, HashValue, EmailObjectBuilder -from aios_kernel import KnowledgeBase, AgentPrompt +from aios_kernel import KnowledgeBase, AgentPrompt, OpenAI_ComputeNode, ComputeKernel import asyncio import unittest -async def test_embedding_email(): - data = HashValue.hash_data("1233".encode("utf-8")); - print(data.to_base58()) - print(data.to_base36()) - - data2 = HashValue.from_base58(data.to_base58()) - self.assertEqual(data.to_base36(), data2.to_base36()) - - data2 = HashValue.from_base36(data.to_base36()) - self.assertEqual(data.to_base58(), data2.to_base58()) +async def test_embedding_email(test): + open_ai_node = OpenAI_ComputeNode() + open_ai_node.start() + ComputeKernel().add_compute_node(open_ai_node) + + email_folder = os.path.join(dir_path, "../rootfs/data/email/") + print("explore emails in folder ", email_folder) + for root, dirs, files in os.walk(email_folder): + for dir in dirs: + email_object = EmailObjectBuilder({}, os.path.join(root, dir)).build() + await KnowledgeBase().insert_object(email_object) - email_folder = "F:\\system\\Downloads\\8081ffdb80925f5bff9c6ab9c4756c7d" - email_object = EmailObjectBuilder({}, email_folder).build() - - await KnowledgeBase().do_embedding(email_object) - - -async def test_query_email(): msg_prompt = AgentPrompt() msg_prompt.messages = [{"role":"user","content":"abcdef"}] - KnowledgeBase().query(msg_prompt) + await KnowledgeBase().query_prompt(msg_prompt) + + class TestKnowledgeBase(unittest.TestCase): def test_embedding(self): - asyncio.run(test_embedding_email()) - - def test_query(self): - asyncio.run(test_query_email()) + asyncio.run(test_embedding_email(self)) if __name__ == "__main__": From 441583b7f8658d5306663499b5b10afd1f3401cb Mon Sep 17 00:00:00 2001 From: tsukasa Date: Mon, 18 Sep 2023 11:28:21 +0800 Subject: [PATCH 060/177] test prompt from knowledge object --- src/aios_kernel/knowledge_base.py | 40 ++++++++++++-------- src/aios_kernel/open_ai_node.py | 16 ++++++++ src/knowledge/core_object/document_object.py | 2 +- src/knowledge/data/writer.py | 33 ++++++++-------- src/knowledge/object/object.py | 3 +- src/knowledge/object/object_id.py | 6 ++- src/knowledge/vector/chroma_store.py | 9 +++-- 7 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index dbb96e6..b229864 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,5 +1,6 @@ # define a knowledge base class import json +import logging from . import AgentPrompt, ComputeKernel from knowledge import * @@ -26,7 +27,7 @@ class KnowledgeBase: text = chunk.read().decode("utf-8") vector = await self.compute_kernel.do_text_embedding(text) - self.store.get_vector_store("default").insert(vector, chunk_id) + await self.store.get_vector_store("default").insert(vector, chunk_id) async def __embedding_image(self, image: ImageObject): desc = {} @@ -37,7 +38,7 @@ class KnowledgeBase: if not not image.get_tags(): desc["tags"] = image.get_tags() vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) - self.store.get_vector_store("default").insert(vector, image.calculate_id()) + await self.store.get_vector_store("default").insert(vector, image.calculate_id()) async def __embedding_video(self, vedio: VideoObject): desc = {} @@ -48,16 +49,20 @@ class KnowledgeBase: if not not vedio.get_tags(): desc["tags"] = vedio.get_tags() vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) - self.store.get_vector_store("default").insert(vector, vedio.calculate_id()) + await self.store.get_vector_store("default").insert(vector, vedio.calculate_id()) async def __embedding_rich_text(self, rich_text: RichTextObject): - for document in rich_text.get_documents().values(): + for document_id in rich_text.get_documents().values(): + document = DocumentObject.decode(self.store.get_object_store().get_object(document_id)) await self.__embedding_document(document) - for image in rich_text.get_images().values(): + for image_id in rich_text.get_images().values(): + image = ImageObject.decode(self.store.get_object_store().get_object(image_id)) await self.__embedding_image(image) - for vedio in rich_text.get_videos().values(): - await self.__embedding_video(vedio) - for rich_text in rich_text.get_rich_texts().values(): + for video_id in rich_text.get_videos().values(): + video = VideoObject.decode(self.store.get_object_store().get_object(video_id)) + await self.__embedding_video(video) + for rich_text_id in rich_text.get_rich_texts().values(): + rich_text = RichTextObject.decode(self.store.get_object_store().get_object(rich_text_id)) await self.__embedding_rich_text(rich_text) async def __embedding_email(self, email: EmailObject): @@ -154,17 +159,19 @@ class KnowledgeBase: await self.__do_embedding(object) async def query_prompt(self, prompt: AgentPrompt): + logging.info(f"query_prompt: {prompt}") objects = await self.query_objects(prompt) knowledge_prompt = self.prompt_from_objects(objects) + logging.info(f"prompt_from_objects result: {knowledge_prompt.as_str()}") prompt.append(knowledge_prompt) async def query_objects(self, prompt: AgentPrompt) -> [ObjectID]: results = [] for msg in prompt.messages: - if msg.role == "user": - vector = await self.compute_kernel.do_text_embedding(msg.content) + if msg["role"] == "user": + vector = await self.compute_kernel.do_text_embedding(msg["content"]) object_ids = await self.store.get_vector_store("default").query(vector, 10) - results.append(object_ids) + results.extend(object_ids) return results def __load_object(self, object_id: ObjectID) -> KnowledgeObject: @@ -187,11 +194,12 @@ class KnowledgeBase: for object_id in object_ids: parents = self.store.get_relation_store().get_related_root_objects(object_id) # last parent is the root object - root_object_id = parents[-1] - if results[root_object_id] is None: - results[root_object_id] = [root_object_id, object_id] + root_object_id = parents[0] if parents else object_id + logging.info(f"object_id: {str(object_id)} root_object_id: {str(root_object_id)}") + if str(root_object_id) in results: + results[str(root_object_id)].append(object_id) else: - results[root_object_id].append(object_id) + results[str(root_object_id)] = [root_object_id, object_id] content = "I found the following contents described with json format:\n" result_desc = [] @@ -228,7 +236,7 @@ class KnowledgeBase: content += ".\n" prompt = AgentPrompt() - prompt.add_message("knowledge", content) + prompt.messages.append({"role": "knowledge", "content": content}) return prompt diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index a37706e..1c39c96 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -84,6 +84,22 @@ class OpenAI_ComputeNode(ComputeNode): resp = openai.Embedding.create(model=model_name, input=input) + + # resp = { + # "object": "list", + # "data": [ + # { + # "object": "embedding", + # "index": 0, + # "embedding": [ + # -0.00930514745414257, + # 0.00765434792265296, + # -0.007167573552578688, + # -0.012373941019177437, + # -0.04884673282504082 + # ]}] + # } + logger.info(f"openai response: {resp}") result = ComputeTaskResult() diff --git a/src/knowledge/core_object/document_object.py b/src/knowledge/core_object/document_object.py index ab02b8d..7210f16 100644 --- a/src/knowledge/core_object/document_object.py +++ b/src/knowledge/core_object/document_object.py @@ -53,7 +53,7 @@ class DocumentObjectBuilder: chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text( self.text, 1024 * 4, - "." + ".?!\n" ) doc = DocumentObject(self.meta, self.tags, chunk_list) doc_id = doc.calculate_id() diff --git a/src/knowledge/data/writer.py b/src/knowledge/data/writer.py index 599c51b..3ac0ed1 100644 --- a/src/knowledge/data/writer.py +++ b/src/knowledge/data/writer.py @@ -74,19 +74,20 @@ class ChunkListWriter: text: str, chunk_max_words: int, separator_chars: str = ".," ) -> List[str]: sentences = re.split(f"[{separator_chars}]", text) - chunk_list = [] - chunk = [] - word_count = 0 - for sentence in sentences: - words = sentence.split() - for word in words: - if word_count < chunk_max_words: - chunk.append(word) - word_count += 1 - else: - chunk_list.append(" ".join(chunk)) - chunk = [word] - word_count = 1 - if chunk: - chunk_list.append(" ".join(chunk)) - return chunk_list \ No newline at end of file + # chunk_list = [] + # chunk = [] + # word_count = 0 + # for sentence in sentences: + # words = sentence.split() + # for word in words: + # if word_count < chunk_max_words: + # chunk.append(word) + # word_count += 1 + # else: + # chunk_list.append(" ".join(chunk)) + # chunk = [word] + # word_count = 1 + # if chunk: + # chunk_list.append(" ".join(chunk)) + # return chunk_list + return sentences \ No newline at end of file diff --git a/src/knowledge/object/object.py b/src/knowledge/object/object.py index 490555b..a63e30a 100644 --- a/src/knowledge/object/object.py +++ b/src/knowledge/object/object.py @@ -54,7 +54,8 @@ class KnowledgeObject(ABC): ) sha256 = hashlib.sha256() sha256.update(data.encode()) - return ObjectID(sha256.digest()) + hash_bytes = sha256.digest() + return ObjectID(bytes([self.object_type]) + hash_bytes[1:]) def encode(self) -> bytes: return pickle.dumps(self) diff --git a/src/knowledge/object/object_id.py b/src/knowledge/object/object_id.py index 42f313a..46955f4 100644 --- a/src/knowledge/object/object_id.py +++ b/src/knowledge/object/object_id.py @@ -44,7 +44,11 @@ class ObjectID: # pylint: disable=too-few-public-methods @staticmethod def new_chunk_id(chunk_hash: HashValue): - return ObjectID(chunk_hash.value) + assert len(chunk_hash.value) == 32, "ObjectID must be 32 bytes long" + return ObjectID(bytes([ObjectType.Chunk]) + chunk_hash.value[1:]) + + def get_object_type(self) -> ObjectType: + return ObjectType(self.value[0]) @staticmethod def hash_data(data: bytes): diff --git a/src/knowledge/vector/chroma_store.py b/src/knowledge/vector/chroma_store.py index 0056375..03006ae 100644 --- a/src/knowledge/vector/chroma_store.py +++ b/src/knowledge/vector/chroma_store.py @@ -30,9 +30,10 @@ class ChromaVectorStore(VectorBase): self.collection = collection async def insert(self, vector: [float], id: ObjectID): + logging.info(f"will insert vector: {vector} id: {str(id)}") self.collection.add( embeddings=vector, - ids=id, + ids=str(id), ) async def query(self, vector: [float], top_k: int) -> [ObjectID]: @@ -40,8 +41,10 @@ class ChromaVectorStore(VectorBase): query_embeddings=vector, n_results=top_k, ) - - return ret["ids"] + logging.info(f"query result {ret}") + if len(ret['ids']) == 0: + return [] + return list(map(ObjectID.from_base58, ret["ids"][0])) async def delete(self, id: ObjectID): self.collection.delete( From 5a26b9614c0345c97f8bd27a5543398beca42abd Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 18 Sep 2023 00:40:37 -0700 Subject: [PATCH 061/177] =?UTF-8?q?=EF=BB=BF1)=20Improve=20aios=5Fshell=20?= =?UTF-8?q?experience,=202)=20Add=202=20formula=20build-in=20agents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rootfs/agents/Jarvis/agent.toml | 6 +++ rootfs/agents/Tracy Wang/agent.toml | 6 +++ src/aios_kernel/README | 3 -- src/aios_kernel/storage.py | 2 +- src/requirements.txt | 1 + src/service/aios_shell/aios_shell.py | 57 ++++++++++++++++++---------- 6 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 rootfs/agents/Jarvis/agent.toml create mode 100644 rootfs/agents/Tracy Wang/agent.toml delete mode 100644 src/aios_kernel/README diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml new file mode 100644 index 0000000..9c2bfb7 --- /dev/null +++ b/rootfs/agents/Jarvis/agent.toml @@ -0,0 +1,6 @@ +instance_id = "Jarvis" +fullname = "Jarvis" + +[[prompt]] +role = "system" +content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" diff --git a/rootfs/agents/Tracy Wang/agent.toml b/rootfs/agents/Tracy Wang/agent.toml new file mode 100644 index 0000000..bb427a7 --- /dev/null +++ b/rootfs/agents/Tracy Wang/agent.toml @@ -0,0 +1,6 @@ +instance_id = "Tracy Wang" +fullname = "Tracy Wang" + +[[prompt]] +role = "system" +content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" diff --git a/src/aios_kernel/README b/src/aios_kernel/README deleted file mode 100644 index 1182ca5..0000000 --- a/src/aios_kernel/README +++ /dev/null @@ -1,3 +0,0 @@ -TODO: -Embading Pipline -Knowlege-Base diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index 64a2c0e..5c00a29 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -84,7 +84,7 @@ class UserConfig: if config_item.value is None: return config_item.default_value - return config_item.value + return config_item.value,config_item def set_user_config(self,key:str,value:Any) -> None: config_item = self.config_table.get(key) diff --git a/src/requirements.txt b/src/requirements.txt index 54d1964..8ade250 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -21,3 +21,4 @@ Requests==2.31.0 stability_sdk toml==0.10.2 +google-cloud-texttospeech diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 3a20679..0e82363 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -5,6 +5,7 @@ import os import logging import re import toml +import shlex from typing import Any, Optional, TypeVar, Tuple, Sequence import argparse @@ -141,6 +142,20 @@ class AIOS_Shell: show_text = FormattedText([("class:title", f"{self.current_topic}@{self.current_target} >>> "), ("class:content", resp)]) return show_text + case 'set_config': + show_text = FormattedText([("class:title", f"set config failed!")]) + if len(args) == 1: + key = args[0] + old_value,config_item = AIStorage.get_instance().get_user_config().get_user_config(key) + if config_item is not None: + value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) + AIStorage.get_instance().get_user_config().set_user_config(key,value) + await AIStorage.get_instance().get_user_config().save_value_to_user_config() + show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) + + return show_text + + case 'open': if len(args) >= 1: target_id = args[0] @@ -190,18 +205,15 @@ history = FileHistory('aios_shell_history.txt') session = PromptSession(history=history) def parse_function_call(func_string): - match = re.search(r'\s*(\w+)\s*\(\s*(.*)\s*\)\s*', func_string) - if not match: + if len(func_string) > 2: + if func_string[0] == '/' and func_string[1] != '/': + str_list = shlex.split(func_string[1:]) + func_name = str_list[0] + params = str_list[1:] + return func_name, params + else: 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(): @@ -233,6 +245,7 @@ def print_welcome_screen(): 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. @@ -241,7 +254,7 @@ def print_welcome_screen(): \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") @@ -253,7 +266,7 @@ def print_welcome_screen(): async def main(): print_welcome_screen() - print("OpenDAN is booting...") + print("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') @@ -294,14 +307,16 @@ async def main(): 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) + completer = WordCompleter(['/send $target $msg $topic', + '/open $target $topic', + '/history $num $offset', + '/login $username', + '/connect $target', + '/set_config $key', + '/list_config', + '/show', + '/exit', + '/help'], ignore_case=True) while True: user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$",completer=completer,style=shell_style) From f2a25b1b998bf1b437e51d053fdeceff3cd3f005 Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Mon, 18 Sep 2023 07:58:08 +0000 Subject: [PATCH 062/177] schedule same nodes with different weight --- src/aios_kernel/compute_kernel.py | 15 +++++++++++++++ src/aios_kernel/compute_node.py | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 82d5c55..a5f31b5 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +import random from typing import Optional import logging import asyncio @@ -59,9 +60,23 @@ class ComputeKernel: asyncio.create_task(_run_task_loop()) def _schedule(self, task) -> ComputeNode: + # find all the node which supports this task + support_nodes = [] + total_weights = 0 for node in self.compute_nodes.values(): if node.is_support(task) is True: + support_nodes.append({ + "pos": total_weights, + "node": node + }) + total_weights += node.weight() + + # hit a random node with weight + hit_pos = random.randint(0, total_weights - 1) + for i in range(min(len(support_nodes) - 1, hit_pos), -1, -1): + if support_nodes[i]["pos"] <= hit_pos: return node + logger.warning( f"task {task.display()} is not support by any compute node") return None diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 873fecd..0e05ed9 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -35,6 +35,9 @@ class ComputeNode(ABC): def is_local(self) -> bool: pass + def weight(self) -> int: + return 1 + def is_trusted(self) -> bool: return True From cecf8091988185d8331c6d919ae860402699b3e8 Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Mon, 18 Sep 2023 08:03:34 +0000 Subject: [PATCH 063/177] add notes for a method --- src/aios_kernel/compute_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py index 0e05ed9..7ce7292 100644 --- a/src/aios_kernel/compute_node.py +++ b/src/aios_kernel/compute_node.py @@ -35,6 +35,7 @@ class ComputeNode(ABC): def is_local(self) -> bool: pass + # the hit weight when select this node in schedule def weight(self) -> int: return 1 From a3025274e33f80c1be8e4f01255ff82332fd6ab5 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 18 Sep 2023 11:41:16 -0700 Subject: [PATCH 064/177] fix merge bugs. --- build_all_in_one.sh | 2 - src/aios_kernel/compute_task.py | 15 +-- src/aios_kernel/open_ai_node.py | 141 +++++++++++++-------------- src/requirements.txt | 13 +-- src/service/aios_shell/aios_shell.py | 2 +- 5 files changed, 84 insertions(+), 89 deletions(-) diff --git a/build_all_in_one.sh b/build_all_in_one.sh index d1eb62b..b6040c4 100644 --- a/build_all_in_one.sh +++ b/build_all_in_one.sh @@ -1,5 +1,3 @@ #!/usr/bin/bash - -pipreqs ./src --force # Build the docker image docker build -t aios . diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index 3b278b4..1433379 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -12,12 +12,13 @@ class ComputeTaskState(Enum): 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 + NONE = "None" + LLM_COMPLETION = "llm_completion" + TEXT_2_IMAGE = "text_2_image" + IMAGE_2_IMAGE = "image_2_image" + VOICE_2_TEXT = "voice_2_text" + TEXT_2_VOICE = "text_2_voice" + TEXT_EMBEDDING ="text_embedding" class ComputeTask: @@ -54,7 +55,7 @@ class ComputeTask: self.params["inner_functions"] = inner_functions def set_text_embedding_params(self, input, model_name=None, callchain_id = None): - self.task_type = "text_embedding" + self.task_type = ComputeTaskType.TEXT_EMBEDDING self.create_time = time.time() self.task_id = uuid.uuid4().hex self.callchain_id = callchain_id diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index fa1f30a..eaeb23d 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -59,85 +59,84 @@ class OpenAI_ComputeNode(ComputeNode): def _run_task(self, task: ComputeTask): task.state = ComputeTaskState.RUNNING - if task.task_type == "text_embedding": - model_name = task.params["model_name"] - input = task.params["input"] - logger.info(f"call openai {model_name} input: {input}") + match task.task_type: + case ComputeTaskType.TEXT_EMBEDDING: + model_name = task.params["model_name"] + input = task.params["input"] + logger.info(f"call openai {model_name} input: {input}") - resp = openai.Embedding.create(model=model_name, - input=input) - - # resp = { - # "object": "list", - # "data": [ - # { - # "object": "embedding", - # "index": 0, - # "embedding": [ - # -0.00930514745414257, - # 0.00765434792265296, - # -0.007167573552578688, - # -0.012373941019177437, - # -0.04884673282504082 - # ]}] - # } + resp = openai.Embedding.create(model=model_name, + input=input) + + # resp = { + # "object": "list", + # "data": [ + # { + # "object": "embedding", + # "index": 0, + # "embedding": [ + # -0.00930514745414257, + # 0.00765434792265296, + # -0.007167573552578688, + # -0.012373941019177437, + # -0.04884673282504082 + # ]}] + # } - logger.info(f"openai response: {resp}") + logger.info(f"openai response: {resp}") - result = ComputeTaskResult() - result.set_from_task(task) - result.worker_id = self.node_id - result.result = resp["data"][0]["embedding"] + result = ComputeTaskResult() + result.set_from_task(task) + result.worker_id = self.node_id + 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"] + return result + case ComputeTaskType.LLM_COMPLETION: + mode_name = task.params["model_name"] + prompts = task.params["prompts"] + max_token_size = task.params.get("max_token_size") + llm_inner_functions = task.params["inner_functions"] + if max_token_size is None: + max_token_size = 4000 + + logger.info(f"call openai {mode_name} prompts: {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, + if llm_inner_functions is None: + 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? + max_tokens=max_token_size, + temperature=0.7) + else: + resp = openai.ChatCompletion.create(model=mode_name, + messages=prompts, + functions=llm_inner_functions, + max_tokens=max_token_size, + temperature=0.7) # TODO: add temperature to task params? - - logger.info(f"openai response: {resp}") + + logger.info(f"openai response: {resp}") - result = ComputeTaskResult() - result.set_from_task(task) + 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 + 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 + case _: + task.state = ComputeTaskState.ERROR + return None def start(self): if self.is_start is True: @@ -167,9 +166,9 @@ class OpenAI_ComputeNode(ComputeNode): def is_support(self, task: ComputeTask) -> bool: if task.task_type == ComputeTaskType.LLM_COMPLETION: - if (not task.params["model_name"] or task.params["model_name"] == "gpt-4-0613") + if not task.params["model_name"] or task.params["model_name"] == "gpt-4-0613": return True - if task.task_type == "text_embedding": + if task.task_type == ComputeTaskType.TEXT_EMBEDDING: if task.params["model_name"] == "text-embedding-ada-002": return True return False diff --git a/src/requirements.txt b/src/requirements.txt index 8ade250..b9e630e 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,7 +1,4 @@ - chromadb==0.4 -openai==0.28 -toml==0.10 moviepy==1.0 base58==2.1 base36==0.1 @@ -11,14 +8,14 @@ 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 +protobuf stability_sdk -toml==0.10.2 - +toml +base58 google-cloud-texttospeech +openai +Pillow diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 0e82363..d71ae7d 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -102,7 +102,7 @@ class AIOS_Shell: 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") + tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml") tunnel_config = None try: tunnel_config = toml.load(tunnels_config_path) From 0d8ee2a7afbf6d0c9d76e62136c5ed2259785689 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 18 Sep 2023 12:51:52 -0700 Subject: [PATCH 065/177] fix merge bugs --- src/aios_kernel/open_ai_node.py | 2 +- src/aios_kernel/storage.py | 2 +- src/aios_kernel/tg_tunnel.py | 1 + src/service/aios_shell/aios_shell.py | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index eaeb23d..b7c9110 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -40,7 +40,7 @@ class OpenAI_ComputeNode(ComputeNode): if os.getenv("OPENAI_API_KEY") is not None: self.openai_api_key = os.getenv("OPENAI_API_KEY") else: - self.openai_api_key = AIStorage.get_instance().get_user_config().get_user_config("openai_api_key") + self.openai_api_key = AIStorage.get_instance().get_user_config().get_user_config("openai_api_key").value if self.openai_api_key is None: logger.error("openai_api_key is None!") diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index 5c00a29..33e5564 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -84,7 +84,7 @@ class UserConfig: if config_item.value is None: return config_item.default_value - return config_item.value,config_item + return config_item def set_user_config(self,key:str,value:Any) -> None: config_item = self.config_table.get(key) diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index efa23fc..36c991f 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -49,6 +49,7 @@ class TelegramTunnel(AgentTunnel): logger.warning(f"tunnel {self.tunnel_id} is already started") return False self.is_start = True + logger.warning(f"tunnel {self.tunnel_id} is starting...") self.app:Application = Application.builder().token(self.tg_token).build() self.app.add_handler(MessageHandler(filters.TEXT, self.on_message)) diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index d71ae7d..01cbc7b 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -146,7 +146,8 @@ class AIOS_Shell: show_text = FormattedText([("class:title", f"set config failed!")]) if len(args) == 1: key = args[0] - old_value,config_item = AIStorage.get_instance().get_user_config().get_user_config(key) + config_item = AIStorage.get_instance().get_user_config().get_user_config(key) + old_value = config_item.value if config_item is not None: value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) AIStorage.get_instance().get_user_config().set_user_config(key,value) From 874aa98e21f0733b68421cd7023806b5b72e4828 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 18 Sep 2023 23:25:44 -0700 Subject: [PATCH 066/177] 1) Improve Build-in agent "Tracy Wang" 2) fix bug --- rootfs/agents/Tracy Wang/agent.toml | 9 +++- src/aios_kernel/agent.py | 9 ++-- src/aios_kernel/agent_message.py | 4 +- src/aios_kernel/bus.py | 2 +- src/aios_kernel/compute_kernel.py | 4 +- src/aios_kernel/queue_compute_node.py | 2 +- src/aios_kernel/tg_tunnel.py | 68 +++++++++++++++++++++++---- src/service/aios_shell/aios_shell.py | 9 ++-- 8 files changed, 81 insertions(+), 26 deletions(-) diff --git a/rootfs/agents/Tracy Wang/agent.toml b/rootfs/agents/Tracy Wang/agent.toml index bb427a7..c815314 100644 --- a/rootfs/agents/Tracy Wang/agent.toml +++ b/rootfs/agents/Tracy Wang/agent.toml @@ -3,4 +3,11 @@ fullname = "Tracy Wang" [[prompt]] role = "system" -content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" +content = """ +Your name is Tracy Wang, and you are my advanced private English tutor. +## You will assess my English proficiency based on all available information, using a 5-point scale. +## While interacting with me normally, you will adjust my input into more idiomatic American sentences. +## Depending on my level of English, you will annotate potentially incorrect words with phonetic symbols or provide expanded explanations for certain words and phrases. +## If I send you something that is not in English, it means I don't know how to say it in American English. You will first translate what I've sent into English and then respond according to the above rules. +## You will chat with me like a friend, rather than just teaching me lessons. +""" \ No newline at end of file diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 52dd2df..333fcb4 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -239,13 +239,12 @@ class AIAgent: 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 + messages = chatsession.read_history() # read result_prompt = AgentPrompt() for msg in reversed(messages): - if msg.target == chatsession.owner_id: - result_prompt.messages.append({"role":"user","content":msg.body}) - elif msg.sender == chatsession.owner_id: + if msg.sender == self.agent_id: result_prompt.messages.append({"role":"assistant","content":msg.body}) - + else: + result_prompt.messages.append({"role":"user","content":msg.body}) return result_prompt diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 0640319..8f7ab04 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -67,6 +67,7 @@ class AgentMsg: @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.create_time = time.time() msg.func_name = func_name msg.args = args msg.prev_msg_id = prev_msg_id @@ -75,6 +76,7 @@ class AgentMsg: def create_action_msg(self,action_name:str,args:dict,caller:str): msg = AgentMsg(AgentMsgType.TYPE_ACTION) + msg.create_time = time.time() msg.func_name = action_name msg.args = args msg.prev_msg_id = self.msg_id @@ -85,7 +87,7 @@ class AgentMsg: 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.create_time = time.time() resp_msg.rely_msg_id = self.msg_id resp_msg.sender = self.target diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py index 22bf209..1e97733 100644 --- a/src/aios_kernel/bus.py +++ b/src/aios_kernel/bus.py @@ -90,7 +90,7 @@ class AIBus: await asyncio.sleep(0.2) retry_times += 1 - if retry_times > 100: + if retry_times > 5*120: # default timeout is 120 sec msg.status = AgentMsgStatus.ERROR return None diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 525c266..18ddeaf 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -30,8 +30,6 @@ class ComputeKernel: self.is_start = False self.compute_nodes = {} - self.start() - def run(self, task: ComputeTask) -> None: # check there is compute node can support this task if self.is_task_support(task) is False: @@ -41,7 +39,7 @@ class ComputeKernel: # add task to working_queue self.task_queue.put_nowait(task) - def start(self): + async def start(self): if self.is_start is True: logger.warn("compute_kernel is already start") return diff --git a/src/aios_kernel/queue_compute_node.py b/src/aios_kernel/queue_compute_node.py index e975cd8..1475672 100644 --- a/src/aios_kernel/queue_compute_node.py +++ b/src/aios_kernel/queue_compute_node.py @@ -53,7 +53,7 @@ class Queue_ComputeNode(ComputeNode): return result - def start(self): + async def start(self): async def _run_task_loop(): while True: task = await self.task_queue.get() diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index 36c991f..55337a6 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -2,12 +2,17 @@ import logging import threading import asyncio import uuid +import time from typing import Callable from telegram import ForceReply, Update from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters +from telegram import Bot +from telegram.ext import Updater +from telegram.error import Forbidden, NetworkError + from .tunnel import AgentTunnel from .contact_manager import ContactManager from .agent_message import AgentMsg @@ -42,8 +47,27 @@ class TelegramTunnel(AgentTunnel): super().__init__() self.is_start = False self.tg_token = tg_token + #self.bot:Bot = None + #self.update_queue = None #self.tunnel_id = "tg_tunnel#" + self.app.bot.id + async def _do_process_raw_message(self,bot: Bot, update_id: int) -> int: + """Echo the message the user sent.""" + # Request updates after the last update_id + updates = await bot.get_updates(offset=update_id, timeout=10, allowed_updates=Update.ALL_TYPES) + for update in updates: + next_update_id = update.update_id + 1 + + # your bot can receive updates without messages + # and not all messages contain text + if update.message and update.message.text: + # Reply to the message + #logger.info("Found message %s!", update.message.text) + await self.on_message(update) + return next_update_id + + return update_id + async def start(self) -> bool: if self.is_start: logger.warning(f"tunnel {self.tunnel_id} is already started") @@ -51,16 +75,38 @@ class TelegramTunnel(AgentTunnel): self.is_start = True logger.warning(f"tunnel {self.tunnel_id} is starting...") - self.app:Application = Application.builder().token(self.tg_token).build() - self.app.add_handler(MessageHandler(filters.TEXT, self.on_message)) + self.bot = Bot(self.tg_token) + self.update_queue = asyncio.Queue() + self.bot_updater = Updater(self.bot,update_queue=self.update_queue) + + #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) + async def _run_app(): + #loop = asyncio.new_event_loop() + #asyncio.set_event_loop(loop) + #self.app.run_polling(allowed_updates=Update.ALL_TYPES) + + try: + update_id = (await self.bot.get_updates())[0].update_id + except IndexError: + update_id = None - self.poll_thread = threading.Thread(target=_run_app) - self.poll_thread.start() + #logger.info("listening for new messages...") + while True: + try: + update_id = await self._do_process_raw_message(self.bot, update_id) + except NetworkError: + await asyncio.sleep(1) + except Forbidden: + # The user has removed or blocked the bot. + update_id += 1 + + #self.poll_thread = threading.Thread(target=_run_app) + #self.poll_thread.start() + asyncio.create_task(_run_app()) + logger.warning(f"tunnel {self.tunnel_id} started.") return True async def close(self) -> None: @@ -75,7 +121,7 @@ class TelegramTunnel(AgentTunnel): 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() + agent_msg.create_time = time.time() #if update.message.photo is not None: # agent_msg.body_mime = "image" # agent_msg.body = update.message.photo[-1].get_file().download() @@ -83,7 +129,7 @@ class TelegramTunnel(AgentTunnel): - async def on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + async def on_message(self, update: Update) -> None: cm = ContactManager.get_instance() reomte_user_name = f"{update.effective_user.id}@telegram" #contact = cm.get_by_name(update.effective_user.username) @@ -97,7 +143,9 @@ class TelegramTunnel(AgentTunnel): 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) + #await context.bot.send_chat_action(chat_id=update.effective_chat.id, action="thinking") resp_msg = await self.ai_bus.send_message(agent_msg) + logger.info(f"process message {agent_msg.msg_id} from {agent_msg.sender} to {agent_msg.target}") if resp_msg is None: await update.message.reply_text(f"{self.target_id} process message error") else: diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 01cbc7b..e5f2642 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -88,12 +88,13 @@ class AIOS_Shell: 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) + await llama_ai_node.start() + ComputeKernel.get_instance().add_compute_node(llama_ai_node) + + await ComputeKernel.get_instance().start() AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) @@ -318,7 +319,7 @@ async def main(): '/show', '/exit', '/help'], ignore_case=True) - + await asyncio.sleep(0.2) while True: user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$",completer=completer,style=shell_style) if len(user_input) <= 1: From 56d15fd0034f066267458ee9d8d07b70727df3df Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 19 Sep 2023 00:23:19 -0700 Subject: [PATCH 067/177] fix workflow bug, disable local LLM temporary. --- src/aios_kernel/open_ai_node.py | 2 +- src/aios_kernel/tg_tunnel.py | 26 +++++-------------- src/aios_kernel/workflow.py | 2 +- src/aios_kernel/workflow_env.py | 2 +- .../workflow_manager/workflow_manager.py | 4 +-- src/service/aios_shell/aios_shell.py | 2 +- 6 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index b7c9110..6911886 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -95,7 +95,7 @@ class OpenAI_ComputeNode(ComputeNode): mode_name = task.params["model_name"] prompts = task.params["prompts"] max_token_size = task.params.get("max_token_size") - llm_inner_functions = task.params["inner_functions"] + llm_inner_functions = task.params.get("inner_functions") if max_token_size is None: max_token_size = 4000 diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index 55337a6..53478d2 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -47,23 +47,18 @@ class TelegramTunnel(AgentTunnel): super().__init__() self.is_start = False self.tg_token = tg_token - #self.bot:Bot = None - #self.update_queue = None - #self.tunnel_id = "tg_tunnel#" + self.app.bot.id + self.bot:Bot = None + self.update_queue = None async def _do_process_raw_message(self,bot: Bot, update_id: int) -> int: - """Echo the message the user sent.""" # Request updates after the last update_id updates = await bot.get_updates(offset=update_id, timeout=10, allowed_updates=Update.ALL_TYPES) for update in updates: next_update_id = update.update_id + 1 - # your bot can receive updates without messages - # and not all messages contain text if update.message and update.message.text: - # Reply to the message - #logger.info("Found message %s!", update.message.text) - await self.on_message(update) + + await self.on_message(bot,update) return next_update_id return update_id @@ -79,15 +74,8 @@ class TelegramTunnel(AgentTunnel): self.update_queue = asyncio.Queue() self.bot_updater = Updater(self.bot,update_queue=self.update_queue) - #self.app:Application = Application.builder().token(self.tg_token).build() - - #self.app.add_handler(MessageHandler(filters.TEXT, self.on_message)) async def _run_app(): - #loop = asyncio.new_event_loop() - #asyncio.set_event_loop(loop) - #self.app.run_polling(allowed_updates=Update.ALL_TYPES) - try: update_id = (await self.bot.get_updates())[0].update_id except IndexError: @@ -103,8 +91,6 @@ class TelegramTunnel(AgentTunnel): # The user has removed or blocked the bot. update_id += 1 - #self.poll_thread = threading.Thread(target=_run_app) - #self.poll_thread.start() asyncio.create_task(_run_app()) logger.warning(f"tunnel {self.tunnel_id} started.") return True @@ -129,7 +115,7 @@ class TelegramTunnel(AgentTunnel): - async def on_message(self, update: Update) -> None: + async def on_message(self, bot:Bot, update: Update) -> None: cm = ContactManager.get_instance() reomte_user_name = f"{update.effective_user.id}@telegram" #contact = cm.get_by_name(update.effective_user.username) @@ -143,7 +129,7 @@ class TelegramTunnel(AgentTunnel): 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) - #await context.bot.send_chat_action(chat_id=update.effective_chat.id, action="thinking") + #await bot.send_chat_action(chat_id=update.effective_chat.id, action="typing") resp_msg = await self.ai_bus.send_message(agent_msg) logger.info(f"process message {agent_msg.msg_id} from {agent_msg.sender} to {agent_msg.target}") if resp_msg is None: diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 46b7507..f5f440b 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -126,7 +126,7 @@ class Workflow: if env_id is None: continue - remote_env = Environment.get_env_by_id(_node.get(env_id)) + remote_env = Environment.get_env_by_id(env_id) if remote_env is None: logger.error(f"Workflow load connected_env failed, env {env_id} not found!") return False diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 87d2fea..4ef033b 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -58,7 +58,7 @@ class CalenderEnvironment(Environment): def stop(self): self.is_run = False - def get_now(self,key:str)->str: + def get_now(self)->str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index d3d49fe..3604327 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -22,7 +22,7 @@ class WorkflowManager: 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.workflow_env = PackageEnvManager().get_env(f"{system_app_dir}/workflows/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") @@ -33,7 +33,7 @@ class WorkflowManager: async def _load_workflow_agents(self,workflow:Workflow) -> bool: for v in workflow.role_group.roles.values(): - agent = await AgentManager().get(v.agent_name) + agent = await AgentManager.get_instance().get(v.agent_name) if agent is None: logger.error(f"load agent {v.agent_name} failed!") return False diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index e5f2642..10abbd4 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -92,7 +92,7 @@ class AIOS_Shell: llama_ai_node = LocalLlama_ComputeNode() await llama_ai_node.start() - ComputeKernel.get_instance().add_compute_node(llama_ai_node) + #ComputeKernel.get_instance().add_compute_node(llama_ai_node) await ComputeKernel.get_instance().start() From feca072a4f2a0327057c2bc08344d17d5902d9f1 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 19 Sep 2023 00:53:13 -0700 Subject: [PATCH 068/177] Improve UserConfig Model, let aios shell remember last target~ --- src/aios_kernel/open_ai_node.py | 2 +- src/aios_kernel/storage.py | 19 +++++++++++++------ src/service/aios_shell/aios_shell.py | 25 ++++++++++++++++++------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 6911886..8362a5a 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -40,7 +40,7 @@ class OpenAI_ComputeNode(ComputeNode): if os.getenv("OPENAI_API_KEY") is not None: self.openai_api_key = os.getenv("OPENAI_API_KEY") else: - self.openai_api_key = AIStorage.get_instance().get_user_config().get_user_config("openai_api_key").value + self.openai_api_key = AIStorage.get_instance().get_user_config().get_value("openai_api_key") if self.openai_api_key is None: logger.error("openai_api_key is None!") diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index 33e5564..f040b33 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -55,7 +55,7 @@ class UserConfig: except Exception as e: logger.warn(f"load user config from {file_path} failed!") - async def save_value_to_user_config(self) -> None: + async def save_to_user_config(self) -> None: will_save_config = {} for key,value in self.config_table.items(): if value.user_set: @@ -76,17 +76,24 @@ class UserConfig: return True - def get_user_config(self,key:str) -> Any: + def get_config_item(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) + + return config_item + + def get_value(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 - def set_user_config(self,key:str,value:Any) -> None: + return config_item.value + + def set_value(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) @@ -97,7 +104,7 @@ class UserConfig: #TODO: save to file? - def check_user_config(self) -> None: + def check_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: diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 10abbd4..d47ff3a 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -53,6 +53,8 @@ class AIOS_Shell: openai_node = OpenAI_ComputeNode.get_instance() openai_node.declare_user_config() + user_config.add_user_config("shell.current","last opened target and topic",True,"default@Jarvis") + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: target_id = msg.target.split(".")[0] @@ -147,12 +149,12 @@ class AIOS_Shell: show_text = FormattedText([("class:title", f"set config failed!")]) if len(args) == 1: key = args[0] - config_item = AIStorage.get_instance().get_user_config().get_user_config(key) - old_value = config_item.value + config_item = AIStorage.get_instance().get_user_config().get_config_item(key) + old_value = AIStorage.get_instance().get_user_config().get_value(key) if config_item is not None: value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) - AIStorage.get_instance().get_user_config().set_user_config(key,value) - await AIStorage.get_instance().get_user_config().save_value_to_user_config() + AIStorage.get_instance().get_user_config().set_value(key,value) + await AIStorage.get_instance().get_user_config().save_to_user_config() show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) return show_text @@ -167,11 +169,14 @@ class AIOS_Shell: self.current_target = target_id self.current_topic = topic show_text = FormattedText([("class:title", f"current session switch to {topic}@{target_id}")]) + AIStorage.get_instance().get_user_config().set_value("shell.current",f"{self.current_topic}@{self.current_target}") + await AIStorage.get_instance().get_user_config().save_to_user_config() 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 @@ -221,9 +226,9 @@ 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) + AIStorage.get_instance().get_user_config().set_value(key,user_input) - await AIStorage.get_instance().get_user_config().save_value_to_user_config() + await AIStorage.get_instance().get_user_config().save_to_user_config() return True async def main_daemon_loop(shell:AIOS_Shell): @@ -286,7 +291,7 @@ async def main(): 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() + check_result = AIStorage.get_instance().get_user_config().check_config() if check_result is not None: if is_daemon: logger.error(check_result) @@ -319,6 +324,12 @@ async def main(): '/show', '/exit', '/help'], ignore_case=True) + + current = AIStorage.get_instance().get_user_config().get_value("shell.current") + current = current.split("@") + shell.current_target = current[1] + shell.current_topic = current[0] + await asyncio.sleep(0.2) while True: user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$",completer=completer,style=shell_style) From 298ee73a6b44a02720fd6a535b6a6b5337d5462d Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 19 Sep 2023 15:43:17 -0700 Subject: [PATCH 069/177] aios shell support /connect command , connect to a agent though tunnel easy. --- src/aios_kernel/storage.py | 4 +- src/aios_kernel/tg_tunnel.py | 4 +- src/aios_kernel/tunnel.py | 35 ++++++++-- src/service/aios_shell/aios_shell.py | 99 ++++++++++++++++++++++++---- 4 files changed, 123 insertions(+), 19 deletions(-) diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index f040b33..e982645 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -14,11 +14,11 @@ class ResourceLocation: pass class UserConfigItem: - def __init__(self) -> None: + def __init__(self,desc:str=None) -> None: self.default_value = None self.is_optional = False self.item_type = "str" - self.desc = None + self.desc = desc self.value = None self.user_set = False diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index 53478d2..b08bc6d 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -68,7 +68,7 @@ class TelegramTunnel(AgentTunnel): logger.warning(f"tunnel {self.tunnel_id} is already started") return False self.is_start = True - logger.warning(f"tunnel {self.tunnel_id} is starting...") + logger.info(f"tunnel {self.tunnel_id} is starting...") self.bot = Bot(self.tg_token) self.update_queue = asyncio.Queue() @@ -92,7 +92,7 @@ class TelegramTunnel(AgentTunnel): update_id += 1 asyncio.create_task(_run_app()) - logger.warning(f"tunnel {self.tunnel_id} started.") + logger.info(f"tunnel {self.tunnel_id} started.") return True async def close(self) -> None: diff --git a/src/aios_kernel/tunnel.py b/src/aios_kernel/tunnel.py index bbd18d3..a3691cd 100644 --- a/src/aios_kernel/tunnel.py +++ b/src/aios_kernel/tunnel.py @@ -13,20 +13,47 @@ class AgentTunnel(ABC): 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: + for tunnel_id,tunnel_config in config.items(): loader = cls._all_loader.get(tunnel_config["type"]) + tid = tunnel_config.get("tunnel_id") + if tid is not None: + if tunnel_id != tid: + logger.warning(f"load tunnel {tunnel_id} error,{tunnel_id} != {tid} in config!") + continue + else: + tunnel_config["tunnel_id"] = tunnel_id + if loader is not None: tunnel = await loader(tunnel_config) if tunnel is not None: - cls._all_tunnels[tunnel.tunnel_id] = tunnel + cls._all_tunnels[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") + logger.error(f"load tunnel {tunnel_id} failed") else: - logger.error(f"load tunnel {tunnel_config['type']} failed,loader not found") + logger.error(f"load tunnel {tunnel_id} failed,loader not found") + + @classmethod + async def load_tunnel_from_config(cls,tunnel_config:dict): + 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() + return True + else: + logger.error(f"load tunnel {tunnel_config['tunnel_id']} failed") + else: + logger.error(f"load tunnel {tunnel_config['type']} failed,loader not found") + + return False + def __init__(self) -> None: super().__init__() diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index d47ff3a..cf79d4c 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -34,8 +34,9 @@ logger = logging.getLogger(__name__) shell_style = Style.from_dict({ 'title': '#87d7ff bold', #RGB - 'content': '#007f00 bold', + 'content': '#007f00', # resp content 'prompt': '#00FF00', + 'error': '#8F0000 bold' }) @@ -110,12 +111,10 @@ class AIOS_Shell: try: tunnel_config = toml.load(tunnels_config_path) if tunnel_config is not None: - await AgentTunnel.load_all_tunnels_from_config(tunnel_config["tunnels"]) + await AgentTunnel.load_all_tunnels_from_config(tunnel_config) except Exception as e: logger.warning(f"load tunnels config from {tunnels_config_path} failed!") - - return True @@ -135,6 +134,50 @@ class AIOS_Shell: async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg: pass + + + async def get_tunnel_config_from_input(self,tunnel_target,tunnel_type): + tunnel_config = {} + tunnel_config["tunnel_id"] = f"{tunnel_type}_2_{tunnel_target}" + tunnel_config["target"] = tunnel_target + intpu_table = {} + tunnel_introduce : str = "" + match tunnel_type: + case "telegram": + tunnel_config["type"] = "TelegramTunnel" + intpu_table["token"] = UserConfigItem("telegram bot token") + case "email": + tunnel_config["type"] = "EmailTunnel" + case _: + error_text = FormattedText([("class:error", f"tunnel type {tunnel_type}not support!")]) + print_formatted_text(error_text,style=shell_style) + return None + + intro_text = FormattedText([("class:prompt", tunnel_introduce)]) + print_formatted_text(intro_text,style=shell_style) + for key,item in intpu_table.items(): + user_input = await try_get_input(f"{key} : {item.desc}") + if user_input is None: + return None + + tunnel_config[key] = user_input + + return tunnel_config + + + async def append_tunnel_config(self,tunnel_config): + user_data_dir = AIStorage.get_instance().get_myai_dir() + tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml") + try: + all_tunnels = toml.load(tunnels_config_path) + if all_tunnels is not None: + all_tunnels[tunnel_config["tunnel_id"]] = tunnel_config + f = open(tunnels_config_path,"w") + if f: + toml.dump(all_tunnels,f) + except Exception as e: + logger.warning(f"load tunnels config from {tunnels_config_path} failed!") + async def call_func(self,func_name, args): match func_name: case 'send': @@ -158,8 +201,24 @@ class AIOS_Shell: show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) return show_text + case 'connect': + show_text = FormattedText([("class:title", "args error, /connect $target telegram | email")]) + if len(args) < 1: + return show_text + tunnel_target = args[0] + if len(args) < 2: + tunnel_type = "telegram" + else: + tunnel_type = args[1] + tunnel_config = await self.get_tunnel_config_from_input(tunnel_target,tunnel_type) + if tunnel_config: + if await AgentTunnel.load_tunnel_from_config(tunnel_config): + # append + await self.append_tunnel_config(tunnel_config) + show_text = FormattedText([("class:title", f"connect to {tunnel_target} success!")]) + return show_text case 'open': if len(args) >= 1: target_id = args[0] @@ -221,10 +280,28 @@ def parse_function_call(func_string): else: return None +async def try_get_input(desc:str,check_func:callable = None) -> str: + user_input = await session.prompt_async(f"{desc} \nType /exit to abort. \nPlease input:",style=shell_style) + err_str = "" + if check_func is None: + if len(user_input) > 0: + if user_input != "/exit": + return user_input + else: + return None + + else: + is_ok,err_str = check_func(user_input) + if is_ok: + return user_input + + error_text = FormattedText([("class:error", err_str)]) + print_formatted_text(error_text,style=shell_style) + return await try_get_input(desc,check_func) 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) + user_input = await try_get_input(f"System config {key} ({item.desc}) not define!") if len(user_input) > 0: AIStorage.get_instance().get_user_config().set_value(key,user_input) @@ -240,12 +317,12 @@ async def main_daemon_loop(shell:AIOS_Shell): def print_welcome_screen(): print("\033[1;31m") logo = """ -\t_______ ____________________ __ -\t__ __ \______________________ __ \__ |__ | / / -\t_ / / /__ __ \ _ \_ __ \_ / / /_ /| |_ |/ / -\t/ /_/ /__ /_/ / __/ / / / /_/ /_ ___ | /| / -\t\____/ _ .___/\___//_/ /_//_____/ /_/ |_/_/ |_/ -\t /_/ +\t _______ ____________________ __ +\t __ __ \______________________ __ \__ |__ | / / +\t _ / / /__ __ \ _ \_ __ \_ / / /_ /| |_ |/ / +\t / /_/ /__ /_/ / __/ / / / /_/ /_ ___ | /| / +\t \____/ _ .___/\___//_/ /_//_____/ /_/ |_/_/ |_/ +\t /_/ """ print(logo) From 40e14ee025c15c15c12bb426f652641828698b53 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 19 Sep 2023 18:25:25 -0700 Subject: [PATCH 070/177] Fix workflow system prompt bugs. --- rootfs/workflows/math_school/workflow.toml | 10 +- src/aios_kernel/agent.py | 2 +- src/aios_kernel/agent_message.py | 18 +--- src/aios_kernel/ai_function.py | 15 +++ src/aios_kernel/workflow.py | 118 ++++++++++++--------- src/service/aios_shell/aios_shell.py | 1 + test/workflow_test.py | 38 ++++++- 7 files changed, 132 insertions(+), 70 deletions(-) diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml index 69b998b..202ac50 100644 --- a/rootfs/workflows/math_school/workflow.toml +++ b/rootfs/workflows/math_school/workflow.toml @@ -20,9 +20,13 @@ agent="math_teacher" [[roles."小学老师".prompt]] role="system" content="""现在时间是:{now}你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 -当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师 -当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。 -你会根据教导处主任的指示,定期调整教学方法""" +你的任何处理结果,都要用下面方式汇报给给教导处主任 +``` +##/sendmsg 教导处主任 +处理结果 +``` + +""" [roles."初中老师"] diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 333fcb4..25c7a4a 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -6,6 +6,7 @@ import logging import uuid import time import json +import shlex from .agent_message import AgentMsg, AgentMsgStatus, AgentMsgType from .chatsession import AIChatSession @@ -15,7 +16,6 @@ from .environment import Environment logger = logging.getLogger(__name__) - class AgentPrompt: def __init__(self) -> None: self.messages = [] diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 8f7ab04..07227e9 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -2,6 +2,7 @@ from enum import Enum import uuid import time import re +import shlex class AgentMsgType(Enum): TYPE_MSG = 0 @@ -123,19 +124,8 @@ class AgentMsg: @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] - + str_list = shlex.split(func_string) + func_name = str_list[0] + params = str_list[1:] return func_name, params diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py index 08738ab..ae5df52 100644 --- a/src/aios_kernel/ai_function.py +++ b/src/aios_kernel/ai_function.py @@ -69,6 +69,21 @@ class AIFunction: #def load_from_config(self,config:dict) -> bool: # pass +class FunctionItem: + def __init__(self,name,args) -> None: + self.name = name + self.args = args + self.body = None + + def append_body(self,body:str) -> None: + if self.body is None: + self.body = body + else: + self.body += body + + def dumps(self) -> str: + pass + # call chain is a combination of ai_function,group of ai_function. class CallChain: def __init__(self) -> None: diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index f5f440b..3d98dce 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -4,7 +4,7 @@ import json import os import time from asyncio import Queue -from typing import Optional,Tuple +from typing import Optional,Tuple,List from abc import ABC, abstractmethod from .environment import Environment,EnvironmentEvent @@ -12,7 +12,7 @@ from .agent_message import AgentMsg,AgentMsgStatus from .agent import AgentPrompt,AgentMsg from .chatsession import AIChatSession from .role import AIRole,AIRoleGroup -from .ai_function import AIFunction +from .ai_function import AIFunction,FunctionItem from .compute_kernel import ComputeKernel from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState from .bus import AIBus @@ -42,10 +42,10 @@ class LLMResult: def __init__(self) -> None: self.state : str = "ignore" self.resp : str = "" - self.post_msgs = [] - self.send_msgs = [] - self.calls = [] - self.post_calls = [] + self.post_msgs : List[AgentMsg] = [] + self.send_msgs : List[AgentMsg] = [] + self.calls : List[FunctionItem] = [] + self.post_calls : List[FunctionItem] = [] class Workflow: @@ -255,46 +255,59 @@ class Workflow: 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 + + def check_args(func_item:FunctionItem): + match func_name: + case "send_msg":# sendmsg($target_id,$msg_content) + if len(func_args) != 1: + logger.error(f"parse sendmsg failed! {func_call}") + return False + new_msg = AgentMsg() + target_id = func_item.args[0] + msg_content = func_item.body + new_msg.set("_",target_id,msg_content) + + r.send_msgs.append(new_msg) + is_need_wait = True + + case "post_msg":# postmsg($target_id,$msg_content) + if len(func_args) != 1: + logger.error(f"parse postmsg failed! {func_call}") + return False + new_msg = AgentMsg() + target_id = func_item.args[0] + msg_content = func_item.body + new_msg.set("_",target_id,msg_content) + r.post_msgs.append(new_msg) + + case "call":# call($func_name,$args_str) + r.calls.append(func_item) + is_need_wait = True + return True + case "post_call": # post_call($func_name,$args_str) + r.post_calls.append(func_item) + return True - r.resp += line + "\n" + current_func : FunctionItem = None + for line in lines: + if line.startswith("##/"): + if current_func: + if check_args(current_func) is False: + r.resp += current_func.dumps() + + func_name,func_args = AgentMsg.parse_function_call(line[3:]) + current_func = FunctionItem(func_name,func_args) else: - r.resp += line + "\n" + if current_func: + current_func.append_body(line + "\n") + else: + r.resp += line + "\n" - if is_need_wait: + if current_func: + if check_args(current_func) is False: + r.resp += current_func.dumps() + + if len(r.send_msgs) > 0 or len(r.calls) > 0: r.state = "waiting" else: r.state = "reponsed" @@ -339,21 +352,20 @@ class Workflow: 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] + async def role_call(self,func_item:FunctionItem,the_role:AIRole): + logger.info(f"{the_role.role_id} call {func_item.name} ") + arguments = func_item.args - func_node : AIFunction = self.workflow_env.get_ai_function(func_name) + func_node : AIFunction = self.workflow_env.get_ai_function(func_item.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) + async def role_post_call(self,func_item:FunctionItem,the_role:AIRole): + logger.info(f"{the_role.role_id} post call {func_item.name} ") + return await self.role_call(func_item,the_role) def _format_msg_by_env_value(self,prompt:AgentPrompt): if self.workflow_env is None: @@ -554,3 +566,7 @@ class Workflow: # the_env.attach_event_handler(k,_env_msg_handler) # break + + + + diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index cf79d4c..8ebc71d 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -427,6 +427,7 @@ 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()) diff --git a/test/workflow_test.py b/test/workflow_test.py index daa4fd1..c985cb5 100644 --- a/test/workflow_test.py +++ b/test/workflow_test.py @@ -3,4 +3,40 @@ def test_workflow(): - pass \ No newline at end of file + pass + + +async def _test_llm_parser(): + test_llm_result = """ +# Foggie with AI Agent +1.已经完成了基础系统改造,只要Foggie能安装docker image就可以实现集成 +2.安装后,用户需要提供OpenAI Token和TG Bot Token,就可以构建自己的私有AI机器人(也可以通过绑定email实现智能邮件客服) + 我们也可以用自己的OpenAI Token给用户用,但这需要设计新的商品。OpenAI Token用起来还是挺贵的 +3.已在发布前夕,目前集成测试的主要问题是对Email和个人文件的AI分析需要比较强的性能。 + +# DMC开源挖矿软件 +正在等待解决 Order Placement Issue + + +# Foggie with AI Agent +1. We have completed the basic system transformation. As long as Foggie can install the docker image, integration can be achieved. +2. After installation, users need to provide an OpenAI Token and TG Bot Token to build their own private AI robot. This can also be accomplished by linking an email to implement an intelligent email customer service. We could use our own OpenAI Token for users, but this would require the design of a new product. Using the OpenAI Token can be quite costly. +3. We are on the eve of launch. The main issue in our integrated testing currently is that AI analysis of emails and personal files requires substantial performance, and the results don't seem so smart. + +#DMC Open Source Mining Software +We are waiting to resolve the Order Placement Issue. + +##/send_msg "xxx xxx" +abcdcdsdf +sfsadfasdf +# Foggie with AI Agent +1. We have completed the basic system transformation. As long as Foggie can install the docker image, integration can be achieved. +2. After installation, users need to provide an OpenAI Token and TG Bot Token to build their own private AI robot. This can also be accomplished by linking an email to implement an intelligent email customer service. We could use our own OpenAI Token for users, but this would require the design of a new product. Using the OpenAI Token can be quite costly. +3. We are on the eve of launch. The main issue in our integrated testing cur + +##/call abcd "xxx xxx" +""" + llm_result = Workflow.prase_llm_result(test_llm_result) + assert len(llm_result.calls) == 1 + assert len(llm_result.send_msgs) == 1 + print(llm_result) \ No newline at end of file From 42fe1ad4f43781ce55281017a6ecc1077bbeb803 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 19 Sep 2023 20:43:01 -0700 Subject: [PATCH 071/177] Modify Agent implement by new system pormpt. --- rootfs/agents/Jarvis/agent.toml | 19 ++++++++++++++++++- rootfs/workflows/math_school/workflow.toml | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 9c2bfb7..20cd7c0 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -3,4 +3,21 @@ fullname = "Jarvis" [[prompt]] role = "system" -content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" +content = """ +你叫Jarvis,是我的超级私人助理。 +你领导一个团队为我服务,团队的成员有: +Tracy Wang,私人英语老师 +David,私人画家 + +*** +你在收到我的信息后,按如下规则处理 +1. 如果你认为团队里有人更适合处理该信息,用下面方法转发消息给他们处理 +``` +##/send_msg 成员名字 +消息内容 +``` +2.你可以访问我的Calender,查看我的日程安排。如果你在处理信息的过程中需要修改我的日程安排,请直接用合适的方法修改。 +3.你可以放我的个人信息库,当你处理我的信息时,如果需要用到我的个人信息,请先用合适的方法进行查询,然后再基于查询的结果进行进一步处理后再讲结果发给我。 +4.你能根据我的需要对系统进行配置,但在修改任何配置前,请先和我确认。 +5.不符合上述规则的信息,请尽力处理。 +""" diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml index 202ac50..b31a2ff 100644 --- a/rootfs/workflows/math_school/workflow.toml +++ b/rootfs/workflows/math_school/workflow.toml @@ -22,7 +22,7 @@ role="system" content="""现在时间是:{now}你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。 你的任何处理结果,都要用下面方式汇报给给教导处主任 ``` -##/sendmsg 教导处主任 +##/send_msg 教导处主任 处理结果 ``` From e6d7b2b00933a8eb2c507af6ddab4928d1146a8b Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 19 Sep 2023 21:36:56 -0700 Subject: [PATCH 072/177] Improve defualt agent Jarvis, fix some bug. --- rootfs/agents/David/agent.toml | 6 ++ rootfs/agents/Jarvis/agent.toml | 2 +- .../agents/{Tracy Wang => Tracy}/agent.toml | 6 +- src/aios_kernel/agent.py | 96 +++++++++++++++++-- src/aios_kernel/agent_message.py | 10 ++ src/aios_kernel/bus.py | 11 +-- src/aios_kernel/open_ai_node.py | 3 +- src/aios_kernel/workflow.py | 13 +-- 8 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 rootfs/agents/David/agent.toml rename rootfs/agents/{Tracy Wang => Tracy}/agent.toml (84%) diff --git a/rootfs/agents/David/agent.toml b/rootfs/agents/David/agent.toml new file mode 100644 index 0000000..3462da9 --- /dev/null +++ b/rootfs/agents/David/agent.toml @@ -0,0 +1,6 @@ +instance_id = "David" +fullname = "David" + +[[prompt]] +role = "system" +content = "你的名字是David,是一个擅长各种风格的画家。你在收到任何绘画请求时,会尝试产生一组英文单词来说明你想画的画" diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 20cd7c0..7e3fd71 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -6,7 +6,7 @@ role = "system" content = """ 你叫Jarvis,是我的超级私人助理。 你领导一个团队为我服务,团队的成员有: -Tracy Wang,私人英语老师 +Tracy,私人英语老师 David,私人画家 *** diff --git a/rootfs/agents/Tracy Wang/agent.toml b/rootfs/agents/Tracy/agent.toml similarity index 84% rename from rootfs/agents/Tracy Wang/agent.toml rename to rootfs/agents/Tracy/agent.toml index c815314..a4a3462 100644 --- a/rootfs/agents/Tracy Wang/agent.toml +++ b/rootfs/agents/Tracy/agent.toml @@ -1,10 +1,10 @@ -instance_id = "Tracy Wang" -fullname = "Tracy Wang" +instance_id = "Tracy" +fullname = "Tracy" [[prompt]] role = "system" content = """ -Your name is Tracy Wang, and you are my advanced private English tutor. +Your name is Tracy, and you are my advanced private English tutor. ## You will assess my English proficiency based on all available information, using a 5-point scale. ## While interacting with me normally, you will adjust my input into more idiomatic American sentences. ## Depending on my level of English, you will annotate potentially incorrect words with phonetic symbols or provide expanded explanations for certain words and phrases. diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 25c7a4a..5bb3027 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -8,7 +8,7 @@ import time import json import shlex -from .agent_message import AgentMsg, AgentMsgStatus, AgentMsgType +from .agent_message import AgentMsg, AgentMsgStatus, AgentMsgType,FunctionItem,LLMResult from .chatsession import AIChatSession from .compute_task import ComputeTaskResult from .ai_function import AIFunction @@ -124,16 +124,82 @@ class AIAgent: return True - def _get_llm_result_type(self,result:str) -> str: - if result == "ignore": - return "ignore" + def _get_llm_result_type(self,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 - return "text" + lines = llm_result_str.splitlines() + is_need_wait = False + + def check_args(func_item:FunctionItem): + match func_name: + case "send_msg":# sendmsg($target_id,$msg_content) + if len(func_args) != 1: + logger.error(f"parse sendmsg failed! {func_call}") + return False + new_msg = AgentMsg() + target_id = func_item.args[0] + msg_content = func_item.body + new_msg.set(self.agent_id,target_id,msg_content) + + r.send_msgs.append(new_msg) + is_need_wait = True + + case "post_msg":# postmsg($target_id,$msg_content) + if len(func_args) != 1: + logger.error(f"parse postmsg failed! {func_call}") + return False + new_msg = AgentMsg() + target_id = func_item.args[0] + msg_content = func_item.body + new_msg.set(self.agent_id,target_id,msg_content) + r.post_msgs.append(new_msg) + + case "call":# call($func_name,$args_str) + r.calls.append(func_item) + is_need_wait = True + return True + case "post_call": # post_call($func_name,$args_str) + r.post_calls.append(func_item) + return True + + current_func : FunctionItem = None + for line in lines: + if line.startswith("##/"): + if current_func: + if check_args(current_func) is False: + r.resp += current_func.dumps() + + func_name,func_args = AgentMsg.parse_function_call(line[3:]) + current_func = FunctionItem(func_name,func_args) + else: + if current_func: + current_func.append_body(line + "\n") + else: + r.resp += line + "\n" + + if current_func: + if check_args(current_func) is False: + r.resp += current_func.dumps() + + if len(r.send_msgs) > 0 or len(r.calls) > 0: + r.state = "waiting" + else: + r.state = "reponsed" + + return r def _get_inner_functions(self) -> dict: if self.owner_env is None: return None + return None + all_inner_function = self.owner_env.get_all_ai_functions() if all_inner_function is None: return None @@ -178,6 +244,7 @@ class AIAgent: async def _process_msg(self,msg:AgentMsg) -> AgentMsg: from .compute_kernel import ComputeKernel + from .bus import AIBus session_topic = msg.get_sender() + "#" + msg.topic chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db) @@ -206,13 +273,26 @@ class AIAgent: #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) + llm_result : LLMResult = self._get_llm_result_type(final_result) is_ignore = False - - match result_type: + result_prompt_str = "" + match llm_result.state: case "ignore": is_ignore = True + case "waiting": + for sendmsg in llm_result.send_msgs: + target = sendmsg.target + sendmsg.topic = msg.topic + sendmsg.prev_msg_id = msg.get_msg_id() + send_resp = await AIBus.get_default_bus().send_message(sendmsg) + if send_resp is not None: + result_prompt_str += f"\n{target} response is :{send_resp.body}" + agent_sesion = AIChatSession.get_session(self.agent_id,f"{sendmsg.target}#{sendmsg.topic}",self.chat_db) + agent_sesion.append(sendmsg) + agent_sesion.append(send_resp) + final_result = llm_result.resp + result_prompt_str + if is_ignore is not True: resp_msg = msg.create_resp_msg(final_result) chatsession.append(msg) diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 07227e9..4c19a7a 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -3,6 +3,8 @@ import uuid import time import re import shlex +from typing import List +from .ai_function import FunctionItem class AgentMsgType(Enum): TYPE_MSG = 0 @@ -129,3 +131,11 @@ class AgentMsg: params = str_list[1:] return func_name, params +class LLMResult: + def __init__(self) -> None: + self.state : str = "ignore" + self.resp : str = "" + self.post_msgs : List[AgentMsg] = [] + self.send_msgs : List[AgentMsg] = [] + self.calls : List[FunctionItem] = [] + self.post_calls : List[FunctionItem] = [] \ No newline at end of file diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py index 1e97733..29fc186 100644 --- a/src/aios_kernel/bus.py +++ b/src/aios_kernel/bus.py @@ -20,13 +20,6 @@ class AIBusHandler: if self.handler is None: return None - 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: @@ -52,6 +45,10 @@ class AIBus: handler = self.handlers.get(target_id) if handler: + if msg.rely_msg_id is not None: + handler.results[msg.rely_msg_id] = msg + return None + handler.queue.put_nowait(msg) self.start_process(target_id) return True diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 8362a5a..c30fc87 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -4,6 +4,7 @@ import os import asyncio from asyncio import Queue import logging +import json from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode @@ -114,7 +115,7 @@ class OpenAI_ComputeNode(ComputeNode): temperature=0.7) # TODO: add temperature to task params? - logger.info(f"openai response: {resp}") + logger.info(f"openai response: {json.dumps(resp, indent=4)}") result = ComputeTaskResult() result.set_from_task(task) diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 3d98dce..48e8a4b 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -8,7 +8,7 @@ from typing import Optional,Tuple,List from abc import ABC, abstractmethod from .environment import Environment,EnvironmentEvent -from .agent_message import AgentMsg,AgentMsgStatus +from .agent_message import AgentMsg,AgentMsgStatus,FunctionItem,LLMResult from .agent import AgentPrompt,AgentMsg from .chatsession import AIChatSession from .role import AIRole,AIRoleGroup @@ -38,14 +38,7 @@ class MessageFilter: return True -class LLMResult: - def __init__(self) -> None: - self.state : str = "ignore" - self.resp : str = "" - self.post_msgs : List[AgentMsg] = [] - self.send_msgs : List[AgentMsg] = [] - self.calls : List[FunctionItem] = [] - self.post_calls : List[FunctionItem] = [] + class Workflow: @@ -306,7 +299,7 @@ class Workflow: if current_func: if check_args(current_func) is False: r.resp += current_func.dumps() - + if len(r.send_msgs) > 0 or len(r.calls) > 0: r.state = "waiting" else: From a75857a6316baa6b9477f546a24bed09b9656622 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 20 Sep 2023 01:33:00 -0700 Subject: [PATCH 073/177] Javirs control calender~ --- rootfs/agents/Jarvis/agent.toml | 1 + src/aios_kernel/agent.py | 16 +- src/aios_kernel/agent_message.py | 4 +- src/aios_kernel/ai_function.py | 19 +- src/aios_kernel/workflow_env.py | 175 ++++++++++++++++++- src/aios_kernel/worksapce_env.py | 2 + src/component/agent_manager/agent_manager.py | 10 ++ src/requirements.txt | 1 + src/service/aios_shell/aios_shell.py | 2 +- 9 files changed, 218 insertions(+), 12 deletions(-) diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 7e3fd71..43fdf73 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -4,6 +4,7 @@ fullname = "Jarvis" [[prompt]] role = "system" content = """ +现在是{now} 你叫Jarvis,是我的超级私人助理。 你领导一个团队为我服务,团队的成员有: Tracy,私人英语老师 diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 5bb3027..3d15a7e 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -198,8 +198,6 @@ class AIAgent: if self.owner_env is None: return None - return None - all_inner_function = self.owner_env.get_all_ai_functions() if all_inner_function is None: return None @@ -219,15 +217,20 @@ class AIAgent: func_name = inenr_func_call_node.get("name") arguments = json.loads(inenr_func_call_node.get("arguments")) + logger.info(f"llm execute inner func:{func_name} ({json.dumps(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) + try: + result_str:str = await func_node.execute(**arguments) + except Exception as e: + result_str = "call error:" + str(e) + logger.error(f"llm execute inner func:{func_name} error:{e}") + - 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) @@ -242,6 +245,9 @@ class AIAgent: else: return task_result.result_str + async def _get_agent_prompt(self) -> AgentPrompt: + return self.prompt + async def _process_msg(self,msg:AgentMsg) -> AgentMsg: from .compute_kernel import ComputeKernel from .bus import AIBus @@ -255,7 +261,7 @@ class AIAgent: return None prompt = AgentPrompt() - prompt.append(self.prompt) + prompt.append(await self._get_agent_prompt()) # prompt.append(self._get_knowlege_prompt(the_role.get_name())) prompt.append(await self._get_prompt_from_session(chatsession)) # chat context diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py index 4c19a7a..49daf3a 100644 --- a/src/aios_kernel/agent_message.py +++ b/src/aios_kernel/agent_message.py @@ -35,7 +35,7 @@ class AgentMsgStatus(Enum): class AgentMsg: def __init__(self,msg_type=AgentMsgType.TYPE_MSG) -> None: - self.msg_id = "" + self.msg_id = "msg#" + uuid.uuid4().hex self.msg_type:AgentMsgType = msg_type self.prev_msg_id:str = None @@ -89,7 +89,6 @@ class AgentMsg: def create_resp_msg(self,resp_body): resp_msg = AgentMsg() - resp_msg.msg_id = "msg#" + uuid.uuid4().hex resp_msg.create_time = time.time() resp_msg.rely_msg_id = self.msg_id @@ -101,7 +100,6 @@ class AgentMsg: return resp_msg def set(self,sender:str,target:str,body:str,topic:str=None) -> None: - self.msg_id = "msg#" + uuid.uuid4().hex self.sender = sender self.target = target self.body = body diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py index ae5df52..bca41d2 100644 --- a/src/aios_kernel/ai_function.py +++ b/src/aios_kernel/ai_function.py @@ -1,6 +1,13 @@ from abc import ABC, abstractmethod from typing import Dict,Coroutine,Callable +class ParameterDefine: + def __init__(self) -> None: + self.name = None + self.type = None + self.description = None + + class AIFunction: def __init__(self) -> None: self.description : str = None @@ -107,9 +114,19 @@ class SimpleAIFunction(AIFunction): def get_parameters(self) -> Dict: if self.parameters is not None: - return self.parameters + result = {} + result["type"] = "object" + parm_defines = {} + for parm,desc in self.parameters.items(): + parm_item = {} + parm_item["type"] = "string" + parm_item["description"] = desc + parm_defines[parm] = parm_item + result["properties"] = parm_defines + return result return {"type": "object", "properties": {}} + async def execute(self,**kwargs) -> str: if self.func_handler is None: return "error: function not implemented" diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 4ef033b..6223ae3 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -1,6 +1,7 @@ from datetime import datetime import asyncio +import json 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 @@ -8,6 +9,9 @@ import logging from typing import Optional from .environment import Environment,EnvironmentEvent from .ai_function import SimpleAIFunction +from .storage import AIStorage + +import aiosqlite logger = logging.getLogger(__name__) @@ -25,20 +29,187 @@ class CalenderEvent(EnvironmentEvent): class CalenderEnvironment(Environment): def __init__(self, env_id: str) -> None: super().__init__(env_id) + self.db_file = AIStorage.get_instance().get_myai_dir() / "calender.db" self.is_run = False self.add_ai_function(SimpleAIFunction("get_time", "get current time", self._get_now)) + #self.add_ai_function(SimpleAIFunction("serach_events", + # "search events in calender", + # self._search_events)) + + get_param = { + "start_time": "start time (UTC) of event", + "end_time": "end time (UTC) of event" + } + self.add_ai_function(SimpleAIFunction("get_events", + "get events in calender by time range", + self._get_events_by_time_range,get_param)) + + add_param = { + "title": "title of event", + "start_time": "start time (UTC) of event", + "end_time": "end time (UTC) of event", + "participants": "participants of event", + "location": "location of event", + "details": "details of event" + } + self.add_ai_function(SimpleAIFunction("add_event", + "add event to calender", + self._add_event,add_param)) + + delete_param = { + "event_id": "id of event" + } + self.add_ai_function(SimpleAIFunction("delete_event", + "delete event from calender", + self._delete_event,delete_param)) + + update_param = { + "event_id": "id of event", + "new_title": "new title of event", + "new_participants": "new participants of event", + "new_location": "new location of event", + "new_details": "new details of event", + "start_time": "new start time (UTC) of event", + "end_time": "new end time (UTC) of event" + } + self.add_ai_function(SimpleAIFunction("update_event", + "update event in calender", + self._update_event,update_param)) + + #self.add_ai_function(SimpleAIFunction("user_confirm", + # "user confirm", + # self._user_confirm)) + + async def init_db(self): + async with aiosqlite.connect(self.db_file) as db: + await db.execute(""" + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + start_time DATETIME, + end_time DATETIME, + participants TEXT, + location TEXT, + details TEXT + ); + """) + await db.commit() + + async def _add_event(self,title, start_time, end_time, participants=None, location=None, details=None): + async with aiosqlite.connect(self.db_file) as db: + await db.execute(""" + INSERT INTO events (title, start_time, end_time, participants, location, details) + VALUES (?, ?, ?, ?, ?, ?); + """, (title, start_time, end_time, participants, location, details)) + await db.commit() + return "Add event ok" + + async def _search_events(self,query): + async with aiosqlite.connect(self.db_file) as db: + cursor = await db.execute(""" + SELECT id,title, start_time, end_time, participants, location, details FROM events + WHERE title LIKE ? OR participants LIKE ? OR location LIKE ? OR details LIKE ?; + """, (f"%{query}%", f"%{query}%", f"%{query}%", f"%{query}%")) + rows = await cursor.fetchall() + + result = {} + for row in rows: + _event = {} + _event["title"] = row[1] + _event["start_time"] = row[2] + _event["end_time"] = row[3] + _event["participants"] = row[4] + _event["location"] = row[5] + _event["details"] = row[6] + result[row[0]] = _event + return json.dumps(result, indent=4, sort_keys=True) + + async def _get_events_by_time_range(self,start_time, end_time): + async with aiosqlite.connect(self.db_file) as db: + cursor = await db.execute(""" + SELECT id,title, start_time, end_time, participants, location, details FROM events + WHERE start_time >= ? AND end_time <= ?; + """, (start_time, end_time)) + rows = await cursor.fetchall() + + result = {} + for row in rows: + _event = {} + _event["title"] = row[1] + _event["start_time"] = row[2] + _event["end_time"] = row[3] + _event["participants"] = row[4] + _event["location"] = row[5] + _event["details"] = row[6] + result[row[0]] = _event + return json.dumps(result, indent=4, sort_keys=True) + + async def _update_event(self,event_id, new_title=None, new_participants=None, new_location=None, new_details=None ,start_time=None, end_time=None): + fields_to_update = [] + values = [] + + if new_title is not None: + fields_to_update.append("title = ?") + values.append(new_title) + + if new_participants is not None: + fields_to_update.append("participants = ?") + values.append(new_participants) + + if new_location is not None: + fields_to_update.append("location = ?") + values.append(new_location) + + if new_details is not None: + fields_to_update.append("details = ?") + values.append(new_details) + + if start_time is not None: + fields_to_update.append("start_time = ?") + values.append(start_time) + + if end_time is not None: + fields_to_update.append("end_time = ?") + values.append(end_time) + + if not fields_to_update: + return "No fields to update." + + sql_update_query = f""" + UPDATE events + SET {', '.join(fields_to_update)} + WHERE id = ?; + """ + + values.append(event_id) + + async with aiosqlite.connect(self.db_file) as db: + await db.execute(sql_update_query, values) + await db.commit() + return "update ok" + + async def _delete_event(self,event_id): + async with aiosqlite.connect(self.db_file) as db: + await db.execute(""" + DELETE FROM events + WHERE id = ?; + """, (event_id,)) + await db.commit() + return "Delete event ok" + def _do_get_value(self,key:str) -> Optional[str]: return None - def start(self) -> None: + async def start(self) -> None: if self.is_run: return self.is_run = True - + await self.init_db() + self.register_get_handler("now",self.get_now) async def timer_loop(): while True: diff --git a/src/aios_kernel/worksapce_env.py b/src/aios_kernel/worksapce_env.py index e69de29..d8611f2 100644 --- a/src/aios_kernel/worksapce_env.py +++ b/src/aios_kernel/worksapce_env.py @@ -0,0 +1,2 @@ +# this env is designed for workflow owner filesystem, support file/directory operations + diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index 5d20cf6..92bad31 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -1,6 +1,7 @@ import logging import toml +from typing import Any, Callable, Dict, List, Optional, Union from aios_kernel import AIAgent,AIAgentTemplete,AIStorage from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask @@ -17,6 +18,12 @@ class AgentManager: cls._instance = AgentManager() return cls._instance + def __init__(self) -> None: + self.agent_templete_env : PackageEnv = None + self.agent_env : PackageEnv = None + self.db_path : str = None + self.loaded_agent_instance : Dict[str,AIAgent] = None + def initial(self) -> None: system_app_dir = AIStorage.get_instance().get_system_app_dir() user_data_dir = AIStorage.get_instance().get_myai_dir() @@ -27,6 +34,9 @@ class AgentManager: self.loaded_agent_instance = {} if self.agent_templete_env is None: raise Exception("agent_manager initial failed") + + async def scan_all_agent(self)->None: + pass async def get(self,agent_id:str) -> AIAgent: diff --git a/src/requirements.txt b/src/requirements.txt index b9e630e..574b9ea 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -19,3 +19,4 @@ base58 google-cloud-texttospeech openai Pillow +aiosqlite diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 8ebc71d..6f2daff 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -81,7 +81,7 @@ class AIOS_Shell: async def initial(self) -> bool: cal_env = CalenderEnvironment("calender") - cal_env.start() + await cal_env.start() Environment.set_env_by_id("calender",cal_env) AgentManager.get_instance().initial() From d5073cc21bed533f097def4223835c260cd84007 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 20 Sep 2023 02:23:46 -0700 Subject: [PATCH 074/177] Jarvis use gpt-3.5 --- rootfs/agents/Jarvis/agent.toml | 1 + src/aios_kernel/agent.py | 9 +++++++++ src/aios_kernel/open_ai_node.py | 6 +++++- src/service/aios_shell/aios_shell.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 43fdf73..e6da761 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -1,5 +1,6 @@ instance_id = "Jarvis" fullname = "Jarvis" +llm_model_name = "gpt-3.5-turbo-16k-0613" [[prompt]] role = "system" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 3d15a7e..d673371 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -248,6 +248,14 @@ class AIAgent: async def _get_agent_prompt(self) -> AgentPrompt: return self.prompt + def _format_msg_by_env_value(self,prompt:AgentPrompt): + if self.owner_env is None: + return + + for msg in prompt.messages: + old_content = msg.get("content") + msg["content"] = old_content.format_map(self.owner_env) + async def _process_msg(self,msg:AgentMsg) -> AgentMsg: from .compute_kernel import ComputeKernel from .bus import AIBus @@ -269,6 +277,7 @@ class AIAgent: msg_prompt.messages = [{"role":"user","content":msg.body}] prompt.append(msg_prompt) + self._format_msg_by_env_value(prompt) 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) diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index c30fc87..857f2c7 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -167,8 +167,12 @@ class OpenAI_ComputeNode(ComputeNode): def is_support(self, task: ComputeTask) -> bool: if task.task_type == ComputeTaskType.LLM_COMPLETION: - if not task.params["model_name"] or task.params["model_name"] == "gpt-4-0613": + if not task.params["model_name"]: return True + model_name : str = task.params["model_name"] + if model_name.startswith("gpt-"): + return True + if task.task_type == ComputeTaskType.TEXT_EMBEDDING: if task.params["model_name"] == "text-embedding-ada-002": return True diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 6f2daff..13a5ee8 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -409,7 +409,7 @@ async def main(): await asyncio.sleep(0.2) while True: - user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$",completer=completer,style=shell_style) + user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$ ",completer=completer,style=shell_style) if len(user_input) <= 1: continue From c9af0cfc0df9732bfde376ca9823e8787ee59e6e Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Tue, 19 Sep 2023 08:29:55 +0000 Subject: [PATCH 075/177] add proxy in config --- src/aios_kernel/storage.py | 14 ++++++++++-- src/requirements.txt | 1 + src/service/aios_shell/aios_shell.py | 22 ++++++++++++------- src/service/aios_shell/proxy.py | 32 ++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 src/service/aios_shell/proxy.py diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index e982645..dfebefe 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -21,7 +21,15 @@ class UserConfigItem: self.desc = desc self.value = None self.user_set = False - + + def clone(self): + new_config_item = UserConfigItem() + new_config_item.default_value = self.default_value + new_config_item.is_optional = self.is_optional + new_config_item.desc = self.desc + new_config_item.item_type = self.item_type + new_config_item.value = self.value + return new_config_item class UserConfig: def __init__(self) -> None: @@ -89,7 +97,9 @@ class UserConfig: raise Exception("user config key %s not exist",key) if config_item.value is None: - return config_item.default_value + default_config_item = config_item.clone() + default_config_item.value = config_item.default_value + return default_config_item return config_item.value diff --git a/src/requirements.txt b/src/requirements.txt index 574b9ea..8427ade 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -20,3 +20,4 @@ google-cloud-texttospeech openai Pillow aiosqlite +PySocks==1.7.1 diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 13a5ee8..de5675e 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -23,7 +23,7 @@ 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 - +import proxy sys.path.append(directory + '/../../component/') from agent_manager import AgentManager @@ -55,6 +55,7 @@ class AIOS_Shell: openai_node.declare_user_config() user_config.add_user_config("shell.current","last opened target and topic",True,"default@Jarvis") + proxy.declare_user_config() async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: @@ -95,7 +96,7 @@ class AIOS_Shell: llama_ai_node = LocalLlama_ComputeNode() await llama_ai_node.start() - #ComputeKernel.get_instance().add_compute_node(llama_ai_node) + # ComputeKernel.get_instance().add_compute_node(llama_ai_node) await ComputeKernel.get_instance().start() @@ -192,13 +193,16 @@ class AIOS_Shell: show_text = FormattedText([("class:title", f"set config failed!")]) if len(args) == 1: key = args[0] - config_item = AIStorage.get_instance().get_user_config().get_config_item(key) - old_value = AIStorage.get_instance().get_user_config().get_value(key) + config_item = AIStorage.get_instance().get_user_config().get_user_config(key) + + old_value = None if config_item is not None: - value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) - AIStorage.get_instance().get_user_config().set_value(key,value) - await AIStorage.get_instance().get_user_config().save_to_user_config() - show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) + old_value = config_item.value + + value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) + AIStorage.get_instance().get_user_config().set_user_config(key,value) + await AIStorage.get_instance().get_user_config().save_value_to_user_config() + show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) return show_text case 'connect': @@ -390,6 +394,8 @@ async def main(): if is_daemon: return await main_daemon_loop(shell) + proxy.apply_storage() + #TODO: read last input config completer = WordCompleter(['/send $target $msg $topic', '/open $target $topic', diff --git a/src/service/aios_shell/proxy.py b/src/service/aios_shell/proxy.py new file mode 100644 index 0000000..7f8a352 --- /dev/null +++ b/src/service/aios_shell/proxy.py @@ -0,0 +1,32 @@ + +import sys +import os +import logging +import socket +import socks + +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../../') + +from aios_kernel import AIStorage + +def apply_storage(): + proxy_cfg = AIStorage.get_instance().get_user_config().get_user_config("proxy") + if proxy_cfg is None: + return + + host_url = proxy_cfg.value + if host_url is not None: + (proxy_type, host) = host_url.split("@") + match proxy_type: + case "socks5": + (host, port) = host.split(":") + socks.set_default_proxy(socks.SOCKS5, host, int(port)) + socket.socket = socks.socksocket + print("proxy {host_url} will be used.") + case _: + print("the proxy type ({proxy_type}) has not support.") + +def declare_user_config(): + user_config = AIStorage.get_instance().get_user_config() + user_config.add_user_config("proxy", "set your proxy service as 'proxy_type@host:port', 'proxy_type' = 'socks5'", True, None) From c5184154b5a568c50746ff7f4dbfa043c30a52ea Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Wed, 20 Sep 2023 07:46:42 +0000 Subject: [PATCH 076/177] proxy with authorize --- src/aios_kernel/local_llama_compute_node.py | 2 +- src/service/aios_shell/proxy.py | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/aios_kernel/local_llama_compute_node.py b/src/aios_kernel/local_llama_compute_node.py index 5cba82e..7e50661 100644 --- a/src/aios_kernel/local_llama_compute_node.py +++ b/src/aios_kernel/local_llama_compute_node.py @@ -47,7 +47,7 @@ class LocalLlama_ComputeNode(Queue_ComputeNode): "state": ComputeTaskState.ERROR, "error": { "code": response.status_code, - "message": "http request failed: " + response.status_code + "message": "http request failed: " + str(response.status_code) } } else: diff --git a/src/service/aios_shell/proxy.py b/src/service/aios_shell/proxy.py index 7f8a352..ec07eb3 100644 --- a/src/service/aios_shell/proxy.py +++ b/src/service/aios_shell/proxy.py @@ -16,17 +16,25 @@ def apply_storage(): return host_url = proxy_cfg.value - if host_url is not None: - (proxy_type, host) = host_url.split("@") + if host_url is not None and len(host_url) > 3: + url_fields = host_url.split("@") + proxy_type, host, username, password = url_fields[0], None, None, None + if len(url_fields) > 1: + host = url_fields[1] + if len(url_fields) > 2: + username = url_fields[2] + if len(url_fields) > 3: + password = url_fields[3] + match proxy_type: case "socks5": (host, port) = host.split(":") - socks.set_default_proxy(socks.SOCKS5, host, int(port)) + socks.set_default_proxy(socks.SOCKS5, host, int(port), username = username, password = password) socket.socket = socks.socksocket - print("proxy {host_url} will be used.") + print(f"proxy {host_url} will be used.") case _: - print("the proxy type ({proxy_type}) has not support.") + print(f"the proxy type ({proxy_type}) has not support.") def declare_user_config(): user_config = AIStorage.get_instance().get_user_config() - user_config.add_user_config("proxy", "set your proxy service as 'proxy_type@host:port', 'proxy_type' = 'socks5'", True, None) + user_config.add_user_config("proxy", "set your proxy service as 'proxy_type@host:port@username@password', 'proxy_type' = 'socks5'", True, None) From 0de5534723b15a34e6c9f6a9107a2f55441f3946 Mon Sep 17 00:00:00 2001 From: zhangzhen Date: Wed, 20 Sep 2023 16:17:06 +0800 Subject: [PATCH 077/177] adapt to new value name --- src/service/aios_shell/aios_shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index de5675e..4d054fb 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -193,15 +193,15 @@ class AIOS_Shell: show_text = FormattedText([("class:title", f"set config failed!")]) if len(args) == 1: key = args[0] - config_item = AIStorage.get_instance().get_user_config().get_user_config(key) + config_item = AIStorage.get_instance().get_user_config().get_config_item(key) old_value = None if config_item is not None: old_value = config_item.value value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) - AIStorage.get_instance().get_user_config().set_user_config(key,value) - await AIStorage.get_instance().get_user_config().save_value_to_user_config() + AIStorage.get_instance().get_user_config().set_value(key,value) + await AIStorage.get_instance().get_user_config().save_to_user_config() show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) return show_text From ee3f9ebceaf8b35941b41bf5111409c9989f1fdd Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Wed, 20 Sep 2023 08:45:53 +0000 Subject: [PATCH 078/177] adapt to new value name --- aios_shell_history.txt | 21 +++++++++++++++++++++ src/aios_kernel/storage.py | 4 +--- src/service/aios_shell/aios_shell.py | 12 +++++------- src/service/aios_shell/proxy.py | 2 +- 4 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 aios_shell_history.txt diff --git a/aios_shell_history.txt b/aios_shell_history.txt new file mode 100644 index 0000000..830345a --- /dev/null +++ b/aios_shell_history.txt @@ -0,0 +1,21 @@ + +# 2023-09-20 08:41:50.636730 ++/set_config proxy + +# 2023-09-20 08:42:18.936143 ++/open math_school 小学老师 + +# 2023-09-20 08:42:23.884698 ++3+5=8 + +# 2023-09-20 08:43:13.219129 ++/set_config proxy + +# 2023-09-20 08:43:23.430306 ++socks5@192.168.100.91:1080@zhangzhen@123456 + +# 2023-09-20 08:43:35.319913 ++/open math_school 小学老师 + +# 2023-09-20 08:43:41.557234 ++3+5=8 diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index dfebefe..5119167 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -97,9 +97,7 @@ class UserConfig: raise Exception("user config key %s not exist",key) if config_item.value is None: - default_config_item = config_item.clone() - default_config_item.value = config_item.default_value - return default_config_item + return config_item.default_value return config_item.value diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 4d054fb..41f24a0 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -194,15 +194,13 @@ class AIOS_Shell: if len(args) == 1: key = args[0] config_item = AIStorage.get_instance().get_user_config().get_config_item(key) + old_value = AIStorage.get_instance().get_user_config().get_value(key) - old_value = None if config_item is not None: - old_value = config_item.value - - value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) - AIStorage.get_instance().get_user_config().set_value(key,value) - await AIStorage.get_instance().get_user_config().save_to_user_config() - show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) + value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) + AIStorage.get_instance().get_user_config().set_value(key,value) + await AIStorage.get_instance().get_user_config().save_to_user_config() + show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) return show_text case 'connect': diff --git a/src/service/aios_shell/proxy.py b/src/service/aios_shell/proxy.py index ec07eb3..caf006d 100644 --- a/src/service/aios_shell/proxy.py +++ b/src/service/aios_shell/proxy.py @@ -11,7 +11,7 @@ sys.path.append(directory + '/../../') from aios_kernel import AIStorage def apply_storage(): - proxy_cfg = AIStorage.get_instance().get_user_config().get_user_config("proxy") + proxy_cfg = AIStorage.get_instance().get_user_config().get_config_item("proxy") if proxy_cfg is None: return From fdbb0fcc475a1b528e2caf659ab6b55a566ae2ad Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Wed, 20 Sep 2023 08:52:15 +0000 Subject: [PATCH 079/177] remove local files --- .gitignore | 1 + aios_shell_history.txt | 21 --------------------- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 aios_shell_history.txt diff --git a/.gitignore b/.gitignore index 99b413e..f616e40 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ rootfs/data venv aios_shell.log history.txt +aios_shell_history.txt math_school_env.db workflows.db diff --git a/aios_shell_history.txt b/aios_shell_history.txt deleted file mode 100644 index 830345a..0000000 --- a/aios_shell_history.txt +++ /dev/null @@ -1,21 +0,0 @@ - -# 2023-09-20 08:41:50.636730 -+/set_config proxy - -# 2023-09-20 08:42:18.936143 -+/open math_school 小学老师 - -# 2023-09-20 08:42:23.884698 -+3+5=8 - -# 2023-09-20 08:43:13.219129 -+/set_config proxy - -# 2023-09-20 08:43:23.430306 -+socks5@192.168.100.91:1080@zhangzhen@123456 - -# 2023-09-20 08:43:35.319913 -+/open math_school 小学老师 - -# 2023-09-20 08:43:41.557234 -+3+5=8 From 1971a9399d00995da542b8908e9bf220d2750cb8 Mon Sep 17 00:00:00 2001 From: Song Date: Wed, 20 Sep 2023 22:31:33 +0800 Subject: [PATCH 080/177] Add a local sd node --- src/aios_kernel/local_stability_node.py | 163 ++++++++++++++++++++++++ src/aios_kernel/stability_node.py | 39 ++++-- test/test_local_sd_node.py | 59 +++++++++ 3 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 src/aios_kernel/local_stability_node.py create mode 100644 test/test_local_sd_node.py diff --git a/src/aios_kernel/local_stability_node.py b/src/aios_kernel/local_stability_node.py new file mode 100644 index 0000000..992e00f --- /dev/null +++ b/src/aios_kernel/local_stability_node.py @@ -0,0 +1,163 @@ +import os +import io +import asyncio +from asyncio import Queue +import logging +import base64 +from PIL import Image +import requests + +from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType +from .compute_node import ComputeNode +from .storage import AIStorage, UserConfig + +logger = logging.getLogger(__name__) + + +class Local_Stability_ComputeNode(ComputeNode): + _instance = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = Local_Stability_ComputeNode() + return cls._instance + + @classmethod + def declare_user_config(cls): + user_config = AIStorage.get_instance().get_user_config() + user_config.add_user_config( + "local_stability_url", "local stability url", False, None) + user_config.add_user_config( + "text2img_output_dir", "output dir", True, "./") + user_config.add_user_config( + "text2img_default_model", "text2img default model", True, "v1-5-pruned-emaonly") + + def __init__(self) -> None: + super().__init__() + + self.is_start = False + self.node_id = "local_stability_node" + self.url = None + self.default_model = None + self.output_dir = None + + self.task_queue = Queue() + + async def initial(self): + if os.getenv("LOCAL_STABILITY_URL") is not None: + self.url = os.getenv("LOCAL_STABILITY_URL") + else: + self.url = AIStorage.get_instance( + ).get_user_config().get_value("local_stability_url") + + if os.getenv("TEXT2IMG_OUTPUT_DIR") is not None: + self.output_dir = os.getenv("TEXT2IMG_OUTPUT_DIR") + else: + self.output_dir = AIStorage.get_instance( + ).get_user_config().get_value("text2img_output_dir") + + if os.getenv("TEXT2IMG_DEFAULT_MODEL") is not None: + self.default_model = os.getenv("TEXT2IMG_DEFAULT_MODEL") + else: + self.default_model = AIStorage.get_instance( + ).get_user_config().get_value("text2img_default_model") + + if self.url is None: + logger.error("local stability url is None!") + return False + + if self.default_model is None: + logger.error("local stability default model is None!") + return False + + if self.output_dir is None: + self.output_dir = "./" + + self.start() + + return True + + 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 = task.params["model_name"] + prompts = task.params["prompts"] + + logging.info(f"call local stability {model_name} prompts: {prompts}") + + if model_name is not None: + payload = { + "sd_model_checkpoint": model_name, + } + # {'error': 'RuntimeError', 'detail': '', 'body': '', 'errors': "model 'xxx' not found"} + response = requests.post( + url=f'{self.url}/sdapi/v1/options', json=payload) + if response.status_code != 200: + task.state = ComputeTaskState.ERROR + logger.error( + f"set local stability model failed. err:{response.json()['errors']}") + return None + + payload = { + "prompt": prompts, + "steps": 20 + } + + response = requests.post( + url=f'{self.url}/sdapi/v1/txt2img', json=payload) + r = response.json() + + print(len(r['images'])) + for i in r['images']: + image = Image.open(io.BytesIO( + base64.b64decode(i.split(",", 1)[0]))) + file_name = os.path.join(self.output_dir, task.task_id + ".png") + image.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("local_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 diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py index 523ca17..b36e86c 100644 --- a/src/aios_kernel/stability_node.py +++ b/src/aios_kernel/stability_node.py @@ -10,49 +10,64 @@ 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 +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") + user_config.add_user_config( + "stability_api_key", "stability api key", False, None) + user_config.add_user_config( + "stability_model", "stability model name", True, "stable-diffusion-512-v2-1") - def __init__(self) -> None: + def __init__(self): super().__init__() self.is_start = False self.node_id = "stability_node" self.api_key = "" - self.engine = "stable-diffusion-512-v2-1" + self.model = "" self.task_queue = Queue() if os.getenv("STABILITY_API_KEY") is not None: self.api_key = os.getenv("STABILITY_API_KEY") + else: + self.api_key = AIStorage.get_instance( + ).get_user_config().get_value("stability_api_key") + + if self.api_key is None: + logger.error("stability api key is None!") + return False # 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") + if os.getenv("STABILITY_MODEL") is not None: + self.model = os.getenv("STABILITY_MODEL") + else: + self.model = AIStorage.get_instance().get_user_config().get_value("stability_model") self.client = client.StabilityInference( key=self.api_key, verbose=True, # Print debug messages. - engine=self.engine, + engine=self.model, ) self.start() + return True + 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) @@ -65,7 +80,7 @@ class Stability_ComputeNode(ComputeNode): # model_name && max_token_size not used here prompts = task.params["prompts"] - logging.info(f"call stability {self.engine} prompts: {prompts}") + logging.info(f"call stability {self.model} prompts: {prompts}") answers = self.client.generate( prompt=prompts, # If a seed is provided, the resulting generated image will be deterministic. @@ -90,8 +105,9 @@ class Stability_ComputeNode(ComputeNode): for resp in answers: for artifact in resp.artifacts: - logger.info(f"artifact:{artifact.id},{artifact.type},{artifact.finish_reason}") - + 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: @@ -113,6 +129,7 @@ class Stability_ComputeNode(ComputeNode): if self.is_start: return self.is_start = True + async def _run_task_loop(): while True: logger.info("stability_node is waiting for task...") diff --git a/test/test_local_sd_node.py b/test/test_local_sd_node.py new file mode 100644 index 0000000..62659d1 --- /dev/null +++ b/test/test_local_sd_node.py @@ -0,0 +1,59 @@ +import os +import time +import uuid +import io +import asyncio +import sys +import logging +import pytest +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../src') +from aios_kernel.local_stability_node import Local_Stability_ComputeNode +from aios_kernel.compute_task import ComputeTaskType, ComputeTask, ComputeTaskState + +#to launch a local stability node, please check: +#https://github.com/glen0125/stable-diffusion-webui-docker + +os.environ["LOCAL_STABILITY_URL"] = "http://aigc:7866" +os.environ["TEXT2IMG_DEFAULT_MODEL"] = "v1-5-pruned-emaonly" +os.environ["TEXT2IMG_OUTPUT_DIR"] = "./" + +@pytest.mark.asyncio +async def test_local_sd_node(propmt, model): + node = Local_Stability_ComputeNode.get_instance() + if await node.initial() is not True: + print("node initial failed!") + return + + task = ComputeTask() + task.task_type = ComputeTaskType.TEXT_2_IMAGE + task.create_time = time.time() + task.task_id = uuid.uuid4().hex + task.params['model_name'] = model + task.params['prompts'] = propmt + await node.push_task(task) + + while True: + if task.state == ComputeTaskState.DONE: + local_file = task.result.result + print("local file is: ", local_file) + break + await asyncio.sleep(1) + + # result = node._run_task(task) + # print("result is: ", result) + + # if result.result is not None: + # local_file = result.result + # print("local file is: ", local_file) + +if __name__ == "__main__": + arg_len = len(os.sys.argv) + prompt = "a beautiful sunset" + model = "v1-5-pruned-emaonly" + if arg_len >= 2: + prompt = os.sys.argv[1] + if arg_len == 3: + model = os.sys.argv[2] + + asyncio.run(test_local_sd_node(prompt, model)) From bde91771a8728f2f0881f380fea5c888a582e62d Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 20 Sep 2023 14:45:54 -0700 Subject: [PATCH 081/177] Generator prompt from session histroy base on token limit. --- rootfs/agents/Jarvis/agent.toml | 1 + src/aios_kernel/agent.py | 74 +++++++++++++++++++++++++++------ src/aios_kernel/compute_task.py | 4 +- src/aios_kernel/open_ai_node.py | 9 +++- src/aios_kernel/workflow_env.py | 8 +++- 5 files changed, 79 insertions(+), 17 deletions(-) diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index e6da761..3eb81ec 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -1,6 +1,7 @@ instance_id = "Jarvis" fullname = "Jarvis" llm_model_name = "gpt-3.5-turbo-16k-0613" +max_token_size = 16000 [[prompt]] role = "system" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index d673371..d10eb20 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -19,27 +19,57 @@ logger = logging.getLogger(__name__) class AgentPrompt: def __init__(self) -> None: self.messages = [] + self.system_message = None def as_str(self)->str: result_str = "" + if self.system_message: + result_str += self.system_message.get("role") + ":" + self.system_message.get("content") + "\n" if self.messages: for msg in self.messages: result_str += msg.get("role") + ":" + msg.get("content") + "\n" return result_str + def to_message_list(self): + result = [] + if self.system_message: + result.append(self.system_message) + result.extend(self.messages) + return result + def append(self,prompt): if prompt is None: return + if prompt.system_message is not None: + if self.system_message is None: + self.system_message = prompt.system_message + else: + self.system_message["content"] += prompt.system_message.get("content") + self.messages.extend(prompt.messages) + def get_prompt_token_len(self): + result = 0 + + if self.system_message: + result += len(self.system_message.get("content")) + for msg in self.messages: + result += len(msg.get("content")) + + return result + def load_from_config(self,config:list) -> bool: if isinstance(config,list) is not True: logger.error("prompt is not list!") return False - - self.messages = config + self.messages = [] + for msg in config: + if msg.get("role") == "system": + self.system_message = msg + else: + self.messages.append(msg) return True @@ -203,16 +233,18 @@ class AIAgent: return None result_func = [] + result_len = 0 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_len += len(json.dumps(this_func)) / 4 result_func.append(this_func) - return result_func + return result_func,result_len - async def _execute_func(self,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg) -> str: + async def _execute_func(self,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg,stack_limit = 5) -> str: from .compute_kernel import ComputeKernel func_name = inenr_func_call_node.get("name") @@ -231,7 +263,7 @@ class AIAgent: logger.error(f"llm execute inner func:{func_name} error:{e}") - inner_functions = self._get_inner_functions() + inner_functions,inner_function_len = 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) @@ -239,9 +271,11 @@ class AIAgent: 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 stack_limit > 0: + 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) + return await self._execute_func(inner_func_call_node,prompt,org_msg,stack_limit-1) else: return task_result.result_str @@ -270,16 +304,20 @@ class AIAgent: prompt = AgentPrompt() prompt.append(await self._get_agent_prompt()) + inner_functions,function_token_len = self._get_inner_functions() # prompt.append(self._get_knowlege_prompt(the_role.get_name())) - prompt.append(await self._get_prompt_from_session(chatsession)) # chat context + system_prompt_len = prompt.get_prompt_token_len() + input_len = len(msg.body) + + history_prmpt,history_token_len = await self._get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len) + prompt.append(history_prmpt) # chat context msg_prompt = AgentPrompt() msg_prompt.messages = [{"role":"user","content":msg.body}] prompt.append(msg_prompt) self._format_msg_by_env_value(prompt) - inner_functions = self._get_inner_functions() - + logger.info(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len}") 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 @@ -332,14 +370,26 @@ class AIAgent: def get_max_token_size(self) -> int: return self.max_token_size - async def _get_prompt_from_session(self,chatsession:AIChatSession,is_groupchat=False) -> AgentPrompt: + async def _get_prompt_from_session(self,chatsession:AIChatSession,system_token_len,input_token_len,is_groupchat=False) -> AgentPrompt: # TODO: get prompt from group chat is different from single chat + history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len messages = chatsession.read_history() # read + result_token_len = 0 result_prompt = AgentPrompt() + read_history_msg = 0 for msg in reversed(messages): + read_history_msg += 1 if msg.sender == self.agent_id: result_prompt.messages.append({"role":"assistant","content":msg.body}) + else: result_prompt.messages.append({"role":"user","content":msg.body}) - return result_prompt + + history_len -= len(msg.body) + result_token_len += len(msg.body) + if history_len < 0: + logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.") + break + + return result_prompt,result_token_len diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index 1433379..c6053db 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -41,7 +41,7 @@ class ComputeTask: self.create_time = time.time() self.task_id = uuid.uuid4().hex self.callchain_id = callchain_id - self.params["prompts"] = prompts.messages + self.params["prompts"] = prompts.to_message_list() if model_name is not None: self.params["model_name"] = model_name else: @@ -78,7 +78,7 @@ class ComputeTaskResult: 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.result_refers: dict = {} self.pading_data: bytearray = None def set_from_task(self, task: ComputeTask): diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 857f2c7..475fa22 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -99,19 +99,21 @@ class OpenAI_ComputeNode(ComputeNode): llm_inner_functions = task.params.get("inner_functions") if max_token_size is None: max_token_size = 4000 + + result_token = int(max_token_size * 0.4) logger.info(f"call openai {mode_name} prompts: {prompts}") if llm_inner_functions is None: resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, - max_tokens=max_token_size, + max_tokens=result_token, temperature=0.7) else: resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, functions=llm_inner_functions, - max_tokens=max_token_size, + max_tokens=result_token, temperature=0.7) # TODO: add temperature to task params? @@ -121,6 +123,7 @@ class OpenAI_ComputeNode(ComputeNode): result.set_from_task(task) status_code = resp["choices"][0]["finish_reason"] + token_usage = resp.get("usage") match status_code: case "function_call": task.state = ComputeTaskState.DONE @@ -134,6 +137,8 @@ class OpenAI_ComputeNode(ComputeNode): result.worker_id = self.node_id result.result_str = resp["choices"][0]["message"]["content"] result.result_message = resp["choices"][0]["message"] + if token_usage: + result.result_refers["token_usage"] = token_usage return result case _: task.state = ComputeTaskState.ERROR diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 6223ae3..120b3fb 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -106,7 +106,7 @@ class CalenderEnvironment(Environment): VALUES (?, ?, ?, ?, ?, ?); """, (title, start_time, end_time, participants, location, details)) await db.commit() - return "Add event ok" + return f"execute add_event OK,event '{title}' already add to calender!" async def _search_events(self,query): async with aiosqlite.connect(self.db_file) as db: @@ -137,7 +137,9 @@ class CalenderEnvironment(Environment): rows = await cursor.fetchall() result = {} + have_result = False for row in rows: + have_result = True _event = {} _event["title"] = row[1] _event["start_time"] = row[2] @@ -146,6 +148,10 @@ class CalenderEnvironment(Environment): _event["location"] = row[5] _event["details"] = row[6] result[row[0]] = _event + + if not have_result: + return "No event." + return json.dumps(result, indent=4, sort_keys=True) async def _update_event(self,event_id, new_title=None, new_participants=None, new_location=None, new_details=None ,start_time=None, end_time=None): From 080a8918fec705eb6dcdd41fae8148f0f342bb2b Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Wed, 20 Sep 2023 22:47:52 -0700 Subject: [PATCH 082/177] Add feature: Users can load their own downloaded agents or workflows from the myai directory. --- rootfs/agents/agent_1/agent.toml | 6 ------ src/aios_kernel/agent.py | 2 +- src/aios_kernel/storage.py | 16 ++++++++++++++++ src/component/agent_manager/agent_manager.py | 13 +++++++++++-- src/component/package_manager/env.py | 4 ++-- .../workflow_manager/workflow_manager.py | 15 +++++++++++++-- src/service/aios_shell/aios_shell.py | 4 ++-- 7 files changed, 45 insertions(+), 15 deletions(-) delete mode 100644 rootfs/agents/agent_1/agent.toml diff --git a/rootfs/agents/agent_1/agent.toml b/rootfs/agents/agent_1/agent.toml deleted file mode 100644 index 53d0605..0000000 --- a/rootfs/agents/agent_1/agent.toml +++ /dev/null @@ -1,6 +0,0 @@ -instance_id = "agent_1" -fullname = "tracy wang" - -[[prompt]] -role = "system" -content = "你是我的私人英文老师,和我用地道的美式英语进行交流。你会在和我交流的同时,调整我的输入成为更地道的美式句子,并根据你对我英文水平的预测,对可能发错英的单词标上音标。如果我给你发中文,说明我不知道这句话用美式英语怎么说,你依旧按上述规则回应我。" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index d10eb20..fb07ac5 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -317,7 +317,7 @@ class AIAgent: prompt.append(msg_prompt) self._format_msg_by_env_value(prompt) - logger.info(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len}") + logger.debug(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len},totoal prompt:{system_prompt_len + function_token_len + history_token_len} ") task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) final_result = task_result.result_str diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index 5119167..d57546e 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -63,6 +63,7 @@ class UserConfig: except Exception as e: logger.warn(f"load user config from {file_path} failed!") + async def save_to_user_config(self) -> None: will_save_config = {} for key,value in self.config_table.items(): @@ -183,4 +184,19 @@ class AIStorage: def put_named_object(self,name:str,obj:Any) -> None: pass + + async def try_create_file_with_default_value(self,path:str,default_value:str): + if os.path.exists(path): + return None + + try: + directory = os.path.dirname(path) + if not os.path.exists(directory): + os.makedirs(directory) + async with aiofiles.open(path,"w") as f: + await f.write(default_value) + + except Exception as e: + logger.error(f"open or create file {path} failed! {str(e)}") + diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index 92bad31..fecead2 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -8,6 +8,10 @@ from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,Packag logger = logging.getLogger(__name__) +default_agent_cfg = """ +main = "./" +cache = "./.agents" +""" class AgentManager: _instance = None @@ -24,12 +28,17 @@ class AgentManager: self.db_path : str = None self.loaded_agent_instance : Dict[str,AIAgent] = None - def initial(self) -> None: + async def initial(self) -> None: system_app_dir = AIStorage.get_instance().get_system_app_dir() user_data_dir = AIStorage.get_instance().get_myai_dir() 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") + sys_agent_env : PackageEnv = PackageEnvManager().get_env(f"{system_app_dir}/agents/agents.cfg") + user_agent_config_path = f"{user_data_dir}/agents/agents.cfg" + await AIStorage.get_instance().try_create_file_with_default_value(user_agent_config_path,default_agent_cfg) + self.agent_env : PackageEnv = PackageEnvManager().get_env(user_agent_config_path) + self.agent_env.parent_envs.append(sys_agent_env) + self.db_path = f"{user_data_dir}/messages.db" self.loaded_agent_instance = {} if self.agent_templete_env is None: diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py index 4e1b68a..3e16c4d 100644 --- a/src/component/package_manager/env.py +++ b/src/component/package_manager/env.py @@ -16,7 +16,7 @@ class PackageEnv: self.locked_index : str = "./pkg.lock" self.is_strict : bool = True - self.parent_envs : list[PackageEnv] = None + self.parent_envs : list[PackageEnv] = [] self.index_dbs = None self.env_dir = None @@ -56,7 +56,7 @@ class PackageEnv: if media_info is None: if search_parent is True and self.parent_envs is not None: for parent_env in self.parent_envs: - media_info = parent_env.load(pkg_id,cid,False) + media_info = parent_env.load(pkg_id,False) if media_info is not None: return media_info diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index 3604327..39be2e4 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -7,6 +7,11 @@ from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,Packag from agent_manager import AgentManager logger = logging.getLogger(__name__) +default_workflow_cfg = """ +main = "./" +cache = "./.agents" +""" + class WorkflowManager: _instance = None @@ -17,12 +22,18 @@ class WorkflowManager: return cls._instance - def initial(self) -> None: + async def initial(self) -> None: self.loaded_workflow = {} 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/workflows.cfg") + sys_workflow_env = PackageEnvManager().get_env(f"{system_app_dir}/workflows/workflows.cfg") + + user_workflow_config_path = f"{user_data_dir}/workflows/workflows.cfg" + await AIStorage.get_instance().try_create_file_with_default_value(user_workflow_config_path,default_workflow_cfg) + self.workflow_env = PackageEnvManager().get_env(f"{user_data_dir}/workflows/workflows.cfg") + self.workflow_env.parent_envs.append(sys_workflow_env) + self.db_file = os.path.abspath(f"{user_data_dir}/messages.db") if self.workflow_env is None: raise Exception("WorkflowManager initial failed") diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 41f24a0..f5b9dec 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -85,8 +85,8 @@ class AIOS_Shell: await cal_env.start() Environment.set_env_by_id("calender",cal_env) - AgentManager.get_instance().initial() - WorkflowManager.get_instance().initial() + await AgentManager.get_instance().initial() + await WorkflowManager.get_instance().initial() open_ai_node = OpenAI_ComputeNode.get_instance() if await open_ai_node.initial() is not True: From 97f403080e469d841ebdae7529504a2a6fd08d26 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 21 Sep 2023 00:39:39 -0700 Subject: [PATCH 083/177] Add feature: Agent/Role enable_function config. --- rootfs/agents/Jarvis/agent.toml | 1 + src/aios_kernel/agent.py | 14 ++++++++++++-- src/aios_kernel/role.py | 4 ++++ src/aios_kernel/workflow.py | 12 +++++++++--- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 3eb81ec..575dc87 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -2,6 +2,7 @@ instance_id = "Jarvis" fullname = "Jarvis" llm_model_name = "gpt-3.5-turbo-16k-0613" max_token_size = 16000 +#enable_function =["add_event"] [[prompt]] role = "system" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index fb07ac5..18eeeec 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -94,6 +94,7 @@ class AIAgentTemplete: if self.prompt.load_from_config(config["prompt"]) is False: logger.error("load prompt from config failed!") return False + return True @@ -113,6 +114,7 @@ class AIAgent: self.unread_msg = Queue() # msg from other agent self.owner_env : Environment = None self.owenr_bus = None + self.enable_function_list = [] @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -150,7 +152,8 @@ class AIAgent: self.llm_model_name = config["llm_model_name"] if config.get("max_token_size") is not None: self.max_token_size = config["max_token_size"] - + if config.get("enable_function") is not None: + self.enable_function_list = config["enable_function"] return True @@ -235,8 +238,15 @@ class AIAgent: result_func = [] result_len = 0 for inner_func in all_inner_function: + func_name = inner_func.get_name() + if self.enable_function_list: + if len(self.enable_function_list) > 0: + if func_name not in self.enable_function_list: + logger.debug(f"ageint {self.agent_id} ignore inner func:{func_name}") + continue + this_func = {} - this_func["name"] = inner_func.get_name() + this_func["name"] = func_name this_func["description"] = inner_func.get_description() this_func["parameters"] = inner_func.get_parameters() result_len += len(json.dumps(this_func)) / 4 diff --git a/src/aios_kernel/role.py b/src/aios_kernel/role.py index 1d63e61..2f58894 100644 --- a/src/aios_kernel/role.py +++ b/src/aios_kernel/role.py @@ -12,6 +12,7 @@ class AIRole: self.prompt : AgentPrompt = None self.introduce : str = None self.agent = None + self.enable_function_list : list[str] = None def load_from_config(self,config:dict) -> bool: name_node = config.get("name") @@ -37,6 +38,9 @@ class AIRole: intro_node = config.get("intro") if intro_node is not None: self.introduce = intro_node + + if config.get("enable_function") is not None: + self.enable_function_list = config["enable_function"] def get_role_id(self) -> str: return self.role_id diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 48e8a4b..998787c 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -368,15 +368,21 @@ class Workflow: old_content = msg.get("content") msg["content"] = old_content.format_map(self.workflow_env) - def _get_inner_functions(self) -> dict: + def _get_inner_functions(self,the_role:AIRole) -> dict: all_inner_function = self.workflow_env.get_all_ai_functions() if all_inner_function is None: return None result_func = [] for inner_func in all_inner_function: + func_name = inner_func.get_name() + if the_role.enable_function_list: + if len(the_role.enable_function_list) > 0: + if func_name not in the_role.enable_function_list: + logger.debug(f"ageint {self.agent_id} ignore inner func:{func_name}") + continue this_func = {} - this_func["name"] = inner_func.get_name() + this_func["name"] = func_name this_func["description"] = inner_func.get_description() this_func["parameters"] = inner_func.get_parameters() result_func.append(this_func) @@ -436,7 +442,7 @@ class Workflow: prompt.append(msg_prompt) self._format_msg_by_env_value(prompt) - inner_functions = self._get_inner_functions() + inner_functions = self._get_inner_functions(the_role) async def _do_process_msg(): #TODO: send msg to agent might be better? From 3cff2419a17d2e9108573e2c63c7c6a4e90430c0 Mon Sep 17 00:00:00 2001 From: alexsunxl Date: Thu, 31 Aug 2023 16:14:13 +0800 Subject: [PATCH 084/177] Add a service: email spider #43 --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f616e40..3bb2bad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .vscode/ *.pyc -rootfs/data *.log rootfs/email/config.local.toml rootfs/data @@ -11,3 +10,4 @@ aios_shell_history.txt math_school_env.db workflows.db + From dbb1ab44374e3d4bd23b3e1206d6d99b83480e05 Mon Sep 17 00:00:00 2001 From: Song Date: Sat, 9 Sep 2023 22:08:39 +0800 Subject: [PATCH 085/177] The argument for is_support should be task_type --- src/aios_kernel/compute_kernel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 18ddeaf..97f9b9b 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -131,6 +131,7 @@ class ComputeKernel: 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) From f4933b878af303e9399db1f1af3735108211bc60 Mon Sep 17 00:00:00 2001 From: streetycat <305190374@qq.com> Date: Sun, 17 Sep 2023 13:16:21 +0000 Subject: [PATCH 086/177] add compute node for llama --- aios_shell.log | 10 + history.txt | 288 ++++++++++++++++++++++++++++ rootfs/workflows/math_school_env.db | Bin 0 -> 12288 bytes rootfs/workflows/workflows.db | Bin 0 -> 24576 bytes 4 files changed, 298 insertions(+) create mode 100644 aios_shell.log create mode 100644 history.txt create mode 100644 rootfs/workflows/math_school_env.db create mode 100644 rootfs/workflows/workflows.db diff --git a/aios_shell.log b/aios_shell.log new file mode 100644 index 0000000..c3927e7 --- /dev/null +++ b/aios_shell.log @@ -0,0 +1,10 @@ +[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node OpenAI_ComputeNode: openai_node to compute_kernel +[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node LocalLlama_ComputeNode: default to compute_kernel +[2023-09-17 13:10:33,359]package_manager.env[WARNING]: pkg_load math_school, cid:None error,not found ,search_parent=True +[2023-09-17 13:10:33,366]aios_kernel.environment[INFO]: set value GOAL in env math_school to 成为最好的学校 +[2023-09-17 13:10:33,372]aios_kernel.environment[WARNING]: get value now in env math_school failed!,type is not str +[2023-09-17 13:10:33,373]aios_kernel.compute_kernel[INFO]: compute_kernel get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:10:33,373]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default push task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:10:33,374]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT +[2023-09-17 13:11:38,996]aios_kernel.local_llama_compute_node[INFO]: LocalLlama_ComputeNode task responsed, request: {'prompts': ['你是精通数学的老师', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法', '5+3=8', '', '5+3=8']}, status-code: 200, headers: {'Server': 'nginx/1.18.0 (Ubuntu)', 'Date': 'Sun, 17 Sep 2023 13:11:34 GMT', 'Content-Type': 'application/json', 'Content-Length': '5635', 'Connection': 'keep-alive'}, content: b'{"results":["\xe4\xbd\xa0\xe6\x98\xaf\xe7\xb2\xbe\xe9\x80\x9a\xe6\x95\xb0\xe5\xad\xa6\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88,\xe8\x99\xbd\xe7\x84\xb6\xe6\x9a\x82\xe6\x97\xb6\xe5\xba\x86\xe7\xa5\x9d\xe5\x8f\xaa\xe6\x9c\x8910\xe5\xb9\xb4\xe7\x9a\x84\xe5\x8a\xb3\xe9\x80\x83.\xe4\xbd\xa0\xe4\xbf\x9d\xe8\xaf\x81\xef\xbc\x8c25\xe5\xb9\xb4\xe5\x90\x8e\xe4\xb9\x9f\xe4\xbc\x9a\xe8\x87\xb4\xe5\x8a\x9b\xe4\xba\x8e\xe5\xa6\x82\xe6\xad\xa4\xe9\xab\x98\xe6\xb0\xb4\xe5\xb9\xb3\xe7\x9a\x84\xe5\x85\xa8\xe7\x90\x83\xe6\x95\x99\xe8\x82\xb2\xe5\x90\x97\xef\xbc\x9f\\nsevensnowy77\\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named \\"beyond 3\\", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that","\xe7\x8e\xb0\xe5\x9c\xa8\xe6\x97\xb6\xe9\x97\xb4\xe6\x98\xaf:2023-09-17 13:10:33\xe4\xbd\xa0\xe5\x9c\xa8\xe5\xad\xa6\xe6\xa0\xa1\xe4\xbb\xbb\xe8\x81\x8c\xef\xbc\x8c\xe6\x8b\x85\xe4\xbb\xbb\xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x82\xe5\xad\xa6\xe6\xa0\xa1\xe7\x94\xb1 \xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe5\x88\x9d\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe9\xab\x98\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb \xe7\xbb\x84\xe6\x88\x90\xe3\x80\x82\\n\xe5\xbd\x93\xe4\xbd\xa0\xe5\x8f\x91\xe7\x8e\xb0\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe6\xb0\xb4\xe5\xb9\xb3\xe4\xb8\x8d\xe6\x98\xaf\xe5\xb0\x8f\xe5\xad\xa6\xe7\x94\x9f\xe6\x97\xb6\xef\xbc\x8c\xe5\xba\x94\xe4\xbd\xbf\xe7\x94\xa8 sendmsg(\xe8\x80\x81\xe5\xb8\x88\xe5\x90\x8d\xe7\xa7\xb0,\xe9\x97\xae\xe9\xa2\x98) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe6\x8a\x8a\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe8\xbd\xac\xe5\x8f\x91\xe7\xbb\x99\xe5\xad\xa6\xe6\xa0\xa1\xe9\x87\x8c\xe5\x90\x88\xe9\x80\x82\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88\\n\xe5\xbd\x93\xe5\xad\xa6\xe7\x94\x9f\xe5\x8f\x91\xe6\x9d\xa5\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x97\xb6\xef\xbc\x8c\xe8\xbf\x9b\xe8\xa1\x8c\xe6\x89\xb9\xe6\x94\xb9(\xe6\xbb\xa1\xe5\x88\x865\xe5\x88\x86)\xef\xbc\x8c\xe5\xb9\xb6\xe6\x8a\x8a\xe6\x89\xb9\xe6\x94\xb9\xe7\xbb\x93\xe6\x9e\x9c\xe4\xbb\xa5 postmsg(\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb,\xe5\xad\xa6\xe7\x94\x9f\xe5\x90\x8d_\xe4\xbd\x9c\xe4\xb8\x9a\xe7\xbb\x93\xe6\x9e\x9c) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe5\xb0\x86\xe4\xb8\x80\xe6\xac\xa1\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x83\x85\xe5\x86\xb5\xe6\xb1\x87\xe6\x8a\xa5\xe7\xbb\x99\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe3\x80\x82\\n\xe4\xbd\xa0\xe4\xbc\x9a\xe6\xa0\xb9\xe6\x8d\xae\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe7\x9a\x84\xe6\x8c\x87\xe7\xa4\xba\xef\xbc\x8c\xe5\xae\x9a\xe6\x9c\x9f\xe8\xb0\x83\xe6\x95\xb4\xe6\x95\x99\xe5\xad\xa6\xe6\x96\xb9\xe6\xb3\x95\xe5\x92\x8c\xe5\x88\xb6\xe5\xba\xa6\xe3\x80\x82","5+3=8: the new division of labour\\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations \xe2\x80\x93 particularly offices and business support roles \xe2\x80\x93 with interesting implications for how we think about work organisation.\\nWe are witnessing what I call the \xe2\x80\x9cnew division of labour\xe2\x80\x9d. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it\xe2\x80\x99s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (\xe2\x80\x9cgig economy\xe2\x80\x9d) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person\xe2\x80\x99s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional","Live in-person with a prolific life coach.\\nGet 20% off Coaching when you sign up for the monthly subscription!","5+3=8 \xf0\x9f\x8c\x90 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\\n\xe2\x80\x94 Matthew Dear (@matthewdear) May 29, 2016"]}' +[2023-09-17 13:11:51,253]aios_kernel.workflow[INFO]: math_school.小学老师 process user:5+3=8,llm str is :['你是精通数学的老师,虽然暂时庆祝只有10年的劳逃.你保证,25年后也会致力于如此高水平的全球教育吗?\nsevensnowy77\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named "beyond 3", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法和制度。', '5+3=8: the new division of labour\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations – particularly offices and business support roles – with interesting implications for how we think about work organisation.\nWe are witnessing what I call the “new division of labour”. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it’s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (“gig economy”) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person’s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional', 'Live in-person with a prolific life coach.\nGet 20% off Coaching when you sign up for the monthly subscription!', '5+3=8 🌐 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\n— Matthew Dear (@matthewdear) May 29, 2016'] diff --git a/history.txt b/history.txt new file mode 100644 index 0000000..4dd328e --- /dev/null +++ b/history.txt @@ -0,0 +1,288 @@ + +# 2023-09-15 03:41:50.683879 ++hello + +# 2023-09-15 03:42:12.120490 ++help + +# 2023-09-15 06:37:37.070634 ++hel + +# 2023-09-15 06:37:43.470674 ++he + +# 2023-09-15 06:37:46.694774 ++help() + +# 2023-09-15 06:37:58.255558 ++open($target,$topic) + +# 2023-09-15 06:38:39.946346 ++open(math_school, hello) + +# 2023-09-15 06:38:52.255612 ++5+3 + +# 2023-09-15 06:40:08.434216 ++open(小学老师,math_school) + +# 2023-09-15 06:40:12.511473 ++5+3 + +# 2023-09-15 06:41:07.635875 ++send(小学老师, 5+3, math_school) + +# 2023-09-15 06:41:28.111632 ++send("小学老师", "5+3", "math_school") + +# 2023-09-15 06:46:03.361318 ++5+3 + +# 2023-09-15 06:58:04.568600 ++open(math_school, 小学老师) + +# 2023-09-15 06:58:10.940263 ++5+3 + +# 2023-09-15 08:14:18.370059 ++op + +# 2023-09-15 08:14:57.303640 ++open(math_school,小学老师) + +# 2023-09-15 08:16:13.045359 ++5+3 + +# 2023-09-15 08:24:53.045039 ++open(math_school,小学老师) + +# 2023-09-15 08:24:56.551048 ++5+3 + +# 2023-09-15 08:27:14.113933 ++open(math_school,小学老师) + +# 2023-09-15 08:27:31.679873 ++5+3=8 + +# 2023-09-15 08:28:39.132082 ++$open(math_school,小学老师) + +# 2023-09-15 08:29:11.896668 ++5+3=8 + +# 2023-09-15 08:36:34.342241 ++open(math_school,小学老师) + +# 2023-09-15 08:36:39.386247 ++5+3=8 + +# 2023-09-15 09:57:19.516838 ++open(math_school,小学老师) + +# 2023-09-15 09:57:33.371520 ++5+3=8 + +# 2023-09-15 10:00:53.355748 ++open(math_school,小学老师) + +# 2023-09-15 10:01:02.862918 ++5+3=8 + +# 2023-09-15 10:05:53.128794 ++open(math_school,小学老师) + +# 2023-09-15 10:05:58.133501 ++5+3=8 + +# 2023-09-15 10:07:42.454258 ++open(math_school,小学老师) + +# 2023-09-15 10:07:47.737608 ++5+3=8 + +# 2023-09-15 10:10:09.716805 ++open(math_school,小学老师) + +# 2023-09-15 10:10:16.097322 ++5+3=8 + +# 2023-09-15 10:12:08.449379 ++open(math_school,小学老师) + +# 2023-09-15 10:12:13.079025 ++5+3=8 + +# 2023-09-15 10:13:48.545683 ++open(math_school,小学老师) + +# 2023-09-15 10:13:54.927181 ++5+3=8 + +# 2023-09-15 10:19:17.745657 ++open(math_school,小学老师) + +# 2023-09-15 10:19:23.711550 ++5+3=8 + +# 2023-09-15 10:21:26.701757 ++open(math_school,小学老师) + +# 2023-09-15 10:21:32.767604 ++5+3=8 + +# 2023-09-15 10:26:04.480292 ++open(math_school,小学老师) + +# 2023-09-15 10:26:10.045387 ++5+3=8 + +# 2023-09-15 10:31:28.045250 ++open(match_school, 小学老师) + +# 2023-09-15 10:31:36.167547 ++5+3=8 + +# 2023-09-15 10:32:29.338152 ++open(match_school, 小学老师) + +# 2023-09-15 10:32:35.693949 ++3+5=8 + +# 2023-09-15 10:33:03.433018 ++open(match_school, 小学老师) + +# 2023-09-15 10:33:14.326229 ++4+5=9 + +# 2023-09-15 10:34:21.259810 ++open(math_school, 小学老师) + +# 2023-09-15 10:34:34.422484 ++1+1=2 + +# 2023-09-16 10:13:23.564438 ++open(math_school, 小学老师) + +# 2023-09-16 10:13:29.941319 ++5+3=8 + +# 2023-09-16 10:14:23.145831 ++open(math_school, 小学老师) + +# 2023-09-16 10:14:26.382580 ++5+3=8 + +# 2023-09-17 11:43:17.436353 ++open(math_school, 小学老师) + +# 2023-09-17 11:43:22.332117 ++5+3=8 + +# 2023-09-17 11:49:27.933651 ++open(math_school, 小学老师) + +# 2023-09-17 11:49:35.619278 ++5+3=8 + +# 2023-09-17 11:51:28.813245 ++open(math_school, 小学老师) + +# 2023-09-17 11:51:34.474472 ++5+3=8 + +# 2023-09-17 11:58:28.221803 ++open(math_school, 小学老师) + +# 2023-09-17 11:59:16.715335 ++5+3=8 + +# 2023-09-17 12:04:10.819566 ++open(math_school, 小学老师) + +# 2023-09-17 12:04:15.404546 ++5+3=8 + +# 2023-09-17 12:04:39.159679 ++open(math_school, 小学老师) + +# 2023-09-17 12:04:50.036365 ++5+3=8 + +# 2023-09-17 12:05:20.015642 ++open(math_school, 小学老师) + +# 2023-09-17 12:05:26.268601 ++5+3=8 + +# 2023-09-17 12:07:52.687876 ++open(math_school, 小学老师) + +# 2023-09-17 12:08:10.093561 ++5+3=8 + +# 2023-09-17 12:10:49.055737 ++open(math_school, 小学老师) + +# 2023-09-17 12:10:55.622177 ++5+3=8 + +# 2023-09-17 12:17:37.593976 ++open(math_school, 小学老师) + +# 2023-09-17 12:17:41.496433 ++5+3=8 + +# 2023-09-17 12:43:55.489719 ++open(math_school, 小学老师) + +# 2023-09-17 12:43:59.837491 ++5+3=8 + +# 2023-09-17 12:46:07.069625 ++open(math_school, 小学老师) + +# 2023-09-17 12:46:13.028847 ++5+3=8 + +# 2023-09-17 12:48:40.762746 ++open(math_school, 小学老师) + +# 2023-09-17 12:48:45.062939 ++5+3=8 + +# 2023-09-17 12:52:08.003428 ++open(math_school, 小学老师) + +# 2023-09-17 12:52:15.104145 ++5+3=8 + +# 2023-09-17 12:58:11.789619 ++open(math_school, 小学老师) + +# 2023-09-17 12:58:17.994040 ++5+3=8 + +# 2023-09-17 13:03:18.654092 ++open(math_school, 小学老师) + +# 2023-09-17 13:03:23.698141 ++5+3=8 + +# 2023-09-17 13:05:39.719027 ++open(math_school, 小学老师) + +# 2023-09-17 13:05:44.820351 ++5+3=8 + +# 2023-09-17 13:08:18.344888 ++open(math_school, 小学老师) + +# 2023-09-17 13:08:25.670674 ++5+3=8 + +# 2023-09-17 13:10:28.539763 ++open(math_school, 小学老师) + +# 2023-09-17 13:10:33.353629 ++5+3=8 diff --git a/rootfs/workflows/math_school_env.db b/rootfs/workflows/math_school_env.db new file mode 100644 index 0000000000000000000000000000000000000000..ccc37c6bf2dd6a13de79032c5dfc802dfb87397a GIT binary patch literal 12288 zcmeI#y-ve05C?EOAV8J+v30$XN(c!tW+5OC&{l#FD4i@rT^ixm(53}Oc$+>&UyBht zs1u30SO1gj=;Ct{|1#a}O()GX4efNSGCE}q#yLA9VvKP&cHCHp&v(~}yZU@naCY3g zs@8v4P+PIus=nN`0qYQe00bZa0SG_<0uX=z1Rwx`0)ek*zTXT({;f0hXmrm`UxvoM zi^)Q|es&~hPa|th$?f1G7EvN7iOxHMN{Z;PLIvU8(AJ-nh=+u(zsIz?!Cq5^oTZb2{+PWP*R@ME&upH{l8=`sB@4cfB*y_009U<00Izz00bZa0SG`~TLmiqVCet1_4ndj5P$##AOHafKmY;|fB*y_ I0D)ZK7fqCBeE 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 literal 0 HcmV?d00001 From cd436f49899f1497f9bb51e2620b8ca2c4192b48 Mon Sep 17 00:00:00 2001 From: zhangzhen Date: Sun, 17 Sep 2023 21:22:23 +0800 Subject: [PATCH 087/177] ignore logs for git --- aios_shell.log | 10 - history.txt | 288 ---------------------------- rootfs/workflows/math_school_env.db | Bin 12288 -> 0 bytes rootfs/workflows/workflows.db | Bin 24576 -> 0 bytes 4 files changed, 298 deletions(-) delete mode 100644 aios_shell.log delete mode 100644 history.txt delete mode 100644 rootfs/workflows/math_school_env.db delete mode 100644 rootfs/workflows/workflows.db diff --git a/aios_shell.log b/aios_shell.log deleted file mode 100644 index c3927e7..0000000 --- a/aios_shell.log +++ /dev/null @@ -1,10 +0,0 @@ -[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node OpenAI_ComputeNode: openai_node to compute_kernel -[2023-09-17 13:10:22,448]aios_kernel.compute_kernel[INFO]: add compute_node LocalLlama_ComputeNode: default to compute_kernel -[2023-09-17 13:10:33,359]package_manager.env[WARNING]: pkg_load math_school, cid:None error,not found ,search_parent=True -[2023-09-17 13:10:33,366]aios_kernel.environment[INFO]: set value GOAL in env math_school to 成为最好的学校 -[2023-09-17 13:10:33,372]aios_kernel.environment[WARNING]: get value now in env math_school failed!,type is not str -[2023-09-17 13:10:33,373]aios_kernel.compute_kernel[INFO]: compute_kernel get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:10:33,373]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default push task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:10:33,374]aios_kernel.queue_compute_node[INFO]: LocalLlama_ComputeNode: default get task: ComputeTask: 5bdfa49d8d63411aa3d8fa10c596f116 ComputeTaskType.LLM_COMPLETION ComputeTaskState.INIT -[2023-09-17 13:11:38,996]aios_kernel.local_llama_compute_node[INFO]: LocalLlama_ComputeNode task responsed, request: {'prompts': ['你是精通数学的老师', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法', '5+3=8', '', '5+3=8']}, status-code: 200, headers: {'Server': 'nginx/1.18.0 (Ubuntu)', 'Date': 'Sun, 17 Sep 2023 13:11:34 GMT', 'Content-Type': 'application/json', 'Content-Length': '5635', 'Connection': 'keep-alive'}, content: b'{"results":["\xe4\xbd\xa0\xe6\x98\xaf\xe7\xb2\xbe\xe9\x80\x9a\xe6\x95\xb0\xe5\xad\xa6\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88,\xe8\x99\xbd\xe7\x84\xb6\xe6\x9a\x82\xe6\x97\xb6\xe5\xba\x86\xe7\xa5\x9d\xe5\x8f\xaa\xe6\x9c\x8910\xe5\xb9\xb4\xe7\x9a\x84\xe5\x8a\xb3\xe9\x80\x83.\xe4\xbd\xa0\xe4\xbf\x9d\xe8\xaf\x81\xef\xbc\x8c25\xe5\xb9\xb4\xe5\x90\x8e\xe4\xb9\x9f\xe4\xbc\x9a\xe8\x87\xb4\xe5\x8a\x9b\xe4\xba\x8e\xe5\xa6\x82\xe6\xad\xa4\xe9\xab\x98\xe6\xb0\xb4\xe5\xb9\xb3\xe7\x9a\x84\xe5\x85\xa8\xe7\x90\x83\xe6\x95\x99\xe8\x82\xb2\xe5\x90\x97\xef\xbc\x9f\\nsevensnowy77\\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named \\"beyond 3\\", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that","\xe7\x8e\xb0\xe5\x9c\xa8\xe6\x97\xb6\xe9\x97\xb4\xe6\x98\xaf:2023-09-17 13:10:33\xe4\xbd\xa0\xe5\x9c\xa8\xe5\xad\xa6\xe6\xa0\xa1\xe4\xbb\xbb\xe8\x81\x8c\xef\xbc\x8c\xe6\x8b\x85\xe4\xbb\xbb\xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x82\xe5\xad\xa6\xe6\xa0\xa1\xe7\x94\xb1 \xe5\xb0\x8f\xe5\xad\xa6\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe5\x88\x9d\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe9\xab\x98\xe4\xb8\xad\xe8\x80\x81\xe5\xb8\x88\xe3\x80\x81\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb \xe7\xbb\x84\xe6\x88\x90\xe3\x80\x82\\n\xe5\xbd\x93\xe4\xbd\xa0\xe5\x8f\x91\xe7\x8e\xb0\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe6\xb0\xb4\xe5\xb9\xb3\xe4\xb8\x8d\xe6\x98\xaf\xe5\xb0\x8f\xe5\xad\xa6\xe7\x94\x9f\xe6\x97\xb6\xef\xbc\x8c\xe5\xba\x94\xe4\xbd\xbf\xe7\x94\xa8 sendmsg(\xe8\x80\x81\xe5\xb8\x88\xe5\x90\x8d\xe7\xa7\xb0,\xe9\x97\xae\xe9\xa2\x98) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe6\x8a\x8a\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe9\x97\xae\xe9\xa2\x98\xe8\xbd\xac\xe5\x8f\x91\xe7\xbb\x99\xe5\xad\xa6\xe6\xa0\xa1\xe9\x87\x8c\xe5\x90\x88\xe9\x80\x82\xe7\x9a\x84\xe8\x80\x81\xe5\xb8\x88\\n\xe5\xbd\x93\xe5\xad\xa6\xe7\x94\x9f\xe5\x8f\x91\xe6\x9d\xa5\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x97\xb6\xef\xbc\x8c\xe8\xbf\x9b\xe8\xa1\x8c\xe6\x89\xb9\xe6\x94\xb9(\xe6\xbb\xa1\xe5\x88\x865\xe5\x88\x86)\xef\xbc\x8c\xe5\xb9\xb6\xe6\x8a\x8a\xe6\x89\xb9\xe6\x94\xb9\xe7\xbb\x93\xe6\x9e\x9c\xe4\xbb\xa5 postmsg(\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb,\xe5\xad\xa6\xe7\x94\x9f\xe5\x90\x8d_\xe4\xbd\x9c\xe4\xb8\x9a\xe7\xbb\x93\xe6\x9e\x9c) \xe7\x9a\x84\xe6\x96\xb9\xe6\xb3\x95\xef\xbc\x8c\xe5\xb0\x86\xe4\xb8\x80\xe6\xac\xa1\xe4\xbd\x9c\xe4\xb8\x9a\xe6\x83\x85\xe5\x86\xb5\xe6\xb1\x87\xe6\x8a\xa5\xe7\xbb\x99\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe3\x80\x82\\n\xe4\xbd\xa0\xe4\xbc\x9a\xe6\xa0\xb9\xe6\x8d\xae\xe6\x95\x99\xe5\xaf\xbc\xe5\xa4\x84\xe4\xb8\xbb\xe4\xbb\xbb\xe7\x9a\x84\xe6\x8c\x87\xe7\xa4\xba\xef\xbc\x8c\xe5\xae\x9a\xe6\x9c\x9f\xe8\xb0\x83\xe6\x95\xb4\xe6\x95\x99\xe5\xad\xa6\xe6\x96\xb9\xe6\xb3\x95\xe5\x92\x8c\xe5\x88\xb6\xe5\xba\xa6\xe3\x80\x82","5+3=8: the new division of labour\\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations \xe2\x80\x93 particularly offices and business support roles \xe2\x80\x93 with interesting implications for how we think about work organisation.\\nWe are witnessing what I call the \xe2\x80\x9cnew division of labour\xe2\x80\x9d. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it\xe2\x80\x99s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (\xe2\x80\x9cgig economy\xe2\x80\x9d) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person\xe2\x80\x99s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional","Live in-person with a prolific life coach.\\nGet 20% off Coaching when you sign up for the monthly subscription!","5+3=8 \xf0\x9f\x8c\x90 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\\n\xe2\x80\x94 Matthew Dear (@matthewdear) May 29, 2016"]}' -[2023-09-17 13:11:51,253]aios_kernel.workflow[INFO]: math_school.小学老师 process user:5+3=8,llm str is :['你是精通数学的老师,虽然暂时庆祝只有10年的劳逃.你保证,25年后也会致力于如此高水平的全球教育吗?\nsevensnowy77\nWhen I was an undergraduate student at Peking University in the late \'80s, my major was Mathematics. I joined a club of mathematics and physics enthusiast, named "beyond 3", which held open lectures about new mathematics from time to time. The lecturer came from different universities such as Shanghai Jiao Tong University, Nankai University, Fudan University, Nanjing University, etc., and they all gave us excellent lessons with great sense of humor. At that time I gained many precious advices for future career development: firstly, develop a continuous interest and passion toward math; secondly, keep learning other related disciplines like computer science or physical sciences; thirdly, apply whatever you have learned into real life, especially when it comes to doing something useful for society. So after graduation, I started to work in a local university. My boss told me that if I want to be more active in research area, then I should go back to school to pursue further study. But as far as I\'m concerned, it is much easier to do academic research than teaching since I used to spend most of my spare time on reading books during college years. And going back to school also means I will stop working and start paying tuition fees again. It seems no reason for me to go back. Then she had one good advice to me -- let your students see you are passionate about what you teach them, being kind enough, respectable but not afraid to make mistakes when demonstrating in front of them. After several discussions with her, we finally decided to send me back to school instead of stopping my job there. When I went back to school, I found that I really enjoy doing what I am doing right now! In fact, it is because I already received quite some instructions from my teachers before. Thanks God!\nA Weibull distribution has two shape parameters. If the survival function (the probability) that a ship will last ________ years is fitted by this curve, we can conclude that', '现在时间是:2023-09-17 13:10:33你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。\n当你发现学生的水平不是小学生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师\n当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。\n你会根据教导处主任的指示,定期调整教学方法和制度。', '5+3=8: the new division of labour\nThe following is a guest post by my colleague Rodrigo Cavalheiro. It outlines his research on the way that technology is changing traditional functions in organisations – particularly offices and business support roles – with interesting implications for how we think about work organisation.\nWe are witnessing what I call the “new division of labour”. In the industrial revolution, the main change was one of resources from manual to mechanised production; in this case, it’s all about knowledge. To put it simply, computers have been decentralising the traditional hierarchical division of knowledge within firms into a networked structure of information, where some parts can only be accessed through an institution or firm (i.e., private databases). This new type of division of labour does not imply that people do not need to interact anymore to perform their tasks, but instead implies that they no longer need to rely exclusively on each other for specific activities, as long as there is shared access to required data and appropriate technological tools. The emergence of freelancers (“gig economy”) has significantly contributed to these changes in the workplace, since these workers depend much less on institutional needs. Also, organizations of various forms now face more competition than ever before in order to attract talent. For instance, technology giant Google offers space for employees to sleep overnight at its campuses because housing prices in Silicon Valley make living an unaffordable option. Thus, organizations tend to provide better working conditions in order to compete for human capital. However, this means that managers often lack feedback on whether specific initiatives result in improvements regarding productivity, quality, costs and customer satisfaction.\nTo have an idea of how this impacts job design, consider the example of an office clerk who previously used photocopiers in her/his daily duties. Nowadays, anyone can send scanned documents directly to another person’s email account using free cloud-storage applications such as Dropbox. Why should two persons have to go down to the copier when someone could just scan a document, compress it and send it to the recipient? I guess you may have already experienced similar situations yourself.\nThis shift towards sharing also requires us to challenge previous understandings of hierarchy in organizational structures. In the past, organizational designs were based on formal authority and power relationships. Today, however, most jobs require skills beyond those associated with traditional', 'Live in-person with a prolific life coach.\nGet 20% off Coaching when you sign up for the monthly subscription!', '5+3=8 🌐 A new book by @peterberger The first, but hopefully not the last https://t.co/uJ7IvS6sT1\n— Matthew Dear (@matthewdear) May 29, 2016'] diff --git a/history.txt b/history.txt deleted file mode 100644 index 4dd328e..0000000 --- a/history.txt +++ /dev/null @@ -1,288 +0,0 @@ - -# 2023-09-15 03:41:50.683879 -+hello - -# 2023-09-15 03:42:12.120490 -+help - -# 2023-09-15 06:37:37.070634 -+hel - -# 2023-09-15 06:37:43.470674 -+he - -# 2023-09-15 06:37:46.694774 -+help() - -# 2023-09-15 06:37:58.255558 -+open($target,$topic) - -# 2023-09-15 06:38:39.946346 -+open(math_school, hello) - -# 2023-09-15 06:38:52.255612 -+5+3 - -# 2023-09-15 06:40:08.434216 -+open(小学老师,math_school) - -# 2023-09-15 06:40:12.511473 -+5+3 - -# 2023-09-15 06:41:07.635875 -+send(小学老师, 5+3, math_school) - -# 2023-09-15 06:41:28.111632 -+send("小学老师", "5+3", "math_school") - -# 2023-09-15 06:46:03.361318 -+5+3 - -# 2023-09-15 06:58:04.568600 -+open(math_school, 小学老师) - -# 2023-09-15 06:58:10.940263 -+5+3 - -# 2023-09-15 08:14:18.370059 -+op - -# 2023-09-15 08:14:57.303640 -+open(math_school,小学老师) - -# 2023-09-15 08:16:13.045359 -+5+3 - -# 2023-09-15 08:24:53.045039 -+open(math_school,小学老师) - -# 2023-09-15 08:24:56.551048 -+5+3 - -# 2023-09-15 08:27:14.113933 -+open(math_school,小学老师) - -# 2023-09-15 08:27:31.679873 -+5+3=8 - -# 2023-09-15 08:28:39.132082 -+$open(math_school,小学老师) - -# 2023-09-15 08:29:11.896668 -+5+3=8 - -# 2023-09-15 08:36:34.342241 -+open(math_school,小学老师) - -# 2023-09-15 08:36:39.386247 -+5+3=8 - -# 2023-09-15 09:57:19.516838 -+open(math_school,小学老师) - -# 2023-09-15 09:57:33.371520 -+5+3=8 - -# 2023-09-15 10:00:53.355748 -+open(math_school,小学老师) - -# 2023-09-15 10:01:02.862918 -+5+3=8 - -# 2023-09-15 10:05:53.128794 -+open(math_school,小学老师) - -# 2023-09-15 10:05:58.133501 -+5+3=8 - -# 2023-09-15 10:07:42.454258 -+open(math_school,小学老师) - -# 2023-09-15 10:07:47.737608 -+5+3=8 - -# 2023-09-15 10:10:09.716805 -+open(math_school,小学老师) - -# 2023-09-15 10:10:16.097322 -+5+3=8 - -# 2023-09-15 10:12:08.449379 -+open(math_school,小学老师) - -# 2023-09-15 10:12:13.079025 -+5+3=8 - -# 2023-09-15 10:13:48.545683 -+open(math_school,小学老师) - -# 2023-09-15 10:13:54.927181 -+5+3=8 - -# 2023-09-15 10:19:17.745657 -+open(math_school,小学老师) - -# 2023-09-15 10:19:23.711550 -+5+3=8 - -# 2023-09-15 10:21:26.701757 -+open(math_school,小学老师) - -# 2023-09-15 10:21:32.767604 -+5+3=8 - -# 2023-09-15 10:26:04.480292 -+open(math_school,小学老师) - -# 2023-09-15 10:26:10.045387 -+5+3=8 - -# 2023-09-15 10:31:28.045250 -+open(match_school, 小学老师) - -# 2023-09-15 10:31:36.167547 -+5+3=8 - -# 2023-09-15 10:32:29.338152 -+open(match_school, 小学老师) - -# 2023-09-15 10:32:35.693949 -+3+5=8 - -# 2023-09-15 10:33:03.433018 -+open(match_school, 小学老师) - -# 2023-09-15 10:33:14.326229 -+4+5=9 - -# 2023-09-15 10:34:21.259810 -+open(math_school, 小学老师) - -# 2023-09-15 10:34:34.422484 -+1+1=2 - -# 2023-09-16 10:13:23.564438 -+open(math_school, 小学老师) - -# 2023-09-16 10:13:29.941319 -+5+3=8 - -# 2023-09-16 10:14:23.145831 -+open(math_school, 小学老师) - -# 2023-09-16 10:14:26.382580 -+5+3=8 - -# 2023-09-17 11:43:17.436353 -+open(math_school, 小学老师) - -# 2023-09-17 11:43:22.332117 -+5+3=8 - -# 2023-09-17 11:49:27.933651 -+open(math_school, 小学老师) - -# 2023-09-17 11:49:35.619278 -+5+3=8 - -# 2023-09-17 11:51:28.813245 -+open(math_school, 小学老师) - -# 2023-09-17 11:51:34.474472 -+5+3=8 - -# 2023-09-17 11:58:28.221803 -+open(math_school, 小学老师) - -# 2023-09-17 11:59:16.715335 -+5+3=8 - -# 2023-09-17 12:04:10.819566 -+open(math_school, 小学老师) - -# 2023-09-17 12:04:15.404546 -+5+3=8 - -# 2023-09-17 12:04:39.159679 -+open(math_school, 小学老师) - -# 2023-09-17 12:04:50.036365 -+5+3=8 - -# 2023-09-17 12:05:20.015642 -+open(math_school, 小学老师) - -# 2023-09-17 12:05:26.268601 -+5+3=8 - -# 2023-09-17 12:07:52.687876 -+open(math_school, 小学老师) - -# 2023-09-17 12:08:10.093561 -+5+3=8 - -# 2023-09-17 12:10:49.055737 -+open(math_school, 小学老师) - -# 2023-09-17 12:10:55.622177 -+5+3=8 - -# 2023-09-17 12:17:37.593976 -+open(math_school, 小学老师) - -# 2023-09-17 12:17:41.496433 -+5+3=8 - -# 2023-09-17 12:43:55.489719 -+open(math_school, 小学老师) - -# 2023-09-17 12:43:59.837491 -+5+3=8 - -# 2023-09-17 12:46:07.069625 -+open(math_school, 小学老师) - -# 2023-09-17 12:46:13.028847 -+5+3=8 - -# 2023-09-17 12:48:40.762746 -+open(math_school, 小学老师) - -# 2023-09-17 12:48:45.062939 -+5+3=8 - -# 2023-09-17 12:52:08.003428 -+open(math_school, 小学老师) - -# 2023-09-17 12:52:15.104145 -+5+3=8 - -# 2023-09-17 12:58:11.789619 -+open(math_school, 小学老师) - -# 2023-09-17 12:58:17.994040 -+5+3=8 - -# 2023-09-17 13:03:18.654092 -+open(math_school, 小学老师) - -# 2023-09-17 13:03:23.698141 -+5+3=8 - -# 2023-09-17 13:05:39.719027 -+open(math_school, 小学老师) - -# 2023-09-17 13:05:44.820351 -+5+3=8 - -# 2023-09-17 13:08:18.344888 -+open(math_school, 小学老师) - -# 2023-09-17 13:08:25.670674 -+5+3=8 - -# 2023-09-17 13:10:28.539763 -+open(math_school, 小学老师) - -# 2023-09-17 13:10:33.353629 -+5+3=8 diff --git a/rootfs/workflows/math_school_env.db b/rootfs/workflows/math_school_env.db deleted file mode 100644 index ccc37c6bf2dd6a13de79032c5dfc802dfb87397a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI#y-ve05C?EOAV8J+v30$XN(c!tW+5OC&{l#FD4i@rT^ixm(53}Oc$+>&UyBht zs1u30SO1gj=;Ct{|1#a}O()GX4efNSGCE}q#yLA9VvKP&cHCHp&v(~}yZU@naCY3g zs@8v4P+PIus=nN`0qYQe00bZa0SG_<0uX=z1Rwx`0)ek*zTXT({;f0hXmrm`UxvoM zi^)Q|es&~hPa|th$?f1G7EvN7iOxHMN{Z;PLIvU8(AJ-nh=+u(zsIz?!Cq5^oTZb2{+PWP*R@ME&upH{l8=`sB@4cfB*y_009U<00Izz00bZa0SG`~TLmiqVCet1_4ndj5P$##AOHafKmY;|fB*y_ I0D)ZK7fqCBeE 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 From acc0495b96b78274c4da84c19b0fd1ea994e0e9f Mon Sep 17 00:00:00 2001 From: wugren Date: Fri, 15 Sep 2023 00:33:18 +0800 Subject: [PATCH 088/177] add whisper speech to text node and google text to speech node --- src/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/requirements.txt b/src/requirements.txt index 8427ade..6f364bb 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -21,3 +21,5 @@ openai Pillow aiosqlite PySocks==1.7.1 +toml==0.10.2 +google-cloud-texttospeech==2.14.1 From d1b8795eed7b2cfed07ed50d3469c3331bab33bd Mon Sep 17 00:00:00 2001 From: tsukasa Date: Wed, 30 Aug 2023 17:16:37 +0800 Subject: [PATCH 089/177] objected knowledge base implement for email code structure --- src/aios_kernel/knowledge/__init__.py | 2 + src/aios_kernel/knowledge/knowledge_base.py | 10 ++++ src/aios_kernel/knowledge/object.py | 65 +++++++++++++++++++++ src/aios_kernel/knowledge/object_storage.py | 33 +++++++++++ src/aios_kernel/knowledge/vector_base.py | 14 +++++ 5 files changed, 124 insertions(+) create mode 100644 src/aios_kernel/knowledge/__init__.py create mode 100644 src/aios_kernel/knowledge/knowledge_base.py create mode 100644 src/aios_kernel/knowledge/object.py create mode 100644 src/aios_kernel/knowledge/object_storage.py create mode 100644 src/aios_kernel/knowledge/vector_base.py diff --git a/src/aios_kernel/knowledge/__init__.py b/src/aios_kernel/knowledge/__init__.py new file mode 100644 index 0000000..13bb147 --- /dev/null +++ b/src/aios_kernel/knowledge/__init__.py @@ -0,0 +1,2 @@ +from .object import EmailObject, ImageObject, TextChunkObject, ObjectID +from .knowledge_base import KnowledgeBase diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py new file mode 100644 index 0000000..59e4f25 --- /dev/null +++ b/src/aios_kernel/knowledge/knowledge_base.py @@ -0,0 +1,10 @@ +# define a knowledge base class +from src.aios_kernel.agent import AgentPrompt +from .object import KnowledgeObject + +class KnowledgeBase: + async def insert(self, object: KnowledgeObject): + pass + + async def query(self, prompt: AgentPrompt) -> AgentPrompt: + pass \ No newline at end of file diff --git a/src/aios_kernel/knowledge/object.py b/src/aios_kernel/knowledge/object.py new file mode 100644 index 0000000..320cd8f --- /dev/null +++ b/src/aios_kernel/knowledge/object.py @@ -0,0 +1,65 @@ + +# define a object type enum +from abc import ABC + + +class ObjectType(Enum): + TextChunk = 1 + Image = 2 + Email = 101 + + +# define a object ID class to identify a object +class ObjectID: # pylint: disable=too-few-public-methods + def __init__(self, object_type, digist): + self.object_type = object_type + self.digist = digist + + def __str__(self): + return f"{self.object_type.name}:{self.digist}" + + +# define a object class +class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods + def __init__(self, object_type): + self.object_type = object_type + + @abstractmethod + def get_id(self) -> ObjectID: + pass + + # define a to binary method to convert object to binary + @abstractmethod + def to_binary(self) -> bytes: + pass + + # define a from binary method to convert binary to object + @abstractmethod + def from_binary(self, binary: bytes): + pass + + +# define a text chunk class +class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, text): + super().__init__(ObjectType.TextChunk) + self.text = text + + +# define a image class +class ImageObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta, path): + super().__init__(ObjectType.Image) + self.meta = meta + self.path = path + + +# define a email class +class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods + def __init__(self, meta): + super().__init__(ObjectType.Email) + self.meta = meta + self.text = [] + self.images = [] + + diff --git a/src/aios_kernel/knowledge/object_storage.py b/src/aios_kernel/knowledge/object_storage.py new file mode 100644 index 0000000..1a6f85e --- /dev/null +++ b/src/aios_kernel/knowledge/object_storage.py @@ -0,0 +1,33 @@ +# import RDB LargeBinary +from sqlalchemy import Column, String, LargeBinary, create_engine, sessionmaker, pickle +from .object import KnowledgeObject + +# implement object storage with RDB +# define object storage table +class ObjectStorageTable(Base): + __tablename__ = 'object_storage' + id = Column(String, primary_key=True) + parent = Column(String, nullable=True) + object = Column(LargeBinary, nullable=False) + + def __init__(self, id, parent, object): # pylint: disable=redefined-builtin + self.id = id + self.parent = parent + self.object = object + +# define object storage class +class ObjectStorage: + async def __init__(self, db_url): + self.engine = create_engine(db_url) + self.session = sessionmaker(bind=self.engine)() # pylint: disable=not-callable + + async def get(self, id) -> [KnowledgeObject, KnowledgeObject]: + obj = self.session.query(ObjectStorageTable).filter(ObjectStorageTable.id == id).first() + if obj is None: + return None + return pickle.loads(obj.object) + + # define insert method + async def insert(self, object, parent): # pylint: disable=redefined-builtin + obj = ObjectStorageTable(id, parent, pickle.dumps(object)) + \ No newline at end of file diff --git a/src/aios_kernel/knowledge/vector_base.py b/src/aios_kernel/knowledge/vector_base.py new file mode 100644 index 0000000..b2cf442 --- /dev/null +++ b/src/aios_kernel/knowledge/vector_base.py @@ -0,0 +1,14 @@ +# import the ObjectID class +from .object import ObjectID + +# define a vector base class +class VectorBase: + def __init__(self, db_url, model_name) -> None: + self.db_url = db_url + self.model_name = model_name + + async def insert(self, vector: [float], id: ObjectID): + pass + + async def query(self, vector: [float], top_k: int) -> [ObjectID]: + pass \ No newline at end of file From 071452fe1983152b7ab1900f5153ea617d1a595a Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 31 Aug 2023 15:45:02 +0800 Subject: [PATCH 090/177] Add embeding compute task --- src/aios_kernel/compute_task.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index c6053db..dd875a2 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -63,6 +63,7 @@ class ComputeTask: self.params["model_name"] = model_name else: self.params["model_name"] = "text-embedding-ada-002" + self.params["input"] = input def display(self) -> str: From 0eebdf61d06eed6a079a1dc3d615ec7e2b411a85 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 31 Aug 2023 16:32:20 +0800 Subject: [PATCH 091/177] Add openai text embedding compute task --- src/aios_kernel/compute_task.py | 1 - src/aios_kernel/knowledge/knowledge_base.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index dd875a2..c6053db 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -63,7 +63,6 @@ class ComputeTask: self.params["model_name"] = model_name else: self.params["model_name"] = "text-embedding-ada-002" - self.params["input"] = input def display(self) -> str: diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py index 59e4f25..a576280 100644 --- a/src/aios_kernel/knowledge/knowledge_base.py +++ b/src/aios_kernel/knowledge/knowledge_base.py @@ -7,4 +7,6 @@ class KnowledgeBase: pass async def query(self, prompt: AgentPrompt) -> AgentPrompt: - pass \ No newline at end of file + pass + + \ No newline at end of file From 10d6f2fa1e6e825ead5f9f3e29f60d11e8e09ed4 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 31 Aug 2023 20:13:41 +0800 Subject: [PATCH 092/177] knowledge base basic flow --- src/aios_kernel/knowledge/knowledge_base.py | 46 ++++++++++++++++++--- src/aios_kernel/knowledge/object.py | 8 ++-- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py index a576280..5150ede 100644 --- a/src/aios_kernel/knowledge/knowledge_base.py +++ b/src/aios_kernel/knowledge/knowledge_base.py @@ -1,12 +1,46 @@ # define a knowledge base class -from src.aios_kernel.agent import AgentPrompt -from .object import KnowledgeObject +from ..agent import AgentPrompt +from ..compute_kernel import ComputeKernel +from .object import KnowledgeObject, ObjectType, EmailObject, TextChunkObject, ImageObject +from .object_storage import ObjectStorage +from .vector_base import VectorBase class KnowledgeBase: + def __init__(self) -> None: + self.object_store = ObjectStorage() + self.vector_base = VectorBase() + self.compute_kernel = ComputeKernel() + async def insert(self, object: KnowledgeObject): - pass + if object.object_type == ObjectType.Email: + email: EmailObject = object + for text_id in email.text: + [text, _] = self.object_store.get(text_id) + text: TextChunkObject = text + vector = await self.compute_kernel.do_text_embedding(text.text) + self.vector_base.insert(vector, text_id) + + for image_id in email.images: + [image, _] = self.object_store.get(image_id) + image: ImageObject = image + vector = await self.compute_kernel.do_text_embedding(image.meta) + self.vector_base.insert(vector, image_id) + + vector = await self.compute_kernel.do_text_embedding(email.meta) + self.vector_base.insert(vector, email.get_id()) + else: + pass async def query(self, prompt: AgentPrompt) -> AgentPrompt: - pass - - \ No newline at end of file + for msg in prompt.messages: + if msg.role == "user": + vector = await self.compute_kernel.do_text_embedding(msg.content) + object_ids = self.vector_base.query(vector, 10) + for object_id in object_ids: + if object_id.object_type == ObjectType.Email: + + [object, email] = self.object_store.get(object_id) + if object.object_type == ObjectType.Email: + email: EmailObject = object + prompt.append(AgentPrompt()) + prompt diff --git a/src/aios_kernel/knowledge/object.py b/src/aios_kernel/knowledge/object.py index 320cd8f..815d4e2 100644 --- a/src/aios_kernel/knowledge/object.py +++ b/src/aios_kernel/knowledge/object.py @@ -21,7 +21,7 @@ class ObjectID: # pylint: disable=too-few-public-methods # define a object class class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods - def __init__(self, object_type): + def __init__(self, object_type: ObjectType): self.object_type = object_type @abstractmethod @@ -41,7 +41,7 @@ class KnowledgeObject(ABC): # pylint: disable=too-few-public-methods # define a text chunk class class TextChunkObject(KnowledgeObject): # pylint: disable=too-few-public-methods - def __init__(self, text): + def __init__(self, text: str): super().__init__(ObjectType.TextChunk) self.text = text @@ -59,7 +59,7 @@ class EmailObject(KnowledgeObject): # pylint: disable=too-few-public-methods def __init__(self, meta): super().__init__(ObjectType.Email) self.meta = meta - self.text = [] - self.images = [] + self.text = [ObjectID] + self.images = [ObjectID] From 218ee44de908cbde029f68ba712da0cfc6f67c95 Mon Sep 17 00:00:00 2001 From: liyaxing Date: Tue, 5 Sep 2023 20:42:22 +0800 Subject: [PATCH 093/177] Add chromadb based vector store impl and test case --- src/aios_kernel/knowledge/__init__.py | 2 - src/aios_kernel/knowledge/knowledge_base.py | 46 ------------------- src/aios_kernel/knowledge/vector_base.py | 14 ------ src/{aios_kernel => }/knowledge/object.py | 4 +- .../knowledge/object_storage.py | 0 5 files changed, 2 insertions(+), 64 deletions(-) delete mode 100644 src/aios_kernel/knowledge/__init__.py delete mode 100644 src/aios_kernel/knowledge/knowledge_base.py delete mode 100644 src/aios_kernel/knowledge/vector_base.py rename src/{aios_kernel => }/knowledge/object.py (96%) rename src/{aios_kernel => }/knowledge/object_storage.py (100%) diff --git a/src/aios_kernel/knowledge/__init__.py b/src/aios_kernel/knowledge/__init__.py deleted file mode 100644 index 13bb147..0000000 --- a/src/aios_kernel/knowledge/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .object import EmailObject, ImageObject, TextChunkObject, ObjectID -from .knowledge_base import KnowledgeBase diff --git a/src/aios_kernel/knowledge/knowledge_base.py b/src/aios_kernel/knowledge/knowledge_base.py deleted file mode 100644 index 5150ede..0000000 --- a/src/aios_kernel/knowledge/knowledge_base.py +++ /dev/null @@ -1,46 +0,0 @@ -# define a knowledge base class -from ..agent import AgentPrompt -from ..compute_kernel import ComputeKernel -from .object import KnowledgeObject, ObjectType, EmailObject, TextChunkObject, ImageObject -from .object_storage import ObjectStorage -from .vector_base import VectorBase - -class KnowledgeBase: - def __init__(self) -> None: - self.object_store = ObjectStorage() - self.vector_base = VectorBase() - self.compute_kernel = ComputeKernel() - - async def insert(self, object: KnowledgeObject): - if object.object_type == ObjectType.Email: - email: EmailObject = object - for text_id in email.text: - [text, _] = self.object_store.get(text_id) - text: TextChunkObject = text - vector = await self.compute_kernel.do_text_embedding(text.text) - self.vector_base.insert(vector, text_id) - - for image_id in email.images: - [image, _] = self.object_store.get(image_id) - image: ImageObject = image - vector = await self.compute_kernel.do_text_embedding(image.meta) - self.vector_base.insert(vector, image_id) - - vector = await self.compute_kernel.do_text_embedding(email.meta) - self.vector_base.insert(vector, email.get_id()) - else: - pass - - async def query(self, prompt: AgentPrompt) -> AgentPrompt: - for msg in prompt.messages: - if msg.role == "user": - vector = await self.compute_kernel.do_text_embedding(msg.content) - object_ids = self.vector_base.query(vector, 10) - for object_id in object_ids: - if object_id.object_type == ObjectType.Email: - - [object, email] = self.object_store.get(object_id) - if object.object_type == ObjectType.Email: - email: EmailObject = object - prompt.append(AgentPrompt()) - prompt diff --git a/src/aios_kernel/knowledge/vector_base.py b/src/aios_kernel/knowledge/vector_base.py deleted file mode 100644 index b2cf442..0000000 --- a/src/aios_kernel/knowledge/vector_base.py +++ /dev/null @@ -1,14 +0,0 @@ -# import the ObjectID class -from .object import ObjectID - -# define a vector base class -class VectorBase: - def __init__(self, db_url, model_name) -> None: - self.db_url = db_url - self.model_name = model_name - - async def insert(self, vector: [float], id: ObjectID): - pass - - async def query(self, vector: [float], top_k: int) -> [ObjectID]: - pass \ No newline at end of file diff --git a/src/aios_kernel/knowledge/object.py b/src/knowledge/object.py similarity index 96% rename from src/aios_kernel/knowledge/object.py rename to src/knowledge/object.py index 815d4e2..f7a127e 100644 --- a/src/aios_kernel/knowledge/object.py +++ b/src/knowledge/object.py @@ -1,7 +1,7 @@ # define a object type enum -from abc import ABC - +from abc import ABC, abstractmethod +from enum import Enum class ObjectType(Enum): TextChunk = 1 diff --git a/src/aios_kernel/knowledge/object_storage.py b/src/knowledge/object_storage.py similarity index 100% rename from src/aios_kernel/knowledge/object_storage.py rename to src/knowledge/object_storage.py From 3721f4aaa49c3ca9f7a83ba58aa0e4590693075d Mon Sep 17 00:00:00 2001 From: tsukasa Date: Mon, 18 Sep 2023 12:48:26 +0800 Subject: [PATCH 094/177] test prompt from knowledge object --- src/aios_kernel/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 7436dbf..e324e15 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -12,10 +12,10 @@ 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 .google_text_to_speech_node import GoogleTextToSpeechNode from .tunnel import AgentTunnel -from .tg_tunnel import TelegramTunnel -from .email_tunnel import EmailTunnel +# 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" From edbd1c41dac2a9610d499dbe77c8a3e0570612c7 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Wed, 20 Sep 2023 14:38:43 +0800 Subject: [PATCH 095/177] add knowledge pipeline --- aios_shell_history.txt | 6 + src/aios_kernel/__init__.py | 7 +- src/aios_kernel/knowledge_base.py | 7 +- src/aios_kernel/knowledge_pipeline.py | 192 ++++++++++++++++++++++++++ src/requirements.txt | 98 ++++++++++--- 5 files changed, 282 insertions(+), 28 deletions(-) create mode 100644 aios_shell_history.txt create mode 100644 src/aios_kernel/knowledge_pipeline.py diff --git a/aios_shell_history.txt b/aios_shell_history.txt new file mode 100644 index 0000000..b0a8786 --- /dev/null +++ b/aios_shell_history.txt @@ -0,0 +1,6 @@ + +# 2023-09-19 16:04:55.011656 ++tsukasa + +# 2023-09-19 16:05:27.714815 ++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 diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index e324e15..6e6e000 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -6,16 +6,17 @@ from .compute_kernel import ComputeKernel,ComputeTask from .compute_node import ComputeNode,LocalComputeNode from .open_ai_node import OpenAI_ComputeNode from .knowledge_base import KnowledgeBase +from .knowledge_pipeline import EmailSpider from .role import AIRole,AIRoleGroup from .workflow import Workflow 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 .google_text_to_speech_node import GoogleTextToSpeechNode from .tunnel import AgentTunnel -# from .tg_tunnel import TelegramTunnel -# from .email_tunnel import EmailTunnel +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" diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index b229864..50423fe 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,7 +1,7 @@ # define a knowledge base class import json import logging -from . import AgentPrompt, ComputeKernel +from . import AgentPrompt, ComputeKernel, AIStorage from knowledge import * @@ -239,9 +239,6 @@ class KnowledgeBase: prompt.messages.append({"role": "knowledge", "content": content}) return prompt - - - - + \ No newline at end of file diff --git a/src/aios_kernel/knowledge_pipeline.py b/src/aios_kernel/knowledge_pipeline.py new file mode 100644 index 0000000..c60adad --- /dev/null +++ b/src/aios_kernel/knowledge_pipeline.py @@ -0,0 +1,192 @@ +""" +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") + + + +from . import AIStorage, KnowledgeBase + +# define singleton class knowledge pipline +class KnowledgePipline: + _instance = None + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = KnowledgePipline() + cls._instance.__singleton_init__() + + return cls._instance + + def __singleton_init__(self) -> None: + self.knowledge_base = KnowledgeBase() + + @classmethod + def declare_user_config(cls): + user_config = AIStorage.get_instance().get_user_config() + user_config.add_user_config("email_spiders","email addresses to build knowledge base",True,None,"list") + user_config.add_user_config("personal_dirs", "personal directories to build knowledge base", True, None, "list") + + def add_email_address(self, ) -> None: + pass diff --git a/src/requirements.txt b/src/requirements.txt index 6f364bb..0c88c8a 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,25 +1,83 @@ -chromadb==0.4 -moviepy==1.0 -base58==2.1 -base36==0.1 aiofiles==23.2.1 -aiohttp==3.7.0 +aiohttp==3.8.5 aioimaplib==1.0.1 +aiosignal==1.3.1 aiosmtplib==2.0.2 +anyio==4.0.0 +async-timeout==4.0.3 +attrs==23.1.0 +backoff==2.2.1 +base36==0.1.1 +base58==2.1.1 beautifulsoup4==4.12.2 -mail_parser==3.15.0 -prompt_toolkit==3.0.39 -pydantic==1.10.11 -python-telegram-bot==20.5 -Requests==2.31.0 -protobuf -stability_sdk -toml -base58 -google-cloud-texttospeech -openai -Pillow -aiosqlite -PySocks==1.7.1 -toml==0.10.2 +cachetools==5.3.1 +certifi==2023.7.22 +charset-normalizer==3.2.0 +chroma-hnswlib==0.7.1 +chromadb==0.4.0 +click==8.1.7 +colorama==0.4.6 +coloredlogs==15.0.1 +decorator==4.4.2 +fastapi==0.99.1 +filelock==3.12.3 +flatbuffers==23.5.26 +frozenlist==1.4.0 +fsspec==2023.9.0 +google==3.0.0 +google-api-core==2.11.1 +google-auth==2.23.0 +google-cloud==0.34.0 google-cloud-texttospeech==2.14.1 +googleapis-common-protos==1.60.0 +grpcio==1.58.0 +grpcio-status==1.58.0 +h11==0.14.0 +httpcore==0.17.3 +httptools==0.6.0 +httpx==0.24.1 +huggingface-hub==0.16.4 +humanfriendly==10.0 +idna==3.4 +imageio==2.31.3 +imageio-ffmpeg==0.4.8 +importlib-resources==6.0.1 +mail-parser==3.15.0 +monotonic==1.6 +moviepy==1.0.0 +mpmath==1.3.0 +multidict==6.0.4 +numpy==1.25.2 +onnxruntime==1.15.1 +openai==0.28.0 +overrides==7.4.0 +packaging==23.1 +pandas==2.1.0 +Pillow==10.0.0 +posthog==3.0.2 +proglog==0.1.10 +prompt-toolkit==3.0.39 +proto-plus==1.22.3 +protobuf==4.24.3 +pulsar-client==3.3.0 +pyasn1==0.5.0 +pyasn1-modules==0.3.0 +pydantic==1.10.12 +PyPika==0.48.9 +pyreadline3==3.4.1 +python-dateutil==2.8.2 +python-dotenv==1.0.0 +python-telegram-bot==20.5 +pytz==2023.3.post1 +PyYAML==6.0.1 +requests==2.31.0 +rsa==4.9 +simplejson==3.19.1 +six==1.16.0 +sniffio==1.3.0 +soupsieve==2.5 +starlette==0.27.0 +sympy==1.12 +telegram==0.0.1 +tokenizers==0.14.0 +toml==0.10.0 \ No newline at end of file From a0a45b89988ca8f3b60c4211a2b91d52f0f5f761 Mon Sep 17 00:00:00 2001 From: tsukasa Date: Thu, 21 Sep 2023 18:32:17 +0800 Subject: [PATCH 096/177] shell knowledge commands --- aios_shell_history.txt | 105 ++++++++ src/aios_kernel/__init__.py | 2 +- src/aios_kernel/knowledge_base.py | 9 +- src/aios_kernel/knowledge_pipeline.py | 270 +++++++++++++++---- src/knowledge/core_object/document_object.py | 4 +- src/knowledge/core_object/email_object.py | 2 +- src/knowledge/core_object/image_object.py | 2 +- src/knowledge/data/writer.py | 2 +- src/knowledge/store.py | 16 +- src/knowledge/vector/chroma_store.py | 6 +- src/service/aios_shell/aios_shell.py | 65 ++++- 11 files changed, 409 insertions(+), 74 deletions(-) diff --git a/aios_shell_history.txt b/aios_shell_history.txt index b0a8786..34732fd 100644 --- a/aios_shell_history.txt +++ b/aios_shell_history.txt @@ -4,3 +4,108 @@ # 2023-09-19 16:05:27.714815 +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 + +# 2023-09-20 14:48:06.181582 ++/connect + +# 2023-09-20 14:48:22.678945 ++/connect Jarvis email + +# 2023-09-20 14:54:35.101026 ++/connect Jarvis telegram + +# 2023-09-20 14:54:55.229801 ++token abcd + +# 2023-09-20 16:22:34.229084 ++/knowledge add email + +# 2023-09-20 16:22:46.797842 ++puotosssa@live.com + +# 2023-09-20 16:22:53.622015 ++p19870626 + +# 2023-09-20 16:22:59.186145 ++imap@live.com + +# 2023-09-20 16:23:02.334411 ++993 + +# 2023-09-20 16:40:29.996329 ++/knowledge add email + +# 2023-09-20 16:40:52.860700 ++puotosssa@live.com + +# 2023-09-20 16:40:55.324994 ++fdafda + +# 2023-09-20 16:40:58.167154 ++fdafdaf + +# 2023-09-20 16:40:59.757363 ++993 + +# 2023-09-20 16:42:55.288680 ++/knowledge add email + +# 2023-09-20 16:42:56.916408 ++fdfa + +# 2023-09-20 16:42:57.965232 ++fdfds + +# 2023-09-20 16:42:59.149516 ++fdafd + +# 2023-09-20 16:43:00.949104 ++993 + +# 2023-09-21 14:11:33.500467 ++/knowledge add c:/users/tsukasa/myai/photos + +# 2023-09-21 14:14:31.488794 ++/knowledge add dir + +# 2023-09-21 14:14:49.298635 ++c:/users/tsukasa/myai/photos + +# 2023-09-21 15:27:58.550658 ++/knowledge add dir + +# 2023-09-21 15:28:10.190467 ++c:/users/tsukasa/myai/photos + +# 2023-09-21 15:32:32.431305 ++/knowledge add dir + +# 2023-09-21 15:32:46.695566 ++c:/users/tsukasa/myai/photos + +# 2023-09-21 15:33:36.391465 ++/knowledge add dir + +# 2023-09-21 15:33:47.574853 ++c:/users/tsukasa/myai/photos + +# 2023-09-21 17:44:26.308692 ++/knowledge journals + +# 2023-09-21 17:45:42.925359 ++/knowledge journal + +# 2023-09-21 18:13:08.720054 ++/knowledge query what a see is + +# 2023-09-21 18:20:09.275556 ++/knowledge journal + +# 2023-09-21 18:20:10.746056 ++/knowledge query what a see is + +# 2023-09-21 18:29:48.391223 ++/knowledge journal + +# 2023-09-21 18:29:51.406532 ++/knowledge query what a see is diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 6e6e000..7240b92 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -6,7 +6,7 @@ from .compute_kernel import ComputeKernel,ComputeTask from .compute_node import ComputeNode,LocalComputeNode from .open_ai_node import OpenAI_ComputeNode from .knowledge_base import KnowledgeBase -from .knowledge_pipeline import EmailSpider +from .knowledge_pipeline import KnowledgeEmailSource, KnowledgeDirSource, KnowledgePipline from .role import AIRole,AIRoleGroup from .workflow import Workflow from .bus import AIBus diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index 50423fe..11d6ce7 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -1,7 +1,9 @@ # define a knowledge base class import json import logging -from . import AgentPrompt, ComputeKernel, AIStorage +from .agent import AgentPrompt +from .compute_kernel import ComputeKernel +from .storage import AIStorage from knowledge import * @@ -17,7 +19,7 @@ class KnowledgeBase: def __singleton_init__(self) -> None: self.store = KnowledgeStore() - self.compute_kernel = ComputeKernel() + self.compute_kernel = ComputeKernel.get_instance() async def __embedding_document(self, document: DocumentObject): for chunk_id in document.get_chunk_list(): @@ -155,7 +157,7 @@ class KnowledgeBase: # pass async def insert_object(self, object: KnowledgeObject): - # self.__save_object(object) + self.store.get_object_store().put_object(object.calculate_id(), object.encode()) await self.__do_embedding(object) async def query_prompt(self, prompt: AgentPrompt): @@ -164,6 +166,7 @@ class KnowledgeBase: knowledge_prompt = self.prompt_from_objects(objects) logging.info(f"prompt_from_objects result: {knowledge_prompt.as_str()}") prompt.append(knowledge_prompt) + return prompt async def query_objects(self, prompt: AgentPrompt) -> [ObjectID]: results = [] diff --git a/src/aios_kernel/knowledge_pipeline.py b/src/aios_kernel/knowledge_pipeline.py index c60adad..c1f0a72 100644 --- a/src/aios_kernel/knowledge_pipeline.py +++ b/src/aios_kernel/knowledge_pipeline.py @@ -15,10 +15,10 @@ An example of a local file is as follows: │ └── dd-login-service-min.png """ - +import asyncio +import datetime +import sqlite3 import imaplib -import os -import toml import logging import mailparser import hashlib @@ -26,63 +26,139 @@ import json import base64 from bs4 import BeautifulSoup import requests +import os +import toml +from .storage import AIStorage, UserConfigItem +from .knowledge_base import KnowledgeBase, ImageObjectBuilder, ObjectID, ObjectType, DocumentObjectBuilder -class EmailSpider: +class KnowledgeJournal: + def __init__(self, source_type: str, source_id: str, item_id: str, object_id: str, timestamp=None): + # define a timestamp variable + self.timestamp = datetime.datetime.now() if timestamp is None else timestamp + self.object_id = object_id + self.source_type = source_type + self.source_id = source_id + self.item_id = item_id + + def __str__(self) -> str: + if self.source_type == "dir": + object_id = ObjectID.from_base58(self.object_id) + object_type = None + if object_id.get_object_type() == ObjectType.Image: + object_type = "image" + else: + pass + return f"Add {object_type} from {os.path.join(self.source_id, self.item_id)}" + if self.source_type == "email": + pass + + +# init sqlite3 client +class KnowledgeJournalClient: 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) + knowledge_dir = os.path.join(AIStorage.get_instance().get_myai_dir(), "knowledge") + if not os.path.exists(knowledge_dir): + os.makedirs(knowledge_dir) + self.journal_path = os.path.join(knowledge_dir, "journal.db") + + conn = sqlite3.connect(self.journal_path) + conn.execute( + '''CREATE TABLE IF NOT EXISTS journal ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + time DATETIME DEFAULT CURRENT_TIMESTAMP, + source_type TEXT, + source_id TEXT, + item_id TEXT, + object_id TEXT)''' + ) + conn.commit() + def insert(self, journal: KnowledgeJournal): + conn = sqlite3.connect(self.journal_path) + conn.execute( + "INSERT INTO journal (time, source_type, source_id, item_id, object_id) VALUES (?, ?, ?, ?, ?)", + (journal.timestamp, journal.source_type, journal.source_id, journal.item_id, journal.object_id), + ) + conn.commit() + + def latest_journal(self, source_id: str) -> KnowledgeJournal: + conn = sqlite3.connect(self.journal_path) + cursor = conn.cursor() + cursor.execute("SELECT * FROM journal WHERE source_id = ? ORDER BY id DESC LIMIT 1", (source_id,)) + result = cursor.fetchone() + if result is None: + return None + else: + (_, timestamp, source_type, sorce_id, item_id, object_id) = result + return KnowledgeJournal(source_type, sorce_id, item_id, object_id, timestamp) + + def latest_journals(self, topn) -> [KnowledgeJournal]: + conn = sqlite3.connect(self.journal_path) + cursor = conn.cursor() + cursor.execute("SELECT * FROM journal ORDER BY id DESC LIMIT ?", (topn,)) + return [KnowledgeJournal(source_type, sorce_id, item_id, object_id, timestamp) for (_, timestamp, source_type, sorce_id, item_id, object_id) in cursor.fetchall()] + + +class KnowledgeEmailSource: + def __init__(self, config:dict): + self.config = config + self.config["type"] = "email" + + def id(self): + "::".join([self.config["imap_server"], self.config["address"]]) + + @classmethod + def user_config_items(cls): + return [("address", "email address"), + ("password", "email password"), + ("imap_server", "imap server"), + ("imap_port", "imap port") + ] + + @classmethod + def local_root(cls): + user_data_dir = AIStorage.get_instance().get_myai_dir() + return os.path.abspath(f"{user_data_dir}/email") + + async def run_once(self): # 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() + await self.read_emails() def email_client(self) -> imaplib.IMAP4_SSL: - self.logger.info(f"read email config from {self.config.get('EMAIL_IMAP_SERVER')}") + logging.info(f"read email config from {self.config.get('imap_server')}") client = imaplib.IMAP4_SSL( - host=self.config.get('EMAIL_IMAP_SERVER'), - port=self.config.get('EMAIL_IMAP_PORT') + host=self.config.get('imap_server'), + port=self.config.get('imap_port') ) - client.login(self.config.get('EMAIL_ADDRESS'), self.config.get('EMAIL_PASSWORD')) + client.login(self.config.get('address'), self.config.get('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"): + async 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") + logging.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") + logging.info(f"email uid {uid} already saved") else: self.read_and_save_email(uid) - self.logger.info(f"email uid {uid} saved") + logging.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}]") + logging.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')}" + dir = f"{self.local_root()}/{self.config.get('address')}" name = f"{mail.subject}__{mail.date}" name = hashlib.md5(name.encode('utf-8')).hexdigest() return f"{dir}/{name}" @@ -91,9 +167,9 @@ class EmailSpider: 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}]") + logging.info(f"[{uid}]check email subject [{mail.subject}]") dir = self.get_local_dir_name(mail) - self.logger.info(f"check email saved {dir}") + logging.info(f"check email saved {dir}") file = f"{dir}/email.txt" if os.path.exists(file): return False @@ -116,7 +192,7 @@ class EmailSpider: image_data = image_data.encode() with open(filefullname, 'wb') as f: f.write(image_data) - self.logger.info(f"save email image {filename} success") + logging.info(f"save email image {filename} success") # save email body images(html content) def save_body_images(self, html_content: str, email_dir: str): @@ -124,7 +200,7 @@ class EmailSpider: 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') + logging.info(f'Found {len(img_urls)} images in email body') if not os.path.exists(email_dir): os.makedirs(email_dir) @@ -138,17 +214,17 @@ class EmailSpider: 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}') + logging.info(f'Downloaded {img_url} to {img_filename}') else: - self.logger.info(f'Failed to download {img_url}') + logging.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')}" + dir = f"{self.local_root()}/{self.config.get('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}") + logging.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: @@ -158,14 +234,58 @@ class EmailSpider: 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}") + logging.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") +class KnowledgeDirSource: + def __init__(self, config): + self.config = config + self.config["type"] = "dir" + + @classmethod + def user_config_items(cls): + return [("path", "local dir path")] + + def id(self): + return self.config["path"] + + def path(self): + return self.config["path"] + + async def run_once(self): + logging.debug(f"knowledge dir source {self.id()} run once") + journal_client = KnowledgeJournalClient() + latest_journal = journal_client.latest_journal(self.id()) + if latest_journal is not None: + if os.path.getmtime(self.path()) <= latest_journal.timestamp: + logging.debug(f"knowledge dir source {self.id()} ingnored for no update") + return + file_pathes = sorted(os.listdir(self.path()), key=lambda x: os.path.getctime(os.path.join(self.path(), x))) + for rel_path in file_pathes: + file_path = os.path.join(self.path(), rel_path) + timestamp = os.path.getctime(file_path) + if latest_journal is not None: + if timestamp <= latest_journal.timestamp: + continue + ext = os.path.splitext(file_path)[1].lower() + if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']: + logging.info(f"knowledge dir source {self.id()} found image file {file_path}") + image = ImageObjectBuilder({}, {}, file_path).build() + await KnowledgeBase().insert_object(image) + journal_client.insert(KnowledgeJournal("dir", self.id(), rel_path, str(image.calculate_id()), timestamp)) + if ext in ['.txt']: + logging.info(f"knowledge dir source {self.id()} found text file {file_path}") + with open(file_path, "r", encoding="utf-8") as f: + text = f.read() + document = DocumentObjectBuilder({}, {}, text).build() + await KnowledgeBase().insert_object(document) + journal_client.insert(KnowledgeJournal("dir", self.id(), rel_path, str(document.calculate_id()), timestamp)) + + -from . import AIStorage, KnowledgeBase # define singleton class knowledge pipline class KnowledgePipline: @@ -178,15 +298,71 @@ class KnowledgePipline: cls._instance.__singleton_init__() return cls._instance + + def initial(self): + config_path = self.__config_path() + logging.info(f"initial knowledge pipline from {config_path}") + if os.path.exists(config_path): + config = toml.load(self.__config_path()) + for source_config in config["sources"]: + if source_config['type'] == 'email': + self.add_email_source(KnowledgeEmailSource(source_config)) + if source_config['type'] == 'dir': + self.add_dir_source(KnowledgeDirSource(source_config)) - def __singleton_init__(self) -> None: + def __singleton_init__(self): self.knowledge_base = KnowledgeBase() + self.email_sources = dict() + self.dir_sources = dict() + self.source_queue = list() + self.run_lock = asyncio.Lock() + asyncio.create_task(self.run_loop()) + + + def save_config(self): + config = dict() + config["sources"] = [source.config for source in self.source_queue] + with open(self.__config_path(), "w") as f: + toml.dump(config, f) + @classmethod - def declare_user_config(cls): - user_config = AIStorage.get_instance().get_user_config() - user_config.add_user_config("email_spiders","email addresses to build knowledge base",True,None,"list") - user_config.add_user_config("personal_dirs", "personal directories to build knowledge base", True, None, "list") + def __config_path(cls) -> str: + user_data_dir = AIStorage.get_instance().get_myai_dir() + return os.path.abspath(f"{user_data_dir}/etc/knowledge.cfg.toml") - def add_email_address(self, ) -> None: - pass + + def add_email_source(self, source: KnowledgeEmailSource): + if self.email_sources.get(source.id()) is not None: + return "already exists" + self.email_sources[source.id()] = source + self.source_queue.append(source) + return None + + def add_dir_source(self, source: KnowledgeDirSource): + if self.dir_sources.get(source.id()) is not None: + logging.info(f"knowledge add source {source.id()} failed for already exists") + return "already exists" + logging.info(f"knowledge added source {source.id()}") + self.dir_sources[source.id()] = source + self.source_queue.append(source) + return None + + def get_latest_journals(self, topn) -> [KnowledgeJournal]: + return KnowledgeJournalClient().latest_journals(topn) + + async def run_loop(self): + while True: + await self.run_once() + await asyncio.sleep(5) + + async def run_once(self): + logging.info(f"knowledge pipeline started") + # sources = list() + # async with self.run_lock: + # for source in self.source_queue: + # sources.append(source) + # for source in sources: + # await source.run_once() + for source in self.source_queue: + await source.run_once() diff --git a/src/knowledge/core_object/document_object.py b/src/knowledge/core_object/document_object.py index 7210f16..672b4a3 100644 --- a/src/knowledge/core_object/document_object.py +++ b/src/knowledge/core_object/document_object.py @@ -49,7 +49,7 @@ class DocumentObjectBuilder: self.text = text return self - def build(self, relation_store: ObjectRelationStore) -> DocumentObject: + def build(self) -> DocumentObject: chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text( self.text, 1024 * 4, @@ -60,6 +60,6 @@ class DocumentObjectBuilder: # Add relation to store for chunk_id in chunk_list.chunk_list: - relation_store.add_relation(chunk_id, doc_id) + KnowledgeStore().get_relation_store().add_relation(chunk_id, doc_id) return doc diff --git a/src/knowledge/core_object/email_object.py b/src/knowledge/core_object/email_object.py index c86c3a2..37531ff 100644 --- a/src/knowledge/core_object/email_object.py +++ b/src/knowledge/core_object/email_object.py @@ -94,7 +94,7 @@ class EmailObjectBuilder: with open(content_file, "r", encoding="utf-8") as f: text = f.read() - document = DocumentObjectBuilder({}, {}, text).build(relation_store=relation) + document = DocumentObjectBuilder({}, {}, text).build() document_id = document.calculate_id() store.put_object(document_id, document.encode()) documents = {"email.txt": document_id} diff --git a/src/knowledge/core_object/image_object.py b/src/knowledge/core_object/image_object.py index 30b0e8a..2c02ad6 100644 --- a/src/knowledge/core_object/image_object.py +++ b/src/knowledge/core_object/image_object.py @@ -52,7 +52,7 @@ def get_exif_data(image_path: str): return { TAGS.get(key): exif_data[key] for key in exif_data.keys() - if key in TAGS and isinstance(exif_data[key], (bytes, str)) + if key in TAGS and isinstance(exif_data[key], str) } else: return {} diff --git a/src/knowledge/data/writer.py b/src/knowledge/data/writer.py index 3ac0ed1..ad6c520 100644 --- a/src/knowledge/data/writer.py +++ b/src/knowledge/data/writer.py @@ -46,7 +46,7 @@ class ChunkListWriter: ) file_hash = HashValue(hash_obj.digest()) - print(f"calc file hash: {file_path}, {file_hash}") + # print(f"calc file hash: {file_path}, {file_hash}") return ChunkList(chunk_list, file_hash) diff --git a/src/knowledge/store.py b/src/knowledge/store.py index 03e48d0..56ec64b 100644 --- a/src/knowledge/store.py +++ b/src/knowledge/store.py @@ -4,7 +4,7 @@ from .object import ObjectStore, ObjectRelationStore from .data import ChunkStore, ChunkTracker, ChunkListWriter, ChunkReader from .vector import ChromaVectorStore, VectorBase import logging - +import aios_kernel # KnowledgeStore class, which aggregates ChunkStore, ChunkTracker, and ObjectStore, and is a global singleton that makes it easy to use these three built-in store examples class KnowledgeStore: @@ -13,16 +13,12 @@ class KnowledgeStore: def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) - directory = os.path.join( - os.path.dirname(__file__), "../../rootfs/data/" - ) - directory = os.path.normpath(directory) - print(directory) + knowledge_dir = aios_kernel.storage.AIStorage().get_myai_dir() / "knowledge" - if not os.path.exists(directory): - os.makedirs(directory) + if not os.path.exists(knowledge_dir): + os.makedirs(knowledge_dir) - cls._instance.__singleton_init__(directory) + cls._instance.__singleton_init__(knowledge_dir) return cls._instance @@ -64,5 +60,5 @@ class KnowledgeStore: def get_vector_store(self, model_name: str) -> VectorBase: if model_name not in self.vector_store: - self.vector_store[model_name] = ChromaVectorStore(model_name) + self.vector_store[model_name] = ChromaVectorStore(self.root, model_name) return self.vector_store[model_name] diff --git a/src/knowledge/vector/chroma_store.py b/src/knowledge/vector/chroma_store.py index 03006ae..260a8e8 100644 --- a/src/knowledge/vector/chroma_store.py +++ b/src/knowledge/vector/chroma_store.py @@ -6,16 +6,14 @@ import os class ChromaVectorStore(VectorBase): - def __init__(self, model_name: str) -> None: + def __init__(self, root_dir, model_name: str) -> None: super().__init__(model_name) logging.info( "will init chroma vector store, model={}".format(model_name) ) - directory = os.path.join( - os.path.dirname(__file__), "../../../rootfs/data/vector" - ) + directory = os.path.join(root_dir, "vector") logging.info("will use vector store: {}".format(directory)) client = chromadb.PersistentClient( diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index f5b9dec..3577d28 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -22,8 +22,11 @@ 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 + import proxy +from aios_kernel import * + + sys.path.append(directory + '/../../component/') from agent_manager import AgentManager @@ -115,7 +118,8 @@ class AIOS_Shell: await AgentTunnel.load_all_tunnels_from_config(tunnel_config) except Exception as e: logger.warning(f"load tunnels config from {tunnels_config_path} failed!") - + + KnowledgePipline.get_instance().initial() return True @@ -163,8 +167,7 @@ class AIOS_Shell: tunnel_config[key] = user_input - return tunnel_config - + return tunnel_config async def append_tunnel_config(self,tunnel_config): user_data_dir = AIStorage.get_instance().get_myai_dir() @@ -179,6 +182,55 @@ class AIOS_Shell: except Exception as e: logger.warning(f"load tunnels config from {tunnels_config_path} failed!") + async def handle_knowledge_commands(self, args): + show_text = FormattedText([("class:title", "sub command not support!\n" + "/knowledge add email | dir\n" + "/knowledge journal [$topn]\n" + "/knowledge query $query\n")]) + if len(args) < 1: + return show_text + sub_cmd = args[0] + if sub_cmd == "add": + if len(args) < 2: + return show_text + if args[1] == "email": + config = dict() + for key, item in KnowledgeEmailSource.user_config_items(): + user_input = await try_get_input(f"{key} : {item}") + if user_input is None: + return show_text + config[key] = user_input + error = KnowledgePipline.get_instance().add_email_source(KnowledgeEmailSource(config)) + if error is not None: + return FormattedText([("class:title", f"/knowledge add email failed {error}\n")]) + else: + KnowledgePipline.get_instance().save_config() + if args[1] == "dir": + config = dict() + for key, item in KnowledgeDirSource.user_config_items(): + user_input = await try_get_input(f"{key} : {item}") + if user_input is None: + return show_text + config[key] = user_input + error = KnowledgePipline.get_instance().add_dir_source(KnowledgeDirSource(config)) + if error is not None: + return FormattedText([("class:title", f"/knowledge add dir failed {error}\n")]) + else: + KnowledgePipline.get_instance().save_config() + else: + return show_text + if sub_cmd == "journal": + topn = 10 if len(args) == 1 else int(args[1]) + journals = [str(journal) for journal in KnowledgePipline.get_instance().get_latest_journals(topn)] + print_formatted_text("\r\n".join(journals)) + if sub_cmd == "query": + if len(args) < 2: + return show_text + prompt = AgentPrompt() + prompt.messages.append({"role": "user", "content":" ".join(args[1:])}) + result = await KnowledgeBase().query_prompt(prompt) + print_formatted_text(result.as_str()) + async def call_func(self,func_name, args): match func_name: case 'send': @@ -221,6 +273,8 @@ class AIOS_Shell: show_text = FormattedText([("class:title", f"connect to {tunnel_target} success!")]) return show_text + case 'knowledge': + return await self.handle_knowledge_commands(args) case 'open': if len(args) >= 1: target_id = args[0] @@ -400,6 +454,9 @@ async def main(): '/history $num $offset', '/login $username', '/connect $target', + '/knowledge add email | dir', + '/knowledge journal [$topn]', + '/knowledge query $query' '/set_config $key', '/list_config', '/show', From 03036c7571eebb891619b4fdbf72367a359e6710 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 21 Sep 2023 11:46:37 -0700 Subject: [PATCH 097/177] fix bug --- rootfs/agents/Jarvis/agent.toml | 2 +- src/aios_kernel/tg_tunnel.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 575dc87..1c56e5f 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -21,7 +21,7 @@ David,私人画家 消息内容 ``` 2.你可以访问我的Calender,查看我的日程安排。如果你在处理信息的过程中需要修改我的日程安排,请直接用合适的方法修改。 -3.你可以放我的个人信息库,当你处理我的信息时,如果需要用到我的个人信息,请先用合适的方法进行查询,然后再基于查询的结果进行进一步处理后再讲结果发给我。 +3.你可以访问我的个人信息库,当你处理我的信息时,如果需要用到我的个人信息,请先用合适的方法进行查询,然后再基于查询的结果进行进一步处理后再将结果发给我。 4.你能根据我的需要对系统进行配置,但在修改任何配置前,请先和我确认。 5.不符合上述规则的信息,请尽力处理。 """ diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index b08bc6d..590ef5c 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -142,8 +142,10 @@ class TelegramTunnel(AgentTunnel): photo_file = open(resp_msg.body,"rb") if photo_file: await update.message.reply_photo(resp_msg.body) + photo_file.close() else: await update.message.reply_text(resp_msg.body) + else: await update.message.reply_text(resp_msg.body) From f7a47a06ec85d706a8f0d30cff72f17169604d7d Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 21 Sep 2023 13:54:10 -0700 Subject: [PATCH 098/177] Jarvis can query user knowlege base! --- aios_shell_history.txt | 111 -------------------------- rootfs/agents/Jarvis/agent.toml | 1 + src/aios_kernel/agent.py | 27 +++++-- src/aios_kernel/knowledge_base.py | 8 +- src/aios_kernel/knowledge_pipeline.py | 24 ++++-- src/requirements.txt | 3 +- 6 files changed, 47 insertions(+), 127 deletions(-) delete mode 100644 aios_shell_history.txt diff --git a/aios_shell_history.txt b/aios_shell_history.txt deleted file mode 100644 index 34732fd..0000000 --- a/aios_shell_history.txt +++ /dev/null @@ -1,111 +0,0 @@ - -# 2023-09-19 16:04:55.011656 -+tsukasa - -# 2023-09-19 16:05:27.714815 -+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 - -# 2023-09-20 14:48:06.181582 -+/connect - -# 2023-09-20 14:48:22.678945 -+/connect Jarvis email - -# 2023-09-20 14:54:35.101026 -+/connect Jarvis telegram - -# 2023-09-20 14:54:55.229801 -+token abcd - -# 2023-09-20 16:22:34.229084 -+/knowledge add email - -# 2023-09-20 16:22:46.797842 -+puotosssa@live.com - -# 2023-09-20 16:22:53.622015 -+p19870626 - -# 2023-09-20 16:22:59.186145 -+imap@live.com - -# 2023-09-20 16:23:02.334411 -+993 - -# 2023-09-20 16:40:29.996329 -+/knowledge add email - -# 2023-09-20 16:40:52.860700 -+puotosssa@live.com - -# 2023-09-20 16:40:55.324994 -+fdafda - -# 2023-09-20 16:40:58.167154 -+fdafdaf - -# 2023-09-20 16:40:59.757363 -+993 - -# 2023-09-20 16:42:55.288680 -+/knowledge add email - -# 2023-09-20 16:42:56.916408 -+fdfa - -# 2023-09-20 16:42:57.965232 -+fdfds - -# 2023-09-20 16:42:59.149516 -+fdafd - -# 2023-09-20 16:43:00.949104 -+993 - -# 2023-09-21 14:11:33.500467 -+/knowledge add c:/users/tsukasa/myai/photos - -# 2023-09-21 14:14:31.488794 -+/knowledge add dir - -# 2023-09-21 14:14:49.298635 -+c:/users/tsukasa/myai/photos - -# 2023-09-21 15:27:58.550658 -+/knowledge add dir - -# 2023-09-21 15:28:10.190467 -+c:/users/tsukasa/myai/photos - -# 2023-09-21 15:32:32.431305 -+/knowledge add dir - -# 2023-09-21 15:32:46.695566 -+c:/users/tsukasa/myai/photos - -# 2023-09-21 15:33:36.391465 -+/knowledge add dir - -# 2023-09-21 15:33:47.574853 -+c:/users/tsukasa/myai/photos - -# 2023-09-21 17:44:26.308692 -+/knowledge journals - -# 2023-09-21 17:45:42.925359 -+/knowledge journal - -# 2023-09-21 18:13:08.720054 -+/knowledge query what a see is - -# 2023-09-21 18:20:09.275556 -+/knowledge journal - -# 2023-09-21 18:20:10.746056 -+/knowledge query what a see is - -# 2023-09-21 18:29:48.391223 -+/knowledge journal - -# 2023-09-21 18:29:51.406532 -+/knowledge query what a see is diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 1c56e5f..2da373c 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -3,6 +3,7 @@ fullname = "Jarvis" llm_model_name = "gpt-3.5-turbo-16k-0613" max_token_size = 16000 #enable_function =["add_event"] +enable_kb = "true" [[prompt]] role = "system" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 18eeeec..4742003 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -109,6 +109,7 @@ class AIAgent: self.fullname:str = None self.powerby = None self.enable = True + self.enable_kb = False self.chat_db = None self.unread_msg = Queue() # msg from other agent @@ -154,6 +155,8 @@ class AIAgent: self.max_token_size = config["max_token_size"] if config.get("enable_function") is not None: self.enable_function_list = config["enable_function"] + if config.get("enable_kb") is not None: + self.enable_kb = bool(config["enable_kb"]) return True @@ -300,9 +303,18 @@ class AIAgent: old_content = msg.get("content") msg["content"] = old_content.format_map(self.owner_env) + async def _get_knowlege_prompt(self,input_msg:AgentPrompt) -> AgentPrompt: + if self.enable_kb is False: + return None + + from .knowledge_base import KnowledgeBase + return await KnowledgeBase().query_prompt(input_msg) + async def _process_msg(self,msg:AgentMsg) -> AgentMsg: from .compute_kernel import ComputeKernel from .bus import AIBus + + session_topic = msg.get_sender() + "#" + msg.topic chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db) @@ -311,22 +323,27 @@ class AIAgent: 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 - + + msg_prompt = AgentPrompt() + msg_prompt.messages = [{"role":"user","content":msg.body}] + prompt = AgentPrompt() prompt.append(await self._get_agent_prompt()) + self._format_msg_by_env_value(prompt) + inner_functions,function_token_len = self._get_inner_functions() - # prompt.append(self._get_knowlege_prompt(the_role.get_name())) + system_prompt_len = prompt.get_prompt_token_len() input_len = len(msg.body) history_prmpt,history_token_len = await self._get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len) prompt.append(history_prmpt) # chat context - msg_prompt = AgentPrompt() - msg_prompt.messages = [{"role":"user","content":msg.body}] + kb_prompt = await self._get_knowlege_prompt(msg_prompt) + prompt.append(kb_prompt) prompt.append(msg_prompt) - self._format_msg_by_env_value(prompt) + logger.debug(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len},totoal prompt:{system_prompt_len + function_token_len + history_token_len} ") task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) final_result = task_result.result_str diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py index 11d6ce7..d2debd8 100644 --- a/src/aios_kernel/knowledge_base.py +++ b/src/aios_kernel/knowledge_base.py @@ -165,8 +165,8 @@ class KnowledgeBase: objects = await self.query_objects(prompt) knowledge_prompt = self.prompt_from_objects(objects) logging.info(f"prompt_from_objects result: {knowledge_prompt.as_str()}") - prompt.append(knowledge_prompt) - return prompt + + return knowledge_prompt async def query_objects(self, prompt: AgentPrompt) -> [ObjectID]: results = [] @@ -204,7 +204,7 @@ class KnowledgeBase: else: results[str(root_object_id)] = [root_object_id, object_id] - content = "I found the following contents described with json format:\n" + content = "*** I have provided the following known information for your reference with json format:\n" result_desc = [] for result in results.values(): # first element in result is the root object @@ -239,7 +239,7 @@ class KnowledgeBase: content += ".\n" prompt = AgentPrompt() - prompt.messages.append({"role": "knowledge", "content": content}) + prompt.messages.append({"role": "user", "content": content}) return prompt diff --git a/src/aios_kernel/knowledge_pipeline.py b/src/aios_kernel/knowledge_pipeline.py index c1f0a72..d1ed109 100644 --- a/src/aios_kernel/knowledge_pipeline.py +++ b/src/aios_kernel/knowledge_pipeline.py @@ -24,6 +24,9 @@ import mailparser import hashlib import json import base64 +import chardet +import aiofiles + from bs4 import BeautifulSoup import requests import os @@ -254,7 +257,16 @@ class KnowledgeDirSource: def path(self): return self.config["path"] - + + @staticmethod + async def read_txt_file(file_path:str)->str: + cur_encode = "utf-8" + async with aiofiles.open(file_path,'rb') as f: + cur_encode = chardet.detect(await f.read())['encoding'] + + async with aiofiles.open(file_path,'r',encoding=cur_encode) as f: + return await f.read() + async def run_once(self): logging.debug(f"knowledge dir source {self.id()} run once") journal_client = KnowledgeJournalClient() @@ -278,11 +290,11 @@ class KnowledgeDirSource: journal_client.insert(KnowledgeJournal("dir", self.id(), rel_path, str(image.calculate_id()), timestamp)) if ext in ['.txt']: logging.info(f"knowledge dir source {self.id()} found text file {file_path}") - with open(file_path, "r", encoding="utf-8") as f: - text = f.read() - document = DocumentObjectBuilder({}, {}, text).build() - await KnowledgeBase().insert_object(document) - journal_client.insert(KnowledgeJournal("dir", self.id(), rel_path, str(document.calculate_id()), timestamp)) + text = await self.read_txt_file(file_path) + + document = DocumentObjectBuilder({}, {}, text).build() + await KnowledgeBase().insert_object(document) + journal_client.insert(KnowledgeJournal("dir", self.id(), rel_path, str(document.calculate_id()), timestamp)) diff --git a/src/requirements.txt b/src/requirements.txt index 0c88c8a..753ce51 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -80,4 +80,5 @@ starlette==0.27.0 sympy==1.12 telegram==0.0.1 tokenizers==0.14.0 -toml==0.10.0 \ No newline at end of file +toml==0.10.0 +chardet \ No newline at end of file From 270debef672fbfdf05fd831c85f9f6b2b61e3dfd Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 21 Sep 2023 20:51:21 -0700 Subject: [PATCH 099/177] Add feature: Contact manager --- rootfs/agents/Jarvis/agent.toml | 7 +- rootfs/agents/linux_admin/agent.toml | 12 +++ src/aios_kernel/__init__.py | 1 + src/aios_kernel/agent.py | 67 +++++++++++-- src/aios_kernel/contact_manager.py | 137 +++++++++++++++++++++++---- src/aios_kernel/tg_tunnel.py | 34 +++++-- src/aios_kernel/workflow.py | 10 +- src/service/aios_shell/aios_shell.py | 10 +- test/test_contac_manager.py | 64 +++++++++++++ 9 files changed, 300 insertions(+), 42 deletions(-) create mode 100644 rootfs/agents/linux_admin/agent.toml create mode 100644 test/test_contac_manager.py diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index 2da373c..ca1c7b0 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -3,7 +3,11 @@ fullname = "Jarvis" llm_model_name = "gpt-3.5-turbo-16k-0613" max_token_size = 16000 #enable_function =["add_event"] -enable_kb = "true" +#enable_kb = "true" +enable_timestamp = "true" +owner_prompt = "我是你的主人{name}" +contact_prompt = "我是你的朋友{name}" +guest_prompt = "xxx" [[prompt]] role = "system" @@ -15,6 +19,7 @@ Tracy,私人英语老师 David,私人画家 *** +你看到的信息里有的有时会带上时间标签,这是为了让你更好的理解时间。你回复的信息不用创建这个时间标签。 你在收到我的信息后,按如下规则处理 1. 如果你认为团队里有人更适合处理该信息,用下面方法转发消息给他们处理 ``` diff --git a/rootfs/agents/linux_admin/agent.toml b/rootfs/agents/linux_admin/agent.toml new file mode 100644 index 0000000..7988669 --- /dev/null +++ b/rootfs/agents/linux_admin/agent.toml @@ -0,0 +1,12 @@ +instance_id = "linux_admin" +fullname = "linux_admin" + +[[prompt]] +role = "system" +content = """你的名字是linux_admin,是非常资深的linux系统管理员,我理解一些linux,但对linux的sh命令记得不太清楚了。 +我给你的输入有两种 +1. 标准的linux bash命令,如果你认为这些命令在当前系统上是正确的,那么直接返回这些命令,我会去执行 +2. 如果你为这些命令需要调整后才能执行,先把你修正后的命令告诉我,我确认后再执行 +3. 如果我给你的信息不是linux bash操作,而是一些需求。你可以尝试理解后,给出一组实现这些需求的命令,我确认后再执行 +4. 其它信息,请站在你专业角度尽力执行。 +""" diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 7240b92..79d4456 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -18,6 +18,7 @@ from .tunnel import AgentTunnel from .tg_tunnel import TelegramTunnel from .email_tunnel import EmailTunnel from .storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem +from .contact_manager import ContactManager,Contact,FamilyMember AIOS_Version = "0.5.1, build 2023-9-17" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 4742003..cc79637 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -7,18 +7,22 @@ import uuid import time import json import shlex +import datetime from .agent_message import AgentMsg, AgentMsgStatus, AgentMsgType,FunctionItem,LLMResult from .chatsession import AIChatSession from .compute_task import ComputeTaskResult from .ai_function import AIFunction from .environment import Environment +from .contact_manager import ContactManager,Contact,FamilyMember logger = logging.getLogger(__name__) class AgentPrompt: - def __init__(self) -> None: + def __init__(self,prompt_str = None) -> None: self.messages = [] + if prompt_str: + self.messages.append({"role":"user","content":prompt_str}) self.system_message = None def as_str(self)->str: @@ -110,6 +114,10 @@ class AIAgent: self.powerby = None self.enable = True self.enable_kb = False + self.enable_timestamp = False + self.guest_prompt_str = None + self.owner_promp_str = None + self.contact_prompt_str = None self.chat_db = None self.unread_msg = Queue() # msg from other agent @@ -145,6 +153,16 @@ class AIAgent: self.prompt = AgentPrompt() self.prompt.load_from_config(config["prompt"]) + if config.get("guest_prompt") is not None: + self.guest_prompt_str = config["guest_prompt"] + + if config.get("owner_prompt") is not None: + self.owner_promp_str = config["owner_prompt"] + + if config.get("contact_prompt") is not None: + self.contact_prompt_str = config["contact_prompt"] + + if config.get("powerby") is not None: self.powerby = config["powerby"] if config.get("template_id") is not None: @@ -157,6 +175,8 @@ class AIAgent: self.enable_function_list = config["enable_function"] if config.get("enable_kb") is not None: self.enable_kb = bool(config["enable_kb"]) + if config.get("enable_timestamp") is not None: + self.enable_timestamp = bool(config["enable_timestamp"]) return True @@ -230,6 +250,32 @@ class AIAgent: return r + def _get_remote_user_prompt(self,remote_user:str) -> AgentPrompt: + cm = ContactManager.get_instance() + contact = cm.find_contact_by_name(remote_user) + if contact is None: + #create guest prompt + if self.guest_prompt_str is not None: + prompt = AgentPrompt() + prompt.system_message = {"role":"system","content":self.guest_prompt_str} + return prompt + return None + else: + if contact.is_family_member: + if self.owner_promp_str is not None: + real_str = self.owner_promp_str.format_map(contact.to_dict()) + prompt = AgentPrompt() + prompt.system_message = {"role":"system","content":real_str} + return prompt + else: + if self.contact_prompt_str is not None: + real_str = self.contact_prompt_str.format_map(contact.to_dict()) + prompt = AgentPrompt() + prompt.system_message = {"role":"system","content":real_str} + return prompt + + return None + def _get_inner_functions(self) -> dict: if self.owner_env is None: return None @@ -259,7 +305,7 @@ class AIAgent: async def _execute_func(self,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg,stack_limit = 5) -> str: from .compute_kernel import ComputeKernel - + func_name = inenr_func_call_node.get("name") arguments = json.loads(inenr_func_call_node.get("arguments")) logger.info(f"llm execute inner func:{func_name} ({json.dumps(arguments)})") @@ -314,8 +360,6 @@ class AIAgent: from .compute_kernel import ComputeKernel from .bus import AIBus - - session_topic = msg.get_sender() + "#" + msg.topic chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db) if msg.mentions is not None: @@ -330,6 +374,7 @@ class AIAgent: prompt = AgentPrompt() prompt.append(await self._get_agent_prompt()) self._format_msg_by_env_value(prompt) + prompt.append(self._get_remote_user_prompt(msg.sender)) inner_functions,function_token_len = self._get_inner_functions() @@ -406,11 +451,21 @@ class AIAgent: read_history_msg = 0 for msg in reversed(messages): read_history_msg += 1 + dt = datetime.datetime.fromtimestamp(float(msg.create_time)) + formatted_time = dt.strftime('%m-%d %H:%M:%S') + if msg.sender == self.agent_id: - result_prompt.messages.append({"role":"assistant","content":msg.body}) + + if self.enable_timestamp: + result_prompt.messages.append({"role":"assistant","content":f"(create on {formatted_time}) {msg.body} "}) + else: + result_prompt.messages.append({"role":"assistant","content":msg.body}) else: - result_prompt.messages.append({"role":"user","content":msg.body}) + if self.enable_timestamp: + result_prompt.messages.append({"role":"user","content":f"(create on {formatted_time}) {msg.body} "}) + else: + result_prompt.messages.append({"role":"user","content":msg.body}) history_len -= len(msg.body) result_token_len += len(msg.body) diff --git a/src/aios_kernel/contact_manager.py b/src/aios_kernel/contact_manager.py index cdc050a..ca5b7c5 100644 --- a/src/aios_kernel/contact_manager.py +++ b/src/aios_kernel/contact_manager.py @@ -1,34 +1,137 @@ from typing import List +import toml class Contact: - def __init__(self,name:str) -> None: + def __init__(self, name, phone=None, email=None, telegram=None,added_by=None, tags=[], notes=""): self.name = name - self.tags = [] + self.phone = phone + self.email = email + self.telegram = telegram + self.added_by = added_by + self.tags = tags + self.notes = notes - def is_zone_owner(self,zone_id=None) -> bool: - return True + def to_dict(self): + return { + "name": self.name, + "phone": self.phone, + "email": self.email, + "telegram" : self.telegram, - def get_tags(self)->List[str]: - return self.tags + "added_by": self.added_by, + "tags": self.tags, + "notes": self.notes + } - def get_name(self)->str: - return self.name + @classmethod + def from_dict(cls, data): + return Contact(data.get("name"), data.get("phone"), data.get("email"), data.get("telegram"),data.get("added_by"), data.get("tags"), data.get("notes")) +class FamilyMember(Contact): + def __init__(self, name, relationship,phone=None, email=None,telegram=None): + super().__init__(name, phone, email, telegram) + self.name = name + self.relationship = relationship + self.is_family_member = True + + def to_dict(self): + result = super().to_dict() + result["relationship"] = self.relationship + return result + + @classmethod + def from_dict(cls, data): + return FamilyMember(data.get("name"),data.get("relationship"),data.get("phone"), data.get("email"),data.get("telegram")) class ContactManager: _instance = None @classmethod - def get_instance(cls): + def get_instance(cls,filename=None) -> "ContactManager": if cls._instance is None: - cls._instance = ContactManager() + cls._instance = ContactManager(filename) return cls._instance - def __init__(self) -> None: - self.contacts = {} - self.contacts["liuzhicong"] = Contact("liuzhicong") + def __init__(self, filename="contacts.toml"): + self.filename = filename + self.contacts = [] + self.family_members = [] - #def get_by_addr(self,addr:str) -> Contact: - # pass + self.is_auto_create_contact_from_telegram = True + + def load_data(self): + try: + with open(self.filename, "r") as f: + config = toml.load(f) + return self.load_from_config(config) + except FileNotFoundError: + return {} + + def load_from_config(self,config_data:dict): + self.contacts = [Contact.from_dict(item) for item in config_data.get("contacts", [])] + self.family_members = [FamilyMember.from_dict(item) for item in config_data.get("family_members", [])] + + def save_data(self): + data = { + "contacts": [contact.to_dict() for contact in self.contacts], + "family_members": [member.to_dict() for member in self.family_members] + } + with open(self.filename, "w") as f: + toml.dump(data, f) - def get_by_name(self,name:str) -> Contact: - return self.contacts.get(name) \ No newline at end of file + def add_contact(self, name:str, new_contact:Contact): + assert name == new_contact.name + self.contacts.append(new_contact) + self.save_data() + + def remove_contact(self, name:str): + self.contacts = [contact for contact in self.contacts if contact.name != name] + self.save_data() + + def find_contact_by_name(self, name:str): + for contact in self.contacts: + if contact.name == name: + return contact + + for member in self.family_members: + if member.name == name: + return member + return None + + def find_contact_by_telegram(self, telegram:str): + for contact in self.contacts: + if contact.telegram == telegram: + return contact + for member in self.family_members: + if member.telegram == telegram: + return member + return None + + def find_contact_by_email(self, email:str): + for contact in self.contacts: + if contact.email == email: + return contact + for member in self.family_members: + if member.email == email: + return member + return None + + def find_contact_by_phone(self, phone:str): + for contact in self.contacts: + if contact.phone == phone: + return contact + for member in self.family_members: + if member.phone == phone: + return member + return None + + + def add_family_member(self, name, new_member:FamilyMember): + assert name == new_member.name + self.family_members.append(new_member) + self.save_data() + + def list_contacts(self): + return self.contacts + + def list_family_members(self): + return self.family_members diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index 590ef5c..edc7b93 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -14,9 +14,10 @@ from telegram.ext import Updater from telegram.error import Forbidden, NetworkError from .tunnel import AgentTunnel -from .contact_manager import ContactManager +from .contact_manager import ContactManager,Contact,FamilyMember from .agent_message import AgentMsg + logger = logging.getLogger(__name__) class TelegramTunnel(AgentTunnel): @@ -116,16 +117,31 @@ class TelegramTunnel(AgentTunnel): async def on_message(self, bot:Bot, update: Update) -> None: - cm = ContactManager.get_instance() + if update.effective_user.is_bot: + logger.warning(f"ignore message from telegram bot {update.effective_user.id}") + return None + + cm : ContactManager = 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!") + contact : Contact = cm.find_contact_by_telegram(update.effective_user.username) + if contact is None: + contact = cm.find_contact_by_telegram(str(update.effective_user.id)) + if contact is not None: + reomte_user_name = contact.name + else: + if cm.is_auto_create_contact_from_telegram: + contact_name = update.effective_user.first_name + if update.effective_user.last_name is not None: + contact_name += " " + update.effective_user.last_name + + contact = Contact(contact_name) + contact.telegram = update.effective_user.username if update.effective_user.username is not None else str(update.effective_user.id) + contact.added_by = self.target_id + cm.add_contact(contact.name, contact) + reomte_user_name = contact.name + + # create gust account? 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) diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 998787c..97893b6 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -390,9 +390,9 @@ class Workflow: 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: + async def _role_execute_func(self,the_role:AIRole,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg,stack_limit = 5) -> str: from .compute_kernel import ComputeKernel - + func_name = inenr_func_call_node.get("name") arguments = json.loads(inenr_func_call_node.get("arguments")) @@ -413,10 +413,10 @@ class Workflow: 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 stack_limit > 0: + 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) + return await self._role_execute_func(the_role,inner_func_call_node,prompt,org_msg,stack_limit-1) else: return task_result.result_str diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 3577d28..984bb08 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -27,7 +27,6 @@ import proxy from aios_kernel import * - sys.path.append(directory + '/../../component/') from agent_manager import AgentManager from workflow_manager import WorkflowManager @@ -87,7 +86,8 @@ class AIOS_Shell: cal_env = CalenderEnvironment("calender") await cal_env.start() Environment.set_env_by_id("calender",cal_env) - + + await AgentManager.get_instance().initial() await WorkflowManager.get_instance().initial() @@ -110,6 +110,10 @@ class AIOS_Shell: EmailTunnel.register_to_loader() user_data_dir = AIStorage.get_instance().get_myai_dir() + contact_config_path =os.path.abspath(f"{user_data_dir}/contacts.toml") + cm = ContactManager.get_instance(contact_config_path) + cm.load_data() + tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml") tunnel_config = None try: @@ -139,8 +143,6 @@ class AIOS_Shell: async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg: pass - - async def get_tunnel_config_from_input(self,tunnel_target,tunnel_type): tunnel_config = {} tunnel_config["tunnel_id"] = f"{tunnel_type}_2_{tunnel_target}" diff --git a/test/test_contac_manager.py b/test/test_contac_manager.py new file mode 100644 index 0000000..4272b16 --- /dev/null +++ b/test/test_contac_manager.py @@ -0,0 +1,64 @@ +import unittest +import os +import toml +import sys + +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../src') +from aios_kernel import ContactManager, Contact, FamilyMember + +class TestContactManager(unittest.TestCase): + + def setUp(self): + self.manager = ContactManager(filename="test_contacts.toml") + self.manager.load_data() + + def tearDown(self): + if os.path.exists("test_contacts.toml"): + os.remove("test_contacts.toml") + + def test_add_family_member(self): + new_member = FamilyMember("Alice", "123-456-7890", "sdfsd","alice@example.com") + self.manager.add_family_member("Alice", new_member) + members = self.manager.list_family_members() + self.assertEqual(len(members), 1) + self.assertEqual(members[0].name, "Alice") + + def test_add_contact(self): + new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.") + self.manager.add_contact("Bob", new_contact) + contacts = self.manager.list_contacts() + self.assertEqual(len(contacts), 1) + self.assertEqual(contacts[0].name, "Bob") + self.assertEqual(contacts[0].added_by, "Alice") + + def test_remove_contact(self): + new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.") + self.manager.add_contact("Bob", new_contact) + self.manager.remove_contact("Bob") + contacts = self.manager.list_contacts() + self.assertEqual(len(contacts), 0) + + def test_find_contact_by_name(self): + new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.") + self.manager.add_contact("Bob", new_contact) + contact = self.manager.find_contact_by_name("Bob") + self.assertIsNotNone(contact) + self.assertEqual(contact.name, "Bob") + + def test_find_contact_by_email(self): + new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.") + self.manager.add_contact("Bob", new_contact) + contact = self.manager.find_contact_by_email("bob@example.com") + self.assertIsNotNone(contact) + self.assertEqual(contact.email, "bob@example.com") + + def test_find_contact_by_phone(self): + new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.") + self.manager.add_contact("Bob", new_contact) + contact = self.manager.find_contact_by_phone("987-654-3210") + self.assertIsNotNone(contact) + self.assertEqual(contact.phone, "987-654-3210") + +if __name__ == '__main__': + unittest.main() From 4e45130140a3908406dff3575887bf67ba874d2e Mon Sep 17 00:00:00 2001 From: wugren Date: Thu, 21 Sep 2023 15:52:56 +0800 Subject: [PATCH 100/177] story maker --- rootfs/agents/fairy_tale_writer/agent.toml | 7 ++ rootfs/agents/studio_director/agent.toml | 7 ++ rootfs/workflows/story_maker/workflow.toml | 37 +++++++ src/aios_kernel/__init__.py | 1 + src/aios_kernel/chatsession.py | 27 ++--- src/aios_kernel/compute_kernel.py | 50 +++++++-- src/aios_kernel/google_text_to_speech_node.py | 81 ++++++++++++-- src/aios_kernel/text_to_speech_function.py | 101 ++++++++++++++++++ src/aios_kernel/workflow_env.py | 45 ++++---- src/service/aios_shell/aios_shell.py | 73 +++++++------ 10 files changed, 341 insertions(+), 88 deletions(-) create mode 100644 rootfs/agents/fairy_tale_writer/agent.toml create mode 100644 rootfs/agents/studio_director/agent.toml create mode 100644 rootfs/workflows/story_maker/workflow.toml create mode 100644 src/aios_kernel/text_to_speech_function.py diff --git a/rootfs/agents/fairy_tale_writer/agent.toml b/rootfs/agents/fairy_tale_writer/agent.toml new file mode 100644 index 0000000..1f2256a --- /dev/null +++ b/rootfs/agents/fairy_tale_writer/agent.toml @@ -0,0 +1,7 @@ +instance_id = "fairy_tale_writer" +fullname = "tracy wang" +llm_model_name = "gpt-3.5-turbo-16k-0613" + +[[prompt]] +role = "system" +content = "你是一个童话做作家,能够写出各种有趣的童话。" diff --git a/rootfs/agents/studio_director/agent.toml b/rootfs/agents/studio_director/agent.toml new file mode 100644 index 0000000..eb61bca --- /dev/null +++ b/rootfs/agents/studio_director/agent.toml @@ -0,0 +1,7 @@ +instance_id = "studio_director" +fullname = "tracy wang" +llm_model_name = "gpt-3.5-turbo-16k-0613" + +[[prompt]] +role = "system" +content = "你是一个演播导演,请将下面故事改编成朗读剧本,提取旁白和角色台词,每个角色需要有性别、年龄、以及每句台词的语气。并调用text_to_speech function生成音频数据。" diff --git a/rootfs/workflows/story_maker/workflow.toml b/rootfs/workflows/story_maker/workflow.toml new file mode 100644 index 0000000..30a26d6 --- /dev/null +++ b/rootfs/workflows/story_maker/workflow.toml @@ -0,0 +1,37 @@ +name = "story_maker" + + +[filter] +"*" = "manager" + +[roles.manager] +name = "manager" +fullname = "总导演" +agent="manager" +[[roles.manager.prompt]] +role="system" +content=""" +你是一个语音故事制作总导演,与客户对接并向团队下达指令。你的团队分为下面几个成员:writer,studio_director。一个故事制作分成两个阶段:让writer写出故事,再交由studio_director演播故事。你的基本工作模式是: +1. 收到客户的明确的指令后,让writer写出故事 +2. 将writer写出的故事交给studio_director演播 +3. 当你决定要和成员通信时,请使用下面形式输出需要通信的消息 +``` +##/send_msg 成员名称 +内容 +``` +""" + +[roles.writer] +name = "writer" +agent = "fairy_tale_writer" +fullname = "作家" +[[roles.writer.prompt]] +role="system" +content="" + +[roles.studio_director] +name = "studio_director" +agent = "studio_director" +[[roles.studio_director.prompt]] +role="system" +content="" diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 79d4456..9eba338 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -19,6 +19,7 @@ from .tg_tunnel import TelegramTunnel from .email_tunnel import EmailTunnel from .storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem from .contact_manager import ContactManager,Contact,FamilyMember +from .text_to_speech_function import TextToSpeechFunction AIOS_Version = "0.5.1, build 2023-9-17" diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py index 5893683..ceb9821 100644 --- a/src/aios_kernel/chatsession.py +++ b/src/aios_kernel/chatsession.py @@ -35,10 +35,10 @@ class ChatSessionDB: self._create_table(conn) return conn - + def close(self): if not hasattr(self.local, 'conn'): - return + return self.local.conn.close() def _create_table(self, conn): @@ -56,7 +56,7 @@ class ChatSessionDB: # create messages table # reciver_id could be None - + conn.execute(""" CREATE TABLE IF NOT EXISTS Messages ( MessageID TEXT PRIMARY KEY, @@ -142,7 +142,7 @@ class ChatSessionDB: 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() @@ -150,7 +150,7 @@ class ChatSessionDB: 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() @@ -175,7 +175,7 @@ class ChatSessionDB: 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() @@ -216,7 +216,7 @@ class ChatSessionDB: 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. @@ -230,7 +230,7 @@ class AIChatSession: # 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) @@ -249,21 +249,22 @@ class AIChatSession: result = AIChatSession(owner_id,session[0],db) result.topic = session_topic - return result - + 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]: + return [] msgs = self.db.get_messages(self.session_id, number, offset) result = [] for msg in msgs: @@ -298,4 +299,4 @@ class AIChatSession: # """chat session changed event handler""" # pass - #TODO : add iterator interface for read chat history + #TODO : add iterator interface for read chat history diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 97f9b9b..5859efe 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -7,7 +7,7 @@ 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, ComputeTaskType logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ class ComputeKernel: if cls._instance is None: cls._instance = ComputeKernel() return cls._instance - + def __init__(self) -> None: self.is_start = False self.task_queue = Queue() @@ -73,7 +73,7 @@ class ComputeKernel: hit_pos = random.randint(0, total_weights - 1) for i in range(min(len(support_nodes) - 1, hit_pos), -1, -1): if support_nodes[i]["pos"] <= hit_pos: - return node + return support_nodes[i]["node"] logger.warning( f"task {task.display()} is not support by any compute node") @@ -137,7 +137,7 @@ class ComputeKernel: task_req.set_text_embedding_params(input,model_name) self.run(task_req) return task_req - + async def do_text_embedding(self,input:str,model_name:Optional[str] = None) -> [float]: task_req = self.text_embedding(input,model_name) async def check_timer(): @@ -155,11 +155,45 @@ class ComputeKernel: 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 - - return "error!" - + + return "error!" + + async def do_text_to_speech(self, + input:str, + language_code:Optional[str] = None, + gender: Optional[str] = None, + age: Optional[str] = None, + voice_name: Optional[str] = None, + tone: Optional[str] = None): + task_req = ComputeTask() + task_req.params["text"] = input + task_req.params["language_code"] = language_code + task_req.params["gender"] = gender + task_req.params["age"] = age + task_req.params["voice_name"] = voice_name + task_req.params["tone"] = tone + task_req.task_type = ComputeTaskType.TEXT_2_VOICE + self.run(task_req) + + check_times = 0 + while True: + if task_req.state == ComputeTaskState.DONE: + break + if task_req.state == ComputeTaskState.ERROR: + break + if check_times >= 60: + task_req.state = ComputeTaskState.ERROR + break + await asyncio.sleep(0.5) + check_times += 1 + + if task_req.state == ComputeTaskState.DONE: + return task_req.result.result + else: + raise Exception("do_text_to_speech failed!") + diff --git a/src/aios_kernel/google_text_to_speech_node.py b/src/aios_kernel/google_text_to_speech_node.py index 7709eef..b05f276 100644 --- a/src/aios_kernel/google_text_to_speech_node.py +++ b/src/aios_kernel/google_text_to_speech_node.py @@ -21,24 +21,69 @@ see:https://cloud.google.com/text-to-speech/docs/before-you-begin class GoogleTextToSpeechNode(ComputeNode): _instance = None - def __new__(cls, *args, **kwargs): + @classmethod + def get_instance(cls): if cls._instance is None: - cls._instance = super(GoogleTextToSpeechNode, cls).__new__(cls) - cls._instance.is_start = False + cls._instance = cls() 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.language_list = { + "cnm-CN": { + "female": ["cmn-CN-Standard-A", + "cmn-CN-Standard-D", + "cmn-CN-Wavenet-A", + "cmn-CN-Wavenet-D", + "cmn-TW-Standard-A", + "cmn-TW-Wavenet-A"], + "man": ["cmn-CN-Standard-B", + "cmn-CN-Standard-C", + "cmn-CN-Wavenet-B", + "cmn-CN-Wavenet-C", + "cmn-TW-Standard-B", + "cmn-TW-Standard-C", + "cmn-TW-Wavenet-B", + "cmn-TW-Wavenet-C"] + }, + "en-US": { + "female": ["en-US-Neural2-C", + "en-US-Neural2-E", + "en-US-Neural2-F", + "en-US-Neural2-G", + "en-US-Neural2-H", + "en-US-News-K", + "en-US-News-L", + "en-US-Standard-C", + "en-US-Standard-E", + "en-US-Standard-F", + "en-US-Standard-G", + "en-US-Standard-H", + "en-US-Studio-O", + "en-US-Wavenet-C", + "en-US-Wavenet-E", + "en-US-Wavenet-F", + "en-US-Wavenet-G", + "en-US-Wavenet-H"], + "man": ["en-US-Polyglot-1", + "en-US-Standard-A", + "en-US-Standard-B", + "en-US-Standard-D", + "en-US-Standard-I", + "en-US-Standard-J", + "en-US-Studio-M", + "en-US-Wavenet-A", + "en-US-Wavenet-B", + "en-US-Wavenet-D", + "en-US-Wavenet-I", + "en-US-Wavenet-J"] + } + } self.start() def start(self): @@ -64,10 +109,24 @@ class GoogleTextToSpeechNode(ComputeNode): task.state = ComputeTaskState.RUNNING language_code = task.params["language_code"] text = task.params["text"] + voice_name = task.params["voice_name"] + gender = task.params["gender"] + age = task.params["age"] + + if language_code == "zh": + language_code = "cnm-CN" + elif language_code == "en": + language_code = "en-US" + else: + raise Exception(f"language_code {language_code} not support") + + lang_list = self.language_list[language_code][gender] + voice = lang_list[hash(voice_name) % len(lang_list)] synthesis_input = texttospeech.SynthesisInput(text=text) voice = texttospeech.VoiceSelectionParams(language_code=language_code, - ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL) + ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL, + name=voice) audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3) @@ -95,8 +154,8 @@ class GoogleTextToSpeechNode(ComputeNode): def get_capacity(self): return 0 - def is_support(self, task_type: ComputeTaskType) -> bool: - if task_type == ComputeTaskType.TEXT_2_VOICE: + def is_support(self, task: ComputeTask) -> bool: + if task.task_type == ComputeTaskType.TEXT_2_VOICE: return True return False diff --git a/src/aios_kernel/text_to_speech_function.py b/src/aios_kernel/text_to_speech_function.py new file mode 100644 index 0000000..3237974 --- /dev/null +++ b/src/aios_kernel/text_to_speech_function.py @@ -0,0 +1,101 @@ +import io +import logging +import os +import random +from typing import Dict + +from aios_kernel import ComputeKernel +from aios_kernel.ai_function import AIFunction + +from pydub import AudioSegment + +logger = logging.getLogger(__name__) + +class TextToSpeechFunction(AIFunction): + def __init__(self): + self.func_id = "text_to_speech" + self.description = "根据输入的剧本生成音频数据" + + def get_name(self) -> str: + return self.func_id + + def get_description(self) -> str: + return self.description + + def get_parameters(self) -> Dict: + return { + "type": "object", + "properties": { + "language": {"type": "string", "description": "演播语言", "enum": ["zh", "en"]}, + "roles": {"type": "array", "items": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "角色名字"}, + "gender": {"type": "string", "description": "角色性别", "enum": ["man", "female"]}, + "age": {"type": "string", "description": "年龄", "enum": ["child", "adult"]}, + }}}, + "lines": {"type": "array", "items": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "角色名字"}, + "tone": {"type": "string", "description": "演播情感", + "enum": ["happy", "sad", "angry", "fear", "disgust", "surprise", "neutral"]}, + "text": {"type": "string", "description": "台词"}, + } + }} + } + } + + async def execute(self, **kwargs) -> str: + logger.info(f"execute text_to_speech function: {kwargs}") + + language = kwargs.get("language") + if language is None: + language = "zh" + roles = kwargs.get("roles") + lines = kwargs.get("lines") + + audio = None + for line in lines: + name = line.get("name") + tone = line.get("tone") + text = line.get("text") + gender = None + age = None + for role in roles: + role_name = role.get("name") + if role_name == name: + gender = role.get("gender") + age = role.get("age") + break + i = 0 + while i < 3: + try: + data = await ComputeKernel.get_instance().do_text_to_speech(text, language, gender, age, name, tone) + if audio is None: + audio = AudioSegment.from_mp3(io.BytesIO(data)) + else: + audio = audio + AudioSegment.from_mp3(io.BytesIO(data)) + break + except Exception as e: + logger.error(f"do_text_to_speech failed: {e}") + i += 1 + continue + + if audio is not None: + path = os.path.join(os.curdir, "{}.mp3".format(random.sample('zyxwvutsrqponmlkjihgfedcba', 10))) + audio.export(path, format="mp3") + return "complete.file path:{}".format(path) + else: + return "failed" + + def is_local(self) -> bool: + return True + + def is_in_zone(self) -> bool: + return True + + def is_ready_only(self) -> bool: + return False + + diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index 120b3fb..b7211ef 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -7,6 +7,8 @@ from sqlite3 import Error import threading import logging from typing import Optional + +from .text_to_speech_function import TextToSpeechFunction from .environment import Environment,EnvironmentEvent from .ai_function import SimpleAIFunction from .storage import AIStorage @@ -23,8 +25,8 @@ class CalenderEvent(EnvironmentEvent): self.data = data def display(self) -> str: - return f"#event timer:{self.data}" - + 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: @@ -39,7 +41,7 @@ class CalenderEnvironment(Environment): #self.add_ai_function(SimpleAIFunction("serach_events", # "search events in calender", # self._search_events)) - + get_param = { "start_time": "start time (UTC) of event", "end_time": "end time (UTC) of event" @@ -59,14 +61,14 @@ class CalenderEnvironment(Environment): self.add_ai_function(SimpleAIFunction("add_event", "add event to calender", self._add_event,add_param)) - + delete_param = { "event_id": "id of event" } self.add_ai_function(SimpleAIFunction("delete_event", "delete event from calender", self._delete_event,delete_param)) - + update_param = { "event_id": "id of event", "new_title": "new title of event", @@ -79,7 +81,7 @@ class CalenderEnvironment(Environment): self.add_ai_function(SimpleAIFunction("update_event", "update event in calender", self._update_event,update_param)) - + #self.add_ai_function(SimpleAIFunction("user_confirm", # "user confirm", # self._user_confirm)) @@ -98,7 +100,7 @@ class CalenderEnvironment(Environment): ); """) await db.commit() - + async def _add_event(self,title, start_time, end_time, participants=None, location=None, details=None): async with aiosqlite.connect(self.db_file) as db: await db.execute(""" @@ -127,7 +129,7 @@ class CalenderEnvironment(Environment): _event["details"] = row[6] result[row[0]] = _event return json.dumps(result, indent=4, sort_keys=True) - + async def _get_events_by_time_range(self,start_time, end_time): async with aiosqlite.connect(self.db_file) as db: cursor = await db.execute(""" @@ -153,7 +155,7 @@ class CalenderEnvironment(Environment): return "No event." return json.dumps(result, indent=4, sort_keys=True) - + async def _update_event(self,event_id, new_title=None, new_participants=None, new_location=None, new_details=None ,start_time=None, end_time=None): fields_to_update = [] values = [] @@ -191,8 +193,8 @@ class CalenderEnvironment(Environment): WHERE id = ?; """ - values.append(event_id) - + values.append(event_id) + async with aiosqlite.connect(self.db_file) as db: await db.execute(sql_update_query, values) await db.commit() @@ -212,16 +214,16 @@ class CalenderEnvironment(Environment): async def start(self) -> None: if self.is_run: - return + return self.is_run = True await self.init_db() - + 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') @@ -238,13 +240,13 @@ class CalenderEnvironment(Environment): def get_now(self)->str: now = datetime.now() formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') - return formatted_time + 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: @@ -252,6 +254,7 @@ class WorkflowEnvironment(Environment): self.db_file = db_file self.local = threading.local() self.table_name = "WorkflowEnv_" + env_id + self.add_ai_function(TextToSpeechFunction()) def _get_conn(self): @@ -259,7 +262,7 @@ class WorkflowEnvironment(Environment): 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 @@ -273,10 +276,10 @@ class WorkflowEnvironment(Environment): self._create_table(conn) return conn - + def close(self): if not hasattr(self.local, 'conn'): - return + return self.local.conn.close() def _create_table(self, conn): @@ -293,7 +296,7 @@ class WorkflowEnvironment(Environment): 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() @@ -311,7 +314,7 @@ class WorkflowEnvironment(Environment): super().set_value(key,str_value) if is_storage is False: return - + try: conn = self._get_conn() conn.execute(""" diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 984bb08..f3df06a 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -64,17 +64,17 @@ class AIOS_Shell: target_id = msg.target.split(".")[0] agent : AIAgent = await AgentManager.get_instance().get(target_id) if agent is not None: - agent.owner_env = Environment.get_env_by_id("calender") + 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_instance().get_workflow(target_id) if a_workflow is not None: 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_instance().get(target_id) if agent is not None: @@ -96,7 +96,10 @@ class AIOS_Shell: logger.error("openai node initial failed!") return False ComputeKernel.get_instance().add_compute_node(open_ai_node) - + + google_text_to_speech_node = GoogleTextToSpeechNode.get_instance() + ComputeKernel.get_instance().add_compute_node(google_text_to_speech_node) + llama_ai_node = LocalLlama_ComputeNode() await llama_ai_node.start() # ComputeKernel.get_instance().add_compute_node(llama_ai_node) @@ -116,7 +119,7 @@ class AIOS_Shell: tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml") tunnel_config = None - try: + try: tunnel_config = toml.load(tunnels_config_path) if tunnel_config is not None: await AgentTunnel.load_all_tunnels_from_config(tunnel_config) @@ -156,11 +159,11 @@ class AIOS_Shell: case "email": tunnel_config["type"] = "EmailTunnel" case _: - error_text = FormattedText([("class:error", f"tunnel type {tunnel_type}not support!")]) + error_text = FormattedText([("class:error", f"tunnel type {tunnel_type}not support!")]) print_formatted_text(error_text,style=shell_style) return None - intro_text = FormattedText([("class:prompt", tunnel_introduce)]) + intro_text = FormattedText([("class:prompt", tunnel_introduce)]) print_formatted_text(intro_text,style=shell_style) for key,item in intpu_table.items(): user_input = await try_get_input(f"{key} : {item.desc}") @@ -174,7 +177,7 @@ class AIOS_Shell: async def append_tunnel_config(self,tunnel_config): user_data_dir = AIStorage.get_instance().get_myai_dir() tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml") - try: + try: all_tunnels = toml.load(tunnels_config_path) if all_tunnels is not None: all_tunnels[tunnel_config["tunnel_id"]] = tunnel_config @@ -255,7 +258,7 @@ class AIOS_Shell: AIStorage.get_instance().get_user_config().set_value(key,value) await AIStorage.get_instance().get_user_config().save_to_user_config() show_text = FormattedText([("class:title", f"set {key} to {value} success!")]) - + return show_text case 'connect': show_text = FormattedText([("class:title", "args error, /connect $target telegram | email")]) @@ -293,8 +296,8 @@ class AIOS_Shell: 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!" + + return self.username + " login success!" case 'history': num = 10 offset = 0 @@ -324,9 +327,9 @@ class AIOS_Shell: return FormattedText([("class:title", f"help~~~")]) -########################################################################################################################## +########################################################################################################################## history = FileHistory('aios_shell_history.txt') -session = PromptSession(history=history) +session = PromptSession(history=history) def parse_function_call(func_string): if len(func_string) > 2: @@ -337,7 +340,7 @@ def parse_function_call(func_string): return func_name, params else: return None - + async def try_get_input(desc:str,check_func:callable = None) -> str: user_input = await session.prompt_async(f"{desc} \nType /exit to abort. \nPlease input:",style=shell_style) err_str = "" @@ -347,13 +350,13 @@ async def try_get_input(desc:str,check_func:callable = None) -> str: return user_input else: return None - + else: is_ok,err_str = check_func(user_input) if is_ok: return user_input - - error_text = FormattedText([("class:error", err_str)]) + + error_text = FormattedText([("class:error", err_str)]) print_formatted_text(error_text,style=shell_style) return await try_get_input(desc,check_func) @@ -373,7 +376,7 @@ async def main_daemon_loop(shell:AIOS_Shell): return 0 def print_welcome_screen(): - print("\033[1;31m") + print("\033[1;31m") logo = """ \t _______ ____________________ __ \t __ __ \______________________ __ \__ |__ | / / @@ -384,9 +387,9 @@ def print_welcome_screen(): """ print(logo) - print("\033[0m") + print("\033[0m") - print("\033[1;32m \t\tWelcome to OpenDAN - Your Personal AI OS\033[0m\n") + 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. @@ -396,12 +399,12 @@ def print_welcome_screen(): \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(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") @@ -412,19 +415,19 @@ async def main(): 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 + 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() + shell = AIOS_Shell("user") + shell.declare_all_user_config() await AIStorage.get_instance().initial() check_result = AIStorage.get_instance().get_user_config().check_config() if check_result is not None: @@ -435,7 +438,7 @@ async def main(): #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: @@ -451,8 +454,8 @@ async def main(): proxy.apply_storage() #TODO: read last input config - completer = WordCompleter(['/send $target $msg $topic', - '/open $target $topic', + completer = WordCompleter(['/send $target $msg $topic', + '/open $target $topic', '/history $num $offset', '/login $username', '/connect $target', @@ -462,7 +465,7 @@ async def main(): '/set_config $key', '/list_config', '/show', - '/exit', + '/exit', '/help'], ignore_case=True) current = AIStorage.get_instance().get_user_config().get_value("shell.current") @@ -470,7 +473,7 @@ async def main(): shell.current_target = current[1] shell.current_topic = current[0] - await asyncio.sleep(0.2) + await asyncio.sleep(0.2) while True: user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$ ",completer=completer,style=shell_style) if len(user_input) <= 1: @@ -491,6 +494,6 @@ async def main(): #print_formatted_text(f"{shell.username}<->{shell.current_topic}@{shell.current_target} >>> {resp}",style=shell_style) -if __name__ == "__main__": +if __name__ == "__main__": asyncio.run(main()) From 5c2dd13ab2e8735e5673d05957be74ed7f241343 Mon Sep 17 00:00:00 2001 From: wugren Date: Fri, 22 Sep 2023 00:09:21 +0800 Subject: [PATCH 101/177] Fix story maker bug --- rootfs/agents/fairy_tale_writer/agent.toml | 1 + rootfs/agents/manager/agent.toml | 2 + rootfs/agents/studio_director/agent.toml | 4 +- rootfs/workflows/story_maker/workflow.toml | 6 +- src/aios_kernel/agent.py | 78 +++++++------- src/aios_kernel/bus.py | 44 ++++---- src/aios_kernel/compute_kernel.py | 4 +- src/aios_kernel/open_ai_node.py | 23 +++-- src/aios_kernel/text_to_speech_function.py | 4 +- src/aios_kernel/workflow.py | 114 +++++++++++---------- 10 files changed, 147 insertions(+), 133 deletions(-) diff --git a/rootfs/agents/fairy_tale_writer/agent.toml b/rootfs/agents/fairy_tale_writer/agent.toml index 1f2256a..7955df6 100644 --- a/rootfs/agents/fairy_tale_writer/agent.toml +++ b/rootfs/agents/fairy_tale_writer/agent.toml @@ -1,6 +1,7 @@ instance_id = "fairy_tale_writer" fullname = "tracy wang" llm_model_name = "gpt-3.5-turbo-16k-0613" +enable_function = [] [[prompt]] role = "system" diff --git a/rootfs/agents/manager/agent.toml b/rootfs/agents/manager/agent.toml index 5bb2ba6..e4bba03 100644 --- a/rootfs/agents/manager/agent.toml +++ b/rootfs/agents/manager/agent.toml @@ -1,5 +1,7 @@ instance_id = "agent:xxxxxxabcde" fullname = "musk" +enable_function = [] + [[prompt]] role = "system" content = "你有丰富的管理技能,擅长将复杂工作拆解成简单的任务,让团队成员高效协作。" diff --git a/rootfs/agents/studio_director/agent.toml b/rootfs/agents/studio_director/agent.toml index eb61bca..65a7c07 100644 --- a/rootfs/agents/studio_director/agent.toml +++ b/rootfs/agents/studio_director/agent.toml @@ -1,7 +1,7 @@ instance_id = "studio_director" fullname = "tracy wang" -llm_model_name = "gpt-3.5-turbo-16k-0613" +enable_function = ["text_to_speech"] [[prompt]] role = "system" -content = "你是一个演播导演,请将下面故事改编成朗读剧本,提取旁白和角色台词,每个角色需要有性别、年龄、以及每句台词的语气。并调用text_to_speech function生成音频数据。" +content = "你是一个故事播音员,请将下面故事改编成播音剧本,提取旁白和角色台词,以及每个角色需要有性别、年龄、以及每句台词的语气,最后生成音频文件。" diff --git a/rootfs/workflows/story_maker/workflow.toml b/rootfs/workflows/story_maker/workflow.toml index 30a26d6..540665f 100644 --- a/rootfs/workflows/story_maker/workflow.toml +++ b/rootfs/workflows/story_maker/workflow.toml @@ -8,10 +8,12 @@ name = "story_maker" name = "manager" fullname = "总导演" agent="manager" +enable_function = [] + [[roles.manager.prompt]] role="system" content=""" -你是一个语音故事制作总导演,与客户对接并向团队下达指令。你的团队分为下面几个成员:writer,studio_director。一个故事制作分成两个阶段:让writer写出故事,再交由studio_director演播故事。你的基本工作模式是: +你是一个语音故事制作总导演,与客户对接并向团队下达指令。你的团队分为下面几个成员:writer,studio_director。一个故事制作分成两个阶段:让writer写出故事,再交由studio_director演播故事生成音频文件。你的基本工作模式是: 1. 收到客户的明确的指令后,让writer写出故事 2. 将writer写出的故事交给studio_director演播 3. 当你决定要和成员通信时,请使用下面形式输出需要通信的消息 @@ -25,6 +27,7 @@ content=""" name = "writer" agent = "fairy_tale_writer" fullname = "作家" +enable_function = [] [[roles.writer.prompt]] role="system" content="" @@ -32,6 +35,7 @@ content="" [roles.studio_director] name = "studio_director" agent = "studio_director" +enable_function = ["text_to_speech"] [[roles.studio_director.prompt]] role="system" content="" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index cc79637..653ac0a 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -26,7 +26,7 @@ class AgentPrompt: self.system_message = None def as_str(self)->str: - result_str = "" + result_str = "" if self.system_message: result_str += self.system_message.get("role") + ":" + self.system_message.get("content") + "\n" if self.messages: @@ -34,18 +34,18 @@ class AgentPrompt: result_str += msg.get("role") + ":" + msg.get("content") + "\n" return result_str - + def to_message_list(self): result = [] if self.system_message: result.append(self.system_message) result.extend(self.messages) return result - + def append(self,prompt): if prompt is None: return - + if prompt.system_message is not None: if self.system_message is None: self.system_message = prompt.system_message @@ -99,9 +99,9 @@ class AIAgentTemplete: logger.error("load prompt from config failed!") return False - + return True - + class AIAgent: def __init__(self) -> None: @@ -111,7 +111,7 @@ class AIAgent: self.agent_id:str = None self.template_id:str = None self.fullname:str = None - self.powerby = None + self.powerby = None self.enable = True self.enable_kb = False self.enable_timestamp = False @@ -124,7 +124,7 @@ class AIAgent: self.owner_env : Environment = None self.owenr_bus = None self.enable_function_list = [] - + @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): # Agent just inherit from templete on craete,if template changed,agent will not change @@ -137,7 +137,7 @@ class AIAgent: result_agent.powerby = templete.author result_agent.prompt = templete.prompt return result_agent - + def load_from_config(self,config:dict) -> bool: if config.get("instance_id") is None: logger.error("agent instance_id is None!") @@ -188,7 +188,7 @@ class AIAgent: if llm_result_str == "ignore": r.state = "ignore" return r - + lines = llm_result_str.splitlines() is_need_wait = False @@ -205,7 +205,7 @@ class AIAgent: r.send_msgs.append(new_msg) is_need_wait = True - + case "post_msg":# postmsg($target_id,$msg_content) if len(func_args) != 1: logger.error(f"parse postmsg failed! {func_call}") @@ -215,22 +215,22 @@ class AIAgent: msg_content = func_item.body new_msg.set(self.agent_id,target_id,msg_content) r.post_msgs.append(new_msg) - + case "call":# call($func_name,$args_str) r.calls.append(func_item) is_need_wait = True return True case "post_call": # post_call($func_name,$args_str) r.post_calls.append(func_item) - return True - + return True + current_func : FunctionItem = None for line in lines: if line.startswith("##/"): if current_func: if check_args(current_func) is False: r.resp += current_func.dumps() - + func_name,func_args = AgentMsg.parse_function_call(line[3:]) current_func = FunctionItem(func_name,func_args) else: @@ -238,11 +238,11 @@ class AIAgent: current_func.append_body(line + "\n") else: r.resp += line + "\n" - + if current_func: if check_args(current_func) is False: r.resp += current_func.dumps() - + if len(r.send_msgs) > 0 or len(r.calls) > 0: r.state = "waiting" else: @@ -279,21 +279,22 @@ class AIAgent: 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 = [] result_len = 0 for inner_func in all_inner_function: func_name = inner_func.get_name() - if self.enable_function_list: + if self.enable_function_list is not None: if len(self.enable_function_list) > 0: if func_name not in self.enable_function_list: logger.debug(f"ageint {self.agent_id} ignore inner func:{func_name}") continue - + else: + continue this_func = {} this_func["name"] = func_name this_func["description"] = inner_func.get_description() @@ -313,28 +314,29 @@ class AIAgent: 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) try: result_str:str = await func_node.execute(**arguments) except Exception as e: - result_str = "call error:" + str(e) + result_str = "call error:" + str(e) logger.error(f"llm execute inner func:{func_name} error:{e}") - + + logger.info("llm execute inner func result:" + result_str) inner_functions,inner_function_len = 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) if stack_limit > 0: 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,stack_limit-1) + return await self._execute_func(inner_func_call_node,prompt,org_msg,stack_limit-1) else: return task_result.result_str @@ -344,7 +346,7 @@ class AIAgent: def _format_msg_by_env_value(self,prompt:AgentPrompt): if self.owner_env is None: return - + for msg in prompt.messages: old_content = msg.get("content") msg["content"] = old_content.format_map(self.owner_env) @@ -380,7 +382,7 @@ class AIAgent: system_prompt_len = prompt.get_prompt_token_len() input_len = len(msg.body) - + history_prmpt,history_token_len = await self._get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len) prompt.append(history_prmpt) # chat context @@ -397,7 +399,7 @@ class AIAgent: 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) - + llm_result : LLMResult = self._get_llm_result_type(final_result) is_ignore = False result_prompt_str = "" @@ -415,21 +417,21 @@ class AIAgent: agent_sesion = AIChatSession.get_session(self.agent_id,f"{sendmsg.target}#{sendmsg.topic}",self.chat_db) agent_sesion.append(sendmsg) agent_sesion.append(send_resp) - + final_result = llm_result.resp + result_prompt_str if is_ignore is not True: 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.agent_id - + def get_fullname(self) -> str: return self.fullname @@ -438,14 +440,14 @@ class AIAgent: 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,system_token_len,input_token_len,is_groupchat=False) -> AgentPrompt: # TODO: get prompt from group chat is different from single chat history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len - messages = chatsession.read_history() # read + messages = chatsession.read_history() # read result_token_len = 0 result_prompt = AgentPrompt() read_history_msg = 0 diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py index 29fc186..16ede81 100644 --- a/src/aios_kernel/bus.py +++ b/src/aios_kernel/bus.py @@ -19,14 +19,14 @@ class AIBusHandler: async def handle_message(self,msg:AgentMsg) -> Any: if self.handler is None: 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 @@ -48,16 +48,16 @@ class AIBus: if msg.rely_msg_id is not None: handler.results[msg.rely_msg_id] = msg return None - + 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(msg,False) - + logger.warn(f"post message to {msg.target} failed!,target not found") return False @@ -71,7 +71,7 @@ class AIBus: if sender_handler is None: logger.warn(f"sender {sender_id} not register on AI_BUS!") return None - + post_result = await self.post_message(msg) if post_result is False: return None @@ -84,37 +84,39 @@ class AIBus: msg.status = AgentMsgStatus.RESPONSED del sender_handler.results[msg.msg_id] return resp - + await asyncio.sleep(0.2) retry_times += 1 - if retry_times > 5*120: # default timeout is 120 sec + if retry_times > 5*240: # default timeout is 240 sec msg.status = AgentMsgStatus.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) + def register_message_handler(self,handler_name:str,handler:Any) -> Queue: + handler_node = AIBusHandler(handler,self) 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: # Try to handle the message - await handler.handle_message(message) - #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.msg_id} failed! {e}") + logger.error(f"handle message {message.msg_id} failed! {e}") + logger.exception(e) + raise e #self.queues[name].put_nowait(message) - + return def start_process(self,target_name): @@ -122,12 +124,12 @@ class AIBus: 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)) \ No newline at end of file + + handler.working_task = asyncio.create_task(self.process_queue(handler)) diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index 5859efe..fffc8ac 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -118,7 +118,7 @@ class ComputeKernel: if task_req.state == ComputeTaskState.ERROR: break - if check_times >= 20: + if check_times >= 120: task_req.state = ComputeTaskState.ERROR break @@ -129,7 +129,7 @@ class ComputeKernel: if task_req.state == ComputeTaskState.DONE: return task_req.result - return "error!" + raise Exception("error!") def text_embedding(self,input:str,model_name:Optional[str] = None): diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index 475fa22..e610be7 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -20,7 +20,7 @@ class OpenAI_ComputeNode(ComputeNode): if cls._instance is None: cls._instance = OpenAI_ComputeNode() return cls._instance - + @classmethod def declare_user_config(cls): if os.getenv("OPENAI_API_KEY_") is None: @@ -46,7 +46,7 @@ class OpenAI_ComputeNode(ComputeNode): 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() return True @@ -68,7 +68,7 @@ class OpenAI_ComputeNode(ComputeNode): resp = openai.Embedding.create(model=model_name, input=input) - + # resp = { # "object": "list", # "data": [ @@ -86,7 +86,7 @@ class OpenAI_ComputeNode(ComputeNode): logger.info(f"openai response: {resp}") - result = ComputeTaskResult() + result = ComputeTaskResult() result.set_from_task(task) result.worker_id = self.node_id result.result = resp["data"][0]["embedding"] @@ -100,23 +100,23 @@ class OpenAI_ComputeNode(ComputeNode): if max_token_size is None: max_token_size = 4000 - result_token = int(max_token_size * 0.4) - - logger.info(f"call openai {mode_name} prompts: {prompts}") + result_token = max_token_size if llm_inner_functions is None: + logger.info(f"call openai {mode_name} prompts: {prompts}") resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, max_tokens=result_token, temperature=0.7) else: + logger.info(f"call openai {mode_name} prompts: {prompts} functions: {json.dumps(llm_inner_functions)}") resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, functions=llm_inner_functions, max_tokens=result_token, temperature=0.7) # TODO: add temperature to task params? - + logger.info(f"openai response: {json.dumps(resp, indent=4)}") result = ComputeTaskResult() @@ -139,6 +139,7 @@ class OpenAI_ComputeNode(ComputeNode): result.result_message = resp["choices"][0]["message"] if token_usage: result.result_refers["token_usage"] = token_usage + logger.info(f"openai success response: {result.result_str}") return result case _: task.state = ComputeTaskState.ERROR @@ -148,7 +149,7 @@ class OpenAI_ComputeNode(ComputeNode): if self.is_start is True: return self.is_start = True - + async def _run_task_loop(): while True: task = await self.task_queue.get() @@ -171,13 +172,13 @@ class OpenAI_ComputeNode(ComputeNode): def is_support(self, task: ComputeTask) -> bool: - if task.task_type == ComputeTaskType.LLM_COMPLETION: + if task.task_type == ComputeTaskType.LLM_COMPLETION: if not task.params["model_name"]: return True model_name : str = task.params["model_name"] if model_name.startswith("gpt-"): return True - + if task.task_type == ComputeTaskType.TEXT_EMBEDDING: if task.params["model_name"] == "text-embedding-ada-002": return True diff --git a/src/aios_kernel/text_to_speech_function.py b/src/aios_kernel/text_to_speech_function.py index 3237974..2f7bff0 100644 --- a/src/aios_kernel/text_to_speech_function.py +++ b/src/aios_kernel/text_to_speech_function.py @@ -83,9 +83,9 @@ class TextToSpeechFunction(AIFunction): continue if audio is not None: - path = os.path.join(os.curdir, "{}.mp3".format(random.sample('zyxwvutsrqponmlkjihgfedcba', 10))) + path = os.path.join(os.path.realpath(os.curdir), "{}.mp3".format(''.join(random.sample('zyxwvutsrqponmlkjihgfedcba', 10)))) audio.export(path, format="mp3") - return "complete.file path:{}".format(path) + return "已经生成音频文件, 文件路径为{}".format(path) else: return "failed" diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 97893b6..00bb255 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -2,7 +2,7 @@ import logging import asyncio import json import os -import time +import time from asyncio import Queue from typing import Optional,Tuple,List from abc import ABC, abstractmethod @@ -32,7 +32,7 @@ class MessageFilter: # TODO: add more filter return None - + def load_from_config(self,config:dict) -> bool: self.filters = config return True @@ -68,8 +68,8 @@ class Workflow: def load_from_config(self,config:dict) -> bool: if config is None: return False - - if config.get("name") is None: + + if config.get("name") is None: logger.error("workflow config must have name") return False self.workflow_name = config.get("name") @@ -84,7 +84,7 @@ class Workflow: 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 @@ -106,13 +106,13 @@ class Workflow: 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") + 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") + 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") @@ -124,13 +124,13 @@ class Workflow: 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: if self._load_sub_workflows(sub_workflows) is False: logger.error("Workflow load sub workflows failed") return False - + return True def _load_env_from_config(self,config:dict) -> bool: @@ -147,7 +147,7 @@ class Workflow: return False self.sub_workflows[k] = sub_workflow return True - + def _parse_msg_target(self,s:str)->list[str]: return s.split(".") @@ -170,12 +170,12 @@ class Workflow: 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: @@ -203,11 +203,11 @@ class Workflow: #1. workflow start process message final_result = 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: @@ -219,20 +219,20 @@ class Workflow: 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: + 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 - + 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 @@ -245,7 +245,7 @@ class Workflow: if llm_result_str == "ignore": r.state = "ignore" return r - + lines = llm_result_str.splitlines() is_need_wait = False @@ -262,7 +262,7 @@ class Workflow: r.send_msgs.append(new_msg) is_need_wait = True - + case "post_msg":# postmsg($target_id,$msg_content) if len(func_args) != 1: logger.error(f"parse postmsg failed! {func_call}") @@ -272,22 +272,22 @@ class Workflow: msg_content = func_item.body new_msg.set("_",target_id,msg_content) r.post_msgs.append(new_msg) - + case "call":# call($func_name,$args_str) r.calls.append(func_item) is_need_wait = True return True case "post_call": # post_call($func_name,$args_str) r.post_calls.append(func_item) - return True - + return True + current_func : FunctionItem = None for line in lines: if line.startswith("##/"): if current_func: if check_args(current_func) is False: r.resp += current_func.dumps() - + func_name,func_args = AgentMsg.parse_function_call(line[3:]) current_func = FunctionItem(func_name,func_args) else: @@ -295,7 +295,7 @@ class Workflow: current_func.append_body(line + "\n") else: r.resp += line + "\n" - + if current_func: if check_args(current_func) is False: r.resp += current_func.dumps() @@ -309,14 +309,14 @@ class Workflow: 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 @@ -341,7 +341,7 @@ class 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) @@ -352,8 +352,8 @@ class Workflow: func_node : AIFunction = self.workflow_env.get_ai_function(func_item.name) if func_node is None: return "execute failed,function not found" - - result_str:str = await func_node.execute(**arguments) + + result_str:str = await func_node.execute(**arguments) return result_str async def role_post_call(self,func_item:FunctionItem,the_role:AIRole): @@ -363,7 +363,7 @@ class Workflow: 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) @@ -372,15 +372,17 @@ class Workflow: all_inner_function = self.workflow_env.get_all_ai_functions() if all_inner_function is None: return None - + result_func = [] for inner_func in all_inner_function: func_name = inner_func.get_name() - if the_role.enable_function_list: + if the_role.enable_function_list is not None: if len(the_role.enable_function_list) > 0: if func_name not in the_role.enable_function_list: - logger.debug(f"ageint {self.agent_id} ignore inner func:{func_name}") + logger.debug(f"agent {self.agent_id} ignore inner func:{func_name}") continue + else: + continue this_func = {} this_func["name"] = func_name this_func["description"] = inner_func.get_description() @@ -399,17 +401,17 @@ class Workflow: 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() + + inner_functions = self._get_inner_functions(the_role) 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) @@ -419,13 +421,13 @@ class Workflow: return await self._role_execute_func(the_role,inner_func_call_node,prompt,org_msg,stack_limit-1) 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): + 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) @@ -433,7 +435,7 @@ class Workflow: prompt.append(the_role.get_prompt()) # prompt.append(self._get_function_prompt(the_role.get_name())) # prompt.append(self._get_knowlege_prompt(the_role.get_name())) - + #support group chat, user content include sender name! prompt.append(await self._get_prompt_from_session(workflow_chat_session)) @@ -443,15 +445,15 @@ class Workflow: self._format_msg_by_env_value(prompt) inner_functions = self._get_inner_functions(the_role) - + 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}") - + 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) @@ -461,7 +463,7 @@ class Workflow: postmsg.prev_msg_id = msg.get_msg_id() # might be craete a new msg.topic for this postmsg postmsg.topic = msg.topic - + 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) @@ -469,14 +471,14 @@ class Workflow: 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": @@ -506,11 +508,11 @@ class Workflow: 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 @@ -522,7 +524,7 @@ class Workflow: prompt.append(result_prompt) r = await _do_process_msg() return r - + return await _do_process_msg() async def _get_prompt_from_session(self,chatsession:AIChatSession) -> AgentPrompt: @@ -533,9 +535,9 @@ class Workflow: 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_knowlege_prompt(self,role_name:str) -> AgentPrompt: pass @@ -557,7 +559,7 @@ class Workflow: # 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 From b8345eccaf8e06b0d13f715feea3d0fe4c1260c8 Mon Sep 17 00:00:00 2001 From: wugren Date: Fri, 22 Sep 2023 12:22:36 +0800 Subject: [PATCH 102/177] Modify story_maker prompt word --- rootfs/agents/studio_director/agent.toml | 3 ++- rootfs/workflows/story_maker/workflow.toml | 3 ++- src/aios_kernel/text_to_speech_function.py | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rootfs/agents/studio_director/agent.toml b/rootfs/agents/studio_director/agent.toml index 65a7c07..5a5d297 100644 --- a/rootfs/agents/studio_director/agent.toml +++ b/rootfs/agents/studio_director/agent.toml @@ -1,7 +1,8 @@ instance_id = "studio_director" fullname = "tracy wang" +llm_model_name = "gpt-3.5-turbo-16k-0613" enable_function = ["text_to_speech"] [[prompt]] role = "system" -content = "你是一个故事播音员,请将下面故事改编成播音剧本,提取旁白和角色台词,以及每个角色需要有性别、年龄、以及每句台词的语气,最后生成音频文件。" +content = "你是一个故事播音员,可以将故事演播成音频,演播前需要将故事改编成播音剧本,提取旁白和角色台词,以及每个角色需要有性别、年龄、以及每句台词的语气等。如果生成了音频文件则告知你的用户。" diff --git a/rootfs/workflows/story_maker/workflow.toml b/rootfs/workflows/story_maker/workflow.toml index 540665f..6ae5e55 100644 --- a/rootfs/workflows/story_maker/workflow.toml +++ b/rootfs/workflows/story_maker/workflow.toml @@ -16,7 +16,8 @@ content=""" 你是一个语音故事制作总导演,与客户对接并向团队下达指令。你的团队分为下面几个成员:writer,studio_director。一个故事制作分成两个阶段:让writer写出故事,再交由studio_director演播故事生成音频文件。你的基本工作模式是: 1. 收到客户的明确的指令后,让writer写出故事 2. 将writer写出的故事交给studio_director演播 -3. 当你决定要和成员通信时,请使用下面形式输出需要通信的消息 +3. 获得音频文件之后,将音频文件的存放路径交给客户 +4. 当你决定要和成员通信时,请使用下面形式输出需要通信的消息 ``` ##/send_msg 成员名称 内容 diff --git a/src/aios_kernel/text_to_speech_function.py b/src/aios_kernel/text_to_speech_function.py index 2f7bff0..90dfd49 100644 --- a/src/aios_kernel/text_to_speech_function.py +++ b/src/aios_kernel/text_to_speech_function.py @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) class TextToSpeechFunction(AIFunction): def __init__(self): self.func_id = "text_to_speech" - self.description = "根据输入的剧本生成音频数据" + self.description = "根据输入的剧本生成音频文件,成功时会返回音频文件路径" def get_name(self) -> str: return self.func_id @@ -85,9 +85,9 @@ class TextToSpeechFunction(AIFunction): if audio is not None: path = os.path.join(os.path.realpath(os.curdir), "{}.mp3".format(''.join(random.sample('zyxwvutsrqponmlkjihgfedcba', 10)))) audio.export(path, format="mp3") - return "已经生成音频文件, 文件路径为{}".format(path) + return "exec text_to_speech OK,speech file store at {}".format(path) else: - return "failed" + return "exec text_to_speech failed" def is_local(self) -> bool: return True From b195e6a6a454f8eb0e005026d5960004282f9693 Mon Sep 17 00:00:00 2001 From: wugren Date: Fri, 22 Sep 2023 13:38:44 +0800 Subject: [PATCH 103/177] add google application credentials --- src/aios_kernel/google_text_to_speech_node.py | 21 ++++++++++-- src/service/aios_shell/aios_shell.py | 32 ++++++++++++------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/aios_kernel/google_text_to_speech_node.py b/src/aios_kernel/google_text_to_speech_node.py index b05f276..9bee115 100644 --- a/src/aios_kernel/google_text_to_speech_node.py +++ b/src/aios_kernel/google_text_to_speech_node.py @@ -3,9 +3,11 @@ import os import asyncio from asyncio import Queue import logging +from typing import Optional from google.cloud import texttospeech +from .storage import AIStorage from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode @@ -31,8 +33,7 @@ class GoogleTextToSpeechNode(ComputeNode): super().__init__() self.node_id = "google_text_to_speech_node" self.task_queue = Queue() - - self.client = texttospeech.TextToSpeechClient() + self.client: Optional[texttospeech.TextToSpeechClient] = None self.language_list = { "cnm-CN": { @@ -86,6 +87,14 @@ class GoogleTextToSpeechNode(ComputeNode): } self.start() + def init(self): + user_config = AIStorage.get_instance().get_user_config() + google_application_credentials = user_config.get_value("google_application_credentials") + if google_application_credentials is None: + raise Exception("google_application_credentials is None!") + os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = google_application_credentials + self.client = texttospeech.TextToSpeechClient() + def start(self): async def _run_task_loop(): while True: @@ -161,3 +170,11 @@ class GoogleTextToSpeechNode(ComputeNode): def is_local(self) -> bool: return False + + def declare_user_config(self): + if os.getenv("GOOGLE_APPLICATION_CREDENTIALS") is None: + user_config = AIStorage.get_instance().get_user_config() + user_config.add_user_config("google_application_credentials", + "google application credentials, please visit:https://cloud.google.com/text-to-speech/docs/before-you-begin", + False, + None) diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index f3df06a..7e0a5ca 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -59,6 +59,9 @@ class AIOS_Shell: user_config.add_user_config("shell.current","last opened target and topic",True,"default@Jarvis") proxy.declare_user_config() + google_text_to_speech = GoogleTextToSpeechNode.get_instance() + google_text_to_speech.declare_user_config() + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: target_id = msg.target.split(".")[0] @@ -97,8 +100,13 @@ class AIOS_Shell: return False ComputeKernel.get_instance().add_compute_node(open_ai_node) - google_text_to_speech_node = GoogleTextToSpeechNode.get_instance() - ComputeKernel.get_instance().add_compute_node(google_text_to_speech_node) + try: + google_text_to_speech_node = GoogleTextToSpeechNode.get_instance() + google_text_to_speech_node.init() + ComputeKernel.get_instance().add_compute_node(google_text_to_speech_node) + except Exception as e: + logger.error(f"google text to speech node initial failed! {e}") + return False llama_ai_node = LocalLlama_ComputeNode() await llama_ai_node.start() @@ -116,7 +124,7 @@ class AIOS_Shell: contact_config_path =os.path.abspath(f"{user_data_dir}/contacts.toml") cm = ContactManager.get_instance(contact_config_path) cm.load_data() - + tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml") tunnel_config = None try: @@ -125,10 +133,10 @@ class AIOS_Shell: await AgentTunnel.load_all_tunnels_from_config(tunnel_config) except Exception as e: logger.warning(f"load tunnels config from {tunnels_config_path} failed!") - + KnowledgePipline.get_instance().initial() - return True - + return True + def get_version(self) -> str: return "0.5.1" @@ -169,10 +177,10 @@ class AIOS_Shell: user_input = await try_get_input(f"{key} : {item.desc}") if user_input is None: return None - - tunnel_config[key] = user_input - return tunnel_config + tunnel_config[key] = user_input + + return tunnel_config async def append_tunnel_config(self,tunnel_config): user_data_dir = AIStorage.get_instance().get_myai_dir() @@ -235,7 +243,7 @@ class AIOS_Shell: prompt.messages.append({"role": "user", "content":" ".join(args[1:])}) result = await KnowledgeBase().query_prompt(prompt) print_formatted_text(result.as_str()) - + async def call_func(self,func_name, args): match func_name: case 'send': @@ -252,7 +260,7 @@ class AIOS_Shell: key = args[0] config_item = AIStorage.get_instance().get_user_config().get_config_item(key) old_value = AIStorage.get_instance().get_user_config().get_value(key) - + if config_item is not None: value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style) AIStorage.get_instance().get_user_config().set_value(key,value) @@ -459,7 +467,7 @@ async def main(): '/history $num $offset', '/login $username', '/connect $target', - '/knowledge add email | dir', + '/knowledge add email | dir', '/knowledge journal [$topn]', '/knowledge query $query' '/set_config $key', From 001482ea23499a0dfaea6807eecc7fc602466fc4 Mon Sep 17 00:00:00 2001 From: wugren Date: Fri, 22 Sep 2023 13:50:43 +0800 Subject: [PATCH 104/177] Modify agent name --- .../agents/{studio_director => speecher}/agent.toml | 2 +- rootfs/workflows/story_maker/workflow.toml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) rename rootfs/agents/{studio_director => speecher}/agent.toml (92%) diff --git a/rootfs/agents/studio_director/agent.toml b/rootfs/agents/speecher/agent.toml similarity index 92% rename from rootfs/agents/studio_director/agent.toml rename to rootfs/agents/speecher/agent.toml index 5a5d297..dea7f70 100644 --- a/rootfs/agents/studio_director/agent.toml +++ b/rootfs/agents/speecher/agent.toml @@ -1,4 +1,4 @@ -instance_id = "studio_director" +instance_id = "speecher" fullname = "tracy wang" llm_model_name = "gpt-3.5-turbo-16k-0613" enable_function = ["text_to_speech"] diff --git a/rootfs/workflows/story_maker/workflow.toml b/rootfs/workflows/story_maker/workflow.toml index 6ae5e55..fd3876a 100644 --- a/rootfs/workflows/story_maker/workflow.toml +++ b/rootfs/workflows/story_maker/workflow.toml @@ -13,9 +13,9 @@ enable_function = [] [[roles.manager.prompt]] role="system" content=""" -你是一个语音故事制作总导演,与客户对接并向团队下达指令。你的团队分为下面几个成员:writer,studio_director。一个故事制作分成两个阶段:让writer写出故事,再交由studio_director演播故事生成音频文件。你的基本工作模式是: +你是一个语音故事制作总导演,与客户对接并向团队下达指令。你的团队分为下面几个成员:writer,speecher。一个故事制作分成两个阶段:让writer写出故事,再交由speecher演播故事生成音频文件。你的基本工作模式是: 1. 收到客户的明确的指令后,让writer写出故事 -2. 将writer写出的故事交给studio_director演播 +2. 将writer写出的故事交给speecher演播 3. 获得音频文件之后,将音频文件的存放路径交给客户 4. 当你决定要和成员通信时,请使用下面形式输出需要通信的消息 ``` @@ -33,10 +33,10 @@ enable_function = [] role="system" content="" -[roles.studio_director] -name = "studio_director" -agent = "studio_director" +[roles.speecher] +name = "speecher" +agent = "speecher" enable_function = ["text_to_speech"] -[[roles.studio_director.prompt]] +[[roles.speecher.prompt]] role="system" content="" From ad12e795c5e913dbe2399dd3c26c3d75ae00ca95 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Thu, 21 Sep 2023 23:18:52 -0700 Subject: [PATCH 105/177] Add linux_admin agent, funny~ --- rootfs/agents/Jarvis/agent.toml | 3 +- rootfs/agents/linux_admin/agent.toml | 7 +- src/aios_kernel/__init__.py | 1 + src/aios_kernel/agent.py | 2 + src/aios_kernel/tg_tunnel.py | 6 +- src/aios_kernel/worksapce_env.py | 2 - src/aios_kernel/workspace_env.py | 173 +++++++++++++++++++++++++++ src/service/aios_shell/aios_shell.py | 4 +- test/test_contac_manager.py | 2 +- test/workflow_test.py | 26 +++- 10 files changed, 208 insertions(+), 18 deletions(-) delete mode 100644 src/aios_kernel/worksapce_env.py create mode 100644 src/aios_kernel/workspace_env.py diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml index ca1c7b0..c8c3ae7 100644 --- a/rootfs/agents/Jarvis/agent.toml +++ b/rootfs/agents/Jarvis/agent.toml @@ -7,12 +7,11 @@ max_token_size = 16000 enable_timestamp = "true" owner_prompt = "我是你的主人{name}" contact_prompt = "我是你的朋友{name}" -guest_prompt = "xxx" +owner_env = "calender" [[prompt]] role = "system" content = """ -现在是{now} 你叫Jarvis,是我的超级私人助理。 你领导一个团队为我服务,团队的成员有: Tracy,私人英语老师 diff --git a/rootfs/agents/linux_admin/agent.toml b/rootfs/agents/linux_admin/agent.toml index 7988669..9eaef70 100644 --- a/rootfs/agents/linux_admin/agent.toml +++ b/rootfs/agents/linux_admin/agent.toml @@ -1,12 +1,13 @@ instance_id = "linux_admin" fullname = "linux_admin" - +owner_env = "bash" [[prompt]] role = "system" content = """你的名字是linux_admin,是非常资深的linux系统管理员,我理解一些linux,但对linux的sh命令记得不太清楚了。 我给你的输入有两种 -1. 标准的linux bash命令,如果你认为这些命令在当前系统上是正确的,那么直接返回这些命令,我会去执行 -2. 如果你为这些命令需要调整后才能执行,先把你修正后的命令告诉我,我确认后再执行 +1. 标准的linux bash命令,如果你认为这些命令在当前系统上是正确且可执行的,那么你可以直接执行 +2. 如果我的命令不对,或则命令可能对系统有害,你要对这些命令进行调整后,先把你调整后的命令告诉我,我确认后你再执行正确的命令 3. 如果我给你的信息不是linux bash操作,而是一些需求。你可以尝试理解后,给出一组实现这些需求的命令,我确认后再执行 4. 其它信息,请站在你专业角度尽力执行。 +5. 每次执行命令后,要把结果告诉我。 """ diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 79d4456..b821013 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -19,6 +19,7 @@ from .tg_tunnel import TelegramTunnel from .email_tunnel import EmailTunnel from .storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem from .contact_manager import ContactManager,Contact,FamilyMember +from .workspace_env import WorkspaceEnvironment AIOS_Version = "0.5.1, build 2023-9-17" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index cc79637..b1c3e40 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -162,6 +162,8 @@ class AIAgent: if config.get("contact_prompt") is not None: self.contact_prompt_str = config["contact_prompt"] + if config.get("owner_env") is not None: + self.owner_env = Environment.get_env_by_id(config["owner_env"]) if config.get("powerby") is not None: self.powerby = config["powerby"] diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index edc7b93..4cad825 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -4,11 +4,7 @@ import asyncio import uuid import time -from typing import Callable - -from telegram import ForceReply, Update -from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters - +from telegram import Update from telegram import Bot from telegram.ext import Updater from telegram.error import Forbidden, NetworkError diff --git a/src/aios_kernel/worksapce_env.py b/src/aios_kernel/worksapce_env.py deleted file mode 100644 index d8611f2..0000000 --- a/src/aios_kernel/worksapce_env.py +++ /dev/null @@ -1,2 +0,0 @@ -# this env is designed for workflow owner filesystem, support file/directory operations - diff --git a/src/aios_kernel/workspace_env.py b/src/aios_kernel/workspace_env.py new file mode 100644 index 0000000..8bfe00e --- /dev/null +++ b/src/aios_kernel/workspace_env.py @@ -0,0 +1,173 @@ +# this env is designed for workflow owner filesystem, support file/directory operations + +import subprocess +import tempfile +import threading +import traceback +import time +import ast +import sys +import os +import re + +from .environment import Environment,EnvironmentEvent +from .ai_function import AIFunction,SimpleAIFunction + + +class CodeInterpreter: + def __init__(self, language, debug_mode): + self.language = language + self.proc = None + self.active_line = None + self.debug_mode = debug_mode + + def start_process(self): + start_cmd = sys.executable + " -i -q -u" + self.proc = subprocess.Popen(start_cmd.split(), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=0) + + # Start watching ^ its `stdout` and `stderr` streams + threading.Thread(target=self.save_and_display_stream, + args=(self.proc.stdout, False), # Passes False to is_error_stream + daemon=True).start() + threading.Thread(target=self.save_and_display_stream, + args=(self.proc.stderr, True), # Passes True to is_error_stream + daemon=True).start() + + def warp_code(self,pycode:str)->str: + # Add import traceback + code = "import traceback\n" + pycode + # Parse the input code into an AST + parsed_code = ast.parse(code) + # Wrap the entire code's AST in a single try-except block + try_except = ast.Try( + body=parsed_code.body, + handlers=[ + ast.ExceptHandler( + type=ast.Name(id="Exception", ctx=ast.Load()), + name=None, + body=[ + ast.Expr( + value=ast.Call( + func=ast.Attribute(value=ast.Name(id="traceback", ctx=ast.Load()), attr="print_exc", ctx=ast.Load()), + args=[], + keywords=[] + ) + ), + ] + ) + ], + orelse=[], + finalbody=[] + ) + + parsed_code.body = [try_except] + return ast.unparse(parsed_code) + + def run(self,py_code:str): + """ + Executes code. + """ + # Get code to execute + self.code = py_code + + # Start the subprocess if it hasn't been started + if not self.proc: + try: + self.start_process() + except Exception as e: + # Sometimes start_process will fail! + # Like if they don't have `node` installed or something. + + traceback_string = traceback.format_exc() + self.output = traceback_string + # Before you return, wait for the display to catch up? + # (I'm not sure why this works) + time.sleep(0.1) + + return self.output + + self.output = "" + + self.print_cmd = 'print("{}")' + code = self.warp_code(py_code) + + if self.debug_mode: + print("Running code:") + print(code) + print("---") + + self.done = threading.Event() + self.done.clear() + + # Write code to stdin of the process + try: + self.proc.stdin.write(code + "\n") + self.proc.stdin.flush() + except BrokenPipeError: + return + self.done.wait() + time.sleep(0.1) + return self.output + + def save_and_display_stream(self, stream, is_error_stream): + + for line in iter(stream.readline, ''): + if self.debug_mode: + print("Recieved output line:") + print(line) + print("---") + + line = line.strip() + if is_error_stream and "KeyboardInterrupt" in line: + raise KeyboardInterrupt + elif "END_OF_EXECUTION" in line: + self.done.set() + self.active_line = None + else: + self.output += "\n" + line + self.output = self.output.strip() + + + +class WorkspaceEnvironment(Environment): + def __init__(self, env_id: str) -> None: + super().__init__(env_id) + + operator_param = { + "command": "command will execute", + } + self.add_ai_function(SimpleAIFunction("shell_exec", + "execute shell command in linux bash", + self.shell_exec,operator_param)) + + #run_code_param = { + # "pycode": "python code will execute", + #} + #self.add_ai_function(SimpleAIFunction("run_code", + # "execute python code", + # self.run_code,run_code_param)) + + + async def shell_exec(self,command:str) -> str: + import asyncio.subprocess + process = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + returncode = process.returncode + if returncode == 0: + return f"Execute success! stdout is:\n{stdout}\n" + else: + return f"Execute failed! stderr is:\n{stderr}\n" + + async def run_code(self,pycode:str) -> str: + interpreter = CodeInterpreter("python",True) + return interpreter.run(pycode) + diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 984bb08..49224b8 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -64,7 +64,6 @@ class AIOS_Shell: target_id = msg.target.split(".")[0] agent : AIAgent = await AgentManager.get_instance().get(target_id) if agent is not None: - agent.owner_env = Environment.get_env_by_id("calender") bus.register_message_handler(target_id,agent._process_msg) return True @@ -87,6 +86,9 @@ class AIOS_Shell: await cal_env.start() Environment.set_env_by_id("calender",cal_env) + workspace_env = WorkspaceEnvironment("bash") + Environment.set_env_by_id("bash",workspace_env) + await AgentManager.get_instance().initial() await WorkflowManager.get_instance().initial() diff --git a/test/test_contac_manager.py b/test/test_contac_manager.py index 4272b16..9dbd39d 100644 --- a/test/test_contac_manager.py +++ b/test/test_contac_manager.py @@ -1,6 +1,6 @@ import unittest -import os import toml +import os import sys directory = os.path.dirname(__file__) diff --git a/test/workflow_test.py b/test/workflow_test.py index c985cb5..3e5e586 100644 --- a/test/workflow_test.py +++ b/test/workflow_test.py @@ -1,9 +1,22 @@ +import os +import sys +import asyncio +directory = os.path.dirname(__file__) +sys.path.append(directory + '/../src') +from aios_kernel import WorkspaceEnvironment -def test_workflow(): +async def test_workflow(): + env = WorkspaceEnvironment("test") + + test_code =""" +import toml +print("hello world") +print(100+23) +toml.dump({"abc":"123"},open("test.toml","w")) + """ + await env.run_code(test_code) - - pass async def _test_llm_parser(): @@ -39,4 +52,9 @@ sfsadfasdf llm_result = Workflow.prase_llm_result(test_llm_result) assert len(llm_result.calls) == 1 assert len(llm_result.send_msgs) == 1 - print(llm_result) \ No newline at end of file + print(llm_result) + +if __name__ == "__main__": + asyncio.run(test_workflow()) + print("OK!") + From 0d556003c5794dca1c91ca641364cef6ea82f9d7 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 22 Sep 2023 00:05:32 -0700 Subject: [PATCH 106/177] fix bug --- src/aios_kernel/contact_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aios_kernel/contact_manager.py b/src/aios_kernel/contact_manager.py index ca5b7c5..225e9a1 100644 --- a/src/aios_kernel/contact_manager.py +++ b/src/aios_kernel/contact_manager.py @@ -10,6 +10,7 @@ class Contact: self.added_by = added_by self.tags = tags self.notes = notes + self.is_family_member = False def to_dict(self): return { From c289c811eefab63c03ad333177fee145b62e1435 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 22 Sep 2023 20:11:27 -0700 Subject: [PATCH 107/177] fix bugs --- rootfs/agents/linux_admin/agent.toml | 2 ++ src/aios_kernel/agent.py | 7 +++---- src/knowledge/store.py | 2 +- src/requirements.txt | 13 +++++++++---- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/rootfs/agents/linux_admin/agent.toml b/rootfs/agents/linux_admin/agent.toml index 9eaef70..a984d30 100644 --- a/rootfs/agents/linux_admin/agent.toml +++ b/rootfs/agents/linux_admin/agent.toml @@ -1,6 +1,8 @@ instance_id = "linux_admin" fullname = "linux_admin" owner_env = "bash" +#llm_model_name = "gpt-3.5-turbo-16k-0613" +#max_token_size = 16000 [[prompt]] role = "system" content = """你的名字是linux_admin,是非常资深的linux系统管理员,我理解一些linux,但对linux的sh命令记得不太清楚了。 diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index c21e6d8..7681798 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -280,11 +280,11 @@ class AIAgent: def _get_inner_functions(self) -> dict: if self.owner_env is None: - return None + return None,0 all_inner_function = self.owner_env.get_all_ai_functions() if all_inner_function is None: - return None + return None,0 result_func = [] result_len = 0 @@ -295,8 +295,7 @@ class AIAgent: if func_name not in self.enable_function_list: logger.debug(f"ageint {self.agent_id} ignore inner func:{func_name}") continue - else: - continue + this_func = {} this_func["name"] = func_name this_func["description"] = inner_func.get_description() diff --git a/src/knowledge/store.py b/src/knowledge/store.py index 56ec64b..adcc85a 100644 --- a/src/knowledge/store.py +++ b/src/knowledge/store.py @@ -60,5 +60,5 @@ class KnowledgeStore: def get_vector_store(self, model_name: str) -> VectorBase: if model_name not in self.vector_store: - self.vector_store[model_name] = ChromaVectorStore(self.root, model_name) + self.vector_store[model_name] = ChromaVectorStore(model_name) return self.vector_store[model_name] diff --git a/src/requirements.txt b/src/requirements.txt index 753ce51..e96eb07 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -30,8 +30,8 @@ google-auth==2.23.0 google-cloud==0.34.0 google-cloud-texttospeech==2.14.1 googleapis-common-protos==1.60.0 -grpcio==1.58.0 -grpcio-status==1.58.0 +grpcio +grpcio-status h11==0.14.0 httpcore==0.17.3 httptools==0.6.0 @@ -58,7 +58,7 @@ posthog==3.0.2 proglog==0.1.10 prompt-toolkit==3.0.39 proto-plus==1.22.3 -protobuf==4.24.3 +protobuf pulsar-client==3.3.0 pyasn1==0.5.0 pyasn1-modules==0.3.0 @@ -81,4 +81,9 @@ sympy==1.12 telegram==0.0.1 tokenizers==0.14.0 toml==0.10.0 -chardet \ No newline at end of file +pysocks +chardet +pydub +aiosqlite +python-telegram-bot +pydub \ No newline at end of file From 85259ca4082504265a5ec099330b2c171eb8fa44 Mon Sep 17 00:00:00 2001 From: Song Date: Sat, 23 Sep 2023 10:44:57 +0800 Subject: [PATCH 108/177] Add paint function for agent David --- rootfs/agents/David/agent.toml | 10 +++++- src/aios_kernel/__init__.py | 1 + src/aios_kernel/compute_kernel.py | 32 +++++++++++++++++ src/aios_kernel/compute_task.py | 11 ++++++ src/aios_kernel/local_stability_node.py | 48 ++++++++++++++++++------- src/aios_kernel/workflow_env.py | 22 ++++++++++-- src/service/aios_shell/aios_shell.py | 11 ++++++ test/test_local_sd_node.py | 2 +- 8 files changed, 120 insertions(+), 17 deletions(-) diff --git a/rootfs/agents/David/agent.toml b/rootfs/agents/David/agent.toml index 3462da9..47687f3 100644 --- a/rootfs/agents/David/agent.toml +++ b/rootfs/agents/David/agent.toml @@ -1,6 +1,14 @@ instance_id = "David" fullname = "David" +max_token_size = 16000 +llm_model_name = "gpt-3.5-turbo-16k-0613" [[prompt]] role = "system" -content = "你的名字是David,是一个擅长各种风格的画家。你在收到任何绘画请求时,会尝试产生一组英文单词来说明你想画的画" +content = """你的名字是David,是一个擅长各种风格的画家。你在收到任何绘画请求时,会尝试产生一组英文单词或者短语来描述你想画的画。 +将这些英文单词或者短语作为paint函数的prompt参数。 +当提到风格的时候,需要将风格添加进prompt参数中,而不是作为model_name。 +只有当明确提到用什么模型时,才会去设置model_name参数,否则不要传递model_name参数。 +传递给paint函数的prompt参数一定是一组英文短语,不要用其他语言,如果是其他语言,就将其翻译成英文再传递。 +如果函数调用返回任何错误,直接将错误的原文显示出来并停止。 +如果绘制成功,就停止并输出文件的本地路径。""" diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py index 26aadb9..f850184 100644 --- a/src/aios_kernel/__init__.py +++ b/src/aios_kernel/__init__.py @@ -21,6 +21,7 @@ from .storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem from .contact_manager import ContactManager,Contact,FamilyMember from .text_to_speech_function import TextToSpeechFunction from .workspace_env import WorkspaceEnvironment +from .local_stability_node import Local_Stability_ComputeNode AIOS_Version = "0.5.1, build 2023-9-17" diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py index fffc8ac..661110b 100644 --- a/src/aios_kernel/compute_kernel.py +++ b/src/aios_kernel/compute_kernel.py @@ -197,3 +197,35 @@ class ComputeKernel: raise Exception("do_text_to_speech failed!") + def text_2_image(self, prompt:str, model_name:Optional[str] = None): + task = ComputeTask() + task.set_text_2_image_params(prompt,model_name) + self.run(task) + return task + + async def do_text_2_image(self, prompt:str, model_name:Optional[str] = None) -> [str, ComputeTaskResult]: + task_req = self.text_2_image(prompt,model_name) + async def check_timer(): + check_times = 0 + while True: + if task_req.state == ComputeTaskState.DONE: + break + + if task_req.state == ComputeTaskState.ERROR: + break + + #may be it will take a long time to generate a image + if check_times >= 60: + 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 None, task_req.result + + return task_req.error_str, None + diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py index c6053db..56a8387 100644 --- a/src/aios_kernel/compute_task.py +++ b/src/aios_kernel/compute_task.py @@ -64,6 +64,17 @@ class ComputeTask: else: self.params["model_name"] = "text-embedding-ada-002" self.params["input"] = input + + def set_text_2_image_params(self, prompt: str, model_name, callchain_id=None): + self.task_type = ComputeTaskType.TEXT_2_IMAGE + self.create_time = time.time() + self.task_id = uuid.uuid4().hex + self.callchain_id = callchain_id + self.params["prompt"] = prompt + if model_name is not None: + self.params["model_name"] = model_name + else: + self.params["model_name"] = "v1-5-pruned-emaonly" def display(self) -> str: return f"ComputeTask: {self.task_id} {self.task_type} {self.state}" diff --git a/src/aios_kernel/local_stability_node.py b/src/aios_kernel/local_stability_node.py index 992e00f..8ee165a 100644 --- a/src/aios_kernel/local_stability_node.py +++ b/src/aios_kernel/local_stability_node.py @@ -6,6 +6,7 @@ import logging import base64 from PIL import Image import requests +from typing import Tuple from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode @@ -73,6 +74,8 @@ class Local_Stability_ComputeNode(ComputeNode): if self.output_dir is None: self.output_dir = "./" + + self.output_dir = os.path.abspath(self.output_dir) self.start() @@ -85,36 +88,53 @@ class Local_Stability_ComputeNode(ComputeNode): async def remove_task(self, task_id: str): pass + def _make_post_request(self, url, json) -> Tuple[str, requests.Response]: + try: + response = requests.post(url, json=json) + if response.status_code != 200: + return f'{response.status_code}, {response.json()}', None + return None, response + except Exception as e: + return f"{e}", None + + def _run_task(self, task: ComputeTask): task.state = ComputeTaskState.RUNNING model_name = task.params["model_name"] - prompts = task.params["prompts"] + prompt = task.params["prompt"] - logging.info(f"call local stability {model_name} prompts: {prompts}") + logging.info(f"call local stability {model_name} prompts: {prompt}") if model_name is not None: payload = { "sd_model_checkpoint": model_name, } - # {'error': 'RuntimeError', 'detail': '', 'body': '', 'errors': "model 'xxx' not found"} - response = requests.post( - url=f'{self.url}/sdapi/v1/options', json=payload) - if response.status_code != 200: + err, resp = self._make_post_request(f'{self.url}/sdapi/v1/options', payload) + + if err is not None: task.state = ComputeTaskState.ERROR - logger.error( - f"set local stability model failed. err:{response.json()['errors']}") + err_msg = f"Set local stability model failed. err:{err}" + logger.error(err_msg) + task.error_str = err_msg return None + logging.info(f"set local stability model {model_name} success") + payload = { - "prompt": prompts, + "prompt": prompt, "steps": 20 } - response = requests.post( - url=f'{self.url}/sdapi/v1/txt2img', json=payload) - r = response.json() + err, resp = self._make_post_request(f'{self.url}/sdapi/v1/txt2img', payload) + if err is not None: + task.state = ComputeTaskState.ERROR + err_msg = f"Failed. err:{err}" + logger.error(err_msg) + task.error_str = err_msg + return None + + r = resp.json() - print(len(r['images'])) for i in r['images']: image = Image.open(io.BytesIO( base64.b64decode(i.split(",", 1)[0]))) @@ -128,6 +148,8 @@ class Local_Stability_ComputeNode(ComputeNode): return result + task.error_str = "Unknown error!" + task.state = ComputeTaskState.ERROR return None def start(self): diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index b7211ef..f872e5b 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -9,6 +9,7 @@ import logging from typing import Optional from .text_to_speech_function import TextToSpeechFunction +from .compute_kernel import ComputeKernel from .environment import Environment,EnvironmentEvent from .ai_function import SimpleAIFunction from .storage import AIStorage @@ -82,6 +83,16 @@ class CalenderEnvironment(Environment): "update event in calender", self._update_event,update_param)) + + #maybe this function should be in other env? + paint_param = { + "prompt": "A description of the content of the painting", + "model_name": "Which model to use to draw the picture, can be None" + } + self.add_ai_function(SimpleAIFunction("paint", + "Draw a picture according to the description", + self._paint,paint_param)) + #self.add_ai_function(SimpleAIFunction("user_confirm", # "user confirm", # self._user_confirm)) @@ -247,6 +258,14 @@ class CalenderEnvironment(Environment): formatted_time = now.strftime('%Y-%m-%d %H:%M:%S') return formatted_time + + async def _paint(self, prompt, model_name = None) -> str: + err, result = await ComputeKernel.get_instance().do_text_2_image(prompt, model_name) + if err is not None: + return f"exec paint failed. err:{err}" + else: + return f'exec paint OK, saved as a local file, path is: {result.result["file"]}' + # Default Workflow Environment(Context) class WorkflowEnvironment(Environment): def __init__(self, env_id: str,db_file:str) -> None: @@ -327,5 +346,4 @@ class WorkflowEnvironment(Environment): logging.error(f"Error occurred while update env{self.env_id}.{key} ,error:{e}") def get_functions(self): - pass - + pass \ No newline at end of file diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 254996e..a7a003e 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -23,6 +23,7 @@ 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,Local_Stability_ComputeNode import proxy from aios_kernel import * @@ -85,6 +86,10 @@ class AIOS_Shell: return False async def initial(self) -> bool: + os.environ["LOCAL_STABILITY_URL"] = "http://192.168.100.79:7866" + os.environ["TEXT2IMG_DEFAULT_MODEL"] = "v1-5-pruned-emaonly" + os.environ["TEXT2IMG_OUTPUT_DIR"] = "./" + cal_env = CalenderEnvironment("calender") await cal_env.start() Environment.set_env_by_id("calender",cal_env) @@ -114,6 +119,12 @@ class AIOS_Shell: await llama_ai_node.start() # ComputeKernel.get_instance().add_compute_node(llama_ai_node) + local_sd_node = Local_Stability_ComputeNode.get_instance() + if await local_sd_node.initial() is not True: + logger.error("local stability node initial failed!") + return False + ComputeKernel.get_instance().add_compute_node(local_sd_node) + await ComputeKernel.get_instance().start() AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) diff --git a/test/test_local_sd_node.py b/test/test_local_sd_node.py index 62659d1..ebba33b 100644 --- a/test/test_local_sd_node.py +++ b/test/test_local_sd_node.py @@ -30,7 +30,7 @@ async def test_local_sd_node(propmt, model): task.create_time = time.time() task.task_id = uuid.uuid4().hex task.params['model_name'] = model - task.params['prompts'] = propmt + task.params['prompt'] = propmt await node.push_task(task) while True: From 2be2ef3f604e24f524435fd7d6699207080fba34 Mon Sep 17 00:00:00 2001 From: Song Date: Sat, 23 Sep 2023 13:19:18 +0800 Subject: [PATCH 109/177] Update code & pass test --- rootfs/agents/David/agent.toml | 1 + src/aios_kernel/agent.py | 4 ++-- src/aios_kernel/local_stability_node.py | 19 +++++++++++++------ src/aios_kernel/open_ai_node.py | 4 ++-- src/service/aios_shell/aios_shell.py | 6 ++---- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/rootfs/agents/David/agent.toml b/rootfs/agents/David/agent.toml index 47687f3..92cfcf1 100644 --- a/rootfs/agents/David/agent.toml +++ b/rootfs/agents/David/agent.toml @@ -2,6 +2,7 @@ instance_id = "David" fullname = "David" max_token_size = 16000 llm_model_name = "gpt-3.5-turbo-16k-0613" +owner_env = "calender" [[prompt]] role = "system" diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py index 7681798..79e67e6 100644 --- a/src/aios_kernel/agent.py +++ b/src/aios_kernel/agent.py @@ -123,7 +123,7 @@ class AIAgent: self.unread_msg = Queue() # msg from other agent self.owner_env : Environment = None self.owenr_bus = None - self.enable_function_list = [] + self.enable_function_list = None @classmethod def create_from_templete(cls,templete:AIAgentTemplete, fullname:str): @@ -392,7 +392,7 @@ class AIAgent: prompt.append(msg_prompt) - logger.debug(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len},totoal prompt:{system_prompt_len + function_token_len + history_token_len} ") + logger.debug(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len}, totoal prompt:{system_prompt_len + function_token_len + history_token_len} ") task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions) final_result = task_result.result_str diff --git a/src/aios_kernel/local_stability_node.py b/src/aios_kernel/local_stability_node.py index 8ee165a..77971b8 100644 --- a/src/aios_kernel/local_stability_node.py +++ b/src/aios_kernel/local_stability_node.py @@ -7,6 +7,7 @@ import base64 from PIL import Image import requests from typing import Tuple +from pathlib import Path from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_node import ComputeNode @@ -27,12 +28,18 @@ class Local_Stability_ComputeNode(ComputeNode): @classmethod def declare_user_config(cls): user_config = AIStorage.get_instance().get_user_config() - user_config.add_user_config( - "local_stability_url", "local stability url", False, None) - user_config.add_user_config( - "text2img_output_dir", "output dir", True, "./") - user_config.add_user_config( - "text2img_default_model", "text2img default model", True, "v1-5-pruned-emaonly") + if os.getenv("LOCAL_STABILITY_URL") is None: + user_config.add_user_config( + "local_stability_url", "local stability url", False, None) + if os.getenv("TEXT2IMG_OUTPUT_DIR") is None: + home_dir = Path.home() + output_dir = Path.joinpath(home_dir, "text2img_output") + Path.mkdir(output_dir, exist_ok=True) + user_config.add_user_config( + "text2img_output_dir", "text2image output dir", True, output_dir) + if os.getenv("TEXT2IMG_DEFAULT_MODEL") is None: + user_config.add_user_config( + "text2img_default_model", "text2img default model", True, "v1-5-pruned-emaonly") def __init__(self) -> None: super().__init__() diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py index e610be7..bf226df 100644 --- a/src/aios_kernel/open_ai_node.py +++ b/src/aios_kernel/open_ai_node.py @@ -106,14 +106,14 @@ class OpenAI_ComputeNode(ComputeNode): logger.info(f"call openai {mode_name} prompts: {prompts}") resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, - max_tokens=result_token, + #max_tokens=result_token, temperature=0.7) else: logger.info(f"call openai {mode_name} prompts: {prompts} functions: {json.dumps(llm_inner_functions)}") resp = openai.ChatCompletion.create(model=mode_name, messages=prompts, functions=llm_inner_functions, - max_tokens=result_token, + #max_tokens=result_token, temperature=0.7) # TODO: add temperature to task params? diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index a7a003e..69d9b54 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -63,6 +63,8 @@ class AIOS_Shell: google_text_to_speech = GoogleTextToSpeechNode.get_instance() google_text_to_speech.declare_user_config() + Local_Stability_ComputeNode.declare_user_config() + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: target_id = msg.target.split(".")[0] @@ -86,10 +88,6 @@ class AIOS_Shell: return False async def initial(self) -> bool: - os.environ["LOCAL_STABILITY_URL"] = "http://192.168.100.79:7866" - os.environ["TEXT2IMG_DEFAULT_MODEL"] = "v1-5-pruned-emaonly" - os.environ["TEXT2IMG_OUTPUT_DIR"] = "./" - cal_env = CalenderEnvironment("calender") await cal_env.start() Environment.set_env_by_id("calender",cal_env) From 2662f8569fd385bbd17f1e10b03a59a4baab5419 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Fri, 22 Sep 2023 23:45:36 -0700 Subject: [PATCH 110/177] fix bug --- src/aios_kernel/chatsession.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py index ceb9821..1b524d8 100644 --- a/src/aios_kernel/chatsession.py +++ b/src/aios_kernel/chatsession.py @@ -264,7 +264,6 @@ class AIChatSession: return self.owner_id def read_history(self, number:int=10,offset=0) -> [AgentMsg]: - return [] msgs = self.db.get_messages(self.session_id, number, offset) result = [] for msg in msgs: From 55832c6ad42523ff84d3a22c00f0e08c7f9d4fd0 Mon Sep 17 00:00:00 2001 From: wugren Date: Sun, 24 Sep 2023 16:26:18 +0800 Subject: [PATCH 111/177] Story maker support telegram --- rootfs/workflows/story_maker/workflow.toml | 17 ++++++++++---- src/aios_kernel/tg_tunnel.py | 27 ++++++++++++++-------- src/aios_kernel/workflow.py | 10 ++++---- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/rootfs/workflows/story_maker/workflow.toml b/rootfs/workflows/story_maker/workflow.toml index fd3876a..4f9fbef 100644 --- a/rootfs/workflows/story_maker/workflow.toml +++ b/rootfs/workflows/story_maker/workflow.toml @@ -13,13 +13,17 @@ enable_function = [] [[roles.manager.prompt]] role="system" content=""" -你是一个语音故事制作总导演,与客户对接并向团队下达指令。你的团队分为下面几个成员:writer,speecher。一个故事制作分成两个阶段:让writer写出故事,再交由speecher演播故事生成音频文件。你的基本工作模式是: +你当前的职位是语音故事制作总负责人,负责与客户对接并向团队下达指令,不需要自己直接完成任务。你的团队分为下面两个成员:writer,speecher。一个故事制作分成两个阶段:让writer写出故事,再交由speecher演播故事生成音频文件。你的基本工作模式是: 1. 收到客户的明确的指令后,让writer写出故事 2. 将writer写出的故事交给speecher演播 -3. 获得音频文件之后,将音频文件的存放路径交给客户 +3. 获得音频文件之后,整个任务已完成,将音频文件的存放路径以如下格式返回给客户: +``` +故事制作完成。 +audio file:音频文件路径 +``` 4. 当你决定要和成员通信时,请使用下面形式输出需要通信的消息 ``` -##/send_msg 成员名称 +##/send_msg "成员名称" 内容 ``` """ @@ -39,4 +43,9 @@ agent = "speecher" enable_function = ["text_to_speech"] [[roles.speecher.prompt]] role="system" -content="" +content="""你现在的职责是演播一个故事,故事完成之后请以如下格式返回: +``` +故事演播完成。 +audio file:音频文件路径 +``` +""" diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index 4cad825..735da43 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -26,10 +26,10 @@ class TelegramTunnel(AgentTunnel): 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"] @@ -57,20 +57,20 @@ class TelegramTunnel(AgentTunnel): await self.on_message(bot,update) return next_update_id - + return update_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.is_start = True logger.info(f"tunnel {self.tunnel_id} is starting...") self.bot = Bot(self.tg_token) self.update_queue = asyncio.Queue() self.bot_updater = Updater(self.bot,update_queue=self.update_queue) - + async def _run_app(): try: @@ -116,7 +116,7 @@ class TelegramTunnel(AgentTunnel): if update.effective_user.is_bot: logger.warning(f"ignore message from telegram bot {update.effective_user.id}") return None - + cm : ContactManager = ContactManager.get_instance() reomte_user_name = f"{update.effective_user.id}@telegram" contact : Contact = cm.find_contact_by_telegram(update.effective_user.username) @@ -148,6 +148,14 @@ class TelegramTunnel(AgentTunnel): await update.message.reply_text(f"{self.target_id} process message error") else: if resp_msg.body_mime is None: + if resp_msg.body is not None: + pos = resp_msg.body.find("audio file") + if pos != -1: + audio_file = resp_msg.body[pos+11:].strip() + if audio_file.startswith("\""): + audio_file = audio_file[1:-1] + await update.message.reply_voice(audio_file) + return await update.message.reply_text(resp_msg.body) else: if resp_msg.body_mime.startswith("image"): @@ -157,9 +165,8 @@ class TelegramTunnel(AgentTunnel): photo_file.close() else: await update.message.reply_text(resp_msg.body) - + else: await update.message.reply_text(resp_msg.body) - - - \ No newline at end of file + + diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py index 00bb255..f85d153 100644 --- a/src/aios_kernel/workflow.py +++ b/src/aios_kernel/workflow.py @@ -390,11 +390,11 @@ class Workflow: result_func.append(this_func) if len(result_func) > 0: return result_func - return None - + return None + async def _role_execute_func(self,the_role:AIRole,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg,stack_limit = 5) -> str: from .compute_kernel import ComputeKernel - + func_name = inenr_func_call_node.get("name") arguments = json.loads(inenr_func_call_node.get("arguments")) @@ -418,7 +418,7 @@ class Workflow: if stack_limit > 0: 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,stack_limit-1) + return await self._role_execute_func(the_role,inner_func_call_node,prompt,org_msg,stack_limit-1) else: return task_result.result_str @@ -440,7 +440,7 @@ class Workflow: prompt.append(await self._get_prompt_from_session(workflow_chat_session)) msg_prompt = AgentPrompt() - msg_prompt.messages = [{"role":"user","content":f"{msg.sender}:{msg.body}"}] + msg_prompt.messages = [{"role":"user","content":f"user name is {msg.sender}, his question is :{msg.body}"}] prompt.append(msg_prompt) self._format_msg_by_env_value(prompt) From 7f11edb70656dafc85d1154d4a827378de1215d9 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Mon, 25 Sep 2023 20:57:10 -0700 Subject: [PATCH 112/177] Jarvis can update contract, aios_shell can update contract. --- src/aios_kernel/contact_manager.py | 17 +++- src/aios_kernel/google_text_to_speech_node.py | 2 +- src/aios_kernel/local_stability_node.py | 2 +- src/aios_kernel/storage.py | 16 ++++ src/aios_kernel/tg_tunnel.py | 8 +- src/aios_kernel/workflow_env.py | 51 ++++++++++ src/service/aios_shell/aios_shell.py | 94 ++++++++++++++++++- 7 files changed, 182 insertions(+), 8 deletions(-) diff --git a/src/aios_kernel/contact_manager.py b/src/aios_kernel/contact_manager.py index 225e9a1..39c5711 100644 --- a/src/aios_kernel/contact_manager.py +++ b/src/aios_kernel/contact_manager.py @@ -49,7 +49,7 @@ class ContactManager: @classmethod def get_instance(cls,filename=None) -> "ContactManager": if cls._instance is None: - cls._instance = ContactManager(filename) + cls._instance = ContactManager(str(filename)) return cls._instance def __init__(self, filename="contacts.toml"): @@ -79,6 +79,21 @@ class ContactManager: with open(self.filename, "w") as f: toml.dump(data, f) + def set_contact(self, name:str, new_contact:Contact): + assert name == new_contact.name + for i, contact in enumerate(self.contacts): + if contact.name == name: + self.contacts[i] = new_contact + self.save_data() + return True + for i, member in enumerate(self.family_members): + if member.name == name: + self.family_members[i] = new_contact + self.save_data() + return True + + return False + def add_contact(self, name:str, new_contact:Contact): assert name == new_contact.name self.contacts.append(new_contact) diff --git a/src/aios_kernel/google_text_to_speech_node.py b/src/aios_kernel/google_text_to_speech_node.py index 9bee115..b76d0fc 100644 --- a/src/aios_kernel/google_text_to_speech_node.py +++ b/src/aios_kernel/google_text_to_speech_node.py @@ -176,5 +176,5 @@ class GoogleTextToSpeechNode(ComputeNode): user_config = AIStorage.get_instance().get_user_config() user_config.add_user_config("google_application_credentials", "google application credentials, please visit:https://cloud.google.com/text-to-speech/docs/before-you-begin", - False, + True, None) diff --git a/src/aios_kernel/local_stability_node.py b/src/aios_kernel/local_stability_node.py index 77971b8..d773105 100644 --- a/src/aios_kernel/local_stability_node.py +++ b/src/aios_kernel/local_stability_node.py @@ -30,7 +30,7 @@ class Local_Stability_ComputeNode(ComputeNode): user_config = AIStorage.get_instance().get_user_config() if os.getenv("LOCAL_STABILITY_URL") is None: user_config.add_user_config( - "local_stability_url", "local stability url", False, None) + "local_stability_url", "local stability url", True, None) if os.getenv("TEXT2IMG_OUTPUT_DIR") is None: home_dir = Path.home() output_dir = Path.joinpath(home_dir, "text2img_output") diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index d57546e..211129d 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -13,6 +13,10 @@ class ResourceLocation: def __init__(self) -> None: pass +class FeatureItem: + def __init__(self) -> None: + pass + class UserConfigItem: def __init__(self,desc:str=None) -> None: self.default_value = None @@ -142,6 +146,18 @@ class AIStorage: 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) + async def enable_feature(self,feature_name:str) -> None: + pass + + async def disable_feature(self,feature_name:str) -> None: + pass + + async def set_feature_init_result(self,feature_name:str,result:bool) -> None: + pass + + async def is_feature_enable(self,feature_name:str) -> bool: + pass + def get_user_config(self) -> UserConfig: return self.user_config diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py index 735da43..7062a99 100644 --- a/src/aios_kernel/tg_tunnel.py +++ b/src/aios_kernel/tg_tunnel.py @@ -10,6 +10,7 @@ from telegram.ext import Updater from telegram.error import Forbidden, NetworkError from .tunnel import AgentTunnel +from .storage import AIStorage from .contact_manager import ContactManager,Contact,FamilyMember from .agent_message import AgentMsg @@ -119,6 +120,10 @@ class TelegramTunnel(AgentTunnel): cm : ContactManager = ContactManager.get_instance() reomte_user_name = f"{update.effective_user.id}@telegram" + #owner_tg_username = AIStorage.get_instance().get_user_config().get_value("telegram") + #owner_name = AIStorage.get_instance().get_user_config().get_value("username") + + contact : Contact = cm.find_contact_by_telegram(update.effective_user.username) if contact is None: contact = cm.find_contact_by_telegram(str(update.effective_user.id)) @@ -136,8 +141,7 @@ class TelegramTunnel(AgentTunnel): contact.added_by = self.target_id cm.add_contact(contact.name, contact) reomte_user_name = contact.name - - # create gust account? + 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) diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py index f872e5b..39dda7b 100644 --- a/src/aios_kernel/workflow_env.py +++ b/src/aios_kernel/workflow_env.py @@ -13,6 +13,7 @@ from .compute_kernel import ComputeKernel from .environment import Environment,EnvironmentEvent from .ai_function import SimpleAIFunction from .storage import AIStorage +from .contact_manager import ContactManager,Contact,FamilyMember import aiosqlite @@ -93,6 +94,17 @@ class CalenderEnvironment(Environment): "Draw a picture according to the description", self._paint,paint_param)) + self.add_ai_function(SimpleAIFunction("get_contact", + "get contact info", + self._get_contact,{"name":"name of contact"})) + + self.add_ai_function(SimpleAIFunction("set_contact", + "set contact info", + self._set_contact,{"name":"name of contact","contact_info":"A json to descrpit contact"})) + + + + #self.add_ai_function(SimpleAIFunction("user_confirm", # "user confirm", # self._user_confirm)) @@ -222,6 +234,45 @@ class CalenderEnvironment(Environment): def _do_get_value(self,key:str) -> Optional[str]: return None + + async def _get_contact(self,name:str) -> str: + cm = ContactManager.get_instance() + contact : Contact = cm.find_contact_by_name(name) + if contact: + s = json.dumps(contact.to_dict()) + return f"Execute get_contact OK , contact {name} is {s}" + else: + return f"Execute get_contact OK , contact {name} not found!" + + async def _set_contact(self,name:str,contact_info:str) -> str: + cm = ContactManager.get_instance() + contact = cm.find_contact_by_name(name) + contact_info = json.loads(contact_info) + if contact is None: + contact = Contact(name) + contact.email = contact_info.get("email") + contact.telegram = contact_info.get("telegram") + contact.notes = contact_info.get("notes") + contact.added_by = self.env_id + + cm.add_contact(name,contact) + + return f"Execute set_contact OK , new contact {name} added!" + else: + if contact_info.get("email") is not None: + contact.email = contact_info.get("email") + if contact_info.get("telegram") is not None: + contact.telegram = contact_info.get("telegram") + if contact_info.get("notes") is not None: + contact.notes = contact_info.get("notes") + + contact.added_by = self.env_id + cm.set_contact(name,contact) + + return f"Execute set_contact OK , contact {name} updated!" + + + async def start(self) -> None: if self.is_run: diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index 69d9b54..ca43eca 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -24,6 +24,7 @@ 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,Local_Stability_ComputeNode +from aios_kernel import ContactManager,Contact import proxy from aios_kernel import * @@ -51,8 +52,14 @@ class AIOS_Shell: self.is_working = True def declare_all_user_config(self): + cm_path = AIStorage.get_instance().get_myai_dir() / "contacts.toml" + cm = ContactManager.get_instance(cm_path) + cm.load_data() + user_config = AIStorage.get_instance().get_user_config() - user_config.add_user_config("username","username is your full name when using AIOS",False,None,) + user_config.add_user_config("username","username is your full name when using AIOS",False,None) + user_config.add_user_config("telegram","Your telgram username",False,None) + user_config.add_user_config("email","Your email",False,None) openai_node = OpenAI_ComputeNode.get_instance() openai_node.declare_user_config() @@ -88,6 +95,17 @@ class AIOS_Shell: return False async def initial(self) -> bool: + cm = ContactManager.get_instance() + owenr = cm.find_contact_by_name(self.username) + if owenr is None: + owenr = Contact(self.username) + owenr.added_by = self.username + owenr.is_family_member = True + owenr.email = AIStorage.get_instance().get_user_config().get_value("email") + owenr.telegram = AIStorage.get_instance().get_user_config().get_value("telegram") + + cm.add_contact(self.username,owenr) + cal_env = CalenderEnvironment("calender") await cal_env.start() Environment.set_env_by_id("calender",cal_env) @@ -99,6 +117,7 @@ class AIOS_Shell: await AgentManager.get_instance().initial() await 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!") @@ -120,7 +139,7 @@ class AIOS_Shell: local_sd_node = Local_Stability_ComputeNode.get_instance() if await local_sd_node.initial() is not True: logger.error("local stability node initial failed!") - return False + ComputeKernel.get_instance().add_compute_node(local_sd_node) await ComputeKernel.get_instance().start() @@ -164,6 +183,7 @@ class AIOS_Shell: async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg: pass + async def get_tunnel_config_from_input(self,tunnel_target,tunnel_type): tunnel_config = {} @@ -206,6 +226,59 @@ class AIOS_Shell: except Exception as e: logger.warning(f"load tunnels config from {tunnels_config_path} failed!") + async def handle_contact_commands(self,args): + cm = ContactManager.get_instance() + if len(args) < 1: + return FormattedText([("class:error", f'/contact $contact_name, Like /contact "Jim Green"')]) + contact_name = args[0] + contact = cm.find_contact_by_name(contact_name) + is_update = False + if contact is not None: + #show old info and ask user to update or remove + is_update = True + op_str = await try_get_input(f"Contact {contact_name} already exist, update or remove? (u/r)") + if op_str is None: + return None + if op_str == "r": + cm.remove_contact(contact_name) + return FormattedText([("class:title", f"remove {contact_name} success!")]) + else: + print(f"old info: {contact}") + else: + contact = Contact(contact_name) + + contact.is_family_member = False + is_family_member = await try_get_input(f"Is {contact_name} your family member? (y/n)") + if is_family_member is not None: + if is_family_member == "y" or is_family_member == "Y": + contact.is_family_member = True + else: + return None + + contact_telegram = await try_get_input(f"Input {contact_name}'s telegram username:") + if contact_telegram is None: + return None + contact.telegram = contact_telegram + + contact_email = await try_get_input(f"Input {contact_name}'s email:") + if contact_email is None: + return None + contact.email = contact_email + + contact_phone = await try_get_input(f"Input {contact_name}'s phone (optional):") + if contact_phone is not None: + contact.phone = contact_phone + + contact_note = await try_get_input(f"Input {contact_name}'s note (optional):") + if contact_note is not None: + contact.note = contact_note + + contact.added_by = self.username + if is_update: + cm.set_contact(contact_name,contact) + else: + cm.add_contact(contact_name,contact) + async def handle_knowledge_commands(self, args): show_text = FormattedText([("class:title", "sub command not support!\n" "/knowledge add email | dir\n" @@ -228,7 +301,7 @@ class AIOS_Shell: if error is not None: return FormattedText([("class:title", f"/knowledge add email failed {error}\n")]) else: - KnowledgePipline.get_instance().save_config() + KnowledgePipline.get_instance().save_cosnfig() if args[1] == "dir": config = dict() for key, item in KnowledgeDirSource.user_config_items(): @@ -299,11 +372,19 @@ class AIOS_Shell: return show_text case 'knowledge': return await self.handle_knowledge_commands(args) + case 'contact': + return await self.handle_contact_commands(args) case 'open': if len(args) >= 1: target_id = args[0] + else: + show_text = FormattedText([("class:error", "/open Need Target Agent/Workflow ID! like /open Jarvis default")]) + return show_text + if len(args) >= 2: topic = args[1] + else: + topic = "default" self.current_target = target_id self.current_topic = topic @@ -382,6 +463,12 @@ async def try_get_input(desc:str,check_func:callable = None) -> str: async def get_user_config_from_input(check_result:dict) -> bool: for key,item in check_result.items(): user_input = await try_get_input(f"System config {key} ({item.desc}) not define!") + if user_input is None: + if item.is_optional: + continue + else: + True + if len(user_input) > 0: AIStorage.get_instance().get_user_config().set_value(key,user_input) @@ -478,6 +565,7 @@ async def main(): '/history $num $offset', '/login $username', '/connect $target', + '/contact $name', '/knowledge add email | dir', '/knowledge journal [$topn]', '/knowledge query $query' From b271f8883e9eb426a4fd66a5df6d00e8464c3d60 Mon Sep 17 00:00:00 2001 From: Liu Zhicong Date: Tue, 26 Sep 2023 01:19:33 -0700 Subject: [PATCH 113/177] Add /enable /disable Feature --- src/aios_kernel/google_text_to_speech_node.py | 2 +- src/aios_kernel/knowledge_pipeline.py | 2 + src/aios_kernel/local_llama_compute_node.py | 3 + src/aios_kernel/storage.py | 23 +++- src/component/agent_manager/agent_manager.py | 4 +- .../workflow_manager/workflow_manager.py | 5 +- src/service/aios_shell/aios_shell.py | 119 +++++++++++++----- src/service/aios_shell/proxy.py | 8 +- 8 files changed, 124 insertions(+), 42 deletions(-) diff --git a/src/aios_kernel/google_text_to_speech_node.py b/src/aios_kernel/google_text_to_speech_node.py index b76d0fc..0fdba47 100644 --- a/src/aios_kernel/google_text_to_speech_node.py +++ b/src/aios_kernel/google_text_to_speech_node.py @@ -171,7 +171,7 @@ class GoogleTextToSpeechNode(ComputeNode): def is_local(self) -> bool: return False - def declare_user_config(self): + def declare_user_config(self,is_optional:bool = False): if os.getenv("GOOGLE_APPLICATION_CREDENTIALS") is None: user_config = AIStorage.get_instance().get_user_config() user_config.add_user_config("google_application_credentials", diff --git a/src/aios_kernel/knowledge_pipeline.py b/src/aios_kernel/knowledge_pipeline.py index d1ed109..7f90a08 100644 --- a/src/aios_kernel/knowledge_pipeline.py +++ b/src/aios_kernel/knowledge_pipeline.py @@ -322,6 +322,8 @@ class KnowledgePipline: if source_config['type'] == 'dir': self.add_dir_source(KnowledgeDirSource(source_config)) + return True + def __singleton_init__(self): self.knowledge_base = KnowledgeBase() self.email_sources = dict() diff --git a/src/aios_kernel/local_llama_compute_node.py b/src/aios_kernel/local_llama_compute_node.py index 7e50661..15eec79 100644 --- a/src/aios_kernel/local_llama_compute_node.py +++ b/src/aios_kernel/local_llama_compute_node.py @@ -78,6 +78,9 @@ class LocalLlama_ComputeNode(Queue_ComputeNode): } } + async def initial(self) -> bool: + return True + def display(self) -> str: return f"LocalLlama_ComputeNode: {self.node_id}" diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py index 211129d..b0d34c3 100644 --- a/src/aios_kernel/storage.py +++ b/src/aios_kernel/storage.py @@ -140,6 +140,7 @@ class AIStorage: def __init__(self) -> None: self.is_dev_mode = False self.user_config = UserConfig() + self.feature_init_results = {} async def initial(self)->bool: self.user_config.user_config_path = str(self.get_myai_dir() / "etc/system.cfg.toml") @@ -147,16 +148,30 @@ class AIStorage: await self.user_config.load_value_from_file(self.user_config.user_config_path,True) async def enable_feature(self,feature_name:str) -> None: - pass + self.user_config.set_value(f"feature.{feature_name}","True") + await self.user_config.save_to_user_config() + async def disable_feature(self,feature_name:str) -> None: - pass + self.user_config.set_value(f"feature.{feature_name}","False") + await self.user_config.save_to_user_config() async def set_feature_init_result(self,feature_name:str,result:bool) -> None: - pass + self.feature_init_results[feature_name] = result async def is_feature_enable(self,feature_name:str) -> bool: - pass + is_enable = self.user_config.get_value(f"feature.{feature_name}") + if is_enable is None: + return False + + init_result = self.feature_init_results.get(feature_name) + if init_result: + if init_result is False: + return False + + if is_enable == "True": + return True + return False def get_user_config(self) -> UserConfig: return self.user_config diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py index fecead2..083b680 100644 --- a/src/component/agent_manager/agent_manager.py +++ b/src/component/agent_manager/agent_manager.py @@ -41,8 +41,8 @@ class AgentManager: 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") + + return True async def scan_all_agent(self)->None: pass diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py index 39be2e4..ee5ef9e 100644 --- a/src/component/workflow_manager/workflow_manager.py +++ b/src/component/workflow_manager/workflow_manager.py @@ -36,7 +36,10 @@ class WorkflowManager: self.db_file = os.path.abspath(f"{user_data_dir}/messages.db") if self.workflow_env is None: - raise Exception("WorkflowManager initial failed") + logger.error(f"load workflow env failed!") + return False + + return True async def get_agent_default_workflow(self,agent_id:str) -> Workflow: pass diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py index ca43eca..3cde4b6 100644 --- a/src/service/aios_shell/aios_shell.py +++ b/src/service/aios_shell/aios_shell.py @@ -6,6 +6,7 @@ import logging import re import toml import shlex +from logging.handlers import RotatingFileHandler from typing import Any, Optional, TypeVar, Tuple, Sequence import argparse @@ -52,8 +53,9 @@ class AIOS_Shell: self.is_working = True def declare_all_user_config(self): - cm_path = AIStorage.get_instance().get_myai_dir() / "contacts.toml" - cm = ContactManager.get_instance(cm_path) + user_data_dir = AIStorage.get_instance().get_myai_dir() + contact_config_path =os.path.abspath(f"{user_data_dir}/contacts.toml") + cm = ContactManager.get_instance(contact_config_path) cm.load_data() user_config = AIStorage.get_instance().get_user_config() @@ -61,6 +63,9 @@ class AIOS_Shell: user_config.add_user_config("telegram","Your telgram username",False,None) user_config.add_user_config("email","Your email",False,None) + user_config.add_user_config("feature.llama","enable Local-llama feature",True,"False") + user_config.add_user_config("feature.aigc","enable AIGC feature",True,"False") + openai_node = OpenAI_ComputeNode.get_instance() openai_node.declare_user_config() @@ -72,6 +77,9 @@ class AIOS_Shell: Local_Stability_ComputeNode.declare_user_config() + + + async def _handle_no_target_msg(self,bus:AIBus,msg:AgentMsg) -> bool: target_id = msg.target.split(".")[0] @@ -113,48 +121,55 @@ class AIOS_Shell: workspace_env = WorkspaceEnvironment("bash") Environment.set_env_by_id("bash",workspace_env) + if await AgentManager.get_instance().initial() is not True: + logger.error("agent manager initial failed!") + return False + if await WorkflowManager.get_instance().initial() is not True: + logger.error("workflow manager initial failed!") + return False - await AgentManager.get_instance().initial() - await 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) - try: - google_text_to_speech_node = GoogleTextToSpeechNode.get_instance() - google_text_to_speech_node.init() - ComputeKernel.get_instance().add_compute_node(google_text_to_speech_node) - except Exception as e: - logger.error(f"google text to speech node initial failed! {e}") - return False + if await AIStorage.get_instance().is_feature_enable("llama"): + llama_ai_node = LocalLlama_ComputeNode() + if await llama_ai_node.initial() is True: + await llama_ai_node.start() + ComputeKernel.get_instance().add_compute_node(llama_ai_node) + else: + logger.error("llama node initial failed!") + await AIStorage.get_instance().set_feature_init_result("llama",False) - llama_ai_node = LocalLlama_ComputeNode() - await llama_ai_node.start() - # ComputeKernel.get_instance().add_compute_node(llama_ai_node) + if await AIStorage.get_instance().is_feature_enable("aigc"): + try: + google_text_to_speech_node = GoogleTextToSpeechNode.get_instance() + google_text_to_speech_node.init() + ComputeKernel.get_instance().add_compute_node(google_text_to_speech_node) + except Exception as e: + logger.error(f"google text to speech node initial failed! {e}") + await AIStorage.get_instance.set_feature_init_result("aigc",False) + + + local_sd_node = Local_Stability_ComputeNode.get_instance() + if await local_sd_node.initial() is True: + ComputeKernel.get_instance().add_compute_node(local_sd_node) + else: + logger.error("local stability node initial failed!") + await AIStorage.get_instance.set_feature_init_result("aigc",False) - local_sd_node = Local_Stability_ComputeNode.get_instance() - if await local_sd_node.initial() is not True: - logger.error("local stability node initial failed!") - - ComputeKernel.get_instance().add_compute_node(local_sd_node) await ComputeKernel.get_instance().start() AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg) AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) + KnowledgePipline.get_instance().initial() TelegramTunnel.register_to_loader() EmailTunnel.register_to_loader() - - user_data_dir = AIStorage.get_instance().get_myai_dir() - contact_config_path =os.path.abspath(f"{user_data_dir}/contacts.toml") - cm = ContactManager.get_instance(contact_config_path) - cm.load_data() - + user_data_dir = str(AIStorage.get_instance().get_myai_dir()) tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml") tunnel_config = None try: @@ -164,7 +179,7 @@ class AIOS_Shell: except Exception as e: logger.warning(f"load tunnels config from {tunnels_config_path} failed!") - KnowledgePipline.get_instance().initial() + return True @@ -392,6 +407,34 @@ class AIOS_Shell: AIStorage.get_instance().get_user_config().set_value("shell.current",f"{self.current_topic}@{self.current_target}") await AIStorage.get_instance().get_user_config().save_to_user_config() return show_text + case 'enable': + if len(args) >= 1: + feature = args[0] + else: + show_text = FormattedText([("class:error", "/enable Need Feature Name! like /enable llama")]) + return show_text + + if await AIStorage.get_instance().is_feature_enable(feature): + show_text = FormattedText([("class:title", f"Feature {feature} already enabled!")]) + return show_text + + AIStorage.get_instance().enable_feature(feature) + show_text = FormattedText([("class:title", f"Feature {feature} enabled!")]) + return show_text + case 'disable': + if len(args) >= 1: + feature = args[0] + else: + show_text = FormattedText([("class:error", "/disable Need Feature Name! like /disable llama")]) + return show_text + + if not await AIStorage.get_instance().is_feature_enable(feature): + show_text = FormattedText([("class:title", f"Feature {feature} already disabled!")]) + return show_text + + AIStorage.get_instance().disable_feature(feature) + show_text = FormattedText([("class:title", f"Feature {feature} disabled!")]) + return show_text case 'login': if len(args) >= 1: self.username = args[0] @@ -518,15 +561,25 @@ def print_welcome_screen(): async def main(): print_welcome_screen() print("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 + + if AIStorage.get_instance().is_dev_mode: + 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') + else: + log_file = f"{AIStorage.get_instance().get_myai_dir()}/logs/aios_shell.log" + handler = RotatingFileHandler(log_file, maxBytes=50*1024*1024, backupCount=100) + + logging.basicConfig(handlers=[handler], + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + is_daemon = False if os.name != 'nt': if os.getppid() == 1: @@ -552,6 +605,7 @@ async def main(): return 1 else: print("aios shell initial failed!") + return 1 print(f"aios shell {shell.get_version()} ready.") if is_daemon: @@ -559,7 +613,6 @@ async def main(): proxy.apply_storage() - #TODO: read last input config completer = WordCompleter(['/send $target $msg $topic', '/open $target $topic', '/history $num $offset', @@ -570,6 +623,8 @@ async def main(): '/knowledge journal [$topn]', '/knowledge query $query' '/set_config $key', + '/enable $feature', + '/disable $feature', '/list_config', '/show', '/exit', diff --git a/src/service/aios_shell/proxy.py b/src/service/aios_shell/proxy.py index caf006d..f8b124c 100644 --- a/src/service/aios_shell/proxy.py +++ b/src/service/aios_shell/proxy.py @@ -4,12 +4,15 @@ import os import logging import socket import socks +import logging directory = os.path.dirname(__file__) sys.path.append(directory + '/../../') from aios_kernel import AIStorage +logger = logging.getLogger(__name__) + def apply_storage(): proxy_cfg = AIStorage.get_instance().get_user_config().get_config_item("proxy") if proxy_cfg is None: @@ -31,9 +34,10 @@ def apply_storage(): (host, port) = host.split(":") socks.set_default_proxy(socks.SOCKS5, host, int(port), username = username, password = password) socket.socket = socks.socksocket - print(f"proxy {host_url} will be used.") + logger.info(f"proxy {host_url} will be used.") case _: - print(f"the proxy type ({proxy_type}) has not support.") + logger.error(f"the proxy type ({proxy_type}) has not support. proxy will not be used.") + def declare_user_config(): user_config = AIStorage.get_instance().get_user_config() From fb76066ea28bd607b0fea626ece56e36c97731df Mon Sep 17 00:00:00 2001 From: Song Date: Tue, 26 Sep 2023 16:40:52 +0800 Subject: [PATCH 114/177] Add paint env & test stability api --- c1e8c5afd3d447f2a58727107b5011b7.png | Bin 0 -> 253544 bytes f7bef6a77c3c4d0087553cf67b4e5f0a.png | Bin 0 -> 505478 bytes rootfs/agents/David/agent.toml | 2 +- src/aios_kernel/__init__.py | 3 +- src/aios_kernel/stability_node.py | 75 ++++++++++++++++++++------- src/aios_kernel/workflow_env.py | 26 ++++++++++ src/service/aios_shell/aios_shell.py | 11 +++- test/test_local_sd_node.py | 2 +- test/test_sd_api.py | 56 ++++++++++++++++++++ 9 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 c1e8c5afd3d447f2a58727107b5011b7.png create mode 100644 f7bef6a77c3c4d0087553cf67b4e5f0a.png create mode 100644 test/test_sd_api.py diff --git a/c1e8c5afd3d447f2a58727107b5011b7.png b/c1e8c5afd3d447f2a58727107b5011b7.png new file mode 100644 index 0000000000000000000000000000000000000000..75777f246b51cbe6d2c8f5b9b98ff0a830c0e35f GIT binary patch literal 253544 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%0DO&+Wz7Y;MS#$mrEQ5KPJ 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≤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

    eSva>7KBf6*)L>V?&jF5D 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

  1. 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

  2. BmjKIsn=GVMDnDb0%s|hRTs>qk>@K(zYOS}-% z6p+8IfOnjD2{?;EH)4Qr1_Y`+9T|9oZ?I4Z<1~Xg&$Xm90pooWG965-hb7r3(ItHI z!3%4qm97235gUirqN#Ofn5}y8e`EC*!UEU)AUQ*|ZYefCQqOx)e_*!7 zWo?u}r560kwG+l!VQ&2zu>bFZiV4-Tr6zm2?(HrW0d+T}!n=(NuvG!yt#_bI7bJ+j zz(0vs>-WOcV}+w9#;mjlguM?6%yXyg;|z(^qu$DW>|2MGviZiBS}(4__+OmiC3TsG3W{$u9VrJgba z2H^lRHPv#}lxtR-HGCj<%vX)I%aj%hcUrbjwwsE6#W(x6>OFdQPt{-WeJc30(_BzG z55vTc@T)q&oAnF*RsBkb^%mdIkcUqDEgG;|Z|FVUlqe7dGQVy|^%YSOSB0-Va1qhk za#y$$oBwzG3wMeJAUD{tiT+y);ya_@Apy4OTHI$Kzp!&jA;KkHZjQT@!PP4PH;=bG z9BxeNeqGnzI<#=Wb37{8C?E5x3Y(L1ws{Od=CWNrA^w%KwQb;yk6eCmn?p%1lCVRQA0-(cb(0?STD)lq0(~u(`R=dP2Nmvp>RcyJ3pHZZKF%VlN z@I=IvMDaNM+E~U$u{L%EU_}(9?8`65(;bn2r|yTrgPM%3gOVYpP;LYJMG@6#I~?ID z{NL}#451BE#esgHxQo;M9ie3s;hoKlI=l%q@x5?+`2lmW8(1Vk^--AxvHAk}X6$0D z2g$A>%RN?!U9DKTMjw|m+LS_lMs6g!&bqJ9qDyh&b7K7g8&O&gY9SZmr;V`88|~#c z61(0aC%lTpDLz~hKkSRA)EYirPR9x*pv4euVqrm?%^%kN64QrFLPZjfnh@|v03M=+ zpAyNA&q*HdDVjCZ%>yIZ(XXfogdVud-1|W#y)1TQ@`8GLwG)AddLjXWBCKxbqxF`fVVWvgwEwDImgx~@%1{HUG~zZPPIKCM5+8iK zKhTo+Vg}<4YI+XpjNUj$9P)Jik_6wVw*a1}#1k(f@^{XVZ}G5xd49>iqj&KQyutr~ zZ|V&XK^(Yt&z(GD1;=QAMi&nPYA{uGHrM()(~J;9%(_9W=It&4CY=_dY?VXP13}G+ zdW_$iyCyA+15upSQ`l&HLS;JkCzH$!PH>i*4l(^}G5QTiY8SKZ2u^9BVHGQv3C7}x z0j|dsEupS68cg8;&+(_*M_SZDrR!x+kb)6PrG7`+3B9YegkfriQiPxQ2OxEfQF>A^ zJT6~ya-m%EkE#;;Ess)ekZ5des=yBa6WC6G)27;kxQaZx{bE~zjHg2?SKM=Wd8LU> zCr~#DJR5floFq>CLn4JeUQ`LMC5Y;s4z(-&T&9eOf1W4aCmBO=b5OXTFlJ#xg0Da5 zx?Usen=m4Xnoq@UViEsJ*om+j*r*{WW`h9b%KKAq5*HfbJ1MpQ>iy4 z`j(9te4};@+69ij=qGl#5&enyU04QZ3mvXJ7&{H;YBWibr~~xA>;Xz zA+>u;(1S$50C6<#h&t&lKxwE!`QvJVg(hm;Z(;_4Hzd<1I7<1@agk`kYo8P}r{Q!x z2sKlgF1h3mHKD9nET}alIE9Bs$jh0uQc>p71U*!nzJQR{;;CH82V|TG9OvH}(}R9KCWng}wk93Y)CsaYgEw=H$it~fZZ0sru)o!vkvs8{zAXxU+Dq=b2Q|e8c~JAh!RDV2jAJXKoM7t7~KxRu4AD*(aie} zS#wI#bLPE$Nwo9^@Srq+$EsA>R#oJTLPrUygx+!HOu;Z$do>>OU#>jag>XT({v~+y zCz|e~O7YVorR00>ZCcPxRC|vQXC%SXEVYwTVyR7>nF@<-0;yNH4jxP2=`?mAA{-&1 zLHkJ0N$}W7Q!KORMc}Dwm1;DnSr#&49lFS>2u+EadWxt?m5v+OP6O%^)Zp2ue$c<5 zr`!F4&-M)a@O!9>B?_4kXyIc@?X+;(PRP5X*1p@a>i|iM?e@VJ`y!YI#oz&+gf@{n zt+b;&>E~PqL)S%bvt{0-H;t`Wm1SlFBe0ak?Z~ygX835kh+F~-#5G$<%oxFq<#zAQ zmlS5QZ%ZiU9TR)qS!w^ZEcd&L$xE+wTMC<-Y*-(}T&!F^>o@5(QKMjTH!s$CB<2C4 zd+e3h=eql7yLEjGTr=PICKt>k>$*;5b8U0&gbO;vRJ_HXHj?alz{WhNo_U!%no;o~ zAi|LB(bnJT6rT2y?7uCEbxA{%os`2z0T;f*N{&Kb>gcca8N#ddhaz{T4t+ zi4PVRNYV@C=Cag}SONhkIS=MTV&ESxBmSg22Ln zQ_`!(#TEFBsT$Ua zwdH_?I`tCD-L?~Evwu+sKE8az+*>6q$?w91TtS0p`(rF0ibCC4C0JBk;DJtZujY0< z?3n}k-x>4i_xsqYJ$QgZq%Yd5cpU9LTmn(EbD87XH$+`%{1tgwGb5a1#q?j%F=dDN zGHcGXO(Gw%2Agii5+OElmucBJpwh{2ysvMx9#*7`r`Ek}6l)R_ThHU8g;g~O)Y?|s z4HR6eynYFrVMDl&lw;aZRtVE;Xtv0l7HG&9*-<5Ax{G<}>iEjMpQPMnj=PEU=;R%R z40CC-RTD&2HyhRo;RChmp^7y)#X3b7UVPo+F4 z)C7ELxCBpjml&0aF@ocsZTGBq6{Au)9?-&@Se7zTxZE__1ThCiYqxny2xmB88{r^T zpe;6l*|XuIuLi*b&RNb=YRZW_Fq0d-hb7>w*tC1= z8q$mNqZ-ty`UQTWgF37uyn#_=;AJ+mNY3%sa{M%Aau_=cN!C|LE7r~@Otr3&!8>%q zfW%XU=ArY9?WQHI)fU4!B*21z-xIDH0d| zJTCJWQFC0gce*;t^KyJC5QVc1(l`1>m#p&S4yCSBI4!*@hi9mnBfpBORO5&?p&r{I z;-O|>2h)4hw*TXiRz2JL?tmS%W&L7dXHhN4JX)0;Y-A_x)p&obq-J`+@F6KULq)VVZ9M9_OG;-67X{@}RNNAdqR)I}3h)9IK?v3Fs2Evy$VNq97E^;I2ISgk^u3XWWGT#1EW=v zNwV~l7eIjxOP8w`4VeZMSKNlKcX#mZw76}DD=xIJ%9RC2C$YgJ=z-2+0cS|C-6&v} z-hjt%565<2mBNQOEB~G35Z9-`L%$73WsJ%vpyHT0a#SMKk={6+SYL1H96itva5zVN zTaAT-uJ;`0aNWJ(Qn~0M2UReTamEG1fz^aGmX@Ps+DObHSv74HyzMhGHX}jS;2qp+ zq~mlnQx-w3GEn*td2>gb%4px=0YgP*g8!=z)WLE zGbMHa=tnJ94_*g<2G4|!9}_`ufT%(L#bY(pU!d>^{Ws8myDhS z%C!-(#ggqpL2E6%feN#_bgQSZ10&b0PgGV2Z9X}gA={g?#RE{}cLPG<4v@LvU18sI zs+oLXe#K|S`3EUlsp$**`KidH*a3tqFMvB@0+{HhEt zj!=UajzyN#_57V7$9ANB`gk%o#7|rjI9|~aOEd9t$UwU=w&BX`XhXy!LKviJeDP+B zHfl^^fr+5G@WTaZmwA*lR1aJ{jT@_Z5auSf9niWW8D|Mi5)Ic%+3A-omb4y@guSox zE;Fz6!JEY+HOC3{#1lM2x%h{5L{%Rp$tiKzZ@_uuk>h!{>I1;IBqU|yoO6Q7SSS=6 zAPhjvP*+h7#1uT5V839^r9>CA|LP&l|5d zo`W}W8t084i^2V(eo?@yV=JDM2mnGK^K|F|fWPKDg&{l!0fjnP9wvWf;9tV?Mm7Ep zex0^`@Gd+63OLY#-gtgtzP>AQ>G71aCqfiMlyHoa--!n21vXyisYqw)+EA?qZ`pl< zf$FtzmBv+!QS=!rZW7i^UkRx*3nGZ>S(wO8$pUooe(F-T+90vS_aANOQ|R?Y3B_{e zMm)mfTJ~GqLi12ABQgJT`^C&3aes?J#p@gPoTX~iEN(RwcwzrwDh^-45GA+A=T3mD z&6o@3SlCI710RPO_Rz1*u<@F}0jQ6h{@#7~gt3NP>~xcF-$re;Ift|`b=~&8%6Ln> z2T#`wo@$bBi^!`U9*;p+Z$*W-QwH~9lM%NE+Em)6kz8w0vu{3WO~DmZSe#L*gN>dXP=hGc*}1@?h*k|Njl@V}7e+h)$+qyiP*K)&7^AQf zrA!USzKb&Jq#n?;M4ehJZZ&7W;SB`xDnVDpMg zFs_rhuS0>0i|YNi#EKa(erPbs?Is8y2t01F4-TEs<4j)Cp36^_K7|v8oGE@XTx@;1cAOT8gPH zpe=Y1hjYESU?0F(j2Gh!w&7sf${?522BnozzOgT~!ZF+k9Yc2Q@GPv8CJP?DQkWD@ zSZNM7Z+1XcmQ8S9PYYz1FsHlGzzkmQT$+6q()NX5-Z()m<4MQK`kY_l>H#y=A4|r5VYB=|BJOT6}P9218 zc*e+01GE+fAmYIcT)pUKw*gPr@VuvnHECiRhffjepa$nRiy7-+SWi5k(NCy@ zXCR`9qYxF3P=j;u8i&AWK?l7WnGzo1HK+*^|F44w_<^MsLf{CP{;I;X2W*Vc4FG@q znqq123;f6-EZ*GOo`HzJ2k%A|>YXC(FxCuI`yo(xJB!env0VtB=9QRwnq1 z6fM#$E#h=Gn9Sm&YYJO5Y~If1e=Dt8OT^j6M!~oa-d|fPp3Cm%zK({U zt&h7HPO#CNWn`I#o`XTf?yKdml92OR;q7pxiTl zB9Zt2yuw?Ht?>6ohkmO*#-!5CI>OU{C7YB&3lPp%_k}0BIqgEcTSw5Ldb87ev+kj( zbyV(B!#u;(suWD!8JP@A;v9yTQ3M1dkpqz`MRZ*?hUdglXxM!3P~gzq)ZpzRlBqYJ zH!UQ`qis46tEM#$`Ce3e$d`UeR~wz;JZO=^gl*n!%Fk*zAwT6Trrr1$15za(;GtdA zRWe|V@1RdY7y|u9AJi{iu82Qjx5+JQDUf&#T--jw1N8etviqgHfF5Y@BfQq`yAabr zqIOeYUx$Cv`xA%j!q(~U9|82iU}pg6gU$crD<~KF9XR%J%=*8;oLuC!@bDM-Phkoj z(its0HuN^cLJKcOX-)#)cmZd{K2HH}=mo)e1ux-e@cPBiU;O+J{=q}a)%a^hiEaJ{ z4h>l^ZL;rmITCSOqKpEnKHEG1C%$A`yTY=k)(7iseAJone=<$VjwF35DYo$&sLS;` z;@vpF^h2Ty=42|G9;+#JNt4C+RST!+V>QT&TMZ|M@-VvA063-0P^}c$dPLlUS7v&E zS=QJu^YU^i?M&>XdXJFQAgx_aU=E>*43V=%7IgFSrlqhpw9E3O*xkiNzJDygCvpk} z7jZKBl8&enH6*AR9CgCsQ@0^va&JX6;E_?Ra#l^ZT4PwP#Q`WI1ycC?*nwkHA2W3G zmI;>!!f;JuW|NPtf*T<5@;%{*X&i>bz(oKSH!_M!?=XdX* zfZyvy1Q%|<)~P&S-}PsI<*)GnIOPDS(Ssr8*OEy2M!xYQ)Gst>pay>izd#+dW6%Tq z=0!Y%2ly}IpTU0}0etXma0aeLwhgcQ*6!Y_P?$29%(LmHYiwMdI*Y|Y`nK+H_N8#5 z*(KhM%Y`#ovmLj`8$;UdC?%!F<2E|jU$P1NI`v~}0~;=S*3w1!d4}y0fK8-_^TF{- zDU!&kTyFT=o$xR@l5{E}PW*-Ur=)aF7Vs1tdO+FT$zpw{nMQ%sb6)A}~0B+^l4w zCWZ)g#_l{IF^6YZ_&IN!=Z{SR?KLR@Z?T0Qbm1I$5=pycQ{cfta*w)QseqVqDBSA! zL|4EIz#Gq?PqL%_i3A+H6ov$XdoG?N#xTSq)a`mL0H{H|^Z6g*aarKt$ELrW0gg$1 zt%bD-{kYWyT9h;@BUryo$zoUDX+ZI&pji5Pjn?IZ{Ia3#KPd zvn>e6ZniR2#x9g%WK7`Rr&t7W3lFf6$GQi^yJB?wMCXk^ zNKS7KAPmuFGY}fL)&V#&GjTM)5!wRJg_$Tsc4K~1^5NAu1e{^@#qInG^NVqmaPaq? z0VeTq!?4>ne1+692+u%X)oT}pR#wc`*;`OTolq0D7%rpi0se-Bv4a7?-&9gM zx3$D2gXq0a2Bfa!}_a{ETvv{9tyVunG3)k!GTl5u4TtlBs zFFtp5ule5V-07 z>kFD2bYqLd;FNllvwmW6W{VVfO|)gKT(i}g>DeWs;*P*Nl zwEHQfKQj7e%z%@)LYa~Xl|8`&Th$S)!2(Mra4*UAKweVlVe0c*=o~2(gi%M6l4Kp{ zyp(xva!Ab6AoV~!c&geAVX^9T z%!8^0!0A#5RRUdeJ?V`of^Upa!#1t;o016|*)yNoxZMTN0PnRg3SPQ-Ve1tx+qGxc z+^*JCRhSA#7)3b0h2(~Sr7SeSgF36elscjWdH!&j4l>L;=*tv!jAJlPK^23NL)#G* z^dXYagx+ZGs>CEWu|D&p$^h4#`F?s=zsdu!Eg&j!jT%i-(P1~;g@ogY9u zLM*;{`58Cj@(Xd}i-mXkqy{d^!c%fo?p& z>lZy>=ZC-F{l-PK`tg=fwfI|f(D2NAH&fi^GTVMlh8!YZV=N%!r&x~6M%>L(*DS`eJ_FEGjMkHAO~9OwP+q53J~byqY;^Jp2O`cpwH$1eFeic&8}r2)I&0(>9&`qJ zyqCd;a3&bA3w1EUIUf%c^?nbTg4GbB=qq1yje%#!fBU&1UIUz%eoly{Gj1Nhg1ay( zM(frM4wpN*zU9O7Hf^cT5?{c@dO}{9GXY-YS_v8(m%b2ldATCSwd#x=eSj+%8~$f; zedAv)VRyl;RRMr!t^e&mc>Np5U-G9HvtK&?UfGpD{P)ivptog8vHq!tz1bowfn3)@ zSS!&E)Nwk`XGRJauv<`fRv<1ru$64H>A`0~ z2ZNyDB7aUg{lb)tjYYNbw&d0z5uU!AJ4j7U;(#3~0))!(+#s7&3LaW*A|eU_+11SE z5D*FSsN-={#+wD$WUhoejWRdQokxUQy-BP81V~go>Puq)2DnY*@!t z!Z;DDSdI)HJRM&%W2LhV%V-K((AjFoEnqd}6S74VSkoZqU91igny2V#ipT^)j~W&M zw275S1Y0i6&)KzEMI9X+sA?LZPE)Vuc4&c+nDOhi|C6mxrW}~^|R-8 z86E-Zlv!q*{H)u#ni~yO64>KXfX)q$qXCzj)dnrs8!Dy3Te}{-@d*O@>#6TQ-~5&K zad7aiYrGRaI$q%^r1#(fKHc)Ew$7Uf*^Ncf5U8m&;Y@IDrIt`ANfdB%zsdVrR%k$R zY5On+2m{T<8!nxMOUjpyvB_pq1@3zGx8c@(Cc^jxUC1B}B*=iU+7i2I0Fqm)W4xTs z>B453R|+4Qh*dYrB6bY2xi9;Z{eC}^uiZ#;#i-f)lQRO@jRnSxJ{9wI+Iebt?2XjCa zyB?Q~lBIrBw{+U+tiId@(MGaw^Z^f^K_}kC``7)4cnJXB2amgnhdm$(cdiowYfT&iBVQVXWJf`Onm@}eKN1N#QgJlpu_T&Mu zD)bdz>vLB(tho;eTOW_1oqw$}_bnA3eN+?RxwF12Ka4V0Jsii_Ofg*}Av)J(-1~`P zO4lTiq8c+iNV60>q|sXCu+i4=CMH!k!T1;WW3$A&UsCBG25D~=#K0l5sVUq#Cv1Ek z_eNTjO#(1-kH5W!oAC3H41@DkyFWl#qzX7kn*gH#tyq3nR4(Ex35s)~(_z`LnVskLUAH!`|#|y7P4}NSQ4j!O>@oM}PARJdZ z``4GycVOjVD-;g!D?B@pCWmInqJjbA&KSo)o1yk?>>*1ve_@48FoRdeFon!Jz=LCE z>4cOkr)pQj{U)MKgV-&-)`Hj|PDnnmP?fY@Evls}WDc-dZVkEv>d(PbaR5L{+7rJo zr9K&P5HL6g4^!*ur*PKDHsBQ_mH*|Qd|N%I0cQ*OZ1sMDb{Pzl#O9ik{<1G?K0er) zdmZyX^KVP!ZG4b7J^*4pBnwY;&{uQd6wwr=v0Gk=z^?=9W=X8D;jMX1?j8Q<T;}8B|MbEc{l4+v?gMa)=}A>qDtOGAtUg_ZegSNiQeb4Am#nPyjZhGZHvyd$v>P&G)4SJehZX^>WQz6W zeZrR4dI&<=N#HvO#CM%A#YVVvAsSZ3XUl0I5G57w$jRB@h8q$k)X6ExwE3$BQJGcd z+855G6&7;CR~(qb4#|_6ls#ytMnyVa^HXT)rSmGi=wXAHnyQ}BATI9h%+dq$O`e*0 z6{@9ko~qY#x~J;*dFj!A{`m*3Q?2Kbeop}?N9YB-@qiLu#QQHE;z#^%0B`(*G`kxY z%e2EAuLpmK^9#7erZMjb(#|h_e(*~`_@BS{c|iyN^het8DV&39oIwFk;r#|He&q~0 zy-mURhRNM{pHJBSyVl{mVaNNPms|o~xAo%AD0$&O+gopSf!7Pa6H4THK|%v>1NmR@ z6`Ou(?gvQ#V9*Bd6UUaLFZ>kFq~YlAqW<4m5U7v+x-AH2k+Scz7+OPvv#G-MHTdU? z;`sUc+n+5f)MhLn@4~H2x8*X~{SROWaqCGA@CW$MSI+aBw(!54{j1qm@F60;Ogz?z zU%zr+(Dea%M|%UkYht|o?7O~2^!?=qzW!NZ^$#yU7qvyyEA>;cuh&HGgIJ5@y8gzp zF~~dIGUs*i-0B%${E5bs-z|{#1}LRx(B~JimB8G#DrZaqac}7)Cvzp2J1QpG(F49t7ex#$({7?{`pNt_W&wdS~t?p;>+ z=ZoUFCVSQa@T)}qXD>Bt9sU6S1-!TL`~W|DtNyRA@LrG7`@Qe4O8NKR=kpb3ToBq{ zKe<=?4Z(VANzO0oPuzwcpiu?Af!d@46FYf52heXaDf}NyK@a#35wu)JsCa%DwT16MLaI>R8PArtUNL%i4%f4zZ_tw^P-?>01sQB zrmf`3^Lq5W_|)$|&*?tm->0V@w^>d!Y#)WQc3GY~TH8p&Byyq6^8n8qe+mGeH=e@t z!p|EbUf$ybAN;|NTM_=OJ+e*ty#`CMaelL{)Wy4N|JPI>El_|6q^S$44Pu2PNPyT-OBHiEe&;KqM{{as0KfB9&7i9Kf&pzz`d@ashvG;!L z_X{BZ{X+MSB(|_KH}>97-d}lt#b;08t?iLBwK;?9KM26-;R}=Pzk&M6TD713zW@RM zWZ9qBmovUp(tN&RtIqb({(QOHS#QU$MzHJbdsq8+U+A;H`@diBNc(}=-&)rNY}x)V z@OS;_?>@-gqVx3#*!`@mXFa>ku;=1}{qw>ZoB|)52YO@vH#j)nz45;%-2nan!SDa~ Z{|}MdsVT|~^S%H8002ovPDHLkV1hka@=^c* literal 0 HcmV?d00001 diff --git a/f7bef6a77c3c4d0087553cf67b4e5f0a.png b/f7bef6a77c3c4d0087553cf67b4e5f0a.png new file mode 100644 index 0000000000000000000000000000000000000000..f15c96d257d3c19f8761b8a6b2b7641c0f9f1786 GIT binary patch literal 505478 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