Skip to content

Ridealist/cscl-tblt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

173 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

English Speaking Practice โ€” CSCL TBLT

LiveKit ๊ธฐ๋ฐ˜ AI ์˜์–ด ๋Œ€ํ™” ์‹ค์Šต ์‹œ์Šคํ…œ. TBLT(Task-Based Language Teaching) ๋ฐฉ์‹์œผ๋กœ, AI ์บ๋ฆญํ„ฐ Kate์™€ ๊ณผ์—… ๊ธฐ๋ฐ˜ ์˜์–ด ๋Œ€ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์‹ค์‹œ๊ฐ„ ์Œ์„ฑ ๋Œ€ํ™” ์—์ด์ „ํŠธ. ๋Œ€์ƒ: ํ•œ๊ตญ์–ด๊ถŒ ํ•™์ƒ.

๊ด€๋ฆฌ์ž๋Š” /admin์—์„œ ์ˆ˜์—… ์šด์˜ ๋ชจ๋“œ๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ (pipeline): ํ•™์ƒ n๋ช… + ์—์ด์ „ํŠธ 1๋ช…์˜ STT โ†’ LLM โ†’ TTS ํŒŒ์ดํ”„๋ผ์ธ. ์—์ด์ „ํŠธ ์ด๋ฆ„: pipeline-agent
  • ๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ (realtime): ํ•™์ƒ 1๋ช… + ์—์ด์ „ํŠธ 1๋ช…์˜ OpenAI Realtime speech-to-speech ํŒŒ์ดํ”„๋ผ์ธ. ์—์ด์ „ํŠธ ์ด๋ฆ„: realtime-agent, role: dominant ๋˜๋Š” collaborative

ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

CSCL_TBLT/
โ”œโ”€โ”€ .env                  # API ํ‚ค (์ง์ ‘ ์ž…๋ ฅ ํ•„์š”, git ์ถ”์  ์•ˆ ๋จ)
โ”œโ”€โ”€ .env.example          # ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ
โ”œโ”€โ”€ .github/
โ”‚   โ””โ”€โ”€ workflows/
โ”‚       โ”œโ”€โ”€ agent-tests.yml            # agent/ ํ…Œ์ŠคํŠธ CI
โ”‚       โ””โ”€โ”€ client-build-and-test.yaml # client/ ๋นŒ๋“œ/๋ฆฐํŠธ CI
โ”‚
โ”œโ”€โ”€ agent/                # AI ์Œ์„ฑ ์—์ด์ „ํŠธ (LiveKit Agents Python SDK)
โ”‚   โ”œโ”€โ”€ main.py           # ์—์ด์ „ํŠธ ์ง„์ž…์  (CLI ์ง€์›)
โ”‚   โ”œโ”€โ”€ prompt_pipeline.py # ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ
โ”‚   โ”œโ”€โ”€ prompt_realtime.py # ๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ
โ”‚   โ”œโ”€โ”€ logger.py         # ๋Œ€ํ™” JSON ๋กœ๊ฑฐ
โ”‚   โ”œโ”€โ”€ pyproject.toml
โ”‚   โ”œโ”€โ”€ uv.lock
โ”‚   โ””โ”€โ”€ tests/
โ”‚       โ””โ”€โ”€ test_conversation.py  # LLM ๋‹จ๋… ๋Œ€ํ™” ํ๋ฆ„ ํ…Œ์ŠคํŠธ
โ”‚
โ”œโ”€โ”€ server/               # legacy static ํด๋ผ์ด์–ธํŠธ์šฉ ํ† ํฐ ๋ฐœ๊ธ‰ ์„œ๋ฒ„ (FastAPI)
โ”‚   โ”œโ”€โ”€ main.py           # GET /token, POST /dispatch
โ”‚   โ”œโ”€โ”€ pyproject.toml
โ”‚   โ””โ”€โ”€ uv.lock
โ”‚
โ”œโ”€โ”€ client/               # Next.js ๋ธŒ๋ผ์šฐ์ € ํด๋ผ์ด์–ธํŠธ
โ”‚   โ”œโ”€โ”€ app/              # Next.js App Router + API routes
โ”‚   โ”œโ”€โ”€ components/       # ๋กœ๋น„, ๊ด€๋ฆฌ์ž, LiveKit UI ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”œโ”€โ”€ static/           # legacy ์‹ฌํ”Œ ํด๋ผ์ด์–ธํŠธ (HTML/JS)
โ”‚   โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ”‚   โ”œโ”€โ”€ app.js
โ”‚   โ”‚   โ””โ”€โ”€ style.css
โ”‚   โ””โ”€โ”€ .env.local        # Next.js์šฉ ํ™˜๊ฒฝ๋ณ€์ˆ˜ (git ์ถ”์  ์•ˆ ๋จ)
โ”‚
โ”œโ”€โ”€ config.example.json   # ๋กœ์ปฌ fallback/import์šฉ ์šด์˜ ์„ค์ • ์˜ˆ์‹œ
โ”œโ”€โ”€ config.json           # ๋กœ์ปฌ fallback ์šด์˜ ์„ค์ • (git ์ถ”์  ์•ˆ ๋จ)
โ””โ”€โ”€ logs/                 # ๋Œ€ํ™” ๋กœ๊ทธ JSON (์ž๋™ ์ƒ์„ฑ, git ์ถ”์  ์•ˆ ๋จ)

๋ธŒ๋žœ์น˜ ์ „๋žต

์ด ์ €์žฅ์†Œ๋Š” main / develop ๊ธฐ์ค€์œผ๋กœ ์šด์˜ํ•œ๋‹ค.

๋ธŒ๋žœ์น˜ ์—ญํ•  ์šด์˜ ์›์น™
main ์‹ค์ œ ์„œ๋ฒ„ ๋ฐฐํฌ์™€ ์—ฐ๊ฒฐ๋˜๋Š” ์•ˆ์ • ๋ธŒ๋žœ์น˜ ๋ฐฐํฌ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋งŒ ์œ ์ง€ํ•œ๋‹ค. ๊ฒ€์ฆ๋œ ๋ณ€๊ฒฝ๋งŒ ๋ณ‘ํ•ฉํ•˜๊ณ , ๋ฏธ์™„์„ฑ ์‹คํ—˜์€ ์˜ฌ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
develop ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ๊ณผ ์‹คํ—˜ ํ†ตํ•ฉ ๋ธŒ๋žœ์น˜ ์ƒˆ ๊ธฐ๋Šฅ, UI/ํ”„๋กฌํ”„ํŠธ ์‹คํ—˜, ๊ฒ€์ฆ ์ „ ๋ณ€๊ฒฝ์˜ ๊ธฐ๋ณธ ํ•ฉ๋ฅ˜ ์ง€์ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

