Feature/memory perceptual (#48)
* perf(workflow): pass JSON data to HTTP node as a string * perf(prompt_opt): simplify log output * feat(memory): add perceptual memory page API and related database schema * perf(log): clean up API exception log output * perf(memory): simplify perceptual memory timeline response by removing metadata
This commit is contained in:
255
api/app/controllers/memory_perceptual_controller.py
Normal file
255
api/app/controllers/memory_perceptual_controller.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.logging_config import get_api_logger
|
||||
from app.core.response_utils import success, fail
|
||||
from app.db import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models import User
|
||||
from app.models.memory_perceptual_model import PerceptualType
|
||||
from app.schemas.memory_perceptual_schema import (
|
||||
PerceptualQuerySchema,
|
||||
PerceptualFilter
|
||||
)
|
||||
from app.schemas.response_schema import ApiResponse
|
||||
from app.services.memory_perceptual_service import MemoryPerceptualService
|
||||
|
||||
api_logger = get_api_logger()
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/memory/perceptual",
|
||||
tags=["Perceptual Memory System"],
|
||||
dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{group_id}/count", response_model=ApiResponse)
|
||||
def get_memory_count(
|
||||
group_id: uuid.UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Retrieve perceptual memory statistics for a user group.
|
||||
|
||||
Args:
|
||||
group_id: ID of the user group (usually end_user_id in this context)
|
||||
current_user: Current authenticated user
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
ApiResponse: Response containing memory count statistics
|
||||
"""
|
||||
api_logger.info(f"Fetching perceptual memory statistics: user={current_user.username}, group_id={group_id}")
|
||||
|
||||
try:
|
||||
service = MemoryPerceptualService(db)
|
||||
count_stats = service.get_memory_count(group_id)
|
||||
|
||||
api_logger.info(f"Memory statistics fetched successfully: total={count_stats.get('total', 0)}")
|
||||
|
||||
return success(
|
||||
data=count_stats,
|
||||
msg="Memory statistics retrieved successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"Failed to fetch memory statistics: group_id={group_id}, error={str(e)}")
|
||||
return fail(
|
||||
code=BizCode.INTERNAL_ERROR,
|
||||
msg="Failed to fetch memory statistics",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{group_id}/last_visual", response_model=ApiResponse)
|
||||
def get_last_visual_memory(
|
||||
group_id: uuid.UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Retrieve the most recent VISION-type memory for a user.
|
||||
|
||||
Args:
|
||||
group_id: ID of the user group
|
||||
current_user: Current authenticated user
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
ApiResponse: Metadata of the latest visual memory
|
||||
"""
|
||||
api_logger.info(f"Fetching latest visual memory: user={current_user.username}, group_id={group_id}")
|
||||
|
||||
try:
|
||||
service = MemoryPerceptualService(db)
|
||||
visual_memory = service.get_latest_visual_memory(group_id)
|
||||
|
||||
if visual_memory is None:
|
||||
api_logger.info(f"No visual memory found: group_id={group_id}")
|
||||
return success(
|
||||
data=None,
|
||||
msg="No visual memory available"
|
||||
)
|
||||
|
||||
api_logger.info(f"Latest visual memory retrieved successfully: file={visual_memory.get('file_name')}")
|
||||
|
||||
return success(
|
||||
data=visual_memory,
|
||||
msg="Latest visual memory retrieved successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"Failed to fetch latest visual memory: group_id={group_id}, error={str(e)}")
|
||||
return fail(
|
||||
code=BizCode.INTERNAL_ERROR,
|
||||
msg="Failed to fetch latest visual memory",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{group_id}/last_listen", response_model=ApiResponse)
|
||||
def get_last_memory_listen(
|
||||
group_id: uuid.UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Retrieve the most recent AUDIO-type memory for a user.
|
||||
|
||||
Args:
|
||||
group_id: ID of the user group
|
||||
current_user: Current authenticated user
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
ApiResponse: Metadata of the latest audio memory
|
||||
"""
|
||||
api_logger.info(f"Fetching latest audio memory: user={current_user.username}, group_id={group_id}")
|
||||
|
||||
try:
|
||||
service = MemoryPerceptualService(db)
|
||||
audio_memory = service.get_latest_audio_memory(group_id)
|
||||
|
||||
if audio_memory is None:
|
||||
api_logger.info(f"No audio memory found: group_id={group_id}")
|
||||
return success(
|
||||
data=None,
|
||||
msg="No audio memory available"
|
||||
)
|
||||
|
||||
api_logger.info(f"Latest audio memory retrieved successfully: file={audio_memory.get('file_name')}")
|
||||
|
||||
return success(
|
||||
data=audio_memory,
|
||||
msg="Latest audio memory retrieved successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"Failed to fetch latest audio memory: group_id={group_id}, error={str(e)}")
|
||||
return fail(
|
||||
code=BizCode.INTERNAL_ERROR,
|
||||
msg="Failed to fetch latest audio memory",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{group_id}/last_text", response_model=ApiResponse)
|
||||
def get_last_text_memory(
|
||||
group_id: uuid.UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Retrieve the most recent TEXT-type memory for a user.
|
||||
|
||||
Args:
|
||||
group_id: ID of the user group
|
||||
current_user: Current authenticated user
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
ApiResponse: Metadata of the latest text memory
|
||||
"""
|
||||
api_logger.info(f"Fetching latest text memory: user={current_user.username}, group_id={group_id}")
|
||||
|
||||
try:
|
||||
# 调用服务层获取最近的文本记忆
|
||||
service = MemoryPerceptualService(db)
|
||||
text_memory = service.get_latest_text_memory(group_id)
|
||||
|
||||
if text_memory is None:
|
||||
api_logger.info(f"No text memory found: group_id={group_id}")
|
||||
return success(
|
||||
data=None,
|
||||
msg="No text memory available"
|
||||
)
|
||||
|
||||
api_logger.info(f"Latest text memory retrieved successfully: file={text_memory.get('file_name')}")
|
||||
|
||||
return success(
|
||||
data=text_memory,
|
||||
msg="Latest text memory retrieved successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"Failed to fetch latest text memory: group_id={group_id}, error={str(e)}")
|
||||
return fail(
|
||||
code=BizCode.INTERNAL_ERROR,
|
||||
msg="Failed to fetch latest text memory",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{group_id}/timeline", response_model=ApiResponse)
|
||||
def get_memory_time_line(
|
||||
group_id: uuid.UUID,
|
||||
perceptual_type: Optional[PerceptualType] = Query(None, description="感知类型过滤"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(10, ge=1, le=100, description="每页大小"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Retrieve a timeline of perceptual memories for a user group.
|
||||
|
||||
Args:
|
||||
group_id: ID of the user group
|
||||
perceptual_type: Optional filter for perceptual type
|
||||
page: Page number for pagination
|
||||
page_size: Number of items per page
|
||||
current_user: Current authenticated user
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
ApiResponse: Timeline data of perceptual memories
|
||||
"""
|
||||
api_logger.info(
|
||||
f"Fetching perceptual memory timeline: user={current_user.username}, "
|
||||
f"group_id={group_id}, type={perceptual_type}, page={page}"
|
||||
)
|
||||
|
||||
try:
|
||||
query = PerceptualQuerySchema(
|
||||
filter=PerceptualFilter(type=perceptual_type),
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
service = MemoryPerceptualService(db)
|
||||
timeline_data = service.get_time_line(group_id, query)
|
||||
|
||||
api_logger.info(
|
||||
f"Perceptual memory timeline retrieved successfully: total={timeline_data.total}, "
|
||||
f"returned={len(timeline_data.memories)}"
|
||||
)
|
||||
|
||||
return success(
|
||||
data=timeline_data.model_dump(),
|
||||
msg="Perceptual memory timeline retrieved successfully"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(
|
||||
f"Failed to fetch perceptual memory timeline: group_id={group_id}, "
|
||||
f"error={str(e)}"
|
||||
)
|
||||
return fail(
|
||||
code=BizCode.INTERNAL_ERROR,
|
||||
msg="Failed to fetch perceptual memory timeline",
|
||||
)
|
||||
@@ -73,8 +73,10 @@ class HttpContentTypeConfig(BaseModel):
|
||||
content_type = info.data.get("content_type")
|
||||
if content_type == HttpContentType.FROM_DATA and not isinstance(v, HttpFormData):
|
||||
raise ValueError("When content_type is 'form-data', data must be of type HttpFormData")
|
||||
elif content_type in [HttpContentType.JSON, HttpContentType.WWW_FORM] and not isinstance(v, dict):
|
||||
raise ValueError("When content_type is JSON or x-www-form-urlencoded, data must be a object")
|
||||
elif content_type in [HttpContentType.JSON] and not isinstance(v, str):
|
||||
raise ValueError("When content_type is JSON, data must be of type str")
|
||||
elif content_type in [HttpContentType.WWW_FORM] and not isinstance(v, dict):
|
||||
raise ValueError("When content_type is x-www-form-urlencoded, data must be a object")
|
||||
elif content_type in [HttpContentType.RAW, HttpContentType.BINARY] and not isinstance(v, str):
|
||||
raise ValueError("When content_type is raw/binary, data must be a string (File descriptor)")
|
||||
return v
|
||||
|
||||
@@ -120,7 +120,7 @@ class HttpRequestNode(BaseNode):
|
||||
return {}
|
||||
case HttpContentType.JSON:
|
||||
content["json"] = json.loads(self._render_template(
|
||||
json.dumps(self.typed_config.body.data), state
|
||||
self.typed_config.body.data, state
|
||||
))
|
||||
case HttpContentType.FROM_DATA:
|
||||
data = {}
|
||||
|
||||
40
api/app/models/memory_perceptual_model.py
Normal file
40
api/app/models/memory_perceptual_model.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from enum import IntEnum
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, DateTime, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from app.db import Base
|
||||
|
||||
|
||||
class PerceptualType(IntEnum):
|
||||
VISION = 1
|
||||
AUDIO = 2
|
||||
TEXT = 3
|
||||
CONVERSATION = 4
|
||||
|
||||
|
||||
class FileStorageType(IntEnum):
|
||||
LOCAL = 1
|
||||
REMOTE = 2
|
||||
|
||||
|
||||
class MemoryPerceptualModel(Base):
|
||||
__tablename__ = "memory_perceptual"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
end_user_id = Column(UUID(as_uuid=True), ForeignKey("end_users.id"), index=True)
|
||||
|
||||
perceptual_type = Column(Integer, index=True, nullable=False, comment="感知类型")
|
||||
|
||||
storage_service = Column(Integer, default=0, comment="存储服务类型")
|
||||
file_path = Column(String, nullable=False, comment="文件路径")
|
||||
file_name = Column(String, nullable=False, comment="文件名称")
|
||||
file_ext = Column(String, nullable=False, comment="文件后缀名")
|
||||
|
||||
summary = Column(String, comment="摘要")
|
||||
meta_data = Column(JSONB, comment="元信息")
|
||||
|
||||
created_time = Column(DateTime, default=datetime.datetime.now, comment="创建时间")
|
||||
156
api/app/repositories/memory_perceptual_repository.py
Normal file
156
api/app/repositories/memory_perceptual_repository.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from sqlalchemy import and_, desc
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging_config import get_db_logger
|
||||
from app.models.memory_perceptual_model import MemoryPerceptualModel, PerceptualType, FileStorageType
|
||||
from app.schemas.memory_perceptual_schema import PerceptualQuerySchema
|
||||
|
||||
db_logger = get_db_logger()
|
||||
|
||||
|
||||
class MemoryPerceptualRepository:
|
||||
"""Data Access Layer for perceptual memory"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
# ==================== Create and update ====================
|
||||
def create_perceptual_memory(
|
||||
self,
|
||||
end_user_id: uuid.UUID,
|
||||
perceptual_type: PerceptualType,
|
||||
file_path: str,
|
||||
file_name: str,
|
||||
file_ext: str,
|
||||
summary: Optional[str] = None,
|
||||
meta_data: Optional[dict] = None,
|
||||
storage_service: FileStorageType = FileStorageType.LOCAL
|
||||
|
||||
) -> MemoryPerceptualModel:
|
||||
|
||||
"""Create perceptual memory"""
|
||||
|
||||
db_logger.debug(f"Creating perceptual memory: end_user_id={end_user_id}, "
|
||||
f"type={perceptual_type}, file={file_name}")
|
||||
|
||||
try:
|
||||
perceptual_memory = MemoryPerceptualModel(
|
||||
end_user_id=end_user_id,
|
||||
perceptual_type=perceptual_type,
|
||||
storage_service=storage_service,
|
||||
file_path=file_path,
|
||||
file_name=file_name,
|
||||
file_ext=file_ext,
|
||||
summary=summary,
|
||||
meta_data=meta_data,
|
||||
created_time=datetime.now()
|
||||
)
|
||||
|
||||
self.db.add(perceptual_memory)
|
||||
self.db.flush()
|
||||
|
||||
db_logger.info(f"Perceptual memory created successfully: id={perceptual_memory.id}, file={file_name}")
|
||||
return perceptual_memory
|
||||
|
||||
except Exception as e:
|
||||
db_logger.error(f"Failed to create perceptual memory: end_user_id={end_user_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
# ==================== Query ====================
|
||||
def get_count_by_user_id(
|
||||
self,
|
||||
end_user_id: uuid.UUID,
|
||||
):
|
||||
db_logger.debug(f"Querying perceptual memory Count: end_user_id={end_user_id}")
|
||||
|
||||
try:
|
||||
count = self.db.query(MemoryPerceptualModel).filter(
|
||||
MemoryPerceptualModel.end_user_id == end_user_id
|
||||
).count()
|
||||
return count
|
||||
except Exception as e:
|
||||
db_logger.error(f"Failed to query perceptual memory count: end_user_id={end_user_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
def get_count_by_type(
|
||||
self,
|
||||
end_user_id: uuid.UUID,
|
||||
perceptual_type: PerceptualType,
|
||||
):
|
||||
db_logger.debug(f"Querying perceptual memory Count: end_user_id={end_user_id}, type={perceptual_type}")
|
||||
|
||||
try:
|
||||
count = self.db.query(MemoryPerceptualModel).filter(
|
||||
MemoryPerceptualModel.end_user_id == end_user_id,
|
||||
MemoryPerceptualModel.perceptual_type == perceptual_type
|
||||
).count()
|
||||
return count
|
||||
except Exception as e:
|
||||
db_logger.error(f"Failed to query perceptual memory count: end_user_id={end_user_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
def get_timeline(
|
||||
self,
|
||||
end_user_id: uuid.UUID,
|
||||
query: PerceptualQuerySchema
|
||||
) -> Tuple[int, List[MemoryPerceptualModel]]:
|
||||
"""Get the timeline of a user's perceptual memories"""
|
||||
db_logger.debug(f"Querying perceptual memory timeline: end_user_id={end_user_id}, filter={query.filter}")
|
||||
|
||||
try:
|
||||
base_query = self.db.query(MemoryPerceptualModel).filter(
|
||||
MemoryPerceptualModel.end_user_id == end_user_id
|
||||
)
|
||||
|
||||
if query.filter.type is not None:
|
||||
base_query = base_query.filter(
|
||||
MemoryPerceptualModel.perceptual_type == query.filter.type
|
||||
)
|
||||
|
||||
total_count = base_query.count()
|
||||
|
||||
memories = base_query.order_by(
|
||||
desc(MemoryPerceptualModel.created_time)
|
||||
).offset(
|
||||
(query.page - 1) * query.page_size
|
||||
).limit(query.page_size).all()
|
||||
|
||||
db_logger.info(
|
||||
f"Perceptual memory timeline query succeeded: end_user_id={end_user_id}, total={total_count}, returned={len(memories)}")
|
||||
return total_count, memories
|
||||
|
||||
except Exception as e:
|
||||
db_logger.error(f"Failed to query perceptual memory timeline: end_user_id={end_user_id} - {str(e)}")
|
||||
raise
|
||||
|
||||
def get_by_type(
|
||||
self,
|
||||
end_user_id: uuid.UUID,
|
||||
perceptual_type: PerceptualType,
|
||||
limit: int = 10,
|
||||
offset: int = 0
|
||||
) -> List[MemoryPerceptualModel]:
|
||||
"""Get memories by perceptual type"""
|
||||
db_logger.debug(f"Querying perceptual memories by type: end_user_id={end_user_id}, type={perceptual_type}")
|
||||
|
||||
try:
|
||||
memories = self.db.query(MemoryPerceptualModel).filter(
|
||||
and_(
|
||||
MemoryPerceptualModel.end_user_id == end_user_id,
|
||||
MemoryPerceptualModel.perceptual_type == perceptual_type
|
||||
)
|
||||
).order_by(
|
||||
desc(MemoryPerceptualModel.created_time)
|
||||
).offset(offset).limit(limit).all()
|
||||
|
||||
db_logger.debug(f"Query by type succeeded: count={len(memories)}")
|
||||
return memories
|
||||
|
||||
except Exception as e:
|
||||
db_logger.error(f"Failed to query perceptual memories by type: end_user_id={end_user_id}, "
|
||||
f"type={perceptual_type} - {str(e)}")
|
||||
raise
|
||||
133
api/app/schemas/memory_perceptual_schema.py
Normal file
133
api/app/schemas/memory_perceptual_schema.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.models.memory_perceptual_model import PerceptualType, FileStorageType
|
||||
|
||||
|
||||
class PerceptualFilter(BaseModel):
|
||||
type: PerceptualType | None = Field(
|
||||
default=None,
|
||||
description="Perceptual type used for filtering the query; optional"
|
||||
)
|
||||
|
||||
|
||||
class PerceptualQuerySchema(BaseModel):
|
||||
filter: PerceptualFilter = Field(
|
||||
default_factory=lambda: PerceptualFilter(),
|
||||
description="Query filter containing perceptual type criteria"
|
||||
)
|
||||
|
||||
page: int = Field(
|
||||
default=1,
|
||||
ge=1,
|
||||
description="Page number for pagination, starting from 1"
|
||||
)
|
||||
|
||||
page_size: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Number of records per page, range 1-100"
|
||||
)
|
||||
|
||||
|
||||
class PerceptualMemoryItem(BaseModel):
|
||||
"""感知记忆项"""
|
||||
id: uuid.UUID = Field(..., description="Unique memory ID")
|
||||
perceptual_type: PerceptualType = Field(..., description="Type of perception, e.g., text, audio, or video")
|
||||
file_path: str = Field(..., description="File path in the storage service")
|
||||
file_name: str = Field(..., description="File name")
|
||||
summary: Optional[str] = Field(None, description="摘要")
|
||||
storage_type: FileStorageType = Field(..., description="Storage type for file")
|
||||
created_time: Optional[datetime] = Field(None, description="创建时间")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PerceptualTimelineResponse(BaseModel):
|
||||
"""感知记忆时间线响应"""
|
||||
total: int = Field(..., description="总数量")
|
||||
page: int = Field(..., description="当前页码")
|
||||
page_size: int = Field(..., description="每页大小")
|
||||
total_pages: int = Field(..., description="总页数")
|
||||
memories: list[PerceptualMemoryItem] = Field(..., description="记忆列表")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# --------------------------
|
||||
# TODO: FileMetaData
|
||||
# --------------------------
|
||||
class Identity(BaseModel):
|
||||
title: str
|
||||
filename: str
|
||||
source: str # upload | crawl | system
|
||||
author: Optional[str] = None
|
||||
|
||||
|
||||
class Semantic(BaseModel):
|
||||
topic: str
|
||||
domain: str
|
||||
difficulty: str # beginner | intermediate | advanced
|
||||
intent: str # informative | instructional | promotional
|
||||
sentiment: str # positive | neutral | negative
|
||||
|
||||
|
||||
class Content(BaseModel):
|
||||
summary: str
|
||||
keywords: list[str]
|
||||
topic: str
|
||||
domain: str
|
||||
|
||||
|
||||
class Usage(BaseModel):
|
||||
target_audience: list[str]
|
||||
use_cases: list[str]
|
||||
|
||||
|
||||
class Stats(BaseModel):
|
||||
duration_sec: Optional[int] = None
|
||||
char_count: int
|
||||
word_count: int
|
||||
|
||||
|
||||
class Processing(BaseModel):
|
||||
transcribed: bool
|
||||
ocr_applied: bool
|
||||
chunked: bool
|
||||
vectorized: bool
|
||||
embedding_model: Optional[str] = None
|
||||
|
||||
|
||||
class VideoModal(BaseModel):
|
||||
scene: list[str]
|
||||
|
||||
|
||||
class AudioModal(BaseModel):
|
||||
speaker_count: int
|
||||
|
||||
|
||||
class TextModal(BaseModel):
|
||||
section_count: int
|
||||
|
||||
|
||||
class Asset(BaseModel):
|
||||
type: str
|
||||
modality: str # text | audio | video
|
||||
format: str # docx | mp3 | mp4
|
||||
language: str
|
||||
encoding: str
|
||||
|
||||
identity: Identity
|
||||
semantic: Semantic
|
||||
content: Content
|
||||
usage: Usage
|
||||
stats: Stats
|
||||
processing: Processing
|
||||
created_at: str
|
||||
modalities: AudioModal | TextModal | VideoModal
|
||||
166
api/app/services/memory_perceptual_service.py
Normal file
166
api/app/services/memory_perceptual_service.py
Normal file
@@ -0,0 +1,166 @@
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.models.memory_perceptual_model import PerceptualType, FileStorageType
|
||||
from app.repositories.memory_perceptual_repository import MemoryPerceptualRepository
|
||||
from app.schemas.memory_perceptual_schema import (
|
||||
PerceptualQuerySchema,
|
||||
PerceptualTimelineResponse,
|
||||
PerceptualMemoryItem,
|
||||
AudioModal, Content, VideoModal, TextModal
|
||||
)
|
||||
|
||||
business_logger = get_business_logger()
|
||||
|
||||
|
||||
class MemoryPerceptualService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.repository = MemoryPerceptualRepository(db)
|
||||
|
||||
def get_memory_count(self, end_user_id: uuid.UUID) -> Dict[str, Any]:
|
||||
"""Retrieve perceptual memory statistics for a user."""
|
||||
business_logger.info(f"Fetching perceptual memory statistics: end_user_id={end_user_id}")
|
||||
try:
|
||||
total_count = self.repository.get_count_by_user_id(end_user_id=end_user_id)
|
||||
|
||||
vision_count = self.repository.get_count_by_type(end_user_id, PerceptualType.VISION)
|
||||
audio_count = self.repository.get_count_by_type(end_user_id, PerceptualType.AUDIO)
|
||||
text_count = self.repository.get_count_by_type(end_user_id, PerceptualType.TEXT)
|
||||
conversation_count = self.repository.get_count_by_type(end_user_id, PerceptualType.CONVERSATION)
|
||||
|
||||
stats = {
|
||||
"total": total_count,
|
||||
"by_type": {
|
||||
"vision": vision_count,
|
||||
"audio": audio_count,
|
||||
"text": text_count,
|
||||
"conversation": conversation_count
|
||||
}
|
||||
}
|
||||
|
||||
business_logger.info(f"Memory statistics fetched successfully: total={total_count}")
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
business_logger.error(f"Failed to fetch memory statistics: {str(e)}")
|
||||
raise BusinessException(f"Failed to fetch memory statistics: {str(e)}", BizCode.DB_ERROR)
|
||||
|
||||
def _get_latest_memory_by_type(
|
||||
self,
|
||||
end_user_id: uuid.UUID,
|
||||
perceptual_type: PerceptualType
|
||||
) -> Optional[dict[str, Any]]:
|
||||
"""Internal helper to retrieve the latest memory by type."""
|
||||
business_logger.info(f"Fetching latest {perceptual_type.name.lower()} memory: end_user_id={end_user_id}")
|
||||
try:
|
||||
memories = self.repository.get_by_type(
|
||||
end_user_id=end_user_id,
|
||||
perceptual_type=perceptual_type,
|
||||
limit=1,
|
||||
offset=0
|
||||
)
|
||||
if not memories:
|
||||
business_logger.info(f"No {perceptual_type.name.lower()} memory found: end_user_id={end_user_id}")
|
||||
return None
|
||||
|
||||
memory = memories[0]
|
||||
meta_data = memory.meta_data or {}
|
||||
modalities = meta_data.get("modalities")
|
||||
content = meta_data.get("content")
|
||||
|
||||
if not modalities:
|
||||
raise BusinessException(f"Modalities not defined, perceptual memory_id={memory.id}", BizCode.DB_ERROR)
|
||||
if not content:
|
||||
raise BusinessException(f"Content not defined, perceptual memory_id={memory.id}", BizCode.DB_ERROR)
|
||||
content = Content(**content)
|
||||
match perceptual_type:
|
||||
case PerceptualType.VISION:
|
||||
modal = VideoModal(**modalities)
|
||||
case PerceptualType.AUDIO:
|
||||
modal = AudioModal(**modalities)
|
||||
case PerceptualType.TEXT:
|
||||
modal = TextModal(**modalities)
|
||||
case _:
|
||||
raise BusinessException("Unsupported perceptual type", BizCode.DB_ERROR)
|
||||
detail = modal.model_dump()
|
||||
|
||||
result = {
|
||||
"id": str(memory.id),
|
||||
"file_name": memory.file_name,
|
||||
"file_path": memory.file_path,
|
||||
"storage_type": memory.storage_service,
|
||||
"summary": memory.summary,
|
||||
"keywords": content.keywords,
|
||||
"topic": content.topic,
|
||||
"domain": content.domain,
|
||||
"created_time": memory.created_time.isoformat() if memory.created_time else None,
|
||||
**detail
|
||||
}
|
||||
|
||||
business_logger.info(
|
||||
f"Latest {perceptual_type.name.lower()} memory retrieved successfully: file={memory.file_name}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
business_logger.error(f"Failed to fetch latest {perceptual_type.name.lower()} memory: {str(e)}")
|
||||
raise BusinessException(f"Failed to fetch latest {perceptual_type.name.lower()} memory: {str(e)}",
|
||||
BizCode.DB_ERROR)
|
||||
|
||||
def get_latest_visual_memory(self, end_user_id: uuid.UUID) -> Optional[Dict[str, Any]]:
|
||||
return self._get_latest_memory_by_type(end_user_id, PerceptualType.VISION)
|
||||
|
||||
def get_latest_audio_memory(self, end_user_id: uuid.UUID) -> Optional[Dict[str, Any]]:
|
||||
return self._get_latest_memory_by_type(end_user_id, PerceptualType.AUDIO)
|
||||
|
||||
def get_latest_text_memory(self, end_user_id: uuid.UUID) -> Optional[Dict[str, Any]]:
|
||||
return self._get_latest_memory_by_type(end_user_id, PerceptualType.TEXT)
|
||||
|
||||
def get_time_line(self, end_user_id: uuid.UUID, query: PerceptualQuerySchema) -> PerceptualTimelineResponse:
|
||||
"""Retrieve a timeline of perceptual memories for a user."""
|
||||
business_logger.info(f"Fetching perceptual memory timeline: "
|
||||
f"end_user_id={end_user_id}, filter={query.filter}")
|
||||
|
||||
try:
|
||||
if query.page < 1:
|
||||
raise BusinessException("Page number must be greater than 0", BizCode.INVALID_PARAMETER)
|
||||
if query.page_size < 1 or query.page_size > 100:
|
||||
raise BusinessException("Page size must be between 1 and 100", BizCode.INVALID_PARAMETER)
|
||||
|
||||
total_count, memories = self.repository.get_timeline(end_user_id, query)
|
||||
|
||||
memory_items = []
|
||||
for memory in memories:
|
||||
memory_item = PerceptualMemoryItem(
|
||||
id=memory.id,
|
||||
perceptual_type=PerceptualType(memory.perceptual_type),
|
||||
file_path=memory.file_path,
|
||||
file_name=memory.file_name,
|
||||
summary=memory.summary,
|
||||
created_time=memory.created_time,
|
||||
storage_type=FileStorageType(memory.storage_service),
|
||||
)
|
||||
memory_items.append(memory_item)
|
||||
|
||||
timeline_response = PerceptualTimelineResponse(
|
||||
total=total_count,
|
||||
page=query.page,
|
||||
page_size=query.page_size,
|
||||
total_pages=(total_count + query.page_size - 1) // query.page_size,
|
||||
memories=memory_items
|
||||
)
|
||||
|
||||
business_logger.info(f"Perceptual memory timeline retrieved successfully: "
|
||||
f"total={total_count}, returned={len(memories)}")
|
||||
return timeline_response
|
||||
|
||||
except BusinessException:
|
||||
raise
|
||||
except Exception as e:
|
||||
business_logger.error(f"Failed to fetch perceptual memory timeline: {str(e)}")
|
||||
raise BusinessException(f"Failed to fetch perceptual memory timeline: {str(e)}", BizCode.DB_ERROR)
|
||||
@@ -166,6 +166,8 @@ class PromptOptimizerService:
|
||||
model_config = self.get_model_config(tenant_id, model_id)
|
||||
session_history = self.get_session_message_history(session_id=session_id, user_id=user_id)
|
||||
|
||||
logger.info(f"Prompt optimization started, user_id={user_id}, session_id={session_id}")
|
||||
|
||||
# Create LLM instance
|
||||
api_config: ModelApiKey = model_config.api_keys[0]
|
||||
llm = RedBearLLM(RedBearModelConfig(
|
||||
@@ -203,7 +205,6 @@ class PromptOptimizerService:
|
||||
|
||||
messages.extend(session_history[:-1]) # last message is current message
|
||||
messages.extend([(RoleType.USER.value, rendered_user_message)])
|
||||
logger.info(f"Prompt optimization message: {messages}")
|
||||
buffer = ""
|
||||
prompt_started = False
|
||||
prompt_finished = False
|
||||
@@ -250,6 +251,7 @@ class PromptOptimizerService:
|
||||
content=desc
|
||||
)
|
||||
variables = self.parser_prompt_variables(optim_result.get("prompt"))
|
||||
logger.info(f"Prompt optimization completed, user_id={user_id}, session_id={session_id}")
|
||||
yield {"desc": optim_result.get("desc"), "variables": variables}
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user