๊ธฐ๋Šฅ ์ƒ์„ธ ๋ณด๊ณ 

10. ์›น UI ๊ธฐ๋Šฅ ๋ถ„์„ ๋ณด๊ณ ์„œ (FastAPI + React + Mol*)

๋ฃจํŠธ: AgenticAI4SCIENCE_pyrosetta_track/repos/ai4sci-kaeri ๋Œ€์ƒ: backend/ (FastAPI), frontend/src/ (React 19 + Vite + Mol*) ๊ทœ์น™: ๋ชจ๋“  ์‚ฌ์‹ค์€ file_path:line ์ธ์šฉ. ๋ฏธํ™•์ธ ํ•ญ๋ชฉ์€ "๋ฏธ๊ฒ€์ฆ" ๋ช…์‹œ. ์ฝ๊ธฐ์ „์šฉ ๋ถ„์„.


โ‘  ๋™์ž‘ ์›๋ฆฌ

A. ๋ฐฑ์—”๋“œ ์•„ํ‚คํ…์ฒ˜ (FastAPI)

  • ์•ฑ ํŒฉํ† ๋ฆฌ: create_app() ์ด ๋ชจ๋“  ๋ผ์šฐํ„ฐ๋ฅผ /api ํ”„๋ฆฌํ”ฝ์Šค๋กœ ๋“ฑ๋ก (backend/main.py:45-135). ๋ชจ๋“ˆ ๋ ˆ๋ฒจ app = create_app() ์œผ๋กœ uvicorn backend.main:app ๊ธฐ๋™ (backend/main.py:139, backend/main.py:7).
  • CORS: http://localhost:5173(Vite dev), http://localhost:8787(API) ๋งŒ ํ—ˆ์šฉ, ๋ฉ”์†Œ๋“œ GET/POST/PUT/DELETE/OPTIONS (backend/main.py:65-70).
  • ํ‘œ์ค€ ์—๋Ÿฌ ์‘๋‹ต: {"error","detail","status_code"} ํ˜•์‹์œผ๋กœ ์ „์—ญ ์˜ˆ์™ธ(500)ยทHTTPExceptionยทValidationError(422) ํ•ธ๋“ค๋Ÿฌ ์ •์˜ (backend/main.py:73-108).
  • ํฌํŠธ: ๊ธฐ๋ณธ 8787, API_PORT env ๋กœ override (backend/state.py:56).

B. StatusEmitter โ†’ JSON ํŒŒ์ผ โ†’ ํด๋ง (ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ ํ๋ฆ„)

์ด ์‹œ์Šคํ…œ์˜ ๋ผ์ด๋ธŒ ๋ชจ๋‹ˆํ„ฐ๋ง์€ SSE/WebSocket ํ‘ธ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ "ํŒŒ์ผ + ํด๋ง" ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.

  1. ์“ฐ๊ธฐ(Writer): ํŒŒ์ดํ”„๋ผ์ธ(scripts/run_pyrosetta_flow.py)์ด StatusEmitter ์ธ์Šคํ„ด์Šค๋ฅผ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ . - ์ƒํƒœ ํŒŒ์ผ ๊ธฐ๋ณธ ๊ฒฝ๋กœ /tmp/pipeline_local_status.json (backend/status_emitter.py:30-34, env PIPELINE_STATUS_FILE). - ์ƒํƒœ dict ์Šคํ‚ค๋งˆ: steps(13๊ฐœ ํŒŒ์ดํ”„๋ผ์ธ ๋‹จ๊ณ„), agents(5๊ฐœ), rosetta_substeps(7๊ฐœ), timeline, candidates, qc_gates, convergence, best_candidate, molecules, visualization_images, completed ๋“ฑ (backend/status_emitter.py:91-114). - ๋ชจ๋“  ๊ฐฑ์‹  ๋ฉ”์†Œ๋“œ(update_step, update_agent, set_candidates ๋“ฑ)๊ฐ€ ๋งค๋ฒˆ flush() ํ˜ธ์ถœ (backend/status_emitter.py:117-134). - flush() ๋Š” ์›์ž์  ์“ฐ๊ธฐ: fcntl.flock ๋ฐฐํƒ€ ๋ฝ + temp write + rename ์œผ๋กœ ๋™์‹œ ์›Œ์ปค์˜ torn-write ๋ฐฉ์ง€ (backend/status_emitter.py:117-133, C5 fix ์ฃผ์„).

  2. ์ฝ๊ธฐ(Reader): GET /api/status (backend/routers/status.py:157-164). - read_status() ๊ฐ€ ํŒŒ์ผ mtime ์บ์‹œ๋กœ I/O ์ ˆ์•ฝํ•˜๋˜ server_time ์€ ์บ์‹œ hit ์‹œ์—๋„ ๋งค๋ฒˆ ๊ฐฑ์‹  (backend/state.py:127-153). - is_active_run ํŒŒ์ƒ ํ•„๋“œ ์ฃผ์ž…: (not completed) AND (steps ๋˜๋Š” candidates ์กด์žฌ) (backend/state.py:101-124). - ํŒŒ์ผ ๋ถ€์žฌ ์‹œ {"error":"no_status_file","connected":False} ๋ฐ˜ํ™˜ (backend/state.py:136-137). - on-the-fly enrichment: candidates ์— 6๊ฐœ ์•ฝ๋ฆฌ/๊ตฌ์กฐ ํ•„๋“œ(instability_index, gravy, net_charge_ph74, selectivity_margin, fwkt_contact, chelator_site_available)๋ฅผ ์‘๋‹ต ์‹œ์ ์— ๋จธ์ง€ (backend/routers/status.py:60-164). โ‘คโ‘ฅ์€ sequence-only ํœด๋ฆฌ์Šคํ‹ฑ์ด๋ผ HEURISTIC ์œผ๋กœ ๋ช…์‹œ (backend/routers/status.py:73-76, 138-150).

  3. ํด๋ง(Frontend): usePipelineStatus(2000) ํ›…์ด 2์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ fetch('/api/status') (frontend/src/hooks/usePipelineStatus.ts:262-296, 329-343). - AbortController ๋กœ ์ด์ „ ์š”์ฒญ ์ทจ์†Œ (usePipelineStatus.ts:263-265). - no_status_file/connected:false ๊ฐ์ง€ ์‹œ INITIAL_STATE ๋กœ ๋ฆฌ์…‹ โ€” Live ๋Œ€์‹œ๋ณด๋“œ๋กœ ์˜ค์ธ ๋ฐฉ์ง€ (usePipelineStatus.ts:274-283). - snake_case BE โ†’ camelCase FE ๋งคํ•‘์€ parseStatusData() (usePipelineStatus.ts:170-236). - ๋™์‹œ์— /api/runs ํด๋ง์œผ๋กœ ์•„์นด์ด๋ธŒ ๋ชฉ๋ก ๊ฐฑ์‹ , switchRun() ์œผ๋กœ ๊ณผ๊ฑฐ run ์กฐํšŒ(/api/runs/{run_id}) (usePipelineStatus.ts:252-260, 298-326).

