diff --git a/app/api/annotation_api.py b/app/api/annotation_api.py index f974189..558152e 100644 --- a/app/api/annotation_api.py +++ b/app/api/annotation_api.py @@ -2,6 +2,7 @@ from app.core.response import ResponseMessage from app.core.status import CommonCode +from app.schemas.annotation.hierarchical_response_model import HierarchicalDBMSAnnotation from app.schemas.annotation.request_model import AnnotationCreateRequest from app.schemas.annotation.response_model import AnnotationDeleteResponse, FullAnnotationResponse from app.services.annotation_service import AnnotationService, annotation_service @@ -59,6 +60,22 @@ def get_annotation_by_db_profile_id( return ResponseMessage.success(value=annotation, code=CommonCode.SUCCESS_FIND_ANNOTATION) +@router.get( + "/find/hierarchical/{db_profile_id}", + response_model=ResponseMessage[HierarchicalDBMSAnnotation], + summary="DB 프로필 ID로 계층적 어노테이션 조회", +) +def get_hierarchical_annotation_by_db_profile_id( + db_profile_id: str, + service: AnnotationService = annotation_service_dependency, +) -> ResponseMessage[HierarchicalDBMSAnnotation]: + """ + `db_profile_id`에 연결된 어노테이션을 계층 구조(DBMS > DB > 스키마 > 테이블 > 컬럼)로 조회합니다. + """ + annotation = service.get_hierarchical_annotation_by_db_profile_id(db_profile_id) + return ResponseMessage.success(value=annotation, code=CommonCode.SUCCESS_FIND_ANNOTATION) + + @router.delete( "/remove/{annotation_id}", response_model=ResponseMessage[AnnotationDeleteResponse], diff --git a/app/core/status.py b/app/core/status.py index 6b88bc4..5e87c8b 100644 --- a/app/core/status.py +++ b/app/core/status.py @@ -118,7 +118,12 @@ class CommonCode(Enum): "5002", "데이터 생성 후 검증 과정에서 에러가 발생했습니다.", ) - + FAIL_AI_SERVER_CONNECTION = (status.HTTP_503_SERVICE_UNAVAILABLE, "5003", "AI 서버 연결에 실패했습니다.") + FAIL_AI_SERVER_PROCESSING = ( + status.HTTP_500_INTERNAL_SERVER_ERROR, + "5004", + "AI 서버가 요청을 처리하는 데 실패했습니다.", + ) """ DRIVER, DB 서버 에러 코드 - 51xx """ FAIL_CONNECT_DB = (status.HTTP_500_INTERNAL_SERVER_ERROR, "5100", "디비 연결 중 에러가 발생했습니다.") FAIL_FIND_PROFILE = (status.HTTP_500_INTERNAL_SERVER_ERROR, "5101", "디비 정보 조회 중 에러가 발생했습니다.") @@ -154,12 +159,6 @@ class CommonCode(Enum): FAIL_CREATE_ANNOTATION = (status.HTTP_500_INTERNAL_SERVER_ERROR, "5400", "어노테이션 생성 중 에러가 발생했습니다.") FAIL_FIND_ANNOTATION = (status.HTTP_500_INTERNAL_SERVER_ERROR, "5401", "어노테이션 조회 중 에러가 발생했습니다.") FAIL_DELETE_ANNOTATION = (status.HTTP_500_INTERNAL_SERVER_ERROR, "5402", "어노테이션 삭제 중 에러가 발생했습니다.") - FAIL_AI_SERVER_CONNECTION = (status.HTTP_503_SERVICE_UNAVAILABLE, "5403", "AI 서버 연결에 실패했습니다.") - FAIL_AI_SERVER_PROCESSING = ( - status.HTTP_500_INTERNAL_SERVER_ERROR, - "5404", - "AI 서버가 요청을 처리하는 데 실패했습니다.", - ) """ SQL 서버 에러 코드 - 55xx """ diff --git a/app/repository/annotation_repository.py b/app/repository/annotation_repository.py index 8ad55c3..2ccbac2 100644 --- a/app/repository/annotation_repository.py +++ b/app/repository/annotation_repository.py @@ -1,5 +1,8 @@ +import logging import sqlite3 +from app.core.exceptions import APIException +from app.core.status import CommonCode from app.core.utils import get_db_path from app.schemas.annotation.db_model import ( ColumnAnnotationInDB, @@ -10,6 +13,13 @@ TableAnnotationInDB, TableConstraintInDB, ) +from app.schemas.annotation.hierarchical_response_model import ( + HierarchicalColumnAnnotation, + HierarchicalDBAnnotation, + HierarchicalDBMSAnnotation, + HierarchicalRelationshipAnnotation, + HierarchicalTableAnnotation, +) from app.schemas.annotation.response_model import ( ColumnAnnotationDetail, ConstraintDetail, @@ -99,7 +109,7 @@ def create_full_annotation( ( c.id, c.table_annotation_id, - c.constraint_type, + c.constraint_type.value, c.name, c.description, c.expression, @@ -279,6 +289,139 @@ def find_full_annotation_by_id(self, annotation_id: str) -> FullAnnotationRespon if conn: conn.close() + def find_hierarchical_annotation_by_profile_id(self, db_profile_id: str) -> HierarchicalDBMSAnnotation | None: + """ + db_profile_id로 계층적 어노테이션 정보를 조회합니다. + - DBMS > DB > 테이블 > 컬럼 구조로 데이터를 조립하여 반환합니다. + """ + db_path = get_db_path() + conn = None + try: + conn = sqlite3.connect(str(db_path), timeout=10) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 1. 기본 정보 조회 (db_profile, database_annotation) + cursor.execute( + """ + SELECT + dp.type as dbms_type, + da.id as annotation_id, + da.db_profile_id, + da.database_name, + da.description as db_description, + da.created_at, + da.updated_at + FROM db_profile dp + JOIN database_annotation da ON dp.annotation_id = da.id + WHERE dp.id = ? + """, + (db_profile_id,), + ) + base_info = cursor.fetchone() + if not base_info: + return None + + # 2. 테이블 및 컬럼 정보 한번에 조회 + cursor.execute( + """ + SELECT + ta.id as table_id, + ta.table_name, + ta.description as table_description, + ca.column_name, + ca.description as column_description, + ca.data_type + FROM table_annotation ta + JOIN column_annotation ca ON ta.id = ca.table_annotation_id + WHERE ta.database_annotation_id = ? + ORDER BY ta.table_name, ca.ordinal_position + """, + (base_info["annotation_id"],), + ) + rows = cursor.fetchall() + + # 3. 데이터 계층 구조로 조립 (테이블, 컬럼) + tables_map = {} + for row in rows: + table_name = row["table_name"] + if table_name not in tables_map: + tables_map[table_name] = HierarchicalTableAnnotation( + table_name=table_name, + description=row["table_description"], + columns=[], + ) + tables_map[table_name].columns.append( + HierarchicalColumnAnnotation( + column_name=row["column_name"], + description=row["column_description"], + data_type=row["data_type"], + ) + ) + + # 4. 관계 정보 조회 + cursor.execute( + """ + SELECT + ta_from.table_name as from_table, + ca_from.column_name as from_column, + tc.ref_table as to_table, + cc.referenced_column_name as to_column, + tc.name as constraint_name, + tc.description as relationship_description + FROM table_constraint tc + JOIN table_annotation ta_from ON tc.table_annotation_id = ta_from.id + JOIN constraint_column cc ON tc.id = cc.constraint_id + JOIN column_annotation ca_from ON cc.column_annotation_id = ca_from.id + WHERE ta_from.database_annotation_id = ? AND tc.constraint_type = 'FOREIGN KEY' + ORDER BY tc.name, cc.position + """, + (base_info["annotation_id"],), + ) + relationship_rows = cursor.fetchall() + logging.info(f"Raw relationship rows from DB: {[dict(row) for row in relationship_rows]}") + + relationships_map = {} + for row in relationship_rows: + constraint_name = row["constraint_name"] + if constraint_name not in relationships_map: + relationships_map[constraint_name] = { + "from_table": row["from_table"], + "to_table": row["to_table"], + "description": row["relationship_description"], + "from_columns": [], + "to_columns": [], + } + relationships_map[constraint_name]["from_columns"].append(row["from_column"]) + relationships_map[constraint_name]["to_columns"].append(row["to_column"]) + + logging.info(f"Processed relationships map: {relationships_map}") + relationships = [HierarchicalRelationshipAnnotation(**data) for data in relationships_map.values()] + logging.info(f"Final relationships list: {relationships}") + + # 5. 최종 데이터 조립 + db = HierarchicalDBAnnotation( + db_name=base_info["database_name"], + description=base_info["db_description"], + tables=list(tables_map.values()), + relationships=relationships, + ) + + return HierarchicalDBMSAnnotation( + dbms_type=base_info["dbms_type"], + databases=[db], + annotation_id=base_info["annotation_id"], + db_profile_id=base_info["db_profile_id"], + created_at=base_info["created_at"], + updated_at=base_info["updated_at"], + ) + + except sqlite3.Error as e: + raise APIException(CommonCode.FAIL_FIND_ANNOTATION) from e + finally: + if conn: + conn.close() + def delete_annotation_by_id(self, annotation_id: str) -> bool: """ annotationId로 특정 어노테이션을 삭제합니다. diff --git a/app/repository/chat_tab_repository.py b/app/repository/chat_tab_repository.py index 472056f..0f40f3a 100644 --- a/app/repository/chat_tab_repository.py +++ b/app/repository/chat_tab_repository.py @@ -56,7 +56,7 @@ def updated_chat_tab(self, id: str, new_name: str | None) -> ChatTabInDB | None: # 데이터 업데이트 cursor.execute( - "UPDATE chat_tab SET name = ?, updated_at = datetime('now', 'localtime') WHERE id = ?", + "UPDATE chat_tab SET name = ?, updated_at = datetime('now') WHERE id = ?", (new_name, id), ) conn.commit() @@ -73,6 +73,20 @@ def updated_chat_tab(self, id: str, new_name: str | None) -> ChatTabInDB | None: if conn: conn.close() + def update_tab_timestamp(self, id: str) -> bool: + """지정된 ID의 채팅 탭의 updated_at 타임스탬프를 현재 시간으로 업데이트합니다.""" + db_path = get_db_path() + conn = None + try: + conn = sqlite3.connect(str(db_path), timeout=10) + cursor = conn.cursor() + cursor.execute("UPDATE chat_tab SET updated_at = datetime('now') WHERE id = ?", (id,)) + conn.commit() + return cursor.rowcount > 0 + finally: + if conn: + conn.close() + def delete_chat_tab(self, id: str) -> bool: """채팅 탭ID에 해당하는 ChatTab을 삭제하고, 성공 여부를 반환합니다.""" db_path = get_db_path() diff --git a/app/schemas/annotation/hierarchical_response_model.py b/app/schemas/annotation/hierarchical_response_model.py new file mode 100644 index 0000000..005ef06 --- /dev/null +++ b/app/schemas/annotation/hierarchical_response_model.py @@ -0,0 +1,39 @@ +from datetime import datetime + +from pydantic import BaseModel, Field + + +class HierarchicalColumnAnnotation(BaseModel): + column_name: str = Field(..., description="컬럼 이름") + description: str | None = Field(None, description="AI가 생성한 컬럼 어노테이션") + data_type: str | None = Field(None, description="컬럼 데이터 타입") + + +class HierarchicalTableAnnotation(BaseModel): + table_name: str = Field(..., description="테이블 이름") + description: str | None = Field(None, description="AI가 생성한 테이블 어노테이션") + columns: list[HierarchicalColumnAnnotation] = Field(..., description="테이블에 속한 컬럼 목록") + + +class HierarchicalRelationshipAnnotation(BaseModel): + from_table: str = Field(..., description="외래 키 제약조건이 시작되는 테이블") + from_columns: list[str] = Field(..., description="외래 키에 포함된 컬럼들") + to_table: str = Field(..., description="외래 키가 참조하는 테이블") + to_columns: list[str] = Field(..., description="참조되는 테이블의 컬럼들") + description: str | None = Field(None, description="AI가 생성한 관계 어노테이션") + + +class HierarchicalDBAnnotation(BaseModel): + db_name: str = Field(..., description="데이터베이스 이름") + description: str | None = Field(None, description="AI가 생성한 데이터베이스 어노테이션") + tables: list[HierarchicalTableAnnotation] = Field(..., description="데이터베이스에 속한 테이블 목록") + relationships: list[HierarchicalRelationshipAnnotation] = Field(..., description="테이블 간의 관계 목록") + + +class HierarchicalDBMSAnnotation(BaseModel): + dbms_type: str = Field(..., description="DBMS 종류 (e.g., postgresql, oracle)") + databases: list[HierarchicalDBAnnotation] = Field(..., description="DBMS에 속한 데이터베이스 목록") + annotation_id: str = Field(..., description="최상위 어노테이션 ID") + db_profile_id: str = Field(..., description="DB 프로필 ID") + created_at: datetime = Field(..., description="생성 일시") + updated_at: datetime = Field(..., description="수정 일시") diff --git a/app/schemas/chat_message/base_model.py b/app/schemas/chat_message/base_model.py index 1e765d0..77efb7f 100644 --- a/app/schemas/chat_message/base_model.py +++ b/app/schemas/chat_message/base_model.py @@ -29,4 +29,4 @@ def validate_chat_tab_id(self, field_value: str) -> None: def validate_message(self, field_value: str) -> None: """메시지 유효성 검사""" if not field_value or field_value.strip() == "": - raise APIException(CommonCode.INVALID_ANNOTATION_REQUEST) + raise APIException(CommonCode.INVALID_CHAT_MESSAGE_REQUEST) diff --git a/app/services/annotation_service.py b/app/services/annotation_service.py index 5da83e2..f794205 100644 --- a/app/services/annotation_service.py +++ b/app/services/annotation_service.py @@ -1,8 +1,10 @@ import logging +import os import sqlite3 from datetime import datetime from typing import Any +import httpx from fastapi import Depends from app.core.enum.constraint_type import ConstraintTypeEnum @@ -29,6 +31,7 @@ TableAnnotationInDB, TableConstraintInDB, ) +from app.schemas.annotation.hierarchical_response_model import HierarchicalDBMSAnnotation from app.schemas.annotation.request_model import AnnotationCreateRequest from app.schemas.annotation.response_model import AnnotationDeleteResponse, FullAnnotationResponse from app.schemas.user_db.db_profile_model import AllDBProfileInfo @@ -37,9 +40,6 @@ user_db_service_dependency = Depends(lambda: user_db_service) -# AI 서버의 주소 (임시) -AI_SERVER_URL = "http://localhost:8001/api/v1/annotate/database" - class AnnotationService: def __init__( @@ -54,13 +54,24 @@ def __init__( """ self.repository = repository self.user_db_service = user_db_serv + self._ai_server_url = None + + def _get_ai_server_url(self) -> str: + """AI 서버 URL을 한 번만 로드하고 캐싱하여 재사용합니다 (지연 로딩).""" + if self._ai_server_url is None: + url = os.getenv("ENV_AI_SERVER_URL") + if not url: + raise ValueError("환경 변수 'ENV_AI_SERVER_URL'가 설정되지 않았거나 .env 파일 로드에 실패했습니다.") + # URL 경로를 annotator로 변경 + self._ai_server_url = url.replace("/chat", "/annotator") + return self._ai_server_url async def create_annotation(self, request: AnnotationCreateRequest) -> FullAnnotationResponse: """ 어노테이션 생성을 위한 전체 프로세스를 관장합니다. 1. DB 프로필, 전체 스키마 정보, 샘플 데이터 조회 2. AI 서버에 요청할 데이터 모델 생성 - 3. TODO: AI 서버에 요청 (현재는 Mock 데이터 사용) + 3. AI 서버에 요청 4. 트랜잭션 내에서 전체 어노테이션 정보 저장 및 DB 프로필 업데이트 """ logging.info(f"Starting annotation creation for db_profile_id: {request.db_profile_id}") @@ -82,12 +93,12 @@ async def create_annotation(self, request: AnnotationCreateRequest) -> FullAnnot # 2. AI 서버에 요청할 데이터 모델 생성 ai_request_body = self._prepare_ai_request_body(db_profile, full_schema_info, sample_rows) logging.info("Prepared AI request body.") - logging.debug(f"AI Request Body: {ai_request_body.model_dump_json(indent=2)}") + logging.info(f"AI Request Body: {ai_request_body.model_dump_json(indent=2)}") - # 3. AI 서버에 요청 (현재는 Mock 데이터 사용) + # 3. AI 서버에 요청 ai_response = await self._request_annotation_to_ai_server(ai_request_body) logging.info("Received AI response.") - logging.debug(f"AI Response: {ai_response}") + logging.info(f"AI Response: {ai_response}") # 4. 트랜잭션 내에서 전체 어노테이션 정보 저장 및 DB 프로필 업데이트 db_path = get_db_path() @@ -134,6 +145,15 @@ def get_annotation_by_db_profile_id(self, db_profile_id: str) -> FullAnnotationR return self.get_full_annotation(db_profile.annotation_id) + def get_hierarchical_annotation_by_db_profile_id(self, db_profile_id: str) -> HierarchicalDBMSAnnotation: + """ + db_profile_id를 기반으로 계층적인 어노테이션 정보를 조회합니다. + """ + annotation = self.repository.find_hierarchical_annotation_by_profile_id(db_profile_id) + if not annotation: + raise APIException(CommonCode.NO_ANNOTATION_FOR_PROFILE) + return annotation + def _prepare_ai_request_body( self, db_profile: AllDBProfileInfo, @@ -194,7 +214,11 @@ def _prepare_ai_request_body( database_name=db_profile.name or db_profile.username, tables=ai_tables, relationships=ai_relationships ) - return AIAnnotationRequest(dbms_type=db_profile.type, databases=[ai_database]) + request_body = AIAnnotationRequest(dbms_type=db_profile.type, databases=[ai_database]) + + logging.info(request_body.model_dump_json(indent=2)) + + return request_body def _transform_ai_response_to_db_models( self, @@ -209,6 +233,12 @@ def _transform_ai_response_to_db_models( now = datetime.now() annotation_id = generate_prefixed_uuid(DBSaveIdEnum.database_annotation.value) + # AI 응답에서 데이터베이스 레벨의 정보 추출 + db_data = ai_response.get("databases", [{}])[0] + db_description = db_data.get("description") + tables_data = db_data.get("tables", []) + relationships_data = db_data.get("relationships", []) + # 원본 스키마 정보를 쉽게 조회할 수 있도록 룩업 테이블 생성 schema_lookup: dict[str, UserDBTableInfo] = {table.name: table for table in full_schema_info} @@ -216,7 +246,7 @@ def _transform_ai_response_to_db_models( id=annotation_id, db_profile_id=db_profile_id, database_name=db_profile.name or db_profile.username, - description=ai_response.get("database_annotation"), + description=db_description, created_at=now, updated_at=now, ) @@ -237,7 +267,7 @@ def _transform_ai_response_to_db_models( [], ) - for tbl_data in ai_response.get("tables", []): + for tbl_data in tables_data: original_table = schema_lookup.get(tbl_data["table_name"]) if not original_table: logging.warning( @@ -252,7 +282,7 @@ def _transform_ai_response_to_db_models( constraint_col_annos, index_annos, index_col_annos, - ) = self._create_annotations_for_table(tbl_data, original_table, annotation_id, now) + ) = self._create_annotations_for_table(tbl_data, original_table, annotation_id, now, relationships_data) all_table_annos.append(table_anno) all_col_annos.extend(col_annos) @@ -277,6 +307,7 @@ def _create_annotations_for_table( original_table: UserDBTableInfo, database_annotation_id: str, now: datetime, + relationships_data: list[dict[str, Any]], ) -> tuple: """ 단일 테이블에 대한 모든 하위 어노테이션(컬럼, 제약조건, 인덱스)을 생성합니다. @@ -286,7 +317,7 @@ def _create_annotations_for_table( id=table_id, database_annotation_id=database_annotation_id, table_name=original_table.name, - description=tbl_data.get("annotation"), + description=tbl_data.get("description"), created_at=now, updated_at=now, ) @@ -297,7 +328,7 @@ def _create_annotations_for_table( col_annos = self._process_columns(tbl_data, original_table, table_id, col_map, now) constraint_annos, constraint_col_annos = self._process_constraints( - tbl_data, original_table, table_id, col_map, now + original_table, table_id, col_map, now, relationships_data ) index_annos, index_col_annos = self._process_indexes(tbl_data, original_table, table_id, col_map, now) @@ -310,10 +341,12 @@ def _process_columns( 테이블의 컬럼 어노테이션 모델 리스트를 생성합니다. """ col_annos = [] - for col_data in tbl_data.get("columns", []): - original_column = next((c for c in original_table.columns if c.name == col_data["column_name"]), None) - if not original_column: - continue + ai_columns_lookup = {c["column_name"]: c for c in tbl_data.get("columns", [])} + + for original_column in original_table.columns: + col_data = ai_columns_lookup.get(original_column.name) + description = col_data.get("description") if col_data else None + col_annos.append( ColumnAnnotationInDB( id=col_map[original_column.name], @@ -322,7 +355,7 @@ def _process_columns( data_type=original_column.type, is_nullable=1 if original_column.nullable else 0, default_value=original_column.default, - description=col_data.get("annotation"), + description=description, ordinal_position=original_column.ordinal_position, created_at=now, updated_at=now, @@ -331,16 +364,33 @@ def _process_columns( return col_annos def _process_constraints( - self, tbl_data: dict, original_table: UserDBTableInfo, table_id: str, col_map: dict, now: datetime + self, + original_table: UserDBTableInfo, + table_id: str, + col_map: dict, + now: datetime, + relationships_data: list[dict[str, Any]], ) -> tuple[list[TableConstraintInDB], list[ConstraintColumnInDB]]: """ 테이블의 제약조건 및 제약조건 컬럼 어노테이션 모델 리스트를 생성합니다. + AI 응답의 'relationships'를 기반으로 FK 제약조건의 설명을 매칭합니다. """ constraint_annos, constraint_col_annos = [], [] - for const_data in tbl_data.get("constraints", []): - original_constraint = next((c for c in original_table.constraints if c.name == const_data["name"]), None) - if not original_constraint: - continue + + for original_constraint in original_table.constraints: + annotation = None + # 외래 키 제약조건인 경우, AI 응답의 relationships에서 설명을 찾습니다. + if original_constraint.type == ConstraintTypeEnum.FOREIGN_KEY.value: + for rel in relationships_data: + if ( + rel.get("from_table") == original_table.name + and set(rel.get("from_columns", [])) == set(original_constraint.columns) + and rel.get("to_table") == original_constraint.referenced_table + and set(rel.get("to_columns", [])) == set(original_constraint.referenced_columns) + ): + annotation = rel.get("description") + break + const_id = generate_prefixed_uuid(DBSaveIdEnum.table_constraint.value) constraint_annos.append( TableConstraintInDB( @@ -348,7 +398,7 @@ def _process_constraints( table_annotation_id=table_id, name=original_constraint.name, constraint_type=ConstraintTypeEnum(original_constraint.type), - description=const_data.get("annotation"), + description=annotation, ref_table=original_constraint.referenced_table, expression=original_constraint.check_expression, on_update_action=original_constraint.on_update, @@ -441,20 +491,30 @@ def delete_annotation(self, annotation_id: str) -> AnnotationDeleteResponse: async def _request_annotation_to_ai_server(self, ai_request: AIAnnotationRequest) -> dict: """AI 서버에 스키마 정보를 보내고 어노테이션을 받아옵니다.""" - # 우선은 목업 데이터 활용 - return self._get_mock_ai_response(ai_request) - - # Real implementation below - # request_body = ai_request.model_dump() - # async with httpx.AsyncClient() as client: - # try: - # response = await client.post(AI_SERVER_URL, json=request_body, timeout=60.0) - # response.raise_for_status() - # return response.json() - # except httpx.HTTPStatusError as e: - # raise APIException(CommonCode.FAIL_AI_SERVER_PROCESSING, detail=f"AI server error: {e.response.text}") from e - # except httpx.RequestError as e: - # raise APIException(CommonCode.FAIL_AI_SERVER_CONNECTION, detail=f"AI server connection failed: {e}") from e + ai_server_url = self._get_ai_server_url() + request_body = ai_request.model_dump() + + logging.info(f"Requesting annotation to AI server at {ai_server_url}") + logging.info(f"Request Body: {request_body}") + + async with httpx.AsyncClient() as client: + try: + response = await client.post(ai_server_url, json=request_body, timeout=60.0) + response.raise_for_status() + ai_response = response.json() + logging.info("Successfully received annotation response from AI server.") + logging.info(f"AI Response JSON: {ai_response}") + return ai_response + except httpx.HTTPStatusError as e: + logging.error(f"AI server returned an error: {e.response.status_code} - {e.response.text}") + raise APIException( + CommonCode.FAIL_AI_SERVER_PROCESSING, detail=f"AI server error: {e.response.text}" + ) from e + except httpx.RequestError as e: + logging.error(f"Failed to connect to AI server: {e}") + raise APIException( + CommonCode.FAIL_AI_SERVER_CONNECTION, detail=f"AI server connection failed: {e}" + ) from e def _get_mock_ai_response(self, ai_request: AIAnnotationRequest) -> dict: """테스트를 위한 Mock AI 서버 응답 생성""" diff --git a/app/services/chat_message_service.py b/app/services/chat_message_service.py index 6df4db8..4fbfb64 100644 --- a/app/services/chat_message_service.py +++ b/app/services/chat_message_service.py @@ -10,24 +10,24 @@ from app.core.status import CommonCode from app.core.utils import generate_prefixed_uuid from app.repository.chat_message_repository import ChatMessageRepository, chat_message_repository +from app.repository.chat_tab_repository import ChatTabRepository, chat_tab_repository from app.schemas.chat_message.base_model import validate_chat_tab_id_format from app.schemas.chat_message.db_model import ChatMessageInDB from app.schemas.chat_message.request_model import ChatMessagesReqeust from app.schemas.chat_message.response_model import ALLChatMessagesResponseByTab, ChatMessagesResponse chat_message_repository_dependency = Depends(lambda: chat_message_repository) - -# AI_SERVER_URL = os.getenv("ENV_AI_SERVER_URL") - -# if not AI_SERVER_URL: -# raise APIException(CommonCode.FAIL_AI_SERVER_CONNECTION) -# -# url: str = AI_SERVER_URL +chat_tab_repository_dependency = Depends(lambda: chat_tab_repository) class ChatMessageService: - def __init__(self, repository: ChatMessageRepository = chat_message_repository): + def __init__( + self, + repository: ChatMessageRepository = chat_message_repository, + chat_tab_repo: ChatTabRepository = chat_tab_repository, + ): self.repository = repository + self.chat_tab_repository = chat_tab_repo self._ai_server_url = None def _get_ai_server_url(self) -> str: @@ -53,7 +53,7 @@ def get_chat_tab_and_messages_by_id(self, tab_id: str) -> ALLChatMessagesRespons except sqlite3.Error as e: raise APIException(CommonCode.FAIL) from e - async def create_chat_message(self, request: ChatMessagesReqeust) -> ChatMessagesResponse: + async def create_chat_message(self, request: ChatMessagesReqeust) -> ChatMessageInDB: # 1. tab_id, message 유효성 검사 및 유무 확인 request.validate() @@ -71,10 +71,10 @@ async def create_chat_message(self, request: ChatMessagesReqeust) -> ChatMessage # 4. AI 서버 응답 저장 response = self._transform_ai_response_to_db_models(request, ai_response) - # DB 모델을 API 응답 모델로 변환 - response_data = ChatMessagesResponse.model_validate(response) + # 5. 채팅 탭의 updated_at 갱신 + self.chat_tab_repository.update_tab_timestamp(request.chat_tab_id) - return response_data + return response def get_chat_tab_by_id(self, request: ChatMessagesReqeust) -> ChatMessageInDB: """특정 채팅 탭 조회"""