Track engine model packages so chat can start.
A bare models ignore rule left the Python packages out of git, so the deployed image failed while importing kamidso.engine.serve.rag.models. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b93ecf1691
commit
228324db7b
29 files changed
+2792
-2
No files matched your search
+3
-1
@@ -15,7 +15,9 @@ __pycache__
|
|||||||
htmlcov
|
htmlcov
|
||||||
data
|
data
|
||||||
logs
|
logs
|
||||||
models
|
# Root weights directory only. A bare name would also drop Python packages
|
||||||
|
# such as kamidso/engine/serve/rag/models.
|
||||||
|
/models
|
||||||
plugins
|
plugins
|
||||||
pilot/data
|
pilot/data
|
||||||
pilot/message
|
pilot/message
|
||||||
|
|||||||
+3
-1
@@ -37,7 +37,9 @@ pilot/meta_data/*.db
|
|||||||
pilot/meta_data/*.db-*
|
pilot/meta_data/*.db-*
|
||||||
pilot/meta_data/alembic/versions/*
|
pilot/meta_data/alembic/versions/*
|
||||||
pilot/benchmark_meta_data/*.db
|
pilot/benchmark_meta_data/*.db
|
||||||
models/
|
# Root weights directory only. A bare `models/` also drops Python packages
|
||||||
|
# such as kamidso/engine/serve/rag/models.
|
||||||
|
/models/
|
||||||
nltk_data/
|
nltk_data/
|
||||||
.chroma/
|
.chroma/
|
||||||
.vectordb/
|
.vectordb/
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This is an auto-generated __init__.py file
|
||||||
|
# generated by `kamidso.engine.core new serve agent/chat`
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""This is an auto-generated model file
|
||||||
|
You can define your own models and DAOs here
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
|
||||||
|
from ..api.schemas import ServeRequest, ServerResponse
|
||||||
|
from ..config import SERVER_APP_TABLE_NAME, ServeConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServeEntity(Model):
|
||||||
|
__tablename__ = SERVER_APP_TABLE_NAME
|
||||||
|
id = Column(Integer, primary_key=True, comment="Auto increment id")
|
||||||
|
|
||||||
|
# TODO: define your own fields here
|
||||||
|
|
||||||
|
gmt_created = Column(DateTime, default=datetime.now, comment="Record creation time")
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, comment="Record update time")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"ServeEntity(id={self.id}, gmt_created='{self.gmt_created}', "
|
||||||
|
f"gmt_modified='{self.gmt_modified}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServeDao(BaseDao[ServeEntity, ServeRequest, ServerResponse]):
|
||||||
|
"""The DAO class for Agent/chat"""
|
||||||
|
|
||||||
|
def __init__(self, serve_config: ServeConfig):
|
||||||
|
super().__init__()
|
||||||
|
self._serve_config = serve_config
|
||||||
|
|
||||||
|
def from_request(self, request: Union[ServeRequest, Dict[str, Any]]) -> ServeEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = request.dict() if isinstance(request, ServeRequest) else request
|
||||||
|
entity = ServeEntity(**request_dict)
|
||||||
|
# TODO implement your own logic here, transfer the request_dict to an entity
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: ServeEntity) -> ServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a request
|
||||||
|
return ServeRequest()
|
||||||
|
|
||||||
|
def to_response(self, entity: ServeEntity) -> ServerResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RES: The response
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a response
|
||||||
|
return ServerResponse()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Connector models module."""
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
|
||||||
|
from ..config import ServeConfig
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorInstanceEntity(Model):
|
||||||
|
__tablename__ = "connector_instance"
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, primary_key=True, autoincrement=True, comment="Auto increment id"
|
||||||
|
)
|
||||||
|
connector_id = Column(
|
||||||
|
String(64), unique=True, nullable=False, comment="Connector UUID"
|
||||||
|
)
|
||||||
|
connector_type = Column(
|
||||||
|
String(64), nullable=False, comment="Connector type, e.g. yuque, feishu"
|
||||||
|
)
|
||||||
|
display_name = Column(String(256), nullable=True, comment="Display name")
|
||||||
|
encrypted_credentials = Column(
|
||||||
|
Text, nullable=True, comment="Encrypted credentials JSON"
|
||||||
|
)
|
||||||
|
encryption_salt = Column(String(256), nullable=True, comment="Encryption salt")
|
||||||
|
status = Column(
|
||||||
|
String(32),
|
||||||
|
nullable=True,
|
||||||
|
comment="Status: active/error/disconnected/needs_reactivation",
|
||||||
|
)
|
||||||
|
config_json = Column(Text, nullable=True, comment="Optional extra config JSON")
|
||||||
|
user_name = Column(String(128), index=True, nullable=True, comment="User name")
|
||||||
|
sys_code = Column(String(128), index=True, nullable=True, comment="System code")
|
||||||
|
gmt_created = Column(DateTime, default=datetime.now, comment="Record creation time")
|
||||||
|
gmt_modified = Column(
|
||||||
|
DateTime,
|
||||||
|
default=datetime.now,
|
||||||
|
onupdate=datetime.now,
|
||||||
|
comment="Record update time",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"ConnectorInstanceEntity(id={self.id}, "
|
||||||
|
f"connector_id='{self.connector_id}', "
|
||||||
|
f"connector_type='{self.connector_type}', "
|
||||||
|
f"display_name='{self.display_name}', "
|
||||||
|
f"status='{self.status}', user_name='{self.user_name}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectorInstanceDao(
|
||||||
|
BaseDao[ConnectorInstanceEntity, Dict[str, Any], Dict[str, Any]]
|
||||||
|
):
|
||||||
|
"""DAO for ConnectorInstanceEntity providing basic CRUD operations."""
|
||||||
|
|
||||||
|
def __init__(self, serve_config: ServeConfig):
|
||||||
|
super().__init__()
|
||||||
|
self._serve_config = serve_config
|
||||||
|
|
||||||
|
def from_request(
|
||||||
|
self, request: Union[Dict[str, Any], Any]
|
||||||
|
) -> ConnectorInstanceEntity:
|
||||||
|
"""Convert a request dict to a ConnectorInstanceEntity.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Request dict or object with connector fields.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ConnectorInstanceEntity: The entity instance.
|
||||||
|
"""
|
||||||
|
request_dict = request if isinstance(request, dict) else request.dict()
|
||||||
|
entity = ConnectorInstanceEntity(**request_dict)
|
||||||
|
if not entity.connector_id:
|
||||||
|
entity.connector_id = str(uuid.uuid4())
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: ConnectorInstanceEntity) -> Dict[str, Any]:
|
||||||
|
"""Convert a ConnectorInstanceEntity to a request dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity: The entity instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: The request dict.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"connector_id": entity.connector_id,
|
||||||
|
"connector_type": entity.connector_type,
|
||||||
|
"display_name": entity.display_name,
|
||||||
|
"encrypted_credentials": entity.encrypted_credentials,
|
||||||
|
"encryption_salt": entity.encryption_salt,
|
||||||
|
"status": entity.status,
|
||||||
|
"config_json": entity.config_json,
|
||||||
|
"user_name": entity.user_name,
|
||||||
|
"sys_code": entity.sys_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_response(self, entity: ConnectorInstanceEntity) -> Dict[str, Any]:
|
||||||
|
"""Convert a ConnectorInstanceEntity to a response dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity: The entity instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: The response dict.
|
||||||
|
"""
|
||||||
|
gmt_created_str = (
|
||||||
|
entity.gmt_created.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if entity.gmt_created
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
gmt_modified_str = (
|
||||||
|
entity.gmt_modified.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if entity.gmt_modified
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": entity.id,
|
||||||
|
"connector_id": entity.connector_id,
|
||||||
|
"connector_type": entity.connector_type,
|
||||||
|
"display_name": entity.display_name,
|
||||||
|
"status": entity.status,
|
||||||
|
"config_json": entity.config_json,
|
||||||
|
"user_name": entity.user_name,
|
||||||
|
"sys_code": entity.sys_code,
|
||||||
|
"gmt_created": gmt_created_str,
|
||||||
|
"gmt_modified": gmt_modified_str,
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This is an auto-generated __init__.py file
|
||||||
|
# generated by `kamidso.engine.core new serve conversation`
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"""This is an auto-generated model file
|
||||||
|
You can define your own models and DAOs here
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from kamidso.engine.core.core import MessageStorageItem
|
||||||
|
from kamidso.engine.core.storage.chat_history.chat_history_db import ChatHistoryEntity as ServeEntity
|
||||||
|
from kamidso.engine.core.storage.chat_history.chat_history_db import ChatHistoryMessageEntity
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao
|
||||||
|
from kamidso.engine.core.util import PaginationResult
|
||||||
|
|
||||||
|
from ..api.schemas import ServeRequest, ServerResponse
|
||||||
|
from ..config import ServeConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServeDao(BaseDao[ServeEntity, ServeRequest, ServerResponse]):
|
||||||
|
"""The DAO class for Conversation"""
|
||||||
|
|
||||||
|
def __init__(self, serve_config: ServeConfig):
|
||||||
|
super().__init__()
|
||||||
|
self._serve_config = serve_config
|
||||||
|
|
||||||
|
def from_request(self, request: Union[ServeRequest, Dict[str, Any]]) -> ServeEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = (
|
||||||
|
request.to_dict() if isinstance(request, ServeRequest) else request
|
||||||
|
)
|
||||||
|
entity = ServeEntity(**request_dict)
|
||||||
|
# TODO implement your own logic here, transfer the request_dict to an entity
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: ServeEntity) -> ServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a request
|
||||||
|
return ServeRequest()
|
||||||
|
|
||||||
|
def to_response(self, entity: ServeEntity) -> ServerResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RES: The response
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a response
|
||||||
|
gmt_created = (
|
||||||
|
entity.gmt_created.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if entity.gmt_created
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
gmt_modified = (
|
||||||
|
entity.gmt_modified.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if entity.gmt_modified
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
return ServerResponse(
|
||||||
|
app_code=entity.app_code,
|
||||||
|
conv_uid=entity.conv_uid,
|
||||||
|
user_input=entity.summary,
|
||||||
|
chat_mode=entity.chat_mode,
|
||||||
|
user_name="",
|
||||||
|
sys_code=entity.sys_code,
|
||||||
|
gmt_created=gmt_created,
|
||||||
|
gmt_modified=gmt_modified,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_latest_message(self, conv_uid: str) -> Optional[MessageStorageItem]:
|
||||||
|
"""Get the latest message of a conversation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conv_uid (str): The conversation UID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ChatHistoryMessageEntity: The latest message
|
||||||
|
"""
|
||||||
|
with self.session() as session:
|
||||||
|
entity: ChatHistoryMessageEntity = (
|
||||||
|
session.query(ChatHistoryMessageEntity)
|
||||||
|
.filter(ChatHistoryMessageEntity.conv_uid == conv_uid)
|
||||||
|
.order_by(ChatHistoryMessageEntity.gmt_created.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not entity:
|
||||||
|
return None
|
||||||
|
message_detail = (
|
||||||
|
json.loads(entity.message_detail) if entity.message_detail else {}
|
||||||
|
)
|
||||||
|
return MessageStorageItem(entity.conv_uid, entity.index, message_detail)
|
||||||
|
|
||||||
|
def _parse_old_messages(self, entity: ServeEntity) -> List[Dict[str, Any]]:
|
||||||
|
"""Parse the old messages
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (ServeEntity): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The old messages
|
||||||
|
"""
|
||||||
|
messages = json.loads(entity.messages)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def get_conv_by_page(
|
||||||
|
self, req: ServeRequest, page: int, page_size: int
|
||||||
|
) -> PaginationResult[ServerResponse]:
|
||||||
|
"""Get conversation by page
|
||||||
|
|
||||||
|
Args:
|
||||||
|
req (ServeRequest): The request
|
||||||
|
page (int): The page number
|
||||||
|
page_size (int): The page size
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[ChatHistoryEntity]: The conversation list
|
||||||
|
"""
|
||||||
|
with self.session(commit=False) as session:
|
||||||
|
query = self._create_query_object(session, req)
|
||||||
|
query = query.order_by(ServeEntity.gmt_created.desc())
|
||||||
|
total_count = query.count()
|
||||||
|
items = query.offset((page - 1) * page_size).limit(page_size)
|
||||||
|
total_pages = (total_count + page_size - 1) // page_size
|
||||||
|
result_items = []
|
||||||
|
for item in items:
|
||||||
|
select_param, model_name = "", None
|
||||||
|
if item.messages:
|
||||||
|
messages = self._parse_old_messages(item)
|
||||||
|
last_round = max(messages, key=lambda x: x["chat_order"])
|
||||||
|
if "param_value" in last_round:
|
||||||
|
select_param = last_round["param_value"]
|
||||||
|
else:
|
||||||
|
select_param = ""
|
||||||
|
else:
|
||||||
|
latest_message = self.get_latest_message(item.conv_uid)
|
||||||
|
if latest_message:
|
||||||
|
message = latest_message.to_message()
|
||||||
|
select_param = message.additional_kwargs.get("param_value")
|
||||||
|
model_name = message.additional_kwargs.get("model_name")
|
||||||
|
res_item = self.to_response(item)
|
||||||
|
res_item.select_param = select_param
|
||||||
|
res_item.model_name = model_name
|
||||||
|
result_items.append(res_item)
|
||||||
|
|
||||||
|
result = PaginationResult(
|
||||||
|
items=result_items,
|
||||||
|
total_count=total_count,
|
||||||
|
total_pages=total_pages,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,2 @@
|
|||||||
|
# This is an auto-generated __init__.py file
|
||||||
|
# generated by `kamidso.engine.core new serve file`
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import json
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from kamidso.engine.core.core.interface.file import FileMetadata, FileMetadataIdentifier
|
||||||
|
from kamidso.engine.core.core.interface.storage import StorageItemAdapter
|
||||||
|
|
||||||
|
from .models import ServeEntity
|
||||||
|
|
||||||
|
|
||||||
|
class FileMetadataAdapter(StorageItemAdapter[FileMetadata, ServeEntity]):
|
||||||
|
"""File metadata adapter.
|
||||||
|
|
||||||
|
Convert between storage format and database model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_storage_format(self, item: FileMetadata) -> ServeEntity:
|
||||||
|
"""Convert to storage format."""
|
||||||
|
custom_metadata = (
|
||||||
|
{k: v for k, v in item.custom_metadata.items()}
|
||||||
|
if item.custom_metadata
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
user_name = item.user_name or custom_metadata.get("user_name")
|
||||||
|
sys_code = item.sys_code or custom_metadata.get("sys_code")
|
||||||
|
if "user_name" in custom_metadata:
|
||||||
|
del custom_metadata["user_name"]
|
||||||
|
if "sys_code" in custom_metadata:
|
||||||
|
del custom_metadata["sys_code"]
|
||||||
|
custom_metadata_json = (
|
||||||
|
json.dumps(custom_metadata, ensure_ascii=False) if custom_metadata else None
|
||||||
|
)
|
||||||
|
return ServeEntity(
|
||||||
|
bucket=item.bucket,
|
||||||
|
file_id=item.file_id,
|
||||||
|
file_name=item.file_name,
|
||||||
|
file_size=item.file_size,
|
||||||
|
storage_type=item.storage_type,
|
||||||
|
storage_path=item.storage_path,
|
||||||
|
uri=item.uri,
|
||||||
|
custom_metadata=custom_metadata_json,
|
||||||
|
file_hash=item.file_hash,
|
||||||
|
user_name=user_name,
|
||||||
|
sys_code=sys_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_storage_format(self, model: ServeEntity) -> FileMetadata:
|
||||||
|
"""Convert from storage format."""
|
||||||
|
custom_metadata = (
|
||||||
|
json.loads(model.custom_metadata) if model.custom_metadata else None
|
||||||
|
)
|
||||||
|
if custom_metadata is None:
|
||||||
|
custom_metadata = {}
|
||||||
|
if model.user_name:
|
||||||
|
custom_metadata["user_name"] = model.user_name
|
||||||
|
if model.sys_code:
|
||||||
|
custom_metadata["sys_code"] = model.sys_code
|
||||||
|
|
||||||
|
return FileMetadata(
|
||||||
|
bucket=model.bucket,
|
||||||
|
file_id=model.file_id,
|
||||||
|
file_name=model.file_name,
|
||||||
|
file_size=model.file_size,
|
||||||
|
storage_type=model.storage_type,
|
||||||
|
storage_path=model.storage_path,
|
||||||
|
uri=model.uri,
|
||||||
|
custom_metadata=custom_metadata,
|
||||||
|
file_hash=model.file_hash,
|
||||||
|
user_name=model.user_name,
|
||||||
|
sys_code=model.sys_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_query_for_identifier(
|
||||||
|
self,
|
||||||
|
storage_format: Type[ServeEntity],
|
||||||
|
resource_id: FileMetadataIdentifier,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Get query for identifier."""
|
||||||
|
session: Session = kwargs.get("session")
|
||||||
|
if session is None:
|
||||||
|
raise Exception("session is None")
|
||||||
|
return (
|
||||||
|
session.query(storage_format)
|
||||||
|
.filter(storage_format.bucket == resource_id.bucket)
|
||||||
|
.filter(storage_format.file_id == resource_id.file_id)
|
||||||
|
)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"""This is an auto-generated model file
|
||||||
|
You can define your own models and DAOs here
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text, UniqueConstraint
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
|
||||||
|
from ..api.schemas import ServeRequest, ServerResponse
|
||||||
|
from ..config import SERVER_APP_TABLE_NAME, ServeConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServeEntity(Model):
|
||||||
|
__tablename__ = SERVER_APP_TABLE_NAME
|
||||||
|
__table_args__ = (UniqueConstraint("bucket", "file_id", name="uk_bucket_file_id"),)
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, comment="Auto increment id")
|
||||||
|
|
||||||
|
bucket = Column(String(255), nullable=False, comment="Bucket name")
|
||||||
|
file_id = Column(String(255), nullable=False, comment="File id")
|
||||||
|
file_name = Column(String(256), nullable=False, comment="File name")
|
||||||
|
file_size = Column(Integer, nullable=True, comment="File size")
|
||||||
|
storage_type = Column(String(32), nullable=False, comment="Storage type")
|
||||||
|
storage_path = Column(String(512), nullable=False, comment="Storage path")
|
||||||
|
uri = Column(String(512), nullable=False, comment="File URI")
|
||||||
|
custom_metadata = Column(
|
||||||
|
Text, nullable=True, comment="Custom metadata, JSON format"
|
||||||
|
)
|
||||||
|
file_hash = Column(String(128), nullable=True, comment="File hash")
|
||||||
|
user_name = Column(String(128), index=True, nullable=True, comment="User name")
|
||||||
|
sys_code = Column(String(128), index=True, nullable=True, comment="System code")
|
||||||
|
gmt_created = Column(DateTime, default=datetime.now, comment="Record creation time")
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, comment="Record update time")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"ServeEntity(id={self.id}, gmt_created='{self.gmt_created}', "
|
||||||
|
f"gmt_modified='{self.gmt_modified}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServeDao(BaseDao[ServeEntity, ServeRequest, ServerResponse]):
|
||||||
|
"""The DAO class for File"""
|
||||||
|
|
||||||
|
def __init__(self, serve_config: ServeConfig):
|
||||||
|
super().__init__()
|
||||||
|
self._serve_config = serve_config
|
||||||
|
|
||||||
|
def from_request(self, request: Union[ServeRequest, Dict[str, Any]]) -> ServeEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = (
|
||||||
|
request.to_dict() if isinstance(request, ServeRequest) else request
|
||||||
|
)
|
||||||
|
entity = ServeEntity(**request_dict)
|
||||||
|
# TODO implement your own logic here, transfer the request_dict to an entity
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: ServeEntity) -> ServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a request
|
||||||
|
return ServeRequest()
|
||||||
|
|
||||||
|
def to_response(self, entity: ServeEntity) -> ServerResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RES: The response
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a response
|
||||||
|
return ServerResponse()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This is an auto-generated __init__.py file
|
||||||
|
# generated by `kamidso.engine.core new serve model`
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import json
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from kamidso.engine.core.core.interface.storage import StorageItemAdapter
|
||||||
|
from kamidso.engine.core.model.cluster.storage import ModelStorageIdentifier, ModelStorageItem
|
||||||
|
|
||||||
|
from .models import ServeEntity
|
||||||
|
|
||||||
|
|
||||||
|
class ModelStorageAdapter(StorageItemAdapter[ModelStorageItem, ServeEntity]):
|
||||||
|
"""File metadata adapter.
|
||||||
|
|
||||||
|
Convert between storage format and database model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def to_storage_format(self, item: ModelStorageItem) -> ServeEntity:
|
||||||
|
"""Convert to storage format."""
|
||||||
|
params = json.dumps(item.params, ensure_ascii=False)
|
||||||
|
enabled = 1 if item.enabled else 0
|
||||||
|
return ServeEntity(
|
||||||
|
host=item.host,
|
||||||
|
port=item.port,
|
||||||
|
model=item.model,
|
||||||
|
provider=item.provider,
|
||||||
|
worker_type=item.worker_type,
|
||||||
|
enabled=enabled,
|
||||||
|
worker_name=item.worker_name,
|
||||||
|
params=params,
|
||||||
|
description=item.description,
|
||||||
|
user_name=item.user_name,
|
||||||
|
sys_code=item.sys_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_storage_format(self, model: ServeEntity) -> ModelStorageItem:
|
||||||
|
"""Convert from storage format."""
|
||||||
|
params = json.loads(model.params)
|
||||||
|
enabled = True if model.enabled else False
|
||||||
|
return ModelStorageItem(
|
||||||
|
host=model.host,
|
||||||
|
port=model.port,
|
||||||
|
model=model.model,
|
||||||
|
provider=model.provider,
|
||||||
|
worker_type=model.worker_type,
|
||||||
|
enabled=enabled,
|
||||||
|
worker_name=model.worker_name,
|
||||||
|
params=params,
|
||||||
|
description=model.description,
|
||||||
|
user_name=model.user_name,
|
||||||
|
sys_code=model.sys_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_query_for_identifier(
|
||||||
|
self,
|
||||||
|
storage_format: Type[ServeEntity],
|
||||||
|
resource_id: ModelStorageIdentifier,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Get query for identifier."""
|
||||||
|
session: Session = kwargs.get("session")
|
||||||
|
if session is None:
|
||||||
|
raise Exception("session is None")
|
||||||
|
query = (
|
||||||
|
session.query(storage_format)
|
||||||
|
.filter(storage_format.model == resource_id.model)
|
||||||
|
.filter(storage_format.worker_type == resource_id.worker_type)
|
||||||
|
)
|
||||||
|
if resource_id.user_name:
|
||||||
|
query = query.filter(storage_format.user_name == resource_id.user_name)
|
||||||
|
if resource_id.sys_code:
|
||||||
|
query = query.filter(storage_format.sys_code == resource_id.sys_code)
|
||||||
|
return query
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""This is an auto-generated model file
|
||||||
|
You can define your own models and DAOs here
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text, UniqueConstraint
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
|
||||||
|
from ..api.schemas import ServeRequest, ServerResponse
|
||||||
|
from ..config import SERVER_APP_TABLE_NAME, ServeConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServeEntity(Model):
|
||||||
|
__tablename__ = SERVER_APP_TABLE_NAME
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"model", "provider", "worker_type", name="uk_model_provider_type"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
id = Column(Integer, primary_key=True, comment="Auto increment id")
|
||||||
|
|
||||||
|
host = Column(String(255), nullable=False, comment="The model worker host")
|
||||||
|
port = Column(Integer, nullable=False, comment="The model worker port")
|
||||||
|
model = Column(String(255), nullable=False, comment="The model name")
|
||||||
|
provider = Column(String(255), nullable=False, comment="The model provider")
|
||||||
|
worker_type = Column(String(255), nullable=False, comment="The worker type")
|
||||||
|
params = Column(Text, nullable=False, comment="The model parameters, JSON format")
|
||||||
|
enabled = Column(
|
||||||
|
Integer,
|
||||||
|
default=1,
|
||||||
|
nullable=True,
|
||||||
|
comment="Whether the model is enabled, if it is enabled, it will be started "
|
||||||
|
"when the system starts, 1 is enabled, 0 is disabled",
|
||||||
|
)
|
||||||
|
worker_name = Column(String(255), nullable=True, comment="The worker name")
|
||||||
|
description = Column(Text, nullable=True, comment="The model description")
|
||||||
|
user_name = Column(String(128), index=True, nullable=True, comment="User name")
|
||||||
|
sys_code = Column(String(128), index=True, nullable=True, comment="System code")
|
||||||
|
|
||||||
|
gmt_created = Column(DateTime, default=datetime.now, comment="Record creation time")
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, comment="Record update time")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"ServeEntity(id={self.id}, gmt_created='{self.gmt_created}', "
|
||||||
|
f"gmt_modified='{self.gmt_modified}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServeDao(BaseDao[ServeEntity, ServeRequest, ServerResponse]):
|
||||||
|
"""The DAO class for Model"""
|
||||||
|
|
||||||
|
def __init__(self, serve_config: ServeConfig):
|
||||||
|
super().__init__()
|
||||||
|
self._serve_config = serve_config
|
||||||
|
|
||||||
|
def from_request(self, request: Union[ServeRequest, Dict[str, Any]]) -> ServeEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = (
|
||||||
|
request.to_dict() if isinstance(request, ServeRequest) else request
|
||||||
|
)
|
||||||
|
entity = ServeEntity(**request_dict)
|
||||||
|
# TODO implement your own logic here, transfer the request_dict to an entity
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: ServeEntity) -> ServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a request
|
||||||
|
return ServeRequest()
|
||||||
|
|
||||||
|
def to_response(self, entity: ServeEntity) -> ServerResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RES: The response
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a response
|
||||||
|
return ServerResponse()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This is an auto-generated __init__.py file
|
||||||
|
# generated by `kamidso.engine.core new serve prompt`
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""This is an auto-generated model file
|
||||||
|
You can define your own models and DAOs here
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text, UniqueConstraint
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
|
||||||
|
from ..api.schemas import ServeRequest, ServerResponse
|
||||||
|
from ..config import ServeConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServeEntity(Model):
|
||||||
|
__tablename__ = "prompt_manage"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"prompt_name",
|
||||||
|
"sys_code",
|
||||||
|
"prompt_language",
|
||||||
|
"model",
|
||||||
|
name="uk_prompt_name_sys_code",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
id = Column(Integer, primary_key=True, comment="Auto increment id")
|
||||||
|
|
||||||
|
chat_scene = Column(String(100), comment="Chat scene")
|
||||||
|
sub_chat_scene = Column(String(100), comment="Sub chat scene")
|
||||||
|
prompt_code = Column(String(256), comment="Prompt Code")
|
||||||
|
prompt_type = Column(String(100), comment="Prompt type(eg: common, private)")
|
||||||
|
prompt_name = Column(String(256), comment="Prompt name")
|
||||||
|
content = Column(Text, comment="Prompt content")
|
||||||
|
input_variables = Column(
|
||||||
|
String(1024), nullable=True, comment="Prompt input variables(split by comma))"
|
||||||
|
)
|
||||||
|
response_schema = Column(Text, comment="Prompt response schema")
|
||||||
|
model = Column(
|
||||||
|
String(128),
|
||||||
|
nullable=True,
|
||||||
|
comment="Prompt model name(we can use different models for different prompt",
|
||||||
|
)
|
||||||
|
prompt_language = Column(
|
||||||
|
String(32), index=True, nullable=True, comment="Prompt language(eg:en, zh-cn)"
|
||||||
|
)
|
||||||
|
prompt_format = Column(
|
||||||
|
String(32),
|
||||||
|
index=True,
|
||||||
|
nullable=True,
|
||||||
|
default="f-string",
|
||||||
|
comment="Prompt format(eg: f-string, jinja2)",
|
||||||
|
)
|
||||||
|
prompt_desc = Column(String(512), nullable=True, comment="Prompt description")
|
||||||
|
user_code = Column(String(128), index=True, nullable=True, comment="User code")
|
||||||
|
user_name = Column(String(128), index=True, nullable=True, comment="User name")
|
||||||
|
sys_code = Column(String(128), index=True, nullable=True, comment="System code")
|
||||||
|
gmt_created = Column(DateTime, default=datetime.now, comment="Record creation time")
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, comment="Record update time")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"ServeEntity(id={self.id}, chat_scene='{self.chat_scene}', "
|
||||||
|
f"sub_chat_scene='{self.sub_chat_scene}', "
|
||||||
|
f"prompt_type='{self.prompt_type}', prompt_name='{self.prompt_name}', "
|
||||||
|
f"content='{self.content}',"
|
||||||
|
f"user_code='{self.user_code}', user_name='{self.user_name}', "
|
||||||
|
f"gmt_created='{self.gmt_created}', gmt_modified='{self.gmt_modified}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServeDao(BaseDao[ServeEntity, ServeRequest, ServerResponse]):
|
||||||
|
"""The DAO class for Prompt"""
|
||||||
|
|
||||||
|
def __init__(self, serve_config: ServeConfig):
|
||||||
|
super().__init__()
|
||||||
|
self._serve_config = serve_config
|
||||||
|
|
||||||
|
def from_request(self, request: Union[ServeRequest, Dict[str, Any]]) -> ServeEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = request.dict() if isinstance(request, ServeRequest) else request
|
||||||
|
entity = ServeEntity(**request_dict)
|
||||||
|
if not entity.prompt_code:
|
||||||
|
entity.prompt_code = uuid.uuid4().hex
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: ServeEntity) -> ServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
return ServeRequest(
|
||||||
|
chat_scene=entity.chat_scene,
|
||||||
|
sub_chat_scene=entity.sub_chat_scene,
|
||||||
|
prompt_type=entity.prompt_type,
|
||||||
|
prompt_name=entity.prompt_name,
|
||||||
|
prompt_code=entity.prompt_code,
|
||||||
|
content=entity.content,
|
||||||
|
response_schema=entity.response_schema,
|
||||||
|
input_variables=entity.input_variables,
|
||||||
|
model=entity.model,
|
||||||
|
prompt_language=entity.prompt_language,
|
||||||
|
prompt_desc=entity.prompt_desc,
|
||||||
|
user_code=entity.user_code,
|
||||||
|
user_name=entity.user_name,
|
||||||
|
sys_code=entity.sys_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_response(self, entity: ServeEntity) -> ServerResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RES: The response
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a response
|
||||||
|
gmt_created_str = entity.gmt_created.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
gmt_modified_str = entity.gmt_modified.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
return ServerResponse(
|
||||||
|
id=entity.id,
|
||||||
|
chat_scene=entity.chat_scene,
|
||||||
|
sub_chat_scene=entity.sub_chat_scene,
|
||||||
|
prompt_code=entity.prompt_code,
|
||||||
|
prompt_type=entity.prompt_type,
|
||||||
|
prompt_name=entity.prompt_name,
|
||||||
|
content=entity.content,
|
||||||
|
prompt_desc=entity.prompt_desc,
|
||||||
|
user_name=entity.user_name,
|
||||||
|
user_code=entity.user_code,
|
||||||
|
model=entity.model,
|
||||||
|
input_variables=entity.input_variables,
|
||||||
|
response_schema=entity.response_schema,
|
||||||
|
prompt_language=entity.prompt_language,
|
||||||
|
sys_code=entity.sys_code,
|
||||||
|
gmt_created=gmt_created_str,
|
||||||
|
gmt_modified=gmt_modified_str,
|
||||||
|
)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from kamidso.engine.core.core.interface.prompt import PromptTemplateIdentifier, StoragePromptTemplate
|
||||||
|
from kamidso.engine.core.core.interface.storage import StorageItemAdapter
|
||||||
|
|
||||||
|
from .models import ServeEntity
|
||||||
|
|
||||||
|
|
||||||
|
class PromptTemplateAdapter(StorageItemAdapter[StoragePromptTemplate, ServeEntity]):
|
||||||
|
def to_storage_format(self, item: StoragePromptTemplate) -> ServeEntity:
|
||||||
|
return ServeEntity(
|
||||||
|
chat_scene=item.chat_scene,
|
||||||
|
sub_chat_scene=item.sub_chat_scene,
|
||||||
|
prompt_type=item.prompt_type,
|
||||||
|
prompt_name=item.prompt_name,
|
||||||
|
content=item.content,
|
||||||
|
input_variables=item.input_variables,
|
||||||
|
model=item.model,
|
||||||
|
prompt_language=item.prompt_language,
|
||||||
|
prompt_format=item.prompt_format,
|
||||||
|
user_name=item.user_name,
|
||||||
|
sys_code=item.sys_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_storage_format(self, model: ServeEntity) -> StoragePromptTemplate:
|
||||||
|
return StoragePromptTemplate(
|
||||||
|
chat_scene=model.chat_scene,
|
||||||
|
sub_chat_scene=model.sub_chat_scene,
|
||||||
|
prompt_type=model.prompt_type,
|
||||||
|
prompt_name=model.prompt_name,
|
||||||
|
content=model.content,
|
||||||
|
input_variables=model.input_variables,
|
||||||
|
model=model.model,
|
||||||
|
prompt_language=model.prompt_language,
|
||||||
|
prompt_format=model.prompt_format,
|
||||||
|
user_name=model.user_name,
|
||||||
|
sys_code=model.sys_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_query_for_identifier(
|
||||||
|
self,
|
||||||
|
storage_format: Type[ServeEntity],
|
||||||
|
resource_id: PromptTemplateIdentifier,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
session: Session = kwargs.get("session")
|
||||||
|
if session is None:
|
||||||
|
raise Exception("session is None")
|
||||||
|
query_obj = session.query(ServeEntity)
|
||||||
|
for key, value in resource_id.to_dict().items():
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
query_obj = query_obj.filter(getattr(ServeEntity, key) == value)
|
||||||
|
return query_obj
|
||||||
Whitespace-only changes.
@@ -0,0 +1,278 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text, func, not_
|
||||||
|
|
||||||
|
from kamidso.engine.core._private.pydantic import model_to_dict
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
from kamidso.engine.core.storage.metadata._base_dao import QUERY_SPEC, REQ, RES
|
||||||
|
from kamidso.engine.serve.rag.api.schemas import ChunkServeRequest, ChunkServeResponse
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentChunkEntity(Model):
|
||||||
|
__tablename__ = "document_chunk"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
document_id = Column(Integer)
|
||||||
|
doc_name = Column(String(100))
|
||||||
|
doc_type = Column(String(100))
|
||||||
|
content = Column(Text)
|
||||||
|
questions = Column(Text)
|
||||||
|
meta_info = Column(String(500))
|
||||||
|
gmt_created = Column(DateTime)
|
||||||
|
gmt_modified = Column(DateTime)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"DocumentChunkEntity(id={self.id}, doc_name='{self.doc_name}', "
|
||||||
|
f"doc_type='{self.doc_type}', "
|
||||||
|
f"document_id='{self.document_id}', content='{self.content}', "
|
||||||
|
f"questions='{self.questions}', meta_info='{self.meta_info}', "
|
||||||
|
f"gmt_created='{self.gmt_created}', gmt_modified='{self.gmt_modified}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"document_id": self.document_id,
|
||||||
|
"doc_name": self.doc_name,
|
||||||
|
"doc_type": self.doc_type,
|
||||||
|
"content": self.content,
|
||||||
|
"questions": self.questions,
|
||||||
|
"meta_info": self.meta_info,
|
||||||
|
"gmt_created": self.gmt_created,
|
||||||
|
"gmt_modified": self.gmt_modified,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentChunkDao(BaseDao):
|
||||||
|
def create_documents_chunks(self, documents: List):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
docs = [
|
||||||
|
DocumentChunkEntity(
|
||||||
|
doc_name=document.doc_name,
|
||||||
|
doc_type=document.doc_type,
|
||||||
|
document_id=document.document_id,
|
||||||
|
content=document.content or "",
|
||||||
|
meta_info=document.meta_info or "",
|
||||||
|
gmt_created=datetime.now(),
|
||||||
|
gmt_modified=datetime.now(),
|
||||||
|
)
|
||||||
|
for document in documents
|
||||||
|
]
|
||||||
|
session.add_all(docs)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_document_chunks(
|
||||||
|
self, query: DocumentChunkEntity, page=1, page_size=20, document_ids=None
|
||||||
|
):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
document_chunks = session.query(DocumentChunkEntity)
|
||||||
|
if query.id is not None:
|
||||||
|
document_chunks = document_chunks.filter(DocumentChunkEntity.id == query.id)
|
||||||
|
if query.document_id is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.document_id == query.document_id
|
||||||
|
)
|
||||||
|
if query.doc_type is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.doc_type == query.doc_type
|
||||||
|
)
|
||||||
|
if query.content is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.content.like(f"%{query.content}%")
|
||||||
|
)
|
||||||
|
if query.doc_name is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.doc_name == query.doc_name
|
||||||
|
)
|
||||||
|
if query.meta_info is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.meta_info == query.meta_info
|
||||||
|
)
|
||||||
|
if document_ids is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.document_id.in_(document_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
document_chunks = document_chunks.order_by(DocumentChunkEntity.id.asc())
|
||||||
|
document_chunks = document_chunks.offset((page - 1) * page_size).limit(
|
||||||
|
page_size
|
||||||
|
)
|
||||||
|
result = document_chunks.all()
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_chunks_with_questions(self, query: DocumentChunkEntity, document_ids=None):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
document_chunks = session.query(DocumentChunkEntity)
|
||||||
|
if query.doc_name is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.doc_name == query.doc_name
|
||||||
|
)
|
||||||
|
if query.meta_info is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.meta_info == query.meta_info
|
||||||
|
)
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
not_(DocumentChunkEntity.questions is None)
|
||||||
|
)
|
||||||
|
if document_ids is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.document_id.in_(document_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
document_chunks = document_chunks.order_by(DocumentChunkEntity.id.asc())
|
||||||
|
result = document_chunks.all()
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update(self, query_request: QUERY_SPEC, update_request: REQ) -> RES:
|
||||||
|
"""Update an entity object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query_request (REQ): The request schema object or dict for query.
|
||||||
|
update_request (REQ): The request schema object for update.
|
||||||
|
Returns:
|
||||||
|
RES: The response schema object.
|
||||||
|
"""
|
||||||
|
with self.session() as session:
|
||||||
|
query = self._create_query_object(session, query_request)
|
||||||
|
entry = query.first()
|
||||||
|
if entry is None:
|
||||||
|
raise Exception("Invalid request")
|
||||||
|
for key, value in model_to_dict(update_request).items(): # type: ignore
|
||||||
|
if value is not None:
|
||||||
|
if key in ["gmt_created", "gmt_modified"]:
|
||||||
|
# Assuming the datetime format is 'YYYY-MM-DD HH:MM:SS'
|
||||||
|
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||||
|
setattr(entry, key, value)
|
||||||
|
session.merge(entry)
|
||||||
|
return self.to_response(entry)
|
||||||
|
|
||||||
|
def update_chunk(self, chunk: DocumentChunkEntity):
|
||||||
|
"""Update a chunk"""
|
||||||
|
try:
|
||||||
|
session = self.get_raw_session()
|
||||||
|
updated = session.merge(chunk)
|
||||||
|
session.commit()
|
||||||
|
return updated.id
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_document_chunks_count(self, query: DocumentChunkEntity):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
document_chunks = session.query(func.count(DocumentChunkEntity.id))
|
||||||
|
if query.id is not None:
|
||||||
|
document_chunks = document_chunks.filter(DocumentChunkEntity.id == query.id)
|
||||||
|
if query.document_id is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.document_id == query.document_id
|
||||||
|
)
|
||||||
|
if query.doc_type is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.doc_type == query.doc_type
|
||||||
|
)
|
||||||
|
if query.doc_name is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.doc_name == query.doc_name
|
||||||
|
)
|
||||||
|
if query.meta_info is not None:
|
||||||
|
document_chunks = document_chunks.filter(
|
||||||
|
DocumentChunkEntity.meta_info == query.meta_info
|
||||||
|
)
|
||||||
|
count = document_chunks.scalar()
|
||||||
|
session.close()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def raw_delete(self, document_id: int):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
if document_id is None:
|
||||||
|
raise Exception("document_id is None")
|
||||||
|
query = DocumentChunkEntity(document_id=document_id)
|
||||||
|
knowledge_documents = session.query(DocumentChunkEntity)
|
||||||
|
if query.document_id is not None:
|
||||||
|
chunks = knowledge_documents.filter(
|
||||||
|
DocumentChunkEntity.document_id == query.document_id
|
||||||
|
)
|
||||||
|
chunks.delete()
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def from_request(
|
||||||
|
self, request: Union[ChunkServeRequest, Dict[str, Any]]
|
||||||
|
) -> DocumentChunkEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = (
|
||||||
|
request.dict() if isinstance(request, ChunkServeRequest) else request
|
||||||
|
)
|
||||||
|
entity = DocumentChunkEntity(**request_dict)
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: DocumentChunkEntity) -> ChunkServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
return ChunkServeRequest(
|
||||||
|
id=entity.id,
|
||||||
|
doc_name=entity.doc_name,
|
||||||
|
doc_type=entity.doc_type,
|
||||||
|
document_id=entity.document_id,
|
||||||
|
content=entity.content,
|
||||||
|
questions=entity.questions,
|
||||||
|
meta_info=entity.meta_info,
|
||||||
|
gmt_created=entity.gmt_created,
|
||||||
|
gmt_modified=entity.gmt_modified,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_response(self, entity: DocumentChunkEntity) -> ChunkServeResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
gmt_created_str = entity.gmt_created.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
gmt_modified_str = entity.gmt_modified.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
return ChunkServeResponse(
|
||||||
|
id=entity.id,
|
||||||
|
doc_name=entity.doc_name,
|
||||||
|
doc_type=entity.doc_type,
|
||||||
|
document_id=entity.document_id,
|
||||||
|
content=entity.content,
|
||||||
|
questions=entity.questions,
|
||||||
|
meta_info=entity.meta_info,
|
||||||
|
gmt_created=gmt_created_str,
|
||||||
|
gmt_modified=gmt_modified_str,
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_response(
|
||||||
|
self, response: Union[ChunkServeResponse, Dict[str, Any]]
|
||||||
|
) -> DocumentChunkEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
response_dict = (
|
||||||
|
response.dict() if isinstance(response, ChunkServeResponse) else response
|
||||||
|
)
|
||||||
|
entity = DocumentChunkEntity(**response_dict)
|
||||||
|
return entity
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
"""Code graph persistence models.
|
||||||
|
|
||||||
|
Three tables:
|
||||||
|
- code_graph_vertex: AST-extracted code nodes (classes, functions, modules, etc.)
|
||||||
|
- code_graph_edge: Structural relationships (contains, calls, imports, etc.)
|
||||||
|
- code_graph_meta: Per-knowledge-space graph metadata (counts, build info)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text, func
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Props that are extracted to dedicated columns (not stored in JSON props)
|
||||||
|
_VERTEX_PROPS_COLUMNS = {"node_type", "source_file", "language", "community"}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Vertex
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class CodeGraphVertexEntity(Model):
|
||||||
|
"""Code graph vertex (node) entity."""
|
||||||
|
|
||||||
|
__tablename__ = "code_graph_vertex"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
knowledge_id = Column(String(100), nullable=False)
|
||||||
|
vid = Column(String(500), nullable=False)
|
||||||
|
name = Column(String(500), nullable=False)
|
||||||
|
node_type = Column(String(50), nullable=False, default="")
|
||||||
|
source_file = Column(String(500), default="")
|
||||||
|
language = Column(String(30), default="")
|
||||||
|
community = Column(String(50), default="")
|
||||||
|
props = Column(Text, default=None)
|
||||||
|
gmt_create = Column(DateTime, default=datetime.now)
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"CodeGraphVertexEntity(id={self.id}, vid='{self.vid}', "
|
||||||
|
f"name='{self.name}', node_type='{self.node_type}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"knowledge_id": self.knowledge_id,
|
||||||
|
"vid": self.vid,
|
||||||
|
"name": self.name,
|
||||||
|
"node_type": self.node_type,
|
||||||
|
"source_file": self.source_file,
|
||||||
|
"language": self.language,
|
||||||
|
"community": self.community,
|
||||||
|
"props": self.props,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CodeGraphVertexDao(BaseDao):
|
||||||
|
"""DAO for code_graph_vertex table."""
|
||||||
|
|
||||||
|
def batch_insert(self, knowledge_id: str, vertices: list) -> int:
|
||||||
|
"""Bulk insert vertices for a knowledge space."""
|
||||||
|
if not vertices:
|
||||||
|
return 0
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
current_time = datetime.now()
|
||||||
|
batch_size = 200
|
||||||
|
for i in range(0, len(vertices), batch_size):
|
||||||
|
batch = vertices[i : i + batch_size]
|
||||||
|
mappings = [
|
||||||
|
{
|
||||||
|
"knowledge_id": knowledge_id,
|
||||||
|
"vid": v["vid"],
|
||||||
|
"name": v["name"],
|
||||||
|
"node_type": v.get("node_type", ""),
|
||||||
|
"source_file": v.get("source_file", ""),
|
||||||
|
"language": v.get("language", ""),
|
||||||
|
"community": v.get("community", ""),
|
||||||
|
"props": v.get("props"),
|
||||||
|
"gmt_create": current_time,
|
||||||
|
"gmt_modified": current_time,
|
||||||
|
}
|
||||||
|
for v in batch
|
||||||
|
]
|
||||||
|
session.bulk_insert_mappings(CodeGraphVertexEntity, mappings)
|
||||||
|
session.commit()
|
||||||
|
return len(vertices)
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_by_knowledge_id(self, knowledge_id: str) -> List[CodeGraphVertexEntity]:
|
||||||
|
"""Get all vertices for a knowledge space."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
session.query(CodeGraphVertexEntity)
|
||||||
|
.filter(CodeGraphVertexEntity.knowledge_id == knowledge_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def search_by_name(
|
||||||
|
self,
|
||||||
|
knowledge_id: str,
|
||||||
|
name_prefix: str,
|
||||||
|
node_type: Optional[str] = None,
|
||||||
|
limit: int = 20,
|
||||||
|
) -> List[CodeGraphVertexEntity]:
|
||||||
|
"""Search vertices by name prefix (uses index)."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
query = session.query(CodeGraphVertexEntity).filter(
|
||||||
|
CodeGraphVertexEntity.knowledge_id == knowledge_id,
|
||||||
|
CodeGraphVertexEntity.name.like(f"{name_prefix}%"),
|
||||||
|
)
|
||||||
|
if node_type:
|
||||||
|
query = query.filter(CodeGraphVertexEntity.node_type == node_type)
|
||||||
|
return query.limit(limit).all()
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_by_source_file(
|
||||||
|
self, knowledge_id: str, source_file: str
|
||||||
|
) -> List[CodeGraphVertexEntity]:
|
||||||
|
"""Get all vertices in a specific source file."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
session.query(CodeGraphVertexEntity)
|
||||||
|
.filter(
|
||||||
|
CodeGraphVertexEntity.knowledge_id == knowledge_id,
|
||||||
|
CodeGraphVertexEntity.source_file == source_file,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_by_vid(
|
||||||
|
self, knowledge_id: str, vid: str
|
||||||
|
) -> Optional[CodeGraphVertexEntity]:
|
||||||
|
"""Get a single vertex by its vid."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
session.query(CodeGraphVertexEntity)
|
||||||
|
.filter(
|
||||||
|
CodeGraphVertexEntity.knowledge_id == knowledge_id,
|
||||||
|
CodeGraphVertexEntity.vid == vid,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def delete_by_knowledge_id(self, knowledge_id: str) -> int:
|
||||||
|
"""Delete all vertices for a knowledge space. Returns count deleted."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
count = (
|
||||||
|
session.query(CodeGraphVertexEntity)
|
||||||
|
.filter(CodeGraphVertexEntity.knowledge_id == knowledge_id)
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
return count
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def count_by_knowledge_id(self, knowledge_id: str) -> int:
|
||||||
|
"""Count vertices for a knowledge space."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
result = (
|
||||||
|
session.query(func.count(CodeGraphVertexEntity.id))
|
||||||
|
.filter(CodeGraphVertexEntity.knowledge_id == knowledge_id)
|
||||||
|
.scalar()
|
||||||
|
)
|
||||||
|
return result or 0
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Edge
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class CodeGraphEdgeEntity(Model):
|
||||||
|
"""Code graph edge (relationship) entity."""
|
||||||
|
|
||||||
|
__tablename__ = "code_graph_edge"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
knowledge_id = Column(String(100), nullable=False)
|
||||||
|
sid = Column(String(500), nullable=False)
|
||||||
|
tid = Column(String(500), nullable=False)
|
||||||
|
edge_type = Column(String(50), nullable=False, default="references")
|
||||||
|
confidence = Column(String(20), default="EXTRACTED")
|
||||||
|
source_file = Column(String(500), default="")
|
||||||
|
source_location = Column(String(30), default="")
|
||||||
|
props = Column(Text, default=None)
|
||||||
|
gmt_create = Column(DateTime, default=datetime.now)
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"CodeGraphEdgeEntity(id={self.id}, sid='{self.sid}', "
|
||||||
|
f"tid='{self.tid}', edge_type='{self.edge_type}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"knowledge_id": self.knowledge_id,
|
||||||
|
"sid": self.sid,
|
||||||
|
"tid": self.tid,
|
||||||
|
"edge_type": self.edge_type,
|
||||||
|
"confidence": self.confidence,
|
||||||
|
"source_file": self.source_file,
|
||||||
|
"source_location": self.source_location,
|
||||||
|
"props": self.props,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CodeGraphEdgeDao(BaseDao):
|
||||||
|
"""DAO for code_graph_edge table."""
|
||||||
|
|
||||||
|
def batch_insert(self, knowledge_id: str, edges: list) -> int:
|
||||||
|
"""Bulk insert edges for a knowledge space."""
|
||||||
|
if not edges:
|
||||||
|
return 0
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
current_time = datetime.now()
|
||||||
|
batch_size = 200
|
||||||
|
for i in range(0, len(edges), batch_size):
|
||||||
|
batch = edges[i : i + batch_size]
|
||||||
|
mappings = [
|
||||||
|
{
|
||||||
|
"knowledge_id": knowledge_id,
|
||||||
|
"sid": e["sid"],
|
||||||
|
"tid": e["tid"],
|
||||||
|
"edge_type": e["edge_type"],
|
||||||
|
"confidence": e.get("confidence", "EXTRACTED"),
|
||||||
|
"source_file": e.get("source_file", ""),
|
||||||
|
"source_location": e.get("source_location", ""),
|
||||||
|
"props": e.get("props"),
|
||||||
|
"gmt_create": current_time,
|
||||||
|
"gmt_modified": current_time,
|
||||||
|
}
|
||||||
|
for e in batch
|
||||||
|
]
|
||||||
|
session.bulk_insert_mappings(CodeGraphEdgeEntity, mappings)
|
||||||
|
session.commit()
|
||||||
|
return len(edges)
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_by_knowledge_id(self, knowledge_id: str) -> List[CodeGraphEdgeEntity]:
|
||||||
|
"""Get all edges for a knowledge space."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
session.query(CodeGraphEdgeEntity)
|
||||||
|
.filter(CodeGraphEdgeEntity.knowledge_id == knowledge_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_out_edges(
|
||||||
|
self, knowledge_id: str, sid: str, edge_type: Optional[str] = None
|
||||||
|
) -> List[CodeGraphEdgeEntity]:
|
||||||
|
"""Get outgoing edges from a vertex (sid matches)."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
query = session.query(CodeGraphEdgeEntity).filter(
|
||||||
|
CodeGraphEdgeEntity.knowledge_id == knowledge_id,
|
||||||
|
CodeGraphEdgeEntity.sid == sid,
|
||||||
|
)
|
||||||
|
if edge_type:
|
||||||
|
query = query.filter(CodeGraphEdgeEntity.edge_type == edge_type)
|
||||||
|
return query.all()
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_in_edges(
|
||||||
|
self, knowledge_id: str, tid: str, edge_type: Optional[str] = None
|
||||||
|
) -> List[CodeGraphEdgeEntity]:
|
||||||
|
"""Get incoming edges to a vertex (tid matches)."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
query = session.query(CodeGraphEdgeEntity).filter(
|
||||||
|
CodeGraphEdgeEntity.knowledge_id == knowledge_id,
|
||||||
|
CodeGraphEdgeEntity.tid == tid,
|
||||||
|
)
|
||||||
|
if edge_type:
|
||||||
|
query = query.filter(CodeGraphEdgeEntity.edge_type == edge_type)
|
||||||
|
return query.all()
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def delete_by_knowledge_id(self, knowledge_id: str) -> int:
|
||||||
|
"""Delete all edges for a knowledge space. Returns count deleted."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
count = (
|
||||||
|
session.query(CodeGraphEdgeEntity)
|
||||||
|
.filter(CodeGraphEdgeEntity.knowledge_id == knowledge_id)
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
return count
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def count_by_knowledge_id(self, knowledge_id: str) -> int:
|
||||||
|
"""Count edges for a knowledge space."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
result = (
|
||||||
|
session.query(func.count(CodeGraphEdgeEntity.id))
|
||||||
|
.filter(CodeGraphEdgeEntity.knowledge_id == knowledge_id)
|
||||||
|
.scalar()
|
||||||
|
)
|
||||||
|
return result or 0
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Meta
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class CodeGraphMetaEntity(Model):
|
||||||
|
"""Code graph metadata entity — one row per knowledge space."""
|
||||||
|
|
||||||
|
__tablename__ = "code_graph_meta"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
knowledge_id = Column(String(100), nullable=False, unique=True)
|
||||||
|
vertex_count = Column(Integer, default=0)
|
||||||
|
edge_count = Column(Integer, default=0)
|
||||||
|
community_count = Column(Integer, default=0)
|
||||||
|
build_source = Column(String(20), default="")
|
||||||
|
repo_url = Column(String(500), default="")
|
||||||
|
branch = Column(String(100), default="")
|
||||||
|
build_status = Column(String(20), default="completed")
|
||||||
|
graph_version = Column(Integer, default=1)
|
||||||
|
gmt_create = Column(DateTime, default=datetime.now)
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"CodeGraphMetaEntity(id={self.id}, knowledge_id='{self.knowledge_id}', "
|
||||||
|
f"vertices={self.vertex_count}, edges={self.edge_count})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"knowledge_id": self.knowledge_id,
|
||||||
|
"vertex_count": self.vertex_count,
|
||||||
|
"edge_count": self.edge_count,
|
||||||
|
"community_count": self.community_count,
|
||||||
|
"build_source": self.build_source,
|
||||||
|
"repo_url": self.repo_url,
|
||||||
|
"branch": self.branch,
|
||||||
|
"build_status": self.build_status,
|
||||||
|
"graph_version": self.graph_version,
|
||||||
|
"gmt_create": self.gmt_create.isoformat() if self.gmt_create else None,
|
||||||
|
"gmt_modified": (
|
||||||
|
self.gmt_modified.isoformat() if self.gmt_modified else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CodeGraphMetaDao(BaseDao):
|
||||||
|
"""DAO for code_graph_meta table."""
|
||||||
|
|
||||||
|
def get_by_knowledge_id(self, knowledge_id: str) -> Optional[CodeGraphMetaEntity]:
|
||||||
|
"""Get meta for a knowledge space."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
session.query(CodeGraphMetaEntity)
|
||||||
|
.filter(CodeGraphMetaEntity.knowledge_id == knowledge_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def upsert(self, meta: CodeGraphMetaEntity) -> int:
|
||||||
|
"""Insert or update meta for a knowledge space.
|
||||||
|
|
||||||
|
Returns the row ID.
|
||||||
|
"""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
existing = (
|
||||||
|
session.query(CodeGraphMetaEntity)
|
||||||
|
.filter(CodeGraphMetaEntity.knowledge_id == meta.knowledge_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
existing.vertex_count = meta.vertex_count
|
||||||
|
existing.edge_count = meta.edge_count
|
||||||
|
existing.community_count = meta.community_count
|
||||||
|
existing.build_source = meta.build_source
|
||||||
|
existing.repo_url = meta.repo_url
|
||||||
|
existing.branch = meta.branch
|
||||||
|
existing.build_status = meta.build_status
|
||||||
|
existing.graph_version = meta.graph_version
|
||||||
|
existing.gmt_modified = datetime.now()
|
||||||
|
session.commit()
|
||||||
|
return existing.id
|
||||||
|
else:
|
||||||
|
meta.gmt_create = datetime.now()
|
||||||
|
meta.gmt_modified = datetime.now()
|
||||||
|
session.add(meta)
|
||||||
|
session.commit()
|
||||||
|
return meta.id
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def delete_by_knowledge_id(self, knowledge_id: str) -> bool:
|
||||||
|
"""Delete meta for a knowledge space."""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
try:
|
||||||
|
count = (
|
||||||
|
session.query(CodeGraphMetaEntity)
|
||||||
|
.filter(CodeGraphMetaEntity.knowledge_id == knowledge_id)
|
||||||
|
.delete()
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
return count > 0
|
||||||
|
except Exception:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text, func
|
||||||
|
|
||||||
|
from kamidso.engine.core._private.config import Config
|
||||||
|
from kamidso.engine.core._private.pydantic import model_to_dict
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
from kamidso.engine.core.storage.metadata._base_dao import QUERY_SPEC, REQ, RES
|
||||||
|
from kamidso.engine.core.util import PaginationResult
|
||||||
|
from kamidso.engine.serve.conversation.api.schemas import ServeRequest
|
||||||
|
from kamidso.engine.serve.rag.api.schemas import (
|
||||||
|
DocumentServeRequest,
|
||||||
|
DocumentServeResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
CFG = Config()
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeDocumentEntity(Model):
|
||||||
|
__tablename__ = "knowledge_document"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
doc_name = Column(String(100))
|
||||||
|
doc_type = Column(String(100))
|
||||||
|
doc_token = Column(String(100))
|
||||||
|
space = Column(String(100))
|
||||||
|
chunk_size = Column(Integer)
|
||||||
|
status = Column(String(100))
|
||||||
|
last_sync = Column(DateTime)
|
||||||
|
content = Column(Text)
|
||||||
|
result = Column(Text)
|
||||||
|
vector_ids = Column(Text)
|
||||||
|
summary = Column(Text)
|
||||||
|
gmt_created = Column(DateTime)
|
||||||
|
gmt_modified = Column(DateTime)
|
||||||
|
questions = Column(Text)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"KnowledgeDocumentEntity(id={self.id}, doc_name='{self.doc_name}', "
|
||||||
|
f"doc_type='{self.doc_type}', chunk_size='{self.chunk_size}', "
|
||||||
|
f"status='{self.status}', last_sync='{self.last_sync}', "
|
||||||
|
f"content='{self.content}', "
|
||||||
|
f"result='{self.result}', summary='{self.summary}', "
|
||||||
|
f"gmt_created='{self.gmt_created}', gmt_modified='{self.gmt_modified}', "
|
||||||
|
f"questions='{self.questions}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"__tablename__": self.__tablename__,
|
||||||
|
"id": self.id,
|
||||||
|
"doc_name": self.doc_name,
|
||||||
|
"doc_type": self.doc_type,
|
||||||
|
"doc_token": self.doc_token,
|
||||||
|
"space": self.space,
|
||||||
|
"chunk_size": self.chunk_size,
|
||||||
|
"status": self.status,
|
||||||
|
"last_sync": self.last_sync,
|
||||||
|
"content": self.content,
|
||||||
|
"result": self.result,
|
||||||
|
"vector_ids": self.vector_ids,
|
||||||
|
"summary": self.summary,
|
||||||
|
"gmt_create": self.gmt_created,
|
||||||
|
"gmt_modified": self.gmt_modified,
|
||||||
|
"questions": self.questions,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeDocumentDao(BaseDao):
|
||||||
|
def create_knowledge_document(self, document: KnowledgeDocumentEntity):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
knowledge_document = KnowledgeDocumentEntity(
|
||||||
|
doc_name=document.doc_name,
|
||||||
|
doc_type=document.doc_type,
|
||||||
|
doc_token=document.doc_token,
|
||||||
|
space=document.space,
|
||||||
|
chunk_size=0.0,
|
||||||
|
status=document.status,
|
||||||
|
last_sync=document.last_sync,
|
||||||
|
content=document.content or "",
|
||||||
|
result=document.result or "",
|
||||||
|
summary=document.summary or "",
|
||||||
|
vector_ids=document.vector_ids,
|
||||||
|
gmt_created=datetime.now(),
|
||||||
|
gmt_modified=datetime.now(),
|
||||||
|
questions=document.questions,
|
||||||
|
)
|
||||||
|
session.add(knowledge_document)
|
||||||
|
session.commit()
|
||||||
|
doc_id = knowledge_document.id
|
||||||
|
session.close()
|
||||||
|
return doc_id
|
||||||
|
|
||||||
|
def get_knowledge_documents(self, query, page=1, page_size=20):
|
||||||
|
"""Get a list of documents that match the given query.
|
||||||
|
Args:
|
||||||
|
query: A KnowledgeDocumentEntity object containing the query parameters.
|
||||||
|
page: The page number to return.
|
||||||
|
page_size: The number of documents to return per page.
|
||||||
|
"""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
print(f"current session:{session}")
|
||||||
|
knowledge_documents = session.query(KnowledgeDocumentEntity)
|
||||||
|
if query.id is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.id == query.id
|
||||||
|
)
|
||||||
|
if query.doc_name is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.doc_name == query.doc_name
|
||||||
|
)
|
||||||
|
if query.doc_type is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.doc_type == query.doc_type
|
||||||
|
)
|
||||||
|
if query.space is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.space == query.space
|
||||||
|
)
|
||||||
|
if query.status is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.status == query.status
|
||||||
|
)
|
||||||
|
|
||||||
|
knowledge_documents = knowledge_documents.order_by(
|
||||||
|
KnowledgeDocumentEntity.id.desc()
|
||||||
|
)
|
||||||
|
knowledge_documents = knowledge_documents.offset((page - 1) * page_size).limit(
|
||||||
|
page_size
|
||||||
|
)
|
||||||
|
result = knowledge_documents.all()
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def document_by_id(self, document_id) -> KnowledgeDocumentEntity:
|
||||||
|
session = self.get_raw_session()
|
||||||
|
query = session.query(KnowledgeDocumentEntity).filter(
|
||||||
|
KnowledgeDocumentEntity.id == document_id
|
||||||
|
)
|
||||||
|
|
||||||
|
result = query.first()
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def documents_by_ids(self, ids) -> List[KnowledgeDocumentEntity]:
|
||||||
|
"""Get a list of documents by their IDs.
|
||||||
|
Args:
|
||||||
|
ids: A list of document IDs.
|
||||||
|
Returns:
|
||||||
|
A list of KnowledgeDocumentEntity objects.
|
||||||
|
"""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
print(f"current session:{session}")
|
||||||
|
knowledge_documents = session.query(KnowledgeDocumentEntity)
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.id.in_(ids)
|
||||||
|
)
|
||||||
|
result = knowledge_documents.all()
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_documents(self, query):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
print(f"current session:{session}")
|
||||||
|
knowledge_documents = session.query(KnowledgeDocumentEntity)
|
||||||
|
if query.id is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.id == query.id
|
||||||
|
)
|
||||||
|
if query.doc_name is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.doc_name == query.doc_name
|
||||||
|
)
|
||||||
|
if query.doc_type is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.doc_type == query.doc_type
|
||||||
|
)
|
||||||
|
if query.space is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.space == query.space
|
||||||
|
)
|
||||||
|
if query.status is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.status == query.status
|
||||||
|
)
|
||||||
|
|
||||||
|
knowledge_documents = knowledge_documents.order_by(
|
||||||
|
KnowledgeDocumentEntity.id.desc()
|
||||||
|
)
|
||||||
|
result = knowledge_documents.all()
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_knowledge_documents_count_bulk(self, space_names):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
"""
|
||||||
|
Perform a batch query to count the number of documents for each knowledge space.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
space_names: A list of knowledge space names to query for document counts.
|
||||||
|
session: A SQLAlchemy session object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary mapping each space name to its document count.
|
||||||
|
"""
|
||||||
|
counts_query = (
|
||||||
|
session.query(
|
||||||
|
KnowledgeDocumentEntity.space,
|
||||||
|
func.count(KnowledgeDocumentEntity.id).label("document_count"),
|
||||||
|
)
|
||||||
|
.filter(KnowledgeDocumentEntity.space.in_(space_names))
|
||||||
|
.group_by(KnowledgeDocumentEntity.space)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = counts_query.all()
|
||||||
|
docs_count = {result.space: result.document_count for result in results}
|
||||||
|
session.close()
|
||||||
|
return docs_count
|
||||||
|
|
||||||
|
def get_knowledge_documents_count_bulk_by_ids(self, spaces):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
"""
|
||||||
|
Perform a batch query to count the number of documents for each knowledge space.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spaces: A list of knowledge space names to query for document counts.
|
||||||
|
session: A SQLAlchemy session object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary mapping each space name to its document count.
|
||||||
|
"""
|
||||||
|
# build the group by query
|
||||||
|
counts_query = (
|
||||||
|
session.query(
|
||||||
|
KnowledgeDocumentEntity.space,
|
||||||
|
func.count(KnowledgeDocumentEntity.id).label("document_count"),
|
||||||
|
)
|
||||||
|
.filter(KnowledgeDocumentEntity.space.in_(spaces))
|
||||||
|
.group_by(KnowledgeDocumentEntity.space)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = counts_query.all()
|
||||||
|
docs_count = {result.space: result.document_count for result in results}
|
||||||
|
session.close()
|
||||||
|
return docs_count
|
||||||
|
|
||||||
|
def get_knowledge_documents_count(self, query):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
knowledge_documents = session.query(func.count(KnowledgeDocumentEntity.id))
|
||||||
|
if query.id is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.id == query.id
|
||||||
|
)
|
||||||
|
if query.doc_name is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.doc_name == query.doc_name
|
||||||
|
)
|
||||||
|
if query.doc_type is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.doc_type == query.doc_type
|
||||||
|
)
|
||||||
|
if query.space is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.space == query.space
|
||||||
|
)
|
||||||
|
if query.status is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.status == query.status
|
||||||
|
)
|
||||||
|
count = knowledge_documents.scalar()
|
||||||
|
session.close()
|
||||||
|
return count
|
||||||
|
|
||||||
|
def update(self, query_request: QUERY_SPEC, update_request: REQ) -> RES:
|
||||||
|
"""Update an entity object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query_request (REQ): The request schema object or dict for query.
|
||||||
|
update_request (REQ): The request schema object for update.
|
||||||
|
Returns:
|
||||||
|
RES: The response schema object.
|
||||||
|
"""
|
||||||
|
with self.session() as session:
|
||||||
|
query = self._create_query_object(session, query_request)
|
||||||
|
entry = query.first()
|
||||||
|
if entry is None:
|
||||||
|
raise Exception("Invalid request")
|
||||||
|
for key, value in model_to_dict(update_request).items(): # type: ignore
|
||||||
|
if value is not None:
|
||||||
|
if key in ["last_sync", "gmt_created", "gmt_modified"]:
|
||||||
|
continue
|
||||||
|
setattr(entry, key, value)
|
||||||
|
session.merge(entry)
|
||||||
|
return self.to_response(entry)
|
||||||
|
|
||||||
|
def update_set_space_id(self, space, space_id):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
knowledge_documents = session.query(KnowledgeDocumentEntity)
|
||||||
|
if space is not None:
|
||||||
|
knowledge_documents.filter(KnowledgeDocumentEntity.space == space).filter(
|
||||||
|
KnowledgeDocumentEntity.id is None
|
||||||
|
).update({KnowledgeDocumentEntity.id: space_id}, synchronize_session=False)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def update_knowledge_document(self, document: KnowledgeDocumentEntity):
|
||||||
|
try:
|
||||||
|
session = self.get_raw_session()
|
||||||
|
updated_document = session.merge(document)
|
||||||
|
session.commit()
|
||||||
|
return updated_document.id
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def raw_delete(self, query: KnowledgeDocumentEntity):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
knowledge_documents = session.query(KnowledgeDocumentEntity)
|
||||||
|
if query.id is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.id == query.id
|
||||||
|
)
|
||||||
|
if query.doc_name is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.doc_name == query.doc_name
|
||||||
|
)
|
||||||
|
if query.space is not None:
|
||||||
|
knowledge_documents = knowledge_documents.filter(
|
||||||
|
KnowledgeDocumentEntity.space == query.space
|
||||||
|
)
|
||||||
|
knowledge_documents.delete()
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def get_list_page(
|
||||||
|
self, query_request: QUERY_SPEC, page: int, page_size: int
|
||||||
|
) -> PaginationResult[RES]:
|
||||||
|
"""Get a page of entity objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query_request (REQ): The request schema object or dict for query.
|
||||||
|
page (int): The page number.
|
||||||
|
page_size (int): The page size.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PaginationResult: The pagination result.
|
||||||
|
"""
|
||||||
|
with self.session() as session:
|
||||||
|
query = self._create_query_object(session, query_request)
|
||||||
|
total_count = query.count()
|
||||||
|
items = (
|
||||||
|
query.order_by(KnowledgeDocumentEntity.id.desc())
|
||||||
|
.offset((page - 1) * page_size)
|
||||||
|
.limit(page_size)
|
||||||
|
)
|
||||||
|
items = [self.to_response(item) for item in items]
|
||||||
|
total_pages = (total_count + page_size - 1) // page_size
|
||||||
|
|
||||||
|
return PaginationResult(
|
||||||
|
items=items,
|
||||||
|
total_count=total_count,
|
||||||
|
total_pages=total_pages,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_request(
|
||||||
|
self, request: Union[ServeRequest, Dict[str, Any]]
|
||||||
|
) -> KnowledgeDocumentEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = (
|
||||||
|
request.dict() if isinstance(request, DocumentServeRequest) else request
|
||||||
|
)
|
||||||
|
entity = KnowledgeDocumentEntity(
|
||||||
|
id=request_dict.get("id"),
|
||||||
|
doc_name=request_dict.get("doc_name"),
|
||||||
|
doc_type=request_dict.get("doc_type"),
|
||||||
|
doc_token=request_dict.get("doc_token"),
|
||||||
|
space=request_dict.get("space"),
|
||||||
|
chunk_size=request_dict.get("chunk_size"),
|
||||||
|
status=request_dict.get("status"),
|
||||||
|
last_sync=request_dict.get("last_sync"),
|
||||||
|
content=request_dict.get("content"),
|
||||||
|
result=request_dict.get("result"),
|
||||||
|
vector_ids=request_dict.get("vector_ids"),
|
||||||
|
summary=request_dict.get("summary"),
|
||||||
|
questions=request_dict.get("questions"),
|
||||||
|
)
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: KnowledgeDocumentEntity) -> DocumentServeResponse:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
return DocumentServeResponse(
|
||||||
|
id=entity.id,
|
||||||
|
doc_name=entity.doc_name,
|
||||||
|
doc_type=entity.doc_type,
|
||||||
|
space=entity.space,
|
||||||
|
chunk_size=entity.chunk_size,
|
||||||
|
status=entity.status,
|
||||||
|
last_sync=entity.last_sync,
|
||||||
|
content=entity.content,
|
||||||
|
result=entity.result,
|
||||||
|
vector_ids=entity.vector_ids,
|
||||||
|
summary=entity.summary,
|
||||||
|
questions=entity.questions,
|
||||||
|
gmt_created=entity.gmt_created,
|
||||||
|
gmt_modified=entity.gmt_modified,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_response(self, entity: KnowledgeDocumentEntity) -> DocumentServeResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
return DocumentServeResponse(
|
||||||
|
id=entity.id,
|
||||||
|
doc_name=entity.doc_name,
|
||||||
|
doc_type=entity.doc_type,
|
||||||
|
space=entity.space,
|
||||||
|
chunk_size=entity.chunk_size,
|
||||||
|
status=entity.status,
|
||||||
|
last_sync=str(entity.last_sync),
|
||||||
|
content=entity.content,
|
||||||
|
result=entity.result,
|
||||||
|
vector_ids=entity.vector_ids,
|
||||||
|
summary=entity.summary,
|
||||||
|
questions=entity.questions,
|
||||||
|
gmt_created=str(entity.gmt_created),
|
||||||
|
gmt_modified=str(entity.gmt_modified),
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_response(
|
||||||
|
self, response: Union[DocumentServeResponse, Dict[str, Any]]
|
||||||
|
) -> KnowledgeDocumentEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
response_dict = (
|
||||||
|
response.dict() if isinstance(response, DocumentServeResponse) else response
|
||||||
|
)
|
||||||
|
entity = KnowledgeDocumentEntity(
|
||||||
|
id=response_dict.get("id"),
|
||||||
|
doc_name=response_dict.get("doc_name"),
|
||||||
|
doc_type=response_dict.get("doc_type"),
|
||||||
|
doc_token=response_dict.get("doc_token"),
|
||||||
|
space=response_dict.get("space"),
|
||||||
|
chunk_size=response_dict.get("chunk_size"),
|
||||||
|
status=response_dict.get("status"),
|
||||||
|
last_sync=response_dict.get("last_sync"),
|
||||||
|
content=response_dict.get("content"),
|
||||||
|
result=response_dict.get("result"),
|
||||||
|
vector_ids=response_dict.get("vector_ids"),
|
||||||
|
summary=response_dict.get("summary"),
|
||||||
|
questions=response_dict.get("questions"),
|
||||||
|
)
|
||||||
|
return entity
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer, String, Text
|
||||||
|
|
||||||
|
from kamidso.engine.core._private.pydantic import model_to_dict
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
from kamidso.engine.app.knowledge.request.request import KnowledgeSpaceRequest
|
||||||
|
from kamidso.engine.serve.rag.api.schemas import SpaceServeRequest, SpaceServeResponse
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeSpaceEntity(Model):
|
||||||
|
__tablename__ = "knowledge_space"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(100))
|
||||||
|
vector_type = Column(String(100))
|
||||||
|
index_methods = Column(String(500)) # JSON string of index methods
|
||||||
|
domain_type = Column(String(100))
|
||||||
|
desc = Column(String(100))
|
||||||
|
owner = Column(String(100))
|
||||||
|
context = Column(Text)
|
||||||
|
gmt_created = Column(DateTime)
|
||||||
|
gmt_modified = Column(DateTime)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"KnowledgeSpaceEntity(id={self.id}, name='{self.name}', "
|
||||||
|
f"vector_type='{self.vector_type}', desc='{self.desc}', "
|
||||||
|
f"owner='{self.owner}' context='{self.context}', "
|
||||||
|
f"gmt_created='{self.gmt_created}', gmt_modified='{self.gmt_modified}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KnowledgeSpaceDao(BaseDao):
|
||||||
|
def create_knowledge_space(self, space: KnowledgeSpaceRequest):
|
||||||
|
"""Create knowledge space"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
session = self.get_raw_session()
|
||||||
|
# Convert index_methods list to JSON string
|
||||||
|
index_methods_json = None
|
||||||
|
if space.index_methods:
|
||||||
|
index_methods_json = json.dumps(space.index_methods)
|
||||||
|
knowledge_space = KnowledgeSpaceEntity(
|
||||||
|
name=space.name,
|
||||||
|
vector_type=space.vector_type,
|
||||||
|
index_methods=index_methods_json,
|
||||||
|
domain_type=space.domain_type,
|
||||||
|
desc=space.desc,
|
||||||
|
owner=space.owner,
|
||||||
|
gmt_created=datetime.now(),
|
||||||
|
gmt_modified=datetime.now(),
|
||||||
|
)
|
||||||
|
session.add(knowledge_space)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
ks = self.get_knowledge_space(KnowledgeSpaceEntity(name=space.name))
|
||||||
|
if ks is not None and len(ks) == 1:
|
||||||
|
return ks[0].id
|
||||||
|
raise Exception("create space error, find more than 1 or 0 space.")
|
||||||
|
|
||||||
|
def get_knowledge_space_by_ids(self, ids):
|
||||||
|
session = self.get_raw_session()
|
||||||
|
if ids:
|
||||||
|
knowledge_spaces = session.query(KnowledgeSpaceEntity).filter(
|
||||||
|
KnowledgeSpaceEntity.id.in_(ids)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
knowledge_spaces_list = knowledge_spaces.all()
|
||||||
|
session.close()
|
||||||
|
return knowledge_spaces_list
|
||||||
|
|
||||||
|
def get_knowledge_space(self, query: KnowledgeSpaceEntity):
|
||||||
|
"""Get knowledge space by query"""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
knowledge_spaces = session.query(KnowledgeSpaceEntity)
|
||||||
|
if query.id is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.id == query.id
|
||||||
|
)
|
||||||
|
if query.name is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.name == query.name
|
||||||
|
)
|
||||||
|
if query.vector_type is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.vector_type == query.vector_type
|
||||||
|
)
|
||||||
|
if query.domain_type is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.domain_type == query.domain_type
|
||||||
|
)
|
||||||
|
if query.desc is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.desc == query.desc
|
||||||
|
)
|
||||||
|
if query.owner is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.owner == query.owner
|
||||||
|
)
|
||||||
|
if query.gmt_created is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.gmt_created == query.gmt_created
|
||||||
|
)
|
||||||
|
if query.gmt_modified is not None:
|
||||||
|
knowledge_spaces = knowledge_spaces.filter(
|
||||||
|
KnowledgeSpaceEntity.gmt_modified == query.gmt_modified
|
||||||
|
)
|
||||||
|
knowledge_spaces = knowledge_spaces.order_by(
|
||||||
|
KnowledgeSpaceEntity.gmt_created.desc()
|
||||||
|
)
|
||||||
|
result = knowledge_spaces.all()
|
||||||
|
session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update_knowledge_space(self, space: KnowledgeSpaceEntity):
|
||||||
|
"""Update knowledge space"""
|
||||||
|
|
||||||
|
session = self.get_raw_session()
|
||||||
|
request = SpaceServeRequest(id=space.id)
|
||||||
|
update_request = self.to_request(space)
|
||||||
|
query = self._create_query_object(session, request)
|
||||||
|
entry = query.first()
|
||||||
|
if entry is None:
|
||||||
|
raise Exception("Invalid request")
|
||||||
|
for key, value in model_to_dict(update_request).items(): # type: ignore
|
||||||
|
if value is not None:
|
||||||
|
setattr(entry, key, value)
|
||||||
|
session.merge(entry)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
return self.to_response(space)
|
||||||
|
|
||||||
|
def delete_knowledge_space(self, space: KnowledgeSpaceEntity):
|
||||||
|
"""Delete knowledge space"""
|
||||||
|
session = self.get_raw_session()
|
||||||
|
if space:
|
||||||
|
session.delete(space)
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def from_request(
|
||||||
|
self, request: Union[SpaceServeRequest, Dict[str, Any]]
|
||||||
|
) -> KnowledgeSpaceEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = (
|
||||||
|
model_to_dict(request)
|
||||||
|
if isinstance(request, SpaceServeRequest)
|
||||||
|
else request
|
||||||
|
)
|
||||||
|
entity = KnowledgeSpaceEntity(**request_dict)
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: KnowledgeSpaceEntity) -> SpaceServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
return SpaceServeRequest(
|
||||||
|
id=entity.id,
|
||||||
|
name=entity.name,
|
||||||
|
vector_type=entity.vector_type,
|
||||||
|
desc=entity.desc,
|
||||||
|
owner=entity.owner,
|
||||||
|
context=entity.context,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_response(self, entity: KnowledgeSpaceEntity) -> SpaceServeResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
return SpaceServeResponse(
|
||||||
|
id=entity.id,
|
||||||
|
name=entity.name,
|
||||||
|
vector_type=entity.vector_type,
|
||||||
|
desc=entity.desc,
|
||||||
|
owner=entity.owner,
|
||||||
|
context=entity.context,
|
||||||
|
domain_type=entity.domain_type,
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Persistence models for session-scoped files."""
|
||||||
|
|
||||||
|
from .dao import SessionFileDao
|
||||||
|
from .models import SessionFileEntity
|
||||||
|
|
||||||
|
__all__ = ["SessionFileDao", "SessionFileEntity"]
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
"""Repository operations for session-scoped files."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao
|
||||||
|
|
||||||
|
from ..domain import (
|
||||||
|
FileScope,
|
||||||
|
SessionFileManifest,
|
||||||
|
SessionFilePrivateRecord,
|
||||||
|
SessionFileSnapshot,
|
||||||
|
SessionFileStatus,
|
||||||
|
)
|
||||||
|
from .models import SessionFileEntity
|
||||||
|
|
||||||
|
|
||||||
|
class SessionFileDao(BaseDao[SessionFileEntity, Dict[str, Any], SessionFileManifest]):
|
||||||
|
"""Store private file locations while exposing only public metadata."""
|
||||||
|
|
||||||
|
def from_request(self, request: Union[Dict[str, Any], Any]) -> SessionFileEntity:
|
||||||
|
if isinstance(request, dict):
|
||||||
|
request_dict = request
|
||||||
|
elif hasattr(request, "model_dump"):
|
||||||
|
request_dict = request.model_dump()
|
||||||
|
else:
|
||||||
|
request_dict = request.dict()
|
||||||
|
return SessionFileEntity(**request_dict)
|
||||||
|
|
||||||
|
def create(self, request: Union[Dict[str, Any], Any]) -> SessionFileManifest:
|
||||||
|
"""Validate scope and insert without using unrestricted BaseDao lookup."""
|
||||||
|
entity = self.from_request(request)
|
||||||
|
self._validate_entity(entity)
|
||||||
|
with self.session() as session:
|
||||||
|
session.add(entity)
|
||||||
|
session.flush()
|
||||||
|
return self.to_manifest(entity)
|
||||||
|
|
||||||
|
def to_response(self, entity: SessionFileEntity) -> SessionFileManifest:
|
||||||
|
"""Map fields explicitly so private storage never crosses this boundary."""
|
||||||
|
return self.to_manifest(entity)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_manifest(entity: SessionFileEntity) -> SessionFileManifest:
|
||||||
|
"""Build agent-safe file metadata from a persistence record."""
|
||||||
|
return SessionFileManifest(
|
||||||
|
file_id=entity.file_id,
|
||||||
|
name=entity.display_name,
|
||||||
|
size=entity.size_bytes,
|
||||||
|
media_type=entity.media_type,
|
||||||
|
kind=entity.file_kind,
|
||||||
|
status=SessionFileStatus(entity.status),
|
||||||
|
ordinal=entity.ordinal,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_snapshot(entity: SessionFileEntity) -> SessionFileSnapshot:
|
||||||
|
"""Build history-safe file metadata from a persistence record."""
|
||||||
|
return SessionFileSnapshot(
|
||||||
|
file_id=entity.file_id,
|
||||||
|
name=entity.display_name,
|
||||||
|
size=entity.size_bytes,
|
||||||
|
media_type=entity.media_type,
|
||||||
|
kind=entity.file_kind,
|
||||||
|
status=SessionFileStatus(entity.status),
|
||||||
|
ordinal=entity.ordinal,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_private_record(entity: SessionFileEntity) -> SessionFilePrivateRecord:
|
||||||
|
"""Detach complete private metadata into an immutable domain value."""
|
||||||
|
return SessionFilePrivateRecord(
|
||||||
|
file_id=entity.file_id,
|
||||||
|
owner_id=entity.owner_id,
|
||||||
|
session_id=entity.session_id,
|
||||||
|
task_id=entity.task_id,
|
||||||
|
display_name=entity.display_name,
|
||||||
|
storage_uri=entity.storage_uri,
|
||||||
|
media_type=entity.media_type,
|
||||||
|
file_kind=entity.file_kind,
|
||||||
|
size_bytes=entity.size_bytes,
|
||||||
|
sha256=entity.sha256,
|
||||||
|
ordinal=entity.ordinal,
|
||||||
|
status=SessionFileStatus(entity.status),
|
||||||
|
inspection_json=entity.inspection_json,
|
||||||
|
error_code=entity.error_code,
|
||||||
|
error_message=entity.error_message,
|
||||||
|
source_file_id=entity.source_file_id,
|
||||||
|
created_at=entity.created_at,
|
||||||
|
updated_at=entity.updated_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_by_file_id(
|
||||||
|
self, file_id: str, scope: FileScope
|
||||||
|
) -> Optional[SessionFileManifest]:
|
||||||
|
"""Return a file only when owner and scope match, otherwise ``None``."""
|
||||||
|
with self.session(commit=False) as session:
|
||||||
|
entity = (
|
||||||
|
self._scope_query(session, scope)
|
||||||
|
.filter(SessionFileEntity.file_id == file_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return self.to_manifest(entity) if entity is not None else None
|
||||||
|
|
||||||
|
def get_private_file_by_id(
|
||||||
|
self, file_id: str, scope: FileScope
|
||||||
|
) -> Optional[SessionFilePrivateRecord]:
|
||||||
|
"""Return immutable private metadata for an exact owner-bound scope."""
|
||||||
|
with self.session(commit=False) as session:
|
||||||
|
entity = (
|
||||||
|
self._scope_query(session, scope)
|
||||||
|
.filter(SessionFileEntity.file_id == file_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return self.to_private_record(entity) if entity is not None else None
|
||||||
|
|
||||||
|
def list_by_scope(self, scope: FileScope) -> List[SessionFileManifest]:
|
||||||
|
"""List an owner's scoped files in stable upload order."""
|
||||||
|
with self.session(commit=False) as session:
|
||||||
|
entities = (
|
||||||
|
self._scope_query(session, scope)
|
||||||
|
.order_by(SessionFileEntity.ordinal)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [self.to_manifest(entity) for entity in entities]
|
||||||
|
|
||||||
|
def list_by_source_file_id(
|
||||||
|
self, source_file_id: str, scope: FileScope
|
||||||
|
) -> List[SessionFileManifest]:
|
||||||
|
"""List lineage records within one exact owner-bound scope."""
|
||||||
|
with self.session(commit=False) as session:
|
||||||
|
entities = (
|
||||||
|
self._scope_query(session, scope)
|
||||||
|
.filter(SessionFileEntity.source_file_id == source_file_id)
|
||||||
|
.order_by(SessionFileEntity.ordinal)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [self.to_manifest(entity) for entity in entities]
|
||||||
|
|
||||||
|
def total_size_bytes(self, scope: FileScope) -> int:
|
||||||
|
"""Return total persisted bytes for one owner-bound scope."""
|
||||||
|
with self.session(commit=False) as session:
|
||||||
|
total = (
|
||||||
|
self._scope_query(session, scope)
|
||||||
|
.with_entities(func.coalesce(func.sum(SessionFileEntity.size_bytes), 0))
|
||||||
|
.scalar()
|
||||||
|
)
|
||||||
|
return int(total)
|
||||||
|
|
||||||
|
def total_owner_size_bytes(self, owner_id: str) -> int:
|
||||||
|
"""Return total persisted bytes across all scopes for one owner."""
|
||||||
|
if not isinstance(owner_id, str) or not owner_id.strip():
|
||||||
|
raise ValueError("owner_id must not be blank")
|
||||||
|
with self.session(commit=False) as session:
|
||||||
|
total = (
|
||||||
|
session.query(func.coalesce(func.sum(SessionFileEntity.size_bytes), 0))
|
||||||
|
.filter(SessionFileEntity.owner_id == owner_id)
|
||||||
|
.scalar()
|
||||||
|
)
|
||||||
|
return int(total)
|
||||||
|
|
||||||
|
def update_status(
|
||||||
|
self,
|
||||||
|
file_id: str,
|
||||||
|
scope: FileScope,
|
||||||
|
status: Union[SessionFileStatus, str],
|
||||||
|
inspection_json: Optional[str] = None,
|
||||||
|
error_code: Optional[str] = None,
|
||||||
|
error_message: Optional[str] = None,
|
||||||
|
) -> Optional[SessionFileManifest]:
|
||||||
|
"""Update processing state only for the exact owner-bound scope."""
|
||||||
|
status_value = self._validate_status(status)
|
||||||
|
with self.session() as session:
|
||||||
|
entity = (
|
||||||
|
self._scope_query(session, scope)
|
||||||
|
.filter(SessionFileEntity.file_id == file_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if entity is None:
|
||||||
|
return None
|
||||||
|
entity.status = status_value
|
||||||
|
entity.inspection_json = inspection_json
|
||||||
|
entity.error_code = error_code
|
||||||
|
entity.error_message = error_message
|
||||||
|
session.flush()
|
||||||
|
return self.to_manifest(entity)
|
||||||
|
|
||||||
|
def delete_by_file_id(self, file_id: str, scope: FileScope) -> bool:
|
||||||
|
"""Delete a file only for the exact owner-bound scope."""
|
||||||
|
with self.session() as session:
|
||||||
|
deleted = (
|
||||||
|
self._scope_query(session, scope)
|
||||||
|
.filter(SessionFileEntity.file_id == file_id)
|
||||||
|
.delete(synchronize_session=False)
|
||||||
|
)
|
||||||
|
return deleted == 1
|
||||||
|
|
||||||
|
def get_one(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError("Use a scope-constrained session file method")
|
||||||
|
|
||||||
|
def get_list(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError("Use a scope-constrained session file method")
|
||||||
|
|
||||||
|
def get_list_page(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError("Use a scope-constrained session file method")
|
||||||
|
|
||||||
|
def update(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError("Use a scope-constrained session file method")
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError("Use a scope-constrained session file method")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_entity(entity: SessionFileEntity) -> None:
|
||||||
|
FileScope(
|
||||||
|
owner_id=entity.owner_id,
|
||||||
|
session_id=entity.session_id,
|
||||||
|
task_id=entity.task_id,
|
||||||
|
)
|
||||||
|
SessionFileDao._validate_status(entity.status)
|
||||||
|
if entity.size_bytes < 0:
|
||||||
|
raise ValueError("size_bytes must be non-negative")
|
||||||
|
if entity.ordinal < 0:
|
||||||
|
raise ValueError("ordinal must be non-negative")
|
||||||
|
if (
|
||||||
|
not isinstance(entity.sha256, str)
|
||||||
|
or re.fullmatch(r"[0-9a-fA-F]{64}", entity.sha256) is None
|
||||||
|
):
|
||||||
|
raise ValueError("sha256 must contain exactly 64 hexadecimal characters")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_status(status: Union[SessionFileStatus, str]) -> str:
|
||||||
|
try:
|
||||||
|
return SessionFileStatus(status).value
|
||||||
|
except (TypeError, ValueError) as error:
|
||||||
|
raise ValueError("invalid session file status") from error
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _scope_query(session, scope: FileScope):
|
||||||
|
query = session.query(SessionFileEntity).filter(
|
||||||
|
SessionFileEntity.owner_id == scope.owner_id
|
||||||
|
)
|
||||||
|
if scope.session_id is not None:
|
||||||
|
return query.filter(
|
||||||
|
SessionFileEntity.session_id == scope.session_id,
|
||||||
|
SessionFileEntity.task_id.is_(None),
|
||||||
|
)
|
||||||
|
return query.filter(
|
||||||
|
SessionFileEntity.task_id == scope.task_id,
|
||||||
|
SessionFileEntity.session_id.is_(None),
|
||||||
|
)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
"""SQLAlchemy model for session-scoped files."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
BigInteger,
|
||||||
|
CheckConstraint,
|
||||||
|
Column,
|
||||||
|
DateTime,
|
||||||
|
Index,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
func,
|
||||||
|
)
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import Model
|
||||||
|
|
||||||
|
|
||||||
|
class SessionFileEntity(Model):
|
||||||
|
"""Private persistence record for an owner-bound session or task file."""
|
||||||
|
|
||||||
|
__tablename__ = "dbgpt_session_file"
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(
|
||||||
|
"(session_id IS NULL) <> (task_id IS NULL)",
|
||||||
|
name="ck_session_file_scope",
|
||||||
|
),
|
||||||
|
Index("uk_session_file_file_id", "file_id", unique=True),
|
||||||
|
Index(
|
||||||
|
"idx_session_file_owner_session",
|
||||||
|
"owner_id",
|
||||||
|
"session_id",
|
||||||
|
"ordinal",
|
||||||
|
),
|
||||||
|
Index(
|
||||||
|
"idx_session_file_owner_task",
|
||||||
|
"owner_id",
|
||||||
|
"task_id",
|
||||||
|
"ordinal",
|
||||||
|
),
|
||||||
|
Index("idx_session_file_sha256", "owner_id", "sha256"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
BigInteger().with_variant(Integer, "sqlite"),
|
||||||
|
primary_key=True,
|
||||||
|
autoincrement=True,
|
||||||
|
)
|
||||||
|
file_id = Column(String(64), nullable=False)
|
||||||
|
owner_id = Column(String(255), nullable=False)
|
||||||
|
session_id = Column(String(255), nullable=True)
|
||||||
|
task_id = Column(String(64), nullable=True)
|
||||||
|
display_name = Column(String(256), nullable=False)
|
||||||
|
storage_uri = Column(String(512), nullable=False)
|
||||||
|
media_type = Column(String(255), nullable=False)
|
||||||
|
file_kind = Column(String(32), nullable=False)
|
||||||
|
size_bytes = Column(BigInteger, nullable=False)
|
||||||
|
sha256 = Column(String(64), nullable=False)
|
||||||
|
ordinal = Column(Integer, nullable=False)
|
||||||
|
status = Column(String(32), nullable=False)
|
||||||
|
inspection_json = Column(
|
||||||
|
Text().with_variant(mysql.LONGTEXT(), "mysql"), nullable=True
|
||||||
|
)
|
||||||
|
error_code = Column(String(64), nullable=True)
|
||||||
|
error_message = Column(Text, nullable=True)
|
||||||
|
source_file_id = Column(String(64), nullable=True)
|
||||||
|
created_at = Column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
default=datetime.now,
|
||||||
|
server_default=func.now(),
|
||||||
|
)
|
||||||
|
updated_at = Column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
default=datetime.now,
|
||||||
|
onupdate=datetime.now,
|
||||||
|
server_default=func.now(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This is an auto-generated __init__.py file
|
||||||
|
# generated by `kamidso.engine.core new serve {__template_app_name__}`
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""This is an auto-generated model file
|
||||||
|
You can define your own models and DAOs here
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from sqlalchemy import Column, DateTime, Integer
|
||||||
|
|
||||||
|
from kamidso.engine.core.storage.metadata import BaseDao, Model
|
||||||
|
|
||||||
|
from ..api.schemas import ServeRequest, ServerResponse
|
||||||
|
from ..config import SERVER_APP_TABLE_NAME, ServeConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServeEntity(Model):
|
||||||
|
__tablename__ = SERVER_APP_TABLE_NAME
|
||||||
|
id = Column(Integer, primary_key=True, comment="Auto increment id")
|
||||||
|
|
||||||
|
# TODO: define your own fields here
|
||||||
|
|
||||||
|
gmt_created = Column(DateTime, default=datetime.now, comment="Record creation time")
|
||||||
|
gmt_modified = Column(DateTime, default=datetime.now, comment="Record update time")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"ServeEntity(id={self.id}, gmt_created='{self.gmt_created}', "
|
||||||
|
f"gmt_modified='{self.gmt_modified}')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServeDao(BaseDao[ServeEntity, ServeRequest, ServerResponse]):
|
||||||
|
"""The DAO class for {__template_app_name__hump__}"""
|
||||||
|
|
||||||
|
def __init__(self, serve_config: ServeConfig):
|
||||||
|
super().__init__()
|
||||||
|
self._serve_config = serve_config
|
||||||
|
|
||||||
|
def from_request(self, request: Union[ServeRequest, Dict[str, Any]]) -> ServeEntity:
|
||||||
|
"""Convert the request to an entity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Union[ServeRequest, Dict[str, Any]]): The request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: The entity
|
||||||
|
"""
|
||||||
|
request_dict = (
|
||||||
|
request.to_dict() if isinstance(request, ServeRequest) else request
|
||||||
|
)
|
||||||
|
entity = ServeEntity(**request_dict)
|
||||||
|
# TODO implement your own logic here, transfer the request_dict to an entity
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def to_request(self, entity: ServeEntity) -> ServeRequest:
|
||||||
|
"""Convert the entity to a request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
REQ: The request
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a request
|
||||||
|
return ServeRequest()
|
||||||
|
|
||||||
|
def to_response(self, entity: ServeEntity) -> ServerResponse:
|
||||||
|
"""Convert the entity to a response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (T): The entity
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RES: The response
|
||||||
|
"""
|
||||||
|
# TODO implement your own logic here, transfer the entity to a response
|
||||||
|
return ServerResponse()
|
||||||
Reference in new issue
Block a user