ํด๋ง ์ค‘๋ณต(์„ค๊ณ„์ƒ ์˜๋„): usePipelineStatus ๊ฐ€ /api/status(2s), useExperiment ๊ฐ€ /api/experiment/status(3s) ๋ฅผ ๋ณ„๋„ ํด๋ง. ์ „์ž๋Š” ๋Œ€์‹œ๋ณด๋“œ ์‹œ๊ฐํ™”, ํ›„์ž๋Š” start/stop ์ œ์–ด์šฉ (usePipelineStatus.ts:238-245, frontend/src/hooks/useExperiment.ts:74-93).

C. ์‹คํ—˜ ์ œ์–ด (Experiment Control)

  • POST /api/experiment/run ์ด subprocess.Popen ์œผ๋กœ scripts/run_pyrosetta_flow.py ๋ฅผ bio-tools conda python ์œผ๋กœ ๊ธฐ๋™ (backend/routers/experiment.py:205-293, 145-155).
  • ํ…œํ”Œ๋ฆฟ PDB ๊ธฐ๋ณธ๊ฐ’ SSTR2_SST14_complex_boltz_1.pdb (experiment.py:220-222).
  • ์‹œ์ž‘ ์ฆ‰์‹œ phase:"initializing" ์ตœ์†Œ status ํŒŒ์ผ ๊ธฐ๋ก (experiment.py:84-98, 273).
  • watchdog ์Šค๋ ˆ๋“œ: ๊ธฐ๋ณธ 1์‹œ๊ฐ„(EXPERIMENT_MAX_RUNTIME) ์ดˆ๊ณผ ์‹œ SIGTERMโ†’SIGKILL (experiment.py:30-31, 112-142).
  • ์ •์ƒ/์ค‘์ง€/ํƒ€์ž„์•„์›ƒ ์ข…๋ฃŒ ์‹œ _auto_archive() ๋กœ archives/{run_id}_dashboard.json ์ €์žฅ (experiment.py:34-70, 296-317).
  • GET /api/experiment/models: vLLM(port 8000 /v1/models) ์šฐ์„  ์กฐํšŒ, ์‹คํŒจ ์‹œ ollama ํด๋ฐฑ, ์ตœ์ข… ํด๋ฐฑ ["qwen3-32b"] (experiment.py:163-189).
  • FE useExperiment ๊ฐ€ config/models ๋กœ๋“œ + start/stop + ์ƒํƒœ ํด๋ง ๋‹ด๋‹น (useExperiment.ts:53-141).

D. ์—์ด์ „ํŠธ ๋กœ๊ทธ โ€” REST + SSE

