Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
# 포트번호는 38XXX

# PostgreSQL
POSTGRES_DB=stackup
POSTGRES_USER=stackup
POSTGRES_PASSWORD=stackup
POSTGRES_PORT=5432
POSTGRES_PORT=38040

# RabbitMQ
RABBITMQ_USER=stackup
RABBITMQ_PASSWORD=stackup
RABBITMQ_PORT=5672
RABBITMQ_MANAGEMENT_PORT=15672
RABBITMQ_PORT=38050
RABBITMQ_MANAGEMENT_PORT=38051

# MinIO (S3)
# MinIO
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
MINIO_API_PORT=9000
MINIO_CONSOLE_PORT=9001
MINIO_API_PORT=38060
MINIO_CONSOLE_PORT=38061
MINIO_BUCKET=stackup

# AI server
AI_PORT=8000
AI_PORT=38030
STORAGE_BACKEND=s3
STORAGE_LOCAL_ROOT=/var/lib/stackup-ai/storage
S3_REGION=us-east-1
LLM_API_KEY=
LLM_BASE_URL=https://factchat-cloud.mindlogic.ai/v1/gateway
LLM_PRO_MODEL=gemini-3.1-pro-preview

# RealTime server
REALTIME_PORT=8081
REALTIME_PORT=38020
REALTIME_LOG_LEVEL=info

# Backend Core (Spring)
BACKEND_PORT=38010

# NGINX
FRONTEND_PORT=38000
97 changes: 97 additions & 0 deletions .github/workflows/deploy-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: STACK-UP deploy ana-server

# dev 브랜치가 업데이트되면 AI서버와 실시간서버를 빌드하고 배포
# 상우 원격컴에서만 작동하니 클라우드로 옮기면 삭제해야 함
# 백엔드 코어랑 프론트는 아직 도커에 정의되지 않았으니.. 그때 이어서 해야 함

on:
push:
branches: [dev]
paths:
- "ai/**"
- "realtime/**"
- "docker-compose.yml"
- "infra/**"
- ".github/workflows/deploy-app.yml"
workflow_dispatch:

concurrency:
group: deploy-app-${{ github.ref }}
cancel-in-progress: false

jobs:
deploy:
runs-on: self-hosted
timeout-minutes: 20

env:
DEPLOY_DIR: /home/stackup/stackup

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Sync repo to deploy dir (preserve .env and data)
run: |
mkdir -p "$DEPLOY_DIR"
rsync -a --delete \
--exclude='.git' \
--exclude='.env' \
--exclude='var/' \
--exclude='**/__pycache__' \
--exclude='**/.venv' \
--exclude='**/node_modules' \
"$GITHUB_WORKSPACE/" "$DEPLOY_DIR/"

- name: Ensure .env exists and contains current LLM_API_KEY
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
run: |
cd "$DEPLOY_DIR"
if [ ! -f .env ]; then
cp .env.example .env
fi
# Write LLM_API_KEY via a temp file to keep the secret off the
# process command line.
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
awk -v key="$LLM_API_KEY" '
BEGIN { written = 0 }
/^LLM_API_KEY=/ { print "LLM_API_KEY=" key; written = 1; next }
{ print }
END { if (!written) print "LLM_API_KEY=" key }
' .env > "$tmp"
mv "$tmp" .env
chmod 600 .env

- name: Build and restart app services
run: |
cd "$DEPLOY_DIR"
docker compose up -d --build ai realtime

- name: Wait for healthy
run: |
cd "$DEPLOY_DIR"
set +e
rc=0
for svc in ai realtime; do
container="stackup-$svc"
ok=0
for i in $(seq 1 36); do
h=$(docker inspect --format '{{ .State.Health.Status }}' "$container" 2>/dev/null || echo "missing")
echo " [$i] $container: $h"
if [ "$h" = "healthy" ]; then ok=1; break; fi
sleep 5
done
if [ "$ok" -ne 1 ]; then
echo "::error::$container did not become healthy"
docker compose logs --tail=100 "$svc" || true
rc=1
fi
done
docker compose ps
exit $rc

- name: Prune dangling images (best-effort)
if: always()
run: docker image prune -f >/dev/null 2>&1 || true
17 changes: 11 additions & 6 deletions ai/.env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
# Application
DEBUG=false

