mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-26 06:16:09 +00:00
chore: stash
This commit is contained in:
@@ -14,7 +14,7 @@ preregistered_groups: list[type[RouterGroup]] = []
|
|||||||
"""RouterGroup 的预注册列表"""
|
"""RouterGroup 的预注册列表"""
|
||||||
|
|
||||||
|
|
||||||
def group_class(name: str, path: str) -> None:
|
def group_class(name: str, path: str) -> typing.Callable[[typing.Type[RouterGroup]], typing.Type[RouterGroup]]:
|
||||||
"""注册一个 RouterGroup"""
|
"""注册一个 RouterGroup"""
|
||||||
|
|
||||||
def decorator(cls: typing.Type[RouterGroup]) -> typing.Type[RouterGroup]:
|
def decorator(cls: typing.Type[RouterGroup]) -> typing.Type[RouterGroup]:
|
||||||
@@ -120,6 +120,6 @@ class RouterGroup(abc.ABC):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def http_status(self, status: int, code: int, msg: str) -> quart.Response:
|
def http_status(self, status: int, code: int, msg: str) -> typing.Tuple[quart.Response, int]:
|
||||||
"""返回一个指定状态码的响应"""
|
"""返回一个指定状态码的响应"""
|
||||||
return self.fail(code, msg), status
|
return (self.fail(code, msg), status)
|
||||||
|
|||||||
@@ -5,77 +5,49 @@ from ... import group
|
|||||||
@group.group_class('knowledge_base', '/api/v1/knowledge/bases')
|
@group.group_class('knowledge_base', '/api/v1/knowledge/bases')
|
||||||
class KnowledgeBaseRouterGroup(group.RouterGroup):
|
class KnowledgeBaseRouterGroup(group.RouterGroup):
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
@self.route('', methods=['POST', 'GET'], endpoint='handle_knowledge_bases')
|
@self.route('', methods=['POST', 'GET'])
|
||||||
async def handle_knowledge_bases() -> str:
|
async def handle_knowledge_bases() -> quart.Response:
|
||||||
if quart.request.method == 'GET':
|
if quart.request.method == 'GET':
|
||||||
knowledge_bases = await self.ap.knowledge_base_service.get_all_knowledge_bases()
|
knowledge_bases = await self.ap.knowledge_service.get_knowledge_bases()
|
||||||
bases_list = [
|
return self.success(data={'bases': knowledge_bases})
|
||||||
{
|
|
||||||
'uuid': kb.id,
|
|
||||||
'name': kb.name,
|
|
||||||
'description': kb.description,
|
|
||||||
'embedding_model_uuid': kb.embedding_model_uuid,
|
|
||||||
'top_k': kb.top_k,
|
|
||||||
}
|
|
||||||
for kb in knowledge_bases
|
|
||||||
]
|
|
||||||
return self.success(data={'bases': bases_list})
|
|
||||||
|
|
||||||
elif quart.request.method == 'POST':
|
elif quart.request.method == 'POST':
|
||||||
json_data = await quart.request.json
|
json_data = await quart.request.json
|
||||||
knowledge_base_uuid = await self.ap.knowledge_base_service.create_knowledge_base(
|
knowledge_base_uuid = await self.ap.knowledge_service.create_knowledge_base(json_data)
|
||||||
json_data.get('name'),
|
|
||||||
json_data.get('description'),
|
|
||||||
json_data.get('embedding_model_uuid'),
|
|
||||||
json_data.get('top_k',5),
|
|
||||||
)
|
|
||||||
return self.success(data={'uuid': knowledge_base_uuid})
|
return self.success(data={'uuid': knowledge_base_uuid})
|
||||||
|
|
||||||
|
return self.http_status(405, -1, 'Method not allowed')
|
||||||
|
|
||||||
@self.route(
|
@self.route(
|
||||||
'/<knowledge_base_uuid>',
|
'/<knowledge_base_uuid>',
|
||||||
methods=['GET', 'DELETE'],
|
methods=['GET', 'DELETE'],
|
||||||
endpoint='handle_specific_knowledge_base',
|
|
||||||
)
|
)
|
||||||
async def handle_specific_knowledge_base(knowledge_base_uuid: str) -> str:
|
async def handle_specific_knowledge_base(knowledge_base_uuid: str) -> quart.Response:
|
||||||
if quart.request.method == 'GET':
|
if quart.request.method == 'GET':
|
||||||
knowledge_base = await self.ap.knowledge_base_service.get_knowledge_base_by_id(knowledge_base_uuid)
|
knowledge_base = await self.ap.knowledge_service.get_knowledge_base(knowledge_base_uuid)
|
||||||
|
|
||||||
if knowledge_base is None:
|
if knowledge_base is None:
|
||||||
return self.http_status(404, -1, 'knowledge base not found')
|
return self.http_status(404, -1, 'knowledge base not found')
|
||||||
|
|
||||||
return self.success(
|
return self.success(
|
||||||
data={
|
data={
|
||||||
'base': {
|
'base': knowledge_base,
|
||||||
'name': knowledge_base.name,
|
|
||||||
'description': knowledge_base.description,
|
|
||||||
'uuid': knowledge_base.id,
|
|
||||||
'embedding_model_uuid': knowledge_base.embedding_model_uuid,
|
|
||||||
'top_k': knowledge_base.top_k,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif quart.request.method == 'DELETE':
|
elif quart.request.method == 'DELETE':
|
||||||
await self.ap.knowledge_base_service.delete_kb_by_id(knowledge_base_uuid)
|
await self.ap.knowledge_service.delete_knowledge_base(knowledge_base_uuid)
|
||||||
return self.success({})
|
return self.success({})
|
||||||
|
|
||||||
@self.route(
|
@self.route(
|
||||||
'/<knowledge_base_uuid>/files',
|
'/<knowledge_base_uuid>/files',
|
||||||
methods=['GET', 'POST'],
|
methods=['GET', 'POST'],
|
||||||
endpoint='get_knowledge_base_files',
|
|
||||||
)
|
)
|
||||||
async def get_knowledge_base_files(knowledge_base_uuid: str) -> str:
|
async def get_knowledge_base_files(knowledge_base_uuid: str) -> str:
|
||||||
if quart.request.method == 'GET':
|
if quart.request.method == 'GET':
|
||||||
files = await self.ap.knowledge_base_service.get_files_by_knowledge_base(knowledge_base_uuid)
|
files = await self.ap.knowledge_service.get_files_by_knowledge_base(knowledge_base_uuid)
|
||||||
return self.success(
|
return self.success(
|
||||||
data={
|
data={
|
||||||
'files': [
|
'files': files,
|
||||||
{
|
|
||||||
'id': file.id,
|
|
||||||
'file_name': file.file_name,
|
|
||||||
'status': file.status,
|
|
||||||
}
|
|
||||||
for file in files
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,14 +58,17 @@ class KnowledgeBaseRouterGroup(group.RouterGroup):
|
|||||||
return self.http_status(400, -1, 'File ID is required')
|
return self.http_status(400, -1, 'File ID is required')
|
||||||
|
|
||||||
# 调用服务层方法将文件与知识库关联
|
# 调用服务层方法将文件与知识库关联
|
||||||
await self.ap.knowledge_base_service.relate_file_id_with_kb(knowledge_base_uuid, file_id)
|
task_id = await self.ap.knowledge_service.store_file(knowledge_base_uuid, file_id)
|
||||||
return self.success({})
|
return self.success(
|
||||||
|
{
|
||||||
|
'task_id': task_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@self.route(
|
@self.route(
|
||||||
'/<knowledge_base_uuid>/files/<file_id>',
|
'/<knowledge_base_uuid>/files/<file_id>',
|
||||||
methods=['DELETE'],
|
methods=['DELETE'],
|
||||||
endpoint='delete_specific_file_in_kb',
|
|
||||||
)
|
)
|
||||||
async def delete_specific_file_in_kb(file_id: str,knowledge_base_uuid: str) -> str:
|
async def delete_specific_file_in_kb(file_id: str, knowledge_base_uuid: str) -> str:
|
||||||
await self.ap.knowledge_base_service.delete_data_by_file_id(file_id)
|
await self.ap.knowledge_service.delete_file(knowledge_base_uuid, file_id)
|
||||||
return self.success({})
|
return self.success({})
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
from ....core import app
|
||||||
|
from ....entity.persistence import rag as persistence_rag
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeService:
|
||||||
|
"""知识库服务"""
|
||||||
|
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application) -> None:
|
||||||
|
self.ap = ap
|
||||||
|
|
||||||
|
async def get_knowledge_bases(self) -> list[dict]:
|
||||||
|
"""获取所有知识库"""
|
||||||
|
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
|
||||||
|
knowledge_bases = result.all()
|
||||||
|
return [
|
||||||
|
self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
||||||
|
for knowledge_base in knowledge_bases
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_knowledge_base(self, kb_uuid: str) -> dict | None:
|
||||||
|
"""获取知识库"""
|
||||||
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.select(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
||||||
|
)
|
||||||
|
knowledge_base = result.first()
|
||||||
|
if knowledge_base is None:
|
||||||
|
return None
|
||||||
|
return self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
||||||
|
|
||||||
|
async def create_knowledge_base(self, kb_data: dict) -> str:
|
||||||
|
"""创建知识库"""
|
||||||
|
kb_data['uuid'] = str(uuid.uuid4())
|
||||||
|
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.KnowledgeBase).values(kb_data))
|
||||||
|
|
||||||
|
kb = await self.get_knowledge_base(kb_data['uuid'])
|
||||||
|
|
||||||
|
await self.ap.rag_mgr.load_knowledge_base(kb)
|
||||||
|
|
||||||
|
return kb_data['uuid']
|
||||||
|
|
||||||
|
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
|
||||||
|
"""更新知识库"""
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.update(persistence_rag.KnowledgeBase)
|
||||||
|
.values(kb_data)
|
||||||
|
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
||||||
|
)
|
||||||
|
await self.ap.rag_mgr.remove_knowledge_base(kb_uuid)
|
||||||
|
|
||||||
|
kb = await self.get_knowledge_base(kb_uuid)
|
||||||
|
|
||||||
|
await self.ap.rag_mgr.load_knowledge_base(kb)
|
||||||
|
|
||||||
|
async def store_file(self, kb_uuid: str, file_id: str) -> int:
|
||||||
|
"""存储文件"""
|
||||||
|
# await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.File).values(kb_id=kb_uuid, file_id=file_id))
|
||||||
|
# await self.ap.rag_mgr.store_file(file_id)
|
||||||
|
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
||||||
|
if runtime_kb is None:
|
||||||
|
raise Exception('Knowledge base not found')
|
||||||
|
return await runtime_kb.store_file(file_id)
|
||||||
|
|
||||||
|
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
|
||||||
|
"""获取知识库文件"""
|
||||||
|
result = await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
|
||||||
|
)
|
||||||
|
files = result.all()
|
||||||
|
return [self.ap.persistence_mgr.serialize_model(persistence_rag.File, file) for file in files]
|
||||||
|
|
||||||
|
async def delete_file(self, kb_uuid: str, file_id: str) -> None:
|
||||||
|
"""删除文件"""
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file_id)
|
||||||
|
)
|
||||||
|
# TODO: remove from memory
|
||||||
|
|
||||||
|
async def delete_knowledge_base(self, kb_uuid: str) -> None:
|
||||||
|
"""删除知识库"""
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.delete(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
||||||
|
)
|
||||||
|
# TODO: remove from memory
|
||||||
+4
-1
@@ -22,6 +22,7 @@ from ..api.http.service import user as user_service
|
|||||||
from ..api.http.service import model as model_service
|
from ..api.http.service import model as model_service
|
||||||
from ..api.http.service import pipeline as pipeline_service
|
from ..api.http.service import pipeline as pipeline_service
|
||||||
from ..api.http.service import bot as bot_service
|
from ..api.http.service import bot as bot_service
|
||||||
|
from ..api.http.service import knowledge as knowledge_service
|
||||||
from ..discover import engine as discover_engine
|
from ..discover import engine as discover_engine
|
||||||
from ..storage import mgr as storagemgr
|
from ..storage import mgr as storagemgr
|
||||||
from ..utils import logcache
|
from ..utils import logcache
|
||||||
@@ -48,6 +49,8 @@ class Application:
|
|||||||
|
|
||||||
model_mgr: llm_model_mgr.ModelManager = None
|
model_mgr: llm_model_mgr.ModelManager = None
|
||||||
|
|
||||||
|
rag_mgr: rag_mgr.RAGManager = None
|
||||||
|
|
||||||
# TODO 移动到 pipeline 里
|
# TODO 移动到 pipeline 里
|
||||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
tool_mgr: llm_tool_mgr.ToolManager = None
|
||||||
|
|
||||||
@@ -112,7 +115,7 @@ class Application:
|
|||||||
|
|
||||||
bot_service: bot_service.BotService = None
|
bot_service: bot_service.BotService = None
|
||||||
|
|
||||||
knowledge_base_service: rag_mgr.RAGManager = None
|
knowledge_service: knowledge_service.KnowledgeService = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from ...api.http.service import user as user_service
|
|||||||
from ...api.http.service import model as model_service
|
from ...api.http.service import model as model_service
|
||||||
from ...api.http.service import pipeline as pipeline_service
|
from ...api.http.service import pipeline as pipeline_service
|
||||||
from ...api.http.service import bot as bot_service
|
from ...api.http.service import bot as bot_service
|
||||||
|
from ...api.http.service import knowledge as knowledge_service
|
||||||
from ...discover import engine as discover_engine
|
from ...discover import engine as discover_engine
|
||||||
from ...storage import mgr as storagemgr
|
from ...storage import mgr as storagemgr
|
||||||
from ...utils import logcache
|
from ...utils import logcache
|
||||||
@@ -89,6 +90,10 @@ class BuildAppStage(stage.BootingStage):
|
|||||||
await pipeline_mgr.initialize()
|
await pipeline_mgr.initialize()
|
||||||
ap.pipeline_mgr = pipeline_mgr
|
ap.pipeline_mgr = pipeline_mgr
|
||||||
|
|
||||||
|
rag_mgr_inst = rag_mgr.RAGManager(ap)
|
||||||
|
await rag_mgr_inst.initialize_rag_system()
|
||||||
|
ap.rag_mgr = rag_mgr_inst
|
||||||
|
|
||||||
http_ctrl = http_controller.HTTPController(ap)
|
http_ctrl = http_controller.HTTPController(ap)
|
||||||
await http_ctrl.initialize()
|
await http_ctrl.initialize()
|
||||||
ap.http_ctrl = http_ctrl
|
ap.http_ctrl = http_ctrl
|
||||||
@@ -102,15 +107,14 @@ class BuildAppStage(stage.BootingStage):
|
|||||||
embedding_models_service_inst = model_service.EmbeddingModelsService(ap)
|
embedding_models_service_inst = model_service.EmbeddingModelsService(ap)
|
||||||
ap.embedding_models_service = embedding_models_service_inst
|
ap.embedding_models_service = embedding_models_service_inst
|
||||||
|
|
||||||
knowledge_base_service_inst = rag_mgr.RAGManager(ap)
|
|
||||||
await knowledge_base_service_inst.initialize_rag_system()
|
|
||||||
ap.knowledge_base_service = knowledge_base_service_inst
|
|
||||||
|
|
||||||
pipeline_service_inst = pipeline_service.PipelineService(ap)
|
pipeline_service_inst = pipeline_service.PipelineService(ap)
|
||||||
ap.pipeline_service = pipeline_service_inst
|
ap.pipeline_service = pipeline_service_inst
|
||||||
|
|
||||||
bot_service_inst = bot_service.BotService(ap)
|
bot_service_inst = bot_service.BotService(ap)
|
||||||
ap.bot_service = bot_service_inst
|
ap.bot_service = bot_service_inst
|
||||||
|
|
||||||
|
knowledge_service_inst = knowledge_service.KnowledgeService(ap)
|
||||||
|
ap.knowledge_service = knowledge_service_inst
|
||||||
|
|
||||||
ctrl = controller.Controller(ap)
|
ctrl = controller.Controller(ap)
|
||||||
ap.ctrl = ctrl
|
ap.ctrl = ctrl
|
||||||
|
|||||||
@@ -1,51 +1,50 @@
|
|||||||
from sqlalchemy import create_engine, Column, String, Text, DateTime, LargeBinary, Integer
|
import sqlalchemy
|
||||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
from .base import Base
|
||||||
from datetime import datetime
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
Base = declarative_base()
|
# Base = declarative_base()
|
||||||
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./rag_knowledge.db')
|
# DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./rag_knowledge.db')
|
||||||
print("Using database URL:", DATABASE_URL)
|
# print("Using database URL:", DATABASE_URL)
|
||||||
|
|
||||||
|
|
||||||
engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
|
# engine = create_engine(DATABASE_URL, connect_args={'check_same_thread': False})
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
def create_db_and_tables():
|
# def create_db_and_tables():
|
||||||
"""Creates all database tables defined in the Base."""
|
# """Creates all database tables defined in the Base."""
|
||||||
Base.metadata.create_all(bind=engine)
|
# Base.metadata.create_all(bind=engine)
|
||||||
print('Database tables created or already exist.')
|
# print('Database tables created or already exist.')
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeBase(Base):
|
class KnowledgeBase(Base):
|
||||||
__tablename__ = 'kb'
|
__tablename__ = 'knowledge_bases'
|
||||||
id = Column(String, primary_key=True, index=True)
|
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||||
name = Column(String, index=True)
|
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
||||||
description = Column(Text)
|
description = sqlalchemy.Column(sqlalchemy.Text)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
|
||||||
embedding_model_uuid = Column(String, default='')
|
embedding_model_uuid = sqlalchemy.Column(sqlalchemy.String, default='')
|
||||||
top_k = Column(Integer, default=5)
|
top_k = sqlalchemy.Column(sqlalchemy.Integer, default=5)
|
||||||
|
|
||||||
|
|
||||||
class File(Base):
|
class File(Base):
|
||||||
__tablename__ = 'file'
|
__tablename__ = 'knowledge_base_files'
|
||||||
id = Column(String, primary_key=True, index=True)
|
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||||
kb_id = Column(String, nullable=True)
|
kb_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||||
file_name = Column(String)
|
file_name = sqlalchemy.Column(sqlalchemy.String)
|
||||||
path = Column(String)
|
extension = sqlalchemy.Column(sqlalchemy.String)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow)
|
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
|
||||||
file_type = Column(String)
|
status = sqlalchemy.Column(sqlalchemy.String, default='pending') # pending, processing, completed, failed
|
||||||
status = Column(Integer, default=0) # 0: uploaded and processing, 1: completed, 2: failed
|
|
||||||
|
|
||||||
class Chunk(Base):
|
class Chunk(Base):
|
||||||
__tablename__ = 'chunks'
|
__tablename__ = 'knowledge_base_chunks'
|
||||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||||
file_id = Column(String, nullable=True)
|
file_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||||
text = Column(Text)
|
text = sqlalchemy.Column(sqlalchemy.Text)
|
||||||
|
|
||||||
class Vector(Base):
|
|
||||||
__tablename__ = 'vectors'
|
# class Vector(Base):
|
||||||
id = Column(String, primary_key=True, index=True)
|
# __tablename__ = 'knowledge_base_vectors'
|
||||||
chunk_id = Column(String, nullable=True)
|
# uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||||
embedding = Column(LargeBinary)
|
# chunk_id = sqlalchemy.Column(sqlalchemy.String, nullable=True)
|
||||||
|
# embedding = sqlalchemy.Column(sqlalchemy.LargeBinary)
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class EventLogger:
|
|||||||
async def _truncate_logs(self):
|
async def _truncate_logs(self):
|
||||||
if len(self.logs) > MAX_LOG_COUNT:
|
if len(self.logs) > MAX_LOG_COUNT:
|
||||||
for i in range(DELETE_COUNT_PER_TIME):
|
for i in range(DELETE_COUNT_PER_TIME):
|
||||||
for image_key in self.logs[i].images:
|
for image_key in self.logs[i].images: # type: ignore
|
||||||
await self.ap.storage_mgr.storage_provider.delete(image_key)
|
await self.ap.storage_mgr.storage_provider.delete(image_key)
|
||||||
self.logs = self.logs[DELETE_COUNT_PER_TIME:]
|
self.logs = self.logs[DELETE_COUNT_PER_TIME:]
|
||||||
|
|
||||||
|
|||||||
+163
-118
@@ -1,149 +1,189 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from pkg.rag.knowledge.services.parser import FileParser
|
from pkg.rag.knowledge.services.parser import FileParser
|
||||||
from pkg.rag.knowledge.services.chunker import Chunker
|
from pkg.rag.knowledge.services.chunker import Chunker
|
||||||
from pkg.rag.knowledge.services.database import create_db_and_tables, SessionLocal, KnowledgeBase, File, Chunk
|
from pkg.rag.knowledge.services.database import (
|
||||||
|
KnowledgeBase,
|
||||||
|
File,
|
||||||
|
Chunk,
|
||||||
|
)
|
||||||
from pkg.core import app
|
from pkg.core import app
|
||||||
from pkg.rag.knowledge.services.embedder import Embedder
|
from pkg.rag.knowledge.services.embedder import Embedder
|
||||||
from pkg.rag.knowledge.services.retriever import Retriever
|
from pkg.rag.knowledge.services.retriever import Retriever
|
||||||
from pkg.rag.knowledge.services.chroma_manager import ChromaIndexManager
|
from pkg.rag.knowledge.services.chroma_manager import ChromaIndexManager
|
||||||
from ...entity.persistence import model as persistence_model
|
from pkg.core import taskmgr
|
||||||
|
from ...entity.persistence import rag as persistence_rag
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeKnowledgeBase:
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
knowledge_base_entity: persistence_rag.KnowledgeBase
|
||||||
|
|
||||||
|
chroma_manager: ChromaIndexManager
|
||||||
|
|
||||||
|
parser: FileParser
|
||||||
|
|
||||||
|
chunker: Chunker
|
||||||
|
|
||||||
|
embedder: Embedder
|
||||||
|
|
||||||
|
retriever: Retriever
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application, knowledge_base_entity: persistence_rag.KnowledgeBase):
|
||||||
|
self.ap = ap
|
||||||
|
self.knowledge_base_entity = knowledge_base_entity
|
||||||
|
self.chroma_manager = ChromaIndexManager(ap=self.ap)
|
||||||
|
self.parser = FileParser(ap=self.ap)
|
||||||
|
self.chunker = Chunker(ap=self.ap)
|
||||||
|
self.embedder = Embedder(ap=self.ap, chroma_manager=self.chroma_manager)
|
||||||
|
self.retriever = Retriever(ap=self.ap, chroma_manager=self.chroma_manager)
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _store_file_task(self, file: persistence_rag.File, task_context: taskmgr.TaskContext):
|
||||||
|
try:
|
||||||
|
# set file status to processing
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.update(persistence_rag.File)
|
||||||
|
.where(persistence_rag.File.uuid == file.uuid)
|
||||||
|
.values(status='processing')
|
||||||
|
)
|
||||||
|
|
||||||
|
task_context.set_current_action('Parsing file')
|
||||||
|
# parse file
|
||||||
|
text = await self.parser.parse(file.file_name, file.extension)
|
||||||
|
if not text:
|
||||||
|
raise Exception(f'No text extracted from file {file.file_name}')
|
||||||
|
|
||||||
|
task_context.set_current_action('Chunking file')
|
||||||
|
# chunk file
|
||||||
|
chunks_texts = await self.chunker.chunk(text)
|
||||||
|
if not chunks_texts:
|
||||||
|
raise Exception(f'No chunks extracted from file {file.file_name}')
|
||||||
|
|
||||||
|
task_context.set_current_action('Embedding chunks')
|
||||||
|
# embed chunks
|
||||||
|
await self.embedder.embed_and_store(
|
||||||
|
file_id=file.uuid, chunks=chunks_texts, embedding_model=self.knowledge_base_entity.embedding_model_uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
# set file status to completed
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.update(persistence_rag.File)
|
||||||
|
.where(persistence_rag.File.uuid == file.uuid)
|
||||||
|
.values(status='completed')
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.ap.logger.error(f'Error storing file {file.file_id}: {e}')
|
||||||
|
# set file status to failed
|
||||||
|
await self.ap.persistence_mgr.execute_async(
|
||||||
|
sqlalchemy.update(persistence_rag.File)
|
||||||
|
.where(persistence_rag.File.uuid == file.uuid)
|
||||||
|
.values(status='failed')
|
||||||
|
)
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def store_file(self, file_id: str) -> int:
|
||||||
|
# pre checking
|
||||||
|
if not await self.ap.storage_mgr.storage_provider.exists(file_id):
|
||||||
|
raise Exception(f'File {file_id} not found')
|
||||||
|
|
||||||
|
file_uuid = str(uuid.uuid4())
|
||||||
|
kb_id = self.knowledge_base_entity.uuid
|
||||||
|
file_name = file_id
|
||||||
|
extension = os.path.splitext(file_id)[1].lstrip('.')
|
||||||
|
|
||||||
|
file = persistence_rag.File(
|
||||||
|
uuid=file_uuid,
|
||||||
|
kb_id=kb_id,
|
||||||
|
file_name=file_name,
|
||||||
|
extension=extension,
|
||||||
|
status='pending',
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.File).values(**file.to_dict()))
|
||||||
|
|
||||||
|
# run background task asynchronously
|
||||||
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
wrapper = self.ap.task_mgr.create_user_task(
|
||||||
|
self._store_file_task(file, task_context=ctx),
|
||||||
|
kind='knowledge-operation',
|
||||||
|
name=f'knowledge-store-file-{file_id}',
|
||||||
|
label=f'Store file {file_id}',
|
||||||
|
context=ctx,
|
||||||
|
)
|
||||||
|
return wrapper.id
|
||||||
|
|
||||||
|
async def dispose(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RAGManager:
|
class RAGManager:
|
||||||
ap: app.Application
|
ap: app.Application
|
||||||
|
|
||||||
|
knowledge_bases: list[RuntimeKnowledgeBase]
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
def __init__(self, ap: app.Application):
|
||||||
self.ap = ap
|
self.ap = ap
|
||||||
self.chroma_manager = ChromaIndexManager()
|
self.knowledge_bases = []
|
||||||
self.parser = FileParser()
|
|
||||||
self.chunker = Chunker()
|
|
||||||
self.embedder = Embedder(ap=self.ap, chroma_manager=self.chroma_manager)
|
|
||||||
self.retriever = Retriever(ap=self.ap, chroma_manager=self.chroma_manager)
|
|
||||||
|
|
||||||
async def initialize_rag_system(self):
|
async def initialize(self):
|
||||||
"""Initializes the RAG system by creating database tables."""
|
pass
|
||||||
await asyncio.to_thread(create_db_and_tables)
|
|
||||||
|
|
||||||
async def create_knowledge_base(
|
async def load_knowledge_bases_from_db(self):
|
||||||
self, kb_name: str, kb_description: str, embedding_model_uuid: str = '', top_k: int = 5
|
self.ap.logger.info('Loading knowledge bases from db...')
|
||||||
):
|
|
||||||
"""
|
|
||||||
Creates a new knowledge base if it doesn't already exist.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not kb_name:
|
|
||||||
raise ValueError('Knowledge base name must be set while creating.')
|
|
||||||
|
|
||||||
def _create_kb_sync():
|
self.knowledge_bases = []
|
||||||
session = SessionLocal()
|
|
||||||
try:
|
|
||||||
kb = session.query(KnowledgeBase).filter_by(name=kb_name).first()
|
|
||||||
if not kb:
|
|
||||||
id = str(uuid.uuid4())
|
|
||||||
new_kb = KnowledgeBase(
|
|
||||||
name=kb_name,
|
|
||||||
description=kb_description,
|
|
||||||
embedding_model_uuid=embedding_model_uuid,
|
|
||||||
top_k=top_k,
|
|
||||||
id=id,
|
|
||||||
)
|
|
||||||
session.add(new_kb)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(new_kb)
|
|
||||||
self.ap.logger.info(f"Knowledge Base '{kb_name}' created.")
|
|
||||||
print(embedding_model_uuid)
|
|
||||||
return new_kb.id
|
|
||||||
else:
|
|
||||||
self.ap.logger.info(f"Knowledge Base '{kb_name}' already exists.")
|
|
||||||
except Exception as e:
|
|
||||||
session.rollback()
|
|
||||||
self.ap.logger.error(f"Error in _create_kb_sync for '{kb_name}': {str(e)}", exc_info=True)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return await asyncio.to_thread(_create_kb_sync)
|
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f"Error creating knowledge base '{kb_name}': {str(e)}", exc_info=True)
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def get_all_knowledge_bases(self):
|
knowledge_bases = result.all()
|
||||||
"""
|
|
||||||
Retrieves all knowledge bases from the database.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
|
|
||||||
def _get_all_kbs_sync():
|
for knowledge_base in knowledge_bases:
|
||||||
session = SessionLocal()
|
try:
|
||||||
try:
|
await self.load_knowledge_base(knowledge_base)
|
||||||
return session.query(KnowledgeBase).all()
|
except Exception as e:
|
||||||
finally:
|
self.ap.logger.error(
|
||||||
session.close()
|
f'Error loading knowledge base {knowledge_base.uuid}: {e}\n{traceback.format_exc()}'
|
||||||
|
)
|
||||||
|
|
||||||
return await asyncio.to_thread(_get_all_kbs_sync)
|
async def load_knowledge_base(
|
||||||
except Exception as e:
|
self,
|
||||||
self.ap.logger.error(f'Error retrieving knowledge bases: {str(e)}', exc_info=True)
|
knowledge_base_entity: persistence_rag.KnowledgeBase | sqlalchemy.Row | dict,
|
||||||
return []
|
) -> RuntimeKnowledgeBase:
|
||||||
|
if isinstance(knowledge_base_entity, sqlalchemy.Row):
|
||||||
|
knowledge_base_entity = persistence_rag.KnowledgeBase(**knowledge_base_entity._mapping)
|
||||||
|
elif isinstance(knowledge_base_entity, dict):
|
||||||
|
knowledge_base_entity = persistence_rag.KnowledgeBase(**knowledge_base_entity)
|
||||||
|
|
||||||
async def get_knowledge_base_by_id(self, kb_id: str):
|
runtime_knowledge_base = RuntimeKnowledgeBase(ap=self.ap, knowledge_base_entity=knowledge_base_entity)
|
||||||
"""
|
|
||||||
Retrieves a specific knowledge base by its ID.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
|
|
||||||
def _get_kb_sync(kb_id_param):
|
await runtime_knowledge_base.initialize()
|
||||||
session = SessionLocal()
|
|
||||||
try:
|
|
||||||
return session.query(KnowledgeBase).filter_by(id=kb_id_param).first()
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return await asyncio.to_thread(_get_kb_sync, kb_id)
|
self.knowledge_bases.append(runtime_knowledge_base)
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'Error retrieving knowledge base with ID {kb_id}: {str(e)}', exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_files_by_knowledge_base(self, kb_id: str):
|
return runtime_knowledge_base
|
||||||
"""
|
|
||||||
Retrieves files associated with a specific knowledge base by querying the File table directly.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
|
|
||||||
def _get_files_sync(kb_id_param):
|
async def get_knowledge_base_by_uuid(self, kb_uuid: str) -> RuntimeKnowledgeBase | None:
|
||||||
session = SessionLocal()
|
for kb in self.knowledge_bases:
|
||||||
try:
|
if kb.knowledge_base_entity.uuid == kb_uuid:
|
||||||
return session.query(File).filter_by(kb_id=kb_id_param).all()
|
return kb
|
||||||
finally:
|
return None
|
||||||
session.close()
|
|
||||||
|
|
||||||
return await asyncio.to_thread(_get_files_sync, kb_id)
|
async def remove_knowledge_base(self, kb_uuid: str):
|
||||||
except Exception as e:
|
for kb in self.knowledge_bases:
|
||||||
self.ap.logger.error(f'Error retrieving files for knowledge base ID {kb_id}: {str(e)}', exc_info=True)
|
if kb.knowledge_base_entity.uuid == kb_uuid:
|
||||||
return []
|
await kb.dispose()
|
||||||
|
self.knowledge_bases.remove(kb)
|
||||||
async def get_all_files(self):
|
return
|
||||||
"""
|
|
||||||
Retrieves all files stored in the database, regardless of their association
|
|
||||||
with any specific knowledge base.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
|
|
||||||
def _get_all_files_sync():
|
|
||||||
session = SessionLocal()
|
|
||||||
try:
|
|
||||||
return session.query(File).all()
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return await asyncio.to_thread(_get_all_files_sync)
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'Error retrieving all files: {str(e)}', exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def store_data(self, file_path: str, kb_id: str, file_type: str, file_id: str = None):
|
async def store_data(self, file_path: str, kb_id: str, file_type: str, file_id: str = None):
|
||||||
"""
|
"""
|
||||||
@@ -220,7 +260,8 @@ class RAGManager:
|
|||||||
await self.ap.storage_mgr.storage_provider.delete(file_id)
|
await self.ap.storage_mgr.storage_provider.delete(file_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.ap.logger.error(
|
self.ap.logger.error(
|
||||||
f'Error deleting file from storage for file_id {file_id}: {str(e)}', exc_info=True
|
f'Error deleting file from storage for file_id {file_id}: {str(e)}',
|
||||||
|
exc_info=True,
|
||||||
)
|
)
|
||||||
self.ap.logger.info(f'Deleted file record for file_id: {file_id}')
|
self.ap.logger.info(f'Deleted file record for file_id: {file_id}')
|
||||||
else:
|
else:
|
||||||
@@ -273,7 +314,10 @@ class RAGManager:
|
|||||||
)
|
)
|
||||||
except Exception as kb_del_e:
|
except Exception as kb_del_e:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
self.ap.logger.error(f'Error deleting KnowledgeBase record for ID {kb_id}: {kb_del_e}', exc_info=True)
|
self.ap.logger.error(
|
||||||
|
f'Error deleting KnowledgeBase record for ID {kb_id}: {kb_del_e}',
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
@@ -283,7 +327,8 @@ class RAGManager:
|
|||||||
if session.is_active:
|
if session.is_active:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
self.ap.logger.error(
|
self.ap.logger.error(
|
||||||
f'Error during overall knowledge base deletion for ID {kb_id}: {str(e)}', exc_info=True
|
f'Error during overall knowledge base deletion for ID {kb_id}: {str(e)}',
|
||||||
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
from chromadb import PersistentClient
|
from chromadb import PersistentClient
|
||||||
import os
|
from pkg.core import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChromaIndexManager:
|
class ChromaIndexManager:
|
||||||
def __init__(self, collection_name: str = "default_collection"):
|
def __init__(self, ap: app.Application, collection_name: str = 'default_collection'):
|
||||||
self.logger = logging.getLogger(self.__class__.__name__)
|
self.ap = ap
|
||||||
chroma_data_path = os.path.abspath(os.path.join(__file__, "../../../../../../data/chroma"))
|
chroma_data_path = './data/chroma'
|
||||||
os.makedirs(chroma_data_path, exist_ok=True)
|
|
||||||
self.client = PersistentClient(path=chroma_data_path)
|
self.client = PersistentClient(path=chroma_data_path)
|
||||||
self._collection_name = collection_name
|
self._collection_name = collection_name
|
||||||
self._collection = None
|
self._collection = None
|
||||||
|
|
||||||
self.logger.info(f"ChromaIndexManager initialized. Collection name: {self._collection_name}")
|
self.ap.logger.info(f'ChromaIndexManager initialized. Collection name: {self._collection_name}')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def collection(self):
|
def collection(self):
|
||||||
if self._collection is None:
|
if self._collection is None:
|
||||||
self._collection = self.client.get_or_create_collection(name=self._collection_name)
|
self._collection = self.client.get_or_create_collection(name=self._collection_name)
|
||||||
self.logger.info(f"Chroma collection '{self._collection_name}' accessed/created.")
|
self.ap.logger.info(f"Chroma collection '{self._collection_name}' accessed/created.")
|
||||||
return self._collection
|
return self._collection
|
||||||
|
|
||||||
def add_embeddings_sync(self, file_ids: list[int], chunk_ids: list[int], embeddings: np.ndarray, documents: list[str]):
|
def add_embeddings_sync(
|
||||||
if embeddings.shape[0] != len(chunk_ids) or embeddings.shape[0] != len(file_ids) or embeddings.shape[0] != len(documents):
|
self, file_ids: list[int], chunk_ids: list[int], embeddings: np.ndarray, documents: list[str]
|
||||||
raise ValueError("Embedding, file_id, chunk_id, and document count mismatch.")
|
):
|
||||||
|
if (
|
||||||
|
embeddings.shape[0] != len(chunk_ids)
|
||||||
|
or embeddings.shape[0] != len(file_ids)
|
||||||
|
or embeddings.shape[0] != len(documents)
|
||||||
|
):
|
||||||
|
raise ValueError('Embedding, file_id, chunk_id, and document count mismatch.')
|
||||||
|
|
||||||
chroma_ids = [f"{file_id}_{chunk_id}" for file_id, chunk_id in zip(file_ids, chunk_ids)]
|
chroma_ids = [f'{file_id}_{chunk_id}' for file_id, chunk_id in zip(file_ids, chunk_ids)]
|
||||||
metadatas = [{"file_id": fid, "chunk_id": cid} for fid, cid in zip(file_ids, chunk_ids)]
|
metadatas = [{'file_id': fid, 'chunk_id': cid} for fid, cid in zip(file_ids, chunk_ids)]
|
||||||
|
|
||||||
self.logger.debug(f"Adding {len(embeddings)} embeddings to Chroma collection '{self._collection_name}'.")
|
self.logger.debug(f"Adding {len(embeddings)} embeddings to Chroma collection '{self._collection_name}'.")
|
||||||
self.collection.add(
|
self.collection.add(embeddings=embeddings.tolist(), ids=chroma_ids, metadatas=metadatas, documents=documents)
|
||||||
embeddings=embeddings.tolist(),
|
|
||||||
ids=chroma_ids,
|
|
||||||
metadatas=metadatas,
|
|
||||||
documents=documents
|
|
||||||
)
|
|
||||||
self.logger.info(f"Added {len(embeddings)} embeddings to Chroma collection '{self._collection_name}'.")
|
self.logger.info(f"Added {len(embeddings)} embeddings to Chroma collection '{self._collection_name}'.")
|
||||||
|
|
||||||
def search_sync(self, query_embedding: np.ndarray, k: int = 5):
|
def search_sync(self, query_embedding: np.ndarray, k: int = 5):
|
||||||
@@ -54,12 +54,14 @@ class ChromaIndexManager:
|
|||||||
query_embeddings=query_embedding.tolist(),
|
query_embeddings=query_embedding.tolist(),
|
||||||
n_results=k,
|
n_results=k,
|
||||||
# REMOVE 'ids' from the include list. It's returned by default.
|
# REMOVE 'ids' from the include list. It's returned by default.
|
||||||
include=["metadatas", "distances", "documents"]
|
include=['metadatas', 'distances', 'documents'],
|
||||||
)
|
)
|
||||||
self.logger.debug(f"Chroma search returned {len(results.get('ids', [[]])[0])} results.")
|
self.logger.debug(f'Chroma search returned {len(results.get("ids", [[]])[0])} results.')
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def delete_by_file_id_sync(self, file_id: int):
|
def delete_by_file_id_sync(self, file_id: int):
|
||||||
self.logger.info(f"Deleting embeddings for file_id: {file_id} from Chroma collection '{self._collection_name}'.")
|
self.logger.info(
|
||||||
self.collection.delete(where={"file_id": file_id})
|
f"Deleting embeddings for file_id: {file_id} from Chroma collection '{self._collection_name}'."
|
||||||
self.logger.info(f"Deleted embeddings for file_id: {file_id} from Chroma.")
|
)
|
||||||
|
self.collection.delete(where={'file_id': file_id})
|
||||||
|
self.logger.info(f'Deleted embeddings for file_id: {file_id} from Chroma.')
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
# services/chunker.py
|
# services/chunker.py
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
from pkg.rag.knowledge.services.base_service import BaseService # Assuming BaseService provides _run_sync
|
from pkg.rag.knowledge.services.base_service import BaseService # Assuming BaseService provides _run_sync
|
||||||
|
from pkg.core import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Chunker(BaseService):
|
class Chunker(BaseService):
|
||||||
"""
|
"""
|
||||||
A class for splitting long texts into smaller, overlapping chunks.
|
A class for splitting long texts into smaller, overlapping chunks.
|
||||||
"""
|
"""
|
||||||
def __init__(self, chunk_size: int = 500, chunk_overlap: int = 50):
|
|
||||||
super().__init__() # Initialize BaseService
|
def __init__(self, ap: app.Application, chunk_size: int = 500, chunk_overlap: int = 50):
|
||||||
self.logger = logging.getLogger(self.__class__.__name__)
|
super().__init__(ap) # Initialize BaseService
|
||||||
|
self.ap = ap
|
||||||
self.chunk_size = chunk_size
|
self.chunk_size = chunk_size
|
||||||
self.chunk_overlap = chunk_overlap
|
self.chunk_overlap = chunk_overlap
|
||||||
if self.chunk_overlap >= self.chunk_size:
|
if self.chunk_overlap >= self.chunk_size:
|
||||||
self.logger.warning("Chunk overlap is greater than or equal to chunk size. This may lead to empty or malformed chunks.")
|
self.logger.warning(
|
||||||
|
'Chunk overlap is greater than or equal to chunk size. This may lead to empty or malformed chunks.'
|
||||||
|
)
|
||||||
|
|
||||||
def _split_text_sync(self, text: str) -> List[str]:
|
def _split_text_sync(self, text: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
@@ -39,6 +44,7 @@ class Chunker(BaseService):
|
|||||||
|
|
||||||
# A more robust chunking strategy (e.g., using recursive character text splitter)
|
# A more robust chunking strategy (e.g., using recursive character text splitter)
|
||||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||||
|
|
||||||
text_splitter = RecursiveCharacterTextSplitter(
|
text_splitter = RecursiveCharacterTextSplitter(
|
||||||
chunk_size=self.chunk_size,
|
chunk_size=self.chunk_size,
|
||||||
chunk_overlap=self.chunk_overlap,
|
chunk_overlap=self.chunk_overlap,
|
||||||
@@ -51,8 +57,8 @@ class Chunker(BaseService):
|
|||||||
"""
|
"""
|
||||||
Asynchronously chunks a given text into smaller pieces.
|
Asynchronously chunks a given text into smaller pieces.
|
||||||
"""
|
"""
|
||||||
self.logger.info(f"Chunking text (length: {len(text)})...")
|
self.ap.logger.info(f'Chunking text (length: {len(text)})...')
|
||||||
# Run the synchronous splitting logic in a separate thread
|
# Run the synchronous splitting logic in a separate thread
|
||||||
chunks = await self._run_sync(self._split_text_sync, text)
|
chunks = await self._run_sync(self._split_text_sync, text)
|
||||||
self.logger.info(f"Text chunked into {len(chunks)} pieces.")
|
self.ap.logger.info(f'Text chunked into {len(chunks)} pieces.')
|
||||||
return chunks
|
return chunks
|
||||||
@@ -7,16 +7,10 @@ from sqlalchemy.orm import Session
|
|||||||
from pkg.rag.knowledge.services.base_service import BaseService
|
from pkg.rag.knowledge.services.base_service import BaseService
|
||||||
from pkg.rag.knowledge.services.database import Chunk, SessionLocal
|
from pkg.rag.knowledge.services.database import Chunk, SessionLocal
|
||||||
from pkg.rag.knowledge.services.chroma_manager import ChromaIndexManager
|
from pkg.rag.knowledge.services.chroma_manager import ChromaIndexManager
|
||||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
|
||||||
from ....core import app
|
from ....core import app
|
||||||
from ....entity.persistence import model as persistence_model
|
|
||||||
import sqlalchemy
|
|
||||||
from ....provider.modelmgr.requester import RuntimeEmbeddingModel
|
from ....provider.modelmgr.requester import RuntimeEmbeddingModel
|
||||||
|
|
||||||
|
|
||||||
base = declarative_base()
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class Embedder(BaseService):
|
class Embedder(BaseService):
|
||||||
def __init__(self, ap: app.Application, chroma_manager: ChromaIndexManager = None) -> None:
|
def __init__(self, ap: app.Application, chroma_manager: ChromaIndexManager = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -30,61 +24,66 @@ class Embedder(BaseService):
|
|||||||
This function assumes it's called within a context where the session
|
This function assumes it's called within a context where the session
|
||||||
will be committed/rolled back and closed by the caller.
|
will be committed/rolled back and closed by the caller.
|
||||||
"""
|
"""
|
||||||
self.logger.debug(f"Saving {len(chunks_texts)} chunks for file_id {file_id} to DB (sync).")
|
self.logger.debug(f'Saving {len(chunks_texts)} chunks for file_id {file_id} to DB (sync).')
|
||||||
chunk_objects = []
|
chunk_objects = []
|
||||||
for text in chunks_texts:
|
for text in chunks_texts:
|
||||||
chunk = Chunk(file_id=file_id, text=text)
|
chunk = Chunk(file_id=file_id, text=text)
|
||||||
session.add(chunk)
|
session.add(chunk)
|
||||||
chunk_objects.append(chunk)
|
chunk_objects.append(chunk)
|
||||||
session.flush() # This populates the .id attribute for each new chunk object
|
session.flush() # This populates the .id attribute for each new chunk object
|
||||||
self.logger.debug(f"Successfully added {len(chunk_objects)} chunk entries to DB.")
|
self.logger.debug(f'Successfully added {len(chunk_objects)} chunk entries to DB.')
|
||||||
return chunk_objects
|
return chunk_objects
|
||||||
|
|
||||||
async def embed_and_store(self, file_id: int, chunks: List[str], embedding_model: RuntimeEmbeddingModel) -> List[Chunk]:
|
async def embed_and_store(
|
||||||
|
self, file_id: int, chunks: List[str], embedding_model: RuntimeEmbeddingModel
|
||||||
|
) -> List[Chunk]:
|
||||||
if not embedding_model:
|
if not embedding_model:
|
||||||
raise RuntimeError("Embedding model not loaded. Please check Embedder initialization.")
|
raise RuntimeError('Embedding model not loaded. Please check Embedder initialization.')
|
||||||
|
|
||||||
session = SessionLocal() # Start a session that will live for the whole operation
|
session = SessionLocal() # Start a session that will live for the whole operation
|
||||||
chunk_objects = []
|
chunk_objects = []
|
||||||
try:
|
try:
|
||||||
# 1. Save chunks to the relational database first to get their IDs
|
# 1. Save chunks to the relational database first to get their IDs
|
||||||
# We call _db_save_chunks_sync directly without _run_sync's session management
|
# We call _db_save_chunks_sync directly without _run_sync's session management
|
||||||
# because we manage the session here across multiple async calls.
|
# because we manage the session here across multiple async calls.
|
||||||
chunk_objects = await asyncio.to_thread(self._db_save_chunks_sync, session, file_id, chunks)
|
chunk_objects = await asyncio.to_thread(self._db_save_chunks_sync, session, file_id, chunks)
|
||||||
session.commit() # Commit chunks to make their IDs permanent and accessible
|
session.commit() # Commit chunks to make their IDs permanent and accessible
|
||||||
|
|
||||||
if not chunk_objects:
|
if not chunk_objects:
|
||||||
self.logger.warning(f"No chunk objects created for file_id {file_id}. Skipping embedding and Chroma storage.")
|
self.logger.warning(
|
||||||
|
f'No chunk objects created for file_id {file_id}. Skipping embedding and Chroma storage.'
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# get the embeddings for the chunks
|
# get the embeddings for the chunks
|
||||||
embeddings = []
|
embeddings: list[list[float]] = []
|
||||||
i = 0
|
|
||||||
while i <len(chunks):
|
for chunk in chunks:
|
||||||
chunk = chunks[i]
|
|
||||||
result = await embedding_model.requester.invoke_embedding(
|
result = await embedding_model.requester.invoke_embedding(
|
||||||
model=embedding_model,
|
model=embedding_model,
|
||||||
input_text=chunk,
|
input_text=chunk,
|
||||||
)
|
)
|
||||||
embeddings.append(result)
|
embeddings.append(result)
|
||||||
i += 1
|
|
||||||
|
|
||||||
embeddings_np = np.array(embeddings, dtype=np.float32)
|
embeddings_np = np.array(embeddings, dtype=np.float32)
|
||||||
|
|
||||||
self.logger.info("Saving embeddings to Chroma...")
|
self.logger.info('Saving embeddings to Chroma...')
|
||||||
chunk_ids = [c.id for c in chunk_objects]
|
chunk_ids = [c.id for c in chunk_objects]
|
||||||
file_ids_for_chroma = [file_id] * len(chunk_ids)
|
file_ids_for_chroma = [file_id] * len(chunk_ids)
|
||||||
|
|
||||||
await self._run_sync( # Use _run_sync for the Chroma operation, as it's a sync call
|
await self._run_sync( # Use _run_sync for the Chroma operation, as it's a sync call
|
||||||
self.chroma_manager.add_embeddings_sync,
|
self.chroma_manager.add_embeddings_sync,
|
||||||
file_ids_for_chroma, chunk_ids, embeddings_np, chunks # Pass original chunks texts for documents
|
file_ids_for_chroma,
|
||||||
|
chunk_ids,
|
||||||
|
embeddings_np,
|
||||||
|
chunks, # Pass original chunks texts for documents
|
||||||
)
|
)
|
||||||
self.logger.info(f"Successfully saved {len(chunk_objects)} embeddings to Chroma.")
|
self.logger.info(f'Successfully saved {len(chunk_objects)} embeddings to Chroma.')
|
||||||
return chunk_objects
|
return chunk_objects
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
session.rollback() # Rollback on any error
|
session.rollback() # Rollback on any error
|
||||||
self.logger.error(f"Failed to process and store data for file_id {file_id}: {e}", exc_info=True)
|
self.logger.error(f'Failed to process and store data for file_id {file_id}: {e}', exc_info=True)
|
||||||
raise # Re-raise the exception to propagate it
|
raise # Re-raise the exception to propagate it
|
||||||
finally:
|
finally:
|
||||||
session.close() # Ensure the session is always closed
|
session.close() # Ensure the session is always closed
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import PyPDF2
|
import PyPDF2
|
||||||
|
import io
|
||||||
from docx import Document
|
from docx import Document
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import chardet
|
import chardet
|
||||||
@@ -10,7 +11,7 @@ import ebooklib
|
|||||||
from ebooklib import epub
|
from ebooklib import epub
|
||||||
import re
|
import re
|
||||||
import asyncio # Import asyncio for async operations
|
import asyncio # Import asyncio for async operations
|
||||||
import os
|
from pkg.core import app
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -24,8 +25,8 @@ class FileParser:
|
|||||||
to avoid blocking the asyncio event loop.
|
to avoid blocking the asyncio event loop.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, ap: app.Application):
|
||||||
self.logger = logging.getLogger(self.__class__.__name__)
|
self.ap = ap
|
||||||
|
|
||||||
async def _run_sync(self, sync_func: Callable, *args: Any, **kwargs: Any) -> Any:
|
async def _run_sync(self, sync_func: Callable, *args: Any, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
@@ -35,138 +36,160 @@ class FileParser:
|
|||||||
try:
|
try:
|
||||||
return await asyncio.to_thread(sync_func, *args, **kwargs)
|
return await asyncio.to_thread(sync_func, *args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f'Error running synchronous function {sync_func.__name__}: {e}')
|
self.ap.logger.error(f'Error running synchronous function {sync_func.__name__}: {e}')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def parse(self, file_path: str) -> Union[str, None]:
|
async def parse(self, file_name: str, extension: str) -> Union[str, None]:
|
||||||
"""
|
"""
|
||||||
Parses the file based on its extension and returns the extracted text content.
|
Parses the file based on its extension and returns the extracted text content.
|
||||||
This is the main asynchronous entry point for parsing.
|
This is the main asynchronous entry point for parsing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_path (str): The path to the file to be parsed.
|
file_name (str): The name of the file to be parsed, get from ap.storage_mgr
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Union[str, None]: The extracted text content as a single string, or None if parsing fails.
|
Union[str, None]: The extracted text content as a single string, or None if parsing fails.
|
||||||
"""
|
"""
|
||||||
if not file_path or not os.path.exists(file_path):
|
|
||||||
self.logger.error(f'Invalid file path provided: {file_path}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
file_extension = file_path.split('.')[-1].lower()
|
file_extension = extension.lower()
|
||||||
parser_method = getattr(self, f'_parse_{file_extension}', None)
|
parser_method = getattr(self, f'_parse_{file_extension}', None)
|
||||||
|
|
||||||
if parser_method is None:
|
if parser_method is None:
|
||||||
self.logger.error(f'Unsupported file format: {file_extension} for file {file_path}')
|
self.ap.logger.error(f'Unsupported file format: {file_extension} for file {file_name}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Pass file_path to the specific parser methods
|
# Pass file_path to the specific parser methods
|
||||||
return await parser_method(file_path)
|
return await parser_method(file_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f'Failed to parse {file_extension} file {file_path}: {e}')
|
self.ap.logger.error(f'Failed to parse {file_extension} file {file_name}: {e}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- Helper for reading files with encoding detection ---
|
# --- Helper for reading files with encoding detection ---
|
||||||
async def _read_file_content(self, file_path: str, mode: str = 'r') -> Union[str, bytes]:
|
async def _read_file_content(self, file_name: str) -> Union[str, bytes]:
|
||||||
"""
|
"""
|
||||||
Reads a file with automatic encoding detection, ensuring the synchronous
|
Reads a file with automatic encoding detection, ensuring the synchronous
|
||||||
file read operation runs in a separate thread.
|
file read operation runs in a separate thread.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _read_sync():
|
# def _read_sync():
|
||||||
with open(file_path, 'rb') as file:
|
# with open(file_path, 'rb') as file:
|
||||||
raw_data = file.read()
|
# raw_data = file.read()
|
||||||
detected = chardet.detect(raw_data)
|
# detected = chardet.detect(raw_data)
|
||||||
encoding = detected['encoding'] or 'utf-8'
|
# encoding = detected['encoding'] or 'utf-8'
|
||||||
|
|
||||||
if mode == 'r':
|
# if mode == 'r':
|
||||||
return raw_data.decode(encoding, errors='ignore')
|
# return raw_data.decode(encoding, errors='ignore')
|
||||||
return raw_data # For binary mode
|
# return raw_data # For binary mode
|
||||||
|
|
||||||
return await self._run_sync(_read_sync)
|
# return await self._run_sync(_read_sync)
|
||||||
|
file_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
|
detected = chardet.detect(file_bytes)
|
||||||
|
encoding = detected['encoding'] or 'utf-8'
|
||||||
|
|
||||||
|
return file_bytes.decode(encoding, errors='ignore')
|
||||||
|
|
||||||
# --- Specific Parser Methods ---
|
# --- Specific Parser Methods ---
|
||||||
|
|
||||||
async def _parse_txt(self, file_path: str) -> str:
|
async def _parse_txt(self, file_name: str) -> str:
|
||||||
"""Parses a TXT file and returns its content."""
|
"""Parses a TXT file and returns its content."""
|
||||||
self.logger.info(f'Parsing TXT file: {file_path}')
|
self.ap.logger.info(f'Parsing TXT file: {file_name}')
|
||||||
return await self._read_file_content(file_path, mode='r')
|
return await self._read_file_content(file_name)
|
||||||
|
|
||||||
async def _parse_pdf(self, file_path: str) -> str:
|
async def _parse_pdf(self, file_name: str) -> str:
|
||||||
"""Parses a PDF file and returns its text content."""
|
"""Parses a PDF file and returns its text content."""
|
||||||
self.logger.info(f'Parsing PDF file: {file_path}')
|
self.ap.logger.info(f'Parsing PDF file: {file_name}')
|
||||||
|
|
||||||
|
# def _parse_pdf_sync():
|
||||||
|
# text_content = []
|
||||||
|
# with open(file_name, 'rb') as file:
|
||||||
|
# pdf_reader = PyPDF2.PdfReader(file)
|
||||||
|
# for page in pdf_reader.pages:
|
||||||
|
# text = page.extract_text()
|
||||||
|
# if text:
|
||||||
|
# text_content.append(text)
|
||||||
|
# return '\n'.join(text_content)
|
||||||
|
|
||||||
|
# return await self._run_sync(_parse_pdf_sync)
|
||||||
|
|
||||||
|
pdf_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
def _parse_pdf_sync():
|
def _parse_pdf_sync():
|
||||||
|
pdf_reader = PyPDF2.PdfReader(io.BytesIO(pdf_bytes))
|
||||||
text_content = []
|
text_content = []
|
||||||
with open(file_path, 'rb') as file:
|
for page in pdf_reader.pages:
|
||||||
pdf_reader = PyPDF2.PdfReader(file)
|
text = page.extract_text()
|
||||||
for page in pdf_reader.pages:
|
if text:
|
||||||
text = page.extract_text()
|
text_content.append(text)
|
||||||
if text:
|
|
||||||
text_content.append(text)
|
|
||||||
return '\n'.join(text_content)
|
return '\n'.join(text_content)
|
||||||
|
|
||||||
return await self._run_sync(_parse_pdf_sync)
|
return await self._run_sync(_parse_pdf_sync)
|
||||||
|
|
||||||
async def _parse_docx(self, file_path: str) -> str:
|
async def _parse_docx(self, file_name: str) -> str:
|
||||||
"""Parses a DOCX file and returns its text content."""
|
"""Parses a DOCX file and returns its text content."""
|
||||||
self.logger.info(f'Parsing DOCX file: {file_path}')
|
self.ap.logger.info(f'Parsing DOCX file: {file_name}')
|
||||||
|
|
||||||
|
docx_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
def _parse_docx_sync():
|
def _parse_docx_sync():
|
||||||
doc = Document(file_path)
|
doc = Document(io.BytesIO(docx_bytes))
|
||||||
text_content = [paragraph.text for paragraph in doc.paragraphs if paragraph.text.strip()]
|
text_content = [paragraph.text for paragraph in doc.paragraphs if paragraph.text.strip()]
|
||||||
return '\n'.join(text_content)
|
return '\n'.join(text_content)
|
||||||
|
|
||||||
return await self._run_sync(_parse_docx_sync)
|
return await self._run_sync(_parse_docx_sync)
|
||||||
|
|
||||||
async def _parse_doc(self, file_path: str) -> str:
|
async def _parse_doc(self, file_name: str) -> str:
|
||||||
"""Handles .doc files, explicitly stating lack of direct support."""
|
"""Handles .doc files, explicitly stating lack of direct support."""
|
||||||
self.logger.warning(f'Direct .doc parsing is not supported for {file_path}. Please convert to .docx first.')
|
self.ap.logger.warning(f'Direct .doc parsing is not supported for {file_name}. Please convert to .docx first.')
|
||||||
raise NotImplementedError('Direct .doc parsing not supported. Please convert to .docx first.')
|
raise NotImplementedError('Direct .doc parsing not supported. Please convert to .docx first.')
|
||||||
|
|
||||||
async def _parse_xlsx(self, file_path: str) -> str:
|
async def _parse_xlsx(self, file_name: str) -> str:
|
||||||
"""Parses an XLSX file, returning text from all sheets."""
|
"""Parses an XLSX file, returning text from all sheets."""
|
||||||
self.logger.info(f'Parsing XLSX file: {file_path}')
|
self.ap.logger.info(f'Parsing XLSX file: {file_name}')
|
||||||
|
|
||||||
|
xlsx_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
def _parse_xlsx_sync():
|
def _parse_xlsx_sync():
|
||||||
excel_file = pd.ExcelFile(file_path)
|
excel_file = pd.ExcelFile(io.BytesIO(xlsx_bytes))
|
||||||
all_sheet_content = []
|
all_sheet_content = []
|
||||||
for sheet_name in excel_file.sheet_names:
|
for sheet_name in excel_file.sheet_names:
|
||||||
df = pd.read_excel(file_path, sheet_name=sheet_name)
|
df = pd.read_excel(io.BytesIO(xlsx_bytes), sheet_name=sheet_name)
|
||||||
sheet_text = f'--- Sheet: {sheet_name} ---\n{df.to_string(index=False)}\n'
|
sheet_text = f'--- Sheet: {sheet_name} ---\n{df.to_string(index=False)}\n'
|
||||||
all_sheet_content.append(sheet_text)
|
all_sheet_content.append(sheet_text)
|
||||||
return '\n'.join(all_sheet_content)
|
return '\n'.join(all_sheet_content)
|
||||||
|
|
||||||
return await self._run_sync(_parse_xlsx_sync)
|
return await self._run_sync(_parse_xlsx_sync)
|
||||||
|
|
||||||
async def _parse_csv(self, file_path: str) -> str:
|
async def _parse_csv(self, file_name: str) -> str:
|
||||||
"""Parses a CSV file and returns its content as a string."""
|
"""Parses a CSV file and returns its content as a string."""
|
||||||
self.logger.info(f'Parsing CSV file: {file_path}')
|
self.ap.logger.info(f'Parsing CSV file: {file_name}')
|
||||||
|
|
||||||
|
csv_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
def _parse_csv_sync():
|
def _parse_csv_sync():
|
||||||
# pd.read_csv can often detect encoding, but explicit detection is safer
|
# pd.read_csv can often detect encoding, but explicit detection is safer
|
||||||
raw_data = self._read_file_content(
|
# raw_data = self._read_file_content(
|
||||||
file_path, mode='rb'
|
# file_name, mode='rb'
|
||||||
) # Note: this will need to be await outside this sync function
|
# ) # Note: this will need to be await outside this sync function
|
||||||
_ = raw_data
|
# _ = raw_data
|
||||||
# For simplicity, we'll let pandas handle encoding internally after a raw read.
|
# For simplicity, we'll let pandas handle encoding internally after a raw read.
|
||||||
# A more robust solution might pass encoding directly to pd.read_csv after detection.
|
# A more robust solution might pass encoding directly to pd.read_csv after detection.
|
||||||
detected = chardet.detect(open(file_path, 'rb').read())
|
detected = chardet.detect(io.BytesIO(csv_bytes))
|
||||||
encoding = detected['encoding'] or 'utf-8'
|
encoding = detected['encoding'] or 'utf-8'
|
||||||
df = pd.read_csv(file_path, encoding=encoding)
|
df = pd.read_csv(io.BytesIO(csv_bytes), encoding=encoding)
|
||||||
return df.to_string(index=False)
|
return df.to_string(index=False)
|
||||||
|
|
||||||
return await self._run_sync(_parse_csv_sync)
|
return await self._run_sync(_parse_csv_sync)
|
||||||
|
|
||||||
async def _parse_markdown(self, file_path: str) -> str:
|
async def _parse_markdown(self, file_name: str) -> str:
|
||||||
"""Parses a Markdown file, converting it to structured plain text."""
|
"""Parses a Markdown file, converting it to structured plain text."""
|
||||||
self.logger.info(f'Parsing Markdown file: {file_path}')
|
self.ap.logger.info(f'Parsing Markdown file: {file_name}')
|
||||||
|
|
||||||
|
md_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
def _parse_markdown_sync():
|
def _parse_markdown_sync():
|
||||||
md_content = self._read_file_content(
|
md_content = io.BytesIO(md_bytes).read().decode('utf-8', errors='ignore')
|
||||||
file_path, mode='r'
|
|
||||||
) # This is a synchronous call within a sync function
|
|
||||||
html_content = markdown.markdown(
|
html_content = markdown.markdown(
|
||||||
md_content, extensions=['extra', 'codehilite', 'tables', 'toc', 'fenced_code']
|
md_content, extensions=['extra', 'codehilite', 'tables', 'toc', 'fenced_code']
|
||||||
)
|
)
|
||||||
@@ -200,12 +223,14 @@ class FileParser:
|
|||||||
|
|
||||||
return await self._run_sync(_parse_markdown_sync)
|
return await self._run_sync(_parse_markdown_sync)
|
||||||
|
|
||||||
async def _parse_html(self, file_path: str) -> str:
|
async def _parse_html(self, file_name: str) -> str:
|
||||||
"""Parses an HTML file, extracting structured plain text."""
|
"""Parses an HTML file, extracting structured plain text."""
|
||||||
self.logger.info(f'Parsing HTML file: {file_path}')
|
self.ap.logger.info(f'Parsing HTML file: {file_name}')
|
||||||
|
|
||||||
|
html_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
def _parse_html_sync():
|
def _parse_html_sync():
|
||||||
html_content = self._read_file_content(file_path, mode='r') # Sync call within sync function
|
html_content = io.BytesIO(html_bytes).read().decode('utf-8', errors='ignore')
|
||||||
soup = BeautifulSoup(html_content, 'html.parser')
|
soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
for script_or_style in soup(['script', 'style']):
|
for script_or_style in soup(['script', 'style']):
|
||||||
script_or_style.decompose()
|
script_or_style.decompose()
|
||||||
@@ -236,12 +261,14 @@ class FileParser:
|
|||||||
|
|
||||||
return await self._run_sync(_parse_html_sync)
|
return await self._run_sync(_parse_html_sync)
|
||||||
|
|
||||||
async def _parse_epub(self, file_path: str) -> str:
|
async def _parse_epub(self, file_name: str) -> str:
|
||||||
"""Parses an EPUB file, extracting metadata and content."""
|
"""Parses an EPUB file, extracting metadata and content."""
|
||||||
self.logger.info(f'Parsing EPUB file: {file_path}')
|
self.ap.logger.info(f'Parsing EPUB file: {file_name}')
|
||||||
|
|
||||||
|
epub_bytes = await self.ap.storage_mgr.storage_provider.load(file_name)
|
||||||
|
|
||||||
def _parse_epub_sync():
|
def _parse_epub_sync():
|
||||||
book = epub.read_epub(file_path)
|
book = epub.read_epub(io.BytesIO(epub_bytes))
|
||||||
text_content = []
|
text_content = []
|
||||||
title_meta = book.get_metadata('DC', 'title')
|
title_meta = book.get_metadata('DC', 'title')
|
||||||
if title_meta:
|
if title_meta:
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..core import app
|
||||||
|
|
||||||
|
|
||||||
|
class VectorDBManager:
|
||||||
|
ap: app.Application
|
||||||
|
|
||||||
|
def __init__(self, ap: app.Application):
|
||||||
|
self.ap = ap
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
|
||||||
|
class VectorDatabase(abc.ABC):
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user