Compare commits

..

No commits in common. "52b32cf978f659114338cd91d94a93e30add3daf" and "ed9fa6f934265a7fdf15002662adcf2e2901803d" have entirely different histories.

187 changed files with 15221 additions and 5036 deletions

View File

@ -1,3 +0,0 @@
# Development environment variables for ai-python service
resource_group=rg-digitalgarage-02
cluster_name=aks-digitalgarage-02

View File

@ -1,7 +0,0 @@
# Azure Resource Configuration
resource_group=rg-digitalgarage-02
cluster_name=aks-digitalgarage-02
# RAG Service Configuration
python_version=3.11
app_port=8088

View File

@ -1,7 +0,0 @@
# Azure Resource Configuration
resource_group=rg-digitalgarage-prod
cluster_name=aks-digitalgarage-prod
# RAG Service Configuration
python_version=3.11
app_port=8088

View File

@ -1,7 +0,0 @@
# Azure Resource Configuration
resource_group=rg-digitalgarage-staging
cluster_name=aks-digitalgarage-staging
# RAG Service Configuration
python_version=3.11
app_port=8088

View File

@ -32,13 +32,6 @@ spec:
name: stt name: stt
port: port:
number: 8080 number: 8080
- path: /api/transcripts
pathType: Prefix
backend:
service:
name: ai-service
port:
number: 8087
- path: /api/ai/suggestions - path: /api/ai/suggestions
pathType: Prefix pathType: Prefix
backend: backend:

View File

@ -1,221 +0,0 @@
name: AI-Python Service CI/CD
on:
push:
branches: [ main, develop ]
paths:
- 'ai-python/**'
- '.github/workflows/ai-python-cicd_ArgoCD.yaml'
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
ENVIRONMENT:
description: 'Target environment'
required: true
default: 'dev'
type: choice
options:
- dev
- staging
- prod
SKIP_TESTS:
description: 'Skip Tests'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
env:
REGISTRY: acrdigitalgarage02.azurecr.io
IMAGE_ORG: hgzero
SERVICE_NAME: ai-python
RESOURCE_GROUP: rg-digitalgarage-02
AKS_CLUSTER: aks-digitalgarage-02
NAMESPACE: hgzero
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.set_outputs.outputs.image_tag }}
environment: ${{ steps.set_outputs.outputs.environment }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python 3.13
uses: actions/setup-python@v4
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: 'ai-python/requirements.txt'
- name: Determine environment
id: determine_env
run: |
ENVIRONMENT="${{ github.event.inputs.ENVIRONMENT || 'dev' }}"
echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT
- name: Load environment variables
id: env_vars
run: |
ENV=${{ steps.determine_env.outputs.environment }}
REGISTRY="acrdigitalgarage02.azurecr.io"
IMAGE_ORG="hgzero"
RESOURCE_GROUP="rg-digitalgarage-02"
AKS_CLUSTER="aks-digitalgarage-02"
NAMESPACE="hgzero"
if [[ -f ".github/config/deploy_env_vars_ai-python_${ENV}" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
key=$(echo "$line" | cut -d '=' -f1)
value=$(echo "$line" | cut -d '=' -f2-)
case "$key" in
"resource_group") RESOURCE_GROUP="$value" ;;
"cluster_name") AKS_CLUSTER="$value" ;;
esac
done < ".github/config/deploy_env_vars_ai-python_${ENV}"
fi
echo "REGISTRY=$REGISTRY" >> $GITHUB_ENV
echo "IMAGE_ORG=$IMAGE_ORG" >> $GITHUB_ENV
echo "RESOURCE_GROUP=$RESOURCE_GROUP" >> $GITHUB_ENV
echo "AKS_CLUSTER=$AKS_CLUSTER" >> $GITHUB_ENV
- name: Install dependencies
run: |
cd ai-python
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
SKIP_TESTS: ${{ github.event.inputs.SKIP_TESTS || 'true' }}
run: |
if [[ "$SKIP_TESTS" == "true" ]]; then
echo "⏭️ Skipping Tests (SKIP_TESTS=$SKIP_TESTS)"
exit 0
fi
cd ai-python
# pytest가 requirements.txt에 있다면 실행
if pip list | grep -q "pytest"; then
if [ -d "tests" ]; then
pytest tests/ --cov=app --cov-report=xml --cov-report=html || echo "⚠️ Tests failed but continuing"
else
echo "⚠️ No tests directory found, skipping tests"
fi
else
echo "⚠️ pytest not installed, skipping tests"
fi
echo "✅ Tests completed successfully"
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
ai-python/htmlcov/
ai-python/coverage.xml
- name: Set outputs
id: set_outputs
run: |
IMAGE_TAG=$(date +%Y%m%d%H%M%S)
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "environment=${{ steps.determine_env.outputs.environment }}" >> $GITHUB_OUTPUT
release:
name: Build and Push Docker Image
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set environment variables from build job
run: |
echo "REGISTRY=${{ env.REGISTRY }}" >> $GITHUB_ENV
echo "IMAGE_ORG=${{ env.IMAGE_ORG }}" >> $GITHUB_ENV
echo "SERVICE_NAME=${{ env.SERVICE_NAME }}" >> $GITHUB_ENV
echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV
echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub (prevent rate limit)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to Azure Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push Docker image
run: |
echo "Building and pushing AI-Python service..."
docker build \
-f deployment/container/Dockerfile-ai-python \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/${{ env.SERVICE_NAME }}:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }} \
ai-python/
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/${{ env.SERVICE_NAME }}:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }}
echo "✅ Docker image pushed successfully"
update-manifest:
name: Update Manifest Repository
needs: [build, release]
runs-on: ubuntu-latest
steps:
- name: Set image tag environment variable
run: |
echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV
echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV
- name: Update Manifest Repository
run: |
# 매니페스트 레포지토리 클론
REPO_URL=$(echo "https://github.com/hjmoons/hgzero-manifest.git" | sed 's|https://||')
git clone https://${{ secrets.GIT_USERNAME }}:${{ secrets.GIT_PASSWORD }}@${REPO_URL} manifest-repo
cd manifest-repo
# Kustomize 설치
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
# 매니페스트 업데이트
cd hgzero-back/kustomize/overlays/${{ env.ENVIRONMENT }}
# AI-Python 서비스 이미지 태그 업데이트
kustomize edit set image acrdigitalgarage02.azurecr.io/hgzero/ai-python:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}
# Git 설정 및 푸시
cd ../../../..
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "🚀 Update AI-Python ${{ env.ENVIRONMENT }} image to ${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}"
git push origin main
echo "✅ 매니페스트 업데이트 완료. ArgoCD가 자동으로 배포합니다."

View File

@ -2,7 +2,7 @@ name: Backend Services CI/CD
on: on:
push: push:
branches: [ main ] branches: [ main, develop ]
paths: paths:
- 'user/**' - 'user/**'
- 'meeting/**' - 'meeting/**'
@ -11,8 +11,8 @@ on:
- 'notification/**' - 'notification/**'
- 'common/**' - 'common/**'
- '.github/**' - '.github/**'
# pull_request: pull_request:
# branches: [ main ] branches: [ main ]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
ENVIRONMENT: ENVIRONMENT:
@ -143,7 +143,6 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: app-builds name: app-builds
retention-days: 1
path: | path: |
user/build/libs/*.jar user/build/libs/*.jar
meeting/build/libs/*.jar meeting/build/libs/*.jar

View File

@ -1,214 +0,0 @@
name: RAG Service CI/CD
on:
push:
branches: [ main, develop ]
paths:
- 'rag/**'
- '.github/workflows/rag-cicd_ArgoCD.yaml'
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
ENVIRONMENT:
description: 'Target environment'
required: true
default: 'dev'
type: choice
options:
- dev
- staging
- prod
SKIP_TESTS:
description: 'Skip Tests'
required: false
default: 'true'
type: choice
options:
- 'false'
- 'true'
env:
REGISTRY: acrdigitalgarage02.azurecr.io
IMAGE_ORG: hgzero
SERVICE_NAME: rag
RESOURCE_GROUP: rg-digitalgarage-02
AKS_CLUSTER: aks-digitalgarage-02
NAMESPACE: hgzero
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.set_outputs.outputs.image_tag }}
environment: ${{ steps.set_outputs.outputs.environment }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
cache-dependency-path: 'rag/requirements.txt'
- name: Determine environment
id: determine_env
run: |
ENVIRONMENT="${{ github.event.inputs.ENVIRONMENT || 'dev' }}"
echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT
- name: Load environment variables
id: env_vars
run: |
ENV=${{ steps.determine_env.outputs.environment }}
REGISTRY="acrdigitalgarage02.azurecr.io"
IMAGE_ORG="hgzero"
RESOURCE_GROUP="rg-digitalgarage-02"
AKS_CLUSTER="aks-digitalgarage-02"
NAMESPACE="hgzero"
if [[ -f ".github/config/deploy_env_vars_rag_${ENV}" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
key=$(echo "$line" | cut -d '=' -f1)
value=$(echo "$line" | cut -d '=' -f2-)
case "$key" in
"resource_group") RESOURCE_GROUP="$value" ;;
"cluster_name") AKS_CLUSTER="$value" ;;
esac
done < ".github/config/deploy_env_vars_rag_${ENV}"
fi
echo "REGISTRY=$REGISTRY" >> $GITHUB_ENV
echo "IMAGE_ORG=$IMAGE_ORG" >> $GITHUB_ENV
echo "RESOURCE_GROUP=$RESOURCE_GROUP" >> $GITHUB_ENV
echo "AKS_CLUSTER=$AKS_CLUSTER" >> $GITHUB_ENV
- name: Install dependencies
run: |
cd rag
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
env:
SKIP_TESTS: ${{ github.event.inputs.SKIP_TESTS || 'true' }}
run: |
if [[ "$SKIP_TESTS" == "true" ]]; then
echo "⏭️ Skipping Tests (SKIP_TESTS=$SKIP_TESTS)"
exit 0
fi
cd rag
# Run pytest with coverage
pytest tests/ --cov=src --cov-report=xml --cov-report=html
echo "✅ Tests completed successfully"
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
rag/htmlcov/
rag/coverage.xml
- name: Set outputs
id: set_outputs
run: |
IMAGE_TAG=$(date +%Y%m%d%H%M%S)
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "environment=${{ steps.determine_env.outputs.environment }}" >> $GITHUB_OUTPUT
release:
name: Build and Push Docker Image
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set environment variables from build job
run: |
echo "REGISTRY=${{ env.REGISTRY }}" >> $GITHUB_ENV
echo "IMAGE_ORG=${{ env.IMAGE_ORG }}" >> $GITHUB_ENV
echo "SERVICE_NAME=${{ env.SERVICE_NAME }}" >> $GITHUB_ENV
echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV
echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub (prevent rate limit)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to Azure Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push Docker image
run: |
echo "Building and pushing RAG service..."
docker build \
--no-cache \
-f deployment/container/Dockerfile-rag \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/${{ env.SERVICE_NAME }}:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }} \
rag/
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/${{ env.SERVICE_NAME }}:${{ needs.build.outputs.environment }}-${{ needs.build.outputs.image_tag }}
echo "✅ Docker image pushed successfully"
update-manifest:
name: Update Manifest Repository
needs: [build, release]
runs-on: ubuntu-latest
steps:
- name: Set image tag environment variable
run: |
echo "IMAGE_TAG=${{ needs.build.outputs.image_tag }}" >> $GITHUB_ENV
echo "ENVIRONMENT=${{ needs.build.outputs.environment }}" >> $GITHUB_ENV
- name: Update Manifest Repository
run: |
# 매니페스트 레포지토리 클론
REPO_URL=$(echo "https://github.com/hjmoons/hgzero-manifest.git" | sed 's|https://||')
git clone https://${{ secrets.GIT_USERNAME }}:${{ secrets.GIT_PASSWORD }}@${REPO_URL} manifest-repo
cd manifest-repo
# Kustomize 설치
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
# 매니페스트 업데이트
cd hgzero-back/kustomize/overlays/${{ env.ENVIRONMENT }}
# RAG 서비스 이미지 태그 업데이트
kustomize edit set image acrdigitalgarage02.azurecr.io/hgzero/rag:${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}
# Git 설정 및 푸시
cd ../../../..
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "🚀 Update RAG ${{ env.ENVIRONMENT }} image to ${{ env.ENVIRONMENT }}-${{ env.IMAGE_TAG }}"
git push origin main
echo "✅ 매니페스트 업데이트 완료. ArgoCD가 자동으로 배포합니다."

6
.gitignore vendored
View File

@ -57,8 +57,4 @@ claudedocs/*back*
logs/ logs/
**/logs/ **/logs/
*.log *.log
**/*.log **/*.log
# Deprecated/Backup directories
ai-java-back/
ai/