์—ฌ๊ธฐ์—๋งŒ ์ง„์งœ SSE(Server-Sent Events) ๊ฐ€ ์กด์žฌํ•œ๋‹ค.

  • GET /api/agents/{run_id}/log: runs_local/{run_id}/experiment_log.jsonl ๋“ฑ์—์„œ ๋ˆ„์  ๋กœ๊ทธ 1ํšŒ ๋ฐ˜ํ™˜ (backend/routers/agents.py:176-181, 60-75).
  • GET /api/agents/{run_id}/stream: text/event-stream ์‘๋‹ต, _tail_log_file ์ด jsonl ํŒŒ์ผ์„ 0.25s ๊ฐ„๊ฒฉ tail ํ•˜๋ฉฐ ์ƒˆ ๋ผ์ธ์„ ํ๋กœ ํ‘ธ์‹œ, 10s ๋ฌด์ด๋ฒคํŠธ ์‹œ : ping keepalive (agents.py:193-225, 133-173).
  • path traversal ๋ฐฉ์–ด: .., /, \ ํ† ํฐ ๊ฑฐ๋ถ€ (agents.py:68-74, 111-114).
  • ์—์ด์ „ํŠธ 6์ข… ๊ณ ์ • ์ •์˜(planner/builder/qcranker/diversity/critic/reporter) (agents.py:40-47). ๋‹จ, FE AgentMonitor ๊ฐ€ ์†Œ๋น„ํ•˜๋Š” ์—์ด์ „ํŠธ๋Š” status JSON ์˜ 5์ข…(status_emitter.py:58-64)์ด๋ฉฐ ์ด SSE ๋กœ๊ทธ 6์ข…๊ณผ ๋ณ„๊ฐœ โ€” ๋‘ ์—์ด์ „ํŠธ ํ‘œํ˜„์ด ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์Œ(๋ฏธ๊ฒ€์ฆ: FE ๊ฐ€ /api/agents/.../stream ์„ ์‹ค์ œ ๊ตฌ๋…ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๋ณธ ๋ถ„์„ ํŒŒ์ผ ๋ฒ”์œ„์—์„œ ํ™•์ธ ์•ˆ ํ•จ).

E. Mol* 3D ๊ตฌ์กฐ ๋ทฐ์–ด

  • PDB ์„œ๋น™: GET /api/structures/{rel_path} ๊ฐ€ runs/, runs/pyrosetta_flow/, runs_local/, data/ 4๊ฐœ ๋ฒ ์ด์Šค์—์„œ .pdb ๋งŒ ์„œ๋น™ (backend/routers/static.py:20-39). ๋ณด์•ˆ: relative_to ๊ฒฝ๋กœ ํƒˆ์ถœ ์ฐจ๋‹จ + symlink ๊ฑฐ๋ถ€ (static.py:24-31). ์ด๋ฏธ์ง€๋Š” /api/images/{rel_path} (static.py:42-60).
  • Mol* ๋กœ๋”ฉ: MoleculeViewer ์ปดํฌ๋„ŒํŠธ๊ฐ€ PluginContext ์ดˆ๊ธฐํ™” โ†’ mountAsync โ†’ builders.data.download(Asset.Url(pdbUrl)) โ†’ parseTrajectory(..., PdbProvider) โ†’ preset ์ ์šฉ (frontend/src/components/MoleculeViewer.tsx:50-91).
  • 4๊ฐ€์ง€ view mode(Complex/Cartoon/Ball&Stick/Surface)๋ฅผ Mol* preset ์œผ๋กœ ๋งคํ•‘ (MoleculeViewer.tsx:240-265).
  • dark theme ๋ฐฐ๊ฒฝ(0x0f172a), ์นด๋ฉ”๋ผ ๋ฆฌ์…‹, fullscreen, ESC ๋‹ซ๊ธฐ, focus trap ์ ‘๊ทผ์„ฑ (MoleculeViewer.tsx:57, 112-129, 41).
  • pdbUrl ์€ candidate pdb_path(=source) ์—์„œ /api/structures/... ๋กœ ๋ณ€ํ™˜ (SelectivityExplorerPage.tsx:423-447, CandidatePage.tsx:81). ์…€๋ ‰ํ‹ฐ๋น„ํ‹ฐ receptor ๊ตฌ์กฐ๋Š” ๋ณ„๋„ GET /api/selectivity/structure/{name} ๋กœ CIF/PDB ์„œ๋น™ (backend/routers/selectivity.py:96-115).

F. ์…€๋ ‰ํ‹ฐ๋น„ํ‹ฐ ๋ผ์šฐํ„ฐ

  • GET /api/selectivity/receptors: SSTR1/2/3/4/5 CIF ํŒŒ์ผ ์กด์žฌ ์—ฌ๋ถ€ยทํฌ๊ธฐ ๋ฐ˜ํ™˜, 0๊ฐœ ๋กœ๋“œ ์‹œ error ๋กœ๊ทธ (selectivity.py:39-68).
  • POST /api/selectivity/upload: receptor ๊ตฌ์กฐ ํŒŒ์ผ ์—…๋กœ๋“œ (selectivity.py:71-93).
  • ์…€๋ ‰ํ‹ฐ๋น„ํ‹ฐ ๋ถ„์„ job ์€ in-memory _JOBS dict ์— ๋ณด๊ด€, status.py ๊ฐ€ ์™„๋ฃŒ job ์˜ margin ์„ candidate ์— ๋จธ์ง€ (selectivity.py:120, status.py:39-57).

