# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from datetime import datetime from enum import IntEnum from pydantic import BaseModel, model_validator from sqlalchemy import Float, Integer from sqlalchemy_utils import ChoiceType from sqlmodel import JSON, Column, Field, SmallInteger, String from app.model.abstract.model import AbstractModel, DefaultTimes class ChatStatus(IntEnum): ongoing = 1 done = 2 class ChatHistory(AbstractModel, DefaultTimes, table=True): """ Chat history model with timestamp tracking. Inherits from DefaultTimes which provides: - created_at: timestamp when record is created (auto-populated) - updated_at: timestamp when record is last modified (auto-updated) - deleted_at: timestamp for soft deletion (nullable) For legacy records without timestamps, sorting falls back to id ordering. """ id: int = Field(default=None, primary_key=True) user_id: int = Field(index=True) task_id: str = Field(index=True, unique=True) project_id: str = Field(index=True, unique=False, nullable=True) question: str language: str model_platform: str model_type: str api_key: str api_url: str = Field(sa_column=Column(String(500))) max_retries: int = Field(default=3) file_save_path: str | None = None installed_mcp: str = Field(sa_type=JSON, default={}) project_name: str = Field(default="", sa_column=Column(String(128))) summary: str = Field(default="", sa_column=Column(String(1024))) tokens: int = Field(default=0, sa_column=(Column(Integer, server_default="0"))) spend: float = Field(default=0, sa_column=(Column(Float, server_default="0"))) status: int = Field(default=1, sa_column=Column(ChoiceType(ChatStatus, SmallInteger()))) class ChatHistoryIn(BaseModel): task_id: str project_id: str | None = None user_id: int | None = None question: str language: str model_platform: str model_type: str api_key: str | None = "" api_url: str | None = None max_retries: int = 3 file_save_path: str | None = None installed_mcp: str | None = None project_name: str | None = None summary: str | None = None tokens: int = 0 spend: float = 0 status: int = ChatStatus.ongoing.value class ChatHistoryOut(BaseModel): id: int task_id: str project_id: str | None = None question: str language: str model_platform: str model_type: str api_key: str | None = None api_url: str | None = None max_retries: int file_save_path: str | None = None installed_mcp: str | None = None project_name: str | None = None summary: str | None = None tokens: int status: int created_at: datetime | None = None updated_at: datetime | None = None @model_validator(mode="after") def fill_project_id_from_task_id(self): """Fill project_id from task_id when project_id is None""" if self.project_id is None: self.project_id = self.task_id return self @model_validator(mode="after") def handle_legacy_timestamps(self): """Handle legacy records that might not have timestamp fields""" # For old records without timestamps, we rely on database-level defaults # The sorting in the controller will handle ordering appropriately return self class ChatHistoryUpdate(BaseModel): project_name: str | None = None summary: str | None = None tokens: int | None = None status: int | None = None project_id: str | None = None