215
Jenkinsfile vendored
View File

@ -1,215 +0,0 @@
def PIPELINE_ID = "${env.BUILD_NUMBER}"
def getImageTag() {
def dateFormat = new java.text.SimpleDateFormat('yyyyMMddHHmmss')
def currentDate = new Date()
return dateFormat.format(currentDate)
}
podTemplate(
label: "${PIPELINE_ID}",
serviceAccount: 'jenkins',
slaveConnectTimeout: 300,
idleMinutes: 1,
activeDeadlineSeconds: 3600,
podRetention: never(),
yaml: '''
spec:
terminationGracePeriodSeconds: 3
restartPolicy: Never
tolerations:
- effect: NoSchedule
key: dedicated
operator: Equal
value: cicd
''',
containers: [
containerTemplate(
name: 'podman',
image: "mgoltzsche/podman",
ttyEnabled: true,
command: 'cat',
privileged: true,
resourceRequestCpu: '500m',
resourceRequestMemory: '2Gi',
resourceLimitCpu: '2000m',
resourceLimitMemory: '4Gi'
),
containerTemplate(
name: 'gradle',
image: 'gradle:jdk21',
ttyEnabled: true,
command: 'cat',
resourceRequestCpu: '500m',
resourceRequestMemory: '1Gi',
resourceLimitCpu: '1000m',
resourceLimitMemory: '2Gi',
envVars: [
envVar(key: 'DOCKER_HOST', value: 'unix:///run/podman/podman.sock'),
envVar(key: 'TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE', value: '/run/podman/podman.sock'),
envVar(key: 'TESTCONTAINERS_RYUK_DISABLED', value: 'true')
]
),
containerTemplate(
name: 'git',
image: 'alpine/git:latest',
command: 'cat',
ttyEnabled: true,
resourceRequestCpu: '100m',
resourceRequestMemory: '256Mi',
resourceLimitCpu: '300m',
resourceLimitMemory: '512Mi'
)
],
volumes: [
emptyDirVolume(mountPath: '/home/gradle/.gradle', memory: false),
emptyDirVolume(mountPath: '/run/podman', memory: false)
]
) {
node(PIPELINE_ID) {
def imageTag = getImageTag()
def environment = params.ENVIRONMENT ?: 'dev'
def services = ['user', 'meeting', 'stt', 'notification']
def registry = 'acrdigitalgarage02.azurecr.io'
def imageOrg = 'hgzero'
try {
stage("Get Source") {
checkout scm
// 환경 변수 로드
def configFile = ".github/config/deploy_env_vars_${environment}"
if (fileExists(configFile)) {
echo "📋 Loading environment variables for ${environment}..."
def props = readProperties file: configFile
echo "Config loaded: ${props}"
}
}
stage('Build') {
container('gradle') {
echo "🔨 Building with Gradle..."
sh """
chmod +x gradlew
./gradlew build -x test
"""
}
}
stage('Archive Artifacts') {
echo "📦 Archiving build artifacts..."
archiveArtifacts artifacts: '**/build/libs/*.jar', fingerprint: true
}
stage('Build & Push Images') {
timeout(time: 30, unit: 'MINUTES') {
container('podman') {
withCredentials([
usernamePassword(
credentialsId: 'acr-credentials',
usernameVariable: 'ACR_USERNAME',
passwordVariable: 'ACR_PASSWORD'
),
usernamePassword(
credentialsId: 'dockerhub-credentials',
usernameVariable: 'DOCKERHUB_USERNAME',
passwordVariable: 'DOCKERHUB_PASSWORD'
)
]) {
echo "🐳 Building and pushing Docker images..."
// Login to Docker Hub (prevent rate limit)
sh "podman login docker.io --username \$DOCKERHUB_USERNAME --password \$DOCKERHUB_PASSWORD"
// Login to Azure Container Registry
sh "podman login ${registry} --username \$ACR_USERNAME --password \$ACR_PASSWORD"
// Build and push each service
services.each { service ->
echo "Building ${service}..."
def imageTagFull = "${registry}/${imageOrg}/${service}:${environment}-${imageTag}"
sh """
podman build \\
--build-arg BUILD_LIB_DIR="${service}/build/libs" \\
--build-arg ARTIFACTORY_FILE="${service}.jar" \\
-f deployment/container/Dockerfile-backend \\
-t ${imageTagFull} .
"""
echo "Pushing ${service}..."
sh "podman push ${imageTagFull}"
echo "✅ ${service} image pushed: ${imageTagFull}"
}
}
}
}
}
stage('Update Manifest Repository') {
container('git') {
withCredentials([usernamePassword(
credentialsId: 'github-credentials-dg0506',
usernameVariable: 'GIT_USERNAME',
passwordVariable: 'GIT_TOKEN'
)]) {
echo "📝 Updating manifest repository..."
sh """
# 매니페스트 레포지토리 클론
REPO_URL=\$(echo "https://github.com/hjmoons/hgzero-manifest.git" | sed 's|https://||')
git clone https://\${GIT_USERNAME}:\${GIT_TOKEN}@\${REPO_URL} manifest-repo
cd manifest-repo
# 각 서비스별 이미지 태그 업데이트 (sed 사용)
cd hgzero-back/kustomize/base
services="user meeting stt notification"
for service in \$services; do
echo "Updating \$service image tag..."
sed -i "s|image: ${registry}/${imageOrg}/\$service:.*|image: ${registry}/${imageOrg}/\$service:${environment}-${imageTag}|g" \\
\$service/deployment.yaml
# 변경 사항 확인
echo "Updated \$service deployment.yaml:"
grep "image: ${registry}/${imageOrg}/\$service" \$service/deployment.yaml
done
# Git 설정 및 푸시
cd ../../..
git config user.name "Jenkins"
git config user.email "jenkins@hgzero.com"
git add .
git commit -m "🚀 Update hgzero ${environment} images to ${environment}-${imageTag}"
git push origin main
echo "✅ 매니페스트 업데이트 완료. ArgoCD가 자동으로 배포합니다."
"""
}
}
}
stage('Pipeline Complete') {
echo "🧹 Pipeline completed. Pod cleanup handled by Jenkins Kubernetes Plugin."
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
echo "✅ Pipeline completed successfully!"
echo "Environment: ${environment}"
echo "Image Tag: ${imageTag}"
} else {
echo "❌ Pipeline failed with result: ${currentBuild.result}"
}
}
} catch (Exception e) {
currentBuild.result = 'FAILURE'
echo "❌ Pipeline failed with exception: ${e.getMessage()}"
throw e
} finally {
echo "🧹 Cleaning up resources and preparing for pod termination..."
echo "Pod will be terminated in 3 seconds due to terminationGracePeriodSeconds: 3"
}
}
}

376
README.md
View File