๊ธฐ๋ณธ ํ๋ฆ„:

  • ์ƒˆ ์ž‘์—…์€ develop์—์„œ feature/..., issue-..., experiment/... ๋ธŒ๋žœ์น˜๋ฅผ ๋งŒ๋“ ๋‹ค.
  • ์ผ๋ฐ˜ ๋ณ€๊ฒฝ์€ ์ž‘์—… ๋ธŒ๋žœ์น˜์—์„œ develop์œผ๋กœ PR์„ ์—ด์–ด ํ•ฉ์นœ๋‹ค.
  • ๋ฐฐํฌํ•  ๋ฌถ์Œ์ด ์•ˆ์ •ํ™”๋˜๋ฉด develop์—์„œ main์œผ๋กœ PR์„ ์—ด์–ด ํ•ฉ์นœ ๋’ค ์‹ค์ œ ์„œ๋ฒ„์— ๋ฐฐํฌํ•œ๋‹ค.
  • ์šด์˜ ์žฅ์• ๋‚˜ ๊ธด๊ธ‰ ์ˆ˜์ •์€ main์—์„œ hotfix/... ๋ธŒ๋žœ์น˜๋ฅผ ๋งŒ๋“ค๊ณ , ๋ฐฐํฌ ํ›„ ๊ฐ™์€ ์ˆ˜์ •์ด develop์—๋„ ๋ฐ˜์˜๋˜๋„๋ก ํ•œ๋‹ค.

์•„ํ‚คํ…์ฒ˜

ํ˜„์žฌ ๊ธฐ๋ณธ ๊ตฌ์กฐ: Next.js ํด๋ผ์ด์–ธํŠธ

๋ธŒ๋ผ์šฐ์ € ํด๋ผ์ด์–ธํŠธ (client/ Next.js)
    โ”‚
    โ”œโ”€[GET /api/rooms]โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Supabase app_settings + LiveKit room ์กฐํšŒ
    โ”œโ”€[POST /api/admin/config]โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Supabase app_settings์— ์šด์˜ ์„ค์ • ์ €์žฅ
    โ”œโ”€[POST /api/token]โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ LiveKit ์ฐธ๊ฐ€์ž ํ† ํฐ ๋ฐœ๊ธ‰
    โ”‚                                  room_config.agents๋กœ agent ์ž๋™ ๋ฐฐ์น˜
    โ”‚
    โ””โ”€[WebRTC ์—ฐ๊ฒฐ]โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ LiveKit Cloud
                                      โ”‚
                                      โ”œโ”€ pipeline-agent: STT โ†’ LLM โ†’ TTS
                                      โ””โ”€ realtime-agent: OpenAI Realtime
  • Next.js ํด๋ผ์ด์–ธํŠธ (client/): ํ•™์ƒ ๋กœ๋น„, ๊ด€๋ฆฌ์ž ์„ค์ •, ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ, ํ† ํฐ ๋ฐœ๊ธ‰ API๋ฅผ ํฌํ•จํ•œ๋‹ค.
  • AI ์—์ด์ „ํŠธ (agent/): ๊ฐ™์€ ์ฝ”๋“œ์—์„œ pipeline-agent, realtime-agent worker๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. Realtime role์€ dominant ๋˜๋Š” collaborative๋กœ ์„ ํƒํ•œ๋‹ค.
  • legacy ํ† ํฐ ์„œ๋ฒ„ (server/): client/static HTML ํด๋ผ์ด์–ธํŠธ์šฉ์ด๋‹ค. ํ˜„์žฌ Next.js ์•ฑ ์‹คํ–‰์—๋Š” ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค.

์ˆ˜์—… ์šด์˜ ๋ชจ๋“œ

๋ชจ๋“œ UX ํ‘œ์‹œ๋ช… Room ์ •์ฑ… Agent ์Œ์„ฑ ์ฒ˜๋ฆฌ
pipeline ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ 12๋ฐ˜-1๊ทธ๋ฃน ๊ฐ™์€ ๊ทธ๋ฃน room pipeline-agent STT deepgram/nova-3 โ†’ LLM openai/gpt-4.1-mini โ†’ TTS cartesia/sonic-3
realtime ๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ realtime-{๋ฐ˜}-{ํ•™์ƒ๋ช…}-{suffix} ์ž๋™ ์ƒ์„ฑ realtime-agent OpenAI Realtime speech-to-speech

์‚ฌ์ „ ์ค€๋น„

1. uv ์„ค์น˜ (์—†๋Š” ๊ฒฝ์šฐ)

curl -LsSf https://astral.sh/uv/install.sh | sh

2. .env ์ž‘์„ฑ

cp .env.example .env
cp config.example.json config.json

config.json์€ Supabase๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์€ ๋กœ์ปฌ ๊ฐœ๋ฐœ fallback ๋ฐ ์ดˆ๊ธฐ import ์ฐธ๊ณ ์šฉ์ด๋‹ค. Production ์šด์˜ ์„ค์ •์€ Supabase app_settings์— ์ €์žฅ๋œ๋‹ค.

.env ํŒŒ์ผ์— ๊ฐ’ ์ž…๋ ฅ:

# LiveKit Cloud (https://cloud.livekit.io โ†’ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ํ›„ ๋ฐœ๊ธ‰)
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret

# ์ฑ„ํŒ…๋ฐฉ ๋ฐ ์—์ด์ „ํŠธ ์ด๋ฆ„
ROOM_NAME=english-practice
AGENT_NAME=pipeline-agent
AGENT_WORKER_MODE=pipeline
AGENT_ROLE=dominant

# Realtime ๋ชจ๋“œ์—๋Š” OPENAI_API_KEY๊ฐ€ ํ•„์š”
# Pipeline ๋ชจ๋“œ๋Š” LiveKit Inference๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด OPENAI_API_KEY / DEEPGRAM_API_KEY ์—†์ด๋„ ๋™์ž‘
OPENAI_API_KEY=
DEEPGRAM_API_KEY=

Next.js ์•ฑ์˜ ์šด์˜ ๋ชจ๋“œ๋Š” .env๊ฐ€ ์•„๋‹ˆ๋ผ Supabase app_settings.agent_mode๋กœ ๊ฒฐ์ •๋˜๋ฉฐ, /admin์—์„œ ๋ณ€๊ฒฝํ•œ๋‹ค. Supabase๊ฐ€ ์—†๋Š” ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” config.json fallback์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. Realtime ์ƒํ˜ธ์ž‘์šฉ role์€ ๊ด€๋ฆฌ์ž ์ „์šฉ agentRole ์„ค์ •์œผ๋กœ๋งŒ ๋…ธ์ถœ๋œ๋‹ค.

3. ์—์ด์ „ํŠธ ๋ชจ๋ธ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ (์ตœ์ดˆ 1ํšŒ)

cd agent
uv sync
uv run python main.py download-files

์‹คํ–‰ ๋ฐฉ๋ฒ•

ํ˜„์žฌ Next.js ์•ฑ ์‹คํ–‰

์ตœ์ดˆ 1ํšŒ ์˜์กด์„ฑ์„ ์„ค์น˜ํ•œ๋‹ค.

pnpm setup

๋ฃจํŠธ์—์„œ ๋‹ค์Œ ๋ช…๋ น ํ•˜๋‚˜๋กœ agent worker์™€ Next.js ํด๋ผ์ด์–ธํŠธ๋ฅผ ํ•จ๊ป˜ ์‹คํ–‰ํ•œ๋‹ค. pnpm dev์˜ auto ๋ชจ๋“œ๋Š” ๋กœ์ปฌ ํ”„๋กœ์„ธ์Šค ์„ ํƒ์„ ์œ„ํ•ด config.json์˜ agentMode/agentRole์„ ์ฝ๋Š”๋‹ค. ์•ฑ route์˜ ๋Ÿฐํƒ€์ž„ ์šด์˜ ์„ค์ •์€ Supabase app_settings๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, Supabase๊ฐ€ ์—†๋Š” ๋กœ์ปฌ fallback ํ™˜๊ฒฝ์—์„œ๋Š” config.json ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.

