update kb design, base on objfs and knowled graph.

This commit is contained in:
Liu Zhicong
2024-04-23 04:56:22 -07:00
parent 342464e386
commit af1bb80283
14 changed files with 847 additions and 122 deletions
+2 -1
View File
@@ -29,6 +29,7 @@ from .ai_functions.image_2_text_function import Image2TextFunction
from .environment.workspace_env import WorkspaceEnvironment
from .storage.storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem
from .storage.objfs import ObjFS
from .net import *
from .knowledge import *
@@ -36,4 +37,4 @@ from .package_manager import *
from .utils import *
AIOS_Version = "0.5.2, build 2023-12-15"
AIOS_Version = "0.5.2, build 2024-3-31"
+119 -98
View File
@@ -9,10 +9,11 @@ import sqlite3
import aiofiles
from ..storage.storage import AIStorage
from ..knowledge.knowledge_base import BaseKnowledgeGraph,ObjFSKnowledgeGrpah
from ..frame.compute_kernel import ComputeKernel
from ..frame.contact_manager import ContactManager
from ..frame.contact import Contact
from ..proto.ai_function import ParameterDefine, SimpleAIAction, SimpleAIFunction
from ..proto.ai_function import ParameterDefine, SimpleAIFunction
from ..proto.agent_msg import AgentMsg, AgentMsgType
from ..proto.agent_task import AgentWorkLog
@@ -35,20 +36,34 @@ logger = logging.getLogger(__name__)
class AgentMemory:
def __init__(self,agent_id:str,base_dir:str) -> None:
def __init__(self,agent_id:str,base_dir:str,enable_knowledge_graph = True) -> None:
self.agent_memory_base_dir = base_dir
self.agent_id:str= agent_id
AIStorage.get_instance().ensure_directory_exists(self.agent_memory_base_dir)
AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/experience")
AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/contacts")
AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/relations")
AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/summary")
#AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/experience")
#AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/contacts")
#AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/relations")
#AIStorage.get_instance().ensure_directory_exists(f"{self.agent_memory_base_dir}/summary")
self.memory_db:str = f"{self.agent_memory_base_dir}/memory.db"
self.model_name:str = "gp4-1106-preview"
self.threshold_hours = 72
self.last_think_time : float = 0.0
self.enable_knowledge_graph : bool = enable_knowledge_graph
if self.enable_knowledge_graph:
kb_desc = """The Knowledgegraph is used to store important information obtained by Agent in the conversation.Use the following ways to store information:
/contacts/$name:Related information of the contact
/relations/$obj1/$obj2:The relationship between obj2 and obj1
/summary/$topic:Based on topic summary
"""
self.knowledge_graph = ObjFSKnowledgeGrpah(f"{self.agent_id}.memory",self.memory_db,kb_desc)
BaseKnowledgeGraph.add_kb(self.knowledge_graph)
self.simple_memory_sentences = None
else:
self.knowledge_graph = None
self.simple_memory_sentences : List[str] = []
self.load_memory_meta()
@@ -84,7 +99,7 @@ class AgentMemory:
return chatsession
# return last record time
async def load_records(self,starttime,tokenlimit=8000)->float:
async def load_records(self,starttime,tokenlimit=8000,model_name=None)->float:
# 专用思路:做聊天记录/工作经验的整理
# 通用思路:没有具体的目的,让Agent根据提示词自己工作(可能效果很差也可能很好)
# 先实现通用思路
@@ -92,7 +107,7 @@ class AgentMemory:
work_records = self.load_worklogs(self.agent_id,token_limit=tokenlimit)
pass
async def load_chatlogs(self,msg:AgentMsg,token_limit=800):
async def load_chatlogs(self,msg:AgentMsg,token_limit=800,model_name=""):
chatsession = self.get_session_from_msg(msg)
# Must load n (n> = 2), and hope to load the M
# The information in the # M is gradually added, knowing that it is less than 72 hours from the current time, and consumes enough tokens
@@ -105,7 +120,7 @@ class AgentMemory:
dt = datetime.fromtimestamp(float(msg.create_time))
formatted_time = dt.strftime('%y-%m-%d %H:%M:%S')
record_str = f"{msg.sender},[{formatted_time}]\n{msg.body}\n"
token_limit -= ComputeKernel.llm_num_tokens_from_text(record_str,self.model_name)
token_limit -= ComputeKernel.llm_num_tokens_from_text(record_str)
if token_limit <= 32:
is_all = False
break
@@ -156,7 +171,7 @@ class AgentMemory:
rows = c.fetchall()
return [self.from_db_row(row) for row in rows]
return [self.worklog_from_db_row(row) for row in rows]
def _create_table(self,conn):
c = conn.cursor()
@@ -176,7 +191,7 @@ class AgentMemory:
#conn.close()
@classmethod
def from_db_row(self,row):
def worklog_from_db_row(self,row):
log = AgentWorkLog()
# 这里高度依赖表结构的顺序
log.logid, log.owner_id, log.work_type, log.timestamp, log.content, log.result, meta_str, log.operator = row
@@ -202,6 +217,7 @@ class AgentMemory:
def load_meta(self,Dict):
self.last_think_time = Dict.get("last_think_time",0.0)
self.simple_memory_sentences = Dict.get("simple_memory_sentences",[])
def load_memory_meta(self):
meta_file_path = f"{self.agent_memory_base_dir}/meta.json"
@@ -230,7 +246,12 @@ class AgentMemory:
self.last_think_time = last_time
self.save_memory_meta()
async def get_contact_summary(self,contact_id:str) -> str:
# There is two part of contact summary
# Part 1. user defined summary (set by owner or by contac) , global , imutable
# Part 2. auto generated summary, local in agent memory , mutable
if contact_id is None:
return "Contact id is None"
@@ -241,107 +262,107 @@ class AgentMemory:
result["relation"] = contact_info.relationship
result["notes"] = contact_info.notes
summary_path = f"{self.agent_memory_base_dir}/contacts/{contact_id}.summary"
try:
async with aiofiles.open(summary_path, mode='r') as file:
result["summary"] = await file.read()
# summary_path = f"{self.agent_memory_base_dir}/contacts/{contact_id}.summary"
# try:
# async with aiofiles.open(summary_path, mode='r') as file:
# result["summary"] = await file.read()
except Exception as e:
logger.error(f"read contact summary failed: {e}")
# except Exception as e:
# logger.error(f"read contact summary failed: {e}")
return json.dumps(result,ensure_ascii=False)
async def update_contact_summary(self,contact_id:str,summary:str):
summary_path = f"{self.agent_memory_base_dir}/contacts/{contact_id}.summary"
try:
async with aiofiles.open(summary_path, mode='w') as file:
await file.write(summary)
return "OK"
except Exception as e:
logger.error(f"write contact summary failed: {e}")
return "write contact summary failed: {e}"
# async def update_contact_summary(self,contact_id:str,summary:str):
# summary_path = f"{self.agent_memory_base_dir}/contacts/{contact_id}.summary"
# try:
# async with aiofiles.open(summary_path, mode='w') as file:
# await file.write(summary)
# return "OK"
# except Exception as e:
# logger.error(f"write contact summary failed: {e}")
# return "write contact summary failed: {e}"
async def get_summary(self,object_name:str) -> str:
summary_path = f"{self.agent_memory_base_dir}/{object_name}.summary"
try:
async with aiofiles.open(summary_path, mode='r') as file:
return await file.read()
except Exception as e:
logger.error(f"read summary failed: {e}")
return f"read summary failed: {e}"
# async def get_summary(self,object_name:str) -> str:
# summary_path = f"{self.agent_memory_base_dir}/{object_name}.summary"
# try:
# async with aiofiles.open(summary_path, mode='r') as file:
# return await file.read()
# except Exception as e:
# logger.error(f"read summary failed: {e}")
# return f"read summary failed: {e}"
async def update_summary(self,object_name:str,summary:str) -> str:
summary_path = f"{self.agent_memory_base_dir}/{object_name}.summary"
try:
async with aiofiles.open(summary_path, mode='w') as file:
await file.write(summary)
return "OK"
except Exception as e:
logger.error(f"write summary failed: {e}")
return f"write summary failed: {e}"
# async def update_summary(self,object_name:str,summary:str) -> str:
# summary_path = f"{self.agent_memory_base_dir}/{object_name}.summary"
# try:
# async with aiofiles.open(summary_path, mode='w') as file:
# await file.write(summary)
# return "OK"
# except Exception as e:
# logger.error(f"write summary failed: {e}")
# return f"write summary failed: {e}"
async def list_summary_object_names(self) -> List[str]:
# list dir
try:
contents = os.listdir(self.agent_memory_base_dir)
return [x for x in contents if x.endswith(".summary")]
except Exception as e:
logger.error(f"list summary object names failed: {e}")
return []
# async def list_summary_object_names(self) -> List[str]:
# # list dir
# try:
# contents = os.listdir(self.agent_memory_base_dir)
# return [x for x in contents if x.endswith(".summary")]
# except Exception as e:
# logger.error(f"list summary object names failed: {e}")
# return []
# means object1 feel object2 is ...
async def get_relation_summary(self,object_name1:str,object_name2:str) -> str:
summary_path = f"{self.agent_memory_base_dir}/relations/{object_name1}.relation.{object_name2}.summary"
try:
async with aiofiles.open(summary_path, mode='r') as file:
await file.read()
except FileNotFoundError:
return "no summary"
except Exception as e:
logger.error(f"read relation summary failed: {e}")
return f"read relation summary failed: {e}"
# async def get_relation_summary(self,object_name1:str,object_name2:str) -> str:
# summary_path = f"{self.agent_memory_base_dir}/relations/{object_name1}.relation.{object_name2}.summary"
# try:
# async with aiofiles.open(summary_path, mode='r') as file:
# await file.read()
# except FileNotFoundError:
# return "no summary"
# except Exception as e:
# logger.error(f"read relation summary failed: {e}")
# return f"read relation summary failed: {e}"
async def update_relation_summary(self,object_name1:str,object_name2:str,summary:Dict):
summary_path = f"{self.agent_memory_base_dir}/relations/{object_name1}.relation.{object_name2}.summary"
try:
async with aiofiles.open(summary_path, mode='w') as file:
await file.write(json.dumps(summary))
return "OK"
except Exception as e:
logger.error(f"write relation summary failed: {e}")
return "write relation summary failed: {e}"
# async def update_relation_summary(self,object_name1:str,object_name2:str,summary:Dict):
# summary_path = f"{self.agent_memory_base_dir}/relations/{object_name1}.relation.{object_name2}.summary"
# try:
# async with aiofiles.open(summary_path, mode='w') as file:
# await file.write(json.dumps(summary))
# return "OK"
# except Exception as e:
# logger.error(f"write relation summary failed: {e}")
# return "write relation summary failed: {e}"
async def get_experience(self,topic_name:str) -> str:
experience_path = f"{self.agent_memory_base_dir}/experience/{topic_name}.experience"
try:
async with aiofiles.open(experience_path, mode='r') as file:
await file.read()
except FileNotFoundError:
return "no experience"
except Exception as e:
logger.error(f"read experience failed: {e}")
return f"read experience failed: {e}"
# async def get_experience(self,topic_name:str) -> str:
# experience_path = f"{self.agent_memory_base_dir}/experience/{topic_name}.experience"
# try:
# async with aiofiles.open(experience_path, mode='r') as file:
# await file.read()
# except FileNotFoundError:
# return "no experience"
# except Exception as e:
# logger.error(f"read experience failed: {e}")
# return f"read experience failed: {e}"
async def set_experience(self,topic_name:str,summary:str) -> str:
experience_path = f"{self.agent_memory_base_dir}/experience/{topic_name}.experience"
try:
async with aiofiles.open(experience_path, mode='w') as file:
await file.write(summary)
return "OK"
except Exception as e:
logger.error(f"write experience failed: {e}")
return "write experience failed: {e}"
# async def set_experience(self,topic_name:str,summary:str) -> str:
# experience_path = f"{self.agent_memory_base_dir}/experience/{topic_name}.experience"
# try:
# async with aiofiles.open(experience_path, mode='w') as file:
# await file.write(summary)
# return "OK"
# except Exception as e:
# logger.error(f"write experience failed: {e}")
# return "write experience failed: {e}"
async def list_experience(self) -> List[str]:
dir_path = f"{self.agent_memory_base_dir}/experience"
try:
contents = os.listdir(dir_path)
return [x for x in contents if x.endswith(".experience")]
except Exception as e:
logger.error(f"list experience failed: {e}")
return []
# async def list_experience(self) -> List[str]:
# dir_path = f"{self.agent_memory_base_dir}/experience"
# try:
# contents = os.listdir(dir_path)
# return [x for x in contents if x.endswith(".experience")]
# except Exception as e:
# logger.error(f"list experience failed: {e}")
# return []
@staticmethod
def register_ai_functions():
+41 -18
View File
@@ -14,6 +14,7 @@ from .workspace import AgentWorkspace
from .llm_context import LLMProcessContext,GlobaToolsLibrary, SimpleLLMContext
from ..frame.compute_kernel import ComputeKernel
from ..knowledge.knowledge_base import BaseKnowledgeGraph
from abc import ABC,abstractmethod
import copy
@@ -229,8 +230,7 @@ class LLMAgentBaseProcess(BaseLLMProcess):
self.workspace : AgentWorkspace = None # If Workspace is not none , enable Agent Tasklist
self.memory : AgentMemory = None
self.enable_kb : bool = False
self.kb = None
self.enable_kb_list : List[str] = None
async def initial(self,params:Dict = None) -> bool:
self.memory = params.get("memory")
@@ -265,26 +265,49 @@ class LLMAgentBaseProcess(BaseLLMProcess):
if config.get("context"):
self.context = config.get("context")
if config.get("knowledge_grpah_introduce"):
self.knowledge_grpah_introduce = config.get("knowledge_grpah_introduce")
self.llm_context = SimpleLLMContext()
if config.get("llm_context"):
self.llm_context.load_from_config(config.get("llm_context"))
if config.get("enable_kb"):
self.enable_kb = config.get("enable_kb") == "true"
def prepare_knowledge_grpah_prompt(self) -> Dict:
result = {}
result["introduce"] = BaseKnowledgeGraph.get_kb_default_desc_str()
result["knowledge_graph_list"] = {}
have_kb = False
if self.memory.enable_knowledge_graph:
result["knowledge_graph_list"][self.memory.knowledge_graph.kb_id] = self.memory.knowledge_graph.get_description()
have_kb = True
if self.enable_kb_list:
for kb_id in self.enable_kb_list:
kb = BaseKnowledgeGraph.get_kb(kb_id)
if kb:
have_kb = True
result["knowledge_graph_list"][kb_id] = kb.get_description()
else:
logger.error(f"knowledge base {kb_id} not found")
if have_kb is False:
return None
return result
def prepare_role_system_prompt(self,context_info:Dict) -> Dict:
system_prompt_dict = {}
# System Prompt
## LLM的身份说明
system_prompt_dict["role_description"] = self.role_description
#prompt.append_system_message(self.role_description)
## 处理信息的流程说明
system_prompt_dict["role_description"] = self.role_description
system_prompt_dict["process_rule"] = self.process_description
#prompt.append_system_message(self.process_description)
### 回复的格式
system_prompt_dict["reply_format"] = self.reply_format
#prompt.append_system_message(self.reply_format)
kb_prompt = self.prepare_knowledge_grpah_prompt()
if kb_prompt:
system_prompt_dict["knowledge_graph"] = kb_prompt
## Context
if self.context:
@@ -301,9 +324,13 @@ class LLMAgentBaseProcess(BaseLLMProcess):
def get_action_desc(self) -> Dict:
result = {}
actions_list = self.llm_context.get_all_ai_action()
actions_list = []
actions_list.extend(self.llm_context.get_all_ai_action())
for action in actions_list:
result[action.get_name()] = action.get_description()
return result
async def get_inner_function_for_exec(self,func_name:str) -> AIFunction:
@@ -483,10 +510,6 @@ class AgentMessageProcess(LLMAgentBaseProcess):
#TODO eanble workspace functions?
logger.info(f"workspace is not none,enable workspace functions")
## 给予查询KB的权限
if self.enable_kb:
logger.info(f"enable kb")
### 根据Token Limit加载聊天记录
remain_token = self.get_remain_prompt_length(prompt,json.dumps(system_prompt_dict,ensure_ascii=False))
@@ -575,7 +598,7 @@ class AgentSelfThinking(LLMAgentBaseProcess):
history_str = history_str + record_str
if read_history_msg >= 2:
if ComputeKernel.llm_num_tokens_from_text(history_str,self.model_name) > self.chat_summary_token_len:
session_history["history"] = history_str
chat_history[session_id] = session_history
chatsession.summarize_pos = cur_pos
+1 -1
View File
@@ -105,7 +105,7 @@ class ComputeKernel:
return True
@staticmethod
def llm_num_tokens_from_text(text:str,model:str) -> int:
def llm_num_tokens_from_text(text:str,model:str = None) -> int:
if model is None:
model = "gpt-4-turbo-preview"
+2 -1
View File
@@ -3,4 +3,5 @@ from .vector import *
from .data import *
from .store import KnowledgeStore
from .core_object import *
from .pipeline import *
from .pipeline import *
from .knowledge_base import *
+371
View File
@@ -0,0 +1,371 @@
from abc import ABC, abstractmethod
import json
import os
import uuid
from typing import List
from ..proto.ai_function import ParameterDefine, SimpleAIAction, SimpleAIFunction
from ..agent.llm_context import GlobaToolsLibrary
from ..storage.objfs import ObjFS
import logging
logger = logging.getLogger(__name__)
class BaseKnowledgeGraph(ABC):
_all_knowledge_bases = {}
_default_kb = None
@classmethod
def get_kb(cls, kb_id:str):
if kb_id is None:
return cls._default_kb
return cls._all_knowledge_bases.get(kb_id)
@classmethod
def add_kb(cls,kb:'BaseKnowledgeGraph',is_default=False):
cls._all_knowledge_bases[kb.kb_id] = kb
if is_default:
cls._default_kb = kb
@classmethod
def remove_kb(cls,kb_id:str):
if cls._default_kb is not None and cls._default_kb.kb_id == kb_id:
cls._default_kb = None
if cls._all_knowledge_bases.get(kb_id) is not None:
del cls._all_knowledge_bases[kb_id]
def __init__(self, kb_id: str,kb_desc:str=None):
self.kb_id = kb_id
if kb_desc is None:
self.kb_desc = """
"""
else:
self.kb_desc = kb_desc
def get_description(self)->str:
return self.kb_desc
# 读接口: 查询,浏览
@abstractmethod
async def serach(self, query: str,query_type:str):
pass
@abstractmethod
async def get_obj_by_path(self,path)->str:
pass
@abstractmethod
async def get_obj_by_id(self,obj_id)->str:
pass
@abstractmethod
async def list_by_path(self,base_path)->List[str]:
pass
@abstractmethod
def list_source(self) -> List[str]:
pass
@abstractmethod
async def add_obj(self,obj_id,obj_name,obj_content,paths) -> bool:
pass
@abstractmethod
async def remove(self,remove_path) -> bool:
pass
@abstractmethod
async def remove_obj(self,objid):
pass
@abstractmethod
async def link(self,obj_id,paths) -> bool:
pass
@abstractmethod
async def unlink(self,paths) -> bool:
pass
@abstractmethod
async def update_obj(self,obj_id,new_content) -> bool:
pass
@staticmethod
def get_kb_default_desc_str():
return """The basic design of the Knowledge Graph is
1. Each object can be described in JSON, and have a unique obj_id.
2. The object can be accessed through the PATH, and multiple paths can point to the same object.
3. Carefully understand the semantics of the path, and follow the description of the knowledge graph.You can list all the sub-paths of a path through the LIST operation
All Knowledge Graph APIs return are json format string."""
# 写接口:通常由KnowledgePipeline调用
@staticmethod
def register_ai_functions():
async def knowledge_graph_access(parameters):
kb_id = parameters['kb_id']
op_name = parameters['op']
param = parameters['param']
if op_name is None:
logger.error("Operation type is not specified")
return "Operation type is not specified"
if param is None:
logger.error("Operation parameters is not specified")
return "Error! Operation parameters is not specified"
param = json.loads(param)
kb = BaseKnowledgeGraph.get_kb(kb_id)
if kb is None:
logger.error(f"Knowledge base is not found id:{kb_id}")
return "Error! Knowledge base is not found"
if op_name == "list":
root_path = param.get("path")
if root_path is None:
logger.error("Path is not specified")
return "Error! Path is not specified"
return json.dumps(await kb.list_by_path(root_path), ensure_ascii=False)
if op_name == "tree":
root_path = param.get("path")
if root_path is None:
logger.error("Path is not specified")
return "Error! Path is not specified"
depth = param.get("depth")
if depth is None:
depth = 3
return json.dumps(await kb.tree(root_path,depth), ensure_ascii=False)
if op_name == "read":
obj_path = param.get("path")
if obj_path is None:
logger.error("Path is not specified")
return "Error! Path is not specified"
return json.dumps(await kb.get_obj_by_path(obj_path), ensure_ascii=False)
if op_name == "get_obj":
obj_id = param.get("obj_id")
if obj_id is None:
logger.error("Object ID is not specified")
return "Error! Object ID is not specified"
return json.dumps(await kb.get_obj_by_id(obj_id), ensure_ascii=False)
return "Error! Operation type is not supported"
# search is not supported currently
func_desc = "Read knowledge graph, op_param format is as follows: list:{'path':$path}, read:{'path':$path}, get_obj:{'obj_id':$obj_id}, tree:{'path':$path,'depth':$depth}"
parameters = ParameterDefine.create_parameters({
"kb_id": "Knowledge Base ID",
"op": "Operation Type,could be [list, read, get_obj]",
"op_param": "Operation Param, must be a json string"
})
knowledge_graph_access_func = SimpleAIFunction("knowledge_base.knowledge_graph_read",
func_desc,
knowledge_graph_access,
parameters)
GlobaToolsLibrary.get_instance().register_tool_function(knowledge_graph_access_func)
async def knwoledge_graph_update(parameters):
kb_id = parameters['kb_id']
op_name = parameters['op']
param = parameters['param']
result = {}
if op_name is None:
logger.error("Operation type is not specified")
result["result"] = "Error! Operation type is not specified"
return json.dumps(result, ensure_ascii=False)
if param is None:
logger.error("Operation parameters is not specified")
result["result"] = "Error! Operation parameters is not specified"
return json.dumps(result, ensure_ascii=False)
param = json.loads(param)
kb = BaseKnowledgeGraph.get_kb(kb_id)
if kb is None:
logger.error(f"Knowledge base is not found id:{kb_id}")
result["result"] = "Error! Knowledge base is not found"
return json.dumps(result, ensure_ascii=False)
if op_name == "write":
write_path = param.get("path")
if write_path is None:
logger.error("Path is not specified")
result["result"] = "Error! Path is not specified"
return json.dumps(result, ensure_ascii=False)
obj_content = param.get("obj_json")
if obj_content is None:
logger.error("Object content is not specified")
result["result"] = "Error! Object content is not specified"
return json.dumps(result, ensure_ascii=False)
objid = uuid.uuid4()
objname = os.path.basename(write_path)
paths = []
paths.append(write_path)
if await kb.add_obj(objid,objname,obj_content['content'],paths):
result["result"] = "OK"
result['obj_id'] = objid
else:
result["result"] = "Error! Add object failed"
if op_name == "remove":
remove_path = param.get("path")
if remove_path is None:
logger.error("Path is not specified")
result["result"] = "Error! Path is not specified"
return json.dumps(result, ensure_ascii=False)
if await kb.remove(remove_path):
result["result"] = "OK"
else:
result["result"] = "Error! Remove path failed"
if op_name == "remove_obj":
obj_id = param.get("obj_id")
if obj_id is None:
logger.error("Object ID is not specified")
result["result"] = "Error! Object ID is not specified"
return result
obj = await kb.get_obj_by_id(obj_id)
if obj is None:
logger.error(f"Object is not found id:{obj_id}")
result["result"] = "Error! Object is not found"
return result
await kb.remove_obj(obj_id)
result["result"] = "OK"
if op_name == "set_obj":
obj_id = param.get("obj_id")
if obj_id is None:
logger.error("Object ID is not specified")
result["result"] = "Error! Object ID is not specified"
return json.dumps(result, ensure_ascii=False)
obj = await kb.get_obj_by_id(obj_id)
if obj is None:
logger.error(f"Object is not found id:{obj_id}")
result["result"] = "Error! Object is not found"
return result
obj_content = param.get("obj_json")
if obj_content is None:
logger.error("new object is not specified")
result["result"] = "Error! new object is not specified"
return json.dumps(result, ensure_ascii=False)
await kb.update_obj(obj_id,obj_content)
result["result"] = "OK"
if op_name == "link":
path_from = param.get("path")
path_to = param.get("target")
if path_from is None or path_to is None:
logger.error("Path is not specified")
result["result"] = "Error! Path is not specified"
return json.dumps(result, ensure_ascii=False)
objid = await kb.get_obj_by_path(path_to)
if objid is None:
logger.error(f"Object is not found path:{path_to}")
result["result"] = "Error!Target Object is not found"
return json.dumps(result, ensure_ascii=False)
await kb.link(objid,[path_from])
result["result"] = "OK"
if op_name == "unlink":
path_will_remove = param.get("path")
if path_will_remove is None:
logger.error("Path is not specified")
result["result"] = "Error! Path is not specified"
return json.dumps(result, ensure_ascii=False)
await kb.unlink([path_will_remove])
result["result"] = "OK"
return json.dumps(result, ensure_ascii=False)
OperationParames = """Parameters is a json string, the format is as follows:
write:{'path':$path,'obj_json':$obj_json},
remove:{'path':$path},
remove_obj:{'obj_id':$obj_id},
set_obj:{'obj_id':$obj_id,'obj_json':$new_obj_json},
link:{'path':$path,'target':$target_obj_path},
unlink:{'path':$path}
"""
parameters = ParameterDefine.create_parameters({
"kb_id": "Knowledge Base ID",
"op": "Operation Type,could be [write, remove, remove_obj, set_obj, link, unlink",
"param": OperationParames
})
knowledge_graph_update_func = SimpleAIFunction("knowledge_base.knowledge_graph_update",
"Update Knowledge Graph APIs",
knwoledge_graph_update,
parameters)
GlobaToolsLibrary.get_instance().register_tool_function(knowledge_graph_update_func)
class ObjFSKnowledgeGrpah(BaseKnowledgeGraph):
def __init__(self, kb_id:str,db_path:str,kb_desc:str=None):
super().__init__(kb_id,kb_desc)
self.db_path = db_path
self.obj_storage : ObjFS = ObjFS(db_path)
async def serach(self, query: str,query_type:str):
pass
def list_source(self):
pass
async def get_obj_by_path(self,path)->str:
return self.obj_storage.get_obj_by_path(path)
async def get_obj_by_id(self,obj_id)->str:
return self.obj_storage.get_obj_by_id(obj_id)
async def list_by_path(self,base_path)->List[str]:
return self.obj_storage.list_paths(base_path)
async def tree(self,base_path,depth:int)->str:
return self.obj_storage.tree(base_path,depth)
async def add_obj(self,obj_id,obj_name,obj_content,paths)->bool:
self.obj_storage.add_obj(obj_id,obj_name,obj_content,paths)
#todo 更新默认是做dict的merge
async def update_obj(self, obj_id, new_content)->bool:
return self.obj_storage.update_obj(obj_id,new_content)
async def remove(self,remove_path)->bool:
self.obj_storage.remove_path(remove_path)
async def remove_obj(self,objid)->bool:
self.obj_storage.remove_obj(objid)
async def link(self,from_path,target_path)->bool:
objid = self.obj_storage.get_obj_by_path(target_path)
if objid is None:
return False
self.obj_storage.add_path(objid,from_path)
return True
async def unlink(self,paths)->bool:
self.obj_storage.remove_path(paths)
+18
View File
@@ -0,0 +1,18 @@
from typing import List
class NamedObjectStorage:
def __init__(self, storage, name: str):
self.storage = storage
self.name = name
async def get(self, key: str) -> bytes:
return await self.storage.get(self.name, key)
async def put(self, key: str, data: bytes):
await self.storage.put(self.name, key, data)
async def delete(self, key: str):
await self.storage.delete(self.name, key)
async def list(self) -> List[str]:
return await self.storage.list(self.name)
+217
View File
@@ -0,0 +1,217 @@
from abc import ABC, abstractmethod
import sqlite3
from sqlite3 import Error
from typing import List
import threading
import time
import uuid
import logging
logger = logging.getLogger(__name__)
class ObjFSReader(ABC):
@abstractmethod
def get_obj_by_path(self,path):
pass
@abstractmethod
def get_obj_by_id(self,obj_id):
pass
@abstractmethod
def list_paths(self,base_path):
pass
#ObjFS provides structured data storage similar to brain-like, as an object storage layer of Agent Friendly
class ObjFS(ObjFSReader):
def __init__(self, db_file):
""" initialize db connection """
self.db_file = db_file
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_file)
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 Error as e:
logger.error("Error occurred while connecting to database: %s", e)
return None
if conn:
self._create_table(conn)
return conn
def _create_table(self, conn):
try:
conn.execute('''CREATE TABLE IF NOT EXISTS objects
(id TEXT PRIMARY KEY, name TEXT, content TEXT, created_at REAL, modified_at REAL, size INTEGER)''')
conn.execute('''CREATE TABLE IF NOT EXISTS paths
(id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE, obj_id TEXT, FOREIGN KEY(obj_id) REFERENCES objects(id))''')
except Error as e:
logger.error("Error occurred while creating tables: %s", e)
def close(self):
local = threading.local()
if not hasattr(local, 'conn'):
return
local.conn.close()
def add_obj(self,obj_uuid, name, content, paths) -> bool:
conn = self._get_conn()
c = conn.cursor()
#obj id是guid,由外部生成
# 获取当前时间戳
current_time = time.time()
# 计算内容大小
content_size = len(content.encode('utf-8'))
try:
# 插入对象
c.execute("INSERT INTO objects (id, name, content, created_at, modified_at, size) VALUES (?, ?, ?, ?, ?, ?)", (obj_uuid, name, content, current_time, current_time, content_size))
# 插入路径
for path in paths:
c.execute("INSERT OR IGNORE INTO paths (path, obj_id) VALUES (?, ?)", (path, obj_uuid))
conn.commit()
except Error as e:
logger.warning("Error occurred while adding object: %s", e)
return False
return True
def update_obj(self,obj_id, new_content) -> bool:
#UPDATE orders
#SET data = json_set(
# data,
# '$.items[1].price',
# 0.35
#)
#WHERE id = 1;
try:
conn = self._get_conn()
c = conn.cursor()
# 获取当前时间戳
current_time = time.time()
# 计算新内容大小
new_content_size = len(new_content.encode('utf-8'))
c.execute("UPDATE objects SET content = ?, modified_at = ?, size = ? WHERE id = ?", (new_content, current_time, new_content_size, obj_id))
conn.commit()
return True
except Error as e:
logger.warning("Error occurred while updating object: %s", e)
return False
def add_path(self,obj_id, new_path) -> bool:
try:
conn = self._get_conn()
c = conn.cursor()
c.execute("INSERT OR IGNORE INTO paths (path, obj_id) VALUES (?, ?)", (new_path, obj_id))
conn.commit()
return True
except Error as e:
logger.warning("Error occurred while adding path: %s", e)
return False
def remove_path(self,path) -> bool:
try:
conn = self._get_conn()
c = conn.cursor()
#TODO
c.execute("DELETE FROM paths WHERE path = ?", (path,))
conn.commit()
return True
except Error as e:
logger.warning("Error occurred while removing path: %s", e)
return False
def remove_obj(self,obj_id) -> bool:
try:
conn = self._get_conn()
c = conn.cursor()
c.execute("DELETE FROM objects WHERE id = ?", (obj_id,))
# 删除所有与该对象相关的路径
c.execute("DELETE FROM paths WHERE obj_id = ?", (obj_id,))
conn.commit()
return True
except Error as e:
logger.warning("Error occurred while removing object: %s", e)
return False
def get_obj_by_path(self,path) -> str:
try:
conn = self._get_conn()
c = conn.cursor()
c.execute("SELECT objects.id, objects.name, objects.content FROM objects JOIN paths ON objects.id = paths.obj_id WHERE paths.path = ?", (path,))
obj_row = c.fetchone()
if obj_row:
return obj_row[2]
return None
except Error as e:
logger.warning("Error occurred while getting object by path: %s", e)
return None
def get_obj_by_id(self,obj_id) -> str:
try:
conn = self._get_conn()
c = conn.cursor()
c.execute("SELECT id, name, content FROM objects WHERE id = ?", (obj_id,))
obj_row = c.fetchone()
if obj_row:
return obj_row[2]
return None
except Error as e:
logger.warning("Error occurred while getting object by id: %s", e)
return None
def list_paths(self,base_path)->List[str]:
try:
conn = self._get_conn()
c = conn.cursor()
c.execute("SELECT path FROM paths WHERE path LIKE ? ESCAPE '/'", (base_path + "/%",))
return [row[0] for row in c.fetchall()]
except Error as e:
logger.warning("Error occurred while listing paths: %s", e)
return None
def tree(self, base_path,max_depth=3):
try:
conn = self._get_conn()
c = conn.cursor()
c.execute("SELECT path FROM paths WHERE path LIKE ? ESCAPE '/'", (base_path + "/%",))
paths = [row[0] for row in c.fetchall()]
tree = {}
for path in paths:
parts = path.split("/")
node = tree
for part in parts:
if part not in node:
node[part] = {}
node = node[part]
return tree
except Error as e:
logger.warning("Error occurred while listing paths: %s", e)
return None
+1 -1
View File
@@ -27,7 +27,7 @@ class OpenAI_ComputeNode(ComputeNode):
@classmethod
def declare_user_config(cls):
if os.getenv("OPENAI_API_KEY_") is None:
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)
+1
View File
@@ -153,6 +153,7 @@ class AIOS_Shell:
#AgentManager.get_instance().register_environment("knowledge", LocalKnowledgeBase)
AgentWorkspace.register_ai_functions()
AgentMemory.register_ai_functions()
BaseKnowledgeGraph.register_ai_functions()
ShellEnvironment.register_ai_functions()