@ -1,376 +0,0 @@
# HGZero - AI 기반 회의록 작성 및 이력 관리 개선 서비스
## 1. 소개
HGZero는 업무지식이 부족한 회의록 작성자도 누락 없이 정확하게 회의록을 작성하여 공유할 수 있는 AI 기반 서비스입니다.
사용자는 실시간 음성 변환(STT), AI 요약, 용어 설명, Todo 자동 추출 등의 기능을 통해 회의록 작성 업무를 효율적으로 수행할 수 있습니다.
### 1.1 핵심 기능
- **실시간 STT**: Azure Speech Services 기반 실시간 음성-텍스트 변환
- **AI 요약**: 안건별 2-3문장 자동 요약 (GPT-4o, 2-5초 처리)
- **맥락 기반 용어 설명**: 관련 회의록과 업무이력 기반 실용 정보 제공
- **Todo 자동 추출**: 회의록에서 액션 아이템 자동 추출 및 배정
- **지능형 회의 진행 지원**: 회의 패턴 분석, 안건 추천, 효율성 분석
- **실시간 협업**: WebSocket 기반 실시간 회의록 편집 및 동기화
### 1.2 MVP 산출물
- **발표자료**: {발표자료 링크}
- **설계결과**:
- [유저스토리](design/userstory.md)
- [논리 아키텍처](design/backend/logical/logical-architecture.md)
- [API 설계서](design/backend/api/API설계서.md)
- **Git Repo**:
- **메인**: https://gitea.cbiz.kubepia.net/shared-dg05-coffeeQuokka/hgzero.git
- **프론트엔드**: {프론트엔드 Repository 링크}
- **manifest**: {Manifest Repository 링크}
- **시연 동영상**: {시연 동영상 링크}
## 2. 시스템 아키텍처
### 2.1 전체 구조
마이크로서비스 아키텍처 기반 클라우드 네이티브 애플리케이션
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ React 18 + TypeScript │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ NGINX Ingress │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ User Service │ │Meeting Service│ │ STT Service │
│ (Java) │ │ (Java) │ │ (Java) │
│ :8081 │ │ :8081/:8082 │ │ :8083 │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
│ │ │
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ AI Service │ │ RAG Service │ │Notification │
│ (Java) │ │ (Python) │ │ (Java) │
│ :8083 │ │ :8000 │ │ :8084 │
└───────────────┘ └───────────────┘ └───────────────┘
│ │
└───────────────────┤
┌───────────────────┐
│ Event Hub │
│ (Pub/Sub MQ) │
└───────────────────┘
┌─────────┼─────────┐
│ │ │
┌──────────┐ ┌────────┐ ┌─────────────┐
│PostgreSQL│ │ Redis │ │ OpenAI │
│ (6 DB) │ │ Cache │ │ │
└──────────┘ └────────┘ └─────────────┘
```
### 2.2 마이크로서비스 구성
- **User 서비스**: 사용자 인증 (LDAP, JWT) 및 프로필 관리
- **Meeting 서비스**: 회의/회의록/Todo 통합 관리, 실시간 동기화 (WebSocket)
- **STT 서비스**: 음성 스트리밍, 실시간 STT 변환 (Azure Speech Services)
- **AI 서비스**: 회의록 자동요약, Todo 추출, 안건별 AI 요약
- **RAG 서비스**: 용어집 검색, 관련자료 검색, 회의록 유사도 검색 (Python/FastAPI)
- **Notification 서비스**: 이메일 알림 (회의 시작, 회의록 확정, Todo 배정)
### 2.3 기술 스택
- **프론트엔드**: React 18, TypeScript, React Context API
- **백엔드**: Spring Boot 3.2.x, Java 17, FastAPI, Python 3.11+
- **인프라**: Azure Kubernetes Service (AKS), Azure Container Registry (ACR)
- **CI/CD**: GitHub Actions (CI), ArgoCD (CD - GitOps)
- **모니터링**: Prometheus, Grafana, Spring Boot Actuator
- **백킹 서비스**:
- **Database**: PostgreSQL 15 (Database per Service - 6개 독립 DB)
- **Message Queue**: Azure Event Hub (AMQP over TLS)
- **Cache**: Azure Redis Cache (Redis 7.x)
- **AI/ML**: Azure OpenAI (GPT-4o, text-embedding-3-large), Azure Speech Services, Azure AI Search
## 3. 백킹 서비스 설치
### 3.1 Database 설치
PostgreSQL 15 설치 (각 서비스별 독립 데이터베이스)
```bash
# Helm 저장소 추가
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# User 서비스용 DB
helm install hgzero-user bitnami/postgresql \
--set global.postgresql.auth.postgresPassword=Passw0rd \
--set global.postgresql.auth.username=hgzerouser \
--set global.postgresql.auth.password=Passw0rd \
--set global.postgresql.auth.database=userdb \
--namespace hgzero
# Meeting 서비스용 DB
helm install hgzero-meeting bitnami/postgresql \
--set global.postgresql.auth.postgresPassword=Passw0rd \
--set global.postgresql.auth.username=hgzerouser \
--set global.postgresql.auth.password=Passw0rd \
--set global.postgresql.auth.database=meetingdb \
--namespace hgzero
# STT 서비스용 DB
helm install hgzero-stt bitnami/postgresql \
--set global.postgresql.auth.postgresPassword=Passw0rd \
--set global.postgresql.auth.username=hgzerouser \
--set global.postgresql.auth.password=Passw0rd \
--set global.postgresql.auth.database=sttdb \
--namespace hgzero
# AI 서비스용 DB
helm install hgzero-ai bitnami/postgresql \
--set global.postgresql.auth.postgresPassword=Passw0rd \
--set global.postgresql.auth.username=hgzerouser \
--set global.postgresql.auth.password=Passw0rd \
--set global.postgresql.auth.database=aidb \
--namespace hgzero
# RAG 서비스용 DB
helm install hgzero-rag bitnami/postgresql \
--set global.postgresql.auth.postgresPassword=Passw0rd \
--set global.postgresql.auth.username=hgzerouser \
--set global.postgresql.auth.password=Passw0rd \
--set global.postgresql.auth.database=ragdb \
--namespace hgzero
# Notification 서비스용 DB
helm install hgzero-notification bitnami/postgresql \
--set global.postgresql.auth.postgresPassword=Passw0rd \
--set global.postgresql.auth.username=hgzerouser \
--set global.postgresql.auth.password=Passw0rd \
--set global.postgresql.auth.database=notificationdb \
--namespace hgzero
```
**접속 정보 확인**:
```bash
# 각 서비스별 DB 접속 정보
# User DB: hgzero-user-postgresql.hgzero.svc.cluster.local:5432
# Meeting DB: hgzero-meeting-postgresql.hgzero.svc.cluster.local:5432
# STT DB: hgzero-stt-postgresql.hgzero.svc.cluster.local:5432
# AI DB: hgzero-ai-postgresql.hgzero.svc.cluster.local:5432
# RAG DB: hgzero-rag-postgresql.hgzero.svc.cluster.local:5432
# Notification DB: hgzero-notification-postgresql.hgzero.svc.cluster.local:5432
```
### 3.2 Cache 설치
Redis 설치
```bash
# Helm으로 Redis 설치
helm install hgzero-redis bitnami/redis \
--set auth.password=Passw0rd \
--set master.persistence.enabled=true \
--set master.persistence.size=8Gi \
--namespace hgzero
# 접속 정보 확인
export REDIS_PASSWORD=$(kubectl get secret --namespace hgzero hgzero-redis -o jsonpath="{.data.redis-password}" | base64 -d)
echo "Redis Password: $REDIS_PASSWORD"
echo "Redis Host: hgzero-redis-master.hgzero.svc.cluster.local"
echo "Redis Port: 6379"
```
**Azure Redis Cache 사용 (프로덕션)**:
```bash
# Azure Portal에서 Redis Cache 생성 후 연결 정보 사용
# 또는 Azure CLI 사용
az redis create \
--name hgzero-redis \
--resource-group your-resource-group \
--location koreacentral \
--sku Basic \
--vm-size c0
```
### 3.3 Message Queue 설치
Azure Event Hub 설정
```bash
# Azure CLI를 통한 Event Hub 생성
az eventhubs namespace create \
--name hgzero-eventhub-ns \
--resource-group your-resource-group \
--location koreacentral \
--sku Standard
az eventhubs eventhub create \
--name hgzero-events \
--namespace-name hgzero-eventhub-ns \
--resource-group your-resource-group \
--partition-count 4 \
--message-retention 7
# 연결 문자열 확인
az eventhubs namespace authorization-rule keys list \
--resource-group your-resource-group \
--namespace-name hgzero-eventhub-ns \
--name RootManageSharedAccessKey \
--query primaryConnectionString \
--output tsv
```
## 4. 빌드 및 배포
### 4.1 프론트엔드 빌드 및 배포
#### 1. 애플리케이션 빌드
```bash
cd frontend
npm install
npm run build
```
#### 2. 컨테이너 이미지 빌드
```bash
docker build \
--build-arg REACT_APP_API_URL="http://api.hgzero.com" \
--build-arg REACT_APP_WS_URL="ws://api.hgzero.com/ws" \
-f deployment/container/Dockerfile-frontend \
-t acrdigitalgarage02.azurecr.io/hgzero/frontend:latest .
```
#### 3. 이미지 푸시
```bash
docker push acrdigitalgarage02.azurecr.io/hgzero/frontend:latest
```
#### 4. Kubernetes 배포
```bash
kubectl apply -f deployment/k8s/frontend/frontend-deployment.yaml -n hgzero
```
### 4.2 백엔드 빌드 및 배포
#### 1. 애플리케이션 빌드
```bash
# 전체 프로젝트 빌드
./gradlew clean build -x test
# 또는 개별 서비스 빌드
./gradlew :user:clean :user:build -x test
./gradlew :meeting:clean :meeting:build -x test
./gradlew :stt:clean :stt:build -x test
./gradlew :ai:clean :ai:build -x test
./gradlew :notification:clean :notification:build -x test
```
#### 2. 컨테이너 이미지 빌드 (각 서비스별로 수행)
```bash
# User 서비스
docker build \
--build-arg BUILD_LIB_DIR="user/build/libs" \
--build-arg ARTIFACTORY_FILE="user.jar" \
-f deployment/container/Dockerfile-user \
-t acrdigitalgarage02.azurecr.io/hgzero/user-service:latest .
# Meeting 서비스
docker build \
--build-arg BUILD_LIB_DIR="meeting/build/libs" \
--build-arg ARTIFACTORY_FILE="meeting.jar" \
-f deployment/container/Dockerfile-meeting \
-t acrdigitalgarage02.azurecr.io/hgzero/meeting-service:latest .
# STT 서비스
docker build \
--build-arg BUILD_LIB_DIR="stt/build/libs" \
--build-arg ARTIFACTORY_FILE="stt.jar" \
-f deployment/container/Dockerfile-stt \
-t acrdigitalgarage02.azurecr.io/hgzero/stt-service:latest .
# AI 서비스
docker build \
--build-arg BUILD_LIB_DIR="ai/build/libs" \
--build-arg ARTIFACTORY_FILE="ai.jar" \
-f deployment/container/Dockerfile-ai \
-t acrdigitalgarage02.azurecr.io/hgzero/ai-service:latest .
# Notification 서비스
docker build \
--build-arg BUILD_LIB_DIR="notification/build/libs" \
--build-arg ARTIFACTORY_FILE="notification.jar" \
-f deployment/container/Dockerfile-notification \
-t acrdigitalgarage02.azurecr.io/hgzero/notification-service:latest .
```
#### 3. RAG 서비스 빌드 (Python)
```bash
docker build \
-f deployment/container/Dockerfile-rag \
-t acrdigitalgarage02.azurecr.io/hgzero/rag-service:latest \
./rag
```
#### 4. 이미지 푸시
```bash
docker push acrdigitalgarage02.azurecr.io/hgzero/user-service:latest
docker push acrdigitalgarage02.azurecr.io/hgzero/meeting-service:latest
docker push acrdigitalgarage02.azurecr.io/hgzero/stt-service:latest
docker push acrdigitalgarage02.azurecr.io/hgzero/ai-service:latest
docker push acrdigitalgarage02.azurecr.io/hgzero/rag-service:latest
docker push acrdigitalgarage02.azurecr.io/hgzero/notification-service:latest
```
#### 5. Kubernetes 배포
```bash
# Namespace 생성
kubectl create namespace hgzero
# Secret 생성 (환경 변수)
kubectl apply -f deployment/k8s/backend/secrets/ -n hgzero
# 서비스 배포
kubectl apply -f deployment/k8s/backend/user-service.yaml -n hgzero
kubectl apply -f deployment/k8s/backend/meeting-service.yaml -n hgzero
kubectl apply -f deployment/k8s/backend/stt-service.yaml -n hgzero
kubectl apply -f deployment/k8s/backend/ai-service.yaml -n hgzero
kubectl apply -f deployment/k8s/backend/rag-service.yaml -n hgzero
kubectl apply -f deployment/k8s/backend/notification-service.yaml -n hgzero
# Ingress 설정
kubectl apply -f deployment/k8s/ingress.yaml -n hgzero
```
### 4.3 테스트
#### 1) 프론트엔드 페이지 주소 구하기
```bash
# Namespace 설정
kubens hgzero
# Service 확인
kubectl get svc
# Ingress 확인
kubectl get ingress
```
#### 2) API 테스트
**Swagger UI 접근**:
- User Service: http://{INGRESS_URL}/user/swagger-ui.html
- Meeting Service: http://{INGRESS_URL}/meeting/swagger-ui.html
- STT Service: http://{INGRESS_URL}/stt/swagger-ui.html
- AI Service: http://{INGRESS_URL}/ai/swagger-ui.html
- RAG Service: http://{INGRESS_URL}/rag/docs
- Notification Service: http://{INGRESS_URL}/notification/swagger-ui.html
#### 3) 로그인 테스트
- ID: user-005, user2@example.com
- PW: 8자리
## 5. 팀
- 유동희 "야보" - Product Owner
- 조민서 "다람지" - AI Specialist
- 김주환 "블랙" - Architect
- 김종희 "페퍼" - Frontend Developer
- 문효종 "카누" - Frontend Developer / DevOps Engineer
- 전대웅 "맥심" - Backend Developer
- 조윤진 "쿼카" - Backend Developer

View File

@ -248,53 +248,3 @@ A: 네, 각 클라이언트는 독립적으로 SSE 연결을 유지합니다.
**Q: 제안사항이 오지 않으면?** **Q: 제안사항이 오지 않으면?**
A: Redis에 충분한 텍스트(10개 세그먼트)가 축적되어야 분석이 시작됩니다. 5초마다 체크합니다. 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:8087/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 처리 중 오류 발생

View File

@ -2,11 +2,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from .transcripts import router as transcripts_router from .transcripts import router as transcripts_router
from .suggestions import router as suggestions_router from .suggestions import router as suggestions_router
from .summary import router as summary_router
router = APIRouter() router = APIRouter()
# 라우터 등록 # 라우터 등록
router.include_router(transcripts_router, prefix="/transcripts", tags=["Transcripts"]) router.include_router(transcripts_router, prefix="/transcripts", tags=["Transcripts"])
router.include_router(suggestions_router, prefix="/ai/suggestions", tags=["AI Suggestions"]) router.include_router(suggestions_router, prefix="/ai/suggestions", tags=["AI Suggestions"])
router.include_router(summary_router, prefix="/ai/summary", tags=["AI Summary"])

View File

@ -26,7 +26,7 @@ claude_service = ClaudeService()
### 동작 방식 ### 동작 방식
1. Redis에서 누적된 회의 텍스트 조회 (5초마다) 1. Redis에서 누적된 회의 텍스트 조회 (5초마다)
2. 임계값(4 세그먼트, 60) 이상이면 Claude API로 분석 2. 임계값(10 세그먼트) 이상이면 Claude API로 분석
3. 분석 결과를 SSE 이벤트로 전송 3. 분석 결과를 SSE 이벤트로 전송
### SSE 이벤트 형식 ### SSE 이벤트 형식
@ -116,45 +116,21 @@ async def stream_ai_suggestions(meeting_id: str):
if accumulated_text: if accumulated_text:
logger.info(f"텍스트 누적 완료 - meetingId: {meeting_id}, 길이: {len(accumulated_text)}") logger.info(f"텍스트 누적 완료 - meetingId: {meeting_id}, 길이: {len(accumulated_text)}")
# 이미 생성된 제안사항 조회
existing_suggestions = await redis_service.get_generated_suggestions(meeting_id)
# Claude API로 분석 # Claude API로 분석
suggestions = await claude_service.analyze_suggestions(accumulated_text) suggestions = await claude_service.analyze_suggestions(accumulated_text)
if suggestions.suggestions: if suggestions.suggestions:
# 중복 제거: 새로운 제안사항만 필터링 # SSE 이벤트 전송
new_suggestions = [ yield {
s for s in suggestions.suggestions "event": "ai-suggestion",
if s.content not in existing_suggestions "id": str(current_count),
] "data": suggestions.json()
}
if new_suggestions: logger.info(
# 새로운 제안사항만 SSE 이벤트 전송 f"AI 제안사항 발행 - meetingId: {meeting_id}, "
from app.models import RealtimeSuggestionsResponse f"개수: {len(suggestions.suggestions)}"
filtered_response = RealtimeSuggestionsResponse(suggestions=new_suggestions) )
yield {
"event": "ai-suggestion",
"id": str(current_count),
"data": filtered_response.json()
}
# Redis에 새로운 제안사항 저장
for suggestion in new_suggestions:
await redis_service.add_generated_suggestion(
meeting_id,
suggestion.content
)
logger.info(
f"AI 제안사항 발행 - meetingId: {meeting_id}, "
f"전체: {len(suggestions.suggestions)}, 신규: {len(new_suggestions)}"
)
else:
logger.info(
f"중복 제거 후 신규 제안사항 없음 - meetingId: {meeting_id}"
)
previous_count = current_count previous_count = current_count
@ -184,6 +160,8 @@ async def stream_ai_suggestions(meeting_id: str):
headers={ headers={
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
"X-Accel-Buffering": "no", "X-Accel-Buffering": "no",
"Access-Control-Allow-Origin": "http://localhost:8888",
"Access-Control-Allow-Credentials": "true",
} }
) )

View File

@ -1,84 +0,0 @@
"""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)}"
)

View File

@ -10,12 +10,12 @@ class Settings(BaseSettings):
# 서버 설정 # 서버 설정
app_name: str = "AI Service (Python)" app_name: str = "AI Service (Python)"
host: str = "0.0.0.0" host: str = "0.0.0.0"
port: int = 8087 port: int = 8087 # feature/stt-ai 브랜치 AI Service(8086)와 충돌 방지
# Claude API # Claude API
claude_api_key: str = "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" claude_api_key: str = "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"
claude_model: str = "claude-sonnet-4-5-20250929" claude_model: str = "claude-3-5-sonnet-20240620"
claude_max_tokens: int = 4096 claude_max_tokens: int = 250000
claude_temperature: float = 0.7 claude_temperature: float = 0.7
# Redis # Redis
@ -36,15 +36,14 @@ class Settings(BaseSettings):
"http://localhost:3000", "http://localhost:3000",
"http://127.0.0.1:8888", "http://127.0.0.1:8888",
"http://127.0.0.1:8080", "http://127.0.0.1:8080",
"http://127.0.0.1:3000", "http://127.0.0.1:3000"
"http://localhost:*" # 모든 localhost 포트 허용
] ]
# 로깅 # 로깅
log_level: str = "INFO" log_level: str = "INFO"
# 분석 임계값 (실시간 응답을 위해 낮춤) # 분석 임계값 (MVP 수준)
min_segments_for_analysis: int = 2 # 2개 세그먼트 (약 30초, 빠른 피드백) min_segments_for_analysis: int = 3 # 3개 세그먼트 = 약 15-30초 분량의 대화
text_retention_seconds: int = 300 # 5분 text_retention_seconds: int = 300 # 5분
class Config: class Config:

View File

@ -10,10 +10,6 @@ from .response import (
SimpleSuggestion, SimpleSuggestion,
RealtimeSuggestionsResponse RealtimeSuggestionsResponse
) )
from .summary import (
SummaryRequest,
SummaryResponse
)
__all__ = [ __all__ = [
"ConsolidateRequest", "ConsolidateRequest",
@ -23,6 +19,4 @@ __all__ = [
"ExtractedTodo", "ExtractedTodo",
"SimpleSuggestion", "SimpleSuggestion",
"RealtimeSuggestionsResponse", "RealtimeSuggestionsResponse",
"SummaryRequest",
"SummaryResponse",
] ]

View File

@ -1,81 +0,0 @@
"""요약 관련 모델"""
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"
}
}

View File

@ -20,9 +20,8 @@ class ConsolidateRequest(BaseModel):
class ExtractedTodo(BaseModel): class ExtractedTodo(BaseModel):
"""추출된 Todo""" """추출된 Todo (제목만)"""
title: str = Field(..., description="Todo 제목") title: str = Field(..., description="Todo 제목")
assignee: str = Field(default="", description="담당자 이름 (있는 경우에만)")
class AgendaSummary(BaseModel): class AgendaSummary(BaseModel):
@ -31,7 +30,6 @@ class AgendaSummary(BaseModel):
agenda_title: str = Field(..., description="안건 제목") agenda_title: str = Field(..., description="안건 제목")
summary_short: str = Field(..., description="AI 생성 짧은 요약 (1줄, 20자 이내)") summary_short: str = Field(..., description="AI 생성 짧은 요약 (1줄, 20자 이내)")
summary: str = Field(..., description="안건별 회의록 요약 (논의사항+결정사항, 사용자 수정 가능)") summary: str = Field(..., description="안건별 회의록 요약 (논의사항+결정사항, 사용자 수정 가능)")
decisions: List[str] = Field(default_factory=list, description="안건별 결정사항 배열 (대시보드 표시용)")
pending: List[str] = Field(default_factory=list, description="보류 사항") pending: List[str] = Field(default_factory=list, description="보류 사항")
todos: List[ExtractedTodo] = Field(default_factory=list, description="Todo 목록 (제목만)") todos: List[ExtractedTodo] = Field(default_factory=list, description="Todo 목록 (제목만)")

View File

@ -49,23 +49,13 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
- **agenda_number**: 안건 번호 (1, 2, 3...) - **agenda_number**: 안건 번호 (1, 2, 3...)
- **agenda_title**: 안건 제목 (간결하게) - **agenda_title**: 안건 제목 (간결하게)
- **summary_short**: AI가 생성한 1 요약 (20 이내, 사용자 수정 불가) - **summary_short**: AI가 생성한 1 요약 (20 이내, 사용자 수정 불가)
- **summary**: 안건별 회의록 요약 (논의사항과 결정사항 모두 포함) - **summary**: 안건별 회의록 요약 (논의사항과 결정사항포함한 전체 요약)
* 회의록 수정 페이지에서 사용자가 수정할 있는 입력 필드 * 회의록 수정 페이지에서 사용자가 수정할 있는 입력 필드
* 형식: "**논의 사항:**\n- 논의내용1\n- 논의내용2\n\n**결정 사항:**\n- 결정1\n- 결정2" * 형식: "**논의 사항:**\n- 논의내용1\n- 논의내용2\n\n**결정 사항:**\n- 결정1\n- 결정2"
* 사용자가 자유롭게 편집할 있도록 구조화된 텍스트로 작성 * 사용자가 자유롭게 편집할 있도록 구조화된 텍스트로 작성
- **decisions**: 안건별 결정사항 배열 (대시보드 표시용, summary의 결정사항 부분을 배열로 추출)
* 형식: ["결정사항1", "결정사항2", "결정사항3"]
* 회의에서 최종 결정된 사항만 포함
- **pending**: 보류 사항 배열 (추가 논의 필요 사항) - **pending**: 보류 사항 배열 (추가 논의 필요 사항)
- **todos**: Todo 배열 (제목과 담당자 추출) - **todos**: Todo 배열 (제목만, 담당자/마감일/우선순위 없음)
- title: Todo 제목 (: "시장 조사 보고서 작성") - title: Todo 제목만 추출 (: "시장 조사 보고서 작성")
- assignee: 담당자 이름 (있는 경우에만, : "김대리", "박과장")
**Todo 추출 가이드:**
- 자연스러운 표현도 인식: "김대리가 ~하기로 함", "박과장은 ~준비합니다", "이차장님께서 ~하시기로 하셨습니다"
- 실행 동사 패턴: ~하기로, ~준비, ~작성, ~제출, ~완료, ~진행, ~검토, ~분석
- 담당자 패턴: "OO님", "OO이/가", "OO은/는", "OO께서"
- 기한 표현: "다음주", "이번주", "~까지", "~일까지", "~월까지"
--- ---
@ -87,16 +77,10 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
"agenda_title": "안건 제목", "agenda_title": "안건 제목",
"summary_short": "짧은 요약 (20자 이내)", "summary_short": "짧은 요약 (20자 이내)",
"summary": "**논의 사항:**\\n- 논의내용1\\n- 논의내용2\\n\\n**결정 사항:**\\n- 결정1\\n- 결정2", "summary": "**논의 사항:**\\n- 논의내용1\\n- 논의내용2\\n\\n**결정 사항:**\\n- 결정1\\n- 결정2",
"decisions": ["결정사항1", "결정사항2"],
"pending": ["보류사항"], "pending": ["보류사항"],
"todos": [ "todos": [
{{ {{
"title": "인플루언서 리스트 작성", "title": "Todo 제목"
"assignee": "김대리"
}},
{{
"title": "캠페인 콘텐츠 기획안 초안 작성",
"assignee": "박과장"
}} }}
] ]
}} }}
@ -113,17 +97,12 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
3. **완전성**: 모든 필드를 빠짐없이 작성 3. **완전성**: 모든 필드를 빠짐없이 작성
4. **구조화**: 안건별로 명확히 분리 4. **구조화**: 안건별로 명확히 분리
5. **결정사항 추출**: 5. **결정사항 추출**:
- 회의 전체 결정사항(decisions): 모든 안건의 결정사항을 포함 (TEXT 형식) - 회의 전체 결정사항(decisions) 모든 안건의 결정사항을 포함
- 안건별 결정사항(agenda_summaries[].decisions): 안건의 결정사항을 배열로 추출 - 안건별 summary에도 결정사항을 포함하여 사용자가 수정 가능하도록 작성
- 결정사항이 명확하게 언급된 경우에만 포함
6. **summary 작성**: 6. **summary 작성**:
- summary_short: AI가 자동 생성한 1 요약 (사용자 수정 불가) - summary_short: AI가 자동 생성한 1 요약 (사용자 수정 불가)
- summary: 논의사항과 결정사항 모두 포함 (사용자 수정 가능) - summary: 논의사항과 결정사항을 포함한 전체 요약 (사용자 수정 가능)
- decisions: summary의 결정사항 부분을 배열로 별도 추출 (대시보드 표시용) 7. **Todo 추출**: 제목만 추출 (담당자나 마감일 없어도 )
7. **Todo 추출**:
- 제목 필수, 담당자는 언급된 경우에만 추출
- 자연스러운 표현에서 추출: "김대리가 ~하기로 함" title: "~", assignee: "김대리"
- 담당자가 없으면 assignee: "" ( 문자열)
8. **JSON만 출력**: 추가 설명 없이 JSON만 반환 8. **JSON만 출력**: 추가 설명 없이 JSON만 반환
이제 회의록들을 분석하여 통합 요약을 JSON 형식으로 생성해주세요. 이제 회의록들을 분석하여 통합 요약을 JSON 형식으로 생성해주세요.

View File

@ -1,432 +1,72 @@
"""AI 제안사항 추출 프롬프트 (회의록 작성 MVP 최적화)""" """AI 제안사항 추출 프롬프트"""
def get_suggestions_prompt(transcript_text: str) -> tuple[str, str]: def get_suggestions_prompt(transcript_text: str) -> tuple[str, str]:
""" """
회의 텍스트에서 AI 제안사항을 추출하는 프롬프트 생성 (회의록 MVP용) 회의 텍스트에서 AI 제안사항을 추출하는 프롬프트 생성
Returns: Returns:
(system_prompt, user_prompt) 튜플 (system_prompt, user_prompt) 튜플
""" """
system_prompt = """당신은 실시간 회의록 작성 AI 비서입니다. system_prompt = """당신은 회의 내용 분석 전문가입니다.
회의 텍스트를 분석하여 실행 가능한 제안사항을 추출해주세요."""
**핵심 역할**: user_prompt = f"""다음 회의 내용을 분석하여 **구체적이고 실행 가능한 제안사항**을 추출해주세요.
회의 발언되는 내용을 실시간으로 분석하여, 회의록 작성자가 놓칠 있는 중요한 정보를 즉시 메모로 제공합니다.
**작업 방식**:
1. 회의 안건, 결정 사항, 이슈, 액션 아이템을 자동으로 분류
2. 담당자, 기한, 우선순위 구조화된 정보로 정리
3. 단순 발언 반복이 아닌, 실무에 바로 사용 가능한 형식으로 요약
4. 회의록 작성 시간을 70% 단축시키는 것이 목표
**핵심 원칙**:
- 인사말, 반복, 불필요한 추임새는 완전히 제거
- 실제 회의록에 들어갈 내용만 추출
- 명확하고 간결하게 (20-50)
- 구어체 종결어미(~, ~, ~습니다) 제거하고 명사형으로 정리"""
user_prompt = f"""다음 회의 대화를 실시간으로 분석하여 **회의록 메모**를 작성하세요.
# 회의 내용 # 회의 내용
{transcript_text} {transcript_text}
--- ---
# 회의록 항목별 패턴 학습 # 제안사항 추출 기준
1. **실행 가능성**: 바로 실행할 있는 구체적인 액션 아이템
2. **명확성**: 누가, 무엇을, 언제까지 해야 하는지 명확한 내용
3. **중요도**: 회의 목표 달성에 중요한 사항
4. **완결성**: 하나의 제안사항이 독립적으로 완결된 내용
## 📋 1. 회의 안건 (Agenda) # 제안사항 유형 예시
- **후속 작업**: "시장 조사 보고서를 다음 주까지 작성하여 공유"
- **의사결정 필요**: "예산안 3안 중 최종안을 이번 주 금요일까지 결정"
- **리스크 대응**: "법률 검토를 위해 법무팀과 사전 협의 필요"
- **일정 조율**: "다음 회의를 3월 15일로 확정하고 참석자에게 공지"
- **자료 준비**: "경쟁사 분석 자료를 회의 전까지 준비"
- **검토 요청**: "초안에 대한 팀원들의 피드백 수집 필요"
- **승인 필요**: "최종 기획안을 경영진에게 보고하여 승인 받기"
### 패턴 인식 # 제안사항 작성 가이드
- "오늘 회의 안건은 ~" - **구체적으로**: "검토 필요" (X) "법무팀과 계약서 조항 검토 미팅 잡기" (O)
- "논의할 주제는 ~" - **명확하게**: "나중에 하기" (X) "다음 주 화요일까지 완료" (O)
- "다룰 내용은 ~" - **실행 가능하게**: "잘 되길 바람" (X) "주간 진행상황 공유 미팅 설정" (O)
- "검토할 사항은 ~"
### ✅ 좋은 예시
**입력**: "오늘 회의 안건은 신제품 출시 일정과 마케팅 전략입니다."
**출력**:
```json
{{
"content": "📋 회의 안건: 신제품 출시 일정, 마케팅 전략",
"confidence": 0.95
}}
```
**입력**: "다음 주 프로젝트 킥오프에 대해 논의하겠습니다."
**출력**:
```json
{{
"content": "📋 회의 안건: 다음 주 프로젝트 킥오프",
"confidence": 0.90
}}
```
### ❌ 나쁜 예시
**입력**: "오늘 회의 안건은 신제품 출시 일정입니다."
**나쁜 출력**:
```json
{{
"content": "오늘 회의 안건은 신제품 출시 일정입니다", 구어체 그대로 반복
"confidence": 0.90
}}
```
**이유**: 구어체 종결어미(~입니다) 그대로 반복. "📋 회의 안건: 신제품 출시 일정"으로 구조화해야
---
## ✅ 2. 결정 사항 (Decisions)
### 패턴 인식
- "결정 사항은 ~", "~로 결정했습니다"
- "~하기로 했습니다", "~로 합의했습니다"
- "~로 확정됐습니다"
- "최종 결론은 ~"
### ✅ 좋은 예시
**입력**: "회의 결과, 신규 프로젝트는 다음 달부터 착수하기로 결정했습니다."
**출력**:
```json
{{
"content": "✅ 결정사항: 신규 프로젝트 다음 달 착수",
"confidence": 0.95
}}
```
**입력**: "최종 결론은 외주 개발사와 계약하기로 합의했습니다."
**출력**:
```json
{{
"content": "✅ 결정사항: 외주 개발사와 계약 진행",
"confidence": 0.92
}}
```
### ❌ 나쁜 예시
**입력**: "신규 프로젝트는 다음 달부터 착수하기로 결정했습니다."
**나쁜 출력**:
```json
{{
"content": "신규 프로젝트는 다음 달부터 착수하기로 결정했습니다", 원문 그대로
"confidence": 0.90
}}
```
**이유**: 발언을 그대로 반복. "✅ 결정사항: 신규 프로젝트 다음 달 착수" 구조화해야
---
## 🎯 3. 액션 아이템 (Action Items)
### 패턴 인식
- "~팀에서 ~해 주세요"
- "~님이 ~까지 ~하기로 했습니다"
- "~을 ~까지 완료하겠습니다"
- "~을 검토해 보겠습니다"
### ✅ 좋은 예시
**입력**: "개발팀에서 API 문서를 이번 주 금요일까지 작성해 주세요."
**출력**:
```json
{{
"content": "🎯 개발팀: API 문서 작성 (기한: 이번 주 금요일)",
"confidence": 0.95
}}
```
**입력**: "김 팀장님이 내일까지 견적서를 검토해서 회신하기로 했습니다."
**출력**:
```json
{{
"content": "🎯 김 팀장: 견적서 검토 및 회신 (기한: 내일)",
"confidence": 0.93
}}
```
**입력**: "제가 고객사에 연락해서 미팅 일정 잡도록 하겠습니다."
**출력**:
```json
{{
"content": "🎯 고객사 미팅 일정 조율 예정",
"confidence": 0.85
}}
```
### ❌ 나쁜 예시
**입력**: "개발팀에서 API 문서를 이번 주 금요일까지 작성해 주세요."
**나쁜 출력 1**:
```json
{{
"content": "개발팀에서 API 문서를 이번 주 금요일까지 작성해 주세요", 원문 반복
"confidence": 0.90
}}
```
**나쁜 출력 2**:
```json
{{
"content": "API 문서 작성", 담당자와 기한 누락
"confidence": 0.80
}}
```
**이유**: "🎯 개발팀: API 문서 작성 (기한: 이번 주 금요일)" 형식으로 구조화해야
---
## ⚠️ 4. 이슈/문제점 (Issues)
### 패턴 인식
- "문제가 있습니다", "이슈가 발생했습니다"
- "우려되는 점은 ~"
- "해결이 필요한 부분은 ~"
- "리스크가 있습니다"
### ✅ 좋은 예시
**입력**: "현재 서버 성능 이슈가 발생해서 긴급 점검이 필요합니다."
**출력**:
```json
{{
"content": "⚠️ 이슈: 서버 성능 문제 발생, 긴급 점검 필요",
"confidence": 0.92
}}
```
**입력**: "예산이 부족할 것 같다는 우려가 있습니다."
**출력**:
```json
{{
"content": "⚠️ 이슈: 예산 부족 우려",
"confidence": 0.80
}}
```
### ❌ 나쁜 예시
**입력**: "현재 서버 성능 이슈가 발생했습니다."
**나쁜 출력**:
```json
{{
"content": "현재 서버 성능 이슈가 발생했습니다", 구어체 그대로
"confidence": 0.85
}}
```
**이유**: "⚠️ 이슈: 서버 성능 문제 발생"으로 구조화하고 구어체 제거해야
---
## 💡 5. 아이디어/제안 (Suggestions)
### 패턴 인식
- "제안하는 바는 ~"
- "~하는 것이 좋을 것 같습니다"
- "~을 고려해 볼 필요가 있습니다"
### ✅ 좋은 예시
**입력**: "자동화 테스트를 도입하는 것을 검토해 보면 좋을 것 같습니다."
**출력**:
```json
{{
"content": "💡 제안: 자동화 테스트 도입 검토",
"confidence": 0.85
}}
```
---
## 📊 6. 진행 상황/보고 (Progress)
### 패턴 인식
- "~까지 완료했습니다"
- "현재 ~% 진행 중입니다"
- "~단계까지 진행됐습니다"
### ✅ 좋은 예시
**입력**: "현재 설계 단계는 80% 완료됐고, 다음 주부터 개발 착수 가능합니다."
**출력**:
```json
{{
"content": "📊 진행상황: 설계 80% 완료, 다음 주 개발 착수 예정",
"confidence": 0.90
}}
```
---
## ❌ 제외해야 할 내용 (반드시 제외)
### 인사말
**입력**: "안녕하세요, 여러분. 회의 시작하겠습니다."
**출력**: (메모 없음 - 인사말은 제외)
### 단순 반복
**입력**: "녹음을 시작합니다. 녹음을 시작합니다."
**출력**: (메모 없음 - 형식적 발언 제외)
### 추임새/불필요한 발언
**입력**: "음, 그러니까, 네 네, 저기요..."
**출력**: (메모 없음 - 추임새 제외)
### 형식적 마무리
**입력**: "수고하셨습니다. 회의를 마치겠습니다."
**출력**: (메모 없음 - 형식적 마무리 제외)
---
# 실전 회의 시뮬레이션
## 예시 1: 프로젝트 킥오프 회의
**입력**:
"안녕하세요. 오늘 회의 안건은 신규 프로젝트 킥오프입니다. 프로젝트명은 HGZero이고, 목표는 회의록 자동화입니다. 개발팀에서 다음 주 월요일까지 기술 스택을 검토해 주세요. 예산은 5천만원으로 확정됐습니다."
**출력**:
```json
{{
"suggestions": [
{{
"content": "📋 회의 안건: 신규 프로젝트(HGZero) 킥오프 - 회의록 자동화",
"confidence": 0.95
}},
{{
"content": "🎯 개발팀: 기술 스택 검토 (기한: 다음 주 월요일)",
"confidence": 0.93
}},
{{
"content": "✅ 결정사항: 프로젝트 예산 5천만원 확정",
"confidence": 0.95
}}
]
}}
```
---
## 예시 2: 이슈 대응 회의
**입력**:
"현재 프로덕션 서버에서 성능 저하가 발생하고 있습니다. 인프라팀에서 긴급 점검을 진행하기로 했고, 오늘 오후 3시까지 원인 파악하겠습니다. 고객사에는 임시로 사과 공지를 게시하기로 결정했습니다."
**출력**:
```json
{{
"suggestions": [
{{
"content": "⚠️ 이슈: 프로덕션 서버 성능 저하 발생",
"confidence": 0.95
}},
{{
"content": "🎯 인프라팀: 긴급 점검 및 원인 파악 (기한: 오늘 오후 3시)",
"confidence": 0.93
}},
{{
"content": "✅ 결정사항: 고객사 사과 공지 게시",
"confidence": 0.90
}}
]
}}
```
---
## 예시 3: 일반 업무 회의 (나쁜 예시 포함)
**입력**:
"안녕하세요, 안녕하세요. 녹음을 시작합니다. 음, 그러니까 마케팅 캠페인을 다음 달에 진행하기로 했습니다. 김 과장님이 기획안을 이번 주까지 작성해 주세요. 감사합니다."
** 나쁜 출력**:
```json
{{
"suggestions": [
{{
"content": "안녕하세요", 인사말 포함
"confidence": 0.50
}},
{{
"content": "녹음을 시작합니다", 형식적 발언
"confidence": 0.60
}},
{{
"content": "마케팅 캠페인을 다음 달에 진행하기로 했습니다", 구어체 그대로
"confidence": 0.80
}}
]
}}
```
** 좋은 출력**:
```json
{{
"suggestions": [
{{
"content": "✅ 결정사항: 마케팅 캠페인 다음 달 진행",
"confidence": 0.92
}},
{{
"content": "🎯 김 과장: 캠페인 기획안 작성 (기한: 이번 주)",
"confidence": 0.93
}}
]
}}
```
--- ---
# 출력 형식 # 출력 형식
반드시 아래 JSON 형식으로만 응답하세요: 반드시 아래 JSON 형식으로만 응답하세요:
```json ```json
{{ {{
"suggestions": [ "suggestions": [
{{ {{
"content": "📋/✅/🎯/⚠️/💡/📊 분류: 구체적인 내용 (담당자/기한 포함)", "content": "제안사항 내용 (구체적이고 실행 가능하게, 50자 이상 작성)",
"confidence": 0.85 "confidence": 0.85 ( 제안사항의 중요도/확실성, 0.7-1.0 사이)
}},
{{
"content": "또 다른 제안사항",
"confidence": 0.92
}} }}
] ]
}} }}
``` ```
--- # 중요 규칙
1. **회의 내용에 명시된 사항만** 추출 (추측하지 않기)
2. **최소 3, 최대 7** 제안사항 추출
3. 중요도가 높은 순서로 정렬
4. confidence는 **0.7 이상** 포함
5. 제안사항은 **50 이상** 구체적으로 작성
6. JSON만 출력 (```json이나 다른 텍스트 포함 금지)
# 최종 작성 규칙 이제 회의 내용에서 제안사항을 JSON 형식으로 추출해주세요."""
## ✅ 반드시 지켜야 할 규칙
1. **이모지 분류 필수**
- 📋 회의 안건
- 결정사항
- 🎯 액션 아이템
- 이슈/문제점
- 💡 제안/아이디어
- 📊 진행상황
2. **구조화 필수**
- 담당자가 있으면 반드시 명시
- 기한이 있으면 반드시 포함
- 형식: "담당자: 업무 내용 (기한: XX)"
3. **구어체 종결어미 제거**
- "~입니다", "~했습니다", "~해요", "~합니다"
- 명사형 종결: "~ 진행", "~ 완료", "~ 확정", "~ 검토"
4. **반드시 제외**
- 인사말 ("안녕하세요", "감사합니다", "수고하셨습니다")
- 반복/추임새 ("네 네", "음 음", "그러니까", "저기")
- 형식적 발언 ("녹음 시작", "회의 종료", "회의 시작")
5. **길이**
- 20-70 (너무 짧거나 길지 않게)
6. **confidence 기준**
- 0.90-1.0: 명확한 결정사항, 기한 포함
- 0.80-0.89: 일반적인 액션 아이템
- 0.70-0.79: 암묵적이거나 추측 필요
7. **출력**
- JSON만 출력 (주석, 설명, ```json 모두 금지)
- 최소 1 이상 추출 (의미 있는 내용이 없으면 배열)
---
이제 회의 내용을 분석하여 **회의록 메모** JSON 형식으로 작성하세요.
학습한 패턴을 활용하여 회의 안건, 결정사항, 액션 아이템, 이슈 등을 자동으로 분류하고 구조화하세요.
반드시 구어체 종결어미(~, ~, ~습니다) 제거하고 명사형으로 정리하세요."""
return system_prompt, user_prompt return system_prompt, user_prompt

View File

@ -1,80 +0,0 @@
"""요약 생성용 프롬프트"""
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

View File

@ -133,76 +133,6 @@ class ClaudeService:
logger.error(f"제안사항 분석 실패: {e}", exc_info=True) logger.error(f"제안사항 분석 실패: {e}", exc_info=True)
# 빈 응답 반환 # 빈 응답 반환
return RealtimeSuggestionsResponse(suggestions=[]) 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
# 싱글톤 인스턴스 # 싱글톤 인스턴스

View File

@ -2,7 +2,6 @@
import asyncio import asyncio
import logging import logging
import json import json
from datetime import datetime
from azure.eventhub.aio import EventHubConsumerClient from azure.eventhub.aio import EventHubConsumerClient
from app.config import get_settings from app.config import get_settings
@ -64,30 +63,12 @@ class EventHubService:
} }
""" """
try: try:
# 이벤트 원본 데이터 추출 # 이벤트 원본 데이터 로깅
try: raw_body = event.body_as_str()
# Event Hub 데이터는 bytes 또는 str일 수 있음 logger.info(f"수신한 이벤트 원본 (처음 300자): {raw_body[:300]}")
if hasattr(event, 'body_as_str'):
raw_body = event.body_as_str()
elif hasattr(event, 'body'):
raw_body = event.body.decode('utf-8') if isinstance(event.body, bytes) else str(event.body)
else:
logger.error(f"이벤트 타입 미지원: {type(event)}")
return
logger.info(f"수신한 이벤트 원본 (처음 300자): {raw_body[:300]}")
logger.debug(f"이벤트 전체 길이: {len(raw_body)}")
except Exception as extract_error:
logger.error(f"이벤트 데이터 추출 실패: {extract_error}", exc_info=True)
return
# 이벤트 데이터 파싱 # 이벤트 데이터 파싱
try: event_data = json.loads(raw_body)
event_data = json.loads(raw_body)
except json.JSONDecodeError as json_error:
logger.error(f"JSON 파싱 실패 - 전체 데이터: {raw_body}")
logger.error(f"파싱 에러: {json_error}")
return
event_type = event_data.get("eventType") event_type = event_data.get("eventType")
meeting_id = event_data.get("meetingId") meeting_id = event_data.get("meetingId")
@ -97,6 +78,7 @@ class EventHubService:
# timestamp 변환: LocalDateTime 배열 → Unix timestamp (ms) # timestamp 변환: LocalDateTime 배열 → Unix timestamp (ms)
# Java LocalDateTime은 [year, month, day, hour, minute, second, nano] 형식 # Java LocalDateTime은 [year, month, day, hour, minute, second, nano] 형식
if isinstance(timestamp_raw, list) and len(timestamp_raw) >= 3: if isinstance(timestamp_raw, list) and len(timestamp_raw) >= 3:
from datetime import datetime
year, month, day = timestamp_raw[0:3] year, month, day = timestamp_raw[0:3]
hour = timestamp_raw[3] if len(timestamp_raw) > 3 else 0 hour = timestamp_raw[3] if len(timestamp_raw) > 3 else 0
minute = timestamp_raw[4] if len(timestamp_raw) > 4 else 0 minute = timestamp_raw[4] if len(timestamp_raw) > 4 else 0

View File

@ -105,34 +105,6 @@ class RedisService:
count = await self.redis_client.zcard(key) count = await self.redis_client.zcard(key)
return count if count else 0 return count if count else 0
async def add_generated_suggestion(self, meeting_id: str, suggestion_content: str):
"""
생성된 제안사항 저장 (중복 방지용)
Args:
meeting_id: 회의 ID
suggestion_content: 제안사항 내용
"""
key = f"meeting:{meeting_id}:suggestions"
await self.redis_client.sadd(key, suggestion_content)
# TTL 설정 (1시간)
await self.redis_client.expire(key, 3600)
logger.debug(f"제안사항 저장 - meetingId: {meeting_id}")
async def get_generated_suggestions(self, meeting_id: str) -> set:
"""
이미 생성된 제안사항 목록 조회
Args:
meeting_id: 회의 ID
Returns:
제안사항 set
"""
key = f"meeting:{meeting_id}:suggestions"
suggestions = await self.redis_client.smembers(key)
return suggestions if suggestions else set()
async def cleanup_meeting_data(self, meeting_id: str): async def cleanup_meeting_data(self, meeting_id: str):
""" """
회의 종료 데이터 정리 회의 종료 데이터 정리
@ -140,10 +112,6 @@ class RedisService:
Args: Args:
meeting_id: 회의 ID meeting_id: 회의 ID
""" """
transcript_key = f"meeting:{meeting_id}:transcript" key = f"meeting:{meeting_id}:transcript"
suggestions_key = f"meeting:{meeting_id}:suggestions" await self.redis_client.delete(key)
await self.redis_client.delete(transcript_key)
await self.redis_client.delete(suggestions_key)
logger.info(f"회의 데이터 정리 완료 - meetingId: {meeting_id}") logger.info(f"회의 데이터 정리 완료 - meetingId: {meeting_id}")

View File

@ -96,7 +96,6 @@ class TranscriptService:
agenda_title=agenda_data.get("agenda_title", ""), agenda_title=agenda_data.get("agenda_title", ""),
summary_short=agenda_data.get("summary_short", ""), summary_short=agenda_data.get("summary_short", ""),
summary=agenda_data.get("summary", ""), summary=agenda_data.get("summary", ""),
decisions=agenda_data.get("decisions", []),
pending=agenda_data.get("pending", []), pending=agenda_data.get("pending", []),
todos=todos todos=todos
) )

View File

@ -28,24 +28,13 @@ app = FastAPI(
openapi_url="/api/openapi.json" openapi_url="/api/openapi.json"
) )
# CORS 미들웨어 설정 (SSE 지원) # CORS 미들웨어 설정
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], # 개발 환경에서는 모든 origin 허용 allow_origins=settings.cors_origins,
allow_credentials=True, allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"], allow_methods=["*"],
allow_headers=[ allow_headers=["*"],
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"Cache-Control",
"X-Accel-Buffering"
],
expose_headers=["*"],
) )
# API 라우터 등록 # API 라우터 등록

View File

@ -9,12 +9,3 @@ anthropic==0.39.0
# Utilities # Utilities
python-dotenv==1.0.1 python-dotenv==1.0.1
sse-starlette==1.8.2 # fastapi 0.115.0과 호환되는 버전
httpx==0.26.0 # anthropic 0.39.0과 호환되는 버전
# Redis
redis==5.2.1
# Azure Event Hub
azure-eventhub==5.15.0

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# AI Python 서비스 재시작 스크립트 # AI Python 서비스 재시작 스크립트
# 8087 포트로 깔끔하게 재시작 # 8086 포트로 깔끔하게 재시작
echo "==================================" echo "=================================="
echo "AI Python 서비스 재시작" echo "AI Python 서비스 재시작"
@ -18,23 +18,23 @@ sleep 2
# 2. 포트 확인 # 2. 포트 확인
echo "2⃣ 포트 상태 확인..." echo "2⃣ 포트 상태 확인..."
if lsof -i:8087 > /dev/null 2>&1; then if lsof -i:8086 > /dev/null 2>&1; then
echo " ⚠️ 8087 포트가 아직 사용 중입니다." echo " ⚠️ 8086 포트가 아직 사용 중입니다."
echo " 강제 종료 시도..." echo " 강제 종료 시도..."
PID=$(lsof -ti:8087) PID=$(lsof -ti:8086)
if [ ! -z "$PID" ]; then if [ ! -z "$PID" ]; then
kill -9 $PID kill -9 $PID
sleep 2 sleep 2
fi fi
fi fi
if lsof -i:8087 > /dev/null 2>&1; then if lsof -i:8086 > /dev/null 2>&1; then
echo " ❌ 8087 포트를 해제할 수 없습니다." echo " ❌ 8086 포트를 해제할 수 없습니다."
echo " 시스템 재부팅 후 다시 시도하거나," echo " 시스템 재부팅 후 다시 시도하거나,"
echo " 다른 포트를 사용하세요." echo " 다른 포트를 사용하세요."
exit 1 exit 1
else else
echo " ✅ 8087 포트 사용 가능" echo " ✅ 8086 포트 사용 가능"
fi fi
# 3. 가상환경 활성화 # 3. 가상환경 활성화
@ -51,7 +51,7 @@ echo " ✅ 가상환경 활성화 완료"
mkdir -p ../logs mkdir -p ../logs
# 5. 서비스 시작 # 5. 서비스 시작
echo "4⃣ AI Python 서비스 시작 (포트: 8087)..." echo "4⃣ AI Python 서비스 시작 (포트: 8086)..."
nohup python3 main.py > ../logs/ai-python.log 2>&1 & nohup python3 main.py > ../logs/ai-python.log 2>&1 &
PID=$! PID=$!
@ -76,16 +76,16 @@ else
fi fi
# 포트 확인 # 포트 확인
if lsof -i:8087 > /dev/null 2>&1; then if lsof -i:8086 > /dev/null 2>&1; then
echo " ✅ 8087 포트 리스닝 중" echo " ✅ 8086 포트 리스닝 중"
else else
echo " ⚠️ 8087 포트 아직 준비 중..." echo " ⚠️ 8086 포트 아직 준비 중..."
fi fi
# Health 체크 # Health 체크
echo "7⃣ Health Check..." echo "7⃣ Health Check..."
sleep 2 sleep 2
HEALTH=$(curl -s http://localhost:8087/health 2>/dev/null) HEALTH=$(curl -s http://localhost:8086/health 2>/dev/null)
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo " ✅ Health Check 성공" echo " ✅ Health Check 성공"
@ -103,13 +103,13 @@ echo "✅ AI Python 서비스 시작 완료"
echo "==================================" echo "=================================="
echo "📊 서비스 정보:" echo "📊 서비스 정보:"
echo " - PID: $PID" echo " - PID: $PID"
echo " - 포트: 8087" echo " - 포트: 8086"
echo " - 로그: tail -f ../logs/ai-python.log" echo " - 로그: tail -f ../logs/ai-python.log"
echo "" echo ""
echo "📡 엔드포인트:" echo "📡 엔드포인트:"
echo " - Health: http://localhost:8087/health" echo " - Health: http://localhost:8086/health"
echo " - Root: http://localhost:8087/" echo " - Root: http://localhost:8086/"
echo " - Swagger: http://localhost:8087/swagger-ui.html" echo " - Swagger: http://localhost:8086/swagger-ui.html"
echo "" echo ""
echo "🛑 서비스 중지: pkill -f 'python.*main.py'" echo "🛑 서비스 중지: pkill -f 'python.*main.py'"
echo "==================================" echo "=================================="

113
ai/.run/ai-service.run.xml Normal file
View File

@ -0,0 +1,113 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ai-service" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<!-- Database Configuration -->
<entry key="DB_KIND" value="postgresql" />
<entry key="DB_HOST" value="20.249.153.213" />
<entry key="DB_PORT" value="5432" />
<entry key="DB_NAME" value="aidb" />
<entry key="DB_USERNAME" value="hgzerouser" />
<entry key="DB_PASSWORD" value="Hi5Jessica!" />
<!-- JPA Configuration -->
<entry key="SHOW_SQL" value="true" />
<entry key="DDL_AUTO" value="update" />
<!-- Redis Configuration -->
<entry key="REDIS_HOST" value="20.249.177.114" />
<entry key="REDIS_PORT" value="6379" />
<entry key="REDIS_PASSWORD" value="Hi5Jessica!" />
<entry key="REDIS_DATABASE" value="4" />
<!-- Server Configuration -->
<entry key="SERVER_PORT" value="8083" />
<entry key="CONTEXT_PATH" value="" />
<!-- JWT Configuration -->
<entry key="JWT_SECRET" value="dev-jwt-secret-key-for-development-only" />
<entry key="JWT_ACCESS_TOKEN_VALIDITY" value="1800" />
<entry key="JWT_REFRESH_TOKEN_VALIDITY" value="86400" />
<!-- CORS Configuration -->
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:*" />
<!-- External AI API Configuration -->
<entry key="CLAUDE_API_KEY" value="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" />
<entry key="CLAUDE_BASE_URL" value="https://api.anthropic.com" />
<entry key="OPENAI_API_KEY" value="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" />
<entry key="OPENAI_BASE_URL" value="https://api.openai.com" />
<entry key="OPENWEATHER_API_KEY" value="1aa5bfca079a20586915b56f29235cc0" />
<entry key="OPENWEATHER_BASE_URL" value="https://api.openweathermap.org" />
<entry key="KAKAO_API_KEY" value="094feac895a3e4a6d7ffa66d877bf48f" />
<entry key="KAKAO_BASE_URL" value="https://dapi.kakao.com" />
<!-- Azure OpenAI Configuration -->
<entry key="AZURE_OPENAI_API_KEY" value="" />
<entry key="AZURE_OPENAI_ENDPOINT" value="" />
<entry key="AZURE_OPENAI_DEPLOYMENT" value="gpt-4o" />
<entry key="AZURE_OPENAI_EMBEDDING_DEPLOYMENT" value="text-embedding-3-large" />
<entry key="AZURE_OPENAI_MAX_TOKENS" value="2000" />
<entry key="AZURE_OPENAI_TEMPERATURE" value="0.3" />
<!-- Azure AI Search Configuration -->
<entry key="AZURE_AI_SEARCH_ENDPOINT" value="" />
<entry key="AZURE_AI_SEARCH_API_KEY" value="" />
<entry key="AZURE_AI_SEARCH_INDEX" value="meeting-transcripts" />
<!-- Azure Event Hubs Configuration -->
<entry key="AZURE_EVENTHUB_CONNECTION_STRING" value="Endpoint=sb://hgzero-eventhub-ns.servicebus.windows.net/;SharedAccessKeyName=ai-listen-policy;SharedAccessKey=wqcbVIXlOMyn/C562lx6DD75AyjHQ87xo+AEhJ7js9Q=;EntityPath=hgzero-eventhub-name" />
<entry key="AZURE_EVENTHUB_NAMESPACE" value="hgzero-eventhub-ns" />
<entry key="AZURE_EVENTHUB_NAME" value="hgzero-eventhub-name" />
<entry key="AZURE_CHECKPOINT_STORAGE_CONNECTION_STRING" value="" />
<entry key="AZURE_CHECKPOINT_CONTAINER" value="hgzero-checkpoints" />
<entry key="AZURE_EVENTHUB_CONSUMER_GROUP_TRANSCRIPT" value="ai-transcript-group" />
<entry key="AZURE_EVENTHUB_CONSUMER_GROUP_MEETING" value="ai-meeting-group" />
<!-- Logging Configuration -->
<entry key="LOG_LEVEL_ROOT" value="INFO" />
<entry key="LOG_LEVEL_APP" value="DEBUG" />
<entry key="LOG_LEVEL_WEB" value="INFO" />
<entry key="LOG_LEVEL_SECURITY" value="DEBUG" />
<entry key="LOG_LEVEL_SQL" value="DEBUG" />
<entry key="LOG_LEVEL_SQL_TYPE" value="TRACE" />
<entry key="LOG_FILE" value="logs/ai-service.log" />
<entry key="LOG_MAX_FILE_SIZE" value="10MB" />
<entry key="LOG_MAX_HISTORY" value="7" />
<entry key="LOG_TOTAL_SIZE_CAP" value="100MB" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":ai:bootRun" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
</ENTRIES>
</extension>
</EXTENSION>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More