pnpm dev

๋ธŒ๋ผ์šฐ์ €์—์„œ:

Next.js ํด๋ผ์ด์–ธํŠธ๋Š” ์ž์ฒด /api/token ๋ผ์šฐํŠธ๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ server/ FastAPI ํ† ํฐ ์„œ๋ฒ„๊ฐ€ ํ•„์š” ์—†๋‹ค.

๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ worker๋กœ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์„ ์‚ฌ์šฉํ•œ๋‹ค.

pnpm dev:realtime

collaborative ์กฐ๊ฑด์œผ๋กœ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์„ ์‚ฌ์šฉํ•œ๋‹ค.

pnpm dev:realtime:collaborative

pipeline, realtime dominant, realtime collaborative worker๋ฅผ ๋ชจ๋‘ ํ•จ๊ป˜ ๋„์šฐ๋ ค๋ฉด ๋‹ค์Œ ๋ช…๋ น์„ ์‚ฌ์šฉํ•œ๋‹ค.

pnpm dev:all

/admin์—์„œ ๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ์™€ ์ƒํ˜ธ์ž‘์šฉ ๋ฐฉ์‹์„ ์„ ํƒํ•˜๋ฉด ํ•™์ƒ์€ ์กฐ๊ฑด๋ช…์„ ๋ณด์ง€ ์•Š๊ณ  ๊ฐœ๋ณ„ room์œผ๋กœ ์ž…์žฅํ•œ๋‹ค.

๊ธฐ์กด์ฒ˜๋Ÿผ ํ„ฐ๋ฏธ๋„์„ ๋‚˜๋ˆ  ์‹คํ–‰ํ•ด์•ผ ํ•  ๋•Œ๋Š” ๋‹ค์Œ ๊ฐœ๋ณ„ ๋ช…๋ น์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

pnpm dev:agent:pipeline
pnpm dev:client

legacy static ํด๋ผ์ด์–ธํŠธ ์‹คํ–‰ (์„ ํƒ)

client/static์„ ์‚ฌ์šฉํ•  ๋•Œ๋งŒ FastAPI ํ† ํฐ ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

# ํ„ฐ๋ฏธ๋„ 1 โ€” legacy ํ† ํฐ ์„œ๋ฒ„
cd server
uv sync
uv run python -m uvicorn main:app --port 8000 --reload
# ํ„ฐ๋ฏธ๋„ 2 โ€” pipeline-agent
cd agent
AGENT_WORKER_MODE=pipeline uv run python main.py dev
# ํ„ฐ๋ฏธ๋„ 3 โ€” static HTML ํด๋ผ์ด์–ธํŠธ
cd client/static
python -m http.server 3000

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:3000 ์ ‘์† โ†’ ์ด๋ฆ„ ์ž…๋ ฅ โ†’ ์ž…์žฅ โ†’ ๐Ÿค– ์—์ด์ „ํŠธ ์ƒ์„ฑ ๋ฒ„ํŠผ ํด๋ฆญ.


CLI ๋‹จ๋… ์‹คํ—˜ (๋ธŒ๋ผ์šฐ์ €/ํ”„๋ก ํŠธ์—”๋“œ ์—†์ด)

์—์ด์ „ํŠธ๋ฅผ ํ„ฐ๋ฏธ๋„์—์„œ ์ง์ ‘ ์‹คํ–‰ํ•ด ๋ฐฑ์—”๋“œ๋งŒ ๋น ๋ฅด๊ฒŒ ์‹คํ—˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋งˆ์ดํฌ์™€ ์Šคํ”ผ์ปค๋งŒ ์žˆ์œผ๋ฉด ๋˜๊ณ , ํ† ํฐ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๋Š” ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

cd agent

# ํ„ฐ๋ฏธ๋„ ์Œ์„ฑ ๋Œ€ํ™” ๋ชจ๋“œ (๋งˆ์ดํฌ๋กœ ๋งํ•˜๊ณ  ์Šคํ”ผ์ปค๋กœ ๋“ฃ๊ธฐ)
uv run python main.py console

# ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ๋ชจ๋“œ (ํ† ํฐ ์„œ๋ฒ„ยทํด๋ผ์ด์–ธํŠธ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ)
uv run python main.py dev

# ํ”„๋กœ๋•์…˜ ๋ชจ๋“œ
uv run python main.py start
๋ช…๋ น ์„ค๋ช… ํ•„์š”ํ•œ ๊ฒƒ
console ํ„ฐ๋ฏธ๋„ ๋‹จ๋… ์Œ์„ฑ ๋Œ€ํ™” ๋งˆ์ดํฌ, ์Šคํ”ผ์ปค
dev ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๊ฒฐ ๋Œ€๊ธฐ (์ž๋™ ์žฌ์‹œ์ž‘) Next.js ํด๋ผ์ด์–ธํŠธ
start ํ”„๋กœ๋•์…˜ ์‹คํ–‰ Next.js ํด๋ผ์ด์–ธํŠธ
download-files VAD ๋“ฑ ML ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ โ€”

Next.js ํด๋ผ์ด์–ธํŠธ

client/.env.local์— LiveKit ์ž๊ฒฉ์ฆ๋ช…์ด ํ•„์š”ํ•˜๋‹ค.

LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret

# Supabase admin auth / persistent state migration
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
SUPABASE_SECRET_KEY=sb_secret_...

Next.js API routes:

Route ์—ญํ• 
POST /api/token ์ฐธ๊ฐ€์ž ํ† ํฐ ๋ฐœ๊ธ‰, ํ˜„์žฌ ์šด์˜ ๋ชจ๋“œ์— ๋งž๋Š” agent ์ž๋™ ๋ฐฐ์น˜
GET /api/rooms ํ™œ์„ฑ ๋ฐ˜์˜ ๊ทธ๋ฃน room๊ณผ realtime ๊ฐœ๋ณ„ room ํ˜„ํ™ฉ ์กฐํšŒ
GET/POST /api/admin/config Supabase app_settings ์ฝ๊ธฐ/์“ฐ๊ธฐ
GET/POST /api/dispatch ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ์—์„œ pipeline-agent ์ˆ˜๋™ ๋ฐฐ์น˜/์กด์žฌ ํ™•์ธ
POST /api/rooms/terminate room ๊ฐ•์ œ ์ข…๋ฃŒ
GET /api/logs, GET /api/logs/stream ๋Œ€ํ™” ๋กœ๊ทธ ๋ชฉ๋ก/์ŠคํŠธ๋ฆฌ๋ฐ

์ฃผ์˜: Production์—์„œ /admin, /api/admin, ๊ด€๋ฆฌ์ž์„ฑ API(/api/dispatch, /api/logs, /api/rooms/terminate)๋Š” Supabase Auth session๊ณผ profiles.role = 'admin' ๊ถŒํ•œ์œผ๋กœ ๋ณดํ˜ธ๋œ๋‹ค. ADMIN_USERNAME/ADMIN_PASSWORD Basic Auth๋Š” ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

Supabase runtime storage