# RabbitMQ
RABBITMQ_URL=amqp://stackup:stackup@localhost:5672/
RABBITMQ_URL=amqp://stackup:stackup@localhost:38050/

STORAGE_BACKEND=s3
STORAGE_LOCAL_ROOT=./var/storage

# S3 / MinIO
S3_ENDPOINT_URL=http://localhost:9000
S3_ENDPOINT_URL=http://localhost:38060
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET_NAME=stackup
S3_REGION=us-east-1

# LLM API Keys
OPENAI_API_KEY=
GOOGLE_API_KEY=
# 충대 API 키 사용
LLM_API_KEY=
LLM_BASE_URL=https://factchat-cloud.mindlogic.ai/v1/gateway
LLM_PRO_MODEL=gemini-3.1-pro-preview
LLM_PRO_TEMPERATURE=0.2
71 changes: 46 additions & 25 deletions ai/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,23 @@ ai/
│ └── ai_server/
│ ├── __init__.py
│ ├── main.py # FastAPI 앱 팩토리
│ ├── api/ # FastAPI 라우터 (health, internal endpoints)
│ │ └── health.py
│ ├── api/ # FastAPI 라우터 (health 등)
│ ├── config/
│ │ └── settings.py # pydantic-settings (env)
│ ├── chain/ # (계획) LangChain 체인 정의
│ ├── rag/ # (계획) 청킹·임베딩·검색
│ ├── analyzer/ # (계획) 이력서·레포 분석
│ ├── voice/ # (계획) STT/TTS, 음성 분석
│ ├── messaging/ # (계획) RabbitMQ consumer/publisher
│ ├── storage/ # (계획) S3 client
│ └── model/ # (계획) Pydantic 모델 (메시지 envelope, 도메인)
│ ├── analyzer/ # 이력서·레포·웹 분석 오케스트레이션
│ │ ├── resume_analyzer.py
│ │ └── sources/ # SourceExtractor 추상화 + 구현체 (PDF; repo/web 후속)
│ ├── chain/ # LangChain 체인 + 프롬프트
│ │ ├── document_analysis_chain.py
│ │ └── prompts/
│ ├── storage/ # ObjectStorage 추상화 (local fs / s3)
│ ├── messaging/ # RabbitMQ consumer/publisher
│ ├── model/ # Pydantic 모델 (envelope, 메시지 페이로드)
│ ├── rag/ # (계획) 청킹·임베딩·pgvector
│ └── voice/ # (Phase 2) STT/TTS, 음성 분석
└── tests/
```

> 현재는 `api/`, `config/`만 존재. 기능 추가 시 위 골격대로 디렉토리 생성.

---

## 3. 책임 매트릭스
Expand Down Expand Up @@ -114,33 +115,44 @@ async def consume_resume_analyze(message: AbstractIncomingMessage) -> None:

## 6. LLM 사용 패턴

### 6.1 모델 선택
| 시점 | 모델 | 용도 |
|------|------|------|
| 세션 시작 | Pro (Gemini 3.1 Pro 기본) | 질문 풀 (품질) |
| 세션 중 | Flash (Gemini 3.1 Flash) | 꼬리질문 (저지연 < 3s) |
### 6.1 게이트웨이
모든 LLM 호출은 **Mindlogic CNU AC Gateway** (OpenAI 호환) 단일 엔드포인트로 통과한다.
- `LLM_BASE_URL` (default `https://factchat-cloud.mindlogic.ai/v1/gateway`)
- `LLM_API_KEY` (학교 발급, secret/`.env`로만 주입)
- 모델 카탈로그: Claude / GPT / Gemini / xAI / Perplexity / Solar / Gemma 등을 단일 키로 사용.
- 게이트웨이 교체(자체 OpenAI 키, vLLM 등) 시 `LLM_BASE_URL` 한 줄만 변경.

### 6.2 모델 선택
| 시점 | 모델 (env override 가능) | 용도 |
|------|--------------------------|------|
| 세션 시작 | Pro (`gemini-3.1-pro-preview` 기본) | 질문 풀 (품질) |
| 세션 중 | Flash (`gemini-3.1-flash-lite-preview` 후보) | 꼬리질문 (저지연 < 3s) |
| 분석 (이력서/레포) | Pro | 마크다운 구조화 |

설정은 `settings.py` + 환경변수로 모델명 주입 (코드에 하드코딩 금지).

