Support Text summary based Knowledge System,

Update Agent Workspace.
This commit is contained in:
Liu Zhicong
2023-11-12 21:59:04 -08:00
parent fb0b88d44a
commit 763360b305
12 changed files with 765 additions and 257 deletions
+1 -1
View File
@@ -16,5 +16,5 @@ You mainly use the following methods to generate summary:
4. Try to understand the attitude of different people on different topics or events 4. Try to understand the attitude of different people on different topics or events
5. For the key information or TODO in the information, such as the time, place, amount and other information of the certainty, it must be stored in the summary. 5. For the key information or TODO in the information, such as the time, place, amount and other information of the certainty, it must be stored in the summary.
You have a summary of simplicity and profound nonsense, and you don't need to have any polite words to me.Just give me a summary. Just give me a summary without any other word.
""" """
+3
View File
@@ -1,5 +1,8 @@
instance_id = "Tracy" instance_id = "Tracy"
fullname = "Tracy" fullname = "Tracy"
llm_model_name = "Llama-2-13b-chat"
max_token_size = 2000
[[prompt]] [[prompt]]
role = "system" role = "system"
content = """ content = """
+1
View File
@@ -24,5 +24,6 @@ from .stability_node import Stability_ComputeNode
from .local_st_compute_node import LocalSentenceTransformer_Text_ComputeNode,LocalSentenceTransformer_Image_ComputeNode from .local_st_compute_node import LocalSentenceTransformer_Text_ComputeNode,LocalSentenceTransformer_Image_ComputeNode
from .compute_node_config import ComputeNodeConfig from .compute_node_config import ComputeNodeConfig
from .ai_function import SimpleAIFunction from .ai_function import SimpleAIFunction
from .workspace_env import WorkspaceEnvironment
AIOS_Version = "0.5.2, build 2023-11-1" AIOS_Version = "0.5.2, build 2023-11-1"
+135 -69
View File
@@ -485,12 +485,12 @@ class AIAgent:
summary = self.llm_select_session_summary(msg,chatsession) summary = self.llm_select_session_summary(msg,chatsession)
prompt.append(AgentPrompt(summary)) prompt.append(AgentPrompt(summary))
known_info_str = "# 已知信息\n" known_info_str = "# Known information\n"
have_known_info = False have_known_info = False
todos_str,todo_count = await workspace.get_todo_tree() todos_str,todo_count = await workspace.get_todo_tree()
if todo_count > 0: if todo_count > 0:
have_known_info = True have_known_info = True
known_info_str += f"## 已有todo\n{todos_str}\n" known_info_str += f"## todo\n{todos_str}\n"
inner_functions,function_token_len = self._get_inner_functions() inner_functions,function_token_len = self._get_inner_functions()
system_prompt_len = prompt.get_prompt_token_len() system_prompt_len = prompt.get_prompt_token_len()
input_len = len(msg.body) input_len = len(msg.body)
@@ -879,40 +879,63 @@ class AIAgent:
# 尝试自我学习,会主动获取、读取资料并进行整理 # 尝试自我学习,会主动获取、读取资料并进行整理
# LLM的本质能力是处理海量知识,应该让LLM能基于知识把自己的工作处理的更好 # LLM的本质能力是处理海量知识,应该让LLM能基于知识把自己的工作处理的更好
def do_self_learn(self) -> None: async def do_self_learn(self) -> None:
# 不同的workspace是否应该有不同的学习方法? # 不同的workspace是否应该有不同的学习方法?
learn_power = self.get_learn_power() workspace = self.get_workspace_by_msg(None)
kb = self.get_knowledge_base() hash_list = workspace.kb_db.get_knowledge_without_llm_title()
for item in kb.un_learn_items(): for hash in hash_list:
if learn_power <= 0: if self.agent_energy <= 0:
break break
match item.type():
case "book": knowledge = workspace.kb_db.get_knowledge_by_hash(hash)
self.llm_read_book(kb,item) if knowledge is None:
learn_power -= 1 continue
case "article":
# 可以用vdb 对不同目录的名字进行选择后,先进行一次快速的插入。有时间再慢慢用LLM整理 if os.path.exists(knowledge.path) is False:
self.llm_read_article(kb,item) logger.warning(f"do_self_learn: knowledge {knowledge.path} is not exists!")
learn_power -= 1 continue
case "video":
self.llm_watch_video(kb,item) #TODO 可以用v-db 对不同目录的名字进行选择后,先进行一次快速的插入。有时间再慢慢用LLM整理
learn_power -= 1 llm_result = await self._llm_read_article(knowledge)
case "audio":
self.llm_listen_audio(kb,item) #根据结果更新knowledge
learn_power -= 1 if llm_result is not None:
case "code_project": workspace.kb_db.update_knowledge_by_hash(hash,llm_result)
self.llm_read_code_project(kb,item) # 在知识库中创建软链接
learn_power -= 1
case "image":
self.llm_view_image(kb,item)
learn_power -= 1 self.agent_energy -= 1
case "other":
self.llm_read_other(kb,item) # match item.type():
learn_power -= 1 # case "book":
case _: # self.llm_read_book(kb,item)
self.llm_learn_any(kb,item) # learn_power -= 1
pass # case "article":
#
# self.llm_read_article(kb,item)
# learn_power -= 1
# case "video":
# self.llm_watch_video(kb,item)
# learn_power -= 1
# case "audio":
# self.llm_listen_audio(kb,item)
# learn_power -= 1
# case "code_project":
# self.llm_read_code_project(kb,item)
# learn_power -= 1
# case "image":
# self.llm_view_image(kb,item)
# learn_power -= 1
# case "other":
# self.llm_read_other(kb,item)
# learn_power -= 1
# case _:
# self.llm_learn_any(kb,item)
# pass
async def do_blance_knowledge_base(selft):
# 整理自己的知识库(让分类更平衡,更由于自己以后的工作),并尝试更新学习目标 # 整理自己的知识库(让分类更平衡,更由于自己以后的工作),并尝试更新学习目标
current_path = "/" current_path = "/"
current_list = kb.get_list(current_path) current_list = kb.get_list(current_path)
@@ -933,48 +956,86 @@ class AIAgent:
def parser_learn_llm_result(self,llm_result:LLMResult): def parser_learn_llm_result(self,llm_result:LLMResult):
pass pass
async def _llm_read_article(self,item:KnowledgeObject) -> ComputeTaskResult: async def gen_known_info_for_knowledge_prompt(self,knowledge_item:dict,need_catalogs = False) -> AgentPrompt:
full_content = item.get_article_full_content() #已知信息:
full_content_len = ComputeKernel.llm_num_tokens_from_text(full_content,self.get_llm_model_name()) # 组织的工作总结(如有)待完成
# 现在知识库的结构(注意大小控制)gen_kb_tree_prompt (当为空的时候应该让LLM生成一个合适的初始目录结构)
# 原始路径,现在标题,摘要,目录
workspace =self.get_workspace_by_msg(None)
kb_tree = await workspace.get_knowledege_catalog()
known_obj = {}
title = knowledge_item.get("title")
if title:
known_obj["title"] = title
summary = knowledge_item.get("summary")
if summary:
known_obj["summary"] = summary
tags = knowledge_item.get("tags")
if tags:
known_obj["tags"] = tags
if need_catalogs:
catalogs = knowledge_item.get("catalogs")
if catalogs:
known_obj["catalogs"] = catalogs
org_path = knowledge_item.get("path")
known_obj["orginal_path"] = org_path
know_info_str = f"# Known information\n{json.dumps(known_obj)}\n"
return AgentPrompt(know_info_str)
async def _llm_read_article(self,knowledge_item:dict) -> ComputeTaskResult:
#目标:
# 得到更好的标题,摘要,目录 (如有必要),tags
# 应放的合适的位置 (结合组织的目标)
#已知信息:
# 整理是为什么目标服务的 learn_prompt
# 组织的工作总结(如有)
# 现在知识库的结构(注意大小控制)gen_kb_tree_prompt (当为空的时候应该让LLM生成一个合适的初始目录结构)
# 原始路径,现在标题,摘要,目录
# 整理长文件(通用技巧)
# 告诉输入的是部分内容,让LLM为任务产生中间结果
# 依次输入内容,在最后一个内容块输入时,LLM得到结果
#full_content = item.get_article_full_content()
workspace = self.get_workspace_by_msg(None)
full_content = await workspace.load_knowledge_content(knowledge_item["hash"])
if full_content is None:
return
full_content_len = self.token_len(full_content)
if full_content_len < self.get_llm_learn_token_limit(): if full_content_len < self.get_llm_learn_token_limit():
# 短文章不用总结catelog # 短文章不用总结catelog
#path_list,summary = llm_get_summary(summary,full_content) #path_list,summary = llm_get_summary(summary,full_content)
prompt = self.get_agent_role_prompt() #prompt = self.get_agent_role_prompt()
learn_prompt = self.get_learn_prompt() prompt = self.get_learn_prompt()
cotent_prompt = AgentPrompt(full_content) known_info_prompt = await self.gen_known_info_for_knowledge_prompt(knowledge_item)
prompt.append(learn_prompt) prompt.append(known_info_prompt)
prompt.append(cotent_prompt) content_prompt = AgentPrompt(full_content)
prompt.append(content_prompt)
env_functions = self._get_inner_functions()
env_functions = workspace.get_knowledge_base_ai_functions()
task_result:ComputeTaskResult = await self._do_llm_complection(prompt,env_functions) task_result:ComputeTaskResult = await self._do_llm_complection(prompt,env_functions)
if task_result.result_code != ComputeTaskResultCode.OK: if task_result.result_code != ComputeTaskResultCode.OK:
return task_result result_obj = {}
llm_result = LLMResult.from_str(task_result.result_str) result_obj["error_str"] = task_result.error_str
path_list,summary = self.parser_learn_llm_result(llm_result) return result_obj
result_obj = json.loads(task_result.result_str)
return result_obj
else: else:
# 用传统方法对文章进行一些处理,目的是尽可能减少LLM调用的次数 logger.warning(f"llm_read_article: article {knowledge_item['path']} is too long,just read summary!")
catelog = item.get_articl_catelog() result_obj = {}
chunk_content = full_content.read(self.get_llm_learn_token_limit()) result_obj["error_str"] = f"llm_read_article: article {knowledge_item['path']} is too long,just read summary!"
summary = kb.try_get_summary(catelog,full_content) return result_obj
while chunk_content is not None:
#path_list,summarycatelog = llm_get_summary(summary,chunk_content)
#learn_prompt = self.get_learn_prompt_with_summary()
prompt = AgentPrompt("summary")
learn_prompt.append(prompt)
prompt = AgentPrompt(chunk_content)
learn_prompt.append(prompt)
#llm_result = self.do_llm_competion(learn_prompt)
#path_list,summary,catelog = parser_learn_llm_result(llm_result)
#chunk_content = full_content.read(self.get_llm_learn_token_limit())
kb.insert_item(path_list,item,catelog,summary)
async def do_self_think(self): async def do_self_think(self):
session_id_list = AIChatSession.list_session(self.agent_id,self.chat_db) session_id_list = AIChatSession.list_session(self.agent_id,self.chat_db)
@@ -1043,7 +1104,7 @@ class AIAgent:
known_info = "" known_info = ""
if chatsession.summary is not None: if chatsession.summary is not None:
if len(chatsession.summary) > 1: if len(chatsession.summary) > 1:
known_info += f"## 最近交流的总结 \n {chatsession.summary}\n" known_info += f"## Recent conversation summary \n {chatsession.summary}\n"
result_token_len -= len(chatsession.summary) result_token_len -= len(chatsession.summary)
have_known_info = True have_known_info = True
@@ -1062,7 +1123,7 @@ class AIAgent:
logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.") logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.")
break break
known_info += f"## 最近的沟通记录 \n {histroy_str}\n" known_info += f"## Recent conversation history \n {histroy_str}\n"
if have_known_info: if have_known_info:
return known_info,result_token_len return known_info,result_token_len
@@ -1103,6 +1164,8 @@ class AIAgent:
return False return False
def need_self_learn(self) -> bool: def need_self_learn(self) -> bool:
if self.learn_prompt is not None:
return True
return False return False
def wake_up(self) -> None: def wake_up(self) -> None:
@@ -1145,6 +1208,9 @@ class AIAgent:
logger.error(f"agent {self.agent_id} on timer error:{e},{tb_str}") logger.error(f"agent {self.agent_id} on timer error:{e},{tb_str}")
continue continue
def token_len(self,text:str) -> int:
return ComputeKernel.llm_num_tokens_from_text(text,self.get_llm_model_name())
+4 -4
View File
@@ -12,15 +12,15 @@ from .agent_base import AgentMsgType, AgentMsg, AgentMsgStatus
class ChatSessionDB: class ChatSessionDB:
def __init__(self, db_file): def __init__(self, db_file):
""" initialize db connection """ """ initialize db connection """
self.local = threading.local()
self.db_file = db_file self.db_file = db_file
self._get_conn() self._get_conn()
def _get_conn(self): def _get_conn(self):
""" get db connection """ """ get db connection """
if not hasattr(self.local, 'conn'): local = threading.local()
self.local.conn = self._create_connection(self.db_file) if not hasattr(local, 'conn'):
return self.local.conn local.conn = self._create_connection(self.db_file)
return local.conn
def _create_connection(self, db_file): def _create_connection(self, db_file):
""" create a database connection to a SQLite database """ """ create a database connection to a SQLite database """
+2 -1
View File
@@ -43,7 +43,8 @@ class Contact:
self.active_tunnels[msg.sender] = tunnel self.active_tunnels[msg.sender] = tunnel
await tunnel.post_message(msg) await tunnel.post_message(msg)
return None return None
logger.warn(f"contact {self.name} cann't get tunnel,post message failed!") logger.warn(f"contact {self.name} cann't get tunnel,post message failed!")
def get_active_tunnel(self,agent_id) -> AgentTunnel: def get_active_tunnel(self,agent_id) -> AgentTunnel:
+225
View File
@@ -0,0 +1,225 @@
import sqlite3
import json
import threading
import logging
from datetime import datetime
from typing import Optional, List
logger = logging.getLogger(__name__)
class SimpleKnowledgeDB:
def __init__(self,db_path:str):
self.db_path = db_path
self._get_conn()
def _get_conn(self):
""" get db connection """
local = threading.local()
if not hasattr(local, 'conn'):
local.conn = self._create_connection(self.db_path)
return 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 Exception as e:
logger.error("Error occurred while connecting to database: %s", e)
return None
if conn:
self._create_tables(conn)
return conn
def _create_tables(self,conn):
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS documents (
doc_path TEXT PRIMARY KEY,
length INTEGER,
last_modify TEXT,
doc_hash TEXT,
create_time TEXT
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS knowledge (
doc_hash TEXT PRIMARY KEY,
title TEXT,
summary TEXT,
content TEXT,
catalogs TEXT,
tags TEXT,
llm_title TEXT,
llm_summary TEXT,
create_time TEXT
)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_documents_doc_hash
ON documents (doc_hash)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_knowledge_tags
ON knowledge (tags)
''')
conn.commit()
def add_doc(self, doc_path: str, length: int, last_modify: str, doc_hash: Optional[str] = None):
conn = self._get_conn()
cursor = conn.cursor()
create_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute('''
INSERT INTO documents (doc_path, length, last_modify, doc_hash,create_time)
VALUES (?, ?, ?, ?,?)
''', (doc_path, length, last_modify, doc_hash,create_time))
conn.commit()
def is_doc_exist(self, doc_path: str) -> bool:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT doc_path
FROM documents
WHERE doc_path = ?
''', (doc_path,))
return len(cursor.fetchall()) > 0
def set_doc_hash(self, doc_path: str, doc_hash: str):
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
UPDATE documents
SET doc_hash = ?
WHERE doc_path = ?
''', (doc_hash, doc_path))
conn.commit()
def get_docs_without_hash(self,limit:int=1024) -> List[str]:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT doc_path
FROM documents
WHERE doc_hash IS NULL OR doc_hash = ''
ORDER BY create_time DESC
LIMIT ?
''',(limit,))
return [row[0] for row in cursor.fetchall()]
#metadata["summary"]
#metadata["catelogs"]
#metadata["tags"]
def add_knowledge(self, doc_hash: str, title: str, metadata: dict,content:str = None,):
conn = self._get_conn()
cursor = conn.cursor()
create_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
summary = metadata.get("summary", "")
catalogs = metadata.get("catalogs","")
tags = ','.join(metadata.get("tags", []))
cursor.execute('''
INSERT INTO knowledge (doc_hash, title , summary , catalogs , tags,create_time)
VALUES (?, ?, ?, ?, ?,?)
''', (doc_hash, title, summary, catalogs, tags,create_time))
conn.commit()
#llm_result["summary"]
#llm_result["tags"]
#llm_result["catelog"]
def set_knowledge_llm_result(self, doc_hash: str, llm_result: dict):
conn = self._get_conn()
cursor = conn.cursor()
title = llm_result.get("title", "")
summary = llm_result.get("summary", "")
catalogs = json.dumps(llm_result.get("catalogs", []))
tags = ','.join(llm_result.get("tags", []))
cursor.execute('''
UPDATE knowledge
SET llm_title = ?,llm_summary = ?, catalogs = ?, tags = ?
WHERE doc_hash = ?
''', (title,summary, catalogs, tags, doc_hash))
conn.commit()
def get_hash_by_doc_path(self, doc_path: str) -> Optional[str]:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT doc_hash
FROM documents
WHERE doc_path = ?
''', (doc_path,))
row = cursor.fetchone()
if row is None:
return None
return row[0]
def get_knowledge(self, doc_hash: str) -> Optional[dict]:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT title, summary, catalogs, tags, llm_title, llm_summary
FROM knowledge
WHERE doc_hash = ?
''', (doc_hash,))
row = cursor.fetchone()
if row is None:
return None
# get doc path
cursor.execute('''
SELECT doc_path
FROM documents
WHERE doc_hash = ?
''', (doc_hash,))
row2 = cursor.fetchone()
if row2 is None:
return None
doc_path = row2[0]
return {
"full_path": doc_path,
"title": row[0],
"summary": row[1],
"catalogs": json.loads(row[2]),
"tags": row[3].split(","),
}
def get_knowledge_without_llm_title(self,limit:int=16) -> List[str]:
conn = self._get_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT doc_hash
FROM knowledge
WHERE llm_title IS NULL OR llm_title = ''
ORDER BY create_time DESC
LIMIT ?
''',(limit,))
return [row[0] for row in cursor.fetchall()]
def query_docs_by_tag(self, tag: str) -> List[str]:
conn = self._get_conn()
cursor = conn.cursor()
tag_json = json.dumps(tag) # 将标签转换为 JSON 字符串
cursor.execute('''
SELECT documents.doc_path
FROM documents
JOIN knowledge ON documents.doc_hash = knowledge.doc_hash
WHERE json_extract(knowledge.tags, '$') LIKE ?
''', (tag))
return [row[0] for row in cursor.fetchall()]
def query(self,sql:str):
pass
#cursor = self.conn.cursor()
+1
View File
@@ -164,6 +164,7 @@ class TelegramTunnel(AgentTunnel):
agent_msg.mentions = [] agent_msg.mentions = []
else: else:
agent_msg.msg_type = AgentMsgType.TYPE_MSG agent_msg.msg_type = AgentMsgType.TYPE_MSG
agent_msg.mentions = []
if message.entities: if message.entities:
for entity in message.entities: for entity in message.entities:
-3
View File
@@ -266,9 +266,6 @@ class CalenderEnvironment(Environment):
return f"Execute set_contact OK , contact {name} updated!" return f"Execute set_contact OK , contact {name} updated!"
async def start(self) -> None: async def start(self) -> None:
if self.is_run: if self.is_run:
return return
+388 -178
View File
@@ -1,5 +1,6 @@
# this env is designed for workflow owner filesystem, support file/directory operations # this env is designed for workflow owner filesystem, support file/directory operations
import hashlib
import json import json
import subprocess import subprocess
import logging import logging
@@ -17,10 +18,13 @@ from typing import Any,List
import os import os
import chardet import chardet
from markdown import Markdown
import PyPDF2
from .agent_base import AgentMsg,AgentTodo,AgentPrompt,AgentTodoResult from .agent_base import AgentMsg,AgentTodo,AgentPrompt,AgentTodoResult
from .environment import Environment,EnvironmentEvent from .environment import Environment,EnvironmentEvent
from .ai_function import AIFunction,SimpleAIFunction from .ai_function import AIFunction,SimpleAIFunction
from .storage import AIStorage,ResourceLocation from .storage import AIStorage,ResourceLocation
from .simple_kb_db import SimpleKnowledgeDB
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,6 +37,10 @@ class WorkspaceEnvironment(Environment):
os.makedirs(self.root_path+"/todos") os.makedirs(self.root_path+"/todos")
self.known_todo = {} self.known_todo = {}
self.kb_db = SimpleKnowledgeDB(f"{self.root_path}/kb.db")
self.doc_dirs = {}
self._scan_thread = None
self._scan_dirthread = None
def set_root_path(self,path:str): def set_root_path(self,path:str):
@@ -44,9 +52,10 @@ class WorkspaceEnvironment(Environment):
def get_role_prompt(self,role_id:str) -> AgentPrompt: def get_role_prompt(self,role_id:str) -> AgentPrompt:
return None return None
def get_knowledge_base(self) -> str: def get_knowledge_base(self,root_dir=None,indent=0) -> str:
pass pass
def get_do_prompt(self,todo:AgentTodo=None)->AgentPrompt: def get_do_prompt(self,todo:AgentTodo=None)->AgentPrompt:
return None return None
@@ -94,7 +103,9 @@ class WorkspaceEnvironment(Environment):
return result_str,have_error return result_str,have_error
# file system operation: list,read,write,delete,move,stat
# inner_function
async def list(self,path:str,only_dir:bool=False) -> str: async def list(self,path:str,only_dir:bool=False) -> str:
directory_path = self.root_path + path directory_path = self.root_path + path
items = [] items = []
@@ -108,7 +119,8 @@ class WorkspaceEnvironment(Environment):
items.append({"name": entry.name, "type": item_type}) items.append({"name": entry.name, "type": item_type})
return json.dumps(items) return json.dumps(items)
# inner_function
async def read(self,path:str) -> str: async def read(self,path:str) -> str:
file_path = self.root_path + path file_path = self.root_path + path
cur_encode = "utf-8" cur_encode = "utf-8"
@@ -119,10 +131,8 @@ class WorkspaceEnvironment(Environment):
content = await f.read(2048) content = await f.read(2048)
return content return content
# use diff to update large file content
async def write_diff(self,path:str,diff):
pass
# operation or inner_function (MOST IMPORTANT FUNCTION)
async def write(self,path:str,content:str,is_append:bool=False) -> str: async def write(self,path:str,content:str,is_append:bool=False) -> str:
file_path = self.root_path + path file_path = self.root_path + path
try: try:
@@ -130,24 +140,24 @@ class WorkspaceEnvironment(Environment):
async with aiofiles.open(file_path, mode='a', encoding="utf-8") as f: async with aiofiles.open(file_path, mode='a', encoding="utf-8") as f:
await f.write(content) await f.write(content)
else: else:
async with aiofiles.open(file_path, mode='w', encoding="utf-8") as f: if content is None:
await f.write(content) # create dir
dir_path = self.root_path + path
os.makedirs(dir_path)
return True
else:
file_path = self.root_path + path
os.makedirs(os.path.dirname(file_path),exist_ok=True)
async with aiofiles.open(file_path, mode='w', encoding="utf-8") as f:
await f.write(content)
return True
except Exception as e: except Exception as e:
return str(e) return str(e)
return None return None
async def create(self,path:str,content:str=None) -> bool:
if content is None:
# create dir
dir_path = self.root_path + path
os.makedirs(dir_path)
return True
else:
file_path = self.root_path + path
async with aiofiles.open(file_path, mode='w', encoding="utf-8") as f:
await f.write(content)
return True
# operation or inner_function
async def delete(self,path:str) -> str: async def delete(self,path:str) -> str:
try: try:
file_path = self.root_path + path file_path = self.root_path + path
@@ -157,21 +167,107 @@ class WorkspaceEnvironment(Environment):
return None return None
async def mkdir(self,path:str) -> bool: # operation or inner_function
dir_path = self.root_path + path async def move(self,path:str,new_path:str) -> str:
os.makedirs(dir_path)
return True
async def rename(self,path:str,new_name:str) -> str:
try: try:
file_path = self.root_path + path file_path = self.root_path + path
new_path = self.root_path + new_name new_path = self.root_path + new_path
os.rename(file_path,new_path) os.rename(file_path,new_path)
except Exception as e: except Exception as e:
return str(e) return str(e)
return None return None
# inner_function
async def stat(self,path:str) -> str:
try:
file_path = self.root_path + path
stat = os.stat(file_path)
return json.dumps(stat)
except Exception as e:
return str(e)
# operation or inner_function
async def symlink(self,path:str,target:str) -> str:
try:
file_path = self.root_path + path
target_path = self.root_path + target
os.symlink(file_path,target_path)
except Exception as e:
return str(e)
return None
# TODO use diff to update large file content
async def update_by_diff(self,path:str,diff):
pass
# doc system read_only,agent cann't modify doc
# inner_function
async def list_db(self) -> str:
pass
# inner_function
async def get_db_desc(self,db_name:str) -> str:
pass
# inner_function
async def query(self,db_name:str,sql:str) -> str:
pass
# search (web)
# inner_function
async def google_search(self,keyword:str,opt=None) -> str:
pass
# inner_function
async def local_search(self,keyword:str,root_path=None ,opt=None) -> str:
pass
# inner_function, might be return a image is better
async def web_get(self,url:str) -> str:
pass
# inner_function
async def blockchain_get(self,chainid:str,query:dict) -> str:
pass
# code interpreter
# inner_function or operation
async def eval_code(self,pycode:str) -> str:
pass
# operation or inner_function
async def improve_code(self,path:str):
pass
# operation or inner_function
async def run(self,file_path:str)->str:
pass
# operation or inner_function
async def pub_service(self,project_path:str):
pass
# operation or inner_function
async def exec_tx(self,chain_id:str,tx:dict) -> str:
pass
# social ability
# operation or inner_function
async def post_message(self,target:str,msg:AgentMsg,wait_time) -> AgentMsg:
pass
# operation or inner_function
async def add_contact(self,name:str,contact_info) -> str:
pass
# inner_function , include contact realtime info
async def get_contact(self,name_list:List[str],opt:dict) -> List:
pass
# Task/todo system , create,update,delete,query
async def get_todo_tree(self,path:str = None,deep:int = 4): async def get_todo_tree(self,path:str = None,deep:int = 4):
if path: if path:
directory_path = self.root_path + "/todos/" + path directory_path = self.root_path + "/todos/" + path
@@ -334,164 +430,244 @@ class WorkspaceEnvironment(Environment):
json_obj["logs"] = logs json_obj["logs"] = logs
await f.write(json.dumps(json_obj)) await f.write(json.dumps(json_obj))
class CodeInterpreter: async def set_wakeup_timer(self,todo_id:str,timestamp:int) -> str:
def __init__(self, language, debug_mode): pass
self.language = language
self.proc = None
self.active_line = None
self.debug_mode = debug_mode
def start_process(self): # knowledge base system
start_cmd = sys.executable + " -i -q -u" def get_knowledge_base_ai_functions(self):
self.proc = subprocess.Popen(start_cmd.split(), func_result = {}
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=0)
# Start watching ^ its `stdout` and `stderr` streams func_result["get_knowledge_catalog"] = SimpleAIFunction("get_knowledge_catalog","get knowledge catalog in tree format",
threading.Thread(target=self.save_and_display_stream, self.get_knowledege_catalog,
args=(self.proc.stdout, False), # Passes False to is_error_stream {"path":f"catalog path,none is /","depth":"max depth of catalog tree,default is 4"})
daemon=True).start() func_result["get_knowledge"] = SimpleAIFunction("get_knowledge","get knowledge metadata",
threading.Thread(target=self.save_and_display_stream, self.get_knowledge,
args=(self.proc.stderr, True), # Passes True to is_error_stream {"path":f"knowledge path"})
daemon=True).start() func_result["load_knowledge_content"] = SimpleAIFunction("load_knowledge_content","load knowledge content",
self.load_knowledge_content,
{"path":f"knowledge path","pos":"start position of content","length":"length of content"})
return func_result
def warp_code(self,pycode:str)->str: async def get_knowledege_catalog(self,path:str=None,only_dir =True,max_depth:int=5)->str:
# Add import traceback if path:
code = "import traceback\n" + pycode full_path = f"{self.root_path}/knowledge/{path}"
# 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 ShellEnvironment(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: else:
return f"Execute failed! stderr is:\n{stderr}\n" full_path = f"{self.root_path}/knowledge"
catlogs,file_count = await self.get_directory_structure(full_path,max_depth,only_dir)
return catlogs
async def get_directory_structure(self,root_dir, max_depth:int=4, only_dir=True, indent=1):
file_count = 0
structure_str = ''
if os.path.isdir(root_dir):
sub_files = []
with os.scandir(root_dir) as it:
for entry in it:
if entry.is_dir():
sub_structure, sub_count = await self.get_directory_structure(entry.path, max_depth, only_dir, indent + 1)
if sub_structure:
structure_str += sub_structure
file_count += sub_count
else:
file_count += 1
sub_files.append(entry.name)
async def run_code(self,pycode:str) -> str: if only_dir is False:
interpreter = CodeInterpreter("python",True) for file_name in sub_files:
return interpreter.run(pycode) structure_str = structure_str + ' ' * (indent+1) + file_name + '\n'
dir_name = os.path.basename(root_dir)
dir_info = f"{dir_name} <count: {file_count}>"
structure_str = ' ' * indent + dir_info + '\n' + structure_str
if indent - 1 >= max_depth:
return None, file_count
else:
return structure_str, file_count
# inner_function
async def get_knowledge(self,path:str) -> str:
full_path = f"{self.root_path}/knowledge/{path}"
if os.islink(full_path):
org_path = os.readlink(full_path)
hash = self.kb_db.get_hash_by_doc_path(org_path)
if hash:
return self.kb_db.get_knowledge(org_path)
return "not found"
async def load_knowledge_content(self,path:str,pos:int=0,length:int=0) -> str:
full_path = f"{self.root_path}/knowledge/{path}"
if os.islink(full_path):
org_path = os.readlink(full_path)
if full_path.endswith("pdf"):
logger.info("load_knowledge_content:pdf")
return "pdf is not support now!"
else:
async with aiofiles.open(full_path,'rb') as f:
cur_encode = chardet.detect(f.read(1024))['encoding']
async with aiofiles.open(full_path, mode='r', encoding=cur_encode) as f:
f.seek(pos)
content = await f.read(length)
return content
return "load content failed."
def _add_document_dir(self,path:str):
self.doc_dirs[path] = 0
def _start_scan_document(self):
if self._scan_thread is None:
self._scan_thread = threading.Thread(target=self._scan_document)
self._scan_thread.start()
if self._scan_dirthread is None:
self._scan_dirthread = threading.Thread(target=self._scan_dir)
self._scan_dirthread.start()
def _parse_pdf_bookmarks(self,bookmarks, parent:list):
for item in bookmarks:
if isinstance(item,list):
self._parse_pdf_bookmarks(item,parent)
else:
if item.title:
new_item = {}
new_item["page"] = item.page.idnum
new_item["title"] = item.title
my_childs = []
if item.childs:
if len(item.childs) > 0:
self._parse_pdf_bookmarks(item.childs, my_childs)
new_item["childs"] = my_childs
parent.append(new_item)
else:
logger.warning("parse pdf bookmarks failed: item.title is None!")
return
def _parse_pdf(self,doc_path:str):
metadata = {}
with open(doc_path, 'rb') as file:
reader = PyPDF2.PdfReader(file)
doc_info = reader.metadata
if doc_info:
if doc_info.title:
metadata["title"] = doc_info.title
if doc_info.author:
metadata["authors"] = doc_info.author
bookmarks = reader.outline
if bookmarks:
catalogs = []
self._parse_pdf_bookmarks(bookmarks,catalogs)
metadata["catalogs"] = json.dumps(catalogs)
return metadata
def _parse_txt(self,doc_path:str):
return {}
def _parse_md(self,doc_path:str):
metadata = {}
cur_encode = "utf-8"
with open(doc_path,'rb') as f:
cur_encode = chardet.detect(f.read(1024))['encoding']
with open(doc_path, mode='r', encoding=cur_encode) as f:
content = f.read()
match = re.search(r'^# (.*)', content, re.MULTILINE)
if match:
metadata['title'] = match.group(1).strip()
md = Markdown(extensions=['toc'])
html_str = md.convert(content)
toc = md.toc
if toc:
metadata['catalogs'] = toc
return metadata
def _parse_document(self,doc_path:str):
hash_result = None
title = os.path.basename(doc_path)
meta_data = {}
with open(doc_path, "rb") as f:
hash_md5 = hashlib.md5()
for chunk in iter(lambda: f.read(1024*1024), b""):
hash_md5.update(chunk)
hash_result = hash_md5.hexdigest()
try:
if doc_path.endswith(".md"):
meta_data = self._parse_md(doc_path)
elif doc_path.endswith(".pdf"):
meta_data = self._parse_pdf(doc_path)
except Exception as e:
logger.error("parse document %s failed:%s",doc_path,e)
traceback.print_exc()
if meta_data.get("title"):
title = meta_data["title"]
return hash_result,title,meta_data
def _support_file(self,file_name:str) -> bool:
if file_name.endswith(".pdf"):
return True
if file_name.endswith(".md"):
return True
if file_name.endswith(".txt"):
return True
return False
def _scan_dir(self):
while True:
time.sleep(10)
for directory in self.doc_dirs.keys():
now = time.time()
if now - self.doc_dirs[directory] > 60*15:
self.doc_dirs[directory] = time.time()
else:
continue
for root, dirs, files in os.walk(directory):
for file in files:
if self._support_file(file):
full_path = os.path.join(root, file)
full_path = os.path.normpath(full_path)
if self.kb_db.is_doc_exist(full_path):
continue
file_stat = os.stat(full_path)
if file_stat.st_size < 1:
continue
if file_stat.st_size < 1024*1024*8:
#parse and insert
hash,title,meta_data = self._parse_document(full_path)
self.kb_db.add_doc(full_path,file_stat.st_size,file_stat.st_mtime,hash)
self.kb_db.add_knowledge(hash,title,meta_data)
else:
self.kb_db.add_doc(full_path,file_stat.st_size,file_stat.st_mtime)
def _scan_document(self):
while True:
time.sleep(10)
parse_queue = self.kb_db.get_docs_without_hash()
for doc_path in parse_queue:
hash,title,meta_data = self._parse_document(doc_path)
self.kb_db.set_doc_hash(doc_path,hash)
self.kb_db.add_knowledge(hash,title,meta_data)
# merge to standard workspace env, **ABANDON this!** # merge to standard workspace env, **ABANDON this!**
class KnowledgeBaseFileSystemEnvironment(Environment): class KnowledgeBaseFileSystemEnvironment(Environment):
def __init__(self, env_id: str) -> None: def __init__(self, env_id: str) -> None:
@@ -537,3 +713,37 @@ class KnowledgeBaseFileSystemEnvironment(Environment):
content = await f.read(2048) content = await f.read(2048)
return content return content
class ShellEnvironment(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"
+2
View File
@@ -138,3 +138,5 @@ pydub
stability_sdk stability_sdk
sentence-transformers==2.2.2 sentence-transformers==2.2.2
tiktoken tiktoken
markdown
PyPDF2
+3 -1
View File
@@ -217,7 +217,9 @@ class AIOS_Shell:
return "0.5.1" return "0.5.1"
async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str: async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str:
#AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg) if sender == self.username:
AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg)
agent_msg = AgentMsg() agent_msg = AgentMsg()
agent_msg.set(sender,target_id,msg) agent_msg.set(sender,target_id,msg)
agent_msg.topic = topic agent_msg.topic = topic