Supabase ๋„์ž…์€ GitHub issue #4์˜ ๋‹จ๊ณ„๋ณ„ ์ž‘์—…์œผ๋กœ ์ง„ํ–‰ํ•œ๋‹ค. ํ˜„์žฌ admin auth์™€ ์ˆ˜์—… ์šด์˜ ์„ค์ •์€ Supabase๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

  • client/lib/supabase/client.ts: browser/client component์šฉ Supabase client
  • client/lib/supabase/server.ts: server component, route handler, server action์šฉ cookie-aware client
  • client/lib/supabase/admin.ts: ์„œ๋ฒ„ ์ „์šฉ secret-key client
  • client/lib/supabase/proxy.ts: Auth session refresh์™€ admin route guard์šฉ proxy/middleware helper
  • client/lib/settings-store.ts: app_settings ๊ธฐ๋ฐ˜ ์šด์˜ ์„ค์ • store์™€ local fallback adapter
  • supabase/config.toml: ๋กœ์ปฌ Supabase CLI ์‹คํ–‰ ํฌํŠธ์™€ migration ์„ค์ •
  • supabase/migrations/20260611000000_foundation.sql: ์ดˆ๊ธฐ schema์™€ RLS policy
  • supabase/migrations/20260612000000_realtime_prompt_version_rpc.sql: active prompt version ๊ต์ฒด RPC

์ดˆ๊ธฐ schema๋Š” ๋‹ค์Œ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ ๋‹ค.

Table ์—ญํ• 
profiles Supabase Auth ์‚ฌ์šฉ์ž๋ณ„ ์•ฑ ๊ถŒํ•œ ๋ฐ ํ‘œ์‹œ๋ช…
app_settings ์ˆ˜์—… ์šด์˜ ์„ค์ • ์ €์žฅ์†Œ
realtime_prompt_versions Realtime prompt override version ์ €์žฅ์†Œ

์ตœ์ดˆ admin์€ Supabase Auth ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํ›„ SQL ๋˜๋Š” dashboard์—์„œ profiles.role = 'admin'์œผ๋กœ ๋ถ€์—ฌํ•œ๋‹ค.

insert into public.profiles (user_id, role, display_name)
values ('<auth-user-id>', 'admin', 'Admin')
on conflict (user_id) do update
set role = 'admin',
    display_name = excluded.display_name;

SUPABASE_SECRET_KEY๋Š” RLS๋ฅผ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ server-only ์ฝ”๋“œ์—์„œ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. Next.js API routes๋Š” client/.env.local์—์„œ ์ด ๊ฐ’์„ ์ฝ๊ณ , Python realtime agent๋Š” root .env์—์„œ ๊ฐ™์€ URL/secret์„ ์ฝ์–ด promptVersionId์— ํ•ด๋‹นํ•˜๋Š” realtime_prompt_versions row๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ํ™”๋ฉด์€ /admin/login์ด๋‹ค. ๋กœ๊ทธ์ธ์€ Supabase email/password ๊ณ„์ •์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ๊ณ„์ •์˜ profiles.role์ด admin์ด์–ด์•ผ /admin ๋ฐ ๊ด€๋ฆฌ์ž์„ฑ API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ์กด config.json ๊ฐ’์„ Supabase๋กœ ์˜ฎ๊ธธ ๋•Œ๋Š” ๋‹ค์Œ ํ˜•ํƒœ๋กœ app_settings ๊ธฐ๋ณธ row๋ฅผ seedํ•œ๋‹ค.

insert into public.app_settings (
  id,
  num_classes,
  num_groups_per_class,
  class_start,
  active_class,
  agent_mode,
  agent_role,
  feedback_condition_id,
  realtime_resetting
)
values (
  'default',
  4,
  12,
  9,
  9,
  'realtime',
  'collaborative',
  'explicit_correction',
  false
)
on conflict (id) do update
set num_classes = excluded.num_classes,
    num_groups_per_class = excluded.num_groups_per_class,
    class_start = excluded.class_start,
    active_class = excluded.active_class,
    agent_mode = excluded.agent_mode,
    agent_role = excluded.agent_role,
    feedback_condition_id = excluded.feedback_condition_id,
    realtime_resetting = excluded.realtime_resetting;

Production์—์„œ๋Š” client/.env.local๊ณผ root .env์— Supabase ์—ฐ๊ฒฐ๊ฐ’์ด ํ•„์š”ํ•˜๋‹ค. Next.js์—๋Š” NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY๊ฐ€ ํ•„์š”ํ•˜๊ณ , Python realtime agent์—๋Š” NEXT_PUBLIC_SUPABASE_URL ๋˜๋Š” SUPABASE_URL๊ณผ SUPABASE_SECRET_KEY ๋˜๋Š” SUPABASE_SERVICE_ROLE_KEY๊ฐ€ ํ•„์š”ํ•˜๋‹ค. Supabase๊ฐ€ ๋ฏธ์„ค์ •์ด๊ฑฐ๋‚˜ DB ์žฅ์• ๊ฐ€ ์žˆ์œผ๋ฉด ์šด์˜ ์„ค์ • route๋Š” setup/runtime error๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๋กœ์ปฌ ๊ฐœ๋ฐœ์—์„œ๋Š” Supabase๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ผ์‹œ์ ์œผ๋กœ ์‹คํŒจํ•  ๋•Œ config.json fallback์„ ์‚ฌ์šฉํ•œ๋‹ค.

๊ธฐ์กด prompt_config.json override๋Š” supabase/README.md์˜ realtime_prompt_versions Migration ์ ˆ์ฐจ๋กœ ํ•œ ๋ฒˆ๋งŒ active version row๋กœ ์ด๊ด€ํ•œ๋‹ค. ์ด๊ด€ ํ›„ Realtime custom prompt source of truth๋Š” Supabase prompt version row๋‹ค. /api/token์€ active row์˜ promptVersionId๋ฅผ LiveKit metadata์— ๋„ฃ๊ณ , Python realtime agent๋Š” ๊ทธ id๋กœ ๊ฐ™์€ row๋ฅผ fetchํ•œ๋‹ค. promptVersionId๊ฐ€ ์—†๋Š” default session์€ tracked markdown prompt๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

๋กœ์ปฌ Supabase CLI๋Š” ๊ธฐ์กด ํ”„๋กœ์ ํŠธ์™€ ๊ธฐ๋ณธ ํฌํŠธ๊ฐ€ ์ถฉ๋Œํ•˜์ง€ ์•Š๋„๋ก 5532x ๋Œ€์—ญ์„ ์‚ฌ์šฉํ•œ๋‹ค.

supabase start
supabase db reset --no-seed
supabase status

client/.env.local์—์„œ ๋กœ์ปฌ Supabase๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” supabase status ์ถœ๋ ฅ์˜ key๋ฅผ ๋„ฃ๋Š”๋‹ค.

NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:55321
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<publishable-key-from-supabase-status>
SUPABASE_SECRET_KEY=<secret-key-from-supabase-status>

Studio๋Š” http://127.0.0.1:55323, Postgres๋Š” postgresql://postgres:postgres@127.0.0.1:55322/postgres์—์„œ ์—ด๋ฆฐ๋‹ค.

๊ด€๋ฆฌ์ž ์„ค์ •

/admin์—์„œ ๋‹ค์Œ ๊ฐ’์„ ๊ด€๋ฆฌํ•œ๋‹ค.