### 6.2 LangChain 사용
### 6.3 LangChain 사용 (OpenAI 호환 클라이언트)
```python
from langchain_core.prompts import ChatPromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages([
("system", "당신은 면접관입니다..."),
("human", "이력서: {resume}\n질문 후보: ..."),
])
llm = ChatGoogleGenerativeAI(model=settings.llm_pro_model)
chain = prompt | llm | StructuredOutputParser(schema=...)
llm = ChatOpenAI(
model=settings.llm_pro_model,
base_url=settings.llm_base_url,
api_key=settings.llm_api_key,
)
chain = prompt | llm | PydanticOutputParser(pydantic_object=...)
```

- 모든 프롬프트는 `chain/prompts/{name}.py` 단일 위치
- 프롬프트 변경은 PR로 (커밋 메시지에 의도 명시)
- 응답은 **반드시 schema validation** (Pydantic) — LLM 결과를 그대로 신뢰 X

### 6.3 호출 로깅
### 6.4 호출 로깅
호출 시작/완료/실패를 Core 서버에 RabbitMQ로 보내 `ai_request_logs`에 기록 — 또는 자체 endpoint POST.
필드: `request_type`, `model_name`, `input_tokens`, `output_tokens`, `latency_ms`, `status`.

Expand Down Expand Up @@ -293,10 +305,19 @@ docker run --env-file .env -p 8000:8000 stackup-ai
## 16. 현재 상태 (2026-05 기준)

- FastAPI 부트스트랩 + 헬스체크
- RabbitMQ consumer **echo만** 구현: `ai.analyze.resume` → `callback.analysis` 즉시 echo 응답 (멱등 LRU + structlog)
- 비즈니스 로직 (PDF 파싱·LLM·임베딩·S3·pgvector) 미구현 — US-09 본 구현 PR에서 추가
- `analyze.repository` / `generate.questions` / `generate.followup` consumer는 큐 정의만 존재, 코드 없음
- LangChain import만 있음, 체인·프롬프트 정의 0
- RabbitMQ consumer `ai.analyze.resume` 본 구현:
- PDF 텍스트 추출 (`analyzer/sources/pdf.py`, pypdf)
- LLM 분석 (`chain/document_analysis_chain.py`, Gemini Pro + Pydantic 출력 파서)
- 분석 MD를 스토리지에 저장
- `callback.analysis` 발행 (status `ANALYZED` / `FAILED`, retriable 플래그 포함)
- **스토리지 추상화 도입** (`storage/`): `ObjectStorage` 인터페이스 + 구현체 두 개.
- `S3Storage` (기본) — boto3 + `asyncio.to_thread`. MinIO·AWS S3 모두 호환. `S3_ENDPOINT_URL`만 바꿔 swap.
- `LocalFilesystemStorage` (dev/test 전용) — aiofiles + traversal 방어.
- `STORAGE_BACKEND=s3|local` 환경변수 한 줄로 전환.
- **소스 추출 추상화 도입** (`analyzer/sources/`): `SourceExtractor` 인터페이스 + `PdfSourceExtractor`.
GitHub repo / 웹 이력서 추출기는 동일 인터페이스로 후속 PR에서 추가 예정.
- 임베딩·청킹·pgvector upsert는 미구현 → 후속 PR. `embedding_chunk_count`는 0으로 발행.
- `analyze.repository` / `generate.questions` / `generate.followup` consumer는 큐 정의만, 코드 없음
- 음성 모듈은 Phase 2

각 도입 시 본 문서 갱신.
4 changes: 2 additions & 2 deletions ai/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ RUN uv sync --frozen --no-dev

COPY src/ src/

EXPOSE 8000
EXPOSE 38030

CMD ["uv", "run", "uvicorn", "ai_server.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uv", "run", "uvicorn", "ai_server.main:app", "--host", "0.0.0.0", "--port", "38030"]
5 changes: 4 additions & 1 deletion ai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ dependencies = [
"pydantic-settings>=2.13.1",
"httpx>=0.28.1",
"aio-pika>=9.6.2",
"boto3>=1.42.77",
"boto3>=1.42.77",
"aiofiles>=24.1.0",
"pypdf>=5.1.0",
"langchain>=1.2.13",
"langchain-core>=1.2.22",
"langchain-community>=0.4.1",
"langchain-openai>=0.3.0",
"structlog>=25.5.0",
]

Expand Down
Loading