""" RPA (Robotic Process Automation) 라우터 - Validation 학습: 프로젝트 소스(models.py) AST 파싱 - RPA 작업 등록/수정/삭제/실행 - 실행 이력 조회 """ from __future__ import annotations import os from datetime import datetime from typing import Any, Dict, List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks from pydantic import BaseModel from sqlalchemy import select, func, delete from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import User from core.rpa_engine import ValidationLearner, RPAValidator, RPAExecutor router = APIRouter(prefix="/api/rpa", tags=["rpa"]) # ── 인메모리 저장소 (DB 미적용 시 fallback) ────────────────────────────────── # 실제 운영 시 SQLAlchemy 모델로 교체 _validation_rules: Dict[str, List[Dict]] = {} # endpoint → rules _rpa_tasks: Dict[int, Dict] = {} _rpa_executions: List[Dict] = [] _task_id_seq = 1 # ── Schemas ────────────────────────────────────────────────────────────────── class LearnRequest(BaseModel): endpoints: str = "all" # "all" 또는 특정 endpoint overwrite: bool = True class RPATaskCreate(BaseModel): task_name: str task_type: str # SR_CREATE | SR_STATUS_UPDATE | APPROVAL_PROCESS | ... schedule: Optional[str] = None # cron expression payload_template: Dict[str, Any] = {} is_active: bool = True description: Optional[str] = None class RPATaskOut(BaseModel): id: int task_name: str task_type: str schedule: Optional[str] payload_template: Dict[str, Any] is_active: bool description: Optional[str] created_at: str last_run: Optional[str] class ExecuteRequest(BaseModel): task_type: str payload: Dict[str, Any] dry_run: bool = False class ExecuteOut(BaseModel): execution_id: int task_type: str status: str dry_run: bool validation_errors: List[str] = [] result: Optional[Dict] = None error: Optional[str] = None started_at: str completed_at: Optional[str] = None # ── Validation 학습 ────────────────────────────────────────────────────────── @router.post("/validations/learn") async def learn_validations( req: LearnRequest, current_user: User = Depends(get_current_user), ): """ 프로젝트 소스(models.py)를 AST 파싱하여 validation 규칙 학습. """ learner = ValidationLearner() try: result = learner.learn_from_source() except Exception as e: raise HTTPException(500, f"소스 파싱 실패: {e}") rules = result["rules"] schemas = result["schemas"] if req.overwrite: _validation_rules.clear() learned = 0 for rule in rules: ep = rule["endpoint"] if ep not in _validation_rules: _validation_rules[ep] = [] # 중복 필드 제거 existing = {r["field_name"] for r in _validation_rules[ep]} if rule["field_name"] not in existing: _validation_rules[ep].append(rule) learned += 1 return { "learned": learned, "schemas": schemas, "endpoints_mapped": len(_validation_rules), "summary": {ep: len(rules_) for ep, rules_ in _validation_rules.items()}, } @router.get("/validations") async def get_validations( endpoint: Optional[str] = Query(None), current_user: User = Depends(get_current_user), ): """학습된 validation 규칙 조회.""" if endpoint: return {"endpoint": endpoint, "rules": _validation_rules.get(endpoint, [])} return { "total_endpoints": len(_validation_rules), "total_rules": sum(len(v) for v in _validation_rules.values()), "endpoints": list(_validation_rules.keys()), } # ── RPA 작업 관리 ───────────────────────────────────────────────────────────── @router.post("/tasks", response_model=RPATaskOut) async def create_rpa_task( body: RPATaskCreate, current_user: User = Depends(get_current_user), ): """RPA 작업 등록.""" global _task_id_seq task = { "id": _task_id_seq, "task_name": body.task_name, "task_type": body.task_type, "schedule": body.schedule, "payload_template": body.payload_template, "is_active": body.is_active, "description": body.description, "created_at": datetime.now().isoformat(), "last_run": None, "created_by": current_user.username, } _rpa_tasks[_task_id_seq] = task _task_id_seq += 1 return task @router.get("/tasks", response_model=List[RPATaskOut]) async def list_rpa_tasks( is_active: Optional[bool] = Query(None), task_type: Optional[str] = Query(None), current_user: User = Depends(get_current_user), ): tasks = list(_rpa_tasks.values()) if is_active is not None: tasks = [t for t in tasks if t["is_active"] == is_active] if task_type: tasks = [t for t in tasks if t["task_type"] == task_type] return tasks @router.get("/tasks/{task_id}", response_model=RPATaskOut) async def get_rpa_task(task_id: int, current_user: User = Depends(get_current_user)): task = _rpa_tasks.get(task_id) if not task: raise HTTPException(404, "RPA 작업을 찾을 수 없습니다.") return task @router.put("/tasks/{task_id}", response_model=RPATaskOut) async def update_rpa_task( task_id: int, body: RPATaskCreate, current_user: User = Depends(get_current_user), ): task = _rpa_tasks.get(task_id) if not task: raise HTTPException(404, "RPA 작업을 찾을 수 없습니다.") task.update({ "task_name": body.task_name, "task_type": body.task_type, "schedule": body.schedule, "payload_template": body.payload_template, "is_active": body.is_active, "description": body.description, }) return task @router.delete("/tasks/{task_id}") async def delete_rpa_task(task_id: int, current_user: User = Depends(get_current_user)): if task_id not in _rpa_tasks: raise HTTPException(404, "RPA 작업을 찾을 수 없습니다.") del _rpa_tasks[task_id] return {"deleted": task_id} # ── RPA 실행 ───────────────────────────────────────────────────────────────── @router.post("/execute", response_model=ExecuteOut) async def execute_rpa( body: ExecuteRequest, current_user: User = Depends(get_current_user), ): """ 단발성 RPA 실행. 1. payload를 학습된 validation 규칙으로 검증 2. dry_run=false 시 실제 API 호출 """ exec_id = len(_rpa_executions) + 1 started = datetime.now().isoformat() # Validation 규칙 찾기 executor_map = { "SR_CREATE": "POST /api/tasks", "SR_STATUS_UPDATE": "PATCH /api/tasks/status", "APPROVAL_PROCESS": "POST /api/approvals", "INCIDENT_CREATE": "POST /api/incidents", } endpoint_key = executor_map.get(body.task_type, f"POST /api/{body.task_type.lower()}") rules = _validation_rules.get(endpoint_key, []) # Validation 검증 validator = RPAValidator(rules) errors = validator.validate(body.payload) if errors: record = { "execution_id": exec_id, "task_type": body.task_type, "status": "VALIDATION_FAILED", "dry_run": body.dry_run, "validation_errors": errors, "result": None, "error": f"{len(errors)}개 validation 오류", "started_at": started, "completed_at": datetime.now().isoformat(), "actor": current_user.username, } _rpa_executions.append(record) return record if body.dry_run: record = { "execution_id": exec_id, "task_type": body.task_type, "status": "DRY_RUN_OK", "dry_run": True, "validation_errors": [], "result": {"message": "Validation 통과. dry_run=true이므로 실제 실행 생략."}, "error": None, "started_at": started, "completed_at": datetime.now().isoformat(), "actor": current_user.username, } _rpa_executions.append(record) return record # 실제 실행 base_url = os.getenv("ITSM_BASE_URL", "http://localhost:8001") token = current_user.username # 실제 환경에서는 서비스 토큰 사용 executor = RPAExecutor(base_url=base_url, token=token) try: result = await executor.execute(body.task_type, body.payload, dry_run=False) except Exception as e: result = {"status": "FAILED", "error": str(e)} record = { "execution_id": exec_id, "task_type": body.task_type, "status": result.get("status", "FAILED"), "dry_run": False, "validation_errors": [], "result": result.get("response"), "error": result.get("error"), "started_at": started, "completed_at": datetime.now().isoformat(), "actor": current_user.username, } _rpa_executions.append(record) return record @router.post("/tasks/{task_id}/run", response_model=ExecuteOut) async def run_rpa_task( task_id: int, dry_run: bool = Query(False), current_user: User = Depends(get_current_user), ): """등록된 RPA 작업 즉시 실행.""" task = _rpa_tasks.get(task_id) if not task: raise HTTPException(404, "RPA 작업을 찾을 수 없습니다.") if not task["is_active"]: raise HTTPException(400, "비활성 작업입니다. 먼저 활성화하세요.") req = ExecuteRequest( task_type=task["task_type"], payload=task["payload_template"], dry_run=dry_run, ) result = await execute_rpa(req, current_user) # last_run 갱신 _rpa_tasks[task_id]["last_run"] = datetime.now().isoformat() return result # ── 실행 이력 ────────────────────────────────────────────────────────────────── @router.get("/executions") async def list_executions( status: Optional[str] = Query(None), task_type: Optional[str] = Query(None), page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), ): execs = list(_rpa_executions) if status: execs = [e for e in execs if e["status"] == status] if task_type: execs = [e for e in execs if e["task_type"] == task_type] total = len(execs) start = (page - 1) * size return { "total": total, "page": page, "size": size, "items": list(reversed(execs))[start:start + size], } @router.get("/executions/{execution_id}") async def get_execution( execution_id: int, current_user: User = Depends(get_current_user), ): for e in _rpa_executions: if e["execution_id"] == execution_id: return e raise HTTPException(404, "실행 이력을 찾을 수 없습니다.")