mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-07 08:26:24 +00:00
Compare commits
No commits in common. "52b32cf978f659114338cd91d94a93e30add3daf" and "ed9fa6f934265a7fdf15002662adcf2e2901803d" have entirely different histories.
52b32cf978
...
ed9fa6f934
3
.github/config/deploy_env_vars_ai-python_dev
vendored
3
.github/config/deploy_env_vars_ai-python_dev
vendored
@ -1,3 +0,0 @@
|
||||
# Development environment variables for ai-python service
|
||||
resource_group=rg-digitalgarage-02
|
||||
cluster_name=aks-digitalgarage-02
|
||||
7
.github/config/deploy_env_vars_rag_dev
vendored
7
.github/config/deploy_env_vars_rag_dev
vendored
@ -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
|
||||
7
.github/config/deploy_env_vars_rag_prod
vendored
7
.github/config/deploy_env_vars_rag_prod
vendored
@ -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
|
||||
7
.github/config/deploy_env_vars_rag_staging
vendored
7
.github/config/deploy_env_vars_rag_staging
vendored
@ -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
|
||||
7
.github/kustomize/base/common/ingress.yaml
vendored
7
.github/kustomize/base/common/ingress.yaml
vendored
@ -32,13 +32,6 @@ spec:
|
||||
name: stt
|
||||
port:
|
||||
number: 8080
|
||||
- path: /api/transcripts
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ai-service
|
||||
port:
|
||||
number: 8087
|
||||
- path: /api/ai/suggestions
|
||||
pathType: Prefix
|
||||
backend:
|
||||
|
||||
221
.github/workflows/ai-python-cicd_ArgoCD.yaml
vendored
221
.github/workflows/ai-python-cicd_ArgoCD.yaml
vendored
@ -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가 자동으로 배포합니다."
|
||||
7
.github/workflows/backend-cicd_ArgoCD.yaml
vendored
7
.github/workflows/backend-cicd_ArgoCD.yaml
vendored
@ -2,7 +2,7 @@ name: Backend Services CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, develop ]
|
||||
paths:
|
||||
- 'user/**'
|
||||
- 'meeting/**'
|
||||
@ -11,8 +11,8 @@ on:
|
||||
- 'notification/**'
|
||||
- 'common/**'
|
||||
- '.github/**'
|
||||
# pull_request:
|
||||
# branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ENVIRONMENT:
|
||||
@ -143,7 +143,6 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-builds
|
||||
retention-days: 1
|
||||
path: |
|
||||
user/build/libs/*.jar
|
||||
meeting/build/libs/*.jar
|
||||
|
||||
214
.github/workflows/rag-cicd_ArgoCD.yaml
vendored
214
.github/workflows/rag-cicd_ArgoCD.yaml
vendored
@ -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가 자동으로 배포합니다."
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -58,7 +58,3 @@ logs/
|
||||
**/logs/
|
||||
*.log
|
||||
**/*.log
|
||||
|
||||
# Deprecated/Backup directories
|
||||
ai-java-back/
|
||||
ai/
|
||||
|
||||
215
Jenkinsfile
vendored
215
Jenkinsfile
vendored
@ -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
376
README.md
@ -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
|
||||
@ -248,53 +248,3 @@ A: 네, 각 클라이언트는 독립적으로 SSE 연결을 유지합니다.
|
||||
|
||||
**Q: 제안사항이 오지 않으면?**
|
||||
A: Redis에 충분한 텍스트(10개 세그먼트)가 축적되어야 분석이 시작됩니다. 5초마다 체크합니다.
|
||||
|
||||
### 3. AI 텍스트 요약 생성
|
||||
|
||||
**엔드포인트**: `POST /api/v1/ai/summary/generate`
|
||||
|
||||
**설명**: 텍스트를 AI로 요약하여 핵심 내용과 포인트를 추출합니다.
|
||||
|
||||
**요청 본문**:
|
||||
```json
|
||||
{
|
||||
"text": "요약할 텍스트 내용",
|
||||
"language": "ko", // ko: 한국어, en: 영어 (기본값: ko)
|
||||
"style": "bullet", // bullet: 불릿포인트, paragraph: 단락형 (기본값: bullet)
|
||||
"max_length": 100 // 최대 요약 길이 (단어 수) - 선택사항
|
||||
}
|
||||
```
|
||||
|
||||
**응답 예시**:
|
||||
```json
|
||||
{
|
||||
"summary": "• 프로젝트 총 개발 기간 3개월 확정 (디자인 2주, 개발 8주, 테스트 2주)\n• 총 예산 5천만원 배정 (인건비 3천만원, 인프라 1천만원, 기타 1천만원)\n• 주간 회의 일정: 매주 화요일 오전 10시",
|
||||
"key_points": [
|
||||
"프로젝트 전체 일정 3개월로 확정",
|
||||
"개발 단계별 기간: 디자인 2주, 개발 8주, 테스트 2주",
|
||||
"총 예산 5천만원 책정",
|
||||
"예산 배분: 인건비 60%, 인프라 20%, 기타 20%",
|
||||
"정기 회의: 매주 화요일 오전 10시"
|
||||
],
|
||||
"word_count": 32,
|
||||
"original_word_count": 46,
|
||||
"compression_ratio": 0.7,
|
||||
"generated_at": "2025-10-29T17:23:49.429982"
|
||||
}
|
||||
```
|
||||
|
||||
**요청 예시 (curl)**:
|
||||
```bash
|
||||
curl -X POST "http://localhost: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 처리 중 오류 발생
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,11 +2,9 @@
|
||||
from fastapi import APIRouter
|
||||
from .transcripts import router as transcripts_router
|
||||
from .suggestions import router as suggestions_router
|
||||
from .summary import router as summary_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 라우터 등록
|
||||
router.include_router(transcripts_router, prefix="/transcripts", tags=["Transcripts"])
|
||||
router.include_router(suggestions_router, prefix="/ai/suggestions", tags=["AI Suggestions"])
|
||||
router.include_router(summary_router, prefix="/ai/summary", tags=["AI Summary"])
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -26,7 +26,7 @@ claude_service = ClaudeService()
|
||||
|
||||
### 동작 방식
|
||||
1. Redis에서 누적된 회의 텍스트 조회 (5초마다)
|
||||
2. 임계값(4개 세그먼트, 약 60초) 이상이면 Claude API로 분석
|
||||
2. 임계값(10개 세그먼트) 이상이면 Claude API로 분석
|
||||
3. 분석 결과를 SSE 이벤트로 전송
|
||||
|
||||
### SSE 이벤트 형식
|
||||
@ -116,45 +116,21 @@ async def stream_ai_suggestions(meeting_id: str):
|
||||
if accumulated_text:
|
||||
logger.info(f"텍스트 누적 완료 - meetingId: {meeting_id}, 길이: {len(accumulated_text)}")
|
||||
|
||||
# 이미 생성된 제안사항 조회
|
||||
existing_suggestions = await redis_service.get_generated_suggestions(meeting_id)
|
||||
|
||||
# Claude API로 분석
|
||||
suggestions = await claude_service.analyze_suggestions(accumulated_text)
|
||||
|
||||
if suggestions.suggestions:
|
||||
# 중복 제거: 새로운 제안사항만 필터링
|
||||
new_suggestions = [
|
||||
s for s in suggestions.suggestions
|
||||
if s.content not in existing_suggestions
|
||||
]
|
||||
# SSE 이벤트 전송
|
||||
yield {
|
||||
"event": "ai-suggestion",
|
||||
"id": str(current_count),
|
||||
"data": suggestions.json()
|
||||
}
|
||||
|
||||
if new_suggestions:
|
||||
# 새로운 제안사항만 SSE 이벤트 전송
|
||||
from app.models import RealtimeSuggestionsResponse
|
||||
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}"
|
||||
)
|
||||
logger.info(
|
||||
f"AI 제안사항 발행 - meetingId: {meeting_id}, "
|
||||
f"개수: {len(suggestions.suggestions)}"
|
||||
)
|
||||
|
||||
previous_count = current_count
|
||||
|
||||
@ -184,6 +160,8 @@ async def stream_ai_suggestions(meeting_id: str):
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
"Access-Control-Allow-Origin": "http://localhost:8888",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -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)}"
|
||||
)
|
||||
@ -10,12 +10,12 @@ class Settings(BaseSettings):
|
||||
# 서버 설정
|
||||
app_name: str = "AI Service (Python)"
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8087
|
||||
port: int = 8087 # feature/stt-ai 브랜치 AI Service(8086)와 충돌 방지
|
||||
|
||||
# Claude API
|
||||
claude_api_key: str = "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"
|
||||
claude_model: str = "claude-sonnet-4-5-20250929"
|
||||
claude_max_tokens: int = 4096
|
||||
claude_model: str = "claude-3-5-sonnet-20240620"
|
||||
claude_max_tokens: int = 250000
|
||||
claude_temperature: float = 0.7
|
||||
|
||||
# Redis
|
||||
@ -36,15 +36,14 @@ class Settings(BaseSettings):
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:8888",
|
||||
"http://127.0.0.1:8080",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:*" # 모든 localhost 포트 허용
|
||||
"http://127.0.0.1:3000"
|
||||
]
|
||||
|
||||
# 로깅
|
||||
log_level: str = "INFO"
|
||||
|
||||
# 분석 임계값 (실시간 응답을 위해 낮춤)
|
||||
min_segments_for_analysis: int = 2 # 2개 세그먼트 (약 30초, 빠른 피드백)
|
||||
# 분석 임계값 (MVP 수준)
|
||||
min_segments_for_analysis: int = 3 # 3개 세그먼트 = 약 15-30초 분량의 대화
|
||||
text_retention_seconds: int = 300 # 5분
|
||||
|
||||
class Config:
|
||||
|
||||
@ -10,10 +10,6 @@ from .response import (
|
||||
SimpleSuggestion,
|
||||
RealtimeSuggestionsResponse
|
||||
)
|
||||
from .summary import (
|
||||
SummaryRequest,
|
||||
SummaryResponse
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ConsolidateRequest",
|
||||
@ -23,6 +19,4 @@ __all__ = [
|
||||
"ExtractedTodo",
|
||||
"SimpleSuggestion",
|
||||
"RealtimeSuggestionsResponse",
|
||||
"SummaryRequest",
|
||||
"SummaryResponse",
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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"
|
||||
}
|
||||
}
|
||||
@ -20,9 +20,8 @@ class ConsolidateRequest(BaseModel):
|
||||
|
||||
|
||||
class ExtractedTodo(BaseModel):
|
||||
"""추출된 Todo"""
|
||||
"""추출된 Todo (제목만)"""
|
||||
title: str = Field(..., description="Todo 제목")
|
||||
assignee: str = Field(default="", description="담당자 이름 (있는 경우에만)")
|
||||
|
||||
|
||||
class AgendaSummary(BaseModel):
|
||||
@ -31,7 +30,6 @@ class AgendaSummary(BaseModel):
|
||||
agenda_title: str = Field(..., description="안건 제목")
|
||||
summary_short: str = Field(..., description="AI 생성 짧은 요약 (1줄, 20자 이내)")
|
||||
summary: str = Field(..., description="안건별 회의록 요약 (논의사항+결정사항, 사용자 수정 가능)")
|
||||
decisions: List[str] = Field(default_factory=list, description="안건별 결정사항 배열 (대시보드 표시용)")
|
||||
pending: List[str] = Field(default_factory=list, description="보류 사항")
|
||||
todos: List[ExtractedTodo] = Field(default_factory=list, description="Todo 목록 (제목만)")
|
||||
|
||||
|
||||
@ -49,23 +49,13 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
|
||||
- **agenda_number**: 안건 번호 (1, 2, 3...)
|
||||
- **agenda_title**: 안건 제목 (간결하게)
|
||||
- **summary_short**: AI가 생성한 1줄 요약 (20자 이내, 사용자 수정 불가)
|
||||
- **summary**: 안건별 회의록 요약 (논의사항과 결정사항 모두 포함)
|
||||
- **summary**: 안건별 회의록 요약 (논의사항과 결정사항을 포함한 전체 요약)
|
||||
* 회의록 수정 페이지에서 사용자가 수정할 수 있는 입력 필드
|
||||
* 형식: "**논의 사항:**\n- 논의내용1\n- 논의내용2\n\n**결정 사항:**\n- 결정1\n- 결정2"
|
||||
* 사용자가 자유롭게 편집할 수 있도록 구조화된 텍스트로 작성
|
||||
- **decisions**: 안건별 결정사항 배열 (대시보드 표시용, summary의 결정사항 부분을 배열로 추출)
|
||||
* 형식: ["결정사항1", "결정사항2", "결정사항3"]
|
||||
* 회의에서 최종 결정된 사항만 포함
|
||||
- **pending**: 보류 사항 배열 (추가 논의 필요 사항)
|
||||
- **todos**: Todo 배열 (제목과 담당자 추출)
|
||||
- title: Todo 제목 (예: "시장 조사 보고서 작성")
|
||||
- assignee: 담당자 이름 (있는 경우에만, 예: "김대리", "박과장")
|
||||
|
||||
**Todo 추출 가이드:**
|
||||
- 자연스러운 표현도 인식: "김대리가 ~하기로 함", "박과장은 ~준비합니다", "이차장님께서 ~하시기로 하셨습니다"
|
||||
- 실행 동사 패턴: ~하기로, ~준비, ~작성, ~제출, ~완료, ~진행, ~검토, ~분석
|
||||
- 담당자 패턴: "OO님", "OO이/가", "OO은/는", "OO께서"
|
||||
- 기한 표현: "다음주", "이번주", "~까지", "~일까지", "~월까지"
|
||||
- **todos**: Todo 배열 (제목만, 담당자/마감일/우선순위 없음)
|
||||
- title: Todo 제목만 추출 (예: "시장 조사 보고서 작성")
|
||||
|
||||
---
|
||||
|
||||
@ -87,16 +77,10 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
|
||||
"agenda_title": "안건 제목",
|
||||
"summary_short": "짧은 요약 (20자 이내)",
|
||||
"summary": "**논의 사항:**\\n- 논의내용1\\n- 논의내용2\\n\\n**결정 사항:**\\n- 결정1\\n- 결정2",
|
||||
"decisions": ["결정사항1", "결정사항2"],
|
||||
"pending": ["보류사항"],
|
||||
"todos": [
|
||||
{{
|
||||
"title": "인플루언서 리스트 작성",
|
||||
"assignee": "김대리"
|
||||
}},
|
||||
{{
|
||||
"title": "캠페인 콘텐츠 기획안 초안 작성",
|
||||
"assignee": "박과장"
|
||||
"title": "Todo 제목"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
@ -113,17 +97,12 @@ def get_consolidate_prompt(participant_minutes: list, agendas: list = None) -> s
|
||||
3. **완전성**: 모든 필드를 빠짐없이 작성
|
||||
4. **구조화**: 안건별로 명확히 분리
|
||||
5. **결정사항 추출**:
|
||||
- 회의 전체 결정사항(decisions): 모든 안건의 결정사항을 포함 (TEXT 형식)
|
||||
- 안건별 결정사항(agenda_summaries[].decisions): 각 안건의 결정사항을 배열로 추출
|
||||
- 결정사항이 명확하게 언급된 경우에만 포함
|
||||
- 회의 전체 결정사항(decisions)은 모든 안건의 결정사항을 포함
|
||||
- 안건별 summary에도 결정사항을 포함하여 사용자가 수정 가능하도록 작성
|
||||
6. **summary 작성**:
|
||||
- summary_short: AI가 자동 생성한 1줄 요약 (사용자 수정 불가)
|
||||
- summary: 논의사항과 결정사항 모두 포함 (사용자 수정 가능)
|
||||
- decisions: summary의 결정사항 부분을 배열로 별도 추출 (대시보드 표시용)
|
||||
7. **Todo 추출**:
|
||||
- 제목 필수, 담당자는 언급된 경우에만 추출
|
||||
- 자연스러운 표현에서 추출: "김대리가 ~하기로 함" → title: "~", assignee: "김대리"
|
||||
- 담당자가 없으면 assignee: "" (빈 문자열)
|
||||
- summary: 논의사항과 결정사항을 포함한 전체 요약 (사용자 수정 가능)
|
||||
7. **Todo 추출**: 제목만 추출 (담당자나 마감일 없어도 됨)
|
||||
8. **JSON만 출력**: 추가 설명 없이 JSON만 반환
|
||||
|
||||
이제 위 회의록들을 분석하여 통합 요약을 JSON 형식으로 생성해주세요.
|
||||
|
||||
@ -1,432 +1,72 @@
|
||||
"""AI 제안사항 추출 프롬프트 (회의록 작성 MVP 최적화)"""
|
||||
"""AI 제안사항 추출 프롬프트"""
|
||||
|
||||
|
||||
def get_suggestions_prompt(transcript_text: str) -> tuple[str, str]:
|
||||
"""
|
||||
회의 텍스트에서 AI 제안사항을 추출하는 프롬프트 생성 (회의록 MVP용)
|
||||
회의 텍스트에서 AI 제안사항을 추출하는 프롬프트 생성
|
||||
|
||||
Returns:
|
||||
(system_prompt, user_prompt) 튜플
|
||||
"""
|
||||
|
||||
system_prompt = """당신은 실시간 회의록 작성 AI 비서입니다.
|
||||
system_prompt = """당신은 회의 내용 분석 전문가입니다.
|
||||
회의 텍스트를 분석하여 실행 가능한 제안사항을 추출해주세요."""
|
||||
|
||||
**핵심 역할**:
|
||||
회의 중 발언되는 내용을 실시간으로 분석하여, 회의록 작성자가 놓칠 수 있는 중요한 정보를 즉시 메모로 제공합니다.
|
||||
|
||||
**작업 방식**:
|
||||
1. 회의 안건, 결정 사항, 이슈, 액션 아이템을 자동으로 분류
|
||||
2. 담당자, 기한, 우선순위 등 구조화된 정보로 정리
|
||||
3. 단순 발언 반복이 아닌, 실무에 바로 사용 가능한 형식으로 요약
|
||||
4. 회의록 작성 시간을 70% 단축시키는 것이 목표
|
||||
|
||||
**핵심 원칙**:
|
||||
- 인사말, 반복, 불필요한 추임새는 완전히 제거
|
||||
- 실제 회의록에 들어갈 내용만 추출
|
||||
- 명확하고 간결하게 (20-50자)
|
||||
- 구어체 종결어미(~다, ~요, ~습니다) 제거하고 명사형으로 정리"""
|
||||
|
||||
user_prompt = f"""다음 회의 대화를 실시간으로 분석하여 **회의록 메모**를 작성하세요.
|
||||
user_prompt = f"""다음 회의 내용을 분석하여 **구체적이고 실행 가능한 제안사항**을 추출해주세요.
|
||||
|
||||
# 회의 내용
|
||||
{transcript_text}
|
||||
|
||||
---
|
||||
|
||||
# 회의록 항목별 패턴 학습
|
||||
# 제안사항 추출 기준
|
||||
1. **실행 가능성**: 바로 실행할 수 있는 구체적인 액션 아이템
|
||||
2. **명확성**: 누가, 무엇을, 언제까지 해야 하는지 명확한 내용
|
||||
3. **중요도**: 회의 목표 달성에 중요한 사항
|
||||
4. **완결성**: 하나의 제안사항이 독립적으로 완결된 내용
|
||||
|
||||
## 📋 1. 회의 안건 (Agenda)
|
||||
# 제안사항 유형 예시
|
||||
- **후속 작업**: "시장 조사 보고서를 다음 주까지 작성하여 공유"
|
||||
- **의사결정 필요**: "예산안 3안 중 최종안을 이번 주 금요일까지 결정"
|
||||
- **리스크 대응**: "법률 검토를 위해 법무팀과 사전 협의 필요"
|
||||
- **일정 조율**: "다음 회의를 3월 15일로 확정하고 참석자에게 공지"
|
||||
- **자료 준비**: "경쟁사 분석 자료를 회의 전까지 준비"
|
||||
- **검토 요청**: "초안에 대한 팀원들의 피드백 수집 필요"
|
||||
- **승인 필요**: "최종 기획안을 경영진에게 보고하여 승인 받기"
|
||||
|
||||
### 패턴 인식
|
||||
- "오늘 회의 안건은 ~"
|
||||
- "논의할 주제는 ~"
|
||||
- "다룰 내용은 ~"
|
||||
- "검토할 사항은 ~"
|
||||
|
||||
### ✅ 좋은 예시
|
||||
**입력**: "오늘 회의 안건은 신제품 출시 일정과 마케팅 전략입니다."
|
||||
**출력**:
|
||||
```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
|
||||
}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
# 제안사항 작성 가이드
|
||||
- **구체적으로**: "검토 필요" (X) → "법무팀과 계약서 조항 검토 미팅 잡기" (O)
|
||||
- **명확하게**: "나중에 하기" (X) → "다음 주 화요일까지 완료" (O)
|
||||
- **실행 가능하게**: "잘 되길 바람" (X) → "주간 진행상황 공유 미팅 설정" (O)
|
||||
|
||||
---
|
||||
|
||||
# 출력 형식
|
||||
|
||||
반드시 아래 JSON 형식으로만 응답하세요:
|
||||
|
||||
```json
|
||||
{{
|
||||
"suggestions": [
|
||||
{{
|
||||
"content": "📋/✅/🎯/⚠️/💡/📊 분류: 구체적인 내용 (담당자/기한 포함)",
|
||||
"confidence": 0.85
|
||||
"content": "제안사항 내용 (구체적이고 실행 가능하게, 50자 이상 작성)",
|
||||
"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이나 다른 텍스트 포함 금지)
|
||||
|
||||
# 최종 작성 규칙
|
||||
|
||||
## ✅ 반드시 지켜야 할 규칙
|
||||
|
||||
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 형식으로 작성하세요.
|
||||
학습한 패턴을 활용하여 회의 안건, 결정사항, 액션 아이템, 이슈 등을 자동으로 분류하고 구조화하세요.
|
||||
반드시 구어체 종결어미(~다, ~요, ~습니다)를 제거하고 명사형으로 정리하세요."""
|
||||
이제 위 회의 내용에서 제안사항을 JSON 형식으로 추출해주세요."""
|
||||
|
||||
return system_prompt, user_prompt
|
||||
|
||||
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -134,76 +134,6 @@ class ClaudeService:
|
||||
# 빈 응답 반환
|
||||
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
|
||||
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
claude_service = ClaudeService()
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime
|
||||
from azure.eventhub.aio import EventHubConsumerClient
|
||||
|
||||
from app.config import get_settings
|
||||
@ -64,30 +63,12 @@ class EventHubService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 이벤트 원본 데이터 추출
|
||||
try:
|
||||
# Event Hub 데이터는 bytes 또는 str일 수 있음
|
||||
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
|
||||
# 이벤트 원본 데이터 로깅
|
||||
raw_body = event.body_as_str()
|
||||
logger.info(f"수신한 이벤트 원본 (처음 300자): {raw_body[:300]}")
|
||||
|
||||
# 이벤트 데이터 파싱
|
||||
try:
|
||||
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_data = json.loads(raw_body)
|
||||
|
||||
event_type = event_data.get("eventType")
|
||||
meeting_id = event_data.get("meetingId")
|
||||
@ -97,6 +78,7 @@ class EventHubService:
|
||||
# timestamp 변환: LocalDateTime 배열 → Unix timestamp (ms)
|
||||
# Java LocalDateTime은 [year, month, day, hour, minute, second, nano] 형식
|
||||
if isinstance(timestamp_raw, list) and len(timestamp_raw) >= 3:
|
||||
from datetime import datetime
|
||||
year, month, day = timestamp_raw[0:3]
|
||||
hour = timestamp_raw[3] if len(timestamp_raw) > 3 else 0
|
||||
minute = timestamp_raw[4] if len(timestamp_raw) > 4 else 0
|
||||
|
||||
@ -105,34 +105,6 @@ class RedisService:
|
||||
count = await self.redis_client.zcard(key)
|
||||
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):
|
||||
"""
|
||||
회의 종료 시 데이터 정리
|
||||
@ -140,10 +112,6 @@ class RedisService:
|
||||
Args:
|
||||
meeting_id: 회의 ID
|
||||
"""
|
||||
transcript_key = f"meeting:{meeting_id}:transcript"
|
||||
suggestions_key = f"meeting:{meeting_id}:suggestions"
|
||||
|
||||
await self.redis_client.delete(transcript_key)
|
||||
await self.redis_client.delete(suggestions_key)
|
||||
|
||||
key = f"meeting:{meeting_id}:transcript"
|
||||
await self.redis_client.delete(key)
|
||||
logger.info(f"회의 데이터 정리 완료 - meetingId: {meeting_id}")
|
||||
|
||||
@ -96,7 +96,6 @@ class TranscriptService:
|
||||
agenda_title=agenda_data.get("agenda_title", ""),
|
||||
summary_short=agenda_data.get("summary_short", ""),
|
||||
summary=agenda_data.get("summary", ""),
|
||||
decisions=agenda_data.get("decisions", []),
|
||||
pending=agenda_data.get("pending", []),
|
||||
todos=todos
|
||||
)
|
||||
|
||||
@ -28,24 +28,13 @@ app = FastAPI(
|
||||
openapi_url="/api/openapi.json"
|
||||
)
|
||||
|
||||
# CORS 미들웨어 설정 (SSE 지원)
|
||||
# CORS 미들웨어 설정
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 개발 환경에서는 모든 origin 허용
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "OPTIONS"],
|
||||
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=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# API 라우터 등록
|
||||
|
||||
@ -9,12 +9,3 @@ anthropic==0.39.0
|
||||
|
||||
# Utilities
|
||||
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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# AI Python 서비스 재시작 스크립트
|
||||
# 8087 포트로 깔끔하게 재시작
|
||||
# 8086 포트로 깔끔하게 재시작
|
||||
|
||||
echo "=================================="
|
||||
echo "AI Python 서비스 재시작"
|
||||
@ -18,23 +18,23 @@ sleep 2
|
||||
|
||||
# 2. 포트 확인
|
||||
echo "2️⃣ 포트 상태 확인..."
|
||||
if lsof -i:8087 > /dev/null 2>&1; then
|
||||
echo " ⚠️ 8087 포트가 아직 사용 중입니다."
|
||||
if lsof -i:8086 > /dev/null 2>&1; then
|
||||
echo " ⚠️ 8086 포트가 아직 사용 중입니다."
|
||||
echo " 강제 종료 시도..."
|
||||
PID=$(lsof -ti:8087)
|
||||
PID=$(lsof -ti:8086)
|
||||
if [ ! -z "$PID" ]; then
|
||||
kill -9 $PID
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if lsof -i:8087 > /dev/null 2>&1; then
|
||||
echo " ❌ 8087 포트를 해제할 수 없습니다."
|
||||
if lsof -i:8086 > /dev/null 2>&1; then
|
||||
echo " ❌ 8086 포트를 해제할 수 없습니다."
|
||||
echo " 시스템 재부팅 후 다시 시도하거나,"
|
||||
echo " 다른 포트를 사용하세요."
|
||||
exit 1
|
||||
else
|
||||
echo " ✅ 8087 포트 사용 가능"
|
||||
echo " ✅ 8086 포트 사용 가능"
|
||||
fi
|
||||
|
||||
# 3. 가상환경 활성화
|
||||
@ -51,7 +51,7 @@ echo " ✅ 가상환경 활성화 완료"
|
||||
mkdir -p ../logs
|
||||
|
||||
# 5. 서비스 시작
|
||||
echo "4️⃣ AI Python 서비스 시작 (포트: 8087)..."
|
||||
echo "4️⃣ AI Python 서비스 시작 (포트: 8086)..."
|
||||
nohup python3 main.py > ../logs/ai-python.log 2>&1 &
|
||||
PID=$!
|
||||
|
||||
@ -76,16 +76,16 @@ else
|
||||
fi
|
||||
|
||||
# 포트 확인
|
||||
if lsof -i:8087 > /dev/null 2>&1; then
|
||||
echo " ✅ 8087 포트 리스닝 중"
|
||||
if lsof -i:8086 > /dev/null 2>&1; then
|
||||
echo " ✅ 8086 포트 리스닝 중"
|
||||
else
|
||||
echo " ⚠️ 8087 포트 아직 준비 중..."
|
||||
echo " ⚠️ 8086 포트 아직 준비 중..."
|
||||
fi
|
||||
|
||||
# Health 체크
|
||||
echo "7️⃣ Health Check..."
|
||||
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
|
||||
echo " ✅ Health Check 성공"
|
||||
@ -103,13 +103,13 @@ echo "✅ AI Python 서비스 시작 완료"
|
||||
echo "=================================="
|
||||
echo "📊 서비스 정보:"
|
||||
echo " - PID: $PID"
|
||||
echo " - 포트: 8087"
|
||||
echo " - 포트: 8086"
|
||||
echo " - 로그: tail -f ../logs/ai-python.log"
|
||||
echo ""
|
||||
echo "📡 엔드포인트:"
|
||||
echo " - Health: http://localhost:8087/health"
|
||||
echo " - Root: http://localhost:8087/"
|
||||
echo " - Swagger: http://localhost:8087/swagger-ui.html"
|
||||
echo " - Health: http://localhost:8086/health"
|
||||
echo " - Root: http://localhost:8086/"
|
||||
echo " - Swagger: http://localhost:8086/swagger-ui.html"
|
||||
echo ""
|
||||
echo "🛑 서비스 중지: pkill -f 'python.*main.py'"
|
||||
echo "=================================="
|
||||
|
||||
113
ai/.run/ai-service.run.xml
Normal file
113
ai/.run/ai-service.run.xml
Normal 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>
|
||||
BIN
ai/logs/ai-service.log.2025-10-24.0.gz
Normal file
BIN
ai/logs/ai-service.log.2025-10-24.0.gz
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user