โ‘ก ์˜ํ–ฅ (Impact)

  • ๋‹จ์ผ SSOT ํŒจํ„ด์˜ ์žฅ์ : ์ปดํ“จํŠธ ๋ ˆ์ด์–ด(ํŒŒ์ดํ”„๋ผ์ธ ์›Œ์ปค)์™€ ํ‘œํ˜„ ๋ ˆ์ด์–ด(์›น)๊ฐ€ JSON ํŒŒ์ผ 1๊ฐœ๋กœ๋งŒ ๊ฒฐํ•ฉ โ†’ ๋ฐฑ์—”๋“œ๊ฐ€ ์ฃฝ์–ด๋„ ํŒŒ์ผ์ด ๋‚จ๊ณ , ํŒŒ์ดํ”„๋ผ์ธ์ด ์ฃฝ์–ด๋„ ๋งˆ์ง€๋ง‰ ์ƒํƒœ๊ฐ€ ๋ณด์กด๋จ. flock ์›์ž ์“ฐ๊ธฐ๋กœ ๋™์‹œ์„ฑ ์•ˆ์ „ (status_emitter.py:117-133, state.py:71-90).
  • ํด๋ง ๋น„์šฉ: /api/status(2s) + /api/runs(2s) + /api/experiment/status(3s) ๊ฐ€ ํด๋ผ์ด์–ธํŠธ๋งˆ๋‹ค ์ƒ์‹œ ๋ฐœ์ƒ. mtime ์บ์‹œ๋กœ ํŒŒ์ผ ํŒŒ์‹ฑ์€ ์ ˆ์•ฝ๋˜๋‚˜ HTTP ์™•๋ณตยทenrichment ๊ณ„์‚ฐ(_enrich_candidates)์€ ๋งค ํด๋ง๋งˆ๋‹ค ์ˆ˜ํ–‰ (status.py:60-164) โ€” ํ›„๋ณด ์ˆ˜ ์ฆ๊ฐ€ ์‹œ ์‘๋‹ต ์ง€์—ฐ ๊ฐ€๋Šฅ(๋ฏธ๊ฒ€์ฆ: ๋ถ€ํ•˜ ์ธก์ • ์—†์Œ).
  • ์•„์นด์ด๋ธŒ ๊ธฐ๋ฐ˜ ํšŒ๊ณ : ์™„๋ฃŒ run ์ด ์ž๋™์œผ๋กœ archives/{run_id}_dashboard.json + PDB ์‚ฌ๋ณธ์œผ๋กœ ๋ณด์กด๋˜์–ด ๊ณผ๊ฑฐ run ์žฌํ˜„ ๊ฐ€๋Šฅ (status_emitter.py:397-425, experiment.py:34-70).
  • ๋ฌดํ•œ ๋ฐœ๊ตด ์—”์ง„๊ณผ์˜ ๋‹จ์ ˆ(์ค‘๋Œ€): ์•„๋ž˜ โ‘ฃ ์ฐธ์กฐ โ€” ๋ณธ UI ๋Š” /api/experiment/*(์œ ํ•œ run) ๋งŒ ์ œ์–ดํ•˜๋ฉฐ, ๋ฌดํ•œ ๋ฐœ๊ตด ์—”์ง„(pyrosetta_flow/continuous.py)์˜ ์ถœ๋ ฅ์„ ์ „ํ˜€ ํ‘œ์‹œยท์ œ์–ดํ•˜์ง€ ๋ชปํ•จ.

โ‘ข ๊ด€๋ จ Action Item

๊ทผ๊ฑฐ: MEMORY.md โ€” sstr2-ui-goal ๋ฉ”๋ชจ๋ฆฌ("์ฐจ๊ธฐ GOAL(๋ฏธ์ฐฉ์ˆ˜): ๋ฌดํ•œ์—”์ง„์šฉ ํ’€ ์›น ๋ชจ๋‹ˆํ„ฐ๋งยท์ œ์–ด UI"), UI goal ํ”„๋กฌํ”„ํŠธ _workspace/GOAL_UI_2026-06-10.md.

  1. [UI ์—ฐ๋™, ์ตœ์šฐ์„ ] ๋ฌดํ•œ์—”์ง„ โ†” ์›น ์—ฐ๊ฒฐ: ๋ฐฑ์—”๋“œ์— /api/discovery/* ๋ผ์šฐํ„ฐ๋ฅผ ์‹ ์„คํ•˜์—ฌ runs/pyrosetta_flow/discovery_status.json / global_selectivity_leaderboard.json ์„ ์„œ๋น™ํ•˜๊ณ , FE ํด๋ง ํ›…(useDiscovery ๋“ฑ)์„ ์ถ”๊ฐ€. (ํ˜„์žฌ 0๊ฐœ โ€” โ‘ฃ ์ฐธ์กฐ)
  2. [UI ์—ฐ๋™] ๋ฌดํ•œ์—”์ง„ ์ œ์–ด: _workspace/discovery_control.json ์„ ํ†ตํ•œ ์‹œ์ž‘/์ •์ง€/ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ • ์—”๋“œํฌ์ธํŠธ. (ํ˜„์žฌ ํŒŒ์ผ๋งŒ ์กด์žฌ, ์›น ์ œ์–ด ๊ฒฝ๋กœ ์—†์Œ)
  3. [UI goal Phase] ์ „๋ฉด ์žฌ๋””์ž์ธ: UI goal(L3)์ด "๋ฌดํ•œ์—”์ง„ ์—ฐ๊ฒฐ + ์ „๋ฉด ์žฌ๋””์ž์ธ"์„ ํ•จ๊ป˜ ์š”๊ตฌ โ€” ํ˜„์žฌ 13๋‹จ๊ณ„ ์œ ํ•œ ํŒŒ์ดํ”„๋ผ์ธ ์‹œ๊ฐํ™”(status_emitter.py:42-56)๊ฐ€ ๋ฌดํ•œ epoch ๋ชจ๋ธ๊ณผ ์ •๋ณด๊ตฌ์กฐ๊ฐ€ ๋งž์ง€ ์•Š์Œ.
  4. [๋ฆฌํŒฉํ† ๋ง ํ›„๋ณด] ์—์ด์ „ํŠธ ํ‘œํ˜„ ์ด์›ํ™” ํ•ด์†Œ: status JSON 5-์—์ด์ „ํŠธ(status_emitter.py:58-64) vs /api/agents SSE 6-์—์ด์ „ํŠธ(agents.py:40-47) ํ†ตํ•ฉ ๊ฒ€ํ† .
  5. [์„ฑ๋Šฅ] ํด๋ง โ†’ SSE ํ†ตํ•ฉ ๊ฒ€ํ† : ๊ธฐ์กด SSE ์ธํ”„๋ผ(agents.py:193-225)๋ฅผ status ์ „๋ฐ˜์œผ๋กœ ํ™•์žฅํ•˜๋ฉด 3์ค‘ ํด๋ง ์ œ๊ฑฐ ๊ฐ€๋Šฅ(usePipelineStatus.ts:238-245 ์˜ TODO ์ฃผ์„๊ณผ ์ผ์น˜).

โ‘ฃ ์™„์„ฑ๋„ ์ถ”์ •: ์•ฝ 70% (์œ ํ•œ ํŒŒ์ดํ”„๋ผ์ธ UI ๊ธฐ์ค€ ํ’๋ถ€, ๋ฌดํ•œ์—”์ง„ ์—ฐ๋™ 0%)

๊ทผ๊ฑฐ โ€” ํ’๋ถ€ํ•œ ๋ถ€๋ถ„ (์™„์„ฑ)

  • ๋ฐฑ์—”๋“œ ๋ผ์šฐํ„ฐ 20+๊ฐœ ๋“ฑ๋ก(main.py:113-133): status/experiment/agents/selectivity/static ์™ธ admet/analysis/validation/cluster/stability/benchmark/wetlab/flexpepdock/binding_pocket/strategies/silo_a ๋“ฑ.
  • ํ”„๋ก ํŠธ 15๊ฐœ ํŽ˜์ด์ง€ ๋ผ์šฐํŠธ + 40+ ์ปดํฌ๋„ŒํŠธ (frontend/src/App.tsx:277-364, frontend/src/components/ ๋‹ค์ˆ˜).
  • ํ•ต์‹ฌ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ธฐ๋Šฅ ์ž‘๋™: StatusEmitter ์›์ž ์“ฐ๊ธฐ, mtime ์บ์‹œ ํด๋ง, candidate 6-field enrichment, ์•„์นด์ด๋ธŒ ์ž๋™ ๋ณด์กด, Mol* 4-mode 3D ๋ทฐ์–ด, ์—์ด์ „ํŠธ SSE ์ŠคํŠธ๋ฆผ, ์…€๋ ‰ํ‹ฐ๋น„ํ‹ฐ receptor ๊ด€๋ฆฌ, ์‹คํ—˜ start/stop/watchdog.
  • ํ…Œ์ŠคํŠธ ์ž์‚ฐ ์กด์žฌ: backend/tests/ 18๊ฐœ, frontend/src/**/__tests__/ 10+๊ฐœ.

