Merge pull request #68 from photosssa/MVP

Add local text/image embedding node; Add an agent Mia to use local knowledge
This commit is contained in:
Liu Zhicong
2023-09-27 20:29:10 -07:00
committed by GitHub
24 changed files with 783 additions and 265 deletions
+10 -22
View File
@@ -2,34 +2,22 @@ instance_id = "Mia"
fullname = "Mia" fullname = "Mia"
llm_model_name = "gpt-3.5-turbo-16k-0613" llm_model_name = "gpt-3.5-turbo-16k-0613"
max_token_size = 16000 max_token_size = 16000
enable_kb = "true" #enable_function =["add_event"]
#enable_timestamp = "true" #enable_kb = "true"
#owner_prompt = "我是你的主人{name}" enable_timestamp = "true"
contact_prompt = "我是你的主人朋友{name},请回答主人允许回答的问题" owner_prompt = "我是你的主人{name}"
guest_prompt = "我是你的的主人的客人{name},请不要回答我的任何问题" contact_prompt = "我是你的朋友{name}"
owner_env = "calender" owner_env = "knowledge"
[[prompt]] [[prompt]]
role = "system" role = "system"
content = """ content = """
你叫Jarvis,是我的超级私人助理 你叫Mia,你可以访问我的个人知识库
你领导一个团队为我服务,团队的成员有:
Tracy,私人英语老师
David,私人画家
*** ***
你看到的信息里有的有时会带上时间标签,这是为了让你更好的理解时间。你回复的信息不用创建这个时间标签。
你在收到我的信息后,按如下规则处理 你在收到我的信息后,按如下规则处理
1. 如果你认为团队里有人更适合处理该信息,用下面方法转发消息给他们处理 1. 在第一次接受到一条信息时,优先尝试用合适的关键字查询去查询知识库。
``` 2. 如果信息中包含一段知识库的查询结果,尝试用查询结果处理,如果还是不能处理,尝试递增index继续查询。
##/send_msg 成员名字 3. 如果知识库返回不了结果了,请尽力返回。
消息内容
```
2.你可以访问我的Calender,查看我的日程安排。如果你在处理信息的过程中需要修改我的日程安排,请直接用合适的方法修改。
3.不符合上述规则的信息,请尽力处理。
""" """
#3.你可以访问我的个人信息库,当你处理我的信息时,如果需要用到我的个人信息,请先用合适的方法进行查询,然后再基于查询的结果进行进一步处理后再将结果发给我。
#4.你能根据我的需要对系统进行配置,但在修改任何配置前,请先和我确认。
+2 -1
View File
@@ -5,7 +5,7 @@ from .agent import AIAgent,AIAgentTemplete,AgentPrompt
from .compute_kernel import ComputeKernel,ComputeTask,ComputeTaskResult,ComputeTaskState,ComputeTaskType from .compute_kernel import ComputeKernel,ComputeTask,ComputeTaskResult,ComputeTaskState,ComputeTaskType
from .compute_node import ComputeNode,LocalComputeNode 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 .knowledge_base import KnowledgeBase, KnowledgeEnvironment
from .knowledge_pipeline import KnowledgeEmailSource, KnowledgeDirSource, KnowledgePipline from .knowledge_pipeline import KnowledgeEmailSource, KnowledgeDirSource, KnowledgePipline
from .role import AIRole,AIRoleGroup from .role import AIRole,AIRoleGroup
from .workflow import Workflow from .workflow import Workflow
@@ -23,6 +23,7 @@ from .text_to_speech_function import TextToSpeechFunction
from .workspace_env import WorkspaceEnvironment from .workspace_env import WorkspaceEnvironment
from .local_stability_node import Local_Stability_ComputeNode from .local_stability_node import Local_Stability_ComputeNode
from .stability_node import Stability_ComputeNode from .stability_node import Stability_ComputeNode
from .local_st_compute_node import LocalSentenceTransformer_Text_ComputeNode,LocalSentenceTransformer_Image_ComputeNode
AIOS_Version = "0.5.1, build 2023-9-27" AIOS_Version = "0.5.1, build 2023-9-27"
+1 -1
View File
@@ -148,7 +148,7 @@ class ComputeKernel:
task_result = await self._send_task(task_req) task_result = await self._send_task(task_req)
if task_req.state == ComputeTaskState.DONE: if task_req.state == ComputeTaskState.DONE:
return task_result.result return task_result.result_str
return "error!" return "error!"
+15 -1
View File
@@ -2,6 +2,8 @@
from enum import Enum from enum import Enum
import uuid import uuid
import time import time
from typing import Union
from knowledge import ObjectID
class ComputeTaskResultCode(Enum): class ComputeTaskResultCode(Enum):
OK = 0 OK = 0
@@ -25,6 +27,7 @@ class ComputeTaskType(Enum):
VOICE_2_TEXT = "voice_2_text" VOICE_2_TEXT = "voice_2_text"
TEXT_2_VOICE = "text_2_voice" TEXT_2_VOICE = "text_2_voice"
TEXT_EMBEDDING ="text_embedding" TEXT_EMBEDDING ="text_embedding"
IMAGE_EMBEDDING ="image_embedding"
class ComputeTask: class ComputeTask:
@@ -60,7 +63,7 @@ class ComputeTask:
if inner_functions is not None: if inner_functions is not None:
self.params["inner_functions"] = inner_functions self.params["inner_functions"] = inner_functions
def set_text_embedding_params(self, input, model_name=None, callchain_id = None): def set_text_embedding_params(self, input: str, model_name=None, callchain_id = None):
self.task_type = ComputeTaskType.TEXT_EMBEDDING self.task_type = ComputeTaskType.TEXT_EMBEDDING
self.create_time = time.time() self.create_time = time.time()
self.task_id = uuid.uuid4().hex self.task_id = uuid.uuid4().hex
@@ -70,6 +73,17 @@ class ComputeTask:
else: else:
self.params["model_name"] = "text-embedding-ada-002" self.params["model_name"] = "text-embedding-ada-002"
self.params["input"] = input self.params["input"] = input
def set_image_embedding_params(self, input = Union[ObjectID, bytes], model_name=None, callchain_id = None):
self.task_type = ComputeTaskType.IMAGE_EMBEDDING
self.create_time = time.time()
self.task_id = uuid.uuid4().hex
self.callchain_id = callchain_id
if model_name is not None:
self.params["model_name"] = model_name
else:
self.params["model_name"] = None
self.params["input"] = input
def set_text_2_image_params(self, prompt: str, model_name, negative_prompt="", callchain_id=None): def set_text_2_image_params(self, prompt: str, model_name, negative_prompt="", callchain_id=None):
self.task_type = ComputeTaskType.TEXT_2_IMAGE self.task_type = ComputeTaskType.TEXT_2_IMAGE
+39 -34
View File
@@ -4,6 +4,8 @@ import logging
from .agent import AgentPrompt from .agent import AgentPrompt
from .compute_kernel import ComputeKernel from .compute_kernel import ComputeKernel
from .storage import AIStorage from .storage import AIStorage
from .environment import Environment
from .ai_function import SimpleAIFunction
from knowledge import * from knowledge import *
@@ -20,6 +22,7 @@ class KnowledgeBase:
def __singleton_init__(self) -> None: def __singleton_init__(self) -> None:
self.store = KnowledgeStore() self.store = KnowledgeStore()
self.compute_kernel = ComputeKernel.get_instance() self.compute_kernel = ComputeKernel.get_instance()
self._default_text_model = "all-MiniLM-L6-v2"
async def __embedding_document(self, document: DocumentObject): async def __embedding_document(self, document: DocumentObject):
for chunk_id in document.get_chunk_list(): for chunk_id in document.get_chunk_list():
@@ -28,8 +31,8 @@ class KnowledgeBase:
raise ValueError(f"text chunk not found: {chunk_id}") raise ValueError(f"text chunk not found: {chunk_id}")
text = chunk.read().decode("utf-8") text = chunk.read().decode("utf-8")
vector = await self.compute_kernel.do_text_embedding(text) vector = await self.compute_kernel.do_text_embedding(text, self._default_text_model)
await self.store.get_vector_store("default").insert(vector, chunk_id) await self.store.get_vector_store(self._default_text_model).insert(vector, chunk_id)
async def __embedding_image(self, image: ImageObject): async def __embedding_image(self, image: ImageObject):
desc = {} desc = {}
@@ -39,8 +42,8 @@ class KnowledgeBase:
desc["exif"] = image.get_exif() desc["exif"] = image.get_exif()
if not not image.get_tags(): if not not image.get_tags():
desc["tags"] = image.get_tags() desc["tags"] = image.get_tags()
vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) vector = await self.compute_kernel.do_text_embedding(json.dumps(desc), self._default_text_model)
await self.store.get_vector_store("default").insert(vector, image.calculate_id()) await self.store.get_vector_store(self._default_text_model).insert(vector, image.calculate_id())
async def __embedding_video(self, vedio: VideoObject): async def __embedding_video(self, vedio: VideoObject):
desc = {} desc = {}
@@ -50,8 +53,8 @@ class KnowledgeBase:
desc["info"] = vedio.get_info() desc["info"] = vedio.get_info()
if not not vedio.get_tags(): if not not vedio.get_tags():
desc["tags"] = vedio.get_tags() desc["tags"] = vedio.get_tags()
vector = await self.compute_kernel.do_text_embedding(json.dumps(desc)) vector = await self.compute_kernel.do_text_embedding(json.dumps(desc), self._default_text_model)
await self.store.get_vector_store("default").insert(vector, vedio.calculate_id()) await self.store.get_vector_store(self._default_text_model).insert(vector, vedio.calculate_id())
async def __embedding_rich_text(self, rich_text: RichTextObject): async def __embedding_rich_text(self, rich_text: RichTextObject):
for document_id in rich_text.get_documents().values(): for document_id in rich_text.get_documents().values():
@@ -68,8 +71,8 @@ class KnowledgeBase:
await self.__embedding_rich_text(rich_text) await self.__embedding_rich_text(rich_text)
async def __embedding_email(self, email: EmailObject): async def __embedding_email(self, email: EmailObject):
vector = await self.compute_kernel.do_text_embedding(json.dumps(email.get_desc())) vector = await self.compute_kernel.do_text_embedding(json.dumps(email.get_desc()), self._default_text_model)
await self.store.get_vector_store("default").insert(vector, email.calculate_id()) await self.store.get_vector_store(self._default_text_model).insert(vector, email.calculate_id())
await self.__embedding_rich_text(email.get_rich_text()) await self.__embedding_rich_text(email.get_rich_text())
@@ -159,23 +162,10 @@ class KnowledgeBase:
async def insert_object(self, object: KnowledgeObject): async def insert_object(self, object: KnowledgeObject):
self.store.get_object_store().put_object(object.calculate_id(), object.encode()) self.store.get_object_store().put_object(object.calculate_id(), object.encode())
await self.__do_embedding(object) await self.__do_embedding(object)
async def query_prompt(self, prompt: AgentPrompt): async def query_objects(self, tokens: str, topk: int) -> [ObjectID]:
logging.info(f"query_prompt: {prompt}") vector = await self.compute_kernel.do_text_embedding(tokens, self._default_text_model)
objects = await self.query_objects(prompt) return await self.store.get_vector_store(self._default_text_model).query(vector, topk)
knowledge_prompt = self.prompt_from_objects(objects)
logging.info(f"prompt_from_objects result: {knowledge_prompt.as_str()}")
return 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 = await self.store.get_vector_store("default").query(vector, 10)
results.extend(object_ids)
return results
def __load_object(self, object_id: ObjectID) -> KnowledgeObject: def __load_object(self, object_id: ObjectID) -> KnowledgeObject:
if object_id.get_object_type() == ObjectType.Document: if object_id.get_object_type() == ObjectType.Document:
@@ -192,7 +182,7 @@ class KnowledgeBase:
pass pass
def prompt_from_objects(self, object_ids: [ObjectID]) -> AgentPrompt: def tokens_from_objects(self, object_ids: [ObjectID]) -> list[str]:
results = dict() results = dict()
for object_id in object_ids: for object_id in object_ids:
parents = self.store.get_relation_store().get_related_root_objects(object_id) parents = self.store.get_relation_store().get_related_root_objects(object_id)
@@ -203,8 +193,7 @@ class KnowledgeBase:
results[str(root_object_id)].append(object_id) results[str(root_object_id)].append(object_id)
else: else:
results[str(root_object_id)] = [root_object_id, object_id] results[str(root_object_id)] = [root_object_id, object_id]
content = ""
content = "*** I have provided the following known information for your reference with json format:\n"
result_desc = [] result_desc = []
for result in results.values(): for result in results.values():
# first element in result is the root object # first element in result is the root object
@@ -236,12 +225,28 @@ class KnowledgeBase:
else: else:
pass pass
content += json.dumps(result_desc) content += json.dumps(result_desc)
content += ".\n" content += ".\n"
prompt = AgentPrompt() return content
prompt.messages.append({"role": "user", "content": content})
return prompt
class KnowledgeEnvironment(Environment):
def __init__(self, env_id: str) -> None:
super().__init__(env_id)
query_param = {
"tokens": "tokens to query",
"index": "index of query result"
}
self.add_ai_function(SimpleAIFunction("query_knowledge",
"vector query content from local knowledge base",
self._query,
query_param))
async def _query(self, tokens: str, index: int=0):
object_ids = await KnowledgeBase().query_objects(tokens, 4)
if len(object_ids) <= index:
return "*** I have no more information for your reference.\n"
else:
content = "*** I have provided the following known information for your reference with json format:\n"
return content + KnowledgeBase().tokens_from_objects(object_ids[index:index + 1])
+38 -17
View File
@@ -32,7 +32,7 @@ import requests
import os import os
import toml import toml
from .storage import AIStorage, UserConfigItem from .storage import AIStorage, UserConfigItem
from .knowledge_base import KnowledgeBase, ImageObjectBuilder, ObjectID, ObjectType, DocumentObjectBuilder from .knowledge_base import KnowledgeBase, ImageObjectBuilder, ObjectID, ObjectType, DocumentObjectBuilder, EmailObjectBuilder, EmailObject
class KnowledgeJournal: class KnowledgeJournal:
def __init__(self, source_type: str, source_id: str, item_id: str, object_id: str, timestamp=None): def __init__(self, source_type: str, source_id: str, item_id: str, object_id: str, timestamp=None):
@@ -53,7 +53,10 @@ class KnowledgeJournal:
pass pass
return f"Add {object_type} from {os.path.join(self.source_id, self.item_id)}" return f"Add {object_type} from {os.path.join(self.source_id, self.item_id)}"
if self.source_type == "email": if self.source_type == "email":
pass object_id = ObjectID.from_base58(self.object_id)
email = EmailObject.decode(KnowledgeBase().store.get_object_store().get_object(object_id))
meta = email.get_meta()
return f'Add email from {os.path.join(self.source_id)} subject {meta["subject"]}'
# init sqlite3 client # init sqlite3 client
@@ -108,7 +111,7 @@ class KnowledgeEmailSource:
self.config["type"] = "email" self.config["type"] = "email"
def id(self): def id(self):
"::".join([self.config["imap_server"], self.config["address"]]) return self.config["address"]
@classmethod @classmethod
def user_config_items(cls): def user_config_items(cls):
@@ -121,13 +124,15 @@ class KnowledgeEmailSource:
@classmethod @classmethod
def local_root(cls): def local_root(cls):
user_data_dir = AIStorage.get_instance().get_myai_dir() user_data_dir = AIStorage.get_instance().get_myai_dir()
return os.path.abspath(f"{user_data_dir}/email") return os.path.abspath(f"{user_data_dir}/knowledge/email")
async def run_once(self): async def run_once(self):
# read config from toml file # read config from toml file
# and read from config config.local.toml if exists (config.local.toml is ignored by git) # and read from config config.local.toml if exists (config.local.toml is ignored by git)
logging.debug(f"knowledge email source {self.id()} run once")
filter = "ALL"
self.client = self.email_client() self.client = self.email_client()
await self.read_emails() await self.read_emails(imap_keyword=filter)
def email_client(self) -> imaplib.IMAP4_SSL: def email_client(self) -> imaplib.IMAP4_SSL:
logging.info(f"read email config from {self.config.get('imap_server')}") logging.info(f"read email config from {self.config.get('imap_server')}")
@@ -139,26 +144,37 @@ class KnowledgeEmailSource:
return client return client
async def read_emails(self, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"): async def read_emails(self, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"):
journal_client = KnowledgeJournalClient()
latest_journal = journal_client.latest_journal(self.id())
latest_uid = 0 if latest_journal is None else int(latest_journal.item_id)
self.client.select(folder) self.client.select(folder)
_, data = self.client.uid('search', None, imap_keyword) _, data = self.client.uid('search', None, imap_keyword)
# get email uid list # get email uid list
email_list = data[0].split() email_list = data[0].split()
logging.info(f"got {len(email_list)} emails") logging.info(f"got {len(email_list)} emails")
email_list.reverse() journal_client = KnowledgeJournalClient()
for uid in email_list: for uid in email_list:
if self.check_email_saved(uid): _uid = int.from_bytes(uid)
logging.info(f"email uid {uid} already saved") if _uid > latest_uid:
else: email_dir = self.check_email_saved(uid)
self.read_and_save_email(uid) if email_dir is not None:
logging.info(f"email uid {uid} saved") logging.info(f"email uid {uid} already saved")
else:
email_dir = self.read_and_save_email(uid)
logging.info(f"email uid {uid} saved")
email_object = EmailObjectBuilder({}, email_dir).build()
await KnowledgeBase().insert_object(email_object)
journal_client.insert(KnowledgeJournal("email", self.id(), str(int.from_bytes(uid)), str(email_object.calculate_id())))
def read_and_save_email(self, uid: str):
def read_and_save_email(self, uid: str) -> str:
message_parts = "(BODY.PEEK[])" message_parts = "(BODY.PEEK[])"
_, email_data = self.client.uid('fetch', uid, message_parts) _, email_data = self.client.uid('fetch', uid, message_parts)
mail = mailparser.parse_from_bytes(email_data[0][1]) mail = mailparser.parse_from_bytes(email_data[0][1])
logging.info(f"got email subject [{mail.subject}]") logging.info(f"got email subject [{mail.subject}]")
self.save_email(mail) self.save_email(mail)
return self.get_local_dir_name(mail)
def get_local_dir_name(self, mail: mailparser.MailParser) -> str: def get_local_dir_name(self, mail: mailparser.MailParser) -> str:
dir = f"{self.local_root()}/{self.config.get('address')}" dir = f"{self.local_root()}/{self.config.get('address')}"
@@ -166,7 +182,7 @@ class KnowledgeEmailSource:
name = hashlib.md5(name.encode('utf-8')).hexdigest() name = hashlib.md5(name.encode('utf-8')).hexdigest()
return f"{dir}/{name}" return f"{dir}/{name}"
def check_email_saved(self, uid: str): def check_email_saved(self, uid: str) -> str:
message_parts = "(BODY[HEADER])" message_parts = "(BODY[HEADER])"
_, email_data = self.client.uid('fetch', uid, message_parts) _, email_data = self.client.uid('fetch', uid, message_parts)
mail = mailparser.parse_from_bytes(email_data[0][1]) mail = mailparser.parse_from_bytes(email_data[0][1])
@@ -175,8 +191,8 @@ class KnowledgeEmailSource:
logging.info(f"check email saved {dir}") logging.info(f"check email saved {dir}")
file = f"{dir}/email.txt" file = f"{dir}/email.txt"
if os.path.exists(file): if os.path.exists(file):
return False return dir
return False return None
# save email attachment(images) # save email attachment(images)
def save_email_attachment(self, mail: mailparser.MailParser, email_dir: str): def save_email_attachment(self, mail: mailparser.MailParser, email_dir: str):
@@ -205,12 +221,16 @@ class KnowledgeEmailSource:
img_urls = [img['src'] for img in img_tags if 'src' in img.attrs] img_urls = [img['src'] for img in img_tags if 'src' in img.attrs]
logging.info(f'Found {len(img_urls)} images in email body') logging.info(f'Found {len(img_urls)} images in email body')
name_count = 0
if not os.path.exists(email_dir): if not os.path.exists(email_dir):
os.makedirs(email_dir) os.makedirs(email_dir)
for img_url in img_urls: for img_url in img_urls:
# keep the original image filename(last of url) # keep the original image filename(last of url)
img_filename = os.path.join(email_dir, img_url.split('/')[-1]) ext = img_url.split('/')[-1].split('.')[-1]
img_filename = os.path.join(email_dir, f"{name_count}.{ext}")
name_count += 1
# download image # download image
response = requests.get(img_url, stream=True) response = requests.get(img_url, stream=True)
if response.status_code == 200: if response.status_code == 200:
@@ -230,7 +250,8 @@ class KnowledgeEmailSource:
logging.info(f"save email to {email_dir}") logging.info(f"save email to {email_dir}")
if not os.path.exists(email_dir): if not os.path.exists(email_dir):
os.makedirs(email_dir) os.makedirs(email_dir)
with open(f"{email_dir}/email.txt", "w") as f: with open(f"{email_dir}/email.txt", "w", encoding='utf-8') as f:
# soup = BeautifulSoup(mail.body, 'html.parser')
f.write(mail.body) f.write(mail.body)
with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f: with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f:
mail_dict = json.loads(mail.mail_json) mail_dict = json.loads(mail.mail_json)
+246
View File
@@ -0,0 +1,246 @@
import logging
import requests
from typing import Optional, List
from pydantic import BaseModel
from typing import Union
from PIL import Image
import io
from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskType
from .queue_compute_node import Queue_ComputeNode
from knowledge import ObjectID
logger = logging.getLogger(__name__)
class LocalSentenceTransformer_Text_ComputeNode(Queue_ComputeNode):
# For valid pretrained models, see https://www.sbert.net/docs/pretrained_models.html
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
super().__init__()
self.node_id = "local_sentence_transformer_text_embedding_node"
self.model_name = model_name
self.model = None
def initial(self) -> bool:
logger.info(
f"LocalSentenceTransformer_Text_ComputeNode init, model_name: {self.model_name}"
)
assert self.model_name is not None
assert self.model is None
try:
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer(self.model_name)
except Exception as err:
logger.error(f"load model {self.model} failed: {err}")
return False
self.start()
return True
async def execute_task(
self, task: ComputeTask
) -> {
"task_type": str,
"content": str,
"message": str,
"state": ComputeTaskState,
"error": {
"code": int,
"message": str,
},
}:
try:
# logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task: {task}")
if task.task_type == ComputeTaskType.TEXT_EMBEDDING:
input = task.params["input"]
logger.debug(
f"LocalSentenceTransformer_Text_ComputeNode task input: {input}"
)
sentence_embeddings = self.model.encode(input, show_progress_bar=False).tolist()
# logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task sentence_embeddings: {sentence_embeddings}")
return {
"state": ComputeTaskState.DONE,
"content": sentence_embeddings,
"message": None,
}
else:
return {
"state": ComputeTaskState.ERROR,
"error": {"code": -1, "message": "unsupport embedding task type"},
}
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"LocalSentenceTransformer_Text_ComputeNode: {self.node_id}, {self.model_name}"
def get_capacity(self):
pass
def is_support(self, task: ComputeTask) -> bool:
return task.task_type == ComputeTaskType.TEXT_EMBEDDING and task.params["model_name"] == "all-MiniLM-L6-v2"
def is_local(self) -> bool:
return True
class LocalSentenceTransformer_Image_ComputeNode(Queue_ComputeNode):
# For valid pretrained models, see https://www.sbert.net/docs/pretrained_models.html
def __init__(
self,
model_name: str = "clip-ViT-B-32",
multi_model_name: str = "clip-ViT-B-32-multilingual-v1",
):
super().__init__()
self.node_id = "local_sentence_transformer_image_embedding_node"
self.model_name = model_name
self.multi_model_name = multi_model_name
self.model = None
self.multi_model = None
def initial(self) -> bool:
logger.info(
f"LocalSentenceTransformer_Image_ComputeNode init, model_name: {self.model_name} {self.multi_model_name}"
)
assert self.model_name is not None
assert self.multi_model_name is not None
assert self.model is None
assert self.multi_model is None
try:
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer(self.model_name)
self.multi_model = SentenceTransformer(self.multi_model_name)
except Exception as err:
logger.error(f"load model {self.model} failed: {err}")
return False
self.start()
return True
def _load_image(self, source: Union[ObjectID, bytes]) -> Optional[Image]:
image_data = None
if isinstance(source, ObjectID):
from knowledge import KnowledgeStore, ImageObject
buf = KnowledgeStore().get_object_store().get_object(source)
if buf is None:
logger.error(f"load image object but not found! {source}")
return None
try:
image_obj = ImageObject.decode(buf)
except Exception as err:
logger.error(f"decode ImageObject from buffer failed: {source}, {err}")
return None
file_size = image_obj.get_file_size()
print(f"got image object: {source.to_base58()}, size: {file_size}")
image_data = (
KnowledgeStore()
.get_chunk_reader()
.read_chunk_list_to_single_bytes(image_obj.get_chunk_list())
)
elif isinstance(source, bytes):
image_data = source
else:
logger.error(f"unsupport image source type: {type(source)}, {source}")
return None
try:
img = Image.open(io.BytesIO(image_data))
return img
except Exception as err:
logger.error(f"load image from buffer failed: {source}, {err}")
return None
async def execute_task(
self, task: ComputeTask
) -> {
"task_type": str,
"content": str,
"message": str,
"state": ComputeTaskState,
"error": {
"code": int,
"message": str,
},
}:
try:
# logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task: {task}")
if task.task_type == ComputeTaskType.TEXT_EMBEDDING:
input = task.params["input"]
logger.debug(
f"LocalSentenceTransformer_Text_ComputeNode task text input: {input}"
)
sentence_embeddings = self.multi_model.encode(input, show_progress_bar=False).tolist()
# logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task sentence_embeddings: {sentence_embeddings}")
return {
"state": ComputeTaskState.DONE,
"content": sentence_embeddings,
"message": None,
}
elif task.task_type == ComputeTaskType.IMAGE_EMBEDDING:
input = task.params["input"]
logger.debug(
f"LocalSentenceTransformer_Image_ComputeNode task image input: {input}"
)
img = self._load_image(input)
if img is None:
return {
"state": ComputeTaskState.ERROR,
"error": {"code": -1, "message": "load image failed"},
}
sentence_embeddings = self.model.encode(img)
# logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task sentence_embeddings: {sentence_embeddings}")
return {
"state": ComputeTaskState.DONE,
"content": sentence_embeddings,
"message": None,
}
else:
return {
"state": ComputeTaskState.ERROR,
"error": {"code": -1, "message": "unsupport embedding task type"},
}
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"LocalSentenceTransformer_Image_ComputeNode: {self.node_id}, {self.model_name}"
def get_capacity(self):
pass
def is_support(self, task: ComputeTask) -> bool:
return (
(task.task_type == ComputeTaskType.TEXT_EMBEDDING and task.params["model_name"] == "clip-ViT-B-32")
or task.task_type == ComputeTaskType.IMAGE_EMBEDDING
)
def is_local(self) -> bool:
return True
+1 -1
View File
@@ -98,7 +98,7 @@ class OpenAI_ComputeNode(ComputeNode):
task.state = ComputeTaskState.DONE task.state = ComputeTaskState.DONE
result.result_code = ComputeTaskResultCode.OK result.result_code = ComputeTaskResultCode.OK
result.worker_id = self.node_id result.worker_id = self.node_id
result.result = resp["data"][0]["embedding"] result.result_str = resp["data"][0]["embedding"]
return result return result
case ComputeTaskType.LLM_COMPLETION: case ComputeTaskType.LLM_COMPLETION:
+20 -15
View File
@@ -4,7 +4,7 @@ from asyncio import Queue
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskResultCode, ComputeTaskState, ComputeTaskType
from .compute_node import ComputeNode from .compute_node import ComputeNode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -13,6 +13,7 @@ class Queue_ComputeNode(ComputeNode):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.task_queue = Queue() self.task_queue = Queue()
self.is_start = False
@abstractmethod @abstractmethod
async def execute_task(self, task: ComputeTask) -> { async def execute_task(self, task: ComputeTask) -> {
@@ -39,28 +40,32 @@ class Queue_ComputeNode(ComputeNode):
resp = await self.execute_task(task) resp = await self.execute_task(task)
result = ComputeTaskResult() 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.worker_id = self.node_id
result.result_str = resp["content"] task.state = resp["state"]
result.result_message = resp["message"]
if task.state == ComputeTaskState.ERROR:
result.result_code = ComputeTaskResultCode.ERROR
task.error_str = resp["error"]["message"]
else:
result.result_code = ComputeTaskResultCode.OK
result.result_str = resp["content"]
result.result_message = resp["message"]
result.set_from_task(task)
return result return result
async def start(self): def start(self):
if self.is_start is True:
return
self.is_start = True
async def _run_task_loop(): async def _run_task_loop():
while True: while True:
task = await self.task_queue.get() task = await self.task_queue.get()
logger.info(f"{self.display()} get task: {task.display()}") logger.info(f"openai_node get task: {task.display()}")
result = await self._run_task(task) await self._run_task(task)
if result is not None:
task.result = result
asyncio.create_task(_run_task_loop()) asyncio.create_task(_run_task_loop())
+1 -5
View File
@@ -50,11 +50,7 @@ class DocumentObjectBuilder:
return self return self
def build(self) -> DocumentObject: def build(self) -> DocumentObject:
chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text( chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text(self.text)
self.text,
1024 * 4,
".?!\n"
)
doc = DocumentObject(self.meta, self.tags, chunk_list) doc = DocumentObject(self.meta, self.tags, chunk_list)
doc_id = doc.calculate_id() doc_id = doc.calculate_id()
+3 -3
View File
@@ -21,13 +21,13 @@ class EmailObject(KnowledgeObject):
super().__init__(ObjectType.Email, desc, body) super().__init__(ObjectType.Email, desc, body)
def get_meta(self): def get_meta(self) -> dict:
return self.desc["meta"] return self.desc["meta"]
def get_tags(self): def get_tags(self) -> dict:
return self.desc["tags"] return self.desc["tags"]
def get_rich_text(self): def get_rich_text(self) -> RichTextObject:
return self.body["content"] return self.body["content"]
+14 -7
View File
@@ -2,6 +2,7 @@ from ..object import KnowledgeObject
from ..data import ChunkList, ChunkListWriter from ..data import ChunkList, ChunkListWriter
from ..object import ObjectType from ..object import ObjectType
from .. import KnowledgeStore from .. import KnowledgeStore
import os
# desc # desc
# meta # meta
@@ -13,30 +14,34 @@ from .. import KnowledgeStore
class ImageObject(KnowledgeObject): class ImageObject(KnowledgeObject):
def __init__(self, meta: dict, tags: dict, exif: dict, chunk_list: ChunkList): def __init__(self, meta: dict, tags: dict, exif: dict, file_size: int, chunk_list: ChunkList):
desc = dict() desc = dict()
body = dict() body = dict()
desc["meta"] = meta desc["meta"] = meta
desc["exif"] = exif desc["exif"] = exif
desc["tags"] = tags desc["tags"] = tags
desc["hash"] = chunk_list.hash.to_base58() desc["hash"] = chunk_list.hash.to_base58()
desc["file_size"] = file_size
body["chunk_list"] = chunk_list.chunk_list body["chunk_list"] = chunk_list.chunk_list
super().__init__(ObjectType.Image, desc, body) super().__init__(ObjectType.Image, desc, body)
def get_meta(self): def get_meta(self) -> dict:
return self.desc["meta"] return self.desc["meta"]
def get_exif(self): def get_exif(self) -> dict:
return self.desc["exif"] return self.desc["exif"]
def get_tags(self): def get_tags(self) -> dict:
return self.desc["tags"] return self.desc["tags"]
def get_hash(self): def get_hash(self) -> str:
return self.desc["hash"] return self.desc["hash"]
def get_chunk_list(self): def get_file_size(self) -> int:
return self.desc["file_size"]
def get_chunk_list(self) -> ChunkList:
return self.body["chunk_list"] return self.body["chunk_list"]
@@ -82,8 +87,10 @@ class ImageObjectBuilder:
return self return self
def build(self) -> ImageObject: def build(self) -> ImageObject:
file_size = os.path.getsize(self.image_file)
chunk_list = KnowledgeStore().get_chunk_list_writer().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 self.image_file, 1024 * 1024 * 4, self.restore_file
) )
exif = get_exif_data(self.image_file) exif = get_exif_data(self.image_file)
return ImageObject(self.meta, self.tags, exif, chunk_list) return ImageObject(self.meta, self.tags, exif, file_size, chunk_list)
+14 -4
View File
@@ -12,7 +12,7 @@ class Chunk:
self.range_start = range_start self.range_start = range_start
self.size = size self.size = size
def read(self): def read(self) -> bytes:
with open(self.file_path, 'rb') as f: with open(self.file_path, 'rb') as f:
f.seek(self.range_start) f.seek(self.range_start)
return f.read(self.size) return f.read(self.size)
@@ -26,6 +26,8 @@ class ChunkReader:
def get_chunk(self, chunk_id: ChunkID) -> Chunk: def get_chunk(self, chunk_id: ChunkID) -> Chunk:
positions = self.chunk_tracker.get_position(chunk_id) positions = self.chunk_tracker.get_position(chunk_id)
logging.info(f"chunk positions: {chunk_id}, {positions}")
if positions is None: if positions is None:
logging.warning(f"chunk not found: {chunk_id}") logging.warning(f"chunk not found: {chunk_id}")
return None return None
@@ -54,15 +56,23 @@ class ChunkReader:
def get_chunk_list(self, chunk_list: List[ChunkID]) -> List[Chunk]: def get_chunk_list(self, chunk_list: List[ChunkID]) -> List[Chunk]:
return [self.get_chunk(chunk_id) for chunk_id in chunk_list] return [self.get_chunk(chunk_id) for chunk_id in chunk_list]
def read_chunk_list(self, chunk_ids: List[ChunkID]): def read_chunk_list(self, chunk_ids: List[ChunkID]) -> bytes:
for chunk_id in chunk_ids: for chunk_id in chunk_ids:
chunk = self.get_chunk(chunk_id) chunk = self.get_chunk(chunk_id)
if chunk is None: if chunk is None:
raise ValueError(f"chunk not found: {chunk_id}") raise ValueError(f"chunk not found: {chunk_id}")
yield from chunk.read() yield chunk.read()
def read_text_chunk_list(self, chunk_ids: List[ChunkID]): def read_chunk_list_to_single_bytes(self, chunk_ids: List[ChunkID]) -> bytes:
chunks = []
for chunk in self.read_chunk_list(chunk_ids):
chunks.append(chunk)
image_data = b''.join(chunks)
return image_data
def read_text_chunk_list(self, chunk_ids: List[ChunkID]) -> str:
for chunk_id in chunk_ids: for chunk_id in chunk_ids:
chunk = self.get_chunk(chunk_id) chunk = self.get_chunk(chunk_id)
if chunk is None: if chunk is None:
+149 -28
View File
@@ -1,13 +1,139 @@
import os import os
import hashlib import hashlib
import re import re
from typing import Tuple, List import tiktoken
import logging
from typing import Callable, Iterable, Optional, Tuple, List
from .chunk_store import ChunkStore from .chunk_store import ChunkStore
from .chunk import ChunkID, PositionFileRange, PositionType from .chunk import ChunkID, PositionFileRange, PositionType
from ..object import HashValue from ..object import HashValue
from .tracker import ChunkTracker from .tracker import ChunkTracker
from .chunk_list import ChunkList from .chunk_list import ChunkList
def _join_docs(docs: List[str], separator: str) -> Optional[str]:
text = separator.join(docs)
text = text.strip()
if text == "":
return None
else:
return text
def _merge_splits(
splits: Iterable[str],
separator: str,
chunk_size: int,
chunk_overlap: int,
length_function: Callable[[str], int]
) -> List[str]:
# We now want to combine these smaller pieces into medium size
# chunks to send to the LLM.
separator_len = length_function(separator)
docs = []
current_doc: List[str] = []
total = 0
for d in splits:
_len = length_function(d)
if (
total + _len + (separator_len if len(current_doc) > 0 else 0)
> chunk_size
):
if total > chunk_size:
logging.warning(
f"Created a chunk of size {total}, "
f"which is longer than the specified {self._chunk_size}"
)
if len(current_doc) > 0:
doc = _join_docs(current_doc, separator)
if doc is not None:
docs.append(doc)
# Keep on popping if:
# - we have a larger chunk than in the chunk overlap
# - or if we still have any chunks and the length is long
while total > chunk_overlap or (
total + _len + (separator_len if len(current_doc) > 0 else 0)
> chunk_size
and total > 0
):
total -= length_function(current_doc[0]) + (
separator_len if len(current_doc) > 1 else 0
)
current_doc = current_doc[1:]
current_doc.append(d)
total += _len + (separator_len if len(current_doc) > 1 else 0)
doc = _join_docs(current_doc, separator)
if doc is not None:
docs.append(doc)
return docs
def _split_text_with_regex(
text: str, separator: str, keep_separator: bool
) -> List[str]:
# Now that we have the separator, split the text
if separator:
if keep_separator:
# The parentheses in the pattern keep the delimiters in the result.
_splits = re.split(f"({separator})", text)
splits = [_splits[i] + _splits[i + 1] for i in range(1, len(_splits), 2)]
if len(_splits) % 2 == 0:
splits += _splits[-1:]
splits = [_splits[0]] + splits
else:
splits = re.split(separator, text)
else:
splits = list(text)
return [s for s in splits if s != ""]
def _split_text(
text: str,
separators: List[str],
chunk_size: int,
chunk_overlap: int,
length_function: Callable[[str], int]
) -> List[str]:
"""Split incoming text and return chunks."""
final_chunks = []
# Get appropriate separator to use
separator = separators[-1]
new_separators = []
for i, _s in enumerate(separators):
_separator = re.escape(_s)
if _s == "":
separator = _s
break
if re.search(_separator, text):
separator = _s
new_separators = separators[i + 1 :]
break
keep_separator = True
_separator = re.escape(separator)
splits = _split_text_with_regex(text, _separator, keep_separator)
# Now go merging things, recursively splitting longer texts.
_good_splits = []
_separator = "" if keep_separator else separator
for s in splits:
if length_function(s) < chunk_size:
_good_splits.append(s)
else:
if _good_splits:
merged_text = _merge_splits(_good_splits, _separator, chunk_size, chunk_overlap, length_function)
final_chunks.extend(merged_text)
_good_splits = []
if not new_separators:
final_chunks.append(s)
else:
other_info = _split_text(s, new_separators, chunk_size, chunk_overlap, length_function)
final_chunks.extend(other_info)
if _good_splits:
merged_text = _merge_splits(_good_splits, _separator, chunk_size, chunk_overlap, length_function)
final_chunks.extend(merged_text)
return final_chunks
class ChunkListWriter: class ChunkListWriter:
def __init__(self, chunk_store: ChunkStore, chunk_tracker: ChunkTracker): def __init__(self, chunk_store: ChunkStore, chunk_tracker: ChunkTracker):
self.chunk_store = chunk_store self.chunk_store = chunk_store
@@ -27,6 +153,8 @@ class ChunkListWriter:
chunk = file.read(chunk_size) chunk = file.read(chunk_size)
if not chunk: if not chunk:
break break
chunk_len = len(chunk)
chunk_id = ChunkID.hash_data(chunk) chunk_id = ChunkID.hash_data(chunk)
chunk_list.append(chunk_id) chunk_list.append(chunk_id)
@@ -38,8 +166,9 @@ class ChunkListWriter:
) )
self.chunk_store.put_chunk(chunk_id, chunk) self.chunk_store.put_chunk(chunk_id, chunk)
else: else:
pos = file.tell()
file_range = PositionFileRange( file_range = PositionFileRange(
file_path, file.tell() - chunk_size, chunk_size file_path, pos - chunk_len, pos
) )
self.chunk_tracker.add_position( self.chunk_tracker.add_position(
chunk_id, str(file_range), PositionType.FileRange chunk_id, str(file_range), PositionType.FileRange
@@ -51,9 +180,24 @@ class ChunkListWriter:
return ChunkList(chunk_list, file_hash) return ChunkList(chunk_list, file_hash)
def create_chunk_list_from_text( def create_chunk_list_from_text(
self, text: str, chunk_max_words: int, separator_chars: str = ".," self,
text: str,
chunk_size: int = 4000,
chunk_overlap: int = 200,
separators: str = ["\n\n", "\n", " ", ""]
) -> ChunkList: ) -> ChunkList:
text_list = self._split_text_list(text, chunk_max_words, separator_chars) enc = tiktoken.encoding_for_model("gpt-3.5-turbo")
def length_function(text: str) -> int:
return len(
enc.encode(
text,
allowed_special=set(),
disallowed_special="all",
)
)
text_list = _split_text(text, separators, chunk_size, chunk_overlap, length_function)
chunk_list = [] chunk_list = []
hash_obj = hashlib.sha256() hash_obj = hashlib.sha256()
@@ -67,27 +211,4 @@ class ChunkListWriter:
self.chunk_store.put_chunk(chunk_id, chunk_bytes) self.chunk_store.put_chunk(chunk_id, chunk_bytes)
hash = HashValue(hash_obj.digest()) hash = HashValue(hash_obj.digest())
return ChunkList(chunk_list, hash) return ChunkList(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
return sentences
-65
View File
@@ -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]
+10 -1
View File
@@ -1,6 +1,8 @@
import os import os
import shutil import shutil
from .object import ObjectID from .object import ObjectID
import logging
logger = logging.getLogger(__name__)
class FileBlobStorage: class FileBlobStorage:
@@ -38,16 +40,23 @@ class FileBlobStorage:
def put(self, object_id: ObjectID, contents: bytes): def put(self, object_id: ObjectID, contents: bytes):
full_path = self.get_full_path(object_id) full_path = self.get_full_path(object_id)
if os.path.exists(full_path):
logger.warning(f"will replace object: {object_id}")
self.write_sync(full_path, contents) self.write_sync(full_path, contents)
def get(self, object_id: ObjectID) -> bytes: def get(self, object_id: ObjectID) -> bytes:
full_path = self.get_full_path(object_id) full_path = self.get_full_path(object_id)
if not os.path.exists(full_path):
return None
with open(full_path, "rb") as f: with open(full_path, "rb") as f:
return f.read() return f.read()
def delete(self, object_id: ObjectID): def delete(self, object_id: ObjectID):
full_path = self.get_full_path(object_id) full_path = self.get_full_path(object_id)
os.remove(full_path) if os.path.exists(full_path):
os.remove(full_path)
def exists(self, object_id: ObjectID) -> bool: def exists(self, object_id: ObjectID) -> bool:
full_path = self.get_full_path(object_id) full_path = self.get_full_path(object_id)
+3 -1
View File
@@ -1,6 +1,8 @@
# define a object type enum # define a object type enum
from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from .object_id import ObjectID, ObjectType from .object_id import ObjectID, ObjectType
import hashlib import hashlib
import json import json
@@ -61,5 +63,5 @@ class KnowledgeObject(ABC):
return pickle.dumps(self) return pickle.dumps(self)
@staticmethod @staticmethod
def decode(data: bytes): def decode(data: bytes) -> "ImageObject":
return pickle.loads(data) return pickle.loads(data)
-33
View File
@@ -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))
+4 -2
View File
@@ -4,7 +4,7 @@ from .object import ObjectStore, ObjectRelationStore
from .data import ChunkStore, ChunkTracker, ChunkListWriter, ChunkReader from .data import ChunkStore, ChunkTracker, ChunkListWriter, ChunkReader
from .vector import ChromaVectorStore, VectorBase from .vector import ChromaVectorStore, VectorBase
import logging 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 # 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: class KnowledgeStore:
@@ -13,6 +13,8 @@ class KnowledgeStore:
def __new__(cls): def __new__(cls):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
import aios_kernel
knowledge_dir = aios_kernel.storage.AIStorage().get_myai_dir() / "knowledge" knowledge_dir = aios_kernel.storage.AIStorage().get_myai_dir() / "knowledge"
if not os.path.exists(knowledge_dir): if not os.path.exists(knowledge_dir):
@@ -60,5 +62,5 @@ class KnowledgeStore:
def get_vector_store(self, model_name: str) -> VectorBase: def get_vector_store(self, model_name: str) -> VectorBase:
if model_name not in self.vector_store: 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] return self.vector_store[model_name]
+52 -1
View File
@@ -80,10 +80,61 @@ toml>=0.10.0
protobuf protobuf
grpcio grpcio
grpcio-status grpcio-status
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
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
pysocks pysocks
chardet chardet
pydub pydub
aiosqlite aiosqlite
python-telegram-bot python-telegram-bot
pydub pydub
stability_sdk stability_sdk
sentence-transformers==2.2.2
tiktoken
+23 -20
View File
@@ -24,8 +24,7 @@ directory = os.path.dirname(__file__)
sys.path.append(directory + '/../../') sys.path.append(directory + '/../../')
from aios_kernel import AIOS_Version,AgentMsgType,UserConfigItem,AIStorage,Workflow,AIAgent,AgentMsg,AgentMsgStatus,ComputeKernel,OpenAI_ComputeNode,AIBus,AIChatSession,AgentTunnel,TelegramTunnel,CalenderEnvironment,Environment,EmailTunnel,LocalLlama_ComputeNode,Local_Stability_ComputeNode,Stability_ComputeNode,PaintEnvironment
from aios_kernel import ContactManager,Contact
import proxy import proxy
from aios_kernel import * from aios_kernel import *
@@ -114,6 +113,10 @@ class AIOS_Shell:
cm.add_family_member(self.username,owenr) cm.add_family_member(self.username,owenr)
knowledge_env = KnowledgeEnvironment("knowledge")
Environment.set_env_by_id("knowledge",knowledge_env)
cal_env = CalenderEnvironment("calender") cal_env = CalenderEnvironment("calender")
await cal_env.start() await cal_env.start()
Environment.set_env_by_id("calender",cal_env) Environment.set_env_by_id("calender",cal_env)
@@ -137,6 +140,7 @@ class AIOS_Shell:
return False return False
ComputeKernel.get_instance().add_compute_node(open_ai_node) ComputeKernel.get_instance().add_compute_node(open_ai_node)
if await AIStorage.get_instance().is_feature_enable("llama"): if await AIStorage.get_instance().is_feature_enable("llama"):
llama_ai_node = LocalLlama_ComputeNode() llama_ai_node = LocalLlama_ComputeNode()
if await llama_ai_node.initial() is True: if await llama_ai_node.initial() is True:
@@ -146,6 +150,7 @@ class AIOS_Shell:
logger.error("llama node initial failed!") logger.error("llama node initial failed!")
await AIStorage.get_instance().set_feature_init_result("llama",False) await AIStorage.get_instance().set_feature_init_result("llama",False)
if await AIStorage.get_instance().is_feature_enable("aigc"): if await AIStorage.get_instance().is_feature_enable("aigc"):
try: try:
google_text_to_speech_node = GoogleTextToSpeechNode.get_instance() google_text_to_speech_node = GoogleTextToSpeechNode.get_instance()
@@ -160,13 +165,20 @@ class AIOS_Shell:
# logger.error("stability api node initial failed!") # logger.error("stability api node initial failed!")
# ComputeKernel.get_instance().add_compute_node(stability_api_node) # ComputeKernel.get_instance().add_compute_node(stability_api_node)
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) local_st_text_compute_node = LocalSentenceTransformer_Text_ComputeNode()
else: if local_st_text_compute_node.initial() is not True:
logger.error("local stability node initial failed!") logger.error("local sentence transformer text embedding node initial failed!")
await AIStorage.get_instance.set_feature_init_result("aigc",False) else:
ComputeKernel.get_instance().add_compute_node(local_st_text_compute_node)
local_st_image_compute_node = LocalSentenceTransformer_Image_ComputeNode()
if local_st_image_compute_node.initial() is not True:
logger.error("local sentence transformer image embedding node initial failed!")
else:
ComputeKernel.get_instance().add_compute_node(local_st_image_compute_node)
await ComputeKernel.get_instance().start() await ComputeKernel.get_instance().start()
@@ -308,8 +320,7 @@ class AIOS_Shell:
async def handle_knowledge_commands(self, args): async def handle_knowledge_commands(self, args):
show_text = FormattedText([("class:title", "sub command not support!\n" show_text = FormattedText([("class:title", "sub command not support!\n"
"/knowledge add email | dir\n" "/knowledge add email | dir\n"
"/knowledge journal [$topn]\n" "/knowledge journal [$topn]\n")])
"/knowledge query $query\n")])
if len(args) < 1: if len(args) < 1:
return show_text return show_text
sub_cmd = args[0] sub_cmd = args[0]
@@ -346,13 +357,6 @@ class AIOS_Shell:
topn = 10 if len(args) == 1 else int(args[1]) topn = 10 if len(args) == 1 else int(args[1])
journals = [str(journal) for journal in KnowledgePipline.get_instance().get_latest_journals(topn)] journals = [str(journal) for journal in KnowledgePipline.get_instance().get_latest_journals(topn)]
print_formatted_text("\r\n".join(journals)) 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): async def call_func(self,func_name, args):
match func_name: match func_name:
@@ -662,8 +666,7 @@ async def main():
'/connect $target', '/connect $target',
'/contact $name', '/contact $name',
'/knowledge add email | dir', '/knowledge add email | dir',
'/knowledge journal [$topn]', '/knowledge journal [$topn]',
'/knowledge query $query'
'/set_config $key', '/set_config $key',
'/enable $feature', '/enable $feature',
'/disable $feature', '/disable $feature',
+1 -1
View File
@@ -59,7 +59,7 @@ class TestChunk(unittest.TestCase):
with open(text_file, "r", encoding="utf-8") as file: with open(text_file, "r", encoding="utf-8") as file:
text = file.read() text = file.read()
gen.create_chunk_list_from_text(text, 1024) gen.create_chunk_list_from_text(text)
if __name__ == "__main__": if __name__ == "__main__":
+98
View File
@@ -0,0 +1,98 @@
import sys
import os
import logging
from sentence_transformers import SentenceTransformer, util
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)
def test_st():
image_model = SentenceTransformer('clip-ViT-B-32-multilingual-v1')
model = SentenceTransformer("all-MiniLM-L6-v2")
# Our sentences we like to encode
sentences = [
"This framework generates embeddings for each input sentence",
"Sentences are passed as a list of string.",
"The quick brown fox jumps over the lazy dog.",
]
# Sentences are encoded by calling model.encode()
sentence_embeddings = model.encode(sentences)
# Print the embeddings
for sentence, embedding in zip(sentences, sentence_embeddings):
print("Sentence:", sentence)
print("Embedding:", embedding)
print("")
# Single list of sentences
"""
sentences = [
"The cat sits outside",
"A man is playing guitar",
"I love pasta",
"The new movie is awesome",
"The cat plays in the garden",
"A woman watches TV",
"The new movie is so great",
"Do you like pizza?",
]
"""
sentences = [
"猫坐在外面",
"狗坐在上面",
"狗坐在里面",
"一个男人在弹吉他",
"我爱意大利面",
"新电影太精彩了",
"猫在花园里玩耍",
"一个女人在看电视",
"新电影太棒了",
"你喜欢披萨吗?",
]
# Compute embeddings
#embeddings = model.encode(sentences, convert_to_tensor=True)
embeddings = model.encode(sentences)
print("embeddings as follows: ")
print(embeddings)
# Compute cosine-similarities for each sentence with each other sentence
cosine_scores = util.cos_sim(embeddings, embeddings)
# Find the pairs with the highest cosine similarity scores
pairs = []
for i in range(len(cosine_scores) - 1):
for j in range(i + 1, len(cosine_scores)):
pairs.append({"index": [i, j], "score": cosine_scores[i][j]})
# Sort scores in decreasing order
pairs = sorted(pairs, key=lambda x: x["score"], reverse=True)
for pair in pairs[0:10]:
i, j = pair["index"]
print(
"{} \t\t {} \t\t Score: {:.4f}".format(
sentences[i], sentences[j], pair["score"]
)
)
if __name__ == "__main__":
test_st()
+39 -2
View File
@@ -24,13 +24,15 @@ from knowledge import (
ObjectRelationStore, ObjectRelationStore,
KnowledgeStore, KnowledgeStore,
EmailObject, EmailObject,
ImageObject,
) )
from aios_kernel import LocalSentenceTransformer_Image_ComputeNode, ComputeTask
import asyncio import asyncio
import unittest import unittest
class TestVectorSTorage(unittest.TestCase): class TestVectorSTorage(unittest.IsolatedAsyncioTestCase):
def test_object(self): async 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_base58())
print(data.to_base36()) print(data.to_base36())
@@ -57,6 +59,41 @@ class TestVectorSTorage(unittest.TestCase):
ret2 = obj.encode() ret2 = obj.encode()
self.assertEqual(ret, ret2) self.assertEqual(ret, ret2)
images = email_object.get_rich_text().get_images()
image_keys = list(images.keys())
print("got image list: ", image_keys)
image_id = images[image_keys[1]]
print(f"got image object: {image_keys[1]} {image_id.to_base58()}")
node = LocalSentenceTransformer_Image_ComputeNode();
ret = node.initial()
self.assertEqual(ret, True)
task = ComputeTask()
task.set_image_embedding_params(image_id)
ret = await node.execute_task(task)
print(ret)
'''
buf = KnowledgeStore().get_object_store().get_object(image_id)
image_obj= ImageObject.decode(buf)
file_size = image_obj.get_file_size()
print(f"got image object: {image_id.to_base58()}, size: {file_size}")
image_data = KnowledgeStore().get_chunk_reader().read_chunk_list_to_single_bytes(image_obj.get_chunk_list())
self.assertEqual(file_size, len(image_data))
from PIL import Image
import io
image = Image.open(io.BytesIO(image_data))
image.show()
from sentence_transformers import SentenceTransformer
#model = SentenceTransformer('clip-ViT-B-32-multilingual-v1')
model = SentenceTransformer('clip-ViT-B-32')
model.encode(image, convert_to_tensor=True)
'''
def test_relation(self): def test_relation(self):
obj1 = ObjectID.hash_data("12345".encode("utf-8")) obj1 = ObjectID.hash_data("12345".encode("utf-8"))