mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 13:46:24 +00:00
Feat: AI 요약 재생성 API 구현
This commit is contained in:
parent
023602027d
commit
ed017129c7
@ -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 처리 중 오류 발생
|
||||
|
||||
@ -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"])
|
||||
|
||||
84
ai-python/app/api/v1/summary.py
Normal file
84
ai-python/app/api/v1/summary.py
Normal file
@ -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)}"
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
81
ai-python/app/models/summary.py
Normal file
81
ai-python/app/models/summary.py
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
80
ai-python/app/prompts/summary_prompt.py
Normal file
80
ai-python/app/prompts/summary_prompt.py
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user