591 lines
21 KiB
Python
591 lines
21 KiB
Python
"""
|
|
GUARDiA ITSM — AI 에이전트 관리 API
|
|
|
|
엔드포인트:
|
|
GET /api/agents 에이전트 목록
|
|
POST /api/agents 에이전트 생성
|
|
GET /api/agents/{id} 에이전트 상세
|
|
PATCH /api/agents/{id} 에이전트 수정
|
|
DELETE /api/agents/{id} 에이전트 삭제
|
|
POST /api/agents/{id}/heartbeat 수동 하트비트 실행
|
|
POST /api/agents/{id}/pause 일시 중지
|
|
POST /api/agents/{id}/resume 재개
|
|
GET /api/agents/{id}/tasks 태스크 목록
|
|
POST /api/agents/{id}/tasks 태스크 직접 생성 (Developer 에이전트용)
|
|
GET /api/agents/approvals 전체 승인 대기 목록
|
|
PATCH /api/agents/approvals/{id}/review 승인 또는 거부
|
|
GET /api/agents/stats 전체 에이전트 통계
|
|
GET /api/agents/orgchart 조직도 반환 (Phase 4 대시보드용)
|
|
GET /api/agents/llm/health Ollama 헬스체크
|
|
GET /api/agents/llm/models 설치된 모델 목록
|
|
POST /api/agents/llm/pull 모델 다운로드 시작
|
|
|
|
RBAC: CUSTOMER 역할 전체 차단.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, date
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
|
from sqlalchemy import select, func, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database import get_db
|
|
from models import (
|
|
User, UserRole,
|
|
AgentConfig, AgentTask, AgentApproval,
|
|
AgentRole, AgentStatus, LLMProvider, AgentTaskStatus, AgentApprovalStatus,
|
|
AgentConfigOut, AgentConfigCreate, AgentConfigUpdate,
|
|
AgentTaskOut, AgentTaskCreate,
|
|
AgentApprovalOut, AgentApprovalReview,
|
|
AgentStatsOut, AgentOrgNode,
|
|
)
|
|
from routers.auth import get_current_user
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/agents", tags=["agents"])
|
|
|
|
|
|
# ── 공통 헬퍼 ────────────────────────────────────────────────────────────────
|
|
|
|
def _check_access(user: User) -> None:
|
|
if user.role == UserRole.CUSTOMER:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="에이전트 관리는 운영 계정만 접근 가능합니다.",
|
|
)
|
|
|
|
|
|
async def _get_agent_or_404(agent_id: int, db: AsyncSession) -> AgentConfig:
|
|
agent = (await db.execute(
|
|
select(AgentConfig).where(AgentConfig.id == agent_id)
|
|
)).scalars().first()
|
|
if not agent:
|
|
raise HTTPException(status_code=404, detail=f"에이전트 ID {agent_id} 없음")
|
|
return agent
|
|
|
|
|
|
# ── 에이전트 CRUD ────────────────────────────────────────────────────────────
|
|
|
|
@router.get("", response_model=List[AgentConfigOut])
|
|
async def list_agents(
|
|
role: Optional[str] = None,
|
|
active_only: bool = False,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""에이전트 목록 조회."""
|
|
_check_access(current_user)
|
|
q = select(AgentConfig)
|
|
if role:
|
|
q = q.where(AgentConfig.role == role)
|
|
if active_only:
|
|
q = q.where(AgentConfig.is_active.is_(True))
|
|
q = q.order_by(AgentConfig.role, AgentConfig.name)
|
|
return (await db.execute(q)).scalars().all()
|
|
|
|
|
|
@router.post("", response_model=AgentConfigOut, status_code=status.HTTP_201_CREATED)
|
|
async def create_agent(
|
|
body: AgentConfigCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""에이전트 등록."""
|
|
_check_access(current_user)
|
|
if current_user.role not in (UserRole.ADMIN, UserRole.PM):
|
|
raise HTTPException(status_code=403, detail="에이전트 생성은 ADMIN/PM만 가능합니다.")
|
|
|
|
agent = AgentConfig(
|
|
name=body.name,
|
|
role=body.role,
|
|
description=body.description,
|
|
llm_provider=body.llm_provider,
|
|
llm_model=body.llm_model,
|
|
system_prompt=body.system_prompt,
|
|
heartbeat_cron=body.heartbeat_cron,
|
|
is_active=body.is_active,
|
|
status=AgentStatus.IDLE,
|
|
created_by=current_user.id,
|
|
)
|
|
db.add(agent)
|
|
await db.commit()
|
|
await db.refresh(agent)
|
|
|
|
# 하트비트 크론 등록 (APScheduler)
|
|
if agent.heartbeat_cron:
|
|
_register_heartbeat(agent)
|
|
|
|
logger.info("에이전트 생성: id=%d role=%s user=%s", agent.id, agent.role, current_user.username)
|
|
return agent
|
|
|
|
|
|
@router.get("/stats", response_model=AgentStatsOut)
|
|
async def get_stats(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""에이전트 전체 통계 — Phase 4 대시보드용."""
|
|
_check_access(current_user)
|
|
|
|
total = (await db.execute(select(func.count()).select_from(AgentConfig))).scalar() or 0
|
|
active = (await db.execute(
|
|
select(func.count()).select_from(AgentConfig).where(AgentConfig.is_active.is_(True))
|
|
)).scalar() or 0
|
|
|
|
today_start = datetime.combine(date.today(), datetime.min.time())
|
|
tasks_today = (await db.execute(
|
|
select(func.count()).select_from(AgentTask)
|
|
.where(AgentTask.created_at >= today_start)
|
|
)).scalar() or 0
|
|
|
|
tokens_today = (await db.execute(
|
|
select(func.coalesce(func.sum(AgentTask.tokens_used), 0))
|
|
.where(AgentTask.created_at >= today_start)
|
|
)).scalar() or 0
|
|
|
|
pending_approvals = (await db.execute(
|
|
select(func.count()).select_from(AgentApproval)
|
|
.where(AgentApproval.status == AgentApprovalStatus.PENDING)
|
|
)).scalar() or 0
|
|
|
|
from core.llm_client import get_llm_client
|
|
llm_online = await get_llm_client().health_check()
|
|
|
|
return AgentStatsOut(
|
|
total_agents=total,
|
|
active_agents=active,
|
|
total_tasks_today=tasks_today,
|
|
total_tokens_today=tokens_today,
|
|
pending_approvals=pending_approvals,
|
|
llm_online=llm_online,
|
|
)
|
|
|
|
|
|
@router.get("/orgchart", response_model=List[AgentOrgNode])
|
|
async def get_orgchart(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""에이전트 조직도 — Phase 4 대시보드용."""
|
|
_check_access(current_user)
|
|
|
|
agents = (await db.execute(
|
|
select(AgentConfig).where(AgentConfig.is_active.is_(True))
|
|
)).scalars().all()
|
|
|
|
# 조직 계층: CEO → CTO/PM_AGENT → DEV/QA
|
|
hierarchy = {
|
|
AgentRole.CEO: [],
|
|
AgentRole.CTO: [AgentRole.DEVELOPER, AgentRole.QA],
|
|
AgentRole.PM_AGENT: [],
|
|
AgentRole.DEVELOPER: [],
|
|
AgentRole.QA: [],
|
|
AgentRole.INCIDENT_TRIAGE: [],
|
|
AgentRole.KB_CURATOR: [],
|
|
AgentRole.SSL_WATCHER: [],
|
|
AgentRole.WBS_MONITOR: [],
|
|
AgentRole.PM_SUGGESTER: [],
|
|
}
|
|
ceo_children = [AgentRole.CTO, AgentRole.PM_AGENT,
|
|
AgentRole.INCIDENT_TRIAGE, AgentRole.KB_CURATOR,
|
|
AgentRole.SSL_WATCHER, AgentRole.WBS_MONITOR, AgentRole.PM_SUGGESTER]
|
|
|
|
def build_node(a: AgentConfig) -> AgentOrgNode:
|
|
child_roles = hierarchy.get(a.role, [])
|
|
child_agents = [x for x in agents if x.role in [r.value for r in child_roles]]
|
|
return AgentOrgNode(
|
|
id=a.id,
|
|
name=a.name,
|
|
role=a.role,
|
|
status=a.status,
|
|
children=[build_node(c) for c in child_agents],
|
|
)
|
|
|
|
ceo_agents = [a for a in agents if a.role == AgentRole.CEO]
|
|
return [build_node(a) for a in ceo_agents] if ceo_agents else [
|
|
AgentOrgNode(id=0, name="(CEO 미등록)", role="CEO", status="IDLE")
|
|
]
|
|
|
|
|
|
@router.get("/approvals", response_model=List[AgentApprovalOut])
|
|
async def list_approvals(
|
|
pending_only: bool = True,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""승인 대기 목록."""
|
|
_check_access(current_user)
|
|
q = select(AgentApproval).order_by(AgentApproval.requested_at.desc())
|
|
if pending_only:
|
|
q = q.where(AgentApproval.status == AgentApprovalStatus.PENDING)
|
|
return (await db.execute(q.limit(50))).scalars().all()
|
|
|
|
|
|
@router.patch("/approvals/{approval_id}/review", response_model=AgentApprovalOut)
|
|
async def review_approval(
|
|
approval_id: int,
|
|
body: AgentApprovalReview,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""승인 또는 거부."""
|
|
_check_access(current_user)
|
|
approval = (await db.execute(
|
|
select(AgentApproval).where(AgentApproval.id == approval_id)
|
|
)).scalars().first()
|
|
if not approval:
|
|
raise HTTPException(status_code=404, detail="승인 항목 없음")
|
|
if approval.status != AgentApprovalStatus.PENDING:
|
|
raise HTTPException(status_code=400, detail="이미 처리된 항목입니다.")
|
|
|
|
approval.status = (
|
|
AgentApprovalStatus.APPROVED if body.approved else AgentApprovalStatus.REJECTED
|
|
)
|
|
approval.notes = body.notes
|
|
approval.reviewed_by = current_user.id
|
|
approval.reviewed_at = datetime.now()
|
|
await db.commit()
|
|
await db.refresh(approval)
|
|
|
|
logger.info(
|
|
"승인 처리: approval_id=%d status=%s user=%s",
|
|
approval_id, approval.status, current_user.username,
|
|
)
|
|
return approval
|
|
|
|
|
|
# ── LLM 헬스·모델 관리 ────────────────────────────────────────────────────
|
|
|
|
@router.get("/llm/health")
|
|
async def llm_health(current_user: User = Depends(get_current_user)):
|
|
"""Ollama 서버 상태 확인."""
|
|
_check_access(current_user)
|
|
from core.llm_client import get_llm_client
|
|
client = get_llm_client()
|
|
online = await client.health_check()
|
|
models = await client.list_models() if online else []
|
|
return {
|
|
"online": online,
|
|
"base_url": client.base_url,
|
|
"models": [{"name": m.name, "size_gb": round(m.size / 1e9, 1)} for m in models],
|
|
}
|
|
|
|
|
|
@router.post("/llm/pull")
|
|
async def pull_model(
|
|
model: str,
|
|
background_tasks: BackgroundTasks,
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Ollama 모델 다운로드 (백그라운드)."""
|
|
_check_access(current_user)
|
|
if current_user.role != UserRole.ADMIN:
|
|
raise HTTPException(status_code=403, detail="모델 다운로드는 ADMIN만 가능합니다.")
|
|
|
|
from core.llm_client import get_llm_client
|
|
background_tasks.add_task(get_llm_client().pull_model, model)
|
|
return {"message": f"모델 '{model}' 다운로드 시작 (백그라운드)", "model": model}
|
|
|
|
|
|
# ── 에이전트 상세·수정·삭제 ───────────────────────────────────────────────
|
|
|
|
@router.get("/{agent_id}", response_model=AgentConfigOut)
|
|
async def get_agent(
|
|
agent_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
_check_access(current_user)
|
|
return await _get_agent_or_404(agent_id, db)
|
|
|
|
|
|
@router.patch("/{agent_id}", response_model=AgentConfigOut)
|
|
async def update_agent(
|
|
agent_id: int,
|
|
body: AgentConfigUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
_check_access(current_user)
|
|
agent = await _get_agent_or_404(agent_id, db)
|
|
for k, v in body.model_dump(exclude_none=True).items():
|
|
setattr(agent, k, v)
|
|
await db.commit()
|
|
await db.refresh(agent)
|
|
return agent
|
|
|
|
|
|
@router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_agent(
|
|
agent_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
if current_user.role != UserRole.ADMIN:
|
|
raise HTTPException(status_code=403, detail="ADMIN만 에이전트 삭제 가능합니다.")
|
|
agent = await _get_agent_or_404(agent_id, db)
|
|
await db.delete(agent)
|
|
await db.commit()
|
|
|
|
|
|
# ── 하트비트·제어 ────────────────────────────────────────────────────────────
|
|
|
|
@router.post("/{agent_id}/heartbeat")
|
|
async def manual_heartbeat(
|
|
agent_id: int,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""수동 하트비트 트리거 (즉시 실행, 백그라운드)."""
|
|
_check_access(current_user)
|
|
agent = await _get_agent_or_404(agent_id, db)
|
|
if not agent.is_active:
|
|
raise HTTPException(status_code=400, detail="비활성 에이전트입니다.")
|
|
|
|
from core.agents import get_agent_engine
|
|
background_tasks.add_task(get_agent_engine().run_heartbeat, agent_id)
|
|
return {"message": f"에이전트 '{agent.name}' 하트비트 시작", "agent_id": agent_id}
|
|
|
|
|
|
@router.post("/{agent_id}/pause")
|
|
async def pause_agent(
|
|
agent_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""에이전트 일시 중지."""
|
|
_check_access(current_user)
|
|
agent = await _get_agent_or_404(agent_id, db)
|
|
agent.is_active = False
|
|
agent.status = AgentStatus.PAUSED
|
|
await db.commit()
|
|
return {"message": f"에이전트 '{agent.name}' 일시 중지", "status": "PAUSED"}
|
|
|
|
|
|
@router.post("/{agent_id}/resume")
|
|
async def resume_agent(
|
|
agent_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""에이전트 재개."""
|
|
_check_access(current_user)
|
|
agent = await _get_agent_or_404(agent_id, db)
|
|
agent.is_active = True
|
|
agent.status = AgentStatus.IDLE
|
|
await db.commit()
|
|
return {"message": f"에이전트 '{agent.name}' 재개", "status": "IDLE"}
|
|
|
|
|
|
# ── 태스크 ───────────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/{agent_id}/tasks", response_model=List[AgentTaskOut])
|
|
async def list_tasks(
|
|
agent_id: int,
|
|
limit: int = 20,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
_check_access(current_user)
|
|
tasks = (await db.execute(
|
|
select(AgentTask)
|
|
.where(AgentTask.agent_id == agent_id)
|
|
.order_by(AgentTask.created_at.desc())
|
|
.limit(limit)
|
|
)).scalars().all()
|
|
return tasks
|
|
|
|
|
|
@router.post("/{agent_id}/tasks", response_model=AgentTaskOut,
|
|
status_code=status.HTTP_201_CREATED)
|
|
async def create_task(
|
|
agent_id: int,
|
|
body: AgentTaskCreate,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Developer 에이전트에게 태스크 직접 등록."""
|
|
_check_access(current_user)
|
|
agent = await _get_agent_or_404(agent_id, db)
|
|
|
|
task = AgentTask(
|
|
agent_id=agent_id,
|
|
title=body.title,
|
|
description=body.description,
|
|
status=AgentTaskStatus.PENDING,
|
|
input_data=body.input_data,
|
|
created_at=datetime.now(),
|
|
)
|
|
db.add(task)
|
|
await db.commit()
|
|
await db.refresh(task)
|
|
|
|
# Developer 에이전트면 바로 하트비트 실행
|
|
if agent.role == AgentRole.DEVELOPER and agent.is_active:
|
|
from core.agents import get_agent_engine
|
|
background_tasks.add_task(get_agent_engine().run_heartbeat, agent_id)
|
|
|
|
return task
|
|
|
|
|
|
# ── 스케줄러 등록 헬퍼 ────────────────────────────────────────────────────
|
|
|
|
def _register_heartbeat(agent: AgentConfig) -> None:
|
|
"""APScheduler에 에이전트 하트비트 크론 잡 등록."""
|
|
try:
|
|
from core.scheduler import get_scheduler
|
|
from apscheduler.triggers.cron import CronTrigger
|
|
|
|
scheduler = get_scheduler()
|
|
if scheduler is None or not scheduler.running:
|
|
return
|
|
|
|
from core.agents import get_agent_engine
|
|
job_id = f"agent_heartbeat_{agent.id}"
|
|
|
|
scheduler.add_job(
|
|
get_agent_engine().run_heartbeat,
|
|
CronTrigger.from_crontab(agent.heartbeat_cron, timezone="Asia/Seoul"),
|
|
args=[agent.id],
|
|
id=job_id,
|
|
name=f"에이전트 하트비트: {agent.name}",
|
|
replace_existing=True,
|
|
misfire_grace_time=300,
|
|
)
|
|
logger.info("에이전트 하트비트 등록: job_id=%s cron=%s", job_id, agent.heartbeat_cron)
|
|
except Exception as exc:
|
|
logger.warning("에이전트 하트비트 스케줄 등록 실패: %s", exc)
|
|
|
|
|
|
# ── 에이전트 간 메시지 API (Priority 3) ───────────────────────────────────────
|
|
|
|
@router.get("/{agent_id}/messages", summary="수신 메시지 목록")
|
|
async def get_agent_messages(
|
|
agent_id: int,
|
|
unread_only: bool = False,
|
|
limit: int = 50,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""에이전트가 수신한 메시지 목록을 반환한다."""
|
|
_check_access(current_user)
|
|
from models import AgentMessage, AgentMessageOut
|
|
q = select(AgentMessage).where(AgentMessage.to_agent_id == agent_id)
|
|
if unread_only:
|
|
q = q.where(AgentMessage.is_read.is_(False))
|
|
q = q.order_by(AgentMessage.created_at.desc()).limit(limit)
|
|
msgs = (await db.execute(q)).scalars().all()
|
|
return [AgentMessageOut.model_validate(m) for m in msgs]
|
|
|
|
|
|
@router.post("/{agent_id}/messages", status_code=201, summary="메시지 전송")
|
|
async def send_agent_message(
|
|
agent_id: int,
|
|
payload: dict,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""
|
|
에이전트에게 메시지를 전송한다.
|
|
|
|
Body: {
|
|
"from_agent_id": int | null,
|
|
"message_type": "TASK_DELEGATION|STATUS_UPDATE|ESCALATION",
|
|
"subject": "string",
|
|
"body": "string",
|
|
"metadata_json": "string"
|
|
}
|
|
"""
|
|
_check_access(current_user)
|
|
from models import AgentMessage, AgentMessageOut
|
|
|
|
msg = AgentMessage(
|
|
from_agent_id=payload.get("from_agent_id"),
|
|
to_agent_id=agent_id,
|
|
message_type=payload.get("message_type", "STATUS_UPDATE"),
|
|
subject=payload.get("subject", ""),
|
|
body=payload.get("body"),
|
|
metadata_json=payload.get("metadata_json"),
|
|
)
|
|
db.add(msg)
|
|
await db.commit()
|
|
await db.refresh(msg)
|
|
return AgentMessageOut.model_validate(msg)
|
|
|
|
|
|
@router.patch("/messages/{msg_id}/read", summary="메시지 읽음 처리")
|
|
async def mark_message_read(
|
|
msg_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""메시지를 읽음 처리한다."""
|
|
_check_access(current_user)
|
|
from models import AgentMessage, AgentMessageOut
|
|
|
|
msg = await db.get(AgentMessage, msg_id)
|
|
if not msg:
|
|
raise HTTPException(404, "메시지를 찾을 수 없습니다.")
|
|
msg.is_read = True
|
|
msg.read_at = datetime.utcnow()
|
|
await db.commit()
|
|
await db.refresh(msg)
|
|
return AgentMessageOut.model_validate(msg)
|
|
|
|
|
|
# ── 파인튜닝 API (Priority 3) ─────────────────────────────────────────────────
|
|
|
|
@router.post("/finetune/start", summary="파인튜닝 시작 (ADMIN only)")
|
|
async def start_finetune(
|
|
background_tasks: BackgroundTasks,
|
|
payload: dict = {},
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""
|
|
tb_agent_task(COMPLETED) 데이터로 Ollama 커스텀 모델 파인튜닝을 시작한다.
|
|
|
|
Body: { "model_name": "guardia-agent-v2", "limit": 1000 }
|
|
"""
|
|
if current_user.role != UserRole.ADMIN:
|
|
raise HTTPException(403, "ADMIN 권한이 필요합니다.")
|
|
|
|
import os
|
|
model_name = payload.get("model_name", "guardia-agent-v2")
|
|
limit = int(payload.get("limit", 1000))
|
|
dataset_path = f"/opt/guardia/finetune/{model_name}.jsonl"
|
|
|
|
async def _run_finetune():
|
|
from core.llm_client import get_llm_client
|
|
llm = get_llm_client()
|
|
count = await llm.export_finetune_dataset(dataset_path, limit=limit)
|
|
if count == 0:
|
|
logger.error("[finetune] 데이터셋 내보내기 실패 또는 데이터 없음")
|
|
return
|
|
ok = await llm.fine_tune(dataset_path, model_name)
|
|
if ok:
|
|
logger.info("[finetune] 파인튜닝 완료: model=%s", model_name)
|
|
else:
|
|
logger.error("[finetune] 파인튜닝 실패: model=%s", model_name)
|
|
|
|
background_tasks.add_task(_run_finetune)
|
|
return {"status": "started", "model_name": model_name, "dataset_path": dataset_path}
|
|
|
|
|
|
@router.get("/finetune/status", summary="파인튜닝 진행 상태 조회")
|
|
async def get_finetune_status(
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""파인튜닝 진행 상태를 반환한다 (Ollama 모델 목록 기반)."""
|
|
_check_access(current_user)
|
|
from core.llm_client import get_llm_client
|
|
llm = get_llm_client()
|
|
models = await llm.list_models()
|
|
return {"models": [{"name": m.name, "size": m.size} for m in models]}
|