mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 16:06:23 +00:00
- AI 서비스 CORS 설정 업데이트 - 회의 진행 프로토타입 수정 - 빌드 리포트 및 로그 파일 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
833 lines
24 KiB
Markdown
833 lines
24 KiB
Markdown
# AI 서비스 개발 가이드
|
|
|
|
## 📋 **목차**
|
|
1. [AI 제안 기능 개발](#1-ai-제안-기능)
|
|
2. [용어 사전 기능 개발](#2-용어-사전-기능)
|
|
3. [관련 회의록 추천 기능 개발](#3-관련-회의록-추천-기능)
|
|
4. [백엔드 API 검증](#4-백엔드-api-검증)
|
|
5. [프롬프트 엔지니어링 가이드](#5-프롬프트-엔지니어링)
|
|
|
|
---
|
|
|
|
## 1. AI 제안 기능
|
|
|
|
### 📌 **기능 개요**
|
|
- **목적**: STT로 5초마다 수신되는 회의 텍스트를 분석하여 실시간 제안사항 제공
|
|
- **입력**: Redis에 축적된 최근 5분간의 회의 텍스트
|
|
- **출력**:
|
|
- 논의사항 (Discussions): 회의와 관련있고 추가 논의가 필요한 주제
|
|
- 결정사항 (Decisions): 명확한 의사결정 패턴이 감지된 내용
|
|
|
|
### ✅ **구현 완료 사항**
|
|
|
|
1. **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)
|
|
|
|
2. **실시간 텍스트 처리**
|
|
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java:120-140`
|
|
- Event Hub에서 TranscriptSegmentReady 이벤트 수신
|
|
- Redis에 최근 5분 텍스트 슬라이딩 윈도우 저장
|
|
|
|
3. **Claude API 통합**
|
|
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/client/ClaudeApiClient.java`
|
|
- 모델: claude-3-5-sonnet-20241022
|
|
- 비동기 분석 및 JSON 파싱 구현
|
|
|
|
### 🔧 **개선 필요 사항**
|
|
|
|
#### 1.1 프롬프트 엔지니어링 개선
|
|
|
|
**현재 문제점**:
|
|
- 회의와 관련 없는 일상 대화도 분석될 가능성
|
|
- 회의 목적/안건 정보가 활용되지 않음
|
|
|
|
**개선 방법**:
|
|
|
|
```java
|
|
// 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`
|
|
|
|
```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 인터페이스**:
|
|
|
|
```java
|
|
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. 용어 사전 기능
|
|
|
|
### 📌 **기능 개요**
|
|
- **목적**: 회의 중 언급된 전문 용어를 맥락에 맞게 설명
|
|
- **입력**: 용어, 회의 컨텍스트
|
|
- **출력**:
|
|
- 기본 정의
|
|
- 회의 맥락에 맞는 설명
|
|
- 이전 회의에서의 사용 예시 (있는 경우)
|
|
|
|
### ✅ **구현 완료 사항**
|
|
|
|
1. **용어 감지 API**
|
|
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java:35-82`
|
|
- 엔드포인트: `POST /api/terms/detect`
|
|
|
|
2. **용어 설명 서비스 (Mock)**
|
|
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java:24-53`
|
|
|
|
### 🔧 **개선 필요 사항**
|
|
|
|
#### 2.1 RAG 기반 용어 설명 구현
|
|
|
|
**구현 방법**:
|
|
|
|
```java
|
|
@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 구현**:
|
|
|
|
```java
|
|
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. 관련 회의록 추천 기능
|
|
|
|
### 📌 **기능 개요**
|
|
- **목적**: 현재 회의와 관련된 과거 회의록 추천
|
|
- **입력**: 회의 목적, 안건, 진행 중인 회의 내용
|
|
- **출력**: 관련도 점수와 함께 관련 회의록 목록
|
|
|
|
### ✅ **구현 완료 사항**
|
|
|
|
1. **관련 회의록 조회 API**
|
|
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java:31-63`
|
|
- 엔드포인트: `GET /api/transcripts/{meetingId}/related`
|
|
|
|
2. **벡터 검색 서비스 (Mock)**
|
|
- 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java:27-47`
|
|
|
|
### 🔧 **개선 필요 사항**
|
|
|
|
#### 3.1 Azure AI Search 벡터 검색 구현
|
|
|
|
**구현 방법**:
|
|
|
|
```java
|
|
@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 활용**:
|
|
|
|
```java
|
|
@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 벡터 검색**:
|
|
|
|
```java
|
|
@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 스트리밍 테스트
|
|
|
|
**테스트 방법**:
|
|
|
|
```bash
|
|
# 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):
|
|
|
|
```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 테스트
|
|
|
|
```bash
|
|
# 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 테스트
|
|
|
|
```bash
|
|
# 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 제안사항 프롬프트
|
|
|
|
**핵심 원칙**:
|
|
1. **명확한 역할 정의**: AI의 역할을 "회의록 작성 전문가"로 명시
|
|
2. **구체적인 기준**: 무엇을 추출하고 무엇을 제외할지 명확히 명시
|
|
3. **컨텍스트 제공**: 회의 목적과 안건을 프롬프트에 포함
|
|
4. **구조화된 출력**: 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 확인 사항
|
|
|
|
```yaml
|
|
# 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 환경 변수 설정
|
|
|
|
```bash
|
|
# 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 전체 통합 테스트 시나리오
|
|
|
|
1. **회의 시작**
|
|
- 회의 생성 API 호출
|
|
- SSE 스트림 연결
|
|
|
|
2. **STT 텍스트 수신**
|
|
- Event Hub로 TranscriptSegmentReady 이벤트 발행
|
|
- Redis에 텍스트 축적 확인
|
|
|
|
3. **AI 제안사항 생성**
|
|
- 5분간 텍스트 축적
|
|
- Claude API 자동 호출
|
|
- SSE로 제안사항 수신 확인
|
|
|
|
4. **용어 설명 요청**
|
|
- 감지된 용어로 설명 API 호출
|
|
- 맥락에 맞는 설명 확인
|
|
|
|
5. **관련 회의록 조회**
|
|
- 관련 회의록 API 호출
|
|
- 유사도 점수 확인
|
|
|
|
---
|
|
|
|
## 8. 다음 단계
|
|
|
|
1. ✅ **MeetingGateway 구현**: Meeting 서비스와 통신하여 회의 정보 조회
|
|
2. ✅ **SearchGateway Azure AI Search 통합**: 벡터 검색 구현
|
|
3. ✅ **ClaudeApiClient 프롬프트 개선**: 회의 컨텍스트 활용
|
|
4. ✅ **프론트엔드 SSE 연동**: 05-회의진행.html에 SSE 클라이언트 추가
|
|
5. ✅ **통합 테스트**: 전체 플로우 동작 확인
|