fix text summary build bug

This commit is contained in:
Liu Zhicong
2023-11-14 16:49:36 -08:00
parent 5fe3073cb6
commit 87fdba9714
4 changed files with 206 additions and 93 deletions
+4 -5
View File
@@ -1,7 +1,6 @@
instance_id = "Jarvis" instance_id = "Jarvis"
fullname = "Jarvis" fullname = "Jarvis"
llm_model_name = "gpt-3.5-turbo-16k-0613" max_token_size = 128000
max_token_size = 16000
#enable_kb = "true" #enable_kb = "true"
enable_timestamp = "true" enable_timestamp = "true"
owner_prompt = "I am your master{name}" owner_prompt = "I am your master{name}"
@@ -23,9 +22,9 @@ Upon receiving a message, handle it according to the following rules:
1. If you believe someone in the team is better suited to address the message, forward the message to them using the method below: 1. If you believe someone in the team is better suited to address the message, forward the message to them using the method below:
##/send_msg "MemberName" ##/send_msg "MemberName"
Message content Message content
2.You can access the master's Calendar to view his schedule. If you need to modify the master's schedule while processing a message, please adjust it using the appropriate method. 2. You can access the master's Calendar to view his schedule. If you need to modify the master's schedule while processing a message, please adjust it using the appropriate method.
3.Be mindful of the identity of the person you are chatting with and provide services accordingly based on their status. 3. Be mindful of the identity of the person you are chatting with and provide services accordingly based on their status.
4.For messages that don't follow the above rules, do your best to handle them. 4. For messages that don't follow the above rules, do your best to handle them.
""" """
+116 -44
View File
@@ -21,6 +21,7 @@ from .contact_manager import ContactManager,Contact,FamilyMember
from .compute_kernel import ComputeKernel from .compute_kernel import ComputeKernel
from .bus import AIBus from .bus import AIBus
from .workspace_env import WorkspaceEnvironment from .workspace_env import WorkspaceEnvironment
from .storage import AIStorage
from knowledge import * from knowledge import *
@@ -53,11 +54,36 @@ DEFAULT_AGENT_GOAL_TO_TODO_PROMPT = """
""" """
DEFAULT_AGENT_LEARN_PROMPT = """ DEFAULT_AGENT_LEARN_PROMPT = """
你拥有非常优秀的资料整理技能。我给你一段内容,你会尝试对其进行摘要,并在已有的资料库中找到合适的位置存放该文章。 我是一名软件工程师,拥有非常优秀的资料学习能力。下面是我学习和整理资料的方法
1. 结合的角色和组织的工作目标构建摘要,尽量精简,长度不超过256个 1. 结合的角色为资料产生长度不超过256个Token的摘要;尝试产生不超过16个tag;
2. 资料库以文件系统的形式组织,浏览知识库是成本高昂的操作,应尝试从根目录往子目录深入来找到最合适的信息。必要的情况下,你可以在合适的位置创建新的目录。为了方便浏览,每一层目录的文件夹数不超过32个,名称长度不超过16个字符,目录深度不超过6层 2. 现有资料库以文件系统的形式组织,我未来借助资料的摘要来浏览知识库
3. 你可以从不同的角度给出最多3个合适的位置 3. 我将学习过的资料另存在资料库的合适位置(以/开始的完整路径)。保存位置的目录深度不超过5层,文件夹名称长度不超过16个字符。
4. 返回一个json来保存摘要和建议保存位置信息 4. 当存在已知信息时,需参考已知信息的内容来思考结果。
5. 由于LLM的Token限制,我学习的可能只是资料的部分内容,此时我应能产生合适的中间结果,中间结果保存在metadata中。当我决定构建中间结果时,我只构建中间结果。
6. 当我收到最后一部分内容时,我能结合已知的中间结果产生最终结果。
7. 总是以json格式返回思考结果,json格式如下
{
think:"$think_result",
metadata:{...} , # temp result for long content
tags:["tag1","tag2"...],
path:["/graphic/opengl","/database/mysql"], # list of directories to save to.
title:"$article_title",
summary:"$summary",
catalogs: [{ # optional,catalogs is a tree
title:"$catalog_name1",
pos:"$pos:$length"
children:[
{
title:"$catalog_name 1.1",
pos:"$pos:$length"
}
]},
{
title:"$catalog_name2",
pos:"$pos:$length"
}
]
}
""" """
DEFAULT_AGENT_LEARN_LONG_CONENT_PROMPT = """ DEFAULT_AGENT_LEARN_LONG_CONENT_PROMPT = """
@@ -125,7 +151,7 @@ class AIAgent:
self.goal_to_todo_prompt = None self.goal_to_todo_prompt = None
self.learn_token_limit = 500 self.learn_token_limit = 4000
self.learn_prompt = AgentPrompt(DEFAULT_AGENT_LEARN_PROMPT) self.learn_prompt = AgentPrompt(DEFAULT_AGENT_LEARN_PROMPT)
self.chat_db = None self.chat_db = None
@@ -217,6 +243,9 @@ class AIAgent:
return self.template_id return self.template_id
def get_llm_model_name(self) -> str: def get_llm_model_name(self) -> str:
if self.llm_model_name is None:
return AIStorage.get_instance().get_user_config().get_value("llm_model_name")
return self.llm_model_name return self.llm_model_name
def get_max_token_size(self) -> int: def get_max_token_size(self) -> int:
@@ -889,22 +918,32 @@ class AIAgent:
if self.agent_energy <= 0: if self.agent_energy <= 0:
break break
knowledge = workspace.kb_db.get_knowledge_by_hash(hash) knowledge = workspace.kb_db.get_knowledge(hash)
if knowledge is None: if knowledge is None:
continue continue
if os.path.exists(knowledge.path) is False: full_path = knowledge.get("full_path")
logger.warning(f"do_self_learn: knowledge {knowledge.path} is not exists!") if full_path is None:
continue
if os.path.exists(full_path) is False:
logger.warning(f"do_self_learn: knowledge {full_path} is not exists!")
continue continue
#TODO 可以用v-db 对不同目录的名字进行选择后,先进行一次快速的插入。有时间再慢慢用LLM整理 #TODO 可以用v-db 对不同目录的名字进行选择后,先进行一次快速的插入。有时间再慢慢用LLM整理
llm_result = await self._llm_read_article(knowledge) result_obj = await self._llm_read_article(knowledge,full_path)
#根据结果更新knowledge #根据结果更新knowledge
if llm_result is not None: if result_obj is not None:
workspace.kb_db.update_knowledge_by_hash(hash,llm_result) workspace.kb_db.set_knowledge_llm_result(hash,result_obj)
# 在知识库中创建软链接 # 在知识库中创建软链接
path_list = result_obj.get("path")
new_title = result_obj.get("title")
if path_list:
for new_path in path_list:
full_new_path = f"/knowledge{new_path}/{new_title}"
await workspace.symlink(full_path,full_new_path)
logger.info(f"create soft link {full_path} -> {full_new_path}")
self.agent_energy -= 1 self.agent_energy -= 1
@@ -958,13 +997,11 @@ class AIAgent:
def parser_learn_llm_result(self,llm_result:LLMResult): def parser_learn_llm_result(self,llm_result:LLMResult):
pass pass
async def gen_known_info_for_knowledge_prompt(self,knowledge_item:dict,need_catalogs = False) -> AgentPrompt: async def gen_known_info_for_knowledge_prompt(self,knowledge_item:dict,temp_meta = None,need_catalogs = False) -> AgentPrompt:
#已知信息:
# 组织的工作总结(如有)待完成
# 现在知识库的结构(注意大小控制)gen_kb_tree_prompt (当为空的时候应该让LLM生成一个合适的初始目录结构)
# 原始路径,现在标题,摘要,目录
workspace =self.get_workspace_by_msg(None) workspace =self.get_workspace_by_msg(None)
kb_tree = await workspace.get_knowledege_catalog() kb_tree = await workspace.get_knowledege_catalog()
known_obj = {} known_obj = {}
title = knowledge_item.get("title") title = knowledge_item.get("title")
if title: if title:
@@ -980,36 +1017,40 @@ class AIAgent:
if catalogs: if catalogs:
known_obj["catalogs"] = catalogs known_obj["catalogs"] = catalogs
org_path = knowledge_item.get("path") if temp_meta:
for key in temp_meta.keys():
known_obj[key] = temp_meta[key]
org_path = knowledge_item.get("full_path")
known_obj["orginal_path"] = org_path known_obj["orginal_path"] = org_path
know_info_str = f"# Known information\n{json.dumps(known_obj)}\n" know_info_str = f"# Known information:\n## Current directory structure:\n{kb_tree}\n## Knowlege Metadata:\n{json.dumps(known_obj)}\n"
return AgentPrompt(know_info_str) return AgentPrompt(know_info_str)
async def _llm_read_article(self,knowledge_item:dict,full_path:str) -> ComputeTaskResult:
# Objectives:
# Obtain better titles, abstracts, table of contents (if necessary), tags
# Determine the appropriate place to put it (in line with the organization's goals)
# Known information:
# The reason why the target service's learn_prompt is being sorted
# Summary of the organization's work (if any)
# The current structure of the knowledge base (note the size control) gen_kb_tree_prompt (when empty, LLM should generate an appropriate initial directory structure)
# Original path, current title, abstract, table of contents
# Sorting long files (general tricks)
# Indicate that the input is part of the content, let LLM generate intermediate results for the task
# Enter the content in sequence, when the last content block is input, LLM gets the result
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() #full_content = item.get_article_full_content()
workspace = self.get_workspace_by_msg(None) workspace = self.get_workspace_by_msg(None)
full_content = await workspace.load_knowledge_content(knowledge_item["hash"]) full_content = await workspace.load_knowledge_content(full_path)
if full_content is None: if full_content is None:
return return None
if len(full_content) < 16:
logger.warning(f"llm_read_article: article {knowledge_item['path']} is too short,just read summary!")
return None
full_content_len = self.token_len(full_content) 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():
@@ -1021,9 +1062,9 @@ class AIAgent:
prompt.append(known_info_prompt) prompt.append(known_info_prompt)
content_prompt = AgentPrompt(full_content) content_prompt = AgentPrompt(full_content)
prompt.append(content_prompt) prompt.append(content_prompt)
env_functions = None
env_functions = workspace.get_knowledge_base_ai_functions() #env_functions,function_len = 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,None,True)
if task_result.result_code != ComputeTaskResultCode.OK: if task_result.result_code != ComputeTaskResultCode.OK:
result_obj = {} result_obj = {}
result_obj["error_str"] = task_result.error_str result_obj["error_str"] = task_result.error_str
@@ -1033,11 +1074,42 @@ class AIAgent:
return result_obj return result_obj
else: else:
logger.warning(f"llm_read_article: article {knowledge_item['path']} is too long,just read summary!") logger.warning(f"llm_read_article: article {full_path} use LLM loop learn!")
pos = 0
read_len = int(self.get_llm_learn_token_limit() * 1.5)
temp_meta_data = {}
is_final = False
while pos < full_content_len:
_content = full_content[pos:pos+read_len]
if len(_content) < read_len:
# last chunk
is_final = True
part_content = f"<<Final Part:start at {pos}>>\n{_content}"
else:
part_content = f"<<Part:start at {pos}>>\n{_content}"
pos = pos + read_len
prompt = self.get_learn_prompt()
known_info_prompt = await self.gen_known_info_for_knowledge_prompt(knowledge_item,temp_meta_data)
prompt.append(known_info_prompt)
content_prompt = AgentPrompt(part_content)
prompt.append(content_prompt)
env_functions = None
#env_functions,function_len = workspace.get_knowledge_base_ai_functions()
task_result:ComputeTaskResult = await self._do_llm_complection(prompt,env_functions,None,True)
if task_result.result_code != ComputeTaskResultCode.OK:
result_obj = {} result_obj = {}
result_obj["error_str"] = f"llm_read_article: article {knowledge_item['path']} is too long,just read summary!" result_obj["error_str"] = task_result.error_str
return result_obj return result_obj
result_obj = json.loads(task_result.result_str)
temp_meta_data = result_obj.get("metadata")
if is_final:
return result_obj
return None
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)
+6 -3
View File
@@ -142,7 +142,7 @@ class SimpleKnowledgeDB:
title = llm_result.get("title", "") title = llm_result.get("title", "")
summary = llm_result.get("summary", "") summary = llm_result.get("summary", "")
catalogs = json.dumps(llm_result.get("catalogs", [])) catalogs = json.dumps(llm_result.get("catalogs", {}))
tags = ','.join(llm_result.get("tags", [])) tags = ','.join(llm_result.get("tags", []))
cursor.execute(''' cursor.execute('''
@@ -188,12 +188,15 @@ class SimpleKnowledgeDB:
return None return None
doc_path = row2[0] doc_path = row2[0]
return { return {
"full_path": doc_path, "full_path": doc_path,
"title": row[0], "title": row[0],
"summary": row[1], "summary": row[1],
"catalogs": json.loads(row[2]), "catalogs": row[2],
"tags": row[3].split(","), "tags": row[3],
"llm_title" : row[4],
"llm_summary" : row[5],
} }
def get_knowledge_without_llm_title(self,limit:int=16) -> List[str]: def get_knowledge_without_llm_title(self,limit:int=16) -> List[str]:
+61 -22
View File
@@ -190,10 +190,13 @@ class WorkspaceEnvironment(Environment):
# operation or inner_function # operation or inner_function
async def symlink(self,path:str,target:str) -> str: async def symlink(self,path:str,target:str) -> str:
try: try:
file_path = self.root_path + path #file_path = self.root_path + path
target_path = self.root_path + target target_path = self.root_path + target
os.symlink(file_path,target_path) dir_path = os.path.dirname(target_path)
os.makedirs(dir_path,exist_ok=True)
os.symlink(path,target_path)
except Exception as e: except Exception as e:
logger.error("symlink failed:%s",e)
return str(e) return str(e)
return None return None
@@ -435,18 +438,30 @@ class WorkspaceEnvironment(Environment):
# knowledge base system # knowledge base system
def get_knowledge_base_ai_functions(self): def get_knowledge_base_ai_functions(self):
func_result = {} all_inner_function = []
func_result["get_knowledge_catalog"] = SimpleAIFunction("get_knowledge_catalog","get knowledge catalog in tree format", all_inner_function.append(SimpleAIFunction("get_knowledge_catalog","get knowledge catalog in tree format",
self.get_knowledege_catalog, self.get_knowledege_catalog,
{"path":f"catalog path,none is /","depth":"max depth of catalog tree,default is 4"}) {"path":f"catalog path,none is /","depth":"max depth of catalog tree,default is 4"}))
func_result["get_knowledge"] = SimpleAIFunction("get_knowledge","get knowledge metadata", all_inner_function.append(SimpleAIFunction("get_knowledge","get knowledge metadata",
self.get_knowledge, self.get_knowledge,
{"path":f"knowledge path"}) {"path":f"knowledge path"}))
func_result["load_knowledge_content"] = SimpleAIFunction("load_knowledge_content","load knowledge content", all_inner_function.append(SimpleAIFunction("load_knowledge_content","load knowledge content",
self.load_knowledge_content, self.load_knowledge_content,
{"path":f"knowledge path","pos":"start position of content","length":"length of content"}) {"path":f"knowledge path","pos":"start position of content","length":"length of content"}))
return func_result result_func = []
result_len = 0
for inner_func in all_inner_function:
func_name = inner_func.get_name()
this_func = {}
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
result_func.append(this_func)
return result_func,result_len
async def get_knowledege_catalog(self,path:str=None,only_dir =True,max_depth:int=5)->str: async def get_knowledege_catalog(self,path:str=None,only_dir =True,max_depth:int=5)->str:
if path: if path:
@@ -499,19 +514,24 @@ class WorkspaceEnvironment(Environment):
return "not found" return "not found"
async def load_knowledge_content(self,path:str,pos:int=0,length:int=0) -> str: async def load_knowledge_content(self,path:str,pos:int=0,length:int=None) -> str:
full_path = f"{self.root_path}/knowledge/{path}" if path.endswith("pdf"):
if os.islink(full_path):
org_path = os.readlink(full_path)
if full_path.endswith("pdf"):
logger.info("load_knowledge_content:pdf") logger.info("load_knowledge_content:pdf")
return "pdf is not support now!" dir_path = os.path.dirname(path)
base_name = os.path.basename(path)
text_content_path = f"{dir_path}/.{base_name}.txt"
if os.path.exists(text_content_path) is False:
return None
async with aiofiles.open(path, mode='r', encoding=cur_encode) as f:
await f.seek(pos)
content = await f.read(length)
return content
else: else:
async with aiofiles.open(full_path,'rb') as f: async with aiofiles.open(path,'rb') as f:
cur_encode = chardet.detect(f.read(1024))['encoding'] cur_encode = chardet.detect(await f.read(1024))['encoding']
async with aiofiles.open(full_path, mode='r', encoding=cur_encode) as f: async with aiofiles.open(path, mode='r', encoding=cur_encode) as f:
f.seek(pos) await f.seek(pos)
content = await f.read(length) content = await f.read(length)
return content return content
@@ -550,22 +570,38 @@ class WorkspaceEnvironment(Environment):
return return
def _parse_pdf(self,doc_path:str): def _parse_pdf(self,doc_path:str):
metadata = {} metadata = {}
with open(doc_path, 'rb') as file: with open(doc_path, 'rb') as file:
reader = PyPDF2.PdfReader(file) reader = PyPDF2.PdfReader(file)
try:
doc_info = reader.metadata doc_info = reader.metadata
if doc_info: if doc_info:
if doc_info.title: if doc_info.title:
metadata["title"] = doc_info.title metadata["title"] = doc_info.title
if doc_info.author: if doc_info.author:
metadata["authors"] = doc_info.author metadata["authors"] = doc_info.author
except Exception as e:
logger.warn("parse pdf metadata failed:%s",e)
dir_path = os.path.dirname(doc_path)
base_name = os.path.basename(doc_path)
text_content_path = f"{dir_path}/.{base_name}.txt"
full_text = ""
for page in reader.pages:
text = page.extract_text()
full_text += text
with open(text_content_path, 'w', encoding='utf-8') as f:
f.write(full_text)
try:
bookmarks = reader.outline bookmarks = reader.outline
if bookmarks: if bookmarks:
catalogs = [] catalogs = []
self._parse_pdf_bookmarks(bookmarks,catalogs) self._parse_pdf_bookmarks(bookmarks,catalogs)
metadata["catalogs"] = json.dumps(catalogs) metadata["catalogs"] = json.dumps(catalogs)
except Exception as e:
logger.warn("parse pdf bookmarks failed:%s",e)
return metadata return metadata
@@ -612,11 +648,14 @@ class WorkspaceEnvironment(Environment):
if meta_data.get("title"): if meta_data.get("title"):
title = meta_data["title"] title = meta_data["title"]
logger.info("parse document %s!",doc_path)
return hash_result,title,meta_data return hash_result,title,meta_data
def _support_file(self,file_name:str) -> bool: def _support_file(self,file_name:str) -> bool:
if file_name.startswith("."):
return False
if file_name.endswith(".pdf"): if file_name.endswith(".pdf"):
return True return True
if file_name.endswith(".md"): if file_name.endswith(".md"):