mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 12:36:23 +00:00
- AI 서비스 CORS 설정 업데이트 - 회의 진행 프로토타입 수정 - 빌드 리포트 및 로그 파일 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
24 KiB
24 KiB
AI 서비스 개발 가이드
📋 목차
1. AI 제안 기능
📌 기능 개요
- 목적: STT로 5초마다 수신되는 회의 텍스트를 분석하여 실시간 제안사항 제공
- 입력: Redis에 축적된 최근 5분간의 회의 텍스트
- 출력:
- 논의사항 (Discussions): 회의와 관련있고 추가 논의가 필요한 주제
- 결정사항 (Decisions): 명확한 의사결정 패턴이 감지된 내용
✅ 구현 완료 사항
-
SSE 스트리밍 엔드포인트
- 파일:
ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SuggestionController.java:111-131 - 엔드포인트:
GET /api/suggestions/meetings/{meetingId}/stream - 프로토콜: Server-Sent Events (SSE)
- 파일:
-
실시간 텍스트 처리
- 파일:
ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java:120-140 - Event Hub에서 TranscriptSegmentReady 이벤트 수신
- Redis에 최근 5분 텍스트 슬라이딩 윈도우 저장
- 파일:
-
Claude API 통합
- 파일:
ai/src/main/java/com/unicorn/hgzero/ai/infra/client/ClaudeApiClient.java - 모델: claude-3-5-sonnet-20241022
- 비동기 분석 및 JSON 파싱 구현
- 파일:
🔧 개선 필요 사항
1.1 프롬프트 엔지니어링 개선
현재 문제점:
- 회의와 관련 없는 일상 대화도 분석될 가능성
- 회의 목적/안건 정보가 활용되지 않음
개선 방법:
// ClaudeApiClient.java의 analyzeSuggestions 메서드 개선
public Mono<RealtimeSuggestionsDto> analyzeSuggestions(
String transcriptText,
String meetingPurpose, // 추가
List<String> agendaItems // 추가
) {
String systemPrompt = """
당신은 비즈니스 회의록 작성 전문 AI 어시스턴트입니다.
**역할**: 실시간 회의 텍스트를 분석하여 업무 관련 핵심 내용만 추출
**분석 기준**:
1. 논의사항 (discussions):
- 회의 안건과 관련된 미결 주제
- 추가 검토가 필요한 업무 항목
- "~에 대해 논의 필요", "~검토해야 함" 등의 패턴
- **제외**: 잡담, 농담, 인사말, 회의와 무관한 대화
2. 결정사항 (decisions):
- 명확한 의사결정 표현 ("~하기로 함", "~로 결정", "~로 합의")
- 구체적인 Action Item
- 책임자와 일정이 언급된 항목
**필터링 규칙**:
- 회의 목적/안건과 무관한 내용 제외
- 단순 질의응답이나 확인 대화 제외
- 업무 맥락이 명확한 내용만 추출
**응답 형식**: 반드시 JSON만 반환
{
"discussions": [
{
"topic": "구체적인 논의 주제 (회의 안건과 직접 연관)",
"reason": "회의 안건과의 연관성 설명",
"priority": "HIGH|MEDIUM|LOW",
"relatedAgenda": "관련 안건"
}
],
"decisions": [
{
"content": "결정된 내용",
"confidence": 0.9,
"extractedFrom": "원문 인용",
"actionOwner": "담당자 (언급된 경우)",
"deadline": "일정 (언급된 경우)"
}
]
}
""";
String userPrompt = String.format("""
**회의 정보**:
- 목적: %s
- 안건: %s
**회의 내용 (최근 5분)**:
%s
위 회의 목적과 안건에 맞춰 **회의와 관련된 내용만** 분석해주세요.
""",
meetingPurpose,
String.join(", ", agendaItems),
transcriptText
);
// 나머지 코드 동일
}
1.2 회의 컨텍스트 조회 기능 추가
구현 위치: SuggestionService.java
@RequiredArgsConstructor
public class SuggestionService implements SuggestionUseCase {
private final MeetingGateway meetingGateway; // 추가 필요
private void analyzeAndEmitSuggestions(String meetingId) {
// 1. 회의 정보 조회
MeetingInfo meetingInfo = meetingGateway.getMeetingInfo(meetingId);
String meetingPurpose = meetingInfo.getPurpose();
List<String> agendaItems = meetingInfo.getAgendaItems();
// 2. Redis에서 최근 5분 텍스트 조회
String key = "meeting:" + meetingId + ":transcript";
Set<String> recentTexts = redisTemplate.opsForZSet().reverseRange(key, 0, -1);
if (recentTexts == null || recentTexts.isEmpty()) {
return;
}
String accumulatedText = recentTexts.stream()
.map(entry -> entry.split(":", 2)[1])
.collect(Collectors.joining("\n"));
// 3. Claude API 분석 (회의 컨텍스트 포함)
claudeApiClient.analyzeSuggestions(
accumulatedText,
meetingPurpose, // 추가
agendaItems // 추가
)
.subscribe(
suggestions -> {
Sinks.Many<RealtimeSuggestionsDto> sink = meetingSinks.get(meetingId);
if (sink != null) {
sink.tryEmitNext(suggestions);
log.info("AI 제안사항 발행 완료 - meetingId: {}, 논의사항: {}, 결정사항: {}",
meetingId,
suggestions.getDiscussionTopics().size(),
suggestions.getDecisions().size());
}
},
error -> log.error("Claude API 분석 실패 - meetingId: {}", meetingId, error)
);
}
}
필요한 Gateway 인터페이스:
package com.unicorn.hgzero.ai.biz.gateway;
public interface MeetingGateway {
MeetingInfo getMeetingInfo(String meetingId);
}
@Data
@Builder
public class MeetingInfo {
private String meetingId;
private String purpose;
private List<String> agendaItems;
}
2. 용어 사전 기능
📌 기능 개요
- 목적: 회의 중 언급된 전문 용어를 맥락에 맞게 설명
- 입력: 용어, 회의 컨텍스트
- 출력:
- 기본 정의
- 회의 맥락에 맞는 설명
- 이전 회의에서의 사용 예시 (있는 경우)
✅ 구현 완료 사항
-
용어 감지 API
- 파일:
ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java:35-82 - 엔드포인트:
POST /api/terms/detect
- 파일:
-
용어 설명 서비스 (Mock)
- 파일:
ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java:24-53
- 파일:
🔧 개선 필요 사항
2.1 RAG 기반 용어 설명 구현
구현 방법:
@Service
@RequiredArgsConstructor
public class TermExplanationService implements TermExplanationUseCase {
private final SearchGateway searchGateway;
private final ClaudeApiClient claudeApiClient; // 추가
@Override
public TermExplanationResult explainTerm(
String term,
String meetingId,
String context
) {
log.info("용어 설명 생성 - term: {}, meetingId: {}", term, meetingId);
// 1. RAG 검색: 이전 회의록에서 해당 용어 사용 사례 검색
List<TermUsageExample> pastUsages = searchGateway.searchTermUsages(
term,
meetingId
);
// 2. Claude API로 맥락 기반 설명 생성
String explanation = generateContextualExplanation(
term,
context,
pastUsages
);
return TermExplanationResult.builder()
.term(term)
.definition(explanation)
.context(context)
.pastUsages(pastUsages.stream()
.map(TermUsageExample::getDescription)
.collect(Collectors.toList()))
.build();
}
private String generateContextualExplanation(
String term,
String context,
List<TermUsageExample> pastUsages
) {
String prompt = String.format("""
다음 용어를 회의 맥락에 맞게 설명해주세요:
**용어**: %s
**현재 회의 맥락**: %s
**이전 회의에서의 사용 사례**:
%s
**설명 형식**:
1. 기본 정의 (1-2문장)
2. 현재 회의 맥락에서의 의미 (1-2문장)
3. 이전 논의 참고사항 (있는 경우)
비즈니스 맥락에 맞는 실용적인 설명을 제공하세요.
""",
term,
context,
formatPastUsages(pastUsages)
);
// Claude API 호출 (동기 방식)
return claudeApiClient.generateExplanation(prompt)
.block(); // 또는 비동기 처리
}
private String formatPastUsages(List<TermUsageExample> pastUsages) {
if (pastUsages.isEmpty()) {
return "이전 회의에서 언급된 적 없음";
}
return pastUsages.stream()
.map(usage -> String.format(
"- [%s] %s: %s",
usage.getMeetingDate(),
usage.getMeetingTitle(),
usage.getDescription()
))
.collect(Collectors.joining("\n"));
}
}
2.2 Azure AI Search 통합 (RAG)
SearchGateway 구현:
package com.unicorn.hgzero.ai.infra.search;
@Service
@RequiredArgsConstructor
public class AzureAiSearchGateway implements SearchGateway {
@Value("${external.ai-search.endpoint}")
private String endpoint;
@Value("${external.ai-search.api-key}")
private String apiKey;
@Value("${external.ai-search.index-name}")
private String indexName;
private final WebClient webClient;
@Override
public List<TermUsageExample> searchTermUsages(String term, String meetingId) {
// 1. 벡터 검색 쿼리 생성
Map<String, Object> searchRequest = Map.of(
"search", term,
"filter", String.format("meetingId ne '%s'", meetingId), // 현재 회의 제외
"top", 5,
"select", "meetingId,meetingTitle,meetingDate,content,score"
);
// 2. Azure AI Search API 호출
String response = webClient.post()
.uri(endpoint + "/indexes/" + indexName + "/docs/search?api-version=2023-11-01")
.header("api-key", apiKey)
.header("Content-Type", "application/json")
.bodyValue(searchRequest)
.retrieve()
.bodyToMono(String.class)
.block();
// 3. 응답 파싱
return parseSearchResults(response);
}
private List<TermUsageExample> parseSearchResults(String response) {
// JSON 파싱 로직
// Azure AI Search 응답 형식에 맞춰 파싱
return List.of(); // 구현 필요
}
}
3. 관련 회의록 추천 기능
📌 기능 개요
- 목적: 현재 회의와 관련된 과거 회의록 추천
- 입력: 회의 목적, 안건, 진행 중인 회의 내용
- 출력: 관련도 점수와 함께 관련 회의록 목록
✅ 구현 완료 사항
-
관련 회의록 조회 API
- 파일:
ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java:31-63 - 엔드포인트:
GET /api/transcripts/{meetingId}/related
- 파일:
-
벡터 검색 서비스 (Mock)
- 파일:
ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java:27-47
- 파일:
🔧 개선 필요 사항
3.1 Azure AI Search 벡터 검색 구현
구현 방법:
@Service
@RequiredArgsConstructor
public class RelatedTranscriptSearchService implements RelatedTranscriptSearchUseCase {
private final SearchGateway searchGateway;
private final MeetingGateway meetingGateway;
private final ClaudeApiClient claudeApiClient;
@Override
public List<RelatedMinutes> findRelatedTranscripts(
String meetingId,
String transcriptId,
int limit
) {
log.info("관련 회의록 검색 - meetingId: {}, limit: {}", meetingId, limit);
// 1. 현재 회의 정보 조회
MeetingInfo currentMeeting = meetingGateway.getMeetingInfo(meetingId);
String searchQuery = buildSearchQuery(currentMeeting);
// 2. Azure AI Search로 벡터 유사도 검색
List<SearchResult> searchResults = searchGateway.searchRelatedMeetings(
searchQuery,
meetingId,
limit
);
// 3. 검색 결과를 RelatedMinutes로 변환
return searchResults.stream()
.map(this::toRelatedMinutes)
.collect(Collectors.toList());
}
private String buildSearchQuery(MeetingInfo meeting) {
// 회의 목적 + 안건을 검색 쿼리로 변환
return String.format("%s %s",
meeting.getPurpose(),
String.join(" ", meeting.getAgendaItems())
);
}
private RelatedMinutes toRelatedMinutes(SearchResult result) {
return RelatedMinutes.builder()
.transcriptId(result.getMeetingId())
.title(result.getTitle())
.date(result.getDate())
.participants(result.getParticipants())
.relevanceScore(result.getScore() * 100) // 0-1 -> 0-100
.commonKeywords(extractCommonKeywords(result))
.summary(result.getSummary())
.link("/transcripts/" + result.getMeetingId())
.build();
}
}
3.2 임베딩 기반 검색 구현
Azure OpenAI Embedding 활용:
@Service
public class EmbeddingService {
@Value("${azure.openai.endpoint}")
private String endpoint;
@Value("${azure.openai.api-key}")
private String apiKey;
@Value("${azure.openai.embedding-deployment}")
private String embeddingDeployment;
private final WebClient webClient;
/**
* 텍스트를 벡터로 변환
*/
public float[] generateEmbedding(String text) {
Map<String, Object> request = Map.of(
"input", text
);
String response = webClient.post()
.uri(endpoint + "/openai/deployments/" + embeddingDeployment + "/embeddings?api-version=2024-02-15-preview")
.header("api-key", apiKey)
.header("Content-Type", "application/json")
.bodyValue(request)
.retrieve()
.bodyToMono(String.class)
.block();
// 응답에서 embedding 벡터 추출
return parseEmbedding(response);
}
private float[] parseEmbedding(String response) {
// JSON 파싱하여 float[] 반환
return new float[0]; // 구현 필요
}
}
Azure AI Search 벡터 검색:
@Override
public List<SearchResult> searchRelatedMeetings(
String query,
String excludeMeetingId,
int limit
) {
// 1. 쿼리 텍스트를 벡터로 변환
float[] queryVector = embeddingService.generateEmbedding(query);
// 2. 벡터 검색 쿼리
Map<String, Object> searchRequest = Map.of(
"vector", Map.of(
"value", queryVector,
"fields", "contentVector",
"k", limit
),
"filter", String.format("meetingId ne '%s'", excludeMeetingId),
"select", "meetingId,title,date,participants,summary,score"
);
// 3. Azure AI Search API 호출
String response = webClient.post()
.uri(endpoint + "/indexes/" + indexName + "/docs/search?api-version=2023-11-01")
.header("api-key", apiKey)
.bodyValue(searchRequest)
.retrieve()
.bodyToMono(String.class)
.block();
return parseSearchResults(response);
}
4. 백엔드 API 검증
4.1 SSE 스트리밍 테스트
테스트 방법:
# 1. SSE 엔드포인트 연결
curl -N -H "Accept: text/event-stream" \
"http://localhost:8083/api/suggestions/meetings/test-meeting-001/stream"
# 예상 출력:
# event: ai-suggestion
# data: {"discussionTopics":[...],"decisions":[...]}
프론트엔드 연동 예시 (JavaScript):
// 05-회의진행.html에 추가
const meetingId = "test-meeting-001";
const eventSource = new EventSource(
`http://localhost:8083/api/suggestions/meetings/${meetingId}/stream`
);
eventSource.addEventListener('ai-suggestion', (event) => {
const suggestions = JSON.parse(event.data);
// 논의사항 표시
suggestions.discussionTopics.forEach(topic => {
addDiscussionCard(topic);
});
// 결정사항 표시
suggestions.decisions.forEach(decision => {
addDecisionCard(decision);
});
});
eventSource.onerror = (error) => {
console.error('SSE 연결 오류:', error);
eventSource.close();
};
function addDiscussionCard(topic) {
const card = document.createElement('div');
card.className = 'ai-suggestion-card';
card.innerHTML = `
<div class="ai-suggestion-header">
<span class="ai-suggestion-time">${new Date().toLocaleTimeString()}</span>
<span class="badge badge-${topic.priority.toLowerCase()}">${topic.priority}</span>
</div>
<div class="ai-suggestion-text">
<strong>[논의사항]</strong> ${topic.topic}
</div>
<div class="ai-suggestion-reason">${topic.reason}</div>
`;
document.getElementById('aiSuggestionList').prepend(card);
}
4.2 용어 설명 API 테스트
# POST /api/terms/detect
curl -X POST http://localhost:8083/api/terms/detect \
-H "Content-Type: application/json" \
-d '{
"meetingId": "test-meeting-001",
"text": "오늘 회의에서는 MSA 아키텍처와 API Gateway 설계에 대해 논의하겠습니다.",
"organizationId": "org-001"
}'
# 예상 응답:
{
"success": true,
"data": {
"detectedTerms": [
{
"term": "MSA",
"confidence": 0.95,
"category": "아키텍처",
"definition": "Microservices Architecture의 약자...",
"context": "회의 맥락에 맞는 설명..."
},
{
"term": "API Gateway",
"confidence": 0.92,
"category": "아키텍처"
}
],
"totalCount": 2
}
}
4.3 관련 회의록 API 테스트
# GET /api/transcripts/{meetingId}/related
curl "http://localhost:8083/api/transcripts/test-meeting-001/related?transcriptId=transcript-001&limit=5"
# 예상 응답:
{
"success": true,
"data": {
"relatedTranscripts": [
{
"transcriptId": "meeting-002",
"title": "MSA 아키텍처 설계 회의",
"date": "2025-01-15",
"participants": ["김철수", "이영희"],
"relevanceScore": 85.5,
"commonKeywords": ["MSA", "API Gateway", "Spring Boot"],
"link": "/transcripts/meeting-002"
}
],
"totalCount": 5
}
}
5. 프롬프트 엔지니어링 가이드
5.1 AI 제안사항 프롬프트
핵심 원칙:
- 명확한 역할 정의: AI의 역할을 "회의록 작성 전문가"로 명시
- 구체적인 기준: 무엇을 추출하고 무엇을 제외할지 명확히 명시
- 컨텍스트 제공: 회의 목적과 안건을 프롬프트에 포함
- 구조화된 출력: JSON 형식으로 파싱 가능한 응답 요청
프롬프트 템플릿:
당신은 비즈니스 회의록 작성 전문 AI 어시스턴트입니다.
**역할**: 실시간 회의 텍스트를 분석하여 업무 관련 핵심 내용만 추출
**분석 기준**:
1. 논의사항 (discussions):
- 회의 안건과 관련된 미결 주제
- 추가 검토가 필요한 업무 항목
- "~에 대해 논의 필요", "~검토해야 함" 등의 패턴
- **제외**: 잡담, 농담, 인사말, 회의와 무관한 대화
2. 결정사항 (decisions):
- 명확한 의사결정 표현 ("~하기로 함", "~로 결정", "~로 합의")
- 구체적인 Action Item
- 책임자와 일정이 언급된 항목
**필터링 규칙**:
- 회의 목적/안건과 무관한 내용 제외
- 단순 질의응답이나 확인 대화 제외
- 업무 맥락이 명확한 내용만 추출
**응답 형식**: 반드시 JSON만 반환
{
"discussions": [
{
"topic": "구체적인 논의 주제",
"reason": "회의 안건과의 연관성",
"priority": "HIGH|MEDIUM|LOW",
"relatedAgenda": "관련 안건"
}
],
"decisions": [
{
"content": "결정 내용",
"confidence": 0.9,
"extractedFrom": "원문 인용",
"actionOwner": "담당자 (있는 경우)",
"deadline": "일정 (있는 경우)"
}
]
}
---
**회의 정보**:
- 목적: {meeting_purpose}
- 안건: {agenda_items}
**회의 내용 (최근 5분)**:
{transcript_text}
위 회의 목적과 안건에 맞춰 **회의와 관련된 내용만** 분석해주세요.
5.2 용어 설명 프롬프트
다음 용어를 회의 맥락에 맞게 설명해주세요:
**용어**: {term}
**현재 회의 맥락**: {context}
**이전 회의에서의 사용 사례**:
{past_usages}
**설명 형식**:
1. 기본 정의 (1-2문장, 비전문가도 이해 가능하도록)
2. 현재 회의 맥락에서의 의미 (1-2문장, 이번 회의에서 이 용어가 어떤 의미로 쓰이는지)
3. 이전 논의 참고사항 (있는 경우, 과거 회의에서 관련 결정사항)
비즈니스 맥락에 맞는 실용적인 설명을 제공하세요.
5.3 관련 회의록 검색 쿼리 생성 프롬프트
현재 회의와 관련된 과거 회의록을 찾기 위한 검색 쿼리를 생성하세요.
**현재 회의 정보**:
- 목적: {meeting_purpose}
- 안건: {agenda_items}
- 진행 중인 주요 논의: {current_discussions}
**검색 쿼리 생성 규칙**:
1. 회의 목적과 안건에서 핵심 키워드 추출
2. 동의어와 관련 용어 포함
3. 너무 일반적인 단어는 제외 (예: "회의", "논의")
4. 5-10개의 키워드로 구성
**출력 형식**: 키워드를 공백으로 구분한 문자열
예시: "MSA 마이크로서비스 API게이트웨이 분산시스템 아키텍처설계"
6. 환경 설정
6.1 application.yml 확인 사항
# Claude API 설정
external:
ai:
claude:
api-key: ${CLAUDE_API_KEY} # 환경변수 설정 필요
base-url: https://api.anthropic.com
model: claude-3-5-sonnet-20241022
max-tokens: 2000
temperature: 0.3
# Azure AI Search 설정
ai-search:
endpoint: ${AZURE_AI_SEARCH_ENDPOINT}
api-key: ${AZURE_AI_SEARCH_API_KEY}
index-name: meeting-transcripts
# Event Hub 설정
eventhub:
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING}
namespace: hgzero-eventhub-ns
eventhub-name: hgzero-eventhub-name
consumer-group:
transcript: ai-transcript-group
6.2 환경 변수 설정
# Claude API
export CLAUDE_API_KEY="sk-ant-..."
# Azure AI Search
export AZURE_AI_SEARCH_ENDPOINT="https://your-search-service.search.windows.net"
export AZURE_AI_SEARCH_API_KEY="your-api-key"
# Azure Event Hub
export AZURE_EVENTHUB_CONNECTION_STRING="Endpoint=sb://..."
7. 테스트 시나리오
7.1 전체 통합 테스트 시나리오
-
회의 시작
- 회의 생성 API 호출
- SSE 스트림 연결
-
STT 텍스트 수신
- Event Hub로 TranscriptSegmentReady 이벤트 발행
- Redis에 텍스트 축적 확인
-
AI 제안사항 생성
- 5분간 텍스트 축적
- Claude API 자동 호출
- SSE로 제안사항 수신 확인
-
용어 설명 요청
- 감지된 용어로 설명 API 호출
- 맥락에 맞는 설명 확인
-
관련 회의록 조회
- 관련 회의록 API 호출
- 유사도 점수 확인
8. 다음 단계
- ✅ MeetingGateway 구현: Meeting 서비스와 통신하여 회의 정보 조회
- ✅ SearchGateway Azure AI Search 통합: 벡터 검색 구현
- ✅ ClaudeApiClient 프롬프트 개선: 회의 컨텍스트 활용
- ✅ 프론트엔드 SSE 연동: 05-회의진행.html에 SSE 클라이언트 추가
- ✅ 통합 테스트: 전체 플로우 동작 확인