๊ทผ๊ฑฐ โ€” ๋ฏธ์™„์„ฑ / ๋‹จ์ ˆ (ํ•ต์‹ฌ ๊ฐ์ )

  • ๋ฌดํ•œ์—”์ง„ ๋ฏธ์—ฐ๊ฒฐ (๊ฒ€์ฆ ์™„๋ฃŒ):
  • grep -rn "discovery" backend/ โ†’ 0๊ฑด.
  • grep -rn "discovery" frontend/src/ โ†’ 0๊ฑด.
  • grep -rn "api/discovery" (py/ts/tsx) โ†’ 0๊ฑด.
  • ์ฆ‰ backend/main.py ๋ผ์šฐํ„ฐ ๋ชฉ๋ก(main.py:113-133)์— /api/discovery/* ์—†์Œ.
  • ๋ฐ˜๋ฉด ๋ฌดํ•œ์—”์ง„ ์ถœ๋ ฅ ํŒŒ์ผ์€ ์‹ค์žฌ: runs/pyrosetta_flow/discovery_status.json(21KB, ํ‚ค: running, epochs_done, elapsed_seconds, global_best_delta_margin, passing_count, diversity_level, top, last_epoch, history), runs/pyrosetta_flow/global_selectivity_leaderboard.json(25KB), ์ œ์–ด ํŒŒ์ผ _workspace/discovery_control.json โ€” ๋ชจ๋‘ ์–ด๋–ค ๋ผ์šฐํ„ฐ๋„ ์ฝ์ง€ ์•Š์Œ.
  • ์—”์ง„ ๋ณธ์ฒด pyrosetta_flow/continuous.py ๋Š” ์กด์žฌ(์›น๊ณผ ๋…๋ฆฝ ์‹คํ–‰).
  • ๊ฒฐ๋ก : ๋ฌดํ•œ ๋ฐœ๊ตด ์—”์ง„์€ ์›น UI ์™€ ์™„์ „ํžˆ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์œผ๋ฉฐ, ํ˜„์žฌ ์›น์—์„œ ๋ฌดํ•œ์—”์ง„ ์ƒํƒœ๋ฅผ ๋ณด๊ฑฐ๋‚˜ ์ œ์–ดํ•  ๋ฐฉ๋ฒ•์ด ์—†๋‹ค.
  • MEMORY.md ๊ฐ€ UI goal ์„ "์ฐจ๊ธฐ GOAL(๋ฏธ์ฐฉ์ˆ˜)"๋กœ ๋ช…์‹œ.

โ‘ค ํ•™์ˆ ์  ๊ฐ€์น˜ (๊ณผํ•™ ๋ชจ๋‹ˆํ„ฐ๋ง UX)

  • ๊ณ„์‚ฐ๊ณผํ•™ ์žฌํ˜„์„ฑ UX: run โ†’ ์ž๋™ ์•„์นด์ด๋ธŒ(JSON+PDB ์‚ฌ๋ณธ) โ†’ switchRun ์œผ๋กœ ๊ณผ๊ฑฐ run ์žฌ์กฐํšŒ(usePipelineStatus.ts:298-326, status_emitter.py:397-425)๋Š” in-silico ์‹คํ—˜์˜ ์žฌํ˜„์„ฑยท๊ฐ์‚ฌ ์ถ”์ ์„ UI ๋ ˆ๋ฒจ์—์„œ ๊ตฌํ˜„ํ•œ ์‚ฌ๋ก€.
  • ํœด๋ฆฌ์Šคํ‹ฑ ์ •์ง์„ฑ: enrichment 6ํ•„๋“œ ์ค‘ sequence-only ์ถ”์ •์น˜(fwkt_contact, chelator_site)๋ฅผ HEURISTIC ์œผ๋กœ ๋ผ๋ฒจ๋ง(status.py:73-76)ํ•˜๊ณ , FE ๊ฐ€ fail reason ์„ ๊ณผํ•™์  ํ•œ๊ตญ์–ด ์„ค๋ช…์œผ๋กœ ๋ณ€ํ™˜(CandidateTable.tsx:13-54) โ€” "๊ณ„์‚ฐ ๋ถˆ๊ฐ€๋Šฅ์„ ๊ฐ€๋Šฅํ•œ ์ฒ™ํ•˜์ง€ ์•Š๋Š”๋‹ค"๋Š” ํ”„๋กœ์ ํŠธ ์›์น™(CLAUDE.md VR-cycle-09)์˜ UX ๊ตฌํ˜„.
  • ๋‹ค์ค‘ ํ‘œํ˜„ ๋ถ„์ž ์‹œ๊ฐํ™”: Mol* 4-mode(complex/cartoon/ball&stick/surface) preset(MoleculeViewer.tsx:240-265)์œผ๋กœ SSTR2-ํŽฉํƒ€์ด๋“œ ๋ณตํ•ฉ์ฒด์˜ ๊ฒฐํ•ฉ ์ธํ„ฐํŽ˜์ด์Šค ๊ฒ€ํ†  ์ง€์›.
  • ์—์ด์ „ํŠธ ์˜์‚ฌ๊ฒฐ์ • ํˆฌ๋ช…์„ฑ: planner hypothesis / critic proposed_changes(oldโ†’new+rationale) / reporter summary ๋ฅผ ํŒจ๋„๋กœ ๋…ธ์ถœ(AgentMonitor.tsx:119-191) โ€” agentic ๊ณผํ•™ ํŒŒ์ดํ”„๋ผ์ธ์˜ ์˜์‚ฌ๊ฒฐ์ • ๊ณผ์ • ๊ฐ€์‹œํ™”.

โ‘ฅ ์‚ฌ์šฉ๋ฒ• (๊ธฐ๋™)

๋ฏธ๊ฒ€์ฆ: ์•„๋ž˜ ๋ช…๋ น์€ backend/main.py:7, frontend/package.json:7, vite.config.ts:14-21 ๊ธฐ๋ฐ˜ ์ถ”๋ก . ์‹ค์ œ ์‹คํ–‰ ๋กœ๊ทธ๋Š” ๋ณธ ๋ถ„์„ ๋ฒ”์œ„ ๋ฐ–.

๋ฐฑ์—”๋“œ (ํฌํŠธ 8787):

# ๋ ˆํฌ ๋ฃจํŠธ(ai4sci-kaeri)์—์„œ
uvicorn backend.main:app --host 0.0.0.0 --port 8787 --reload   # backend/main.py:7
# ๋˜๋Š”
python -m backend.main                                          # backend/main.py:142-144
  • ์ƒํƒœ ํŒŒ์ผ ๊ฒฝ๋กœ override: PIPELINE_STATUS_FILE=/tmp/pipeline_local_status.json (state.py:40-45).
  • ๋ชจ๋ธ ๋ชฉ๋ก์€ vLLM localhost:8000 ๊ฐ€ ๋–  ์žˆ์–ด์•ผ ์ •์ƒ(์—†์œผ๋ฉด ollamaโ†’default ํด๋ฐฑ) (experiment.py:163-189).

ํ”„๋ก ํŠธ์—”๋“œ (ํฌํŠธ 5173, Vite dev):

cd frontend
npm install
npm run dev          # vite โ€” package.json:7
  • Vite dev ์„œ๋ฒ„๊ฐ€ /api ์š”์ฒญ์„ http://127.0.0.1:8787 ๋กœ ํ”„๋ก์‹œ (vite.config.ts:14-21).
  • ํ”„๋กœ๋•์…˜: npm run build โ†’ npm run preview (package.json:8,10).

๋ธŒ๋ผ์šฐ์ €: http://localhost:5173 ์ ‘์† โ†’ / ๊ฐ€ /console ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (App.tsx:278).


โ‘ฆ ํ•„์š” ์ด์œ  (Why)

  • agentic in-silico ์Šคํฌ๋ฆฌ๋‹์€ ์ˆ˜์‹ญ~์ˆ˜๋ฐฑ epoch/iterationยท๋‹ค์ˆ˜ ํ›„๋ณด๋ฅผ ์ƒ์„ฑ โ†’ CLI ๋กœ๊ทธ๋งŒ์œผ๋กœ๋Š” ์ˆ˜๋ ด ์ถ”์„ธยทํ›„๋ณด ๋žญํ‚นยท๊ฒŒ์ดํŠธ ํ†ต๊ณผ์œจยท3D ๊ฒฐํ•ฉ ๊ตฌ์กฐ๋ฅผ ์‹ค์‹œ๊ฐ„ ํŒŒ์•… ๋ถˆ๊ฐ€. ์›น UI ๋Š” ์ด๋ฅผ ๋‹จ์ผ ํ™”๋ฉด ๋ชจ๋‹ˆํ„ฐ๋ง์œผ๋กœ ์••์ถ•.
  • ํŒŒ์ดํ”„๋ผ์ธ(์ปดํ“จํŠธ, bio-tools conda)๊ณผ ํ‘œํ˜„(์›น)์„ JSON ํŒŒ์ผ๋กœ ๋””์ปคํ”Œ๋ง(status_emitter.py:30-34)ํ•จ์œผ๋กœ์จ, GPU ์›Œ์ปค ํ™˜๊ฒฝ๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ์–ด๋””์„œ๋“  ๋ธŒ๋ผ์šฐ์ €๋กœ ๊ด€์ฐฐยท์ œ์–ด(start/stop) ๊ฐ€๋Šฅ.
  • ๊ทธ๋Ÿฌ๋‚˜ โ‘ฃ์—์„œ ํ™•์ธํ–ˆ๋“ฏ ๋ฌดํ•œ ๋ฐœ๊ตด ์—”์ง„(ํ˜„ ํ”„๋กœ์ ํŠธ์˜ ํ•ต์‹ฌ ์‚ฐ์ถœ๋ฌผ)์€ ์•„์ง ์ด ๋ชจ๋‹ˆํ„ฐ๋ง ์ฒด๊ณ„ ๋ฐ–์— ์žˆ์–ด, ๋ฌดํ•œ์—”์ง„์„ ์›น์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒƒ์ด UI goal ์˜ ์กด์žฌ ์ด์œ ์ด์ž ๋‹ค์Œ ์šฐ์„ ์ˆœ์œ„๋‹ค.

๊ฒ€์ฆ ์ธ์šฉ ๋ชฉ๋ก (file_path:line)

๋ฐฑ์—”๋“œ - backend/main.py:7 โ€” uvicorn ๊ธฐ๋™ ์ปค๋งจ๋“œ (docstring) - backend/main.py:45-135 โ€” create_app, ๋ผ์šฐํ„ฐ ๋“ฑ๋ก 20+๊ฐœ - backend/main.py:65-70 โ€” CORS (5173/8787) - backend/main.py:73-108 โ€” ํ‘œ์ค€ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ - backend/main.py:113-133 โ€” ๋ผ์šฐํ„ฐ ๋ชฉ๋ก(discovery ๋ถ€์žฌ ๊ทผ๊ฑฐ) - backend/main.py:139,142-144 โ€” module-level app, main - backend/status_emitter.py:30-34 โ€” STATUS_FILE ๊ฒฝ๋กœ(/tmp/pipeline_local_status.json) - backend/status_emitter.py:42-56 โ€” DEFAULT_STEPS 13๊ฐœ(์œ ํ•œ ํŒŒ์ดํ”„๋ผ์ธ) - backend/status_emitter.py:58-64 โ€” DEFAULT_AGENTS 5๊ฐœ - backend/status_emitter.py:91-114 โ€” ์ƒํƒœ dict ์Šคํ‚ค๋งˆ - backend/status_emitter.py:117-133 โ€” flush() flock ์›์ž ์“ฐ๊ธฐ - backend/status_emitter.py:199-224 โ€” set_candidates ๋ณ‘ํ•ฉยทddG ์žฌ๋žญํฌ - backend/status_emitter.py:397-425 โ€” _save_archive(PDB ์‚ฌ๋ณธ ํฌํ•จ) - backend/state.py:40-49 โ€” STATUS_FILE/ARCHIVE_DIR env - backend/state.py:56 โ€” PORT 8787 - backend/state.py:71-90 โ€” atomic_write_json - backend/state.py:101-124 โ€” _with_runtime_fields(is_active_run/server_time) - backend/state.py:127-153 โ€” read_status mtime ์บ์‹œ - backend/routers/status.py:39-57 โ€” selectivity margin lookup - backend/routers/status.py:60-164 โ€” _enrich_candidates 6ํ•„๋“œ(ํœด๋ฆฌ์Šคํ‹ฑ ๋ผ๋ฒจ 73-76) - backend/routers/status.py:157-164 โ€” GET /api/status - backend/routers/status.py:187-225 โ€” /api/runs, /api/runs/{run_id} - backend/routers/experiment.py:30-31,112-142 โ€” watchdog - backend/routers/experiment.py:34-70,296-317 โ€” _auto_archive - backend/routers/experiment.py:84-98,273 โ€” initializing status - backend/routers/experiment.py:163-189 โ€” /experiment/models(vLLMโ†’ollamaโ†’default) - backend/routers/experiment.py:205-293 โ€” POST /experiment/run(subprocess) - backend/routers/agents.py:40-47 โ€” SSE ์—์ด์ „ํŠธ 6์ข… - backend/routers/agents.py:60-75,111-114 โ€” ๋กœ๊ทธ ๊ฒฝ๋กœยทpath traversal ๋ฐฉ์–ด - backend/routers/agents.py:133-173 โ€” _tail_log_file - backend/routers/agents.py:176-181 โ€” GET /agents/{run_id}/log - backend/routers/agents.py:193-225 โ€” GET /agents/{run_id}/stream (SSE) - backend/routers/static.py:20-39 โ€” GET /api/structures/{rel_path} (.pdb) - backend/routers/static.py:42-60 โ€” GET /api/images/{rel_path} - backend/routers/selectivity.py:39-68 โ€” /selectivity/receptors - backend/routers/selectivity.py:71-93 โ€” /selectivity/upload - backend/routers/selectivity.py:96-115 โ€” /selectivity/structure/{name} (CIF/PDB) - backend/routers/selectivity.py:120 โ€” _JOBS in-memory

ํ”„๋ก ํŠธ์—”๋“œ - frontend/src/hooks/usePipelineStatus.ts:170-236 โ€” parseStatusData(snakeโ†’camel) - frontend/src/hooks/usePipelineStatus.ts:238-245 โ€” ํด๋ง ์ค‘๋ณต NOTE - frontend/src/hooks/usePipelineStatus.ts:262-296 โ€” fetchLiveStatus(/api/status) - frontend/src/hooks/usePipelineStatus.ts:274-283 โ€” no_status_file ๋ฆฌ์…‹ - frontend/src/hooks/usePipelineStatus.ts:298-326 โ€” switchRun(/api/runs/{id}) - frontend/src/hooks/usePipelineStatus.ts:329-343 โ€” 2s setInterval ํด๋ง - frontend/src/hooks/useExperiment.ts:53-141 โ€” config/models/start/stop/3s ํด๋ง - frontend/src/components/MoleculeViewer.tsx:50-91 โ€” Mol* init/PDB ๋กœ๋“œ - frontend/src/components/MoleculeViewer.tsx:240-265 โ€” view mode preset ๋งคํ•‘ - frontend/src/components/AgentMonitor.tsx:119-191 โ€” ReportPanel(plan/critic/reporter) - frontend/src/components/CandidateTable.tsx:13-54 โ€” humanizeFailReason(๊ณผํ•™์  ์„ค๋ช…) - frontend/src/components/ConvergenceGraph.tsx:56-60 โ€” ๋นˆ ๋ฐ์ดํ„ฐ ๊ฐ€๋“œ - frontend/src/pages/SelectivityExplorerPage.tsx:423-447 โ€” pdb_pathโ†’/api/structures URL - frontend/src/App.tsx:277-364 โ€” 15๊ฐœ ๋ผ์šฐํŠธ - frontend/vite.config.ts:14-21 โ€” /api โ†’ 127.0.0.1:8787 ํ”„๋ก์‹œ - frontend/package.json:7,8,10 โ€” dev/build/preview ์Šคํฌ๋ฆฝํŠธ - frontend/package.json:20 โ€” molstar ^5.6.1

๋ฌดํ•œ์—”์ง„ ๋ฏธ์—ฐ๊ฒฐ ๊ทผ๊ฑฐ (grep/ls ๊ฒ€์ฆ) - grep -rn "discovery" backend/ โ†’ 0๊ฑด - grep -rn "discovery" frontend/src/ โ†’ 0๊ฑด - grep -rn "api/discovery" (py/ts/tsx) โ†’ 0๊ฑด - runs/pyrosetta_flow/discovery_status.json (21KB, ์‹ค์žฌ, ๋ฏธ์†Œ๋น„) - runs/pyrosetta_flow/global_selectivity_leaderboard.json (25KB, ์‹ค์žฌ, ๋ฏธ์†Œ๋น„) - _workspace/discovery_control.json (์‹ค์žฌ, ์›น ์ œ์–ด ๊ฒฝ๋กœ ์—†์Œ) - pyrosetta_flow/continuous.py (์—”์ง„ ๋ณธ์ฒด, ์›น ๋…๋ฆฝ ์‹คํ–‰) - MEMORY.md sstr2-ui-goal โ€” UI goal "๋ฏธ์ฐฉ์ˆ˜" ๋ช…์‹œ