์„ค์ • ์ €์žฅ ์œ„์น˜ ์„ค๋ช…
์ˆ˜์—… ์šด์˜ ๋ชจ๋“œ app_settings.agent_mode pipeline ๋˜๋Š” realtime
ํ˜„์žฌ ์ˆ˜์—… ์ค‘์ธ ๋ฐ˜ app_settings.active_class ํ•™์ƒ ๋กœ๋น„์— ํ‘œ์‹œ๋˜๋Š” ๋ฐ˜
๋ฐ˜ ๋ฒˆํ˜ธ ์‹œ์ž‘ app_settings.class_start ๋ฐ˜ ๋ฒˆํ˜ธ ๋ฒ”์œ„์˜ ์‹œ์ž‘
์ „์ฒด ํ•™๊ธ‰ ์ˆ˜ app_settings.num_classes ๊ด€๋ฆฌ์ž ํ™”๋ฉด์— ํ‘œ์‹œํ•  ๋ฐ˜ ๊ฐœ์ˆ˜
๋ฐ˜๋‹น ๊ทธ๋ฃน ์ˆ˜ app_settings.num_groups_per_class ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ์—์„œ ํ‘œ์‹œํ•  ๊ทธ๋ฃน ์ˆ˜

์˜ˆ์‹œ:

{
  "numClasses": 4,
  "numGroupsPerClass": 12,
  "classStart": 9,
  "activeClass": 12,
  "agentMode": "pipeline"
}

ํ…Œ์ŠคํŠธ

์—์ด์ „ํŠธ LLM ๋™์ž‘ ํ…Œ์ŠคํŠธ

๋งˆ์ดํฌยท๋ธŒ๋ผ์šฐ์ € ์—†์ด LLM๋งŒ ์‚ฌ์šฉํ•ด ๋Œ€ํ™” ํ๋ฆ„์„ ์ž๋™ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

cd agent
uv run pytest tests/ -v

ํ…Œ์ŠคํŠธ ๋‚ด์šฉ (tests/test_conversation.py):

  • 5ํ„ด ๋Œ€ํ™” ํ๋ฆ„ ๊ฒ€์ฆ (์ฃผ๋ง ์•ฝ์† ๋งŒ๋“ค๊ธฐ ํƒœ์Šคํฌ)
  • ๋ฌธ๋ฒ• ์˜ค๋ฅ˜ ๋ฐœํ™”์— ๋Œ€ํ•œ Kate์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฐ˜์‘ ํ™•์ธ
  • ์—ญ์งˆ๋ฌธ(์ฃผ๋ง ํ™œ๋™)์— ๋Œ€ํ•œ ์‘๋‹ต ํ™•์ธ

๊ด€๋ฆฌ์ž ๋ชจ๋‹ˆํ„ฐ๋ง

Next.js ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‹คํ–‰ ์ค‘์ผ ๋•Œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์•„๋ž˜ ์ฃผ์†Œ์— ์ ‘์†ํ•˜๋ฉด ์„ธ์…˜ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

http://localhost:3000/admin/dashboard
  • ์ €์žฅ๋œ ์„ธ์…˜ ๋ชฉ๋ก ํ™•์ธ
  • ์„ธ์…˜๋ณ„ ๋Œ€ํ™” ๋กœ๊ทธ ํ™•์ธ
  • ์ฐธ๊ฐ€์ž ์ด๋ฆ„, ๋ฐœํ™” ์‹œ๊ฐ, ์—ญํ• (User/Agent) ํ‘œ์‹œ

legacy static ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” client/static/admin.html์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


๋Œ€ํ™” ๋กœ๊ทธ

๋Œ€ํ™”๊ฐ€ ๋๋‚˜๋ฉด logs/ ํด๋”์— JSON ํŒŒ์ผ๋กœ ์ž๋™ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. root .env์—์„œ LOG_PROMPT_STACK=true๋ฅผ ์„ค์ •ํ•˜๋ฉด ๊ฐ Realtime ์„ธ์…˜์˜ ์ „์ฒด resolved prompt stack์ด metadata.prompt_stack์— ํ•จ๊ป˜ ์ €์žฅ๋˜๊ณ  /admin/dashboard์—์„œ ์ ‘์–ด์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ off์ž…๋‹ˆ๋‹ค.

logs/
โ””โ”€โ”€ RM_9GkS7d8BWCsf--260409_14:23.json

ํŒŒ์ผ๋ช… ํ˜•์‹: {session_id}_{started_at}.json

{
  "session_id": "RM_9GkS7d8BWCsf",
  "room": "english-practice",
  "entries": [
    {
      "timestamp": "2026-04-09T14:23:05",
      "role": "user",
      "text": "Hello!",
      "participant_identity": "user_1234",
      "participant_name": "๊ณ ์ค€๋ณด"
    },
    {
      "timestamp": "2026-04-09T14:23:07",
      "role": "agent",
      "text": "Hi! How are you today?"
    }
  ]
}

์ฃผ์š” ์„ค์ •

