feat(rpa): GUARDiA ITSM RPA 봇 기능 구현 + 하네스 구성
[하네스] - agents/validation-learner.md: 소스 AST 파싱 validation 학습 에이전트 - agents/rpa-bot.md: 학습 규칙 참조 자동화 실행 에이전트 - skills/rpa-orchestrator/SKILL.md: RPA E2E 워크플로우 스킬 - skills/rpa-validation/SKILL.md: 소스 기반 validation 학습 스킬 [구현] - core/rpa_engine.py: ValidationLearner(AST 파서) + RPAValidator + RPAExecutor - routers/rpa.py: 11개 API 엔드포인트 POST /api/rpa/validations/learn — models.py AST 파싱 → 1357개 규칙 학습 GET /api/rpa/validations — 학습 규칙 조회 (119 endpoints) POST /api/rpa/tasks — RPA 작업 등록 POST /api/rpa/execute — 즉시 실행 (validation + API 호출) GET /api/rpa/executions — 실행 이력 [테스트 결과] - validation 학습: 140개 스키마 / 1357개 규칙 / 119개 엔드포인트 - WRONG_TYPE → enum 오류 감지 정확 - 필수 필드 누락 → validation 오류 상세 반환 - 실행 이력 조회 정상 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9e4932640a
commit
79973261b0
51
.claude/agents/rpa-bot.md
Normal file
51
.claude/agents/rpa-bot.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: rpa-bot
|
||||||
|
description: "RPA 봇 실행 에이전트. validation-learner가 학습한 규칙을 참조하여 ITSM 반복 작업(SR 자동 접수, 승인 처리, 상태 변경, 배포 요청, 정기 점검)을 자동으로 수행한다. 입력값은 학습된 validation으로 검증 후 실행."
|
||||||
|
model: opus
|
||||||
|
---
|
||||||
|
|
||||||
|
# RPA Bot — 자동화 실행 에이전트
|
||||||
|
|
||||||
|
## 핵심 역할
|
||||||
|
|
||||||
|
학습된 validation 규칙에 따라 ITSM API를 자동 호출하여 반복 업무를 수행한다.
|
||||||
|
모든 실행은 tb_rpa_execution에 기록되고 감사 추적이 보장된다.
|
||||||
|
|
||||||
|
## 자동화 가능 작업
|
||||||
|
|
||||||
|
| 작업 유형 | API | 설명 |
|
||||||
|
|----------|-----|------|
|
||||||
|
| SR 자동 접수 | POST /api/tasks | 정기/예약 SR 생성 |
|
||||||
|
| SR 상태 일괄 변경 | PATCH /api/tasks/{id}/status | 승인 대기 → 승인 자동화 |
|
||||||
|
| 기관별 서버 점검 | POST /api/tasks | 주기적 헬스체크 SR |
|
||||||
|
| SSL 만료 경보 SR | POST /api/tasks | SSL 만료일 N일 전 자동 SR |
|
||||||
|
| SLA 초과 에스컬레이션 | 내부 로직 | SLA 위반 SR 자동 에스컬레이션 |
|
||||||
|
| 쉘 스크립트 실행 | POST /api/ssh/exec | 정기 유지보수 명령 실행 |
|
||||||
|
|
||||||
|
## 실행 원칙
|
||||||
|
|
||||||
|
1. **Validation 우선**: 모든 입력은 tb_rpa_validation 규칙으로 검증 후 API 호출
|
||||||
|
2. **Dry-run 지원**: `dry_run=true` 시 실제 실행 없이 입력값 검증만 수행
|
||||||
|
3. **감사 추적**: 모든 실행은 tb_rpa_execution + tb_audit_log에 이중 기록
|
||||||
|
4. **Rollback**: 실패 시 생성된 SR/변경사항 자동 취소 (취소 가능한 경우)
|
||||||
|
5. **보안**: 서버 자격증명·IP·비밀번호는 API 응답/로그에 노출 금지
|
||||||
|
|
||||||
|
## 입력/출력
|
||||||
|
|
||||||
|
- **입력**: RPA 작업 정의 (task_type, payload_template, schedule)
|
||||||
|
- **출력**: 실행 결과 (status, result, error_msg, execution_id)
|
||||||
|
|
||||||
|
## 팀 통신 프로토콜
|
||||||
|
|
||||||
|
- **수신**: guardia-orchestrator 또는 사용자 트리거
|
||||||
|
- **발신**:
|
||||||
|
- validation-learner: 규칙 갱신 요청
|
||||||
|
- incident-responder: 실행 실패 → 인시던트 자동 생성
|
||||||
|
- sla-guardian: SLA 위반 SR 에스컬레이션 요청
|
||||||
|
|
||||||
|
## 에러 핸들링
|
||||||
|
|
||||||
|
- Validation 실패 → 실행 중단, 오류 상세 반환 (어떤 필드가 어떤 규칙 위반)
|
||||||
|
- API 호출 실패(4xx) → 입력 오류로 기록, 재시도 없음
|
||||||
|
- API 호출 실패(5xx) → 최대 3회 재시도 (지수 백오프)
|
||||||
|
- 연속 실패 → incident-responder에게 인시던트 생성 요청
|
||||||
42
.claude/agents/validation-learner.md
Normal file
42
.claude/agents/validation-learner.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: validation-learner
|
||||||
|
description: "RPA Validation 학습 에이전트. ITSM 모든 API 엔드포인트의 Pydantic 스키마를 스캔하여 입력 항목(필드명·타입·제약조건·필수여부)을 자동 학습하고 tb_rpa_validation 테이블에 저장한다."
|
||||||
|
model: opus
|
||||||
|
---
|
||||||
|
|
||||||
|
# Validation Learner — RPA 입력 학습 에이전트
|
||||||
|
|
||||||
|
## 핵심 역할
|
||||||
|
|
||||||
|
GUARDiA ITSM의 모든 FastAPI 라우터를 분석하여 입력 스키마(Pydantic BaseModel)에서
|
||||||
|
validation 규칙을 추출하고 DB에 저장한다. RPA 봇이 이 규칙을 참조하여 유효한 입력을 자동 생성한다.
|
||||||
|
|
||||||
|
## 학습 대상
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| API 엔드포인트 | `/api/tasks`, `/api/approvals`, `/api/institutions`, `/api/servers` 등 모든 POST/PUT |
|
||||||
|
| Pydantic 모델 | SRCreate, SRStatusUpdate, InstitutionCreate, ServerCreate 등 |
|
||||||
|
| Validation 규칙 | required, type, min/max length, enum values, regex pattern, ge/le |
|
||||||
|
|
||||||
|
## 작업 원칙
|
||||||
|
|
||||||
|
1. `GET /api/openapi.json` 로 전체 스키마 수집 (FastAPI 자동 생성)
|
||||||
|
2. `POST /api/rpa/validations/learn` 호출로 DB 저장 트리거
|
||||||
|
3. 학습 완료 후 규칙 요약을 rpa-bot에게 SendMessage로 전달
|
||||||
|
4. 새 엔드포인트 추가 시 증분 학습 지원
|
||||||
|
|
||||||
|
## 입력/출력
|
||||||
|
|
||||||
|
- **입력**: 학습 트리거 요청 (endpoint 목록 또는 전체)
|
||||||
|
- **출력**: 학습된 규칙 수, 엔드포인트별 필드 목록
|
||||||
|
|
||||||
|
## 팀 통신 프로토콜
|
||||||
|
|
||||||
|
- **수신**: guardia-orchestrator / rpa-bot 의 학습 요청
|
||||||
|
- **발신**: rpa-bot에게 `{learned_rules: [...], endpoint_count: N}` 전달
|
||||||
|
|
||||||
|
## 에러 핸들링
|
||||||
|
|
||||||
|
- OpenAPI 스키마 파싱 실패 → 이전 학습 규칙 유지, 경고 로그
|
||||||
|
- DB 저장 실패 → 재시도 1회 후 실패 목록 보고
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: guardia-orchestrator
|
name: guardia-orchestrator
|
||||||
description: "GUARDiA ITSM 통합 오케스트레이터. SR 접수·배포·코드리뷰·SLA·인시던트·RCA·보안패치·Jira동기화·대량처리·DR자동화·네트워크장비관리·CSAP점검 등 ITSM 운영 전반을 조율하는 메인 스킬. 다음 상황에서 반드시 사용: (1) 'SR 처리해줘', '배포 진행', '코드 리뷰', 'SLA 현황', '인시던트 대응', 'RCA 분석', '보안 패치', 'Jira 연동' 등 ITSM 운영 요청; (2) 'DR 테스트', 'Failover', 'RTO/RPO', '재해복구' 요청; (3) '네트워크 장비', '스위치 백업', '설정 변경 감지', '방화벽' 관련 요청; (4) 'CSAP', 'ISMS', '보안 점검', '준수율' 관련 요청; (5) 여러 에이전트 협업이 필요한 복합 작업; (6) 'GUARDiA 작업', '하네스 실행', '에이전트팀 구성' 요청; (7) SR-배포-리뷰를 한 번에 처리하는 End-to-End 워크플로우; (8) 다시 실행, 재실행, 업데이트, 수정, 보완 요청. 단순 질문(API 경로, 모델 설명 등)은 직접 응답 가능."
|
description: "GUARDiA ITSM 통합 오케스트레이터. SR 접수·배포·코드리뷰·SLA·인시던트·RCA·보안패치·Jira동기화·대량처리·DR자동화·네트워크장비관리·CSAP점검·RPA봇자동화 등 ITSM 운영 전반을 조율하는 메인 스킬. 다음 상황에서 반드시 사용: (1) 'SR 처리해줘', '배포 진행', '코드 리뷰', 'SLA 현황', '인시던트 대응', 'RCA 분석', '보안 패치', 'Jira 연동' 등 ITSM 운영 요청; (2) 'DR 테스트', 'Failover', 'RTO/RPO', '재해복구' 요청; (3) '네트워크 장비', '스위치 백업', '설정 변경 감지', '방화벽' 관련 요청; (4) 'CSAP', 'ISMS', '보안 점검', '준수율' 관련 요청; (5) 'RPA', '봇 자동화', '반복 작업 자동화', 'validation 학습' 요청 → rpa-orchestrator 스킬 위임; (6) 여러 에이전트 협업이 필요한 복합 작업; (7) 'GUARDiA 작업', '하네스 실행', '에이전트팀 구성' 요청; (8) SR-배포-리뷰를 한 번에 처리하는 End-to-End 워크플로우; (9) 다시 실행, 재실행, 업데이트, 수정, 보완 요청. 단순 질문(API 경로, 모델 설명 등)은 직접 응답 가능."
|
||||||
---
|
---
|
||||||
|
|
||||||
# GUARDiA ITSM 오케스트레이터
|
# GUARDiA ITSM 오케스트레이터
|
||||||
|
|||||||
150
.claude/skills/rpa-orchestrator/SKILL.md
Normal file
150
.claude/skills/rpa-orchestrator/SKILL.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
name: rpa-orchestrator
|
||||||
|
description: "GUARDiA ITSM RPA 봇 오케스트레이터. ITSM 반복 업무 자동화, RPA 작업 등록/실행/스케줄링, 입력 Validation 학습, 실행 이력 조회를 총괄한다. 다음 상황에서 반드시 사용: (1) 'RPA', '봇 자동화', '자동 처리', '반복 작업 자동화' 요청; (2) 'validation 학습', '입력 규칙 학습', 'API 스키마 학습' 요청; (3) 'RPA 작업 등록', 'RPA 실행', 'RPA 스케줄' 요청; (4) 'SR 자동 접수', 'SSL 만료 자동 알림', '정기 점검 자동화' 요청; (5) 'RPA 이력', 'RPA 실행 결과', 'RPA 현황' 조회; (6) 다시 실행, 업데이트, 수정, 보완, 재실행 요청."
|
||||||
|
---
|
||||||
|
|
||||||
|
# GUARDiA ITSM RPA 오케스트레이터
|
||||||
|
|
||||||
|
RPA 봇(자동화)과 Validation 학습을 조율하는 통합 워크플로우.
|
||||||
|
**실행 모드: 파이프라인 (에이전트 팀)** — validation-learner → rpa-bot → 기존 에이전트 연동.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 에이전트 팀 구성
|
||||||
|
|
||||||
|
| 에이전트 | 역할 |
|
||||||
|
|---------|------|
|
||||||
|
| validation-learner | ITSM API 스키마 스캔 → validation 규칙 DB 저장 |
|
||||||
|
| rpa-bot | 학습 규칙 참조 → ITSM API 자동 호출 실행 |
|
||||||
|
| incident-responder | RPA 실행 실패 → 인시던트 자동 생성 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0: 컨텍스트 확인
|
||||||
|
|
||||||
|
사용자 요청 분류:
|
||||||
|
- **학습 요청** ("validation 학습해줘", "API 스키마 학습") → Phase 1만 실행
|
||||||
|
- **실행 요청** ("RPA 실행", "자동 처리") → Phase 2 실행 (학습 규칙이 없으면 Phase 1 선행)
|
||||||
|
- **등록 요청** ("RPA 작업 추가", "봇 등록") → Phase 3 실행
|
||||||
|
- **조회 요청** ("RPA 현황", "실행 이력") → `GET /api/rpa/tasks`, `GET /api/rpa/executions`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Validation 학습
|
||||||
|
|
||||||
|
`validation-learner` 서브 에이전트 호출.
|
||||||
|
|
||||||
|
```
|
||||||
|
# 전체 학습 (최초 또는 엔드포인트 추가 후)
|
||||||
|
POST /api/rpa/validations/learn
|
||||||
|
{
|
||||||
|
"endpoints": "all", # 또는 특정 endpoint 목록
|
||||||
|
"overwrite": true
|
||||||
|
}
|
||||||
|
|
||||||
|
응답: { learned: N, endpoints: [...] }
|
||||||
|
```
|
||||||
|
|
||||||
|
학습 순서:
|
||||||
|
1. FastAPI OpenAPI 스펙 수집: `GET /api/openapi.json`
|
||||||
|
2. 각 `POST`/`PUT` 엔드포인트의 `requestBody.schema` 파싱
|
||||||
|
3. 필드별 rules 추출 → `tb_rpa_validation` upsert
|
||||||
|
4. 학습 결과 요약 출력
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: RPA 작업 실행
|
||||||
|
|
||||||
|
`rpa-bot` 에이전트 호출. 실행 전 반드시 validation 확인.
|
||||||
|
|
||||||
|
```
|
||||||
|
# 단발성 즉시 실행
|
||||||
|
POST /api/rpa/execute
|
||||||
|
{
|
||||||
|
"task_type": "SR_CREATE" | "SR_STATUS_UPDATE" | "SHELL_EXEC" | "SSL_CHECK",
|
||||||
|
"payload": { ... }, # 입력 데이터 (validation 학습 규칙 준수 필수)
|
||||||
|
"dry_run": false # true 시 검증만, API 호출 없음
|
||||||
|
}
|
||||||
|
|
||||||
|
# 스케줄 작업 실행 (등록된 태스크)
|
||||||
|
POST /api/rpa/tasks/{task_id}/run
|
||||||
|
```
|
||||||
|
|
||||||
|
**실행 흐름:**
|
||||||
|
```
|
||||||
|
payload 입력
|
||||||
|
→ validation 검증 (tb_rpa_validation 규칙)
|
||||||
|
→ 실패: 오류 필드 + 위반 규칙 상세 반환 (실행 중단)
|
||||||
|
→ 성공: API 호출
|
||||||
|
→ 성공: tb_rpa_execution 기록 (SUCCESS)
|
||||||
|
→ 실패: 재시도 3회 → incident-responder 인시던트 생성
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: RPA 작업 등록/관리
|
||||||
|
|
||||||
|
```
|
||||||
|
# 작업 등록
|
||||||
|
POST /api/rpa/tasks
|
||||||
|
{
|
||||||
|
"task_name": "SSL 만료 30일 전 SR 자동 생성",
|
||||||
|
"task_type": "SR_CREATE",
|
||||||
|
"schedule": "0 9 * * *", # cron: 매일 09:00
|
||||||
|
"payload_template": {
|
||||||
|
"sr_type": "INQUIRY",
|
||||||
|
"priority": "HIGH",
|
||||||
|
"title": "SSL 인증서 만료 예정 점검",
|
||||||
|
"description": "{{server_name}} SSL 만료일 {{ssl_expire_date}}"
|
||||||
|
},
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
|
||||||
|
# 목록 조회
|
||||||
|
GET /api/rpa/tasks?page=1&size=20&is_active=true
|
||||||
|
|
||||||
|
# 실행 이력
|
||||||
|
GET /api/rpa/executions?task_id={id}&status=FAILED
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: 결과 보고
|
||||||
|
|
||||||
|
실행 완료 후 요약:
|
||||||
|
- 실행된 RPA 작업 목록
|
||||||
|
- 성공/실패 건수
|
||||||
|
- 실패 원인 (validation 오류 or API 오류)
|
||||||
|
- 생성된 SR/인시던트 ID 목록
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 API 엔드포인트
|
||||||
|
|
||||||
|
| Method | Path | 설명 |
|
||||||
|
|--------|------|------|
|
||||||
|
| POST | /api/rpa/validations/learn | Validation 학습 트리거 |
|
||||||
|
| GET | /api/rpa/validations | 학습된 규칙 목록 |
|
||||||
|
| POST | /api/rpa/tasks | RPA 작업 등록 |
|
||||||
|
| GET | /api/rpa/tasks | 작업 목록 |
|
||||||
|
| PUT | /api/rpa/tasks/{id} | 작업 수정 |
|
||||||
|
| DELETE | /api/rpa/tasks/{id} | 작업 삭제 |
|
||||||
|
| POST | /api/rpa/tasks/{id}/run | 즉시 실행 |
|
||||||
|
| POST | /api/rpa/execute | 단발성 즉시 실행 |
|
||||||
|
| GET | /api/rpa/executions | 실행 이력 |
|
||||||
|
| GET | /api/rpa/executions/{id} | 실행 상세 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 테스트 시나리오
|
||||||
|
|
||||||
|
**정상 흐름:**
|
||||||
|
1. `POST /api/rpa/validations/learn` → 전체 학습
|
||||||
|
2. `POST /api/rpa/execute` with `dry_run: true` → validation 통과 확인
|
||||||
|
3. `POST /api/rpa/execute` with `dry_run: false` → 실제 SR 생성
|
||||||
|
4. `GET /api/rpa/executions` → 실행 이력 확인
|
||||||
|
|
||||||
|
**에러 흐름:**
|
||||||
|
1. 필수 필드 누락 → `validation 오류: title 필드 필수` 반환
|
||||||
|
2. enum 오류 → `sr_type 허용값: DEPLOY|RESTART|LOG|INQUIRY|OTHER` 반환
|
||||||
|
3. API 5xx → 3회 재시도 → incident-responder 인시던트 생성
|
||||||
116
.claude/skills/rpa-validation/SKILL.md
Normal file
116
.claude/skills/rpa-validation/SKILL.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
name: rpa-validation
|
||||||
|
description: "RPA 입력 항목 Validation 학습 스킬. ITSM 프로젝트 소스코드(models.py, routers/)에서 Pydantic 스키마를 파싱하여 모든 입력 항목의 validation 규칙(타입·필수·제약·enum)을 학습하고 DB에 저장한다. 다음 상황에서 반드시 사용: (1) 'validation 학습', 'API 스키마 학습', '입력 규칙 학습'; (2) 'Pydantic 모델 파싱', '소스 분석'; (3) RPA 봇 실행 전 입력 검증 규칙 갱신; (4) 새 라우터/모델 추가 후 재학습; (5) 다시 실행, 업데이트, 보완."
|
||||||
|
---
|
||||||
|
|
||||||
|
# RPA Validation 학습 스킬
|
||||||
|
|
||||||
|
ITSM 프로젝트 소스코드를 직접 분석하여 모든 입력 항목의 validation 규칙을 학습한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 학습 전략: 소스 기반 정적 분석
|
||||||
|
|
||||||
|
OpenAPI JSON 대신 **소스코드를 직접 파싱**한다.
|
||||||
|
이유: OpenAPI JSON은 일부 validator가 누락되고, 소스 파싱이 더 정확하다.
|
||||||
|
|
||||||
|
### 학습 대상 파일
|
||||||
|
|
||||||
|
```
|
||||||
|
itsm/models.py ← Pydantic BaseModel (SRCreate, SRStatusUpdate, 등)
|
||||||
|
itsm/routers/*.py ← 각 라우터에서 사용하는 스키마 매핑
|
||||||
|
```
|
||||||
|
|
||||||
|
### 파싱 방법
|
||||||
|
|
||||||
|
`POST /api/rpa/validations/learn` 호출 시 서버가:
|
||||||
|
1. `itsm/models.py` AST 파싱
|
||||||
|
2. `class XXXCreate(BaseModel)` / `class XXXUpdate(BaseModel)` 클래스 탐색
|
||||||
|
3. 각 클래스의 필드 분석:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 분석 대상 패턴
|
||||||
|
class SRCreate(BaseModel):
|
||||||
|
sr_type: SRType # Enum → allowed_values 추출
|
||||||
|
title: str # required str
|
||||||
|
description: Optional[str] = None # optional
|
||||||
|
priority: Priority = Priority.MEDIUM # enum + default
|
||||||
|
server_id: Optional[int] = None # optional int
|
||||||
|
inst_id: int # required int
|
||||||
|
assigned_to: Optional[str] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
### 추출되는 규칙 구조
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"endpoint": "POST /api/tasks",
|
||||||
|
"schema_class": "SRCreate",
|
||||||
|
"field_name": "sr_type",
|
||||||
|
"field_type": "enum",
|
||||||
|
"is_required": true,
|
||||||
|
"allowed_values": ["DEPLOY", "RESTART", "LOG", "INQUIRY", "OTHER"],
|
||||||
|
"default": null,
|
||||||
|
"constraints": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 학습 API
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/rpa/validations/learn
|
||||||
|
Body: { "source_path": "auto", "overwrite": true }
|
||||||
|
|
||||||
|
응답:
|
||||||
|
{
|
||||||
|
"learned": 127,
|
||||||
|
"schemas": ["SRCreate", "SRStatusUpdate", "InstitutionCreate", ...],
|
||||||
|
"endpoints_mapped": 43,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 검증 적용
|
||||||
|
|
||||||
|
RPA 봇이 `POST /api/rpa/execute` 호출 시:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 내부 검증 흐름
|
||||||
|
rules = db.query(RPAValidationRule).filter_by(endpoint="POST /api/tasks")
|
||||||
|
for rule in rules:
|
||||||
|
field_val = payload.get(rule.field_name)
|
||||||
|
if rule.is_required and field_val is None:
|
||||||
|
raise RPAValidationError(f"{rule.field_name}: 필수 항목")
|
||||||
|
if rule.field_type == "enum" and field_val not in rule.allowed_values:
|
||||||
|
raise RPAValidationError(
|
||||||
|
f"{rule.field_name}: 허용값 {rule.allowed_values} 중 하나"
|
||||||
|
)
|
||||||
|
if rule.constraints.get("max_length") and len(str(field_val)) > rule.constraints["max_length"]:
|
||||||
|
raise RPAValidationError(f"{rule.field_name}: 최대 {rule.constraints['max_length']}자")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 학습 대상 스키마
|
||||||
|
|
||||||
|
| 스키마 | 엔드포인트 | 핵심 필드 |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| SRCreate | POST /api/tasks | sr_type(enum), title(required), inst_id(required) |
|
||||||
|
| SRStatusUpdate | PATCH /api/tasks/{id}/status | status(enum), comment |
|
||||||
|
| InstitutionCreate | POST /api/institutions | inst_code, inst_name |
|
||||||
|
| ServerCreate | POST /api/servers | server_name, inst_id, server_role |
|
||||||
|
| ApprovalCreate | POST /api/approvals | sr_id, result(enum) |
|
||||||
|
| IncidentCreate | POST /api/incidents | title, severity(enum), server_id |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 재학습 트리거 조건
|
||||||
|
|
||||||
|
- 신규 라우터 추가 후
|
||||||
|
- models.py 스키마 변경 후
|
||||||
|
- RPA 봇 validation 오류 급증 시
|
||||||
|
- 주 1회 자동 재학습 (스케줄러)
|
||||||
23
CLAUDE.md
23
CLAUDE.md
@ -116,12 +116,33 @@ C:\GUARDiA\
|
|||||||
- **deploy-engineer.md**: VibeSession 배포 파이프라인, Jenkins 연동
|
- **deploy-engineer.md**: VibeSession 배포 파이프라인, Jenkins 연동
|
||||||
- **sla-guardian.md**: SLA 모니터링, 에스컬레이션
|
- **sla-guardian.md**: SLA 모니터링, 에스컬레이션
|
||||||
- **incident-responder.md**: 인시던트 생성, 온콜 호출, RCA
|
- **incident-responder.md**: 인시던트 생성, 온콜 호출, RCA
|
||||||
|
- **dr-coordinator.md**: DR 자동화, Failover, RTO/RPO
|
||||||
|
- **network-guardian.md**: 네트워크 장비 관리, 설정 백업
|
||||||
|
- **csap-auditor.md**: CSAP/ISMS 자동 점검, 보고서
|
||||||
|
- **validation-learner.md**: ITSM 소스 AST 파싱 → validation 규칙 학습
|
||||||
|
- **rpa-bot.md**: 학습 규칙 참조 → ITSM 반복 작업 자동화 실행
|
||||||
|
|
||||||
### 스킬 (skills/)
|
### 스킬 (skills/)
|
||||||
- **guardia-orchestrator/SKILL.md**: E2E SR→코드리뷰→배포 워크플로우
|
- **guardia-orchestrator/SKILL.md**: E2E SR→코드리뷰→배포 워크플로우 + RPA 위임
|
||||||
- **code-review/SKILL.md**: B-3 코드 리뷰 실행 가이드
|
- **code-review/SKILL.md**: B-3 코드 리뷰 실행 가이드
|
||||||
- **sr-lifecycle/SKILL.md**: SR 상태 흐름, SLA 기준
|
- **sr-lifecycle/SKILL.md**: SR 상태 흐름, SLA 기준
|
||||||
- **deploy-pipeline/SKILL.md**: VibeSession 배포 단계 관리
|
- **deploy-pipeline/SKILL.md**: VibeSession 배포 단계 관리
|
||||||
|
- **dr-automation/SKILL.md**: DR 자동화, Failover 실행
|
||||||
|
- **network-devices/SKILL.md**: 네트워크 장비 SSH 관리
|
||||||
|
- **csap-compliance/SKILL.md**: CSAP/ISMS 점검 자동화
|
||||||
|
- **rpa-orchestrator/SKILL.md**: RPA 봇 E2E 워크플로우 (validation 학습 + 실행)
|
||||||
|
- **rpa-validation/SKILL.md**: 소스 기반 validation 규칙 학습
|
||||||
|
|
||||||
|
## 하네스: GUARDiA RPA 봇
|
||||||
|
|
||||||
|
**목표:** ITSM 반복 업무(SR 자동 접수, 승인, 점검 등)를 소스 기반 Validation 학습으로 안전하게 자동화
|
||||||
|
|
||||||
|
**트리거:** RPA, 봇 자동화, 반복 작업, validation 학습 요청 시 `rpa-orchestrator` 스킬을 사용하라.
|
||||||
|
|
||||||
|
**변경 이력:**
|
||||||
|
| 날짜 | 변경 내용 | 대상 | 사유 |
|
||||||
|
|------|----------|------|------|
|
||||||
|
| 2026-05-31 | RPA 하네스 초기 구성 | validation-learner, rpa-bot, rpa-orchestrator, rpa-validation | RPA 봇 기능 추가 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
275
core/rpa_engine.py
Normal file
275
core/rpa_engine.py
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
"""
|
||||||
|
RPA Engine — 소스 기반 Validation 학습 + 자동화 실행
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import inspect
|
||||||
|
import importlib
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, delete
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent # itsm/
|
||||||
|
|
||||||
|
|
||||||
|
# ── Validation 학습 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class ValidationLearner:
|
||||||
|
"""프로젝트 소스(models.py)를 AST 파싱하여 Pydantic 스키마 validation 규칙 추출."""
|
||||||
|
|
||||||
|
ENUM_MAP: Dict[str, List[str]] = {
|
||||||
|
"SRType": ["DEPLOY", "RESTART", "LOG", "INQUIRY", "OTHER"],
|
||||||
|
"SRStatus": ["RECEIVED","PARSED","PENDING_APPROVAL","APPROVED",
|
||||||
|
"IN_PROGRESS","PENDING_PM_VALIDATION","COMPLETED",
|
||||||
|
"FAILED_ROLLBACK","REJECTED"],
|
||||||
|
"Priority": ["CRITICAL", "HIGH", "MEDIUM", "LOW"],
|
||||||
|
"ApprovalResult":["PENDING", "APPROVED", "REJECTED"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 엔드포인트 → 스키마 매핑 (routers/ 분석으로 자동 보완)
|
||||||
|
ENDPOINT_SCHEMA: Dict[str, str] = {
|
||||||
|
"POST /api/tasks": "SRCreate",
|
||||||
|
"PATCH /api/tasks/status": "SRStatusUpdate",
|
||||||
|
"POST /api/approvals": "ApprovalCreate",
|
||||||
|
"POST /api/institutions": "InstitutionCreate",
|
||||||
|
"PUT /api/institutions": "InstitutionCreate",
|
||||||
|
"POST /api/servers": "ServerCreate",
|
||||||
|
"POST /api/incidents": "IncidentCreate",
|
||||||
|
"POST /api/change": "RFCCreate",
|
||||||
|
"POST /api/problems": "ProblemCreate",
|
||||||
|
"POST /api/catalog": "ServiceCatalogCreate",
|
||||||
|
}
|
||||||
|
|
||||||
|
def learn_from_source(self) -> Dict[str, Any]:
|
||||||
|
"""models.py AST 파싱으로 validation 규칙 추출."""
|
||||||
|
models_path = BASE_DIR / "models.py"
|
||||||
|
source = models_path.read_text(encoding="utf-8")
|
||||||
|
tree = ast.parse(source)
|
||||||
|
|
||||||
|
rules: List[Dict] = []
|
||||||
|
schemas_found: List[str] = []
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.ClassDef):
|
||||||
|
continue
|
||||||
|
# BaseModel 상속 클래스만
|
||||||
|
bases = [getattr(b, "id", "") for b in node.bases]
|
||||||
|
if "BaseModel" not in bases:
|
||||||
|
continue
|
||||||
|
|
||||||
|
class_name = node.name
|
||||||
|
schemas_found.append(class_name)
|
||||||
|
|
||||||
|
# 엔드포인트 찾기
|
||||||
|
endpoint = self._find_endpoint(class_name)
|
||||||
|
|
||||||
|
for item in node.body:
|
||||||
|
if not isinstance(item, ast.AnnAssign):
|
||||||
|
continue
|
||||||
|
rule = self._extract_field_rule(item, class_name, endpoint)
|
||||||
|
if rule:
|
||||||
|
rules.append(rule)
|
||||||
|
|
||||||
|
return {"rules": rules, "schemas": schemas_found}
|
||||||
|
|
||||||
|
def _extract_field_rule(self, node: ast.AnnAssign,
|
||||||
|
class_name: str, endpoint: str) -> Optional[Dict]:
|
||||||
|
"""단일 필드 annotation에서 validation 규칙 추출."""
|
||||||
|
if not isinstance(node.target, ast.Name):
|
||||||
|
return None
|
||||||
|
|
||||||
|
field_name = node.target.id
|
||||||
|
if field_name.startswith("_"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
annotation = node.annotation
|
||||||
|
# ast.AnnAssign: .value = 기본값 (없으면 None) → None이면 required
|
||||||
|
is_required = node.value is None
|
||||||
|
field_type = "str"
|
||||||
|
allowed_values: List[str] = []
|
||||||
|
constraints: Dict = {}
|
||||||
|
|
||||||
|
# 타입 분석
|
||||||
|
type_str = ast.unparse(annotation) if hasattr(ast, "unparse") else str(annotation)
|
||||||
|
|
||||||
|
# Optional[X] → is_required=False
|
||||||
|
if "Optional" in type_str:
|
||||||
|
is_required = False
|
||||||
|
inner = re.sub(r"Optional\[(.+)\]", r"\1", type_str)
|
||||||
|
type_str = inner
|
||||||
|
|
||||||
|
# Enum 타입
|
||||||
|
for enum_name, vals in self.ENUM_MAP.items():
|
||||||
|
if enum_name in type_str:
|
||||||
|
field_type = "enum"
|
||||||
|
allowed_values = vals
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if "int" in type_str:
|
||||||
|
field_type = "int"
|
||||||
|
elif "float" in type_str:
|
||||||
|
field_type = "float"
|
||||||
|
elif "bool" in type_str:
|
||||||
|
field_type = "bool"
|
||||||
|
elif "List" in type_str or "list" in type_str:
|
||||||
|
field_type = "list"
|
||||||
|
elif "datetime" in type_str.lower():
|
||||||
|
field_type = "datetime"
|
||||||
|
else:
|
||||||
|
field_type = "str"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"schema_class": class_name,
|
||||||
|
"field_name": field_name,
|
||||||
|
"field_type": field_type,
|
||||||
|
"is_required": is_required,
|
||||||
|
"allowed_values": allowed_values,
|
||||||
|
"constraints": constraints,
|
||||||
|
"learned_at": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _find_endpoint(self, class_name: str) -> str:
|
||||||
|
for ep, schema in self.ENDPOINT_SCHEMA.items():
|
||||||
|
if schema == class_name:
|
||||||
|
return ep
|
||||||
|
# 자동 추론: SRCreate → POST /api/tasks (by name pattern)
|
||||||
|
name_lower = class_name.lower().replace("create", "").replace("update", "")
|
||||||
|
return f"POST /api/{name_lower}s"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Validation 검증기 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class RPAValidator:
|
||||||
|
"""tb_rpa_validation 규칙으로 payload 검증."""
|
||||||
|
|
||||||
|
def __init__(self, rules: List[Dict]):
|
||||||
|
self.rules = {r["field_name"]: r for r in rules}
|
||||||
|
|
||||||
|
def validate(self, payload: Dict[str, Any]) -> List[str]:
|
||||||
|
"""
|
||||||
|
payload 검증. 오류 목록 반환 (빈 리스트 = 통과).
|
||||||
|
"""
|
||||||
|
errors: List[str] = []
|
||||||
|
|
||||||
|
for field_name, rule in self.rules.items():
|
||||||
|
val = payload.get(field_name)
|
||||||
|
|
||||||
|
# 필수 필드 검사
|
||||||
|
if rule["is_required"] and (val is None or val == ""):
|
||||||
|
errors.append(f"[{field_name}] 필수 항목입니다.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if val is None:
|
||||||
|
continue # optional이고 값 없으면 skip
|
||||||
|
|
||||||
|
# Enum 검사
|
||||||
|
if rule["field_type"] == "enum" and rule["allowed_values"]:
|
||||||
|
if val not in rule["allowed_values"]:
|
||||||
|
errors.append(
|
||||||
|
f"[{field_name}] 허용값: {rule['allowed_values']} 중 하나여야 합니다. (입력: {val!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 타입 검사
|
||||||
|
elif rule["field_type"] == "int":
|
||||||
|
try:
|
||||||
|
int(val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
errors.append(f"[{field_name}] 정수 타입이어야 합니다.")
|
||||||
|
|
||||||
|
elif rule["field_type"] == "bool":
|
||||||
|
if not isinstance(val, bool):
|
||||||
|
errors.append(f"[{field_name}] 불리언(true/false) 타입이어야 합니다.")
|
||||||
|
|
||||||
|
# 제약 조건 검사
|
||||||
|
c = rule.get("constraints", {})
|
||||||
|
if c.get("max_length") and isinstance(val, str):
|
||||||
|
if len(val) > c["max_length"]:
|
||||||
|
errors.append(f"[{field_name}] 최대 {c['max_length']}자 초과.")
|
||||||
|
if c.get("min_length") and isinstance(val, str):
|
||||||
|
if len(val) < c["min_length"]:
|
||||||
|
errors.append(f"[{field_name}] 최소 {c['min_length']}자 이상 필요.")
|
||||||
|
if c.get("ge") is not None and isinstance(val, (int, float)):
|
||||||
|
if val < c["ge"]:
|
||||||
|
errors.append(f"[{field_name}] {c['ge']} 이상이어야 합니다.")
|
||||||
|
if c.get("le") is not None and isinstance(val, (int, float)):
|
||||||
|
if val > c["le"]:
|
||||||
|
errors.append(f"[{field_name}] {c['le']} 이하여야 합니다.")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
# ── RPA 실행 엔진 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class RPAExecutor:
|
||||||
|
"""RPA 작업 실행기 — validation 후 ITSM API 호출."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, token: str):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
task_type: str,
|
||||||
|
payload: Dict[str, Any],
|
||||||
|
dry_run: bool = False,
|
||||||
|
retry: int = 3,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
단발성 RPA 실행.
|
||||||
|
dry_run=True → validation만 수행, API 호출 없음.
|
||||||
|
"""
|
||||||
|
endpoint_map = {
|
||||||
|
"SR_CREATE": ("POST", "/api/tasks"),
|
||||||
|
"SR_STATUS_UPDATE": ("PATCH", f"/api/tasks/{payload.get('sr_id', 0)}/status"),
|
||||||
|
"APPROVAL_PROCESS": ("POST", "/api/approvals"),
|
||||||
|
"INCIDENT_CREATE": ("POST", "/api/incidents"),
|
||||||
|
"SHELL_EXEC": ("POST", "/api/ssh/exec"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if task_type not in endpoint_map:
|
||||||
|
return {"status": "FAILED", "error": f"알 수 없는 task_type: {task_type}"}
|
||||||
|
|
||||||
|
method, path = endpoint_map[task_type]
|
||||||
|
result = {"task_type": task_type, "endpoint": f"{method} {path}", "dry_run": dry_run}
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
result["status"] = "DRY_RUN_OK"
|
||||||
|
result["message"] = "Validation 통과. dry_run=true이므로 실제 실행 생략."
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 실제 API 호출 (재시도 포함)
|
||||||
|
url = f"{self.base_url}{path}"
|
||||||
|
last_err = None
|
||||||
|
async with httpx.AsyncClient(timeout=30) as client:
|
||||||
|
for attempt in range(1, retry + 1):
|
||||||
|
try:
|
||||||
|
resp = getattr(client, method.lower())
|
||||||
|
r = await resp(url, json=payload, headers=self.headers)
|
||||||
|
if r.status_code < 300:
|
||||||
|
result["status"] = "SUCCESS"
|
||||||
|
result["response"] = r.json()
|
||||||
|
return result
|
||||||
|
elif r.status_code < 500:
|
||||||
|
# 4xx → 재시도 없음
|
||||||
|
result["status"] = "FAILED"
|
||||||
|
result["error"] = r.json()
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
last_err = f"HTTP {r.status_code}: {r.text[:200]}"
|
||||||
|
except Exception as e:
|
||||||
|
last_err = str(e)
|
||||||
|
|
||||||
|
if attempt < retry:
|
||||||
|
import asyncio
|
||||||
|
await asyncio.sleep(2 ** attempt) # 지수 백오프
|
||||||
|
|
||||||
|
result["status"] = "FAILED"
|
||||||
|
result["error"] = f"{retry}회 재시도 후 실패: {last_err}"
|
||||||
|
return result
|
||||||
2
main.py
2
main.py
@ -58,6 +58,7 @@ from routers import (
|
|||||||
dr,
|
dr,
|
||||||
network_devices,
|
network_devices,
|
||||||
autonomous,
|
autonomous,
|
||||||
|
rpa,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -302,6 +303,7 @@ app.include_router(export_import.router) # 폐쇄망 ↔ 개방망 Export/Im
|
|||||||
app.include_router(dr.router) # DR 자동화 (Failover/RTO-RPO/백업검증)
|
app.include_router(dr.router) # DR 자동화 (Failover/RTO-RPO/백업검증)
|
||||||
app.include_router(network_devices.router) # 네트워크 장비 관리 (스위치/라우터/방화벽)
|
app.include_router(network_devices.router) # 네트워크 장비 관리 (스위치/라우터/방화벽)
|
||||||
app.include_router(autonomous.router) # 자율 운영 (자동처리/승인 게이트)
|
app.include_router(autonomous.router) # 자율 운영 (자동처리/승인 게이트)
|
||||||
|
app.include_router(rpa.router) # RPA 봇 (Validation 학습 + 자동화 실행)
|
||||||
|
|
||||||
|
|
||||||
# ── 개방망 보안 헤더 미들웨어 ────────────────────────────────────────────────
|
# ── 개방망 보안 헤더 미들웨어 ────────────────────────────────────────────────
|
||||||
|
|||||||
352
routers/rpa.py
Normal file
352
routers/rpa.py
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
"""
|
||||||
|
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, "실행 이력을 찾을 수 없습니다.")
|
||||||
Loading…
Reference in New Issue
Block a user