Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: CI

# PR 트리거 한정 — push마다 분당 비용을 누적하지 않는다.
on:
pull_request:
branches: [master]
workflow_dispatch:

jobs:
python-lint:
name: Python Lint (ruff)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: ruff 설치
run: pip install ruff==0.5.7

- name: ruff 검사
# main.py, learning_store.py, pattern_index.py, tests/ 전체
run: ruff check .

- name: ruff 포맷 검사
run: ruff format --check .

compose-validate:
name: docker-compose 구문 검증
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

- name: docker compose config 검증
# 시크릿 미설정 상태에서도 schema/필수 키 검사
run: docker compose -f docker-compose.yml config --quiet

yaml-lint:
name: YAML 구문 검증
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: PyYAML 설치
run: pip install pyyaml

- name: 모든 YAML 파일 syntax 검증
run: |
python3 -c "
import yaml, glob, sys
ok = True
for f in glob.glob('**/*.yml', recursive=True) + glob.glob('**/*.yaml', recursive=True):
try:
yaml.safe_load(open(f))
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The YAML validation script uses yaml.safe_load(open(f)), which leaves file handles unclosed and only parses the first YAML document in a file. Use a with open(..., encoding='utf-8') block and ensure all documents are consumed (e.g., iterate safe_load_all) so the check reliably validates the entire file contents.

Suggested change
yaml.safe_load(open(f))
with open(f, encoding='utf-8') as fp:
for _ in yaml.safe_load_all(fp):
pass

Copilot uses AI. Check for mistakes.
print(f'OK {f}')
except yaml.YAMLError as e:
ok = False
print(f'FAIL {f}: {e}')
sys.exit(0 if ok else 1)
"
Comment on lines +49 to +69
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yaml-lint step runs import yaml but the workflow never installs PyYAML (it’s not in the stdlib and isn’t listed in requirements). This job will fail with ModuleNotFoundError: No module named 'yaml' on a fresh runner. Install pyyaml (or vendor another YAML parser) before running the validation script.

Copilot uses AI. Check for mistakes.

# 패턴 JSON 파일 + tests 폴더 무결성 — 패턴 라이브러리가 깨지면 서버 기동 실패하므로
# PR 단계에서 catch.
patterns-validate:
name: 패턴 JSON 검증
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

- name: patterns/*.json syntax 검증
run: |
python3 -c "
import json, glob, sys
ok = True
for f in glob.glob('patterns/*.json'):
try:
with open(f) as fp:
json.load(fp)
print(f'OK {f}')
except json.JSONDecodeError as e:
ok = False
print(f'FAIL {f}: {e}')
sys.exit(0 if ok else 1)
"
20 changes: 16 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,33 @@ def _learned_case_to_result(semantic: Dict[str, Any]) -> "AnalysisResult":
match_method="semantic",
)


# FastAPI 앱 초기화
app = FastAPI(
title="AI DevOps Orchestrator API",
description="LangChain 기반 자동 트러블슈팅 및 배포 파이프라인",
version="1.0.0"
version="1.0.0",
)

# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:5678").split(","),
allow_origins=os.getenv(
"CORS_ORIGINS", "http://localhost:3000,http://localhost:5678"
).split(","),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


# 데이터 모델
class AnalysisRequest(BaseModel):
error_log: str
project_config: Dict[str, Any]
framework: str = "unknown"


class AnalysisResult(BaseModel):
pattern_type: str
confidence: float
Expand All @@ -114,6 +119,7 @@ class AnalysisResult(BaseModel):
estimated_time_saved: str
match_method: str = "none" # "exact" | "semantic" | "none"


# 헬스체크 엔드포인트
@app.get("/health")
async def health_check():
Expand All @@ -132,6 +138,7 @@ async def health_check():
"note": "정확 매칭 + 의미 매칭(큐레이션 패턴 + 학습 케이스). LangChain/Redis 미구현.",
}


# 에러 분석 엔드포인트
@app.post("/analyze", response_model=AnalysisResult)
async def analyze_error(request: AnalysisRequest):
Expand Down Expand Up @@ -200,7 +207,9 @@ class SearchRequest(BaseModel):
async def semantic_search(request: SearchRequest):
"""ChromaDB 기반 패턴 의미 검색 (정확 매칭 없이 직접 호출)."""
if not SEMANTIC_INDEX_READY:
raise HTTPException(status_code=503, detail="ChromaDB 의미 검색이 비활성화돼 있습니다")
raise HTTPException(
status_code=503, detail="ChromaDB 의미 검색이 비활성화돼 있습니다"
)
result = pattern_index.search(request.query, max_distance=request.max_distance)
if result is None:
return {"matched": False}
Expand All @@ -216,6 +225,7 @@ async def semantic_search(request: SearchRequest):
},
}


# 학습 엔드포인트
class LearnSuccessRequest(BaseModel):
error_log: Optional[str] = None
Expand Down Expand Up @@ -314,6 +324,8 @@ async def get_learning_cases(case_type: Optional[str] = None, limit: int = 50):
"cases": learning_store.list_cases(case_type=case_type, limit=limit),
}


if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

uvicorn.run(app, host="0.0.0.0", port=8000)
2 changes: 1 addition & 1 deletion tests/test_pattern_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_exact_match_no_match():
def test_semantic_match_pure_rand_variant():
"""정확 토큰 없이도 의미 기반으로 매칭되어야 한다."""
import pattern_index
from main import PATTERNS, SEMANTIC_INDEX_READY
from main import SEMANTIC_INDEX_READY

if not SEMANTIC_INDEX_READY:
import pytest
Expand Down
Loading