ํ•ญ๋ชฉ ์œ„์น˜
Realtime AI ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ˆ˜์ • ๊ธฐ๋ณธ๊ฐ’์€ prompts/realtime/*.md, ๋Ÿฐํƒ€์ž„ ์ˆ˜์ •์€ /admin์—์„œ Supabase realtime_prompt_versions๋กœ ์ €์žฅ
๊ธฐ๋ณธ AI ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ fallback ๊ทธ๋ฃน ๋Œ€ํ™”๋Š” agent/prompt_pipeline.py, ๊ฐœ๋ณ„ ๋Œ€ํ™”๋Š” prompts/realtime/*.md
๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ ๋ชจ๋ธ ๋ณ€๊ฒฝ agent/main.py (STT deepgram/nova-3, LLM openai/gpt-4.1-mini, TTS cartesia/sonic-3)
๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ ๋ชจ๋ธ ๋ณ€๊ฒฝ agent/main.py (openai.realtime.RealtimeModel)
์‹คํ–‰ํ•  worker ๋ชจ๋“œ AGENT_WORKER_MODE=pipeline ๋˜๋Š” AGENT_WORKER_MODE=realtime + AGENT_ROLE=dominant/collaborative
์ˆ˜์—… ์šด์˜ ๋ชจ๋“œ ๋ณ€๊ฒฝ /admin ๋˜๋Š” Supabase app_settings.agent_mode
Realtime ์ƒํ˜ธ์ž‘์šฉ role ๋ณ€๊ฒฝ /admin ๋˜๋Š” Supabase app_settings.agent_role
ํ† ํฐ ์„œ๋ฒ„ ํฌํŠธ ๋ณ€๊ฒฝ server/main.py + client/static/app.js ์ƒ๋‹จ SERVER ๋ณ€์ˆ˜
legacy static Room ์ด๋ฆ„ ๋ณ€๊ฒฝ .env โ†’ ROOM_NAME
์—์ด์ „ํŠธ ์ด๋ฆ„ ๋ณ€๊ฒฝ agent/main.py, client/lib/agent-role.ts (pipeline-agent, realtime-agent)
๋กœ๊ทธ ์ €์žฅ ์œ„์น˜ ๋ณ€๊ฒฝ agent/logger.py โ†’ LOGS_DIR

Realtime ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ๋Š” prompts/realtime/ ์•„๋ž˜์˜ ๋ฌธ์„œํ˜• ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. agent์™€ admin ๊ธฐ๋ณธ๊ฐ’์€ ์ด md ํŒŒ์ผ๋“ค์„ ์ง์ ‘ ์ฝ์Šต๋‹ˆ๋‹ค.

pnpm prompts:check

์›๋ณธ ํŒŒ์ผ์€ base.md, roles/dominant.md, roles/collaborative.md, condition-combinations/*.md, task-cards/*.md์ž…๋‹ˆ๋‹ค. Realtime runtime stack์€ base prompt, role prompt, role/feedback condition-combination prompt, task card ์ˆœ์„œ๋กœ ์กฐํ•ฉ๋ฉ๋‹ˆ๋‹ค. task-cards/manifest.json์— ๋“ฑ๋ก๋œ ์ฃผ์ œ๋ณ„ task card ์ค‘ /admin์—์„œ ์„ ํƒํ•œ ํ•ญ๋ชฉ์ด ๊ฐœ๋ณ„ Realtime ์„ธ์…˜์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. pnpm prompts:check๋Š” ํŒŒ์ผ ๋ˆ„๋ฝ์ด๋‚˜ ๋นˆ ํŒŒ์ผ ๊ฐ™์€ ๊ธฐ๋ณธ ์˜ค๋ฅ˜๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.

/admin์—์„œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ €์žฅํ•˜๋ฉด Supabase realtime_prompt_versions์— ์ƒˆ version row๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ํ•ด๋‹น row๋งŒ active๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. active custom version์€ prompts/realtime/*.md ๊ธฐ๋ณธ๊ฐ’๋ณด๋‹ค ์šฐ์„ ํ•˜๋ฉฐ, base/role prompt์™€ ํ•จ๊ป˜ conditionCombinationPrompts, feedbackConditionId, taskCardId, taskCardPrompt snapshot์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. feedbackPrompt๋Š” legacy metadata๋กœ ๋‚จ์•„ ์žˆ์ง€๋งŒ runtime stack์—๋Š” ์‚ฝ์ž…๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. md ๊ธฐ๋ณธ๊ฐ’์„ ๋‹ค์‹œ ์“ฐ๋ ค๋ฉด /admin์—์„œ "๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ณต์›"์„ ์‹คํ–‰ํ•ด active custom version์„ ๋น„ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ผ ๋ฌธ์„œ์—์„œ ๋ณต์‚ฌํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ํ•œ ํŒŒ์ผ๋กœ ๊ฒ€์‚ฌ๊ธฐ์— ๋„˜๊ธธ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ๊ฒ€์‚ฌ๊ธฐ๋Š” # BASE PROMPT:, # INTERLOCUTOR ROLE PROMPT: Dominant, # INTERLOCUTOR ROLE PROMPT: Collaborative, # TASK CARD: ํ—ค๋”ฉ์„ ๊ธฐ์ค€์œผ๋กœ ๋„ค ๊ตฌ๊ฐ„์„ ๋‚˜๋ˆ•๋‹ˆ๋‹ค.

pnpm dev, pnpm build, pnpm start, pnpm dev:agent:*, ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ๋Š” ์‹คํ–‰ ์ „์— prompts:check์™€ ๊ฐ™์€ ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.


ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ (AWS EC2)

ํ˜„์žฌ production ๊ธฐ์ค€๊ฐ’๊ณผ ๋ฐฐํฌ ์ ˆ์ฐจ๋Š” docs/operations/๋ฅผ ๋”ฐ๋ฅธ๋‹ค.

  • Production Environment: ํ˜„์žฌ EC2, ์„œ๋น„์Šค๋ช…, ์šด์˜ ๋ชจ๋“œ, ์™ธ๋ถ€ ์„œ๋น„์Šค ๊ธฐ์ค€๊ฐ’
  • Deployment Runbook: GitHub Actions ๋ฐฐํฌ, ์žฌ์‹œ์ž‘, health check ์ ˆ์ฐจ
  • Capacity Plan: ํ‰์ƒ์‹œ/์‹คํ—˜์ผ ์ธ์Šคํ„ด์Šค์™€ Realtime ๋™์‹œ ์„ธ์…˜ ๊ธฐ์ค€
  • Incident Checklist: ์žฅ์•  ํ™•์ธ ์ˆœ์„œ์™€ ์ฃผ์š” ๋กœ๊ทธ

์š”์•ฝ:

ํ•ญ๋ชฉ ํ˜„์žฌ ๊ธฐ์ค€
Production domain https://tblt-agent.net/
App path /opt/cscl-tblt
Client PM2 cscl-client, port 3000
Agent systemd cscl-agent.service
Operation mode source of truth Supabase app_settings
Current production mode realtime
Deploy script scripts/deploy-production.sh
Health check https://tblt-agent.net/api/health

CI/CD ํŒŒ์ดํ”„๋ผ์ธ

GitHub Actions๋Š” ์„ธ ์ข…๋ฅ˜๋กœ ๊ตฌ์„ฑํ•œ๋‹ค.

Workflow Trigger ์—ญํ• 
pre-commit PR/push โ†’ main, develop, ์ˆ˜๋™ ์‹คํ–‰ ๊ณตํ†ต ํŒŒ์ผ ํ˜•์‹ ๋ฐ repo guardrail ๊ฒ€์‚ฌ
CI PR โ†’ main, develop, ์ˆ˜๋™ ์‹คํ–‰ Client lint/format/build, Agent tests
Deploy Production main push ๋˜๋Š” ์ˆ˜๋™ ์‹คํ–‰ ๊ฒ€์ฆ ํ›„ EC2 production ๋ฐฐํฌ

Production ๋ฐฐํฌ๋Š” GitHub Actions๊ฐ€ SSH๋กœ EC2์— ์ ‘์†ํ•ด scripts/deploy-production.sh๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

ํ•„์š”ํ•œ GitHub Environment/Secrets:

์ด๋ฆ„ ์ข…๋ฅ˜ ์„ค๋ช…
PROD_SSH_HOST Secret EC2 host ๋˜๋Š” IP
PROD_SSH_USER Secret ๋ฐฐํฌ SSH ์‚ฌ์šฉ์ž, ์˜ˆ: ubuntu
PROD_SSH_KEY Secret ๋ฐฐํฌ์šฉ private key
PROD_SSH_PORT Secret SSH port, ๊ธฐ๋ณธ๊ฐ’ 22
PROD_APP_DIR Environment variable ์„œ๋ฒ„ repo ๊ฒฝ๋กœ, ๊ธฐ๋ณธ๊ฐ’ /opt/cscl-tblt

์„œ๋ฒ„ ์กฐ๊ฑด:

  • /opt/cscl-tblt๊ฐ€ Git checkout์ด์–ด์•ผ ํ•œ๋‹ค.
  • ๋ฐฐํฌ ์‚ฌ์šฉ์ž๊ฐ€ systemctl restart/is-active cscl-agent๋ฅผ password ์—†์ด ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • pnpm, uv, pm2, curl์ด ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • ์„œ๋ฒ„์˜ .env, client/.env.local, config.json, prompt_config.json์€ ๋ฐฐํฌ ์ค‘ ๋ฐฑ์—… ํ›„ ๋ณต์›๋œ๋‹ค.
  • config.json์€ git ์ถ”์  ๋Œ€์ƒ์ด ์•„๋‹ˆ๋ฉฐ, Supabase๊ฐ€ ์—†๋Š” ๋กœ์ปฌ fallback/import ์šฉ๋„๋กœ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.
  • Production์—์„œ๋Š” client/.env.local์— NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค. Realtime custom prompt๋ฅผ ์“ฐ๋Š” agent host์˜ root .env์—๋„ Supabase URL๊ณผ server-only secret์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • Production ์šด์˜ ์„ค์ •์€ Supabase app_settings(id = 'default') row์— ์ €์žฅ๋œ๋‹ค.
  • ์ตœ์ดˆ admin ๊ณ„์ •์€ Supabase Auth ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํ›„ profiles.role = 'admin'์œผ๋กœ bootstrapํ•ด์•ผ ํ•œ๋‹ค.

๋ฐฐํฌ ํ›„ health check:

  • cscl-agent active ํ™•์ธ
  • cscl-client PM2 process ํ™•์ธ ๋ฐ ์žฌ์‹œ์ž‘
  • http://localhost:3000/api/health ์‘๋‹ต ํ™•์ธ

main์— merge๋˜๋ฉด ๋ฐฐํฌ๊ฐ€ ๋ฐ”๋กœ ์‹คํ–‰๋˜๋ฏ€๋กœ, GitHub branch protection์—์„œ pre-commit, CI / Client, CI / Agent๋ฅผ required check๋กœ ์„ค์ •ํ•œ๋‹ค.


์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ

# Agent ์ƒํƒœ
sudo systemctl status cscl-agent
sudo journalctl -u cscl-agent --no-pager | tail -30

# Client ์ƒํƒœ
pm2 list
pm2 logs cscl-client --lines 30

# nginx ์ƒํƒœ
sudo systemctl status nginx
sudo nginx -t

ํ˜„์žฌ ๊ตฌํ˜„ ๋ฉ”๋ชจ

ํ•ญ๋ชฉ ๋‚ด์šฉ
์šด์˜ ๋ชจ๋“œ ์ €์žฅ Supabase app_settings.agent_mode์— pipeline ๋˜๋Š” realtime ์ €์žฅ
Realtime ์ƒํ˜ธ์ž‘์šฉ role ์ €์žฅ Supabase app_settings.agent_role์— dominant ๋˜๋Š” collaborative ์ €์žฅ
Agent entrypoint agent/main.py์— pipeline-agent, realtime-agent ๋“ฑ๋ก
ํ”„๋กฌํ”„ํŠธ ๋ถ„๋ฆฌ ๊ทธ๋ฃน ๋Œ€ํ™”๋Š” agent/prompt_pipeline.py, ๊ฐœ๋ณ„ ๋Œ€ํ™” ๊ธฐ๋ณธ๊ฐ’์€ prompts/realtime/*.md
Realtime ์˜์กด์„ฑ livekit-agents[openai,silero,turn-detector]~=1.5
Egress ์—…๋กœ๋“œ S3_ENDPOINT ๊ฐ’์ด http๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ๋งŒ endpoint๋กœ ์‚ฌ์šฉ

์Œ์„ฑ ๋…น์Œ (Egress)

์„ธ์…˜ ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ LiveKit Egress API๊ฐ€ ํ˜ธ์ถœ๋˜์–ด ๋ชจ๋“  ์ฐธ๊ฐ€์ž(ํ•™์ƒ + AI) ์Œ์„ฑ์ด ํ˜ผํ•ฉ๋œ MP3 ํŒŒ์ผ์ด S3์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

  • ์ €์žฅ ๊ฒฝ๋กœ: s3://tblt-agent-recordings/recordings/{๋ฃธ๋ช…}-{LiveKit room SID}-{ํƒ€์ž„์Šคํƒฌํ”„}.mp3
  • ํŠธ๋ฆฌ๊ฑฐ: session.start() ์งํ›„ ์ž๋™ ์‹œ์ž‘
  • ์ข…๋ฃŒ: ๋ฃธ disconnected ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ ์ž๋™ ์ข…๋ฃŒ
  • ๊ด€๋ จ ์ฝ”๋“œ: agent/egress_recorder.py

์Œ์„ฑ ๋…น์Œ (Egress) ๋‹ค์šด๋กœ๋“œ ๋ฐฉ๋ฒ•

  • ๋กœ์ปฌ ๋‹ค์šด๋กœ๋“œ: scripts/download_s3_recordings.sh [output_dir]
  • ๋‚ ์งœ ํ•„ํ„ฐ: SINCE_YYYYMMDD=20260701 scripts/download_s3_recordings.sh [output_dir]
  • ์‚ญ์ œ dry run: BEFORE_YYYYMMDD=20260601 scripts/delete_s3_recordings.sh
  • ์‹ค์ œ ์‚ญ์ œ: BEFORE_YYYYMMDD=20260601 APPLY=true scripts/delete_s3_recordings.sh
  • LastModified ๊ธฐ์ค€ ์‚ญ์ œ: BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z scripts/delete_s3_recordings.sh

Egress๋Š” LiveKit Cloud ์ธํ”„๋ผ์—์„œ ์‹คํ–‰๋˜๋ฏ€๋กœ ์„œ๋ฒ„ ๋ถ€ํ•˜ ์—†์Œ. ๋‹จ, S3 ๋ฒ„ํ‚ท ๋ฐ IAM ๊ถŒํ•œ(s3:PutObject) ์„ค์ •์ด ๋˜์–ด ์žˆ์–ด์•ผ ํ•จ.

๋กœ์ปฌ ๋‹ค์šด๋กœ๋“œ/์‚ญ์ œ ์Šคํฌ๋ฆฝํŠธ ์‚ฌ์šฉ๋ฒ•

S3์— ์ €์žฅ๋œ ๋…น์Œ ํŒŒ์ผ์„ ๋กœ์ปฌ๋กœ ๋ฐ›๊ฑฐ๋‚˜ ์ •๋ฆฌํ•  ๋•Œ๋Š” ์•„๋ž˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์‚ฌ์ „ ์กฐ๊ฑด:

  • ๋กœ์ปฌ์— AWS CLI(aws)๊ฐ€ ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • .env ๋˜๋Š” ์ง€์ •ํ•œ ENV_FILE์— ์ตœ์†Œํ•œ ๋‹ค์Œ ๊ฐ’์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.
    • S3_BUCKET
    • S3_REGION
    • AWS_ACCESS_KEY ๋˜๋Š” AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY
  • AWS IAM ๊ถŒํ•œ:
    • ๋‹ค์šด๋กœ๋“œ: s3:ListBucket, s3:GetObject
    • ์‚ญ์ œ: s3:ListBucket, s3:DeleteObject

์Šคํฌ๋ฆฝํŠธ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์˜ .env๋ฅผ ์ฝ๋Š”๋‹ค. ์šด์˜ ์„œ๋ฒ„์˜ ๊ฐ’์„ ์“ฐ๊ณ  ์‹ถ์œผ๋ฉด ENV_FILE=/opt/cscl-tblt/.env์ฒ˜๋Ÿผ ์ง€์ •ํ•˜๋ฉด ๋œ๋‹ค.

scripts/download_s3_recordings.sh

์ „์ฒด ๋…น์Œ ํŒŒ์ผ์„ ๋กœ์ปฌ ๋””๋ ‰ํ„ฐ๋ฆฌ๋กœ ๋‹ค์šด๋กœ๋“œํ•œ๋‹ค.

scripts/download_s3_recordings.sh

๊ธฐ๋ณธ ๋‹ค์šด๋กœ๋“œ ๊ฒฝ๋กœ๋Š” ./downloads/recordings์ด๋‹ค. ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋กœ ๋ฐ›๊ณ  ์‹ถ์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋„˜๊ธด๋‹ค.

scripts/download_s3_recordings.sh ./tmp/recordings

ํŒŒ์ผ๋ช…์— ํฌํ•จ๋œ ๋‚ ์งœ ๊ธฐ์ค€์œผ๋กœ ํŠน์ • ์‹œ์  ์ดํ›„๋งŒ ๋ฐ›๊ณ  ์‹ถ์œผ๋ฉด SINCE_YYYYMMDD๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ํ˜•์‹์€ ๋ฐ˜๋“œ์‹œ YYYYMMDD๋‹ค.

SINCE_YYYYMMDD=20260701 scripts/download_s3_recordings.sh
SINCE_YYYYMMDD=20260601 scripts/download_s3_recordings.sh ./tmp/recordings

์ด ํ•„ํ„ฐ๋Š” S3 LastModified๊ฐ€ ์•„๋‹ˆ๋ผ ํŒŒ์ผ๋ช… ๊ทœ์น™ recordings/{๋ฃธ๋ช…}-{LiveKit room SID}-YYYYMMDD_HHMMSS.mp3์˜ ๋‚ ์งœ ๋ถ€๋ถ„์„ ๊ธฐ์ค€์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.

์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜:

  • ENV_FILE: ์ฝ์–ด์˜ฌ env ํŒŒ์ผ ๊ฒฝ๋กœ
  • S3_PREFIX: ๊ธฐ๋ณธ๊ฐ’ recordings
  • SINCE_YYYYMMDD: ํ•ด๋‹น ๋‚ ์งœ ์ด์ƒ์ธ ํŒŒ์ผ๋งŒ ๋‹ค์šด๋กœ๋“œ
  • S3_ENDPOINT: Cloudflare R2, MinIO ๊ฐ™์€ S3 ํ˜ธํ™˜ ์Šคํ† ๋ฆฌ์ง€์šฉ endpoint

scripts/delete_s3_recordings.sh

S3 ๊ฐ์ฒด ์‚ญ์ œ๋Š” ์‹ค์ˆ˜ ๋น„์šฉ์ด ํฌ๋ฏ€๋กœ ๊ธฐ๋ณธ๊ฐ’์ด dry run์ด๋‹ค. APPLY=true๋ฅผ ์ฃผ์ง€ ์•Š์œผ๋ฉด ์‹ค์ œ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ  ์‚ญ์ œ ๋Œ€์ƒ๋งŒ ์ถœ๋ ฅํ•œ๋‹ค.

ํŒŒ์ผ๋ช… ๋‚ ์งœ ๊ธฐ์ค€์œผ๋กœ ํŠน์ • ๋‚ ์งœ ์ด์ „ ํŒŒ์ผ์„ ํ™•์ธ:

BEFORE_YYYYMMDD=20260601 scripts/delete_s3_recordings.sh

ํŒŒ์ผ๋ช… ๋‚ ์งœ ๊ธฐ์ค€์œผ๋กœ ์‹ค์ œ ์‚ญ์ œ:

BEFORE_YYYYMMDD=20260601 APPLY=true scripts/delete_s3_recordings.sh

S3 ๊ฐ์ฒด์˜ LastModified ๊ธฐ์ค€์œผ๋กœ ํŠน์ • ์‹œ๊ฐ ์ด์ „ ํŒŒ์ผ์„ ํ™•์ธ:

BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z scripts/delete_s3_recordings.sh

LastModified ๊ธฐ์ค€ ์‹ค์ œ ์‚ญ์ œ:

BEFORE_LAST_MODIFIED=2026-06-01T00:00:00Z APPLY=true scripts/delete_s3_recordings.sh

์ฃผ์˜์‚ฌํ•ญ:

  • BEFORE_YYYYMMDD์™€ BEFORE_LAST_MODIFIED๋Š” ๋™์‹œ์— ์„ค์ •ํ•  ์ˆ˜ ์—†๋‹ค.
  • BEFORE_YYYYMMDD ํ˜•์‹์€ YYYYMMDD๋‹ค.
  • BEFORE_LAST_MODIFIED ํ˜•์‹์€ UTC ISO-8601์ด์–ด์•ผ ํ•œ๋‹ค. ์˜ˆ: 2026-06-01T00:00:00Z
  • ์‹ค์ œ ์‚ญ์ œ ์ „์— dry run ์ถœ๋ ฅ์ด ์˜๋„ํ•œ ๋Œ€์ƒ๊ณผ ๋งž๋Š”์ง€ ๋จผ์ € ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

์šด์˜ ์„œ๋ฒ„ env๋ฅผ ์ง์ ‘ ์จ์„œ ์‚ญ์ œ ๋Œ€์ƒ์„ ํ™•์ธํ•˜๋ ค๋ฉด:

ENV_FILE=/opt/cscl-tblt/.env BEFORE_YYYYMMDD=20260601 scripts/delete_s3_recordings.sh

๋ถ€ํ•˜ ๋ฐ ์‚ฌ์–‘ ์ฐธ๊ณ 

  • ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ ๊ธฐ์ค€: ๋™์‹œ 30๋ช… = ์•ฝ 15๊ฐœ Agent ์„ธ์…˜ (2์ธ 1๊ทธ๋ฃน)
  • ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ ๋ณ‘๋ชฉ: Silero VAD (10ms ๋‹จ์œ„ ๋กœ์ปฌ ์‹ ๊ฒฝ๋ง ์ถ”๋ก , ์„ธ์…˜๋‹น 812% CPU)
  • ๊ทธ๋ฃน ๋Œ€ํ™” ๋ชจ๋“œ STT/LLM/TTS: LiveKit Cloud inference์—์„œ ์ฒ˜๋ฆฌ
  • ๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ: ํ•™์ƒ 1๋ช…๋‹น realtime Agent ์„ธ์…˜ 1๊ฐœ
  • ๊ฐœ๋ณ„ ๋Œ€ํ™” ๋ชจ๋“œ์—๋Š” OpenAI Realtime ์‚ฌ์šฉ๋Ÿ‰๊ณผ ๋™์‹œ ์„ธ์…˜ ํ•œ๋„ ํ™•์ธ ํ•„์š”
  • ํ˜„์žฌ production ๊ธฐ์ค€์€ Capacity Plan์„ ๋”ฐ๋ฅธ๋‹ค.
  • t3 ๊ณ„์—ด์€ ์ง€์† ๊ณ ๋ถ€ํ•˜ ์ˆ˜์—… ์šด์˜ ์‹œ CPU ํฌ๋ ˆ๋”ง ์†Œ์ง„์œผ๋กœ ์Šค๋กœํ‹€๋ง๋  ์ˆ˜ ์žˆ๋‹ค.

About

No description, website, or topics provided.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors