From c52b3600683bda7950a76238e0aed61f28e28346 Mon Sep 17 00:00:00 2001 From: Eternity <61316157+myhMARS@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:00:22 +0800 Subject: [PATCH] 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 --- .../memory_perceptual_controller.py | 255 ++++++++++++++++++ .../workflow/nodes/http_request/config.py | 6 +- .../core/workflow/nodes/http_request/node.py | 2 +- api/app/models/memory_perceptual_model.py | 40 +++ .../memory_perceptual_repository.py | 156 +++++++++++ api/app/schemas/memory_perceptual_schema.py | 133 +++++++++ api/app/services/memory_perceptual_service.py | 166 ++++++++++++ api/app/services/prompt_optimizer_service.py | 4 +- 8 files changed, 758 insertions(+), 4 deletions(-) create mode 100644 api/app/controllers/memory_perceptual_controller.py create mode 100644 api/app/models/memory_perceptual_model.py create mode 100644 api/app/repositories/memory_perceptual_repository.py create mode 100644 api/app/schemas/memory_perceptual_schema.py create mode 100644 api/app/services/memory_perceptual_service.py diff --git a/api/app/controllers/memory_perceptual_controller.py b/api/app/controllers/memory_perceptual_controller.py new file mode 100644 index 00000000..5154c763 --- /dev/null +++ b/api/app/controllers/memory_perceptual_controller.py @@ -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", + ) diff --git a/api/app/core/workflow/nodes/http_request/config.py b/api/app/core/workflow/nodes/http_request/config.py index 6bb7baaf..810a716f 100644 --- a/api/app/core/workflow/nodes/http_request/config.py +++ b/api/app/core/workflow/nodes/http_request/config.py @@ -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 diff --git a/api/app/core/workflow/nodes/http_request/node.py b/api/app/core/workflow/nodes/http_request/node.py index 4374d847..2e5de796 100644 --- a/api/app/core/workflow/nodes/http_request/node.py +++ b/api/app/core/workflow/nodes/http_request/node.py @@ -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 = {} diff --git a/api/app/models/memory_perceptual_model.py b/api/app/models/memory_perceptual_model.py new file mode 100644 index 00000000..59eb0222 --- /dev/null +++ b/api/app/models/memory_perceptual_model.py @@ -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="创建时间") diff --git a/api/app/repositories/memory_perceptual_repository.py b/api/app/repositories/memory_perceptual_repository.py new file mode 100644 index 00000000..8415c2d0 --- /dev/null +++ b/api/app/repositories/memory_perceptual_repository.py @@ -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 diff --git a/api/app/schemas/memory_perceptual_schema.py b/api/app/schemas/memory_perceptual_schema.py new file mode 100644 index 00000000..41b74a36 --- /dev/null +++ b/api/app/schemas/memory_perceptual_schema.py @@ -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 diff --git a/api/app/services/memory_perceptual_service.py b/api/app/services/memory_perceptual_service.py new file mode 100644 index 00000000..a74dc5a7 --- /dev/null +++ b/api/app/services/memory_perceptual_service.py @@ -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) diff --git a/api/app/services/prompt_optimizer_service.py b/api/app/services/prompt_optimizer_service.py index b3ac1b79..135ddc5d 100644 --- a/api/app/services/prompt_optimizer_service.py +++ b/api/app/services/prompt_optimizer_service.py @@ -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