From ed017129c7ba16b76b458220cfaf3c0f4b278405 Mon Sep 17 00:00:00 2001 From: cyjadela Date: Wed, 29 Oct 2025 17:35:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20AI=20=EC=9A=94=EC=95=BD=20=EC=9E=AC?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-python/API-DOCUMENTATION.md | 56 +++++++++++++++- ai-python/app/api/v1/__init__.py | 2 + ai-python/app/api/v1/summary.py | 84 ++++++++++++++++++++++++ ai-python/app/config.py | 6 +- ai-python/app/models/__init__.py | 6 ++ ai-python/app/models/summary.py | 81 +++++++++++++++++++++++ ai-python/app/prompts/summary_prompt.py | 80 ++++++++++++++++++++++ ai-python/app/services/claude_service.py | 70 ++++++++++++++++++++ ai-python/main.py | 2 +- 9 files changed, 380 insertions(+), 7 deletions(-) create mode 100644 ai-python/app/api/v1/summary.py create mode 100644 ai-python/app/models/summary.py create mode 100644 ai-python/app/prompts/summary_prompt.py diff --git a/ai-python/API-DOCUMENTATION.md b/ai-python/API-DOCUMENTATION.md index 0a15167..77fed86 100644 --- a/ai-python/API-DOCUMENTATION.md +++ b/ai-python/API-DOCUMENTATION.md @@ -1,9 +1,9 @@ # AI Service API Documentation ## 서비스 정보 -- **Base URL**: `http://localhost:8087` -- **프로덕션 URL**: `http://{AKS-IP}:8087` (배포 후) -- **포트**: 8087 +- **Base URL**: `http://localhost:8086` +- **프로덕션 URL**: `http://{AKS-IP}:8086` (배포 후) +- **포트**: 8086 - **프로토콜**: HTTP - **CORS**: 모든 origin 허용 (개발 환경) @@ -248,3 +248,53 @@ A: 네, 각 클라이언트는 독립적으로 SSE 연결을 유지합니다. **Q: 제안사항이 오지 않으면?** A: Redis에 충분한 텍스트(10개 세그먼트)가 축적되어야 분석이 시작됩니다. 5초마다 체크합니다. + +### 3. AI 텍스트 요약 생성 + +**엔드포인트**: `POST /api/v1/ai/summary/generate` + +**설명**: 텍스트를 AI로 요약하여 핵심 내용과 포인트를 추출합니다. + +**요청 본문**: +```json +{ + "text": "요약할 텍스트 내용", + "language": "ko", // ko: 한국어, en: 영어 (기본값: ko) + "style": "bullet", // bullet: 불릿포인트, paragraph: 단락형 (기본값: bullet) + "max_length": 100 // 최대 요약 길이 (단어 수) - 선택사항 +} +``` + +**응답 예시**: +```json +{ + "summary": "• 프로젝트 총 개발 기간 3개월 확정 (디자인 2주, 개발 8주, 테스트 2주)\n• 총 예산 5천만원 배정 (인건비 3천만원, 인프라 1천만원, 기타 1천만원)\n• 주간 회의 일정: 매주 화요일 오전 10시", + "key_points": [ + "프로젝트 전체 일정 3개월로 확정", + "개발 단계별 기간: 디자인 2주, 개발 8주, 테스트 2주", + "총 예산 5천만원 책정", + "예산 배분: 인건비 60%, 인프라 20%, 기타 20%", + "정기 회의: 매주 화요일 오전 10시" + ], + "word_count": 32, + "original_word_count": 46, + "compression_ratio": 0.7, + "generated_at": "2025-10-29T17:23:49.429982" +} +``` + +**요청 예시 (curl)**: +```bash +curl -X POST "http://localhost:8086/api/v1/ai/summary/generate" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "오늘 회의에서는 프로젝트 일정과 예산에 대해 논의했습니다...", + "language": "ko", + "style": "bullet" + }' +``` + +**에러 응답**: +- `400 Bad Request`: 텍스트가 비어있거나 너무 짧은 경우 (최소 20자) +- `400 Bad Request`: 텍스트가 너무 긴 경우 (최대 10,000자) +- `500 Internal Server Error`: AI 처리 중 오류 발생 diff --git a/ai-python/app/api/v1/__init__.py b/ai-python/app/api/v1/__init__.py index b5b6d39..3660ee0 100644 --- a/ai-python/app/api/v1/__init__.py +++ b/ai-python/app/api/v1/__init__.py @@ -2,9 +2,11 @@ from fastapi import APIRouter from .transcripts import router as transcripts_router from .suggestions import router as suggestions_router +from .summary import router as summary_router router = APIRouter() # 라우터 등록 router.include_router(transcripts_router, prefix="/transcripts", tags=["Transcripts"]) router.include_router(suggestions_router, prefix="/ai/suggestions", tags=["AI Suggestions"]) +router.include_router(summary_router, prefix="/ai/summary", tags=["AI Summary"]) diff --git a/ai-python/app/api/v1/summary.py b/ai-python/app/api/v1/summary.py new file mode 100644 index 0000000..d51ce7b --- /dev/null +++ b/ai-python/app/api/v1/summary.py @@ -0,0 +1,84 @@ +"""AI 요약 API 라우터""" +from fastapi import APIRouter, HTTPException +from app.models.summary import SummaryRequest, SummaryResponse +from app.services.claude_service import claude_service +import logging + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.post("/generate", response_model=SummaryResponse) +async def generate_summary(request: SummaryRequest): + """ + 텍스트 요약 생성 API + + - **text**: 요약할 텍스트 (필수) + - **language**: 요약 언어 (ko: 한국어, en: 영어) - 기본값: ko + - **style**: 요약 스타일 (bullet: 불릿포인트, paragraph: 단락형) - 기본값: bullet + - **max_length**: 최대 요약 길이 (단어 수) - 선택사항 + + Returns: + 요약 결과 (요약문, 핵심 포인트, 통계 정보) + """ + try: + # 입력 검증 + if not request.text or len(request.text.strip()) == 0: + raise HTTPException( + status_code=400, + detail="요약할 텍스트가 비어있습니다." + ) + + if len(request.text) < 20: + raise HTTPException( + status_code=400, + detail="텍스트가 너무 짧습니다. 최소 20자 이상의 텍스트를 입력해주세요." + ) + + if len(request.text) > 10000: + raise HTTPException( + status_code=400, + detail="텍스트가 너무 깁니다. 최대 10,000자까지 요약 가능합니다." + ) + + # 언어 검증 + if request.language not in ["ko", "en"]: + raise HTTPException( + status_code=400, + detail="지원하지 않는 언어입니다. 'ko' 또는 'en'만 사용 가능합니다." + ) + + # 스타일 검증 + if request.style not in ["bullet", "paragraph"]: + raise HTTPException( + status_code=400, + detail="지원하지 않는 스타일입니다. 'bullet' 또는 'paragraph'만 사용 가능합니다." + ) + + # 최대 길이 검증 + if request.max_length and request.max_length < 10: + raise HTTPException( + status_code=400, + detail="최대 길이는 10단어 이상이어야 합니다." + ) + + logger.info(f"요약 요청 - 텍스트 길이: {len(request.text)}, 언어: {request.language}, 스타일: {request.style}") + + # Claude 서비스 호출 + result = await claude_service.generate_summary( + text=request.text, + language=request.language, + style=request.style, + max_length=request.max_length + ) + + return SummaryResponse(**result) + + except HTTPException: + raise + except Exception as e: + logger.error(f"요약 생성 중 오류 발생: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"요약 생성 중 오류가 발생했습니다: {str(e)}" + ) \ No newline at end of file diff --git a/ai-python/app/config.py b/ai-python/app/config.py index fd74ade..f5a62de 100644 --- a/ai-python/app/config.py +++ b/ai-python/app/config.py @@ -10,12 +10,12 @@ class Settings(BaseSettings): # 서버 설정 app_name: str = "AI Service (Python)" host: str = "0.0.0.0" - port: int = 8087 # feature/stt-ai 브랜치 AI Service(8086)와 충돌 방지 + port: int = 8086 # Claude API claude_api_key: str = "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" - claude_model: str = "claude-3-5-sonnet-20240620" - claude_max_tokens: int = 250000 + claude_model: str = "claude-sonnet-4-5-20250929" + claude_max_tokens: int = 4096 claude_temperature: float = 0.7 # Redis diff --git a/ai-python/app/models/__init__.py b/ai-python/app/models/__init__.py index 0863f5d..9f5c7a4 100644 --- a/ai-python/app/models/__init__.py +++ b/ai-python/app/models/__init__.py @@ -10,6 +10,10 @@ from .response import ( SimpleSuggestion, RealtimeSuggestionsResponse ) +from .summary import ( + SummaryRequest, + SummaryResponse +) __all__ = [ "ConsolidateRequest", @@ -19,4 +23,6 @@ __all__ = [ "ExtractedTodo", "SimpleSuggestion", "RealtimeSuggestionsResponse", + "SummaryRequest", + "SummaryResponse", ] diff --git a/ai-python/app/models/summary.py b/ai-python/app/models/summary.py new file mode 100644 index 0000000..76b7d03 --- /dev/null +++ b/ai-python/app/models/summary.py @@ -0,0 +1,81 @@ +"""요약 관련 모델""" +from pydantic import BaseModel, Field +from typing import List, Optional +from datetime import datetime + + +class SummaryRequest(BaseModel): + """요약 요청 모델""" + text: str = Field( + ..., + description="요약할 텍스트", + example="오늘 회의에서는 프로젝트 일정과 예산에 대해 논의했습니다. 첫째, 개발 일정은 3개월로 확정되었고, 디자인 단계는 2주, 개발 단계는 8주, 테스트 단계는 2주로 배분하기로 했습니다. 둘째, 예산은 총 5천만원으로 책정되었으며, 인건비 3천만원, 인프라 비용 1천만원, 기타 비용 1천만원으로 배분됩니다. 셋째, 주간 회의는 매주 화요일 오전 10시에 진행하기로 했습니다." + ) + language: str = Field( + default="ko", + description="요약 언어 (ko: 한국어, en: 영어)", + example="ko" + ) + style: str = Field( + default="bullet", + description="요약 스타일 (bullet: 불릿 포인트, paragraph: 단락형)", + example="bullet" + ) + max_length: Optional[int] = Field( + default=None, + description="최대 요약 길이 (단어 수)", + example=100 + ) + + +class SummaryResponse(BaseModel): + """요약 응답 모델""" + summary: str = Field( + ..., + description="생성된 요약", + example="• 프로젝트 일정: 총 3개월 (디자인 2주, 개발 8주, 테스트 2주)\n• 예산: 총 5천만원 (인건비 3천만원, 인프라 1천만원, 기타 1천만원)\n• 주간 회의: 매주 화요일 오전 10시" + ) + key_points: List[str] = Field( + ..., + description="핵심 포인트 리스트", + example=[ + "프로젝트 일정 3개월 확정", + "총 예산 5천만원 책정", + "주간 회의 화요일 10시" + ] + ) + word_count: int = Field( + ..., + description="요약 단어 수", + example=42 + ) + original_word_count: int = Field( + ..., + description="원본 텍스트 단어 수", + example=156 + ) + compression_ratio: float = Field( + ..., + description="압축률 (요약 길이 / 원본 길이)", + example=0.27 + ) + generated_at: datetime = Field( + default_factory=datetime.now, + description="생성 시간" + ) + + class Config: + json_schema_extra = { + "example": { + "summary": "• 프로젝트 일정: 총 3개월 (디자인 2주, 개발 8주, 테스트 2주)\n• 예산: 총 5천만원 (인건비 3천만원, 인프라 1천만원, 기타 1천만원)\n• 주간 회의: 매주 화요일 오전 10시", + "key_points": [ + "프로젝트 일정 3개월 확정", + "총 예산 5천만원 책정", + "주간 회의 화요일 10시" + ], + "word_count": 42, + "original_word_count": 156, + "compression_ratio": 0.27, + "generated_at": "2024-10-29T17:15:30.123456" + } + } \ No newline at end of file diff --git a/ai-python/app/prompts/summary_prompt.py b/ai-python/app/prompts/summary_prompt.py new file mode 100644 index 0000000..b0c61dc --- /dev/null +++ b/ai-python/app/prompts/summary_prompt.py @@ -0,0 +1,80 @@ +"""요약 생성용 프롬프트""" + + +def get_summary_prompt(text: str, language: str = "ko", style: str = "bullet", max_length: int = None): + """ + 텍스트 요약을 위한 프롬프트 생성 + + Args: + text: 요약할 텍스트 + language: 요약 언어 (ko/en) + style: 요약 스타일 (bullet/paragraph) + max_length: 최대 요약 길이 (단어 수) + + Returns: + tuple: (system_prompt, user_prompt) + """ + + # 언어별 설정 + if language == "ko": + lang_instruction = "한국어로 요약을 작성하세요." + bullet_prefix = "•" + style_name = "불릿 포인트" if style == "bullet" else "단락형" + else: + lang_instruction = "Write the summary in English." + bullet_prefix = "•" + style_name = "bullet points" if style == "bullet" else "paragraph" + + # 길이 제한 설정 + length_instruction = "" + if max_length: + if language == "ko": + length_instruction = f"\n- 요약은 {max_length}단어 이내로 작성하세요." + else: + length_instruction = f"\n- Keep the summary within {max_length} words." + + system_prompt = f"""당신은 전문적인 텍스트 요약 전문가입니다. +주어진 텍스트를 명확하고 간결하게 요약하는 것이 당신의 임무입니다. + +요약 원칙: +1. 핵심 정보를 빠뜨리지 않고 포함 +2. 중복되는 내용은 제거 +3. 원문의 의미를 왜곡하지 않음 +4. {style_name} 형식으로 작성 +5. {lang_instruction}{length_instruction} + +응답은 반드시 다음 JSON 형식으로 제공하세요: +{{ + "summary": "요약 내용", + "key_points": ["핵심 포인트 1", "핵심 포인트 2", ...], + "analysis": {{ + "main_topics": ["주요 주제들"], + "sentiment": "positive/negative/neutral", + "importance_level": "high/medium/low" + }} +}}""" + + if style == "bullet": + style_instruction = f""" +불릿 포인트 형식 지침: +- 각 포인트는 '{bullet_prefix}'로 시작 +- 하나의 포인트는 한 문장으로 구성 +- 가장 중요한 정보부터 나열 +- 3-7개의 주요 포인트로 구성""" + else: + style_instruction = """ +단락형 형식 지침: +- 자연스러운 문장으로 연결 +- 논리적 흐름을 유지 +- 적절한 접속사 사용 +- 2-3개의 단락으로 구성""" + + user_prompt = f"""다음 텍스트를 요약해주세요: + +{text} + +{style_instruction} + +JSON 형식으로 응답하세요.""" + + return system_prompt, user_prompt \ No newline at end of file diff --git a/ai-python/app/services/claude_service.py b/ai-python/app/services/claude_service.py index 896da50..a2b4dcf 100644 --- a/ai-python/app/services/claude_service.py +++ b/ai-python/app/services/claude_service.py @@ -133,6 +133,76 @@ class ClaudeService: logger.error(f"제안사항 분석 실패: {e}", exc_info=True) # 빈 응답 반환 return RealtimeSuggestionsResponse(suggestions=[]) + + async def generate_summary( + self, + text: str, + language: str = "ko", + style: str = "bullet", + max_length: int = None + ) -> Dict[str, Any]: + """ + 텍스트 요약 생성 + + Args: + text: 요약할 텍스트 + language: 요약 언어 (ko/en) + style: 요약 스타일 (bullet/paragraph) + max_length: 최대 요약 길이 + + Returns: + 요약 결과 딕셔너리 + """ + from app.models.summary import SummaryResponse + from app.prompts.summary_prompt import get_summary_prompt + + try: + # 프롬프트 생성 + system_prompt, user_prompt = get_summary_prompt( + text=text, + language=language, + style=style, + max_length=max_length + ) + + # Claude API 호출 + result = await self.generate_completion( + prompt=user_prompt, + system_prompt=system_prompt + ) + + # 단어 수 계산 + summary_text = result.get("summary", "") + key_points = result.get("key_points", []) + + # 한국어와 영어의 단어 수 계산 방식 다르게 처리 + if language == "ko": + # 한국어: 공백으로 구분된 어절 수 + original_word_count = len(text.split()) + summary_word_count = len(summary_text.split()) + else: + # 영어: 공백으로 구분된 단어 수 + original_word_count = len(text.split()) + summary_word_count = len(summary_text.split()) + + compression_ratio = summary_word_count / original_word_count if original_word_count > 0 else 0 + + # 응답 생성 + response = SummaryResponse( + summary=summary_text, + key_points=key_points, + word_count=summary_word_count, + original_word_count=original_word_count, + compression_ratio=round(compression_ratio, 2) + ) + + logger.info(f"요약 생성 완료 - 원본: {original_word_count}단어, 요약: {summary_word_count}단어") + + return response.model_dump() + + except Exception as e: + logger.error(f"요약 생성 실패: {e}", exc_info=True) + raise # 싱글톤 인스턴스 diff --git a/ai-python/main.py b/ai-python/main.py index c1b2e77..a92cf16 100644 --- a/ai-python/main.py +++ b/ai-python/main.py @@ -36,7 +36,7 @@ app.add_middleware( ) # API 라우터 등록 -app.include_router(api_v1_router, prefix="/api") +app.include_router(api_v1_router, prefix="/api/v1") @app.get("/health") From 0615538b1fa69d164dec817831bd0c077f8b2d24 Mon Sep 17 00:00:00 2001 From: Minseo-Jo Date: Wed, 29 Oct 2025 18:11:32 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Fix:=20AI=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=208087=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- ai-python/app/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-python/app/config.py b/ai-python/app/config.py index f5a62de..ccfa815 100644 --- a/ai-python/app/config.py +++ b/ai-python/app/config.py @@ -10,7 +10,7 @@ class Settings(BaseSettings): # 서버 설정 app_name: str = "AI Service (Python)" host: str = "0.0.0.0" - port: int = 8086 + port: int = 8087 # Claude API claude_api_key: str = "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"