From 9eaba046ab39e4efa2d018b7766f17e4a82e0bdc Mon Sep 17 00:00:00 2001 From: "LASTA_DEV01\\lasta" Date: Wed, 20 May 2026 13:25:27 +0900 Subject: [PATCH] [] --- IMPLEMENTATION_SUMMARY.md | 552 ------ Instructor_분석_및_기능명세.md | 586 ------- ONTOLOGY_PLATFORM_MASTER_DESIGN.md | 649 ------- ONTOLOGY_PLATFORM_OVERVIEW.md | 598 ------- Ontology Platform Research Report.docx | Bin 34101 -> 0 bytes PHASE2_COMPLETION.md | 166 -- PHASE3_COMPLETION.md | 223 --- PHASE3_OPTION_B.md | 222 --- PHASE4_COMPLETION.md | 383 ---- PHASE5_COMPLETION.md | 312 ---- PHASE5_REFERENCE_ANALYSIS.md | 67 - PHASE_5_SUMMARY.md | 435 ----- PHASE_6_API_GUIDE.md | 678 ------- PHASE_7_IMPLEMENTATION_SUMMARY.md | 617 ------- PHASE_7_LLM_GUIDE.md | 645 ------- PHASE_8_COMPLETION_SUMMARY.md | 539 ------ PHASE_8_ENTERPRISE_PLAN.md | 645 ------- PROCESSING_REPORT_2026-05-11.md | 88 - PROCESS_OWNER_ONLY_2026-05-11.md | 81 - Playwright_분석_및_기능명세.md | 882 ---------- README_KO.md | 452 ----- README_old.md | 234 --- RESPONSIBILITY_REFACTOR_REPORT_2026-05-11.md | 263 --- UI_REBUILD_PLAN.md | 160 -- docker-compose.neo4j.yml | 40 - ontology_platform_research_report.md | 1653 ------------------ pyproject.toml | 23 - requirements.txt | 17 - start_webserver.bat | 54 - test_extraction.py | 62 - test_phase0_extraction.py | 97 - test_phase2_crawl.py | 66 - test_phase3_option_b.py | 217 --- test_phase3_validation.py | 214 --- test_phase4_integration.py | 376 ---- test_phase5_entity_resolver.py | 264 --- test_phase5_graph_analytics.py | 307 ---- test_phase5_integration_graphrag.py | 309 ---- test_phase5_pattern_matcher.py | 356 ---- test_phase5_subgraph_retriever.py | 224 --- test_phase6_api.py | 414 ----- 온톨로지플랫폼_통합설계서.md | 1181 ------------- 42 files changed, 15351 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md delete mode 100644 Instructor_분석_및_기능명세.md delete mode 100644 ONTOLOGY_PLATFORM_MASTER_DESIGN.md delete mode 100644 ONTOLOGY_PLATFORM_OVERVIEW.md delete mode 100644 Ontology Platform Research Report.docx delete mode 100644 PHASE2_COMPLETION.md delete mode 100644 PHASE3_COMPLETION.md delete mode 100644 PHASE3_OPTION_B.md delete mode 100644 PHASE4_COMPLETION.md delete mode 100644 PHASE5_COMPLETION.md delete mode 100644 PHASE5_REFERENCE_ANALYSIS.md delete mode 100644 PHASE_5_SUMMARY.md delete mode 100644 PHASE_6_API_GUIDE.md delete mode 100644 PHASE_7_IMPLEMENTATION_SUMMARY.md delete mode 100644 PHASE_7_LLM_GUIDE.md delete mode 100644 PHASE_8_COMPLETION_SUMMARY.md delete mode 100644 PHASE_8_ENTERPRISE_PLAN.md delete mode 100644 PROCESSING_REPORT_2026-05-11.md delete mode 100644 PROCESS_OWNER_ONLY_2026-05-11.md delete mode 100644 Playwright_분석_및_기능명세.md delete mode 100644 README_KO.md delete mode 100644 README_old.md delete mode 100644 RESPONSIBILITY_REFACTOR_REPORT_2026-05-11.md delete mode 100644 UI_REBUILD_PLAN.md delete mode 100644 docker-compose.neo4j.yml delete mode 100644 ontology_platform_research_report.md delete mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 start_webserver.bat delete mode 100644 test_extraction.py delete mode 100644 test_phase0_extraction.py delete mode 100644 test_phase2_crawl.py delete mode 100644 test_phase3_option_b.py delete mode 100644 test_phase3_validation.py delete mode 100644 test_phase4_integration.py delete mode 100644 test_phase5_entity_resolver.py delete mode 100644 test_phase5_graph_analytics.py delete mode 100644 test_phase5_integration_graphrag.py delete mode 100644 test_phase5_pattern_matcher.py delete mode 100644 test_phase5_subgraph_retriever.py delete mode 100644 test_phase6_api.py delete mode 100644 온톨로지플랫폼_통합설계서.md diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 4b129e5..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,552 +0,0 @@ -# Ontology Platform: Phase 0-4 구현 완료 보고서 - -**완료일**: 2026-05-14 -**총 작업 기간**: Phase 0 ~ Phase 4 -**상태**: ✅ 모든 Phase 구현 완료 - -## 프로젝트 개요 - -온톨로지 플랫폼은 웹 콘텐츠에서 구조화된 지식(엔티티/관계)을 자동으로 추출하고, 검증하며, 그래프 형태로 저장하고 검색하는 종합 시스템입니다. - -### 설계 원칙 -- **Phase-gated**: 각 Phase는 독립적이며 필요에 따라 선택 가능 -- **Pluggable**: 여러 구현 옵션 간에 자유로운 전환 -- **Async-first**: 높은 동시성과 확장성 -- **Graceful degradation**: 의존성 부재 시에도 동작 - -## Phase 별 구현 요약 - -### Phase 0-1: 콘텐츠 추출 (기본, 필수) - -**목표**: 웹 URL에서 텍스트와 메타데이터 추출 -**시간**: 10-15초/URL - -**기술 스택**: -- **Trafilatura**: HTML 파싱 및 텍스트 추출 -- **메타데이터**: 제목, 저자, 발행일, 언어 - -**핵심 클래스**: -- `extract_web_content()`: URL → 정제된 텍스트 + 메타데이터 -- `WebContent`: 추출 결과 데이터 모델 - -**테스트**: `test_phase0_extraction.py` ✅ - ---- - -### Phase 2: 동적 페이지 크롤링 (선택) - -**목표**: JavaScript로 렌더링되는 페이지 지원 -**시간**: 20-30초/URL (동적) - -**기술 스택**: -- **Crawl4AI**: 브라우저 기반 크롤링 -- **Profile-based selection**: 페이지 유형별 최적 전략 -- **Fallback mechanism**: 실패 시 기본 HTTP 재시도 - -**프로필**: -| Profile | 대상 | 성능 | -|---------|------|------| -| FAST_STATIC | 정적 HTML | 5-10초 | -| DYNAMIC_PAGE | JS 렌더링 | 15-30초 | -| FULL_CAPTURE | 완전 캡처 | 30-60초 | - -**핵심 클래스**: -- `Crawl4AIAdapter`: Crawl4AI 래퍼 -- `BasicCrawler`: HTTP 폴백 -- `CrawlProfile`: 프로필 열거형 - -**테스트**: `test_phase2_crawl.py` ✅ - ---- - -### Phase 3: 검증 (pluggable) - -**목표**: 추출된 엔티티/관계 검증 -**옵션**: A (경량 MVP) 또는 B (Hybrid SPARQL) - -#### Option A: 경량 검증 (기본) -``` -엔티티 검증: - ✓ ID 형식 (E_xxx) - ✓ Confidence 범위 (0.0-1.0) - ✓ 필수 필드 (label, type) - -관계 검증: - ✓ 종료점 존재 확인 - ✓ Self-loop 방지 - ✓ Confidence 범위 -``` - -**핵심 클래스**: -- `LightweightValidator`: Pydantic 기반 검증 -- `OntologyGuard`: 검증 파사드 - -**테스트**: `test_phase3_validation.py` ✅ - -#### Option B: Hybrid SPARQL 검증 (추가) -``` -SPARQL 검증: - ✓ 문법 검사 (괄호, 키워드) - ✓ 작업 순서 (INSERT → UPDATE → DELETE) - ✓ 프리픽스 선언 확인 - ✓ SQL 인젝션 패턴 감지 - -GraphUpdate 지원: - ✓ RDF 쿼리 유효성 - ✓ 작업 우선순위 검증 - ✓ 예비 준비됨: Critic loop -``` - -**핵심 클래스**: -- `SPARQLValidator`: SPARQL 문법 검증 -- `OntoCastValidator`: GraphUpdate 검증 - -**테스트**: `test_phase3_option_b.py` ✅ - ---- - -### Phase 4: 그래프 저장소 + 벡터 검색 (선택) - -**목표**: 엔티티/관계를 그래프 저장소에 저장하고 검색 -**옵션**: 4-Lite (Neo4j + Vector) 선택 - -**기술 스택**: -- **Neo4j**: Property Graph 데이터베이스 -- **SentenceTransformer**: 벡터 임베딩 (all-MiniLM-L6-v2, 384-dim) -- **Cosine Similarity**: 의미 유사도 검색 - -**핵심 클래스**: -- `Neo4jAdapter`: 비동기 Neo4j 클라이언트 - - `create_entity_nodes()`: 엔티티 노드 + 임베딩 - - `create_relation_edges()`: 관계 엣지 - - `vector_search()`: 벡터 유사도 검색 - - `get_entity_neighbors()`: 이웃 그래프 순회 - - `get_stats()`: 그래프 통계 - -**Docker 지원**: -```bash -docker-compose -f docker-compose.neo4j.yml up -d -``` - -**테스트**: `test_phase4_integration.py` ✅ - ---- - -## API 엔드포인트 전체 맵 - -### 추출 엔드포인트 - -#### POST /api/v1/extract/url -```python -# 파라미터 -url: str (필수) - 추출 대상 URL -profile: "fast_static" | "dynamic_page" (선택) - -# 응답 -{ - "url": "...", - "title": "...", - "author": "...", - "published_date": "...", - "language": "...", - "text_length": 5000, - "profile_used": "trafilatura", - "entities": [...], # Phase 3에서 검증됨 - "relations": [...], # Phase 3에서 검증됨 - "extraction_time_sec": 12.5, - "entity_count": 15, - "relation_count": 8, - "warnings": [], - "validation_passed": true, - "validation_errors": [] -} -``` - -### 검색 엔드포인트 (Phase 4) - -#### POST /api/v1/search/vector -```python -# 파라미터 -query: str (필수) - 검색 쿼리 -limit: int = 10 (1-100) -threshold: float = 0.5 (0.0-1.0) - -# 응답 -{ - "query": "Machine learning", - "results": [ - { - "id": "E_1", - "label": "Python", - "type": "ProgrammingLanguage", - "confidence": 0.95, - "similarity": 0.87 - }, - ... - ], - "result_count": 5, - "limit": 10, - "threshold": 0.5 -} -``` - -#### GET /api/v1/search/stats -```python -# 응답 -{ - "status": "connected", - "stats": { - "total_nodes": 1250, - "total_edges": 2100, - "entity_nodes": 1200 - } -} -``` - -#### GET /api/v1/search/entity/{entity_id} -```python -# 파라미터 -entity_id: str (필수) - 엔티티 ID -depth: int = 1 (1-2) - -# 응답 -{ - "entity": "E_1", - "label": "Python", - "type": "ProgrammingLanguage", - "neighbors": 3, - "relations": [ - { - "source": "Python", - "target": "Django", - "predicate": "RELATES", - "confidence": 0.85 - }, - ... - ] -} -``` - -#### POST /api/v1/search/ingest -```python -# 요청 본문 -{ - "entities": [ - { - "id": "E_1", - "label": "Python", - "type": "ProgrammingLanguage", - "confidence": 0.95 - }, - ... - ], - "relations": [ - { - "source_id": "E_1", - "target_id": "E_2", - "predicate": "used_in", - "confidence": 0.88 - }, - ... - ] -} - -# 응답 -{ - "status": "success", - "entities_ingested": 5, - "relations_ingested": 3, - "total_ingested": 8 -} -``` - ---- - -## 디렉토리 구조 - -``` -ontology_platform/ -├── ont_platform/ -│ ├── api/ -│ │ └── phase0_app.py # FastAPI 주 애플리케이션 -│ └── core/ -│ ├── extractors/ -│ │ └── web_extractor.py # Phase 0-1: Trafilatura -│ ├── crawler/ -│ │ └── crawl4ai_adapter.py # Phase 2: Crawl4AI -│ ├── extraction/ -│ │ └── lightweight_extractor.py # LightweightExtractor -│ ├── validation/ -│ │ ├── validators.py # Phase 3A: 경량 검증 -│ │ ├── ontocast_validator.py # Phase 3B: SPARQL 검증 -│ │ ├── models.py # Pydantic 모델 -│ │ └── guards.py # OntologyGuard -│ └── graph/ -│ └── neo4j_adapter.py # Phase 4: Neo4j -│ -├── docker-compose.neo4j.yml # Neo4j 컨테이너 -│ -├── test_phase0_extraction.py # Phase 0-1 테스트 -├── test_phase2_crawl.py # Phase 2 테스트 -├── test_phase3_validation.py # Phase 3A 테스트 -├── test_phase3_option_b.py # Phase 3B 테스트 -├── test_phase4_integration.py # Phase 4 통합 테스트 -│ -├── PHASE2_COMPLETION.md # Phase 2 완료 보고서 -├── PHASE3_COMPLETION.md # Phase 3A 완료 보고서 -├── PHASE3_OPTION_B.md # Phase 3B 상세 설계 -├── PHASE4_COMPLETION.md # Phase 4 완료 보고서 -└── IMPLEMENTATION_SUMMARY.md # 이 문서 -``` - ---- - -## 설정 및 의존성 - -### 필수 패키지 -```bash -pip install fastapi==0.109.0 -pip install uvicorn==0.27.0 -pip install pydantic==2.5.0 -pip install trafilatura==2.0.0 -pip install httpx==0.26.0 -``` - -### 선택적 패키지 - -**Phase 2 (동적 페이지)**: -```bash -pip install crawl4ai # 또는 사용자 설치 버전 -``` - -**Phase 3B (OntoCast)**: -```bash -# OntoCastValidator는 자체 포함됨 -# SPARQL 검증만 제공 (Critic loop는 Phase 4+) -``` - -**Phase 4 (Neo4j)**: -```bash -pip install neo4j==6.2.0 -pip install sentence-transformers==5.5.0 -``` - ---- - -## 사용 시나리오 - -### 시나리오 1: 빠른 추출 (Phase 0-1만) -```bash -# 정적 웹페이지에서 빠르게 추출 -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://example.com" - -# 응답: 엔티티/관계 즉시 반환 (10-15초) -``` - -### 시나리오 2: 동적 페이지 포함 (Phase 0-2) -```bash -# JavaScript로 렌더링되는 페이지 지원 -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://spa.example.com&profile=dynamic_page" - -# 응답: 동적 콘텐츠도 추출 (20-30초) -``` - -### 시나리오 3: 검증 강화 (Phase 0-3A) -```bash -# 기본 설정: 경량 검증 (엔티티/관계) -# OntologyGuard(validator_type="lightweight") - -# 또는 SPARQL 검증 (Phase 3B) -# OntologyGuard(validator_type="ontocast") -``` - -### 시나리오 4: 그래프 기반 검색 (Phase 0-4) -```bash -# 1. 추출 -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://example.com" - -# 2. 수집 (Neo4j에 저장) -curl -X POST "http://localhost:8000/api/v1/search/ingest" \ - -d '{"entities": [...], "relations": [...]}' - -# 3. 벡터 검색 -curl "http://localhost:8000/api/v1/search/vector?query=python+programming" - -# 4. 이웃 탐색 -curl "http://localhost:8000/api/v1/search/entity/E_1" - -# 5. 통계 조회 -curl "http://localhost:8000/api/v1/search/stats" -``` - ---- - -## 성능 특성 - -### 추출 성능 -| Phase | 기술 | 시간 | 메모리 | -|-------|------|------|--------| -| 0-1 | Trafilatura | 10-15초 | ~50MB | -| 2 | Crawl4AI | 20-30초 | ~200MB | - -### 검증 성능 -| 옵션 | 기술 | 시간 | 메모리 | -|------|------|------|--------| -| 3A | Pydantic | <100ms | ~10MB | -| 3B | SPARQL | <500ms | ~10MB | - -### 그래프 성능 (Phase 4) -| 작업 | 시간 | 확장성 | -|------|------|--------| -| 노드 생성 | 10-50ms | 배치 최적화 가능 | -| 벡터 검색 | 50-200ms | GDS 라이브러리로 확장 | -| 이웃 순회 | 20-100ms | 깊이 1-2로 제한 | - ---- - -## 향후 확장 계획 - -### Phase 5: GraphRAG (선택) -```python -# 복잡한 쿼리와 컨텍스트 검색 -- RDF ↔ Property Graph 변환 -- Entity Resolver (중복 제거) -- Subgraph retrieval -- Complex pattern matching -``` - -### Phase 5+: Advanced Features -```python -# LLM 기반 개선 -- Critic loop (자동 수정) -- Few-shot learning -- Relation extraction 개선 -- Zero-shot 엔티티 분류 -``` - ---- - -## 테스트 결과 요약 - -| Phase | 테스트 | 결과 | 세부사항 | -|-------|--------|------|---------| -| 0-1 | `test_phase0_extraction.py` | ✅ PASS | URL 추출 10초 이내 | -| 2 | `test_phase2_crawl.py` | ✅ PASS | Profile 기반 크롤링 | -| 3A | `test_phase3_validation.py` | ✅ PASS | 5/5 검증 규칙 | -| 3B | `test_phase3_option_b.py` | ✅ PASS | 6/6 SPARQL 검증 | -| 4 | `test_phase4_integration.py` | ✅ PASS | 2/8 통과 (Neo4j 필요) | - ---- - -## 배포 및 운영 - -### 개발 환경 -```bash -# 1. 저장소 클론 -git clone && cd ontology_platform - -# 2. 의존성 설치 -pip install -r requirements.txt -pip install -r requirements-optional.txt # Phase 2/4용 - -# 3. Neo4j 시작 (Phase 4 필요 시) -docker-compose -f docker-compose.neo4j.yml up -d - -# 4. API 서버 시작 -python -m uvicorn ontology_platform.ont_platform.api.phase0_app:app --reload - -# 5. 테스트 실행 -python test_phase0_extraction.py -python test_phase2_crawl.py -python test_phase3_validation.py -python test_phase3_option_b.py -python test_phase4_integration.py -``` - -### 프로덕션 배포 -```bash -# 1. Docker 이미지 빌드 -docker build -t ontology-platform:0.4.0 . - -# 2. docker-compose로 전체 스택 배포 -docker-compose -f docker-compose.yml up -d - -# 3. 헬스 체크 -curl http://localhost:8000/health - -# 4. API 문서 -http://localhost:8000/docs (Swagger UI) -http://localhost:8000/redoc (ReDoc) -``` - ---- - -## 아키텍처 다이어그램 - -``` -┌────────────────────────────────────────────────────────┐ -│ Ontology Platform Stack │ -├────────────────────────────────────────────────────────┤ -│ │ -│ Phase 0-1: Content Extraction │ -│ ┌────────────────────────────────────────────────┐ │ -│ │ FastAPI Endpoint: POST /api/v1/extract/url │ │ -│ │ └─ Trafilatura (static) or Crawl4AI (dynamic) │ │ -│ │ └─ Output: WebContent { text, metadata } │ │ -│ └────────────────────────────────────────────────┘ │ -│ ↓ │ -│ Phase 3: Validation (Pluggable) │ -│ ┌────────────────────────────────────────────────┐ │ -│ │ LightweightValidator (Option A) │ │ -│ │ OntoCastValidator (Option B - SPARQL) │ │ -│ │ └─ Output: OntologyExtractionResult │ │ -│ │ { entities, relations, validation_passed } │ │ -│ └────────────────────────────────────────────────┘ │ -│ ↓ │ -│ Phase 4: Graph Storage & Search (Optional) │ -│ ┌────────────────────────────────────────────────┐ │ -│ │ Neo4j Adapter │ │ -│ │ ├─ POST /api/v1/search/ingest │ │ -│ │ ├─ POST /api/v1/search/vector (semantic) │ │ -│ │ ├─ GET /api/v1/search/stats │ │ -│ │ └─ GET /api/v1/search/entity/{id} │ │ -│ │ │ │ -│ │ [Entity Nodes] ──(RELATES)──> [Entity Nodes] │ │ -│ │ + embedding vectors (384-dim) │ │ -│ └────────────────────────────────────────────────┘ │ -│ │ -└────────────────────────────────────────────────────────┘ -``` - ---- - -## 주요 특징 요약 - -✅ **Phase-gated Architecture**: 각 Phase는 독립적이며 필요에 따라 선택 가능 -✅ **Pluggable Validators**: 경량(Pydantic) 또는 SPARQL 기반 검증 -✅ **Async/Await**: 높은 동시성과 확장성 -✅ **Graceful Degradation**: 의존성(Crawl4AI, Neo4j) 부재 시에도 동작 -✅ **Comprehensive Testing**: 6개 테스트 스위트, 20+ 테스트 케이스 -✅ **Full Documentation**: 각 Phase별 상세 설계 및 API 문서 -✅ **Docker Support**: Neo4j 컨테이너 + 프로덕션 배포 준비 - ---- - -## 문의 및 지원 - -### 기술 문서 -- [온톨로지플랫폼 통합설계서](온톨로지플랫폼_통합설계서.md) -- [Phase 2 완료 보고서](PHASE2_COMPLETION.md) -- [Phase 3 완료 보고서](PHASE3_COMPLETION.md) -- [Phase 3 Option B](PHASE3_OPTION_B.md) -- [Phase 4 완료 보고서](PHASE4_COMPLETION.md) - -### API 문서 -서버 시작 후: -- Swagger UI: http://localhost:8000/docs -- ReDoc: http://localhost:8000/redoc - ---- - -**작성일**: 2026-05-14 -**버전**: 0.4.0 (Phase 0-4 완료) diff --git a/Instructor_분석_및_기능명세.md b/Instructor_분석_및_기능명세.md deleted file mode 100644 index 6ae8276..0000000 --- a/Instructor_분석_및_기능명세.md +++ /dev/null @@ -1,586 +0,0 @@ -# Instructor 분석 및 범용 온톨로지 구축 플랫폼 기능명세 - 2순위 검토 - -## 1. 분석 대상 - -- 원본 경로: `C:\Users\lasta\MyProject\AI\참고\instructor-main` -- 프로젝트명: `instructor` -- 확인 버전: `1.15.1` -- 라이선스: MIT -- 언어/런타임: Python `>=3.9,<4.0` -- 성격: LLM 응답을 Pydantic 모델로 강제 변환하고 검증하는 구조화 출력 라이브러리 -- 핵심 가치: 자연어/문서/이미지 입력에서 엔티티, 관계, 속성, 근거를 안정적인 JSON/Pydantic 객체로 추출 - -Instructor는 크롤러나 온톨로지 저장소가 아니라, LLM 기반 추출 단계의 신뢰성 레이어다. 범용 온톨로지 구축 플랫폼에서는 “웹/문서에서 수집한 비정형 텍스트를 명세된 스키마로 추출하고, 검증 실패 시 자동 재질문하며, 결과를 typed 객체로 돌려주는 모듈”로 거의 원형 그대로 사용할 수 있다. - -## 2. 프로젝트 구조 요약 - -```text -instructor-main/ - instructor/ - __init__.py # 공개 API export - auto_client.py # provider/model 문자열 기반 자동 클라이언트 생성 - mode.py # provider별 응답 처리 모드 enum - core/ - client.py # Instructor/AsyncInstructor 래퍼 API - patch.py # provider create() 함수 monkey patch - retry.py # tenacity 기반 재시도 및 reask - hooks.py # 이벤트 훅 - exceptions.py # 예외 모델 - processing/ - response.py # 중앙 dispatcher, mode별 request/response 처리 - function_calls.py # OpenAISchema, provider 응답 파싱 - schema.py # OpenAI/Anthropic/Gemini schema 생성 - multimodal.py # image/audio/pdf 메시지 변환 - dsl/ - partial.py # streaming partial object - iterable.py # streaming iterable extraction - maybe.py # 추출 실패 가능성을 모델화 - parallel.py # 병렬 tool call 모델 - citation.py # 원문 근거 quote 검증 mixin - simple_type.py # str/int 등 단순 타입 wrapper - providers/ - openai, anthropic, gemini, genai, bedrock, cohere, ... - batch/ - processor.py, request.py # 배치 요청 생성/처리 - cache/ - __init__.py # AutoCache, DiskCache, cache key - validation/ - llm_validators.py # LLM 기반 field validator - cli/ - batch/files/jobs/usage # CLI 유틸리티 - docs/ # 사용자 문서, 통합 가이드, 튜토리얼 - examples/ # 추출, 지식그래프, SQL, FastAPI, batch 예제 - tests/ # 단위/통합/LLM provider 테스트 -``` - -## 3. 핵심 실행 흐름 - -Instructor의 기본 동작은 다음 순서다. - -1. 사용자가 Pydantic `BaseModel`로 원하는 출력 스키마를 정의한다. -2. `instructor.from_provider("openai/gpt-4o")` 또는 `from_openai()`로 provider client를 래핑한다. -3. `client.chat.completions.create(response_model=MyModel, messages=[...])`를 호출한다. -4. `core.patch.patch()`가 provider의 `create()` 호출을 가로채 `response_model`, `max_retries`, `strict`, `context`, `hooks`, `cache`를 처리한다. -5. `processing.response.handle_response_model()`이 Pydantic 모델을 provider별 tool schema 또는 JSON schema 요청으로 변환한다. -6. `core.retry.retry_sync()` 또는 `retry_async()`가 provider 호출을 실행한다. -7. `processing.response.process_response()`가 raw LLM 응답을 `OpenAISchema.from_response()`로 넘겨 모드별 파서를 선택한다. -8. Pydantic 검증이 성공하면 typed model을 반환하고 `_raw_response`에 원본 provider 응답을 붙인다. -9. JSON 파싱 또는 Pydantic 검증 실패 시 `handle_reask_kwargs()`가 에러 내용을 다음 프롬프트에 반영해 재시도한다. -10. 모든 재시도 실패 시 `InstructorRetryException`에 실패 이력, 마지막 응답, 사용량, 재현 가능한 create kwargs를 담아 예외를 발생시킨다. - -이 흐름은 온톨로지 플랫폼에서 `문서 청크 -> 후보 엔티티/관계 추출 -> 스키마 검증 -> 실패 재시도 -> 근거 포함 결과 저장` 파이프라인으로 바로 매핑된다. - -## 4. 주요 공개 API - -### 4.1 클라이언트 생성 - -- `instructor.from_provider(model: str, async_client=False, cache=None, mode=None, **kwargs)` - - `"provider/model-name"` 형식으로 provider를 자동 선택한다. - - 지원 provider: `openai`, `azure_openai`, `anthropic`, `google`, `vertexai`, `mistral`, `cohere`, `perplexity`, `groq`, `writer`, `bedrock`, `cerebras`, `deepseek`, `fireworks`, `ollama`, `openrouter`, `xai`, `litellm`. - - 기본 모델명을 `Instructor.default_model`에 저장하고 호출 시 `model` 생략을 허용한다. - -- `instructor.from_openai(client, model=None, mode=Mode.TOOLS, **kwargs)` - - 기존 OpenAI 호환 client를 래핑한다. - - OpenAI-compatible endpoint, OpenRouter, Ollama, vLLM류 연동에 유리하다. - -- `instructor.patch(client=..., create=..., mode=...)` - - provider client 또는 독립 create 함수를 직접 patch한다. - - 기존 코드 변경을 최소화하면서 `response_model` 기능을 추가할 수 있다. - -### 4.2 구조화 호출 - -- `client.create(response_model, messages, max_retries=3, strict=True, context=None, hooks=None, **kwargs)` - - 가장 중요한 API다. - - 반환값은 raw JSON이 아니라 Pydantic model instance다. - - `response_model=None`이면 provider raw response를 그대로 반환한다. - -- `client.create_with_completion(...)` - - `(parsed_model, raw_completion)` tuple을 반환한다. - - 디버깅, 감사로그, 추출 근거 저장에 유용하다. - -- `client.create_iterable(response_model, messages, **kwargs)` - - streaming으로 여러 객체를 순차 반환한다. - - 긴 문서에서 엔티티/관계 후보를 점진적으로 받을 때 적합하다. - -- `client.create_partial(response_model, messages, **kwargs)` - - streaming 중 불완전한 partial model을 계속 반환한다. - - UI에서 추출 진행 상태를 보여주거나 긴 ontology 생성 작업을 관찰할 때 유용하다. - -### 4.3 DSL 타입 - -- `Partial[T]` - - 스트리밍 중 채워지는 부분 객체. - - 대형 ontology schema 추출의 진행률 표시와 중간 검증에 적합하다. - -- `IterableModel[T]` - - 하나의 LLM 응답에서 다수의 typed item을 순차 추출한다. - - `EntityCandidate`, `RelationCandidate` 목록 추출에 적합하다. - -- `Maybe(T)` - - `result`, `error`, `message`를 가진 wrapper 모델을 동적으로 만든다. - - “해당 청크에 관계가 없을 수도 있음” 같은 불확실성을 명시적으로 표현한다. - -- `CitationMixin` - - `substring_quotes` 필드를 통해 추출 결과의 원문 근거를 검증한다. - - `context={"context": 원문}`을 전달하면 quote가 실제 원문에 존재하는지 fuzzy matching으로 정리한다. - - 온톨로지 신뢰도, human review, provenance 저장에 중요하다. - -- `ModelAdapter`, simple type adapter - - `str`, `int`, `list[str]` 같은 단순 타입 응답을 Pydantic 검증 경로로 통합한다. - -### 4.4 검증/재시도 - -- `max_retries` - - int 또는 `tenacity.Retrying`/`AsyncRetrying` 객체를 받을 수 있다. - - 검증 실패, JSON 파싱 실패 시 자동 reask를 수행한다. - -- `strict` - - strict JSON/Pydantic validation 여부를 제어한다. - - ontology 저장 전 단계는 `strict=True`를 기본값으로 권장한다. - -- `context` - - Pydantic validator에 전달되는 runtime context다. - - 도메인 ontology, 허용 relation type, source document metadata, language 같은 동적 검증 조건을 전달할 수 있다. - -- `llm_validator(statement, client, allow_override=False, model=..., temperature=0)` - - 특정 필드를 LLM으로 한 번 더 검증한다. - - “관계명은 ontology relation vocabulary에 맞아야 한다” 같은 semantic validation에 사용할 수 있으나 비용과 지연이 있으므로 핵심 필드에 제한하는 것이 좋다. - -### 4.5 캐시 - -- `AutoCache(maxsize=128)` - - thread-safe in-memory LRU cache. - - schema, model, messages, mode를 기반으로 cache key를 만든다. - -- `DiskCache(directory=".instructor_cache")` - - optional `diskcache` 의존성 기반 persistent cache. - -- 캐시 key 구성 요소 - - provider/model - - messages 또는 contents/chat_history - - mode - - response_model JSON schema - -온톨로지 플랫폼에서는 동일 문서 청크와 동일 schema로 재처리할 때 비용 절감을 기대할 수 있다. 단, prompt에 시간/외부 상태가 들어가면 cache 오염을 막기 위해 cache scope를 작업 단위로 제한해야 한다. - -### 4.6 Hooks/Observability - -지원 이벤트: - -- `completion:kwargs`: provider 호출 직전 -- `completion:response`: provider 응답 직후 -- `parse:error`: JSON/Pydantic parsing 실패 -- `completion:last_attempt`: 마지막 시도 직전/시점 -- `completion:error`: provider/network 등 일반 오류 - -온톨로지 플랫폼에서는 이 훅을 사용해 추출 요청 로그, retry 사유, token usage, 실패 샘플, provider별 품질 통계를 저장할 수 있다. - -## 5. Provider/Mode 명세 - -`mode.py`는 provider별 요청 포맷과 응답 파싱 전략을 enum으로 정의한다. - -주요 모드: - -- OpenAI 계열: `TOOLS`, `TOOLS_STRICT`, `JSON`, `MD_JSON`, `JSON_SCHEMA`, `RESPONSES_TOOLS` -- Anthropic: `ANTHROPIC_TOOLS`, `ANTHROPIC_REASONING_TOOLS`, `ANTHROPIC_JSON`, `ANTHROPIC_PARALLEL_TOOLS` -- Google/Gemini: `GEMINI_JSON`, `GEMINI_TOOLS`, `GENAI_TOOLS`, `GENAI_STRUCTURED_OUTPUTS`, `VERTEXAI_TOOLS`, `VERTEXAI_JSON` -- Mistral/Cohere/Cerebras/Fireworks/Writer/Bedrock/XAI 등 provider 전용 모드 -- `PARALLEL_TOOLS`: OpenAI 병렬 tool call -- `OPENROUTER_STRUCTURED_OUTPUTS`: OpenRouter 구조화 출력 - -플랫폼 적용 권장: - -- 기본 OpenAI-compatible provider: `Mode.TOOLS` 또는 provider native structured output -- schema 엄격성이 중요한 ontology extraction: `TOOLS_STRICT` 또는 `JSON_SCHEMA` -- local/open-source 모델: `from_provider("ollama/model")`, OpenAI-compatible base_url 또는 LiteLLM 경유 -- 여러 추출 타입을 한 번에 받을 경우: `PARALLEL_TOOLS`는 유용하나 streaming 미지원이므로 대량 처리에는 분리 호출도 고려 - -## 6. 온톨로지 플랫폼에 필요한 기능 매핑 - -### 6.1 엔티티 추출 - -원본 Instructor 기능: - -- Pydantic `EntityCandidate` 모델 정의 -- `IterableModel[EntityCandidate]` 또는 `list[EntityCandidate]` 추출 -- field validator로 label normalization, type validation -- retry/reask로 누락/타입 오류 자동 수정 - -플랫폼 기능: - -- 문서 청크에서 개체명, 표준명, 별칭, 타입, 설명, 근거 quote, confidence 추출 -- 기존 ontology vocabulary와 비교해 허용 타입만 통과 -- 중복 후보 병합 전 structured candidate pool 생성 - -권장 모델 예시: - -```python -class EntityCandidate(CitationMixin): - name: str - canonical_name: str - entity_type: str - aliases: list[str] = [] - description: str | None = None - confidence: float -``` - -### 6.2 관계 추출 - -원본 Instructor 기능: - -- nested model, enum/literal validation -- `Maybe(RelationCandidate)`로 관계 부재 표현 -- `context` 기반 validator에서 허용 relation vocabulary 검사 - -플랫폼 기능: - -- source entity, target entity, predicate, direction, evidence, confidence 추출 -- entity 후보와 relation 후보를 분리 추출 후 graph builder에서 연결 -- 관계 근거가 없는 경우 저장하지 않고 review queue로 이동 - -권장 모델 예시: - -```python -class RelationCandidate(CitationMixin): - source_name: str - target_name: str - relation_type: str - relation_label: str - confidence: float -``` - -### 6.3 속성/스키마 추출 - -원본 Instructor 기능: - -- nested Pydantic models -- JSON schema 기반 출력 강제 -- `strict=True` validation - -플랫폼 기능: - -- 엔티티별 속성명, 값, 단위, 데이터 타입, source span 추출 -- domain schema 후보 생성 -- ontology class/property 자동 제안 - -### 6.4 근거와 provenance - -원본 Instructor 기능: - -- `CitationMixin` -- `_raw_response` 보존 -- `create_with_completion()` - -플랫폼 기능: - -- 각 triple 또는 property assertion에 source document id, chunk id, quote, model, prompt hash, raw completion id 저장 -- 신뢰도 낮은 결과를 human review로 라우팅 - -### 6.5 대량 처리 - -원본 Instructor 기능: - -- `batch/` 모듈 -- provider별 batch request 생성 -- `create_iterable()` streaming -- cache - -플랫폼 기능: - -- 크롤링된 문서 청크를 batch job으로 변환 -- provider batch API 또는 내부 queue worker에서 처리 -- 실패한 청크만 재시도 -- 같은 schema/prompt 조합 재처리 시 cache 사용 - -### 6.6 멀티모달 추출 - -원본 Instructor 기능: - -- `processing.multimodal.Image`, `Audio` -- provider별 message conversion -- PDF/image/audio 예제 포함 - -플랫폼 기능: - -- 문서 이미지, 표, 영수증, PDF에서 구조화 정보 추출 -- 온톨로지 구축 대상이 제품/인물/기관/문헌 등일 때 이미지 기반 보조 evidence 확보 - -## 7. 상세 기능명세 - -### F-INST-001 Provider Client Wrapping - -- 목적: 다양한 LLM provider를 동일한 structured output API로 호출한다. -- 입력: provider/model 문자열, API key, base_url, async 여부, mode -- 출력: `Instructor` 또는 `AsyncInstructor` -- 성공 조건: `client.chat.completions.create(response_model=...)` 호출 가능 -- 적용 우선도: 필수 -- 원본 사용 가능성: 거의 변형 없이 사용 - -### F-INST-002 Pydantic Response Model Extraction - -- 목적: 비정형 LLM 응답을 Pydantic 모델로 검증된 객체로 반환한다. -- 입력: `response_model`, `messages`, provider kwargs -- 출력: Pydantic model instance -- 오류: `ValidationError`, `JSONDecodeError`, `InstructorRetryException` -- 적용 우선도: 필수 -- 원본 사용 가능성: 그대로 사용 - -### F-INST-003 Automatic Reask/Retry - -- 목적: 스키마 검증 실패 시 오류 내용을 LLM에 전달해 자동 수정한다. -- 입력: 실패 응답, exception, failed_attempts, mode -- 출력: 수정된 kwargs/messages로 재시도 -- 설정: `max_retries`, `timeout`, tenacity policy -- 적용 우선도: 필수 -- 원본 사용 가능성: 그대로 사용하되 retry 횟수/timeout 정책은 플랫폼 설정화 필요 - -### F-INST-004 Strict Schema Validation - -- 목적: ontology 저장소에 잘못된 shape의 데이터를 넣지 않는다. -- 입력: Pydantic schema, JSON response, strict flag -- 출력: valid model 또는 validation error -- 적용 우선도: 필수 -- 원본 사용 가능성: 그대로 사용 - -### F-INST-005 Citation/Evidence Validation - -- 목적: 추출 결과가 원문에 기반하는지 확인한다. -- 입력: `CitationMixin` 모델, `context={"context": source_text}` -- 출력: 원문에 존재하는 quote로 정리된 `substring_quotes` -- 적용 우선도: 필수 -- 원본 사용 가능성: 대부분 사용 가능. 한국어/긴 문서 fuzzy match 성능은 추가 검증 필요 - -### F-INST-006 Maybe Wrapper - -- 목적: 추출 대상이 없을 수 있는 상황을 예외가 아니라 정상 결과로 표현한다. -- 입력: `Maybe(EntityCandidate)` 또는 `Maybe(RelationCandidate)` -- 출력: `{result, error, message}` -- 적용 우선도: 높음 -- 원본 사용 가능성: 그대로 사용 - -### F-INST-007 Iterable Extraction - -- 목적: 긴 응답에서 여러 후보 객체를 안정적으로 추출한다. -- 입력: item model, stream response -- 출력: item generator 또는 `ListResponse` -- 적용 우선도: 높음 -- 원본 사용 가능성: 그대로 사용 - -### F-INST-008 Partial Streaming - -- 목적: 추출 중간 결과를 UI/로그/작업 상태에 반영한다. -- 입력: `Partial[Model]`, `stream=True` -- 출력: partial model stream -- 적용 우선도: 중간 -- 원본 사용 가능성: 그대로 사용 - -### F-INST-009 Parallel Tool Extraction - -- 목적: 한 요청에서 여러 구조화 모델을 동시에 추출한다. -- 입력: model list 또는 parallel wrapper, `Mode.PARALLEL_TOOLS` -- 출력: 여러 typed model -- 제약: streaming 미지원 -- 적용 우선도: 중간 -- 원본 사용 가능성: 그대로 사용하되 대량 처리에서는 비용/재시도 단위를 고려 - -### F-INST-010 Cache - -- 목적: 동일 청크/동일 schema 추출 요청의 비용을 줄인다. -- 입력: cache backend, messages, model, mode, response_model schema -- 출력: cached model 또는 miss 후 저장 -- 적용 우선도: 높음 -- 원본 사용 가능성: `AutoCache`는 개발/단일 프로세스용으로 그대로 사용, 운영은 Redis/DB backend 구현 권장 - -### F-INST-011 Hooks and Audit Logging - -- 목적: 추출 호출, 응답, 실패, 재시도, 마지막 실패를 관측한다. -- 입력: hook handler -- 출력: 내부 이벤트 -- 적용 우선도: 필수 -- 원본 사용 가능성: 그대로 사용하되 플랫폼 audit logger와 연결 필요 - -### F-INST-012 LLM Semantic Validator - -- 목적: Pydantic으로 표현하기 어려운 의미 검증을 LLM에 위임한다. -- 입력: validation statement, value, validator model -- 출력: valid/fixed value 또는 validation error -- 적용 우선도: 선택 -- 원본 사용 가능성: 제한적으로 사용. 비용/재현성/지연시간 때문에 핵심 relation 검증에만 권장 - -### F-INST-013 Batch Processing - -- 목적: 많은 문서 청크를 provider batch API 또는 내부 batch 구조로 처리한다. -- 입력: batch requests, provider config -- 출력: batch job, results -- 적용 우선도: 높음 -- 원본 사용 가능성: OpenAI/Anthropic 중심으로 재사용 가능. 플랫폼 job queue와 통합 필요 - -### F-INST-014 Multimodal Input Conversion - -- 목적: 이미지, 오디오, PDF 등 비텍스트 입력을 provider 메시지 형식으로 변환한다. -- 입력: path/url/base64/data URI -- 출력: provider-ready content block -- 적용 우선도: 중간 -- 원본 사용 가능성: 그대로 사용 가능하나 provider별 비용/지원 범위 검증 필요 - -### F-INST-015 Raw Response Preservation - -- 목적: 추출 결과의 감사 가능성과 재현성을 확보한다. -- 입력: provider raw response -- 출력: parsed model의 `_raw_response` -- 적용 우선도: 필수 -- 원본 사용 가능성: 그대로 사용. 저장소에는 필요한 metadata만 선별 저장 권장 - -## 8. 플랫폼 아키텍처 적용안 - -권장 구성: - -```text -Crawler / Document Loader - -> Chunker - -> InstructorExtractionService - - provider client registry - - response model registry - - prompt template registry - - retry/cache/hooks policy - -> Candidate Normalizer - -> Entity Resolution - -> Ontology Graph Builder - -> Human Review Queue - -> Graph DB / Relational Store -``` - -`InstructorExtractionService`는 Instructor를 직접 노출하지 말고 플랫폼 내부 adapter로 감싼다. - -필수 adapter 책임: - -- provider/model 설정 로딩 -- domain별 response_model 선택 -- prompt/context 생성 -- `context`에 ontology vocabulary와 source metadata 주입 -- hooks로 audit log 저장 -- cache scope 결정 -- `InstructorRetryException`을 플랫폼 표준 에러로 변환 -- raw response/token usage/provenance 저장 - -## 9. 기본 소스로 가져올 때의 권장 범위 - -거의 변형 없이 사용: - -- `instructor.core.patch` -- `instructor.core.client` -- `instructor.core.retry` -- `instructor.processing.response` -- `instructor.processing.function_calls` -- `instructor.processing.schema` -- `instructor.mode` -- `instructor.dsl.*` -- `instructor.cache` -- `instructor.validation.llm_validators` - -플랫폼에 맞게 감쌀 부분: - -- `auto_client.from_provider`: 플랫폼 provider registry와 secret manager에 맞게 thin wrapper 작성 -- `hooks`: audit/event bus에 연결 -- `cache`: 운영용 Redis/DB cache backend 추가 -- `batch`: 플랫폼 job queue, chunk id, dataset id와 매핑 -- `CitationMixin`: 한국어/긴 문서/정규화 quote에 대한 보강 validator 추가 가능 - -굳이 가져오지 않아도 되는 부분: - -- `docs/`, `examples/`, `scripts/` 전체 -- `cli/`는 운영 필요성이 생기기 전에는 제외 가능 -- provider 중 사용하지 않는 optional provider dependency - -## 10. 온톨로지 추출용 최소 구현 예시 - -```python -from pydantic import BaseModel, Field, field_validator -from instructor import CitationMixin, Maybe - - -class EntityCandidate(CitationMixin): - name: str = Field(description="Surface form found in the source text") - canonical_name: str = Field(description="Normalized canonical entity name") - entity_type: str = Field(description="Ontology class/type") - aliases: list[str] = Field(default_factory=list) - confidence: float = Field(ge=0, le=1) - - -class RelationCandidate(CitationMixin): - source_name: str - target_name: str - relation_type: str - confidence: float = Field(ge=0, le=1) - - @field_validator("relation_type") - @classmethod - def relation_must_be_allowed(cls, value: str, info): - allowed = (info.context or {}).get("allowed_relations", set()) - if allowed and value not in allowed: - raise ValueError(f"relation_type must be one of {sorted(allowed)}") - return value - - -MaybeRelation = Maybe(RelationCandidate) -``` - -호출 패턴: - -```python -client = instructor.from_provider("openai/gpt-4o-mini") - -result = client.chat.completions.create( - response_model=MaybeRelation, - messages=[ - {"role": "system", "content": "Extract ontology relation candidates only from the provided source."}, - {"role": "user", "content": source_chunk}, - ], - context={ - "context": source_chunk, - "allowed_relations": {"is_a", "part_of", "used_for", "located_in"}, - }, - max_retries=3, - strict=True, -) -``` - -## 11. 리스크와 보완 필요점 - -- Instructor는 ontology reasoner가 아니다. OWL/RDF reasoning, graph merge, entity resolution은 별도 모듈이 필요하다. -- Pydantic schema가 너무 크면 LLM 출력 품질이 떨어진다. 추출 단계를 엔티티, 관계, 속성, 검증으로 분리하는 것이 좋다. -- `CitationMixin`은 quote 존재성 검증에 가깝고 “의미적으로 올바른 근거”를 보장하지 않는다. confidence와 human review가 필요하다. -- LLM validator는 강력하지만 비용이 크다. 전체 필드가 아니라 고위험 필드에만 적용해야 한다. -- provider별 mode 동작 차이가 있다. 운영 전 provider별 golden test set이 필요하다. -- local model/Ollama/OpenRouter 사용 시 tool calling 품질이 모델마다 크게 다르다. JSON mode fallback을 준비해야 한다. -- cache는 prompt/schema/source version을 엄격히 key에 포함해야 한다. 원본 cache key는 schema와 messages를 포함하므로 안전한 편이지만, 플랫폼 metadata까지 포함하려면 wrapper 수준에서 messages/context에 명확히 반영해야 한다. - -## 12. 테스트 전략 - -원본 테스트에서 참고할 영역: - -- `tests/test_patch.py`: patch 동작 -- `tests/test_retry_json_mode.py`: JSON mode retry -- `tests/test_json_extraction.py`: JSON 추출 -- `tests/test_process_response.py`: 응답 dispatcher -- `tests/test_schema.py`, `tests/test_schema_utils.py`: schema 생성 -- `tests/dsl/test_partial.py`: partial streaming -- `tests/test_list_response.py`: list response -- `tests/test_cache_integration.py`: cache -- `tests/llm/test_core_providers/*`: provider capability 공통 테스트 - -플랫폼 추가 테스트: - -- ontology entity schema validation unit test -- relation vocabulary validator test -- source quote/provenance validation test -- retry 후 수정 성공 golden test -- invalid extraction이 graph DB에 저장되지 않는 integration test -- provider별 동일 chunk extraction 품질 비교 test -- cache hit/miss와 schema 변경 cache busting test - -## 13. 결론 - -Instructor는 범용 온톨로지 구축 플랫폼의 “LLM 구조화 추출 엔진”으로 매우 적합하다. 특히 Pydantic 중심 스키마, provider 추상화, 자동 재시도, streaming DSL, 근거 quote mixin, hooks, cache가 플랫폼 핵심 요구와 잘 맞는다. - -기본 소스로 사용할 때는 Instructor 자체를 크게 변형하기보다, 플랫폼 내부에 `InstructorExtractionService` adapter를 두고 provider 설정, prompt, ontology vocabulary, audit log, cache, job queue를 연결하는 방식이 가장 안전하다. 이렇게 하면 원본 업데이트를 따라가기 쉽고, 온톨로지 플랫폼 고유 로직은 adapter와 domain schema 레이어에 깔끔하게 남길 수 있다. diff --git a/ONTOLOGY_PLATFORM_MASTER_DESIGN.md b/ONTOLOGY_PLATFORM_MASTER_DESIGN.md deleted file mode 100644 index 80a6022..0000000 --- a/ONTOLOGY_PLATFORM_MASTER_DESIGN.md +++ /dev/null @@ -1,649 +0,0 @@ -# 범용 온톨로지 구축 플랫폼 통합 설계안 - -작성일: 2026-05-13 -대상 자료: `오픈소스분석자료` 폴더의 8개 분석 문서 - -## 0. 최종 결론 - -시작 프로젝트는 현재 저장소의 `crawler_platform`을 유지한다. 이미 FastAPI, SQLAlchemy, 프로젝트/소스/Page/Entity/Claim/Evidence 모델, 크롤링 파이프라인, 웹 UI, 연구 루프 일부가 존재하므로 이것을 버리고 외부 프로젝트 하나로 갈아타는 것은 손실이 크다. - -다만 8개 오픈소스 중 “기본 엔진” 역할은 `OntoCast`가 가장 적합하다. OntoCast는 문서 입력에서 RDF 온톨로지와 Facts를 만들고, GraphUpdate/SPARQL 증분 갱신, renderer/critic retry loop, entity aggregation, triple store abstraction을 갖고 있어 온톨로지 구축 코어에 가장 직접적이다. - -권장 구조는 다음과 같다. - -```text -crawler_platform # 제품/플랫폼 껍데기. 계속 유지 - Platform API / UI / DB / Jobs - Source & Dataset Management - Review / Approval / Versioning - Adapters - Trafilatura # 웹 본문/메타/구조 추출 - Crawl4AI # JS/동적/딥 크롤링, 필요 시 추가 - OntoCast Core # RDF ontology/facts 생성 엔진 - Guardrails # LLM 구조화 출력 검증 게이트 - Neo4j GraphRAG # KG projection, GraphRAG, Text2Cypher - Optional / reference only - Firecrawl # API/옵션 설계 참고, 초기 직접 통합 제외 - Knowledge Agent # gap/audit workflow 패턴 참고 - OpenDeepResearcher # 외부 검색 루프 패턴 참고 -``` - -초기 실제 통합 수는 최대한 줄인다. - -1. 1차 실제 통합: `Trafilatura`, `OntoCast`, `Guardrails` -2. 2차 실제 통합: `Neo4j GraphRAG` -3. 3차 실제 통합: `Crawl4AI` -4. 코드 통합 보류: `Firecrawl` -5. 패턴/프롬프트만 차용: `Knowledge Agent`, `OpenDeepResearcher` - -이렇게 하면 핵심 기능은 상용제품 수준으로 설계하면서도, 한 번에 여러 거대 프로젝트를 섞어서 생기는 버그를 피할 수 있다. - -## 1. 8개 오픈소스별 채택 판단 - -| 오픈소스 | 가장 큰 장점 | 채택 방식 | 초기 통합 여부 | -|---|---|---|---| -| OntoCast | 문서 기반 RDF ontology/facts 생성, GraphUpdate/SPARQL 증분 갱신, renderer/critic retry loop, entity aggregation | 코어 엔진으로 거의 원형 유지. API/UI는 현재 플랫폼에서 새로 감싼다 | 필수 P0 | -| Trafilatura | HTML 본문/메타데이터/링크/표/중복 fingerprint 추출이 안정적이고 Apache-2.0 | Python API를 adapter로 사용. `bare_extraction(output_format="python")` 중심 | 필수 P0 | -| Guardrails | Pydantic/JSON Schema 기반 LLM 출력 검증, validator, reask/fix/filter 정책 | `OntologyGuard` facade로 감싸고 온톨로지 전용 validator 추가 | 필수 P0 | -| Neo4j GraphRAG | GraphSchema, SimpleKGPipeline, Neo4jWriter, Vector/Hybrid/Text2Cypher Retriever | 고정 버전 dependency + wrapper. Neo4j는 canonical store가 아니라 projection/search 계층 | 필수 P1 | -| Crawl4AI | Python 기반 비동기 크롤링, Playwright, Markdown, deep crawl, dispatcher/cache | JS-heavy/dynamic source 전용 adapter. Trafilatura 실패 시 fallback | 필수 P2 | -| Firecrawl | 상용급 scrape/map/crawl/batch/search API 표면과 job 운영 모델 | API/옵션/상태 모델 참고. AGPL/TypeScript/운영 복잡도 때문에 초기 직접 병합 제외 | 보류 | -| Knowledge Agent | 지식 공백 탐지, 연구-큐레이션-감사-수정-개선 루프 | LangGraph workflow와 LightRAG 프롬프트 패턴만 차용. 코드 안정화 후 일부 도입 | 패턴 P2 | -| OpenDeepResearcher | 검색어 생성, 검색, 페이지 유용성 평가, 추가 검색 판단 반복 루프 | 작은 모듈로 재작성. 원본 notebook 코드는 그대로 제품 코드에 넣지 않음 | 패턴 P2 | - -## 2. 중복 기능 제거 원칙 - -중복되는 프로젝트를 동시에 같은 책임으로 쓰지 않는다. - -| 책임 | 최종 선택 | 제외/보류 | -|---|---|---| -| 정적 웹 본문 추출 | Trafilatura | Firecrawl scrape를 기본으로 쓰지 않음 | -| 동적 페이지/딥 크롤 | Crawl4AI | Firecrawl과 Crawl4AI 동시 기본 사용 금지 | -| 문서 기반 RDF 온톨로지 생성 | OntoCast | Neo4j GraphRAG의 자유 KG 추출을 canonical ontology로 직접 확정하지 않음 | -| LLM 출력 검증 | Guardrails | 자체 ad-hoc JSON validation만으로 끝내지 않음 | -| GraphRAG/질의응답 | Neo4j GraphRAG | OntoCast triple store에 질의응답 기능을 억지로 모두 구현하지 않음 | -| 외부 검색 연구 | Platform ResearchLoop | Knowledge Agent와 OpenDeepResearcher를 각각 독립 실행하지 않음 | -| Canonical 저장소 | RDF/Fuseki + relational metadata | Neo4j를 원본 truth store로 삼지 않음 | - -핵심 규칙: - -1. 모든 수집 결과는 먼저 `Document`와 `ContentUnit`으로 정규화한다. -2. 모든 LLM 산출물은 `Candidate` 상태로 저장하고 바로 published graph에 넣지 않는다. -3. 모든 엔티티/관계/트리플은 evidence와 provenance 없이는 승인할 수 없다. -4. RDF/Fuseki를 canonical semantic store로 둔다. -5. Neo4j는 projection, graph search, GraphRAG, Text2Cypher용으로 둔다. -6. Firecrawl은 초기에는 직접 통합하지 않고, API 설계와 운영 상태 모델만 참고한다. - -## 3. 상용제품 기준 전체 기능 설계 - -### 3.1 제품 모듈 - -```text -Ontology Studio Platform - Project & Tenant - - 프로젝트 생성/설정/권한 - - 도메인 정책, 언어 정책, LLM/embedding profile - - source trust policy, robots/license/privacy policy - - Dataset & Source - - 파일 업로드, URL seed, sitemap/feed discovery - - source catalog, update schedule - - source reliability score, blocklist, allowlist - - Ingestion - - Trafilatura static extraction - - Crawl4AI dynamic/deep crawling - - document parse: PDF/DOCX/HTML/JSON/CSV - - dedup, content hash, fingerprint - - provenance, raw artifact storage - - Ontology Build Engine - - ContentUnit chunking - - ontology selection or fresh ontology creation - - GraphUpdate/SPARQL delta generation - - facts extraction - - renderer/critic/retry loop - - entity aggregation and URI normalization - - Validation Gate - - Pydantic schema validation - - JSON repair/type normalization - - ontology-specific validator - - SHACL/OWL validation - - evidence alignment validation - - reask/fix/filter/refrain policy - - Review & Governance - - candidate entity/relation/triple review - - source evidence highlight - - GraphUpdate diff viewer - - approve/reject/merge/split/edit - - reviewer audit log - - schema draft -> published workflow - - rollback/release/version tagging - - Storage - - relational DB: project/job/source/page/review/audit metadata - - artifact store: raw HTML, markdown, extracted JSON, TTL, screenshots - - Fuseki/RDF store: canonical ontology/facts - - Neo4j: graph projection, vector/fulltext index, GraphRAG - - Search & Use - - SPARQL query - - graph neighborhood search - - vector/hybrid search - - GraphRAG answer with provenance - - read-only Text2Cypher - - export: TTL, RDF/XML, JSON-LD, CSV, Parquet - - Research & Improvement - - knowledge gap detection - - external search planning - - usefulness scoring - - context/evidence extraction - - repeated failure analysis - - schema/prompt/source policy improvement suggestions - - Operations - - async job queue - - progress/cancel/retry - - cost/budget tracking - - LLM cache - - metrics/logs/traces - - backup/restore - - admin safety controls -``` - -### 3.2 기준 데이터 모델 - -현재 `crawler_platform` 모델을 확장한다. - -필수 추가/정리 모델: - -| 모델 | 목적 | -|---|---| -| `Dataset` | 프로젝트 내 문서 묶음, import batch 단위 | -| `Document` | URL/파일/API 응답의 정규화 원문 | -| `ContentUnit` | chunk, source offsets, section/table/list 정보 | -| `Artifact` | raw html, markdown, body xml, TTL, JSON, screenshot 저장 위치 | -| `OntologySchemaVersion` | draft/published/archived schema, version, hash | -| `GraphDelta` | OntoCast GraphUpdate/SPARQL delta와 적용 상태 | -| `CandidateEntity` | 검수 전 엔티티 후보 | -| `CandidateRelation` | 검수 전 관계 후보 | -| `CandidateTriple` | 검수 전 RDF/property graph 후보 | -| `ValidationRun` | Guardrails/SHACL/OWL 검증 결과 | -| `ReviewDecision` | 승인/반려/수정/병합 이력 | -| `EntityMergeCandidate` | exact/fuzzy/embedding merge 후보 | -| `ResearchSession` | 외부 검색/공백 보완 세션 | -| `JobRun` | 수집/추출/검증/저장 작업 상태 | - -기존 `Page`, `Entity`, `Claim`, `Evidence`, `OntologyTriple`, `KnowledgeGap`은 유지하되 아래 필드를 보강한다. - -- `Page`: `markdown`, `body_xml_ref`, `fingerprint`, `language`, `change_status`, `last_checked_at` -- `Claim`: `candidate_status`, `validation_status`, `review_status`, `ontology_version` -- `Evidence`: `content_unit_id`, `char_start`, `char_end`, `selector`, `quote_hash` -- `OntologyTriple`: `graph_uri`, `ontology_version_id`, `rdf_subject`, `rdf_predicate`, `rdf_object`, `provenance_graph_uri` - -## 4. 최종 아키텍처 - -```mermaid -flowchart TB - UI["Ontology Studio UI"] --> API["FastAPI Platform API"] - API --> JOB["Job Queue / Worker"] - API --> DB["Relational Metadata DB"] - - JOB --> ING["Ingestion Pipeline"] - ING --> TRA["Trafilatura Adapter"] - ING --> C4A["Crawl4AI Adapter"] - ING --> DOC["Document / ContentUnit Store"] - - DOC --> ONTO["OntoCast Core Engine"] - ONTO --> GUARD["Guardrails Validation Gate"] - GUARD --> REVIEW["Human Review Queue"] - REVIEW --> RDF["Canonical RDF Store / Fuseki"] - - RDF --> NEO["Neo4j Projection"] - NEO --> RAG["GraphRAG / Text2Cypher / Hybrid Search"] - RDF --> EXPORT["TTL / JSON-LD / RDF Export"] - - DB --> OBS["Audit / Metrics / Cost Dashboard"] - JOB --> OBS - - RESEARCH["Research Loop"] --> ING - RESEARCH --> DOC - RESEARCH --> REVIEW -``` - -저장소 원칙: - -1. `Relational DB`: 제품 상태, 작업 상태, 검수/승인/감사 이력. -2. `Artifact Store`: 원문과 중간 산출물. -3. `RDF Store`: 승인된 canonical ontology/facts. -4. `Neo4j`: 검색/탐색/GraphRAG projection. - -## 5. 기능별 상세 설계 - -### 5.1 Project & Tenant - -상용제품 수준 필수 기능: - -- 프로젝트 생성/복제/보관 -- 프로젝트별 namespace/base IRI -- 프로젝트별 언어, ontology naming policy -- LLM profile, embedding profile -- source trust policy -- 승인 정책: 자동 승인 금지, 저위험 자동 승인, 고위험 수동 승인 -- 사용자/역할: admin, ontologist, reviewer, operator, viewer - -### 5.2 Source & Dataset - -기능: - -- URL seed 등록 -- sitemap/feed discovery -- 파일 업로드 -- API/DB source 등록 -- source trust score -- robots/license/privacy policy -- update schedule -- change detection -- 실패 URL과 denial reason 저장 - -채택 소스: - -- Trafilatura: feed/sitemap discovery, metadata extraction -- Crawl4AI: JS-heavy/dynamic page, deep crawl -- Firecrawl: map/crawl/search 옵션 설계 참고 - -### 5.3 Ingestion - -표준 파이프라인: - -```text -Source - -> URL/File discovery - -> fetch/render - -> raw artifact save - -> Trafilatura bare_extraction - -> metadata normalize - -> content hash/fingerprint - -> ContentUnit chunking - -> quality score - -> Document ready -``` - -수용 기준: - -- HTML 없이 텍스트만 있는 문서도 처리 -- JS 렌더링 필요 시 Crawl4AI fallback -- 동일 URL/동일 본문/near duplicate 구분 -- 제목/날짜/저자/canonical URL/source URL 보존 -- table/list/heading 구조를 잃지 않음 -- evidence offset 또는 selector를 가능한 한 보존 - -### 5.4 Ontology Build - -OntoCast를 중심에 둔다. - -기능: - -- ontology 선택 또는 신규 생성 -- RDFGraph/ Ontology/ContentUnit 모델 사용 -- GraphUpdate 기반 증분 갱신 -- facts renderer/critic loop -- ontology renderer/critic loop -- unit별 병렬 처리 -- entity aggregation -- URI 정규화 -- owl:sameAs 보존 -- budget/caching - -플랫폼에서 추가할 기능: - -- project/dataset/job 식별자 -- 다중 문서 corpus 처리 -- 비동기 job progress -- output artifact 저장 -- Korean/domain prompt profile -- versioning/diff/rollback -- human review 연결 - -### 5.5 Validation Gate - -Guardrails를 `OntologyGuard`로 감싼다. - -초기 필수 validator: - -| Validator | 기능 | 실패 정책 | -|---|---|---| -| `EntityIdFormatValidator` | ID/URI 형식 검증 | fix/reask | -| `UniqueEntityValidator` | 중복 엔티티 후보 검증 | reask/filter | -| `RelationEndpointExistsValidator` | 관계 양끝 엔티티 존재 확인 | reask | -| `PredicateVocabularyValidator` | 허용 predicate/ontology schema 매핑 | custom/reask | -| `EvidenceExistsValidator` | evidence가 원문 ContentUnit에 존재하는지 확인 | filter/reask | -| `NoHallucinatedClassValidator` | 근거 없는 class/property 생성 차단 | reask | -| `ConfidenceRangeValidator` | 0~1 confidence 보정 | fix | -| `SHACLShapeValidator` | SHACL/OWL 제약 검증 | exception/reask | -| `NoUnsafeCypherValidator` | Text2Cypher write/delete 차단 | exception | - -정책: - -- parsing/schema 오류는 reask 1회 -- 의미가 바뀔 수 있는 자동 fix는 금지 -- evidence 없는 triple은 저장 금지 -- 최종 실패는 review queue로 이동 - -### 5.6 Review & Versioning - -상용제품 차별화의 핵심이다. - -필수 화면/API: - -- candidate entity/relation/triple 목록 -- 원문 evidence highlight -- GraphUpdate diff -- accepted/rejected/edited 상태 -- schema draft/published 전환 -- version diff -- rollback -- merge/split editor -- reviewer comment -- audit log - -승인 상태: - -```text -generated - -> validated - -> pending_review - -> approved - -> published - -> superseded / rejected / archived -``` - -### 5.7 Storage & Projection - -Canonical: - -- Fuseki/RDF store에 승인된 ontology/facts 저장 -- provenance는 named graph 또는 side graph로 분리 - -Projection: - -- Neo4j에 RDF/property graph projection -- Document/Chunk/Entity/Relation/Evidence 연결 유지 -- vector/fulltext index 생성 -- GraphRAG와 Text2Cypher는 read-only API로 제공 - -보안: - -- Text2Cypher는 read-only 검사 -- result limit, timeout, 금지 키워드, 권한 필터 -- schema allowlist 적용 - -### 5.8 Research Loop - -Knowledge Agent와 OpenDeepResearcher는 독립 프로젝트로 붙이지 않는다. 현재 `crawler_platform.app.core.research` 아래에 하나의 `ResearchLoop`로 재구성한다. - -기능: - -- knowledge gap detection -- 검색어 생성 -- 검색 provider 인터페이스 -- URL 중복 제거 -- page usefulness scoring -- context/evidence extraction -- gap coverage 계산 -- 추가 검색 판단 -- source curation -- review queue로 후보 전달 - -차용: - -- Knowledge Agent: Analyst/Researcher/Curator/Auditor/Fixer/Advisor 역할 분리, LightRAG JSON 추출 프롬프트 -- OpenDeepResearcher: 반복형 검색 루프, page usefulness, 추가 검색 판단, 비동기 병렬 처리 - -필수 수정: - -- notebook/CLI/input 구조 제거 -- `eval` 금지 -- JSON schema + Guardrails 검증 -- API key/config 하드코딩 제거 -- session history DB 저장 -- cancellation/retry/progress 추가 - -## 6. 단계별 통합 계획 - -각 단계는 독립적으로 완료/검증한 뒤 다음 단계로 간다. 한 단계에서 실패하면 다음 오픈소스를 붙이지 않는다. - -### Phase 0. 기준선 고정 - -목표: - -- 현재 `crawler_platform` 기능과 테스트 기준선을 고정한다. - -작업: - -1. 현재 테스트 전체 실행 -2. DB schema snapshot 작성 -3. 주요 API smoke test 작성 -4. sample HTML 수집 -> entity/claim 저장 흐름 고정 - -완료 조건: - -- 기존 기능 regression 없음 -- 현재 DB 모델과 API 목록 문서화 - -### Phase 1. 공통 데이터 계약 먼저 구축 - -목표: - -- 외부 소스 통합 전 내부 표준 모델을 확정한다. - -작업: - -1. `Document`, `ContentUnit`, `Artifact`, `CandidateTriple`, `ValidationRun`, `ReviewDecision`, `GraphDelta` 모델 추가 -2. 기존 `Page/Claim/Evidence/OntologyTriple`과 호환 mapping 작성 -3. 모든 ingestion 결과가 `Document -> ContentUnit`으로 들어오게 한다. - -완료 조건: - -- 기존 crawl 결과가 새 Document/ContentUnit으로 저장됨 -- 기존 Claim/Evidence 저장이 깨지지 않음 - -### Phase 2. Trafilatura 통합 - -목표: - -- 정적 웹 문서를 ontology-ready document로 안정 정제한다. - -작업: - -1. `TrafilaturaAdapter` 추가 -2. `bare_extraction(output_format="python", with_metadata=True)` 사용 -3. metadata, body XML, fingerprint, links/tables를 Artifact/ContentUnit에 저장 -4. 기존 parser/fetcher와 교체하지 않고 source profile로 선택 가능하게 한다. - -완료 조건: - -- 한국어/영어 sample HTML 10개 이상에서 본문/메타/링크 추출 -- table/list/heading chunk 보존 -- 동일 본문 fingerprint 중복 감지 - -### Phase 3. OntoCast Core 통합 - -목표: - -- 문서에서 ontology/facts RDF 후보를 생성한다. - -작업: - -1. OntoCast의 `onto/`, `agent/`, `stategraph/`, `tool/` 핵심만 별도 package로 가져온다. -2. Robyn API/CLI는 가져오지 않는다. -3. `OntologyBuildService.process_document()` facade 작성 -4. ContentUnit -> OntoCast ContentUnit adapter 작성 -5. output TTL/GraphUpdate/Facts를 Artifact와 GraphDelta로 저장 - -완료 조건: - -- 단일 문서로 ontology TTL과 facts TTL 생성 -- GraphUpdate delta 저장 -- critic retry 결과와 budget 기록 -- 아직 published store에는 자동 반영하지 않음 - -### Phase 4. Guardrails 검증 게이트 통합 - -목표: - -- LLM 산출물의 schema 오류와 근거 없는 triple을 저장 전에 차단한다. - -작업: - -1. `OntologyGuard` facade 작성 -2. Pydantic output schema 정의 -3. 최소 validator 5개 구현 -4. OntoCast output과 기존 extractor output을 같은 검증 게이트에 통과 -5. ValidationRun 저장 - -완료 조건: - -- 잘못된 JSON/schema는 저장 차단 -- evidence 없는 triple은 후보에서 제거 또는 reask -- validation log가 UI/API에서 조회 가능 - -### Phase 5. Review/Versioning 최소 구현 - -목표: - -- 검증된 후보를 사람이 승인해야 canonical graph에 반영한다. - -작업: - -1. candidate list API -2. evidence 조회 API -3. approve/reject/edit API -4. GraphDelta publish API -5. ontology schema draft/published version 모델 - -완료 조건: - -- candidate -> approved -> published 상태 전이 -- rollback 가능한 version 기록 -- 누가 무엇을 승인했는지 audit log 저장 - -### Phase 6. Neo4j GraphRAG 통합 - -목표: - -- 승인된 RDF/claim을 Neo4j projection으로 만들고 검색/RAG를 제공한다. - -작업: - -1. `neo4j-graphrag` 고정 버전 의존성 추가 -2. RDF/Claim/ContentUnit -> Neo4j graph projection 작성 -3. Vector/Hybrid/VectorCypher retriever wrapper 작성 -4. GraphRAG API 작성 -5. Text2Cypher read-only sandbox 작성 - -완료 조건: - -- approved graph가 Neo4j에 projection됨 -- chunk -> entity -> evidence 검색 가능 -- Text2Cypher가 쓰기/삭제 명령을 차단 - -### Phase 7. Crawl4AI 통합 - -목표: - -- JS-heavy/dynamic site와 deep crawl을 안정 처리한다. - -작업: - -1. `Crawl4AIAdapter` 추가 -2. source profile: `static`, `dynamic_page`, `deep_discovery`, `structured_extract` -3. rendered HTML/markdown/screenshot 선택 저장 -4. rendered HTML을 Trafilatura에 다시 넣는 hybrid path 구성 - -완료 조건: - -- JS page에서 rendered HTML 추출 -- cache/session/dispatcher 설정 가능 -- deep crawl은 domain/path/rate limit 정책을 지킴 - -### Phase 8. Research Loop 통합 - -목표: - -- 사용자가 seed URL을 몰라도 지식 공백을 기반으로 외부 자료를 찾는다. - -작업: - -1. `ExternalResearchLoop` 추가 -2. search provider interface 작성 -3. query/usefulness/context/next-decision JSON schema 작성 -4. KnowledgeGap과 ResearchSession 연결 -5. 유용 context를 Document/ContentUnit/Candidate로 저장 - -완료 조건: - -- gap -> search query -> useful page -> evidence context -> candidate 저장 -- session history/cost/progress/cancel 지원 - -### Phase 9. Firecrawl optional adapter 검토 - -목표: - -- 필요할 때만 Firecrawl을 외부 수집 서비스로 붙인다. - -조건: - -- Crawl4AI 운영이 불안정하거나, Firecrawl의 self-host scrape/map/crawl API가 운영상 더 낫다고 판단될 때만 진행 -- AGPL/라이선스 검토 완료 -- TypeScript service를 Python 코드베이스에 직접 병합하지 않음 - -완료 조건: - -- `FirecrawlAdapter`가 외부 HTTP API만 호출 -- core ontology pipeline은 Firecrawl에 의존하지 않음 - -## 7. 개발 지시 원칙 - -다른 AI 에이전트에게 작업시킬 때 반드시 지킬 원칙: - -1. 한 번에 하나의 오픈소스만 붙인다. -2. 원본 코드를 직접 대량 수정하지 말고 adapter/facade를 만든다. -3. 외부 프로젝트 API가 흔들리면 wrapper만 고친다. -4. canonical data contract는 `Document`, `ContentUnit`, `Candidate`, `Evidence`, `GraphDelta`, `ValidationRun`이다. -5. published graph는 review/approval 없이 변경하지 않는다. -6. Firecrawl과 Crawl4AI를 같은 기본 crawler로 동시에 쓰지 않는다. -7. OntoCast와 Neo4j GraphRAG의 역할을 섞지 않는다. OntoCast는 RDF 생성/갱신, Neo4j는 projection/search/RAG다. -8. Guardrails를 통과하지 않은 LLM output은 DB에 확정 저장하지 않는다. -9. evidence/provenance 없는 entity/relation/triple은 폐기하거나 review queue로 보낸다. -10. 각 단계마다 unit test, integration test, DB migration test, sample data smoke test를 통과한 뒤 다음 단계로 간다. - -## 8. 최종 권장 구현 순서 요약 - -가장 안전한 순서: - -1. 현재 `crawler_platform` 기준선 테스트 고정 -2. 공통 데이터 계약 확장 -3. Trafilatura adapter -4. OntoCast core facade -5. Guardrails validation gate -6. review/versioning workflow -7. Neo4j GraphRAG projection/search -8. Crawl4AI dynamic crawler -9. Knowledge Agent/OpenDeepResearcher 기반 ResearchLoop -10. Firecrawl optional adapter - -가장 중요한 결정: - -- 기본 프로젝트: 현재 `crawler_platform` -- 기본 온톨로지 엔진: `OntoCast` -- 기본 웹 정제 엔진: `Trafilatura` -- 기본 검증 엔진: `Guardrails` -- 기본 검색/RAG 엔진: `Neo4j GraphRAG` -- 동적 크롤링 엔진: `Crawl4AI` -- Firecrawl: 초기 제외, 설계 참고 또는 선택 외부 서비스 - -이 설계는 “있는 소스를 최대한 그대로 쓰되, 제품으로 필요한 연결/검수/버전/운영 계층만 우리가 만든다”는 방향이다. 실제 통합 코드는 얇은 adapter와 안정된 내부 데이터 계약 위에 쌓아야 한다. diff --git a/ONTOLOGY_PLATFORM_OVERVIEW.md b/ONTOLOGY_PLATFORM_OVERVIEW.md deleted file mode 100644 index 52141ba..0000000 --- a/ONTOLOGY_PLATFORM_OVERVIEW.md +++ /dev/null @@ -1,598 +0,0 @@ -# 온톨로지 시스템 구축 플랫폼 (Ontology System Construction Platform) - -## 플랫폼 개요 - -이 플랫폼은 **웹 데이터에서 시작하여 구조화된 지식 그래프(Knowledge Graph)를 자동으로 구축하고, 이를 활용해 지능형 응답을 제공하는 end-to-end 시스템**입니다. - -### 핵심 목표 -``` -Raw Web Data → Structured Ontology → Knowledge Graph → AI Reasoning -``` - ---- - -## 온톨로지(Ontology)란? - -### 정의 -**온톨로지**: 어떤 영역의 개념(entities), 속성(properties), 관계(relationships)를 형식화(formalize)한 구조 - -### 예시 -``` -의학 온톨로지: -├── Entity (개념) -│ ├── Disease (질병) -│ │ ├── Diabetes -│ │ ├── Hypertension -│ └── Drug (약) -│ ├── Aspirin -│ └── Metformin -│ -├── Relationships -│ ├── treats (약이 질병을 치료함) -│ ├── causes (원인 관계) -│ └── prevents (예방 관계) -│ -└── Properties - ├── Disease.severity (중증도) - ├── Drug.sideEffects (부작용) - └── Drug.dosage (용량) - -Example: - Aspirin --treats--> Headache - Aspirin --has_sideEffect--> GastricBleeding -``` - -### 온톨로지의 가치 -- **상호운용성**: 다양한 시스템 간 데이터 교환 가능 -- **추론 능력**: 규칙 기반 새로운 지식 도출 -- **질의응답**: 구조화된 데이터로 정확한 답변 -- **재사용성**: 한번 구축한 온톨로지는 여러 앱에서 사용 - ---- - -## 플랫폼 아키텍처 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ ONTOLOGY PLATFORM │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ [Phase 0-2: Data Collection & Extraction] │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ 웹 크롤링 → HTML/텍스트 추출 → 후보 데이터 수집 │ │ -│ │ - URL 추출 (Phase 0) │ │ -│ │ - Crawl4AI 동적 크롤링 (Phase 1-2) │ │ -│ │ - 정적/동적 페이지 모두 지원 │ │ -│ └────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ [Phase 3: Validation & Cleaning] │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ 추출 데이터 검증 → 정제 → 온톨로지 변환 │ │ -│ │ - OntoCast 가벼운 검증 (Phase 3) │ │ -│ │ - 데이터 품질 확인 │ │ -│ │ - RDF/트리플 변환 │ │ -│ └────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ [Phase 4: Knowledge Graph Storage] │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ 구조화된 데이터 → Neo4j 저장 │ │ -│ │ - 벡터 임베딩 (all-MiniLM-L6-v2) │ │ -│ │ - 유사도 기반 검색 가능 │ │ -│ │ - 대규모 그래프 지원 (10K+ 노드) │ │ -│ └────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ [Phase 5: Graph Intelligence] │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ 그래프 분석 및 최적화 │ │ -│ │ - 의미적 중복 제거 (Entity Resolution) │ │ -│ │ - 부분 그래프 추출 (Subgraph Retrieval) │ │ -│ │ - 패턴 분석 (Path Finding, Cycles, Motifs) │ │ -│ │ - 중심성/커뮤니티 분석 (Graph Analytics) │ │ -│ └────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ [Phase 6: API & Integration] │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ REST API / GraphQL / RAG 파이프라인 제공 │ │ -│ │ - /graph/* - 그래프 작업 (10개 엔드포인트) │ │ -│ │ - /rag/* - RAG 컨텍스트 추출 │ │ -│ │ - /graphql - 유연한 쿼리 │ │ -│ └────────────────────────────────────────────────────┘ │ -│ ↓ │ -│ [Phase 7-8: Future Enhancements] │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ - Phase 7: LLM 직접 통합 (스트리밍, 캐싱) │ │ -│ │ - Phase 8: 멀티테넌트, 실시간 업데이트 │ │ -│ └────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Phase별 역할 정리 - -### Phase 0-2: 데이터 수집 (Data Collection) -**목표**: 웹에서 원본 데이터 추출 - -| Phase | 기능 | 기술 | -|-------|------|------| -| **0** | URL 기반 텍스트 추출 | Trafilatura | -| **1** | 동적 페이지 크롤링 | Crawl4AI (Playwright) | -| **2** | 프로필별 크롤링 전략 | fast_static, dynamic_page | - -**Input**: `웹 URL` -**Output**: `텍스트, HTML, 메타데이터` - -``` -예: https://example.com → "Apple is a technology company..." -``` - ---- - -### Phase 3: 데이터 검증 (Validation & Cleaning) -**목표**: 추출 데이터의 품질 확보 및 온톨로지 변환 - -| 작업 | 기술 | 결과 | -|------|------|------| -| **텍스트 정제** | 정규식, 토큰화 | 깔끔한 텍스트 | -| **엔티티 추출** | NER (Named Entity Recognition) | ["Apple", "Tim Cook"] | -| **관계 추출** | 경량 NLP | [("Apple", "produces", "iPhone")] | -| **검증** | OntoCast, 규칙 기반 | 신뢰도 점수 | - -**Input**: `추출된 텍스트` -**Output**: `RDF 트리플 (Subject-Predicate-Object)` - -``` -예: - ("Apple Inc.", "produces", "iPhone") - ("Apple Inc.", "founded_by", "Steve Jobs") - ("iPhone", "has_feature", "Face ID") -``` - ---- - -### Phase 4: 그래프 저장 (Knowledge Graph Storage) -**목표**: 온톨로지를 Neo4j 그래프 데이터베이스에 저장 - -| 작업 | 기술 | 특징 | -|------|------|------| -| **변환** | RDF → Property Graph | 노드 + 관계 변환 | -| **임베딩** | SentenceTransformer | 벡터 유사도 검색 | -| **인덱싱** | Neo4j 인덱스 | 빠른 조회 | -| **배치 처리** | UNWIND + MERGE | 대량 데이터 효율 처리 | - -**Input**: `RDF 트리플` -**Output**: `Neo4j Knowledge Graph` - -``` -Neo4j에 저장: - (Apple:Company) -[produces]-> (iPhone:Product) - (Apple:Company) -[founded_by]-> (Steve_Jobs:Person) - (iPhone:Product) -[has_feature]-> (FaceID:Feature) - -벡터 저장: - Apple → [0.23, -0.45, 0.67, ...] (384차원) - iPhone → [0.12, 0.34, -0.56, ...] (384차원) -``` - ---- - -### Phase 5: 그래프 지능화 (Graph Intelligence) -**목표**: 저장된 그래프를 분석하여 품질 향상 및 인사이트 도출 - -#### 5.0: 데이터 정제 (Deduplication & Conversion) -- **Entity Resolver**: "Apple Inc." vs "Apple" 같은 중복 감지 -- **RDF 변환**: 쿼리 성능을 위해 Property Graph 최적화 - -``` -Before: Apple, APPLE, Apple Inc., Apple Corporation (4개) -After: Apple Inc. (1개) + aliases: [Apple, APPLE, Apple Inc., ...] -``` - -#### 5.1: 컨텍스트 추출 (Subgraph & Patterns) -- **Neighborhood Extraction**: 특정 엔티티 주변 2-hop 이웃 추출 -- **Pattern Matching**: 경로, 순환, 구조 패턴 분석 -- **데이터 품질 검증**: 순환 의존성, 연결성 분석 - -``` -Apple의 2-hop 이웃: - Apple → produces → iPhone → has_feature → Face ID - Apple → founded_by → Steve Jobs - Apple → headquarters → Cupertino -``` - -#### 5.2: 분석 (Analytics) -- **중심성 분석**: 가장 중요한 엔티티 식별 -- **커뮤니티 감지**: 자동으로 관련 엔티티 그룹화 -- **그래프 통계**: 전체 구조 이해 - -``` -Top entities by importance: - 1. Apple (PageRank: 0.95) - 2. iPhone (PageRank: 0.87) - 3. Steve Jobs (PageRank: 0.82) - -Communities: - - Apple Products (iPhone, iPad, Mac) - - Apple People (Tim Cook, Steve Jobs) - - Apple Locations (Cupertino, China) -``` - ---- - -### Phase 6: API & 통합 (API & RAG Integration) -**목표**: 구축한 온톨로지를 외부에 공개하고 LLM과 연계 - -#### REST API -```bash -# 그래프 조회 -GET /api/v1/graph/analytics/influential -→ 가장 영향력 있는 엔티티들 - -# 패턴 분석 -POST /api/v1/graph/patterns/paths -→ Apple에서 iPhone까지의 모든 경로 - -# RAG 컨텍스트 -POST /api/v1/rag/context-extraction -→ "Apple의 제품?"에 필요한 그래프 컨텍스트 -``` - -#### RAG (Retrieval Augmented Generation) 파이프라인 -``` -사용자 쿼리: "Apple의 제품은?" - ↓ -그래프 검색: Apple 엔티티 찾기 - ↓ -컨텍스트 추출: Apple 주변 2-hop 이웃 - ↓ -LLM 프롬프트 구성: - You are a helpful assistant. - - KNOWLEDGE GRAPH CONTEXT: - Apple produces: iPhone, iPad, Mac, Apple Watch - Apple was founded by Steve Jobs - Apple is headquartered in Cupertino - - Question: Apple의 제품은? - ↓ -LLM 응답: "Apple의 주요 제품은..." -``` - -#### GraphQL 지원 -```graphql -{ - entity(id: 1) { - label - type - neighbors(hops: 2) { - label - relationship - } - } -} -``` - -**Input**: `REST/GraphQL 쿼리` -**Output**: `JSON 응답 + LLM 프롬프트` - ---- - -## 엔드투엔드 워크플로우 - -### 시나리오: 기술 회사 온톨로지 구축 - -#### 1단계: 데이터 수집 -```bash -# Phase 0-2 -URL 목록 입력: - - apple.com - - wikipedia.org/wiki/Apple - - crunchbase.com/organization/apple - -↓ - -추출 결과: - "Apple is a technology company..." - "Founded by Steve Jobs in 1976" - "Produces iPhone, iPad, Mac..." -``` - -#### 2단계: 데이터 검증 및 온톨로지 변환 -```python -# Phase 3 -Raw Text Input: - "Apple produces iPhone and iPad" - -↓ - -검증 및 추출: - Entity 1: Apple (Company) - confidence: 0.95 - Entity 2: iPhone (Product) - confidence: 0.92 - Relation: produces - confidence: 0.88 - -↓ - -RDF 트리플: - (Apple, produces, iPhone) - (Apple, produces, iPad) -``` - -#### 3단계: 그래프 저장 및 벡터화 -``` -# Phase 4 -Neo4j 저장: - CREATE (a:Company {name: "Apple"}) - CREATE (p:Product {name: "iPhone"}) - CREATE (a)-[:PRODUCES]->(p) - SET a.embedding = [0.23, -0.45, ...] - SET p.embedding = [0.12, 0.34, ...] -``` - -#### 4단계: 그래프 지능화 -``` -# Phase 5 -Quality Check: - - 중복 감지: "Apple", "APPLE", "Apple Inc." → 1개로 통합 - - 구조 분석: Apple의 2-hop 이웃 = 45개 엔티티 - - 중요도: Apple (0.95), iPhone (0.87), iPad (0.85) - -Communities: - - Apple Products: [iPhone, iPad, Mac, Watch] - - Apple People: [Tim Cook, Steve Jobs] - - Apple Locations: [Cupertino, China Factory] -``` - -#### 5단계: API 공개 및 LLM 통합 -``` -# Phase 6 -API 엔드포인트: - -GET /graph/analytics/influential -→ Top 20 entities by importance - -POST /graph/patterns/paths -→ Apple과 Steve Jobs를 연결하는 모든 경로 - -POST /rag/query - Input: "Apple의 제품은?" - Output: - { - "llm_prompt": "Knowledge Graph...\n\nQuestion: Apple의 제품은?", - "context": {nodes: 45, edges: 120}, - "relevant_entities": ["iPhone", "iPad", "Mac"] - } - -↓ - -LLM Service (외부): - Input: llm_prompt - Output: "Apple의 주요 제품은 iPhone, iPad, Mac 등입니다..." -``` - ---- - -## 플랫폼이 해결하는 문제 - -### 1️⃣ 정보의 구조화 -**문제**: 웹에 산재된 정보는 비구조화 상태 -**해결**: Phase 0-3으로 자동 구조화 - -``` -Before: "Apple produces iPhone, iPad, and Mac. Steve Jobs founded it." -After: - (Apple) -[produces]-> (iPhone) - (Apple) -[produces]-> (iPad) - (Apple) -[produces]-> (Mac) - (Apple) -[founded_by]-> (Steve Jobs) -``` - -### 2️⃣ 중복된 정보 -**문제**: "Apple", "APPLE Inc.", "Apple Computer"는 같은가? -**해결**: Phase 5.0 Entity Resolver로 자동 중복 제거 - -``` -Before: 100개 Apple 관련 엔티티 -After: 1개 Apple + aliases: [APPLE, Apple Inc., ...] -``` - -### 3️⃣ 데이터 품질 문제 -**문제**: 추출 데이터에 오류, 불완전, 부정확 -**해결**: Phase 3 검증 + Phase 5 분석으로 문제 식별 - -``` -확인: - ✓ 필수 엔티티 모두 포함? - ✓ 관계가 논리적으로 타당? - ✓ 순환 의존성은 없나? - ✓ 신뢰도 점수는 충분한가? -``` - -### 4️⃣ 정보 활용의 어려움 -**문제**: "Apple의 제품은?" 같은 질문에 자동으로 답하기 어려움 -**해결**: Phase 4-6으로 검색 가능한 지식 그래프 구축 + LLM 연계 - -``` -자동 답변: - Q: "Apple의 제품은?" - A: "Apple은 iPhone, iPad, Mac, Watch를 생산합니다" -``` - ---- - -## 플랫폼 사용 시나리오 - -### 시나리오 1: 의료 온톨로지 구축 -``` -목표: 의약 정보 자동 추출 및 의사 지원 - -Phase 0-2: 의료 사이트 크롤링 - ✓ FDA.gov, Medline, 의료 뉴스 등 - -Phase 3: 약물-질병-치료법 추출 - ✓ "Aspirin treats Headache" - ✓ "Metformin manages Diabetes" - -Phase 4: Neo4j에 저장 - ✓ 약물, 질병, 부작용, 용량 등 관계 - -Phase 5: 의료 지식 분석 - ✓ "이 증상을 일으키는 약물은?" - ✓ "안전한 약물 조합은?" - -Phase 6: 의사용 API - GET /api/drug/{drugId}/interactions - → 상호작용 정보 즉시 제공 -``` - -### 시나리오 2: 기업 경쟁 분석 -``` -목표: 경쟁사 정보 자동 수집 및 분석 - -Phase 0-2: 뉴스, 재무제표, 공식 사이트 크롤링 - ✓ Samsung, Apple, Sony 정보 - -Phase 3: 제품, 전략, 파트너십 추출 - ✓ "Samsung produces OLED displays" - ✓ "Apple partners with TSMC" - -Phase 4: 경쟁 관계 그래프 - ✓ 공급망, 기술 경쟁, M&A 관계 - -Phase 5: 분석 - ✓ "Apple과 경쟁하는 기업은?" - ✓ "가장 영향력 있는 기업은?" - -Phase 6: 분석가용 API - POST /api/competitor-analysis - → 경쟁 지형도 자동 생성 -``` - -### 시나리오 3: 학술 지식 그래프 -``` -목표: 과학 논문에서 자동으로 지식 추출 - -Phase 0-2: arXiv, PubMed 크롤링 - ✓ 학술 논문 데이터 - -Phase 3: 개념, 방법론, 결과 추출 - ✓ "BERT improves NLP tasks" - ✓ "Transformer uses attention mechanism" - -Phase 4: 학술 지식 그래프 - ✓ 기술, 저자, 논문, 인용 관계 - -Phase 5: 분석 - ✓ "가장 영향력 있는 논문은?" - ✓ "이 분야의 선도 연구자는?" - -Phase 6: 연구자용 API - GET /api/research-topics/trending - → 최신 연구 방향 추천 -``` - ---- - -## 기술 스택 - -### 데이터 수집 -- **Trafilatura**: HTML → 텍스트 추출 -- **Crawl4AI**: 동적 페이지 크롤링 (Playwright 기반) - -### NLP & 추출 -- **LightweightExtractor**: 엔티티/관계 추출 -- **OntoCast**: 검증 및 온톨로지 변환 - -### 그래프 데이터베이스 -- **Neo4j**: 그래프 저장 및 쿼리 -- **SentenceTransformer**: 벡터 임베딩 - -### API & 서빙 -- **FastAPI**: REST API 서버 -- **GraphQL**: 유연한 쿼리 언어 - -### LLM 통합 -- **OpenAI/Claude API**: 자연어 생성 -- **SSE/WebSocket**: 스트리밍 응답 - ---- - -## 플랫폼 사용 시작하기 - -### 1️⃣ 온톨로지 구축 -```bash -# Phase 0-2: 데이터 수집 -python -m ontology_platform.crawler --url https://example.com - -# Phase 3: 검증 및 변환 -python -m ontology_platform.validator --input extracted_data.json - -# Phase 4: 그래프 저장 -python -m ontology_platform.graph_builder --triples ontology.rdf -``` - -### 2️⃣ 그래프 분석 -```bash -# Phase 5: 품질 분석 -python -m ontology_platform.analyzer --graph_id my_ontology - -# 결과: 중복 제거, 커뮤니티 감지, 통계 -``` - -### 3️⃣ API 서빙 -```bash -# Phase 6: API 시작 -python -m uvicorn ontology_platform.api.phase6_app:app --reload - -# http://localhost:8000/docs에서 확인 -``` - -### 4️⃣ LLM 통합 -```python -# Phase 6+: RAG 쿼리 -response = requests.post( - "http://localhost:8000/api/v1/rag/query", - json={"query": "Apple의 제품은?"} -) - -# LLM으로 프롬프트 전달 -llm_answer = call_llm(response["llm_prompt"]) -print(llm_answer) -``` - ---- - -## 성능 특성 - -| 작업 | 규모 | 시간 | -|------|------|------| -| 웹 크롤링 | 1 URL | 5-30초 | -| 데이터 검증 | 1000 후보 | < 2초 | -| 벡터 임베딩 | 10K 엔티티 | 4초 | -| 배치 저장 | 100K 노드/에지 | 28초 | -| 2-hop 쿼리 | 10K 노드 | < 200ms | -| 경로 찾기 | max_length=5 | < 300ms | -| 중심성 계산 | top_n=100 | < 600ms | - ---- - -## 결론 - -이 **온톨로지 시스템 구축 플랫폼**은: - -✅ **자동화**: 웹 데이터 → 구조화된 지식 자동 변환 -✅ **확장성**: 10K+ 노드 대규모 그래프 지원 -✅ **지능화**: 중복 제거, 패턴 분석, 중심성 계산 -✅ **통합성**: REST API, GraphQL, LLM 연계 -✅ **실용성**: 실제 비즈니스 문제 해결 가능 - -### 다음 단계 -- **Phase 7**: LLM 스트리밍 + 캐싱 -- **Phase 8**: 멀티테넌트 + 실시간 업데이트 -- **Production**: Docker/Kubernetes 배포 - ---- - -**플랫폼 버전**: 0.6.0 -**상태**: Phase 0-6 완료, Phase 7-8 계획 -**마지막 업데이트**: 2026-05-14 diff --git a/Ontology Platform Research Report.docx b/Ontology Platform Research Report.docx deleted file mode 100644 index 2f9e6202e684ba32b9f29d584a63e1c1aea1b0dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34101 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 $@-v|>m|H3` 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.3` 설치 -- [x] AsyncWebCrawler 초기화 및 생명 주기 관리 -- [x] CacheMode.ENABLED 기본 설정 - -### 2. 프로파일 기반 수집 전략 -구현된 프로파일: -- [x] **fast_static**: HTTP fetch만 (Phase 0-1 호환) - - BasicCrawler 사용 - - 빠른 응답 시간 (0.1-0.5초) - - 정적 콘텐츠 최적화 - -- [x] **dynamic_page**: Playwright + JS rendering (Phase 2) - - AsyncWebCrawler 사용 - - JavaScript 렌더링 지원 - - 동적 페이지 처리 가능 - - Crawl4AI Markdown 출력 지원 - -- [ ] **full_capture**: 스크린샷/PDF/MHTML (미구현, Phase 2+) -- [ ] **structured_extract**: CSS/XPath 스키마 (미구현, Phase 2+) -- [ ] **deep_discovery**: URL Seeder + BFS (미구현, Phase 3+) - -### 3. 지능형 프로파일 선택 (_select_profile) -```python -def _select_profile(url: str) -> CrawlProfile: - """ - URL 특성에 따른 자동 프로파일 선택: - - robots.txt JS-heavy 도메인 → dynamic_page - - 기본값 → fast_static - """ -``` -**현재**: fast_static 기본값 (Phase 2 MVP) -**TODO**: robots.txt 파싱, 도메인 화이트리스트 추가 - -### 4. Trafilatura 후처리 통합 -- HTML → Trafilatura 추출 → ContentUnit -- Markdown (Crawl4AI) 또는 cleaned_html 지원 -- 메타데이터 정규화 (title, author, publish_date, language) - -### 5. API 개선 - -#### 기존 엔드포인트 (Phase 0-1) -``` -POST /api/v1/extract/url?url= -→ profile: trafilatura (기본값) -``` - -#### Phase 2 추가 기능 -``` -POST /api/v1/extract/url?url=&profile= -→ profile: fast_static | dynamic_page -``` - -응답 추가 필드: -```json -{ - "profile_used": "trafilatura", // 실제 사용된 프로파일 - "url": "...", - "title": "...", - "entities": [...], - ... -} -``` - -### 6. 폴백 메커니즘 (Robustness) -``` -시도 1: 지정된 프로파일 사용 - └─ 실패 → 시도 2 -시도 2: BasicCrawler (HTTP only) - └─ 실패 → 에러 반환 -``` - -## Acceptance Gate 2 검수 항목 - -### ✅ 완료된 항목 -- [x] JS 렌더링이 필요한 동적 페이지 프로파일 구현 - - Crawl4AI + Playwright 기반 - - 실제 작동 검증 필요 (Playwright 설정 완료 시) - -- [x] 정적 페이지 fast_static 프로파일 ✓ 0.15초 - - HTTP fetch + Trafilatura - - Phase 0-1 완전 호환 - -- [x] 프로파일 자동 선택 로직 구현 - - _select_profile() 메서드 - - 도메인 기반 선택 가능 - -- [x] 폴백 메커니즘 구현 - - dynamic_page 실패 → basic_http 자동 전환 - - 메모리 누수 방지 (async context manager) - -- [x] Phase 0-1 회귀 테스트 ✓ (기존 기능 정상) - - extract_web_content() 호환 - - LightweightExtractor 호환 - -### ⏳ 검증 필요 항목 -- [ ] Playwright 기반 동적 페이지 실제 렌더링 테스트 - - 현재: deep_discovery 불가 (URL Seeder 미구현) - - dynamic_page: 코드 준비 완료, Playwright 브라우저 풀 설정 필요 - -- [ ] 메모리 누수 테스트 (50회 연속 크롤) - - AsyncWebCrawler lifetime 관리 필요 - - 테스트 환경 준비 필요 - -## 기술 스택 - -| 컴포넌트 | 버전 | 용도 | -|---------|------|------| -| Crawl4AI | 0.3+ | 동적 페이지 수집 | -| Playwright | auto | Crawl4AI 내부 (JS 렌더링) | -| Trafilatura | 2.0.0 | 메타데이터 + 본문 추출 | -| FastAPI | 0.x | API 엔드포인트 | - -## 다음 단계 (Phase 3+) - -1. **Phase 3 (Guardrails)**: LLM 출력 검증 게이트 - - OntologyExtractionResult 스키마 검증 - - confidence/evidence 필드 강제 - -2. **Phase 4 (Neo4j GraphRAG)**: RDF ↔ Property Graph 프로젝션 - - Fuseki → Neo4j 동기화 - - Vector 검색 지원 - -3. **Phase 5 (Knowledge Agent)**: 멀티에이전트 유지보수 루프 - - Analyst → Researcher → Curator 패턴 - - 자동 지식 공백 채우기 - -## 파일 변경 사항 - -``` -✏️ ontology_platform/ont_platform/core/crawler/crawl4ai_adapter.py - - BasicCrawler 유지 (폴백용) - - Crawl4AIAdapter 전면 재작성 - - CrawlProfile enum 추가 - - Profile 기반 crawl() 메서드 - -✏️ ontology_platform/ont_platform/api/phase0_app.py - - profile 파라미터 추가 - - dynamic_page 지원 - - profile_used 응답 필드 추가 - -✨ test_phase2_crawl.py (신규) - - Phase 2 프로파일 테스트 - - fast_static 검증 완료 - -``` - -## 성능 지표 - -| 작업 | 소요시간 | 상태 | -|------|---------|------| -| fast_static (example.com) | 0.15초 | ✅ 30초 목표 달성 | -| dynamic_page (준비 완료) | 미측정 | ⏳ Playwright 설정 필요 | - -## 참고 문헌 - -- 설계서 §5 Phase 2 (p. 191-194) -- Crawl4AI 분석 §21.2 (Profile 권장사항) -- OntoCast 분석 §12 (Content Acquisition 아키텍처) diff --git a/PHASE3_COMPLETION.md b/PHASE3_COMPLETION.md deleted file mode 100644 index 1cf1b95..0000000 --- a/PHASE3_COMPLETION.md +++ /dev/null @@ -1,223 +0,0 @@ -# Phase 3: Guardrails 통합 (LLM 출력 검증) 완료 보고서 - -**완료일**: 2026-05-14 -**상태**: ✅ Acceptance Gate 3 검수 준비 완료 - -## 개요 - -Phase 3은 **LLM 출력 검증 게이트**를 구현했습니다. 추출된 온톨로지 후보(entities, relations)를 검증하여 잘못된 데이터가 RDF graph에 들어가는 것을 차단합니다. - -### 아키텍처: 플러그인 방식 -``` -┌─────────────────────────────────┐ -│ OntologyGuard (Facade) │ ← 사용자 facing API -└────────────┬────────────────────┘ - │ - ├─→ LightweightValidator (현재, Phase 3 MVP) - ├─→ GuardrailsValidator (미구현, Phase 3+) - └─→ OntoCastValidator (미구현, Phase 3 Option B) -``` - -**장점**: 검증 엔진을 언제든지 교체 가능 (Guardrails, OntoCast 추가 비용 없음) - -## 구현 내용 - -### 1. Pydantic 기반 검증 모델 (`models.py`) - -```python -# 핵심 모델 -- OntologyEntity: id, label, type, confidence, evidence -- OntologyRelation: id, source_id, predicate, target_id, confidence -- OntologyExtractionResult: entities, relations, validation status -- Evidence: source_url, offset, confidence -``` - -### 2. 검증 규칙 (`validators.py`) - -#### 현재 구현된 검증 (Phase 3 MVP) - -✅ **Entity 검증** -- ID 형식: `E_` 프리픽스 필수 -- Label: 최소 1자, 최대 500자 -- Confidence: 0.0 ~ 1.0 범위 -- Type: class, individual, property 중 하나 - -✅ **Relation 검증** -- ID 형식: `R_` 프리픽스 필수 -- 엔드포인트 존재 확인: source_id, target_id가 entities에 있는지 확인 -- 자기 루프 방지: source_id != target_id -- Confidence: 0.0 ~ 1.0 범위 - -✅ **그래프 일관성** -- 중복 entity ID 감지 -- 의미 없는 entity 경고 (value, keyword, type, name 등) - -### 3. 플러그인 팩토리 (`ValidatorFactory`) - -```python -# 현재 -ValidatorFactory.create("lightweight") # Phase 3 MVP ✅ - -# 향후 확장 -ValidatorFactory.create("guardrails") # Phase 3+ (구현 준비됨) -ValidatorFactory.create("ontocast") # Phase 3 Option B (구현 준비됨) -``` - -### 4. 사용자 API (`guards.py`) - -```python -# 간단한 사용법 -guard = OntologyGuard(validator_type="lightweight") -validated = await guard.validate(raw_extraction_result) - -# strict 모드 (에러 시 즉시 실패) -guard_strict = OntologyGuard(validator_type="lightweight", strict=True) -``` - -### 5. API 통합 (`phase0_app.py`) - -POST `/api/v1/extract/url` 응답에 검증 정보 추가: - -```json -{ - "url": "https://example.com", - "title": "Example Domain", - "entities": [...], - "relations": [...], - "validation_passed": true, // ← Phase 3 NEW - "validation_errors": [], // ← Phase 3 NEW - "warnings": [] -} -``` - -## 테스트 결과 - -### 검증 케이스 (모두 통과 ✅) - -| 테스트 | 설명 | 결과 | -|-------|------|------| -| Valid extraction | 올바른 extraction | ✅ validation_passed=true | -| Invalid entity ID | E_ 프리픽스 없음 | ✅ 감지 및 경고 | -| Missing relation endpoint | 존재하지 않는 entity 참조 | ✅ 감지 및 거부 | -| Confidence out of range | confidence > 1.0 | ✅ 감지 및 거부 | -| Self-relation | E_001 → E_001 | ✅ 감지 및 거부 | - -### 실제 API 테스트 - -``` -POST http://127.0.0.1:8000/api/v1/extract/url?url=https://example.com - -Response: -{ - "validation_passed": true, - "entity_count": 2, - "relation_count": 0, - "warnings": [] -} -``` - -## Acceptance Gate 3 검수 항목 - -### ✅ 완료된 항목 - -- [x] LLM 출력 스키마 검증 (Pydantic) - - Entity ID format 강제 - - Confidence range 검증 - - Relation endpoint 존재 확인 - -- [x] 잘못된 스키마 응답 자동 처리 - - Non-strict 모드: 경고로 수집 - - Strict 모드: 예외 발생 - -- [x] Reask 메커니즘 준비 - - validation_errors 리스트로 재추출 정보 전달 가능 - - 나중에 LLM에 피드백으로 전달 가능 - -- [x] Phase 0-2 기능 회귀 없음 - - Trafilatura 추출 ✓ - - Crawl4AI 통합 ✓ - - Lightweight extraction ✓ - -- [x] 외부 의존성 최소화 - - Guardrails 미설치 상태에서도 작동 ✓ - - Pydantic만 사용 (이미 설치됨) ✓ - -### ⏳ 향후 옵션 - -#### 옵션 B: Full OntoCast 통합 -```python -# 나중에 구현 가능 -guard = OntologyGuard(validator_type="ontocast") -# OntoCast의 Renderer/Critic 출력을 검증 -``` - -#### Guardrails 통합 -```python -# 나중에 구현 가능 -guard = OntologyGuard(validator_type="guardrails") -# Guardrails Hub와 연동, reask 루프 추가 -``` - -## 파일 구조 - -``` -ontology_platform/ont_platform/core/validation/ -├── __init__.py # 모듈 export -├── models.py # Pydantic 모델 (OntologyEntity, OntologyRelation) -├── validators.py # 검증 로직 (BaseValidator, LightweightValidator, Factory) -└── guards.py # 사용자 API (OntologyGuard) - -ontology_platform/ont_platform/api/ -└── phase0_app.py # API 통합 (validation_passed 필드 추가) - -tests/ -└── test_phase3_validation.py # 검증 테스트 (5개 케이스) -``` - -## 성능 지표 - -| 작업 | 소요시간 | 상태 | -|------|---------|------| -| 추출 + 검증 (example.com) | 0.15초 | ✅ 30초 목표 달성 | -| 5개 검증 테스트 | 0.5초 | ✅ 빠른 피드백 | - -## 설계의 유연성 - -### Phase 3 MVP → Phase 3+ 업그레이드 경로 - -```python -# 현재 (Phase 3 MVP, 이 PR) -guard = OntologyGuard(validator_type="lightweight") - -# Phase 3+ (Guardrails 추가 후) -pip install guardrails-ai -guard = OntologyGuard(validator_type="guardrails") -# 코드 한 줄 변경으로 업그레이드 - -# Phase 3 Option B (OntoCast 통합) -guard = OntologyGuard(validator_type="ontocast") -# OntoCast의 critic loop와 통합 -``` - -### 구현 없이 준비된 구조 -- `guardrails_guards.py` (구현 대기) -- `ontocast_guards.py` (구현 대기) -- `ValidatorFactory` 이미 확장 가능 - -## 참고 문헌 - -- 설계서 §5 Phase 3 (p. 222-224) -- Guardrails 분석 §15.4-15.5 (validator 패턴) -- Pydantic v2 문서 (field validators) - -## 다음 단계 - -### Phase 4: Neo4j GraphRAG 통합 -- RDF ↔ Property Graph 프로젝션 -- Vector 검색 지원 -- Entity Resolver 통합 - -### 또는: Phase 3 Option B 선택 -- OntoCast와의 full integration -- Critic loop 통합 -- SPARQL UPDATE 검증 diff --git a/PHASE3_OPTION_B.md b/PHASE3_OPTION_B.md deleted file mode 100644 index c172505..0000000 --- a/PHASE3_OPTION_B.md +++ /dev/null @@ -1,222 +0,0 @@ -# Phase 3 Option B: OntoCast GraphUpdate 검증 (Hybrid 접근법) - -**완료일**: 2026-05-14 -**상태**: ✅ Acceptance Gate 3 Option B 검수 준비 완료 - -## 개요 - -Phase 3 Option B는 **점진적 OntoCast 통합** (Hybrid approach)입니다. - -### 핵심 전략 -- **Phase 0-2 유지**: 현재 경량 구조 그대로 -- **GraphUpdate 검증 추가**: OntoCast의 SPARQL 쿼리 검증 -- **Critic loop 준비**: Phase 4+에서 추가 가능하도록 설계 - -``` -시간 축 -──────────────────────────────────────── -Phase 0-2: 경량 추출 (완료) -Phase 3 MVP (A): 엔티티 검증 (완료) -Phase 3 Option B: SPARQL 검증 (지금 이것) ← 지금 여기 -Phase 4+: Critic loop + Fuseki (향후) -``` - -## 구현 내용 - -### 1. SPARQL 검증기 (`SPARQLValidator`) - -**SPARQL 쿼리 기본 검증**: -```python -✓ 문법 검사: 괄호/중괄호 균형, 키워드 확인 -✓ SQL 인젝션 패턴 감지 -✓ 프리픽스 선언 확인 -✓ 쿼리 크기 경고 (너무 큰 쿼리 감지) -``` - -### 2. OntoCastValidator (Phase 3 Option B) - -**GraphUpdate 검증**: -```python -단계 1: SPARQL 문법 검증 - - 빈 쿼리 감지 - - 괄호 불균형 감지 - - 위험한 패턴 감지 (SQL injection 등) - -단계 2: 작업 순서 검증 - - 안전한 순서: INSERT → UPDATE → DELETE - - 불안전한 순서 감지 (DELETE 후 INSERT 등) - -단계 3: 프리픽스 검증 - - 선언되지 않은 프리픽스 감지 - - 표준 RDF 프리픽스 자동 인식 - -단계 4: 작업 수량 체크 - - 빈 작업 목록 경고 - - 과도하게 큰 작업 경고 (100개 초과) -``` - -### 3. 테스트 결과 (모두 통과 ✅) - -| 테스트 | 설명 | 결과 | -|-------|------|------| -| Valid SPARQL | 올바른 INSERT 작업 | ✅ 통과 | -| Invalid syntax | 괄호 불균형 | ✅ 감지 | -| Safe order | INSERT → UPDATE → DELETE | ✅ 통과 | -| Unsafe order | DELETE 후 INSERT | ✅ 감지 | -| Undeclared prefix | 선언되지 않은 프리픽스 | ✅ 경고 | -| Utility functions | SPARQLValidator 직접 사용 | ✅ 통과 | - -## 아키텍처: Phase 0-2와의 호환성 - -``` -┌─────────────────────────────────────┐ -│ OntologyGuard (통합 인터페이스) │ -└──────────┬──────────────────────────┘ - │ - ┌─────┴──────────┐ - │ │ - 경량 검증 OntoCast 검증 -(Phase 3 MVP) (Phase 3 Option B) - ↓ ↓ -엔티티/관계 SPARQL 쿼리 -검증 검증 -``` - -두 검증을 **동시에 사용 가능**: -```python -# 둘 다 활성화 -guard_entity = OntologyGuard(validator_type="lightweight") -guard_sparql = OntologyGuard(validator_type="ontocast") - -# 또는 런타임에 선택 -validator_type = "ontocast" if use_ontocast else "lightweight" -guard = OntologyGuard(validator_type=validator_type) -``` - -## 파일 구조 - -``` -신규 생성: - ✨ ont_platform/core/validation/ontocast_validator.py - ├── SPARQLValidator: 기본 SPARQL 검증 - ├── OntoCastValidator: GraphUpdate 검증 - └── GraphUpdate: 검증 결과 모델 - -수정: - ✏️ ont_platform/core/validation/validators.py - (ValidatorFactory에 OntoCast 지원 추가) - ✏️ ont_platform/core/validation/__init__.py - (OntoCastValidator export) - -테스트: - ✨ test_phase3_option_b.py (6개 테스트, 모두 통과) -``` - -## Acceptance Gate 3 Option B 상태 - -### ✅ 완료된 항목 - -- [x] SPARQL 문법 검증 - - 괄호/중괄호 균형 ✓ - - 키워드 확인 ✓ - - SQL 인젝션 패턴 감지 ✓ - -- [x] 안전한 작업 순서 검증 (INSERT → UPDATE → DELETE) - - 불안전한 순서 감지 ✓ - - 순서 강제 가능 ✓ - -- [x] 프리픽스 선언 검증 - - 미선언 프리픽스 감지 ✓ - - 표준 RDF 프리픽스 자동 인식 ✓ - -- [x] Phase 0-2 회귀 없음 - - 경량 검증 여전히 작동 ✓ - - API 호환성 유지 ✓ - -### ⏳ 향후 추가 예정 (Phase 4+) - -#### Critic Loop 통합 -```python -# Phase 4에서 구현될 것 -if validation_errors: - suggestions = generate_critic_suggestions(errors) - retry_result = await llm.retry(original_query, suggestions) -``` - -#### RDF 일관성 검증 -```python -# Fuseki 사용 가능 시 -if fuseki_available: - # 1. 쿼리 실행 시뮬레이션 - # 2. 결과 그래프 검증 - # 3. 일관성 확인 - validate_rdf_consistency(update) -``` - -#### GraphUpdate 추적 -```python -# 감사 로그 -graph_update_history.append({ - "timestamp": now, - "operation_count": len(operations), - "validation_status": "passed", - "execution_time": elapsed_ms, -}) -``` - -## 향후 옵션 - -### Phase 3 Option A (경량 MVP) vs Option B (Hybrid) 비교 - -| 항목 | Option A (MVP) | Option B (Hybrid) | -|------|---|---| -| 엔티티 검증 | ✅ | ✅ | -| SPARQL 검증 | ❌ | ✅ | -| OntoCast 의존성 | ❌ | 부분적 | -| Critic loop | ❌ (Phase 4+) | 준비됨 (Phase 4+) | -| 구현 복잡도 | 낮음 | 중간 | -| Phase 0-2 호환성 | ✅ | ✅ | - -## 설계의 확장성 - -### ValidatorFactory 플러그인 구조 - -현재: -```python -ValidatorFactory.create("lightweight") # Option A -ValidatorFactory.create("ontocast") # Option B (지금) -``` - -향후 추가 가능: -```python -ValidatorFactory.create("guardrails") # Phase 3+ (제3 선택지) -ValidatorFactory.create("full_ontocast") # Phase 4+ (완전통합) -``` - -## Phase 3 완료 상태 요약 - -| 선택지 | 상태 | 특징 | -|-------|------|------| -| **A: 경량 MVP** | ✅ 완료 | 엔티티/관계 검증만 | -| **B: Hybrid** | ✅ 완료 | + SPARQL 검증 | -| **C: Guardrails** | ⏳ 준비 | + Reask 루프 | - -**현재**: 옵션 A + B 모두 선택 가능한 상태 - -## 다음 단계 - -### Phase 4: Neo4j GraphRAG (권장) -- RDF ↔ Property Graph 프로젝션 -- Vector 검색 + Entity Resolver -- GraphUpdate 실행 시뮬레이션 - -### 또는: Phase 3+ (향후) -- Guardrails 통합 (더 정교한 reask) -- Full OntoCast (Critic loop 본격화) -- Fuseki 연동 (RDF 저장소) - -## 참고 문헌 - -- 설계서 §5 Phase 3 (p. 222-224) -- OntoCast sparql_models.py (GraphUpdate 모델) -- SPARQL 1.1 명세 (검증 규칙) diff --git a/PHASE4_COMPLETION.md b/PHASE4_COMPLETION.md deleted file mode 100644 index fea6425..0000000 --- a/PHASE4_COMPLETION.md +++ /dev/null @@ -1,383 +0,0 @@ -# Phase 4: Neo4j Graph + Vector Search - Completion Report - -**완료일**: 2026-05-14 -**상태**: ✅ Phase 4 (4-Lite) 구현 완료 - -## 개요 - -Phase 4는 **Neo4j 기반 벡터 검색** (4-Lite 옵션)을 구현합니다. - -### 핵심 기능 -- Neo4j Property Graph 저장소 -- SentenceTransformer 벡터 임베딩 (all-MiniLM-L6-v2) -- 의미 유사도 검색 (Cosine Similarity) -- 엔티티 이웃 그래프 순회 -- 그래프 통계 조회 - -## 구현 내용 - -### 1. Neo4jAdapter (`neo4j_adapter.py`) - -**비동기 연결 관리**: -```python -✓ AsyncGraphDatabase 지원 -✓ 세션 풀 관리 -✓ 연결 테스트 -✓ Graceful shutdown -``` - -**엔티티 관리**: -```python -✓ 엔티티 노드 생성 (임베딩 포함) -✓ 관계 엣지 생성 -✓ 배치 처리 -✓ 에러 처리 및 로깅 -``` - -**검색 기능**: -```python -✓ 벡터 유사도 검색 -✓ 폴백: 라벨 기반 검색 -✓ 임계값 필터링 -✓ 상위 K개 결과 -``` - -**그래프 순회**: -```python -✓ 깊이 제한 이웃 탐색 (depth 1-2) -✓ 관계 정보 포함 -✓ 연결 노드 계산 -``` - -### 2. FastAPI 통합 (`phase0_app.py`) - -**새로운 엔드포인트**: - -#### `/api/v1/search/vector` (POST) -```python -query: str - 검색 쿼리 -limit: int = 10 - 결과 개수 (1-100) -threshold: float = 0.5 - 유사도 임계값 (0.0-1.0) - -응답: { - "query": "...", - "results": [...], - "result_count": N, - "limit": 10, - "threshold": 0.5 -} -``` - -#### `/api/v1/search/stats` (GET) -```python -응답: { - "status": "connected|disconnected", - "stats": { - "total_nodes": N, - "total_edges": M, - "entity_nodes": K - } -} -``` - -#### `/api/v1/search/entity/{entity_id}` (GET) -```python -entity_id: str - 엔티티 ID -depth: int = 1 - 순회 깊이 (1-2) - -응답: { - "entity": "E_...", - "label": "...", - "type": "...", - "neighbors": N, - "relations": [...] -} -``` - -#### `/api/v1/search/ingest` (POST) [NEW] -```python -입력: { - "entities": [{id, label, type, confidence}], - "relations": [{source_id, target_id, predicate, confidence}] -} - -응답: { - "status": "success", - "entities_ingested": N, - "relations_ingested": M, - "total_ingested": N+M -} -``` - -### 3. Docker 지원 (`docker-compose.neo4j.yml`) - -**Neo4j 5.18.1 설정**: -```yaml -컨테이너: ontology-neo4j -포트: - - 7687 (Bolt, 드라이버 연결) - - 7474 (HTTP, 브라우저) - - 7473 (HTTPS) - -인증: neo4j / ontology123 -메모리: 1G 초기, 2G 최대 -APOC: 고급 그래프 연산 지원 -``` - -**시작 명령어**: -```bash -docker-compose -f docker-compose.neo4j.yml up -d -``` - -### 4. 벡터 임베딩 - -**모델**: all-MiniLM-L6-v2 -- 차원: 384 -- 다국어 지원 -- 빠른 처리 (CPU 친화적) - -**처리**: -```python -# 엔티티 레이블 임베딩 -embedding = model.encode([entity.label]) - -# 쿼리 임베딩 -query_embedding = model.encode([query_text]) - -# 유사도 계산 -similarity = cosine_similarity(embedding, query_embedding) -``` - -### 5. 통합 테스트 (`test_phase4_integration.py`) - -**테스트 항목** (8개): - -| 테스트 | 설명 | 상태 | -|-------|------|------| -| Neo4j Connection | 연결 성공 여부 | ✅ (Docker 필요) | -| Embedder Init | 모델 로드 | ✅ 통과 | -| Entity Creation | 엔티티 노드 생성 | ✅ (Docker 필요) | -| Relation Creation | 관계 엣지 생성 | ✅ (Docker 필요) | -| Vector Search | 의미 검색 | ✅ (Docker 필요) | -| Entity Neighbors | 이웃 탐색 | ✅ (Docker 필요) | -| Graph Stats | 통계 조회 | ✅ (Docker 필요) | -| End-to-End Pipeline | 전체 파이프라인 | ✅ 통과 | - -## 아키텍처 - -### Phase 0-4 전체 흐름 - -``` -┌─────────────────────────────────────┐ -│ Phase 0-1: 콘텐츠 추출 (Trafilatura) │ -│ ↓ │ -│ Phase 2: 동적 페이지 (Crawl4AI) │ -│ ↓ │ -│ Phase 3: 검증 (OntologyGuard) │ -│ ↓ │ -│ Phase 4: 그래프 저장 + 검색 │ -│ ├─ Entity Nodes (with embeddings) │ -│ ├─ Relation Edges │ -│ └─ Vector Search │ -└─────────────────────────────────────┘ -``` - -### 엔드포인트 매핑 - -``` -POST /api/v1/extract/url - ├─ Phase 0-1: Trafilatura 추출 - ├─ Phase 2: Crawl4AI 동적 크롤링 (선택) - ├─ Phase 3: LightweightValidator 검증 - └─ 응답: 엔티티 + 관계 - -POST /api/v1/search/ingest - ├─ Neo4j 연결 - ├─ Entity 노드 생성 (임베딩) - ├─ Relation 엣지 생성 - └─ 응답: 수집된 노드/엣지 수 - -POST /api/v1/search/vector - ├─ 쿼리 텍스트 임베딩 - ├─ Cosine 유사도 검색 - ├─ 폴백: 라벨 기반 검색 - └─ 응답: 유사 엔티티 목록 - -GET /api/v1/search/stats - └─ 그래프 통계 (노드/엣지 수) - -GET /api/v1/search/entity/{entity_id} - └─ 엔티티 이웃 정보 (깊이 1-2) -``` - -## 파일 구조 - -``` -신규 생성: - ✨ ontology_platform/ont_platform/core/graph/ - └── neo4j_adapter.py (Neo4jAdapter, Neo4jConfig) - - ✨ ontology_platform/ont_platform/core/graph/__init__.py - (Neo4jAdapter 및 Neo4jConfig export) - - ✨ docker-compose.neo4j.yml (Neo4j 컨테이너) - - ✨ test_phase4_integration.py (8개 테스트) - -수정: - ✏️ ontology_platform/ont_platform/api/phase0_app.py - ├─ search_router 추가 - ├─ /api/v1/search/vector 엔드포인트 - ├─ /api/v1/search/stats 엔드포인트 - ├─ /api/v1/search/entity/{entity_id} 엔드포인트 - ├─ /api/v1/search/ingest 엔드포인트 - └─ get_neo4j_adapter() 초기화 함수 -``` - -## 설정 및 의존성 - -### 설치된 패키지 - -```bash -pip install neo4j==6.2.0 -pip install sentence-transformers==5.5.0 -``` - -### 환경 설정 - -**Neo4j 기본값**: -- URI: bolt://localhost:7687 -- Username: neo4j -- Password: ontology123 -- Database: neo4j - -**커스텀 설정**: -```python -config = Neo4jConfig( - uri="bolt://custom-host:7687", - username="custom_user", - password="custom_pass", - database="custom_db" -) -adapter = Neo4jAdapter(config=config) -``` - -## 성능 특성 - -### 벡터 임베딩 -- 모델 로드: ~2-3초 (첫 실행) -- 임베딩 생성: ~5-10ms (텍스트당) -- 메모리: ~350MB (모델) - -### Neo4j 작업 -- 노드 생성: ~10-50ms (배치 모드) -- 엣지 생성: ~5-30ms -- 벡터 검색: ~50-200ms (그래프 크기에 따라) -- 이웃 순회: ~20-100ms - -### 확장성 -- 권장 그래프 크기: 10K-100K 노드 (Neo4j 기본) -- 더 큰 그래프: Neo4j Enterprise + GDS 라이브러리 - -## 다음 단계 - -### Phase 5: GraphRAG (선택사항) - -```python -# 향후 구현 -1. RDF ↔ Property Graph 변환 -2. Entity Resolver (중복 제거) -3. Complex pattern matching -4. Subgraph retrieval for context -``` - -### 최적화 기회 - -```python -# 배치 임베딩 -embeddings = model.encode(labels, batch_size=32) - -# Neo4j 배치 쓰기 -with driver.session() as session: - for batch in chunked(entities, 100): - session.execute_write(create_nodes_batch, batch) - -# 벡터 인덱스 생성 (Neo4j 5.11+) -CREATE VECTOR INDEX entity_embeddings -FOR (n:Entity) ON (n.embedding) -OPTIONS {indexConfig: {`vector.dimensions`: 384}} -``` - -## Phase 4 상태 요약 - -| 항목 | 상태 | 설명 | -|------|------|------| -| **Neo4j Adapter** | ✅ 완료 | 비동기 드라이버, 임베딩, 검색 | -| **API 엔드포인트** | ✅ 완료 | 5개 엔드포인트 (검색, 통계, 수집) | -| **Docker 설정** | ✅ 완료 | neo4j 5.18.1 컨테이너 | -| **벡터 임베딩** | ✅ 완료 | all-MiniLM-L6-v2 (384-dim) | -| **통합 테스트** | ✅ 완료 | 8개 테스트 (2개 통과, 6개 Docker 대기) | -| **문서화** | ✅ 완료 | 완전한 API 및 구성 문서 | - -## 실행 방법 - -### 1. Neo4j 시작 -```bash -docker-compose -f docker-compose.neo4j.yml up -d -``` - -### 2. 임베딩 모델 다운로드 (자동) -```bash -python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')" -``` - -### 3. FastAPI 서버 시작 -```bash -python -m uvicorn ontology_platform.ont_platform.api.phase0_app:app --reload -``` - -### 4. 테스트 실행 -```bash -python test_phase4_integration.py -``` - -## 사용 예시 - -### 1. URL에서 추출 -```bash -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://example.com" -``` - -### 2. 그래프에 수집 -```bash -curl -X POST "http://localhost:8000/api/v1/search/ingest" \ - -H "Content-Type: application/json" \ - -d '{ - "entities": [ - {"id": "E_1", "label": "Python", "type": "Language", "confidence": 0.95} - ], - "relations": [] - }' -``` - -### 3. 의미 검색 -```bash -curl "http://localhost:8000/api/v1/search/vector?query=programming+languages&limit=10" -``` - -### 4. 통계 조회 -```bash -curl "http://localhost:8000/api/v1/search/stats" -``` - -### 5. 이웃 탐색 -```bash -curl "http://localhost:8000/api/v1/search/entity/E_1?depth=1" -``` - -## 참고 문헌 - -- 설계서 §6 Phase 4 (p. 225-240) -- Neo4j Python Driver: https://neo4j.com/docs/python-manual/current/ -- SentenceTransformers: https://www.sbert.net/ -- Cosine Similarity: https://en.wikipedia.org/wiki/Cosine_similarity diff --git a/PHASE5_COMPLETION.md b/PHASE5_COMPLETION.md deleted file mode 100644 index fda575a..0000000 --- a/PHASE5_COMPLETION.md +++ /dev/null @@ -1,312 +0,0 @@ -# Phase 5 GraphRAG 실행 현황 (2026-05-14) - -## ✅ Phase 5.0 완료 (필수 기능) - -### 1. EntityResolver - 의미 기반 중복 감지 -**파일**: `ontology_platform/ont_platform/core/graph/entity_resolver.py` - -- ✅ 벡터 임베딩 (SentenceTransformer all-MiniLM-L6-v2) -- ✅ Jaro-Winkler 텍스트 유사도 -- ✅ 2단계 매칭 (0.6*벡터 + 0.4*텍스트) -- ✅ 라벨 정규화 (소문자, 특수문자 제거, 공백 처리) -- ✅ 엔티티 병합 및 증거 통합 -- ✅ 해상도 리포트 생성 - -**테스트**: 24개 모두 통과 -``` -✓ Label normalization (4 tests) -✓ Jaro-Winkler similarity (4 tests) -✓ Text similarity (4 tests) -✓ Initialization (3 tests) -✓ Duplicate detection (4 tests) -✓ Cluster resolution (2 tests) -✓ Resolution report (3 tests) -``` - -### 2. RDFToPropertyGraphConverter - 양방향 변환 -**파일**: `ontology_platform/ont_platform/core/graph/rdf_converter.py` - -- ✅ RDF 트리플 → Property Graph 변환 -- ✅ Neo4j 노드/엣지 생성 -- ✅ 네임스페이스 URI 처리 -- ✅ 역변환 지원 (Property Graph → RDF) - -**테스트**: 2개 모두 통과 - ---- - -## ✅ Phase 5.1 완료 (고급 기능) - -### 3. SubgraphRetriever - 의미 기반 부분그래프 -**파일**: `ontology_platform/ont_platform/core/graph/subgraph_retriever.py` - -**신규 기능**: -- ✅ `retrieve_by_semantic_query()` - 쿼리 임베딩 기반 검색 - - 쿼리를 벡터로 임베딩 - - 모든 엔티티와 코사인 유사도 계산 - - 임계값 기반 필터링 (min_similarity) - - 상위 K개 매칭 엔티티 반환 - - N-hop 확장으로 컨텍스트 추출 - -**기존 기능**: -- ✅ `retrieve_neighborhood()` - N-hop 부분그래프 (1-3 hops) -- ✅ `retrieve_context()` - 다중 엔티티 공통 경로 -- ✅ `retrieve_induced_subgraph()` - 유도 부분그래프 - -**테스트**: 15개 모두 통과 -``` -✓ Initialization (2 tests) -✓ Semantic query (8 tests) -✓ Neighborhood retrieval (3 tests) -✓ Induced subgraph (2 tests) -``` - -### 4. Phase 5 GraphRAG API - 완전한 엔드포인트 스위트 -**파일**: `ontology_platform/ont_platform/api/phase5_app.py` - -**엔드포인트** (총 9개): -``` -POST /api/v1/graph/resolve - → 엔티티 중복 감지 및 병합 - -POST /api/v1/graph/subgraph - → N-hop 부분그래프 추출 - -POST /api/v1/graph/subgraph/semantic - → 의미 기반 부분그래프 검색 (신규) - -POST /api/v1/graph/patterns/paths - → 두 엔티티 사이의 경로 검색 - -POST /api/v1/graph/patterns/cycles - → 순환 경로 감지 - -POST /api/v1/graph/analytics/centrality - → PageRank, Betweenness, Closeness, Degree - -POST /api/v1/graph/analytics/communities - → Louvain, Leiden 커뮤니티 감지 - -GET /health - → 상태 확인 - -GET /info - → 플랫폼 정보 -``` - -**테스트**: 25개 모두 통과 (경고 0개) -``` -✓ Health check (2 tests) -✓ Entity resolution (2 tests) -✓ Subgraph extraction (4 tests) -✓ Pattern matching (3 tests) -✓ Graph analytics (6 tests) -✓ Error handling (2 tests) -✓ Parameter validation (4 tests) -✓ Endpoint routing (2 tests) -``` - ---- - -## 🔗 Phase 5 + Phase 7 통합 검증 - -**파일**: `tests/integration/test_phase5_phase7_integration.py` - -통합 테스트 (8개 모두 통과): -``` -✓ Entity resolution enhances LLM RAG -✓ Semantic query finds relevant context -✓ Graph analytics for data quality -✓ Pattern matching detects inconsistencies -✓ RAG context quality with Phase 5 -✓ End-to-end entity resolution pipeline -✓ Semantic context more relevant than random -✓ Deduplicated graph smaller and cleaner -``` - ---- - -## 📊 테스트 현황 - -| 컴포넌트 | 테스트 | 상태 | -|---------|--------|------| -| EntityResolver | 24 | ✅ 통과 | -| SubgraphRetriever | 15 | ✅ 통과 | -| RDF Converter | 2 | ✅ 통과 | -| Phase 5 API | 25 | ✅ 통과 | -| Integration | 8 | ✅ 통과 | -| **총계** | **74** | **✅ 통과** | - ---- - -## 🎯 성능 목표 및 달성 상황 - -| 작업 | 목표 | 상태 | -|------|------|------| -| 벡터 임베딩 | 10K 엔티티 < 5초 | ✅ 배치 처리 최적화 | -| 텍스트 유사도 | 10K 엔티티 < 2초 | ✅ 벡터화 가능 | -| 부분그래프 추출 | 2-hop < 200ms | ✅ Cypher 최적화 | -| 의미 검색 | 상위 K개 < 200ms | ✅ 코사인 유사도 | -| Neo4j 배치 쓰기 | 100K 노드 < 30초 | ✅ UNWIND + MERGE | - ---- - -## 📝 Phase 7 LLM 통합 포인트 - -### RAG 파이프라인 강화 - -**이전 (Phase 7 alone)**: -``` -쿼리 → GraphAnalytics.find_influential_entities() - → 상위 중요 엔티티만 반환 - → LLM에 전달 -``` - -**현재 (Phase 5 + Phase 7)**: -``` -쿼리 → EntityResolver.detect_duplicates() - ↓ - 데이터 정제/병합 - ↓ - SubgraphRetriever.retrieve_by_semantic_query() - ↓ - 의미 기반 관련 엔티티 검색 - ↓ - N-hop 컨텍스트 그래프 - ↓ - LLM에 전달 -``` - -**개선 효과**: -- 중복 제거로 데이터 품질 30% 향상 -- 의미 검색으로 관련성 높은 컨텍스트 -- 더 정확한 LLM 응답 기대 - ---- - -## 🚀 Phase 5.2 준비 현황 - -### 아직 구현할 기능 (선택사항) - -1. **PatternMatcher 고급 기능** - - `find_motifs()` - 빈번한 그래프 패턴 검색 - - `find_strongly_connected_components()` - SCC 분석 - - 현재 기본 구현됨 - -2. **GraphAnalytics 확장** - - 추가 중심성 메트릭 - - 동적 커뮤니티 감지 - - 현재 기본 구현됨 - -3. **성능 최적화** - - Neo4j 인덱스 튜닝 - - 벡터 임베딩 캐싱 - - 배치 크기 동적 조정 - ---- - -## 📦 디렉토리 구조 - -``` -ontology_platform/ont_platform/ -├── core/graph/ -│ ├── __init__.py (전체 내보내기) -│ ├── entity_resolver.py [✅ Phase 5.0] -│ ├── rdf_converter.py [✅ Phase 5.0] -│ ├── subgraph_retriever.py [✅ Phase 5.1] -│ ├── pattern_matcher.py [Phase 5.2] -│ └── graph_analytics.py [Phase 5.2] -│ -└── api/ - ├── phase0_app.py - ├── ... - ├── phase5_app.py [✅ Phase 5.1] - ├── phase6_app.py - └── phase7_app.py - -tests/ -├── core/graph/ -│ ├── test_entity_resolver.py [✅ 24 tests] -│ ├── test_subgraph_retriever.py [✅ 15 tests] -│ └── test_rdf_converter.py [✅ 2 tests] -├── api/ -│ └── test_phase5_app.py [✅ 25 tests] -└── integration/ - └── test_phase5_phase7_integration.py [✅ 8 tests] -``` - ---- - -## 🔧 의존성 - -**이미 포함됨** (vendored): -- ✅ `sentence-transformers>=5.1.1` - 벡터 임베딩 -- ✅ `numpy` - 수치 계산 -- ✅ `neo4j>=5.28.1` - Neo4j 드라이버 -- ✅ `networkx>=3.0` - 그래프 알고리즘 - -**새로 추가됨**: -- ✅ `textdistance>=4.6.0` - Jaro-Winkler 유사도 - ---- - -## 📈 개발 프로세스 - -### Phase 5.0 (완료) -- 2024년 말: EntityResolver + RDF Converter 구현 -- 2025년 초: 단위 테스트 24개 작성 -- 통과율: 100% ✅ - -### Phase 5.1 (완료) -- 2025년 중반: SubgraphRetriever 의미 검색 추가 -- Phase 5 API 엔드포인트 9개 구현 -- API 테스트 25개 + 통합 테스트 8개 -- 통과율: 100% ✅ - -### Phase 5.2 (계획 중) -- PatternMatcher 고급 기능 -- GraphAnalytics 확장 -- 성능 벤치마크 및 최적화 - ---- - -## ✨ 핵심 성과 - -| 항목 | 수치 | -|------|------| -| 총 테스트 | 74 개 | -| 통과 | 74 개 (100%) | -| API 엔드포인트 | 9 개 | -| 통합 지점 | Phase 7 LLM RAG | -| 예상 RAG 품질 개선 | ~30% | - ---- - -## 🎓 기술 하이라이트 - -1. **벡터 + 텍스트 하이브리드 유사도** - - 임베딩 유사도 (0.6 가중치) - - 텍스트 유사도 (0.4 가중치) - - 정규화된 레이블 비교 - -2. **의미 기반 부분그래프 추출** - - 쿼리 임베딩 → 코사인 유사도 계산 - - 동적 K값 조정 - - N-hop 확장으로 컨텍스트 확보 - -3. **Async/Await 최적화** - - 배치 처리로 네트워크 왕복 최소화 - - Neo4j 연결 풀링 - - 병렬 임베딩 계산 - -4. **FastAPI 정식 구현** - - RESTful API 설계 - - 파라미터 검증 - - 에러 처리 - - 상태 모니터링 - ---- - -**생성 일시**: 2026-05-14 -**담당**: Claude Haiku 4.5 -**상태**: Phase 5.0 + 5.1 완료 (74/74 테스트 ✅) diff --git a/PHASE5_REFERENCE_ANALYSIS.md b/PHASE5_REFERENCE_ANALYSIS.md deleted file mode 100644 index 0aece3c..0000000 --- a/PHASE5_REFERENCE_ANALYSIS.md +++ /dev/null @@ -1,67 +0,0 @@ -# Phase 5 Reference Analysis - -## 1. 참조한 오픈소스 목록 - -- `참고/ontocast-main` -- `참고/neo4j-graphrag-python-main` -- `참고/knowledge_agent-main` -- `참고/OpenDeepResearcher-main` -- `참고/instructor-main` -- `참고/guardrails-main` -- Phase 1 계열 참고: `playwright-main`, `trafilatura-master`, `crawl4ai-main`, `firecrawl-main` - -## 2. 각 오픈소스에서 참고한 코드 구조 - -- ontocast: Pydantic report model, RDF triple payload, graph update/fix model, external evidence request 분리. -- neo4j-graphrag: `NodeType`, `RelationshipType`, `GraphSchema`, constraint/property schema, storage/query layer 분리. -- knowledge_agent: LangGraph state machine, role별 node, state에 todo/complete/report를 축적하는 loop. -- OpenDeepResearcher: query generation, source fetch, relevance evaluation, context extraction, iterative refinement loop. -- instructor: response model 중심 structured output, retry/validation exception 경계. -- guardrails: validation outcome, validator/reask/on-fail action 분리. - -## 3. 현재 프로젝트에 직접 적용한 구조 - -- Ontology schema를 DB registry로 분리: `OntologyEntityType`, `OntologyRelationType`. -- Claim과 graph fact를 분리: `Claim`은 provenance/evidence, `OntologyTriple`은 graph fact. -- Schema evolution을 review 대상으로 분리: `OntologyProposal`. -- Ontology가 모르는 영역을 별도 객체로 관리: `KnowledgeGap`. -- Domain-specific relation/type alias는 `crawler_platform/app/adapters`로 분리. -- Knowledge gap을 research queue item으로 변환하는 `GapTaskPlanner` 추가. - -## 4. 제외한 구조와 제외 이유 - -- Neo4j 전용 driver/query 구조: 현재 저장소는 SQLAlchemy/SQLite 중심이라 storage abstraction만 반영. -- LangGraph/MCP runtime: 현재 앱에 무거운 agent runtime을 넣기보다 queue/state 구조만 반영. -- OpenDeepResearcher의 외부 검색 API 호출: 네트워크/API 의존이 크므로 research task 추상화만 반영. -- Instructor/Guardrails 라이브러리 직접 의존: 현재 provider 독립 구조를 유지하기 위해 schema/retry/validation 개념만 반영. - -## 5. 새 core architecture 방향 - -```text -Crawler / Browser -→ Main Content Extraction -→ Semantic Cleaning -→ Structured Document -→ Structured Extraction -→ Validation -→ Ontology Registry -→ Claim / Evidence -→ Ontology Triple -→ Knowledge Gap -→ Gap-driven Research Queue -→ Governance Proposal -``` - -## 6. 기존 domain-specific hardcoding 제거 방향 - -- `relation_schema.py`에서 perfume relation rule을 제거하고 perfume adapter로 이동. -- `entity_normalizer.py`에서 perfume alias를 제거하고 adapter 기반 normalization으로 변경. -- Core validation은 config/registry/adapter constraint만 사용하도록 변경. -- Multi-domain test에서 `academic` domain의 `authoredBy` relation이 perfume adapter 없이 동작함을 검증. - -## 7. 남은 Phase 6 후보 - -- page classifier와 content zone도 adapter 기반으로 분리. -- external search planner를 gap-driven research loop에 연결. -- schema proposal approve/reject API와 migration history 추가. -- source conflict resolver를 triple merge 단계에 더 깊게 연결. diff --git a/PHASE_5_SUMMARY.md b/PHASE_5_SUMMARY.md deleted file mode 100644 index 0a4ff54..0000000 --- a/PHASE_5_SUMMARY.md +++ /dev/null @@ -1,435 +0,0 @@ -# Phase 5 GraphRAG 구현 완료 보고서 - -## 개요 - -Phase 5는 Neo4j 기반 그래프 데이터베이스를 활용하여 GraphRAG (Graph-based Retrieval Augmented Generation) 기능을 구현했습니다. - -**구현 기간**: Phase 0-4 → Phase 5.0-5.2 -**상태**: ✅ 완료 (모든 단계 구현 및 테스트 통과) - ---- - -## Phase 5.0: 기초 (Neo4j 통합 + RDF 변환 + Entity Resolver) - -### 파일 구조 - -``` -ontology_platform/ont_platform/core/graph/ -├── neo4j_adapter.py # Phase 4 확장 (배치 쓰기, 인덱스) -├── rdf_converter.py # RDF ↔ Property Graph 양방향 변환 -├── entity_resolver.py # 의미적 중복 제거 (벡터 + 텍스트) -├── subgraph_retriever.py # Phase 5.1: N-hop 부분 그래프 -├── pattern_matcher.py # Phase 5.1: 경로/순환/SCC 검색 -├── graph_analytics.py # Phase 5.2: 중심성/커뮤니티 -└── __init__.py # 모듈 내보내기 -``` - -### 핵심 구현 - -#### 1. Neo4j Adapter 확장 -```python -# 배치 처리 (UNWIND + MERGE) -async def batch_create_entity_nodes(entities, batch_size=1000) -async def batch_create_relation_edges(relations, batch_size=1000) - -# 인덱스 생성 -async def create_indexes() # entity_id, label, confidence - -# 임의 Cypher 쿼리 실행 -async def execute_cypher(cypher, params) -``` - -**성능**: -- 배치 크기 1000: ~30초에 100K 노드/에지 -- UNWIND + MERGE 최적화 - -#### 2. RDF ↔ Property Graph 변환 -```python -class RDFToPropertyGraphConverter: - # 트리플 → 노드/에지 변환 - async def convert_triples_to_graph(triples) - - # 노드/에지 → 트리플 역변환 - async def to_rdf_triples(nodes, edges) - - # RDF 일관성 검증 - async def validate_rdf_consistency(triples) -``` - -**특징**: -- 표준 네임스페이스 (RDF, RDFS, OWL, FOAF, SKOS) -- URI 정규화 및 라벨 추출 -- 경고 및 오류 수집 - -#### 3. Entity Resolver (의미적 중복 제거) -```python -class EntityResolver: - # 2단계 중복 감지 - async def detect_duplicates(entities, batch_size=1000) - # Stage 1: 벡터 유사도 (cosine, threshold=0.85) - # Stage 2: Jaro-Winkler 텍스트 유사도 (threshold=0.88) - # 복합 점수: 0.6×벡터 + 0.4×텍스트 - - # 엔티티 병합 - async def resolve_cluster(cluster, entities_map) - # - 대표 엔티티로 통합 - # - 모든 별칭 통합 - # - 증거 히스토리 보존 -``` - -**임베딩 모델**: `all-MiniLM-L6-v2` (384차원) -**성능**: 10K 엔티티 < 5초 - ---- - -## Phase 5.1: 그래프 쿼리 (SubgraphRetriever + PatternMatcher) - -### SubgraphRetriever - -```python -class SubgraphRetriever: - # N-hop 이웃 추출 (RAG 컨텍스트용) - async def retrieve_neighborhood( - entity_id, hops=2, limit=500, min_confidence=0.0 - ) - - # 다중 엔티티 공통 경로 검색 - async def retrieve_context( - entity_ids, context_hops=2 - ) - - # 유도 부분 그래프 (entity_ids로 유도) - async def retrieve_induced_subgraph( - entity_ids, include_intermediate=True - ) -``` - -**성능**: 2-hop 쿼리 < 200ms (10K 노드 그래프) - -### PatternMatcher - -```python -class PatternMatcher: - # 모든 경로 탐색 (깊이 우선) - async def find_paths( - start_id, end_id, max_length=5 - ) - - # 순환 의존성 감지 - async def find_cycles(min_length=2, max_length=5) - - # 강한 연결 성분 분석 - async def find_strongly_connected_components() - - # 그래프 모티프 검출 (삼각형, 체인, 별) - async def find_motifs(motif_type="triangle") - - # 엔티티 연결성 메트릭 - async def analyze_entity_connectivity(entity_id) -``` - ---- - -## Phase 5.2: 분석 (GraphAnalytics) - -### GraphAnalytics - -```python -class GraphAnalytics: - # 중심성 계산 (degree, pagerank, betweenness, closeness) - async def calculate_centrality(centrality_type="pagerank", top_n=100) - - # 커뮤니티 감지 (Louvain, label propagation) - async def detect_communities(algorithm="louvain") - - # 그래프 통계 (밀도, 직경, 연결 성분) - async def get_graph_statistics() - - # 영향력 있는 엔티티 (복합 점수) - async def find_influential_entities(top_n=20) -``` - -**특징**: -- 정규화된 점수 (0-1 범위) -- 순위 지정 (rank field) -- GDS 라이브러리 지원 + Cypher 폴백 - ---- - -## 테스트 결과 - -### Phase 5.0 테스트 -- ✅ `test_phase5_entity_resolver.py` (7 테스트) - - Label normalization - - Jaro-Winkler similarity - - Vector embeddings - - Duplicate detection - - Entity merging - - Resolution reporting - -### Phase 5.1 테스트 -- ✅ `test_phase5_subgraph_retriever.py` (6 테스트) - - Neighborhood extraction - - Multi-entity context - - Induced subgraph - - Input validation - -- ✅ `test_phase5_pattern_matcher.py` (10 테스트) - - Path finding - - Cycle detection - - Motif detection (triangle, chain, star) - - Entity connectivity - - Input validation - -### Phase 5.2 테스트 -- ✅ `test_phase5_graph_analytics.py` (8 테스트) - - Degree centrality - - PageRank centrality - - Community detection - - Graph statistics - - Influential entities - -### 통합 테스트 -- ✅ `test_phase5_integration_graphrag.py` (6 통합 테스트) - - RDF 변환 파이프라인 - - Entity resolution 파이프라인 - - Subgraph retrieval - - Pattern analysis - - Complete RAG workflow - -**전체 테스트 통과 현황**: 37/37 테스트 ✅ - ---- - -## 주요 기능 - -### 1. RDF ↔ Property Graph 양방향 변환 -``` -원본 데이터 (RDF 트리플) - ↓ -Subject-Predicate-Object - ↓ -Neo4j Property Graph - ↓ -노드(Entities) + 관계(Relationships) -``` - -### 2. 의미적 중복 감지 및 병합 -``` -입력: [Apple Inc., Apple Inc, apple inc, APPLE] - ↓ -임베딩 유사도 계산 - ↓ -텍스트 유사도 계산 (Jaro-Winkler) - ↓ -임계값 기반 클러스터링 - ↓ -출력: Apple Inc. (대표) + [Apple Inc, apple inc, APPLE] (중복) -``` - -### 3. RAG 컨텍스트 추출 -``` -쿼리 엔티티: Apple Inc. - ↓ -2-hop 이웃 추출 - ↓ -관련 엔티티 그룹 - ↓ -Subgraph로 LLM 제공 -``` - -### 4. 데이터 품질 검증 -``` -- 순환 의존성 감지 (cycles) -- 강한 연결 성분 분석 (SCC) -- 연결성 메트릭 (degree, reachability) -- 그래프 모티프 분석 -``` - ---- - -## 성능 지표 - -| 작업 | 목표 | 달성 | -|------|------|------| -| 벡터 임베딩 | 10K 엔티티 < 5초 | ✅ 4초 | -| Neo4j 배치 쓰기 | 100K 노드/에지 < 30초 | ✅ 28초 | -| 2-hop 부분 그래프 추출 | < 200ms | ✅ 120-180ms | -| 경로 탐색 | max_length=5 < 500ms | ✅ 200-400ms | -| 중심성 계산 | top_n=100 < 1초 | ✅ 300-600ms | -| 커뮤니티 감지 | < 2초 | ✅ 1-1.5초 | - ---- - -## 코드 통계 - -| 파일 | 라인 수 | 클래스 | 메서드 | -|------|--------|--------|--------| -| entity_resolver.py | 324 | 2 | 10+ | -| rdf_converter.py | 309 | 1 | 8+ | -| subgraph_retriever.py | 385 | 1 | 3 | -| pattern_matcher.py | 362 | 3 | 7 | -| graph_analytics.py | 437 | 2 | 6 | -| neo4j_adapter.py | 587 | 2 | 15+ (확장) | - -**총 코드량**: ~2,000 라인 (테스트 제외) - ---- - -## 아키텍처 - -``` -┌─────────────────────────────────────────────┐ -│ Application Layer (API) │ -│ POST /graph/resolve │ -│ POST /graph/subgraph │ -│ POST /graph/patterns │ -│ POST /graph/analytics │ -└─────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────┐ -│ Graph Operations Layer │ -│ ┌─────────────────────────────────────┐ │ -│ │ SubgraphRetriever │ │ -│ │ PatternMatcher │ │ -│ │ GraphAnalytics │ │ -│ └─────────────────────────────────────┘ │ -└─────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────┐ -│ Entity Layer │ -│ ┌─────────────────────────────────────┐ │ -│ │ EntityResolver │ │ -│ │ RDFConverter │ │ -│ └─────────────────────────────────────┘ │ -└─────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────┐ -│ Neo4j Adapter (배치, 인덱스, 트랜잭션) │ -│ Cypher Query Engine │ -└─────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────┐ -│ Neo4j Database │ -│ Property Graph │ -└─────────────────────────────────────────────┘ -``` - ---- - -## 의존성 - -``` -neo4j>=5.0.0 # Neo4j async driver -sentence-transformers>=2.2.0 # all-MiniLM-L6-v2 모델 -numpy>=1.20.0 # 수치 계산 -scipy>=1.7.0 # 거리 계산 -textdistance>=4.6.0 # Jaro-Winkler -networkx>=3.0 # SCC 알고리즘 (선택) -``` - ---- - -## 다음 단계 (Phase 6+) - -### Phase 6: API 통합 -- REST 엔드포인트 구현 (Flask/FastAPI) -- GraphQL 지원 (선택) -- Rate limiting 및 캐싱 - -### Phase 7: LLM 통합 -- Entity Description 자동 생성 -- RAG 파이프라인 (context → LLM) -- Knowledge graph embedding - -### Phase 8: 고급 기능 -- Temporal graphs (버전 관리) -- Change tracking (감사 로그) -- Incremental updates -- Multi-project isolation - ---- - -## 사용 예시 - -### 엔티티 중복 감지 및 병합 -```python -from ont_platform.core.graph import EntityResolver - -resolver = EntityResolver() -await resolver.initialize_embedder() - -entities = [ - {"id": 1, "label": "Apple Inc.", "type": "Company"}, - {"id": 2, "label": "Apple Inc", "type": "Company"}, -] - -clusters = await resolver.detect_duplicates(entities) -# → EntityCluster(canonical_id=1, duplicates=[2], confidence=0.92) -``` - -### RAG 컨텍스트 추출 -```python -from ont_platform.core.graph import SubgraphRetriever - -retriever = SubgraphRetriever(adapter) - -context = await retriever.retrieve_neighborhood( - entity_id=1, - hops=2, - limit=500 -) -# → {nodes: [...], edges: [...], center_entity: {...}} -``` - -### 경로 탐색 -```python -from ont_platform.core.graph import PatternMatcher - -matcher = PatternMatcher(adapter) - -paths = await matcher.find_paths( - start_entity_id=1, - end_entity_id=5, - max_length=5 -) -# → [{path: [1, 2, 3, 5], length: 3, confidence: 0.87}, ...] -``` - -### 영향력 있는 엔티티 검색 -```python -from ont_platform.core.graph import GraphAnalytics - -analytics = GraphAnalytics(adapter) - -influential = await analytics.find_influential_entities(top_n=20) -# → [{entity_id: 1, label: "Apple", composite_score: 1.0}, ...] -``` - ---- - -## 결론 - -**Phase 5 GraphRAG는 완전히 구현되고 테스트되었습니다.** - -- ✅ 모든 핵심 기능 구현 (Phase 5.0-5.2) -- ✅ 포괄적인 테스트 커버리지 (37/37 테스트) -- ✅ 성능 목표 달성 -- ✅ 깔끔한 아키텍처 설계 -- ✅ 명확한 문서화 - -### 주요 성과 - -1. **RDF ↔ Property Graph 양방향 변환**: 온톨로지 메타데이터 유지 -2. **의미적 엔티티 중복 제거**: 벡터 + 텍스트 유사도 조합 -3. **RAG 컨텍스트 추출**: N-hop 이웃 및 유도 부분 그래프 -4. **복잡 패턴 분석**: 경로, 순환, SCC, 모티프 검출 -5. **그래프 분석**: 중심성, 커뮤니티, 영향력 분석 - -시스템은 대규모 지식 그래프 (10K+ 노드) 에서도 안정적으로 동작합니다. - ---- - -**작성일**: 2026-05-14 -**버전**: Phase 5.2 -**상태**: ✅ 완료 및 검증 diff --git a/PHASE_6_API_GUIDE.md b/PHASE_6_API_GUIDE.md deleted file mode 100644 index e39ac8f..0000000 --- a/PHASE_6_API_GUIDE.md +++ /dev/null @@ -1,678 +0,0 @@ -# Phase 6 GraphRAG API 가이드 - -## 개요 - -Phase 6는 Phase 5의 그래프 분석 기능을 REST API, GraphQL, RAG 파이프라인으로 노출합니다. - -**특징**: -- ✅ REST API 엔드포인트 (10개 그래프 작업) -- ✅ GraphQL 지원 (유연한 쿼리) -- ✅ RAG 파이프라인 (LLM 통합) -- ✅ 자동 API 문서 (Swagger/OpenAPI) - ---- - -## 빠른 시작 - -### 1. 서버 시작 - -```bash -python -m uvicorn ontology_platform.ont_platform.api.phase6_app:app --reload -``` - -기본 포트: `http://localhost:8000` - -### 2. API 문서 확인 - -``` -http://localhost:8000/docs # Swagger UI -http://localhost:8000/redoc # ReDoc -``` - -### 3. 헬스 체크 - -```bash -curl http://localhost:8000/health -``` - -응답: -```json -{ - "status": "ok", - "version": "0.6.0", - "neo4j": "connected" -} -``` - ---- - -## REST API 엔드포인트 - -### 엔티티 중복 해결 (Entity Resolution) - -#### `POST /api/v1/graph/resolve` - -의미적 중복 감지 및 병합 - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/graph/resolve \ - -H "Content-Type: application/json" \ - -d '{ - "entities": [ - {"id": 1, "label": "Apple Inc.", "type": "Company"}, - {"id": 2, "label": "Apple Inc", "type": "Company"}, - {"id": 3, "label": "Microsoft", "type": "Company"} - ], - "vector_threshold": 0.85, - "text_threshold": 0.88 - }' -``` - -**응답**: -```json -{ - "status": "success", - "clusters": [ - { - "cluster_id": "C_1_2", - "canonical_id": 1, - "duplicates": [2], - "confidence": 0.92, - "reason": "combined" - } - ], - "total_clusters": 1 -} -``` - ---- - -### 부분 그래프 추출 (Subgraph Retrieval) - -#### `GET /api/v1/graph/subgraph/neighborhood/{entity_id}` - -N-hop 이웃 추출 - -**요청**: -```bash -curl "http://localhost:8000/api/v1/graph/subgraph/neighborhood/1?hops=2&limit=500" -``` - -**응답**: -```json -{ - "status": "success", - "data": { - "center_entity": { - "id": 1, - "label": "Apple Inc.", - "type": "Company", - "confidence": 0.95 - }, - "nodes": [ - {"id": 1, "label": "Apple Inc.", "type": "Company", "confidence": 0.95}, - {"id": 5, "label": "iPhone", "type": "Product", "confidence": 0.92}, - {"id": 6, "label": "Steve Jobs", "type": "Person", "confidence": 0.88} - ], - "edges": [ - { - "source_id": 1, - "target_id": 5, - "predicate": "produces", - "confidence": 0.95 - } - ], - "node_count": 3, - "edge_count": 1 - } -} -``` - -#### `POST /api/v1/graph/subgraph/context` - -다중 엔티티 공통 컨텍스트 - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/graph/subgraph/context \ - -H "Content-Type: application/json" \ - -d '{ - "entity_ids": [1, 2, 3], - "context_hops": 2 - }' -``` - -**응답**: -```json -{ - "status": "success", - "data": { - "seed_entities": [...], - "common_neighbors": [...], - "nodes": [...], - "edges": [...], - "total_nodes": 50, - "total_edges": 120 - } -} -``` - ---- - -### 패턴 매칭 (Pattern Matching) - -#### `POST /api/v1/graph/patterns/paths` - -두 엔티티 사이의 모든 경로 찾기 - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/graph/patterns/paths \ - -H "Content-Type: application/json" \ - -d '{ - "start_id": 1, - "end_id": 5, - "max_length": 5 - }' -``` - -**응답**: -```json -{ - "status": "success", - "paths": [ - {"path": [1, 2, 3, 5], "length": 3, "confidence": 0.87}, - {"path": [1, 4, 5], "length": 2, "confidence": 0.91} - ], - "total_paths": 2 -} -``` - -#### `POST /api/v1/graph/patterns/cycles` - -순환 의존성 감지 - -```bash -curl -X POST http://localhost:8000/api/v1/graph/patterns/cycles \ - -H "Content-Type: application/json" \ - -d '{ - "min_length": 2, - "max_length": 5 - }' -``` - -#### `POST /api/v1/graph/patterns/motifs` - -그래프 모티프 검출 (삼각형, 체인, 별) - -```bash -curl -X POST http://localhost:8000/api/v1/graph/patterns/motifs \ - -H "Content-Type: application/json" \ - -d '{ - "motif_type": "triangle", - "limit": 100 - }' -``` - ---- - -### 그래프 분석 (Graph Analytics) - -#### `POST /api/v1/graph/analytics/centrality` - -중심성 계산 (degree, pagerank, betweenness, closeness) - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/graph/analytics/centrality \ - -H "Content-Type: application/json" \ - -d '{ - "centrality_type": "pagerank", - "top_n": 20 - }' -``` - -**응답**: -```json -{ - "status": "success", - "centrality_type": "pagerank", - "entities": [ - {"entity_id": 1, "label": "Apple", "centrality_score": 0.95, "rank": 1}, - {"entity_id": 5, "label": "iPhone", "centrality_score": 0.87, "rank": 2} - ], - "total_entities": 2 -} -``` - -#### `POST /api/v1/graph/analytics/communities` - -커뮤니티 감지 - -```bash -curl -X POST http://localhost:8000/api/v1/graph/analytics/communities \ - -H "Content-Type: application/json" \ - -d '{ - "algorithm": "louvain", - "min_size": 3 - }' -``` - -#### `GET /api/v1/graph/analytics/statistics` - -그래프 전체 통계 - -```bash -curl http://localhost:8000/api/v1/graph/analytics/statistics -``` - -**응답**: -```json -{ - "status": "success", - "statistics": { - "total_nodes": 1000, - "total_edges": 5000, - "avg_degree": 10.0, - "density": 0.01, - "diameter": 7, - "is_connected": true - } -} -``` - -#### `GET /api/v1/graph/analytics/influential` - -영향력 있는 엔티티 - -```bash -curl "http://localhost:8000/api/v1/graph/analytics/influential?top_n=20" -``` - ---- - -## RAG 파이프라인 - -### 컨텍스트 추출 - -#### `POST /api/v1/rag/context-extraction` - -지식 그래프에서 RAG 컨텍스트 추출 - -**요청 (엔티티 ID로)**: -```bash -curl -X POST http://localhost:8000/api/v1/rag/context-extraction \ - -H "Content-Type: application/json" \ - -d '{ - "entity_id": 1, - "hops": 2, - "max_entities": 100 - }' -``` - -**요청 (텍스트 검색으로)**: -```bash -curl -X POST http://localhost:8000/api/v1/rag/context-extraction \ - -H "Content-Type: application/json" \ - -d '{ - "query_text": "What is Apple?", - "hops": 2 - }' -``` - -**응답**: -```json -{ - "status": "success", - "query": "What is Apple?", - "context": { - "center_entity": {...}, - "nodes": [...], - "edges": [...], - "node_count": 50 - }, - "context_size": 50 -} -``` - -### RAG 쿼리 (LLM 통합) - -#### `POST /api/v1/rag/query` - -LLM 통합 RAG 쿼리 - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/rag/query \ - -H "Content-Type: application/json" \ - -d '{ - "query": "What products does Apple make?", - "context_hops": 2, - "use_graph_context": true - }' -``` - -**응답**: -```json -{ - "status": "success", - "query": "What products does Apple make?", - "relevant_entities": ["Apple Inc.", "iPhone", "iPad"], - "context_nodes": 45, - "llm_prompt": "You are a helpful assistant...\n\nKNOWLEDGE GRAPH CONTEXT:\n...", - "ready_for_llm": true, - "context": [...] -} -``` - -### LLM에 프롬프트 전달 - -RAG 응답에서 `llm_prompt`를 받으면, 이를 LLM 서비스로 전달: - -```python -import requests - -# Phase 6 RAG 서버에서 컨텍스트 획득 -rag_response = requests.post( - "http://localhost:8000/api/v1/rag/query", - json={"query": "What is Apple?"} -).json() - -# LLM 서비스 호출 (예: OpenAI) -llm_response = requests.post( - "https://api.openai.com/v1/chat/completions", - headers={"Authorization": "Bearer YOUR_API_KEY"}, - json={ - "model": "gpt-4", - "messages": [ - { - "role": "user", - "content": rag_response["llm_prompt"] - } - ], - "temperature": 0.7, - "max_tokens": 500 - } -).json() - -print(llm_response["choices"][0]["message"]["content"]) -``` - ---- - -## GraphQL 엔드포인트 - -### `POST /graphql` - -유연한 GraphQL 쿼리 지원 - -**엔티티 조회**: -```graphql -{ - entity(id: 1) { - id - label - type - neighbors(hops: 2) { - id - label - distance - } - } -} -``` - -**요청**: -```bash -curl -X POST http://localhost:8000/graphql \ - -H "Content-Type: application/json" \ - -d '{ - "query": "{ entity(id: 1) { id label type } }" - }' -``` - -**응답**: -```json -{ - "data": { - "entity": { - "id": 1, - "label": "Apple Inc.", - "type": "Company" - } - } -} -``` - ---- - -## 에러 처리 - -### 표준 에러 응답 - -```json -{ - "detail": "Entity not found", - "status_code": 404 -} -``` - -### 검증 에러 - -```json -{ - "detail": [ - { - "loc": ["query", "hops"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le" - } - ] -} -``` - ---- - -## 예제 워크플로우 - -### 1단계: 엔티티 중복 해결 - -```bash -# 중복 엔티티 감지 -POST /api/v1/graph/resolve -Body: {"entities": [{"id": 1, "label": "Apple Inc."}, {"id": 2, "label": "Apple"}]} - -Response: -{ - "status": "success", - "clusters": [{"canonical_id": 1, "duplicates": [2], "confidence": 0.92}] -} -``` - -### 2단계: RAG 컨텍스트 추출 - -```bash -# 대표 엔티티 주변 컨텍스트 추출 -GET /api/v1/graph/subgraph/neighborhood/1?hops=2 - -Response: -{ - "status": "success", - "data": {"nodes": [...], "edges": [...], "node_count": 50} -} -``` - -### 3단계: LLM 쿼리 - -```bash -# RAG 쿼리 (LLM용 프롬프트 자동 생성) -POST /api/v1/rag/query -Body: {"query": "What does Apple do?"} - -Response: -{ - "status": "success", - "llm_prompt": "You are a helpful assistant...", - "ready_for_llm": true -} -``` - -### 4단계: LLM 응답 - -```python -# LLM 서비스로 프롬프트 전달 -response = llm_service(rag_response["llm_prompt"]) -print(response) # LLM 답변 -``` - ---- - -## 성능 특성 - -| 엔드포인트 | 데이터셋 | 응답 시간 | -|-----------|---------|---------| -| `/graph/resolve` | 1K 엔티티 | < 500ms | -| `/graph/subgraph/neighborhood` | 2-hop, 10K 노드 | < 200ms | -| `/graph/patterns/paths` | max_length=5 | < 300ms | -| `/graph/analytics/centrality` | top_n=100 | < 600ms | -| `/graph/analytics/communities` | 1K 노드 | < 1초 | -| `/rag/query` | 벡터 검색 + 컨텍스트 | < 1초 | - ---- - -## 설정 - -### 환경 변수 - -```bash -# Neo4j 연결 -NEO4J_URI=bolt://localhost:7687 -NEO4J_USER=neo4j -NEO4J_PASSWORD=ontology123 - -# API 설정 -API_HOST=0.0.0.0 -API_PORT=8000 -API_RELOAD=true # 개발 모드 -``` - -### 신뢰도 임계값 - -```python -# Entity Resolver -VECTOR_THRESHOLD=0.85 # 벡터 유사도 -TEXT_THRESHOLD=0.88 # 텍스트 유사도 - -# Subgraph Retriever -MIN_CONFIDENCE=0.0 # 최소 신뢰도 -``` - ---- - -## 보안 - -### 권장사항 - -1. **인증**: 프로덕션에서 JWT/OAuth 추가 -2. **Rate Limiting**: API 요청 제한 -3. **HTTPS**: TLS 암호화 -4. **입력 검증**: 모든 쿼리 검증 - -### 예: FastAPI 보안 - -```python -from fastapi.security import HTTPBearer, HTTPAuthCredential - -security = HTTPBearer() - -@app.get("/api/v1/graph/resolve") -async def resolve_entities(credentials: HTTPAuthCredential = Depends(security)): - # JWT 검증 - token = credentials.credentials - # ... -``` - ---- - -## 배포 - -### Docker - -```dockerfile -FROM python:3.10 -WORKDIR /app -COPY requirements.txt . -RUN pip install -r requirements.txt -COPY . . -CMD ["uvicorn", "ontology_platform.ont_platform.api.phase6_app:app", "--host", "0.0.0.0"] -``` - -### Kubernetes - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ontology-api -spec: - replicas: 3 - selector: - matchLabels: - app: ontology-api - template: - metadata: - labels: - app: ontology-api - spec: - containers: - - name: api - image: ontology-api:0.6.0 - ports: - - containerPort: 8000 -``` - ---- - -## 문제 해결 - -### Neo4j 연결 실패 - -```bash -# Neo4j 상태 확인 -http://localhost:7687 - -# 연결 테스트 -curl http://localhost:8000/health -``` - -### 높은 응답 시간 - -- 쿼리 최적화: Cypher 인덱스 확인 -- 배치 크기 조정 -- 최대 깊이/한계 감소 - -### 메모리 부족 - -- Neo4j 힙 크기 증가 -- 배치 크기 감소 -- 캐싱 활성화 - ---- - -## 다음 단계 - -### Phase 7: LLM 엔드투엔드 통합 -- FastAPI 미들웨어로 LLM 직접 호출 -- 스트리밍 응답 -- 응답 캐싱 - -### Phase 8: 고급 기능 -- 멀티 테넌트 지원 -- 실시간 그래프 업데이트 -- 버전 관리 - ---- - -**API 버전**: 0.6.0 -**마지막 업데이트**: 2026-05-14 diff --git a/PHASE_7_IMPLEMENTATION_SUMMARY.md b/PHASE_7_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 225f90a..0000000 --- a/PHASE_7_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,617 +0,0 @@ -# Phase 7 LLM 엔드투엔드 통합 - 구현 요약 - -## 📋 개요 - -Phase 7는 **온톨로지 시스템 구축 플랫폼**의 마지막 핵심 단계입니다. Phase 6의 GraphRAG 파이프라인을 확장하여 **LLM(대언어모델)을 직접 통합**하고, **스트리밍 응답**, **Redis 캐싱**, **다중 LLM 프로바이더 지원**을 추가합니다. - ---- - -## 🎯 Phase 7의 목표 - -| 목표 | 달성 | 설명 | -|------|------|------| -| LLM 프로바이더 추상화 | ✅ | OpenAI, Anthropic, Local 지원 | -| 스트리밍 응답 (SSE) | ✅ | 실시간 토큰 전달 | -| Redis 캐싱 | ✅ | 1시간 TTL, 자동 무효화 | -| RAG + LLM 통합 | ✅ | 그래프 컨텍스트 자동 추출 | -| 메타데이터 추적 | ✅ | 레이턴시, 토큰 수, 모델 정보 | -| 다중 엔드포인트 | ✅ | 기본/스트리밍/메타데이터 조회 | - ---- - -## 📁 생성된 파일 - -### 1. 핵심 구현 파일 - -#### `ontology_platform/ont_platform/api/phase7_app.py` -**FastAPI 애플리케이션 (포트 8001)** - -``` -구성: -├── 모듈 임포트 -│ ├── LLMManager, LLMConfig, LLMProvider -│ ├── Phase 6 컴포넌트 (EntityResolver, SubgraphRetriever, ...) -│ └── Redis async client -│ -├── Request/Response 모델 -│ ├── AskRequest (기본 쿼리) -│ ├── AskResponse (응답 + 메타데이터) -│ ├── StreamingAskRequest -│ └── RAGMetadata -│ -├── 전역 인스턴스 관리 -│ ├── _neo4j_adapter -│ ├── _llm_manager -│ ├── _redis_client -│ └── 초기화 함수들 -│ -├── 캐싱 유틸리티 -│ ├── _generate_cache_key() - SHA256 기반 -│ ├── _get_cached_response() - Redis 조회 -│ └── _cache_response() - Redis 저장 (TTL) -│ -├── RAG 컨텍스트 추출 -│ ├── extract_rag_context() - 그래프에서 관련 엔티티 검색 -│ └── _build_rag_prompt_for_llm() - 구조화된 프롬프트 생성 -│ -├── LLM 엔드포인트 (llm_router) -│ ├── POST /api/v1/llm/ask (캐싱 포함) -│ ├── POST /api/v1/llm/ask/stream (SSE 스트리밍) -│ ├── POST /api/v1/llm/ask/metadata (메타만) -│ ├── POST /api/v1/llm/configure (설정 변경) -│ └── GET /api/v1/llm/info (정보 조회) -│ -├── 캐시 관리 -│ ├── DELETE /api/v1/llm/cache (전체 삭제) -│ └── GET /api/v1/llm/cache/info (통계) -│ -└── 헬스/정보 엔드포인트 - ├── GET /health (상태 확인) - └── GET /info (플랫폼 정보) -``` - -**파일 크기**: 약 600줄 -**의존성**: redis, openai, anthropic, httpx - -#### `ontology_platform/ont_platform/llm/__init__.py` -**LLM 모듈 내보내기** - -```python -from ont_platform.llm.llm_integration import ( - LLMProvider, - LLMConfig, - BaseLLMClient, - OpenAIClient, - AnthropicClient, - LocalLLMClient, - LLMManager, -) -``` - -### 2. 테스트 파일 - -#### `tests/test_phase7_llm_integration.py` -**Phase 7 종합 테스트 (약 400줄)** - -``` -테스트 조직: -├── Fixtures (설정) -│ ├── openai_config -│ ├── anthropic_config -│ └── local_config -│ -├── TestLLMConfig -│ ├── test_openai_config_creation() -│ ├── test_anthropic_config_creation() -│ ├── test_local_config_creation() -│ ├── test_config_temperature_bounds() -│ └── ... -│ -├── TestLLMManager -│ ├── test_openai_manager_creation() -│ ├── test_anthropic_manager_creation() -│ ├── test_local_manager_creation() -│ └── test_manager_config_update() -│ -├── TestOpenAIClient -│ ├── test_openai_generate_non_streaming() -│ └── test_openai_generate_streaming() -│ -├── TestStreamingResponses -│ ├── test_stream_format() - SSE 포맷 검증 -│ ├── test_metadata_streaming() -│ └── test_completion_signal_streaming() -│ -├── TestCaching -│ ├── test_cache_key_generation() - 결정론적 키 -│ ├── test_cache_key_uniqueness() - 고유성 -│ ├── test_cache_hit_detection() -│ └── test_response_serialization() -│ -├── TestRAGPipeline -│ ├── test_rag_prompt_structure() -│ ├── test_rag_context_formatting() -│ └── test_rag_metadata_inclusion() -│ -├── TestErrorHandling -│ ├── test_invalid_provider() -│ ├── test_missing_api_key_openai() -│ ├── test_empty_query_handling() -│ └── test_very_long_query_handling() -│ -├── TestPhase7Integration -│ ├── test_rag_to_llm_workflow() -│ ├── test_cache_to_llm_selection() -│ └── test_streaming_to_cache_flow() -│ -└── TestPerformance - ├── test_cache_lookup_speed() (< 1ms) - └── test_prompt_building_speed() (< 10ms) -``` - -**테스트 케이스**: 30개 이상 -**커버리지**: LLM 통합의 주요 경로 - -### 3. 문서 파일 - -#### `PHASE_7_LLM_GUIDE.md` -**Phase 7 완전 가이드 (약 600줄)** - -``` -내용: -├── 개요 (특징, 목표) -├── 빠른 시작 (서버 시작, 헬스 체크) -├── REST API 엔드포인트 (자세한 설명) -│ ├── /api/v1/llm/ask (기본 쿼리 + 캐싱) -│ ├── /api/v1/llm/ask/stream (스트리밍) -│ ├── /api/v1/llm/ask/metadata (메타만) -│ ├── /api/v1/llm/configure (설정) -│ └── /api/v1/llm/info (정보) -├── 캐싱 관리 (/cache, /cache/info) -├── 설정 (환경 변수) -├── 사용 예제 -│ ├── 기본 질문응답 (Python) -│ ├── 스트리밍 응답 (Python) -│ ├── LLM 설정 변경 (curl) -│ └── RAG + LLM 파이프라인 -├── 다중 LLM 프로바이더 -│ ├── OpenAI (gpt-4) -│ ├── Anthropic (claude-3) -│ └── Local (llama2, mistral) -├── 성능 최적화 -│ ├── 캐싱 활용 (30배 빠름) -│ ├── 스트리밍 (UI 반응성) -│ └── 온도 조정 -├── 성능 특성 (응답 시간 표) -├── 배포 (Docker, K8s) -├── 문제 해결 -└── 다음 단계 (Phase 8) -``` - -#### `PHASE_7_IMPLEMENTATION_SUMMARY.md` (이 파일) -**구현 세부 사항 및 기술 스택** - -### 4. requirements.txt 업데이트 -**신규 의존성 추가**: -``` -redis>=5.0 -openai>=1.0 -anthropic>=0.25 -httpx>=0.25 -sentence-transformers>=2.2 -numpy>=1.20 -python-multipart>=0.0.6 -``` - ---- - -## 🏗️ 아키텍처 - -### 전체 흐름 - -``` -클라이언트 - ↓ -┌─────────────────────────────────────┐ -│ FastAPI (phase7_app.py) │ -│ ┌──────────────────────────────┐ │ -│ │ /api/v1/llm/ask │ │ -│ │ /api/v1/llm/ask/stream │ │ -│ │ /api/v1/llm/configure │ │ -│ └──────────────────────────────┘ │ -└─────────────────────────────────────┘ - ↓ ↓ ↓ -[Redis 캐시] [Neo4j 그래프] [LLM API] - ↓ ↓ ↓ - TTL=1h [RAG 컨텍스트] [응답 생성] - ↓ ↓ - [메타데이터] [토큰 스트림] -``` - -### 컴포넌트 상호작용 - -``` -1. 사용자 쿼리 입력 - │ - ├→ Redis 캐시 확인 (cache_key: SHA256 해시) - │ ├─ Hit → 즉시 반환 (50-100ms) - │ └─ Miss → 계속 진행 - │ - ├→ RAG 컨텍스트 추출 - │ ├─ Neo4j 그래프 조회 - │ ├─ 관련 엔티티 검색 - │ └─ 메타데이터 수집 (추출 시간 등) - │ - ├→ 프롬프트 생성 - │ ├─ 구조화된 시스템 프롬프트 - │ ├─ 그래프 컨텍스트 포함 - │ └─ 사용자 질문 추가 - │ - ├→ LLM 호출 - │ ├─ 선택된 프로바이더 (OpenAI/Anthropic/Local) - │ ├─ 응답 생성 - │ └─ 토큰 수 계산 - │ - └→ 결과 처리 - ├─ Redis 캐시 저장 (1시간 TTL) - ├─ 메타데이터 추가 (레이턴시, 모델 등) - └─ 응답 반환 - ├─ 기본 API: JSON - └─ 스트리밍 API: SSE 이벤트 -``` - ---- - -## 🔑 핵심 기능 - -### 1. 다중 LLM 프로바이더 - -**LLMManager 추상화**: -```python -manager = LLMManager(config) - -# 프로바이더별 처리 -├─ OpenAI: openai.AsyncOpenAI -├─ Anthropic: anthropic.AsyncAnthropic -└─ Local: httpx.AsyncClient → /v1/completions - -# 동일한 인터페이스 -await manager.generate(prompt) # 단일 응답 -async for token in manager.generate_stream(prompt): # 스트림 -``` - -**지원 모델**: -- OpenAI: gpt-4, gpt-3.5-turbo, gpt-4-turbo -- Anthropic: claude-3-opus, claude-3-sonnet, claude-2 -- Local: llama2, mistral, neural-chat, etc. - -### 2. 응답 캐싱 (Redis) - -**캐시 전략**: -``` -Cache Key: SHA256(query + context_hops)[:16] -Format: "phase7:rag:{hash}" -TTL: 1시간 (설정 가능) - -저장 데이터: -{ - "query": "...", - "answer": "...", - "context_size": N, - "relevant_entities": [...], - "latency_ms": T, - "model": "gpt-4", - "provider": "openai" -} -``` - -**성능 개선**: -- 캐시 미스: 1-3초 (RAG + LLM) -- 캐시 히트: 50-100ms (30배 빠름) - -### 3. 스트리밍 응답 (SSE) - -**Server-Sent Events 포맷**: -``` -data: {"type": "metadata", "context_nodes": 50, ...} - -data: {"type": "token", "content": "토큰", "token_index": 0} -data: {"type": "token", "content": "1", "token_index": 1} -data: {"type": "token", "content": "입니다", "token_index": 2} - -data: {"type": "complete", "total_tokens": 156, ...} -``` - -**클라이언트 처리**: -- JavaScript: EventSource API -- Python: requests stream + JSON parsing -- cURL: 실시간 이벤트 수신 - -### 4. RAG + LLM 통합 - -**파이프라인**: -``` -1. 질문 입력 - ↓ -2. 지식 그래프 검색 (Neo4j) - → 관련 엔티티 추출 - → 부분 그래프 추출 - ↓ -3. 컨텍스트 생성 - → 엔티티 리스트 - → 관계 정보 - → 메타데이터 - ↓ -4. 프롬프트 생성 - → 시스템 프롬프트 (지식 그래프 기반) - → 컨텍스트 섹션 - → 사용자 질문 - ↓ -5. LLM 호출 - → 선택된 모델로 생성 - ↓ -6. 답변 반환 - → 메타데이터 포함 - → 캐시 저장 -``` - ---- - -## 📊 성능 메트릭 - -### 응답 시간 - -| 시나리오 | 시간 | 설명 | -|---------|------|------| -| 캐시 히트 | 50-100ms | Redis 조회 | -| RAG만 추출 | 100-300ms | LLM 호출 없음 | -| LLM 첫 응답 | 500-800ms | 스트리밍 시 첫 토큰 | -| 전체 응답 (캐시 미스) | 1-3초 | RAG + LLM | -| 스트리밍 완료 | 3-5초 | 모든 토큰 전달 | - -### 리소스 사용 - -| 리소스 | 사용 | 메모 | -|-------|------|------| -| Redis 메모리 | ~125MB | 300+ 캐시 항목 | -| Neo4j 쿼리 | 2-3 쿼리/요청 | 부분 그래프 추출 | -| LLM 토큰 | 50-500 토큰 | 질문/답변 크기 | -| 동시 요청 | 10+ | FastAPI async | - ---- - -## 🧪 테스트 커버리지 - -### 테스트 통계 - -``` -테스트 파일: test_phase7_llm_integration.py -총 테스트: 30+개 -테스트 클래스: - ├─ TestLLMConfig (4개) - ├─ TestLLMManager (4개) - ├─ TestOpenAIClient (2개) - ├─ TestStreamingResponses (3개) - ├─ TestCaching (4개) - ├─ TestRAGPipeline (3개) - ├─ TestErrorHandling (4개) - ├─ TestPhase7Integration (3개) - └─ TestPerformance (2개) - -주요 테스트 항목: - ✓ LLM 프로바이더 생성 (OpenAI, Anthropic, Local) - ✓ 스트리밍 응답 (SSE 포맷, 메타데이터, 완료 신호) - ✓ 캐시 키 생성 (결정론적, 고유성) - ✓ 응답 캐싱 (직렬화, 검색) - ✓ RAG 파이프라인 (컨텍스트, 프롬프트) - ✓ 에러 처리 (유효하지 않은 입력, API 실패) - ✓ 성능 (캐시 < 1ms, 프롬프트 < 10ms) -``` - ---- - -## 📚 사용 패턴 - -### 패턴 1: 기본 질문응답 (캐싱) - -```bash -curl -X POST http://localhost:8000/api/v1/llm/ask \ - -H "Content-Type: application/json" \ - -d '{ - "query": "Apple의 제품은?", - "use_cache": true - }' -``` - -**응답**: ~1-3초 (첫 요청), ~50-100ms (이후) - -### 패턴 2: 실시간 스트리밍 - -```bash -curl -X POST http://localhost:8000/api/v1/llm/ask/stream \ - -H "Content-Type: application/json" \ - -d '{"query": "..."}' -``` - -**응답**: 실시간 토큰 스트림 (SSE) - -### 패턴 3: LLM 설정 변경 - -```bash -curl "http://localhost:8000/api/v1/llm/configure?provider=anthropic&model=claude-3-opus" -``` - -**응답**: 즉시 적용 (< 50ms) - -### 패턴 4: RAG 메타데이터만 - -```bash -curl -X POST http://localhost:8000/api/v1/llm/ask/metadata \ - -H "Content-Type: application/json" \ - -d '{"query": "..."}' -``` - -**응답**: ~100-300ms (LLM 호출 없음) - ---- - -## 🔌 API 요약 - -| 엔드포인트 | 메서드 | 목적 | 응답 시간 | -|-----------|--------|------|---------| -| `/api/v1/llm/ask` | POST | 기본 쿼리 (캐싱) | 50ms-3초 | -| `/api/v1/llm/ask/stream` | POST | 실시간 스트림 | 3-5초 | -| `/api/v1/llm/ask/metadata` | POST | RAG 메타만 | 100-300ms | -| `/api/v1/llm/configure` | POST | 설정 변경 | < 50ms | -| `/api/v1/llm/info` | GET | 정보 조회 | < 50ms | -| `/api/v1/llm/cache` | DELETE | 캐시 삭제 | < 100ms | -| `/api/v1/llm/cache/info` | GET | 캐시 통계 | < 50ms | -| `/health` | GET | 헬스 체크 | < 50ms | -| `/info` | GET | 플랫폼 정보 | < 100ms | - ---- - -## 🚀 다음 단계 (Phase 8) - -### Phase 8 계획 - -``` -목표: 엔터프라이즈급 플랫폼 -├─ 멀티테넌트 -│ ├─ 조직별 격리 -│ ├─ API 키 관리 -│ └─ 권한 제어 -│ -├─ 실시간 그래프 업데이트 -│ ├─ WebSocket 지원 -│ ├─ 실시간 데이터 푸시 -│ └─ 동기화 -│ -├─ 변경 이력 추적 -│ ├─ 감사 로그 -│ ├─ 버전 관리 -│ └─ 롤백 지원 -│ -└─ 고급 분석 - ├─ 사용자별 통계 - ├─ 비용 추적 - └─ 성능 모니터링 -``` - ---- - -## 📋 검증 체크리스트 - -``` -Phase 7 구현: -✅ LLM 프로바이더 추상화 (openai, anthropic, local) -✅ 스트리밍 응답 (SSE 기반) -✅ Redis 캐싱 (TTL, 결정론적 키) -✅ RAG 컨텍스트 추출 및 프롬프트 생성 -✅ 메타데이터 추적 (레이턴시, 토큰, 모델) -✅ 다중 엔드포인트 (ask, stream, metadata, config) -✅ 캐시 관리 (조회, 삭제) -✅ 에러 처리 및 예외 관리 -✅ 성능 최적화 (캐시 < 100ms) -✅ 종합 테스트 (30+ 테스트 케이스) -✅ 완전 문서화 (PHASE_7_LLM_GUIDE.md) -✅ 배포 가이드 (Docker, K8s) - -통합: -✅ Phase 6과의 호환성 -✅ Neo4j 그래프 접근 -✅ 메타데이터 수집 -✅ 에러 로깅 -``` - ---- - -## 📝 파일 구조 - -``` -온톨로지 플랫폼/ -├─ ontology_platform/ -│ └─ ont_platform/ -│ ├─ api/ -│ │ ├─ phase0_app.py (원본) -│ │ ├─ phase6_app.py (GraphRAG) -│ │ └─ phase7_app.py ✨ NEW -│ ├─ llm/ -│ │ ├─ llm_integration.py (이전 작업) -│ │ └─ __init__.py ✨ NEW -│ └─ core/ -│ └─ graph/ -│ ├─ entity_resolver.py -│ ├─ subgraph_retriever.py -│ ├─ pattern_matcher.py -│ ├─ graph_analytics.py -│ └─ neo4j_adapter.py -│ -├─ tests/ -│ ├─ test_entity_resolver.py -│ ├─ test_subgraph_retriever.py -│ ├─ test_pattern_matcher.py -│ ├─ test_graph_analytics.py -│ └─ test_phase7_llm_integration.py ✨ NEW -│ -├─ docs/ -│ ├─ PHASE_5_SUMMARY.md -│ ├─ PHASE_6_API_GUIDE.md -│ ├─ PHASE_7_LLM_GUIDE.md ✨ NEW -│ └─ PHASE_7_IMPLEMENTATION_SUMMARY.md ✨ NEW -│ -├─ requirements.txt ✨ UPDATED -├─ README.md -├─ README_KO.md -└─ ONTOLOGY_PLATFORM_OVERVIEW.md -``` - ---- - -## 🎓 학습 포인트 - -### 구현된 주요 개념 - -1. **LLM 프로바이더 추상화** - - 다형성을 통한 유연한 프로바이더 선택 - - 동일한 인터페이스로 여러 API 지원 - -2. **캐싱 전략** - - 결정론적 캐시 키 생성 (SHA256) - - TTL 기반 자동 무효화 - - 성능 향상 (30배) - -3. **스트리밍 응답** - - Server-Sent Events (SSE) 프로토콜 - - 비동기 생성기 (AsyncGenerator) - - 실시간 UI 업데이트 - -4. **RAG 파이프라인** - - 지식 그래프와 LLM 통합 - - 구조화된 컨텍스트 생성 - - 프롬프트 엔지니어링 - -5. **메타데이터 추적** - - 성능 모니터링 - - 감사 로깅 - - 비용 분석 - ---- - -## 📞 지원 - -### 문제 해결 - -- **Redis 연결 실패**: Redis 서버 확인 (`redis-cli ping`) -- **LLM API 오류**: API 키 확인 (`echo $OPENAI_API_KEY`) -- **높은 응답 시간**: 캐싱 활성화 및 토큰 제한 감소 - -### 문서 - -- **API 가이드**: [PHASE_7_LLM_GUIDE.md](./PHASE_7_LLM_GUIDE.md) -- **플랫폼 개요**: [ONTOLOGY_PLATFORM_OVERVIEW.md](./ONTOLOGY_PLATFORM_OVERVIEW.md) -- **테스트**: [tests/test_phase7_llm_integration.py](./tests/test_phase7_llm_integration.py) - ---- - -**Phase 7 완성! 이제 지식 그래프 기반 지능형 질문응답 시스템이 준비되었습니다.** 🎉 diff --git a/PHASE_7_LLM_GUIDE.md b/PHASE_7_LLM_GUIDE.md deleted file mode 100644 index e204a18..0000000 --- a/PHASE_7_LLM_GUIDE.md +++ /dev/null @@ -1,645 +0,0 @@ -# Phase 7 LLM 엔드투엔드 통합 가이드 - -## 개요 - -Phase 7는 Phase 6의 GraphRAG 파이프라인을 확장하여 **LLM(대언어모델)을 직접 통합**합니다. - -**특징**: -- ✅ 다중 LLM 프로바이더 지원 (OpenAI, Anthropic, Local) -- ✅ 실시간 스트리밍 응답 (Server-Sent Events) -- ✅ Redis 기반 응답 캐싱 (TTL 설정 가능) -- ✅ RAG 컨텍스트 자동 추출 + 프롬프트 생성 -- ✅ 메타데이터 추적 (레이턴시, 토큰 수, 모델 정보) - ---- - -## 빠른 시작 - -### 1. 서버 시작 - -```bash -# Phase 7 앱 시작 (포트 8001) -python -m uvicorn ontology_platform.ont_platform.api.phase7_app:app --reload --port 8001 - -# 또는 기본 포트 8000 -python -m uvicorn ontology_platform.ont_platform.api.phase7_app:app --reload -``` - -### 2. 헬스 체크 - -```bash -curl http://localhost:8000/health -``` - -응답: -```json -{ - "status": "healthy", - "version": "0.7.0", - "neo4j": "connected", - "redis": "available", - "llm_provider": "openai", - "timestamp": "2026-05-14T10:30:45.123456" -} -``` - -### 3. LLM 설정 - -```bash -# 현재 LLM 설정 확인 -curl http://localhost:8000/api/v1/llm/info - -# LLM 변경 (OpenAI → Anthropic) -curl "http://localhost:8000/api/v1/llm/configure?provider=anthropic&model=claude-3-opus&api_key=sk-ant-xxx" -``` - ---- - -## REST API 엔드포인트 - -### 1. 기본 LLM 쿼리 (캐싱 포함) - -#### `POST /api/v1/llm/ask` - -LLM에 질문하고 **캐시된 응답**을 반환합니다. - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/llm/ask \ - -H "Content-Type: application/json" \ - -d '{ - "query": "Apple의 주요 제품은 무엇인가?", - "context_hops": 2, - "use_cache": true, - "temperature": 0.7, - "max_tokens": 500 - }' -``` - -**요청 파라미터**: -- `query` (필수): 사용자 질문 -- `context_hops` (선택): 그래프 컨텍스트 깊이 (기본: 2) -- `use_cache` (선택): 캐시 사용 여부 (기본: true) -- `temperature` (선택): 응답 다양성 (0.0~2.0, 기본: 0.7) -- `max_tokens` (선택): 최대 토큰 수 (기본: 500) - -**응답**: -```json -{ - "query": "Apple의 주요 제품은 무엇인가?", - "answer": "Apple의 주요 제품으로는 iPhone, iPad, Mac, Apple Watch 등이 있습니다. iPhone은 Apple의 핵심 수익원이며...", - "context_size": 45, - "relevant_entities": ["Apple Inc.", "iPhone", "iPad", "Mac", "Steve Jobs"], - "latency_ms": 245.5, - "cached": false, - "model": "gpt-4", - "provider": "openai" -} -``` - -**응답 필드**: -- `query`: 입력 질문 -- `answer`: LLM의 최종 답변 -- `context_size`: 사용된 그래프 노드 수 -- `relevant_entities`: 검색된 관련 엔티티 -- `latency_ms`: 전체 응답 시간 (밀리초) -- `cached`: 캐시된 응답 여부 (true면 실제 레이턴시는 훨씬 적음) -- `model`: 사용된 모델 -- `provider`: LLM 프로바이더 - -**성능**: -- 캐시 미스: 1-3초 (RAG 추출 + LLM 생성) -- 캐시 히트: 50-100ms (Redis 조회) - ---- - -### 2. 스트리밍 응답 (실시간 토큰) - -#### `POST /api/v1/llm/ask/stream` - -LLM 응답을 **실시간 스트리밍**합니다 (Server-Sent Events). - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/llm/ask/stream \ - -H "Content-Type: application/json" \ - -d '{ - "query": "온톨로지란 무엇인가?", - "context_hops": 2, - "temperature": 0.7 - }' -``` - -**응답 (SSE 스트림)**: -``` -data: {"type": "metadata", "query": "온톨로지란 무엇인가?", "context_nodes": 50, "relevant_entities": ["Ontology", "Knowledge Graph"], "extraction_time_ms": 120.5} - -data: {"type": "token", "content": "온톨로지는", "token_index": 0} - -data: {"type": "token", "content": " ", "token_index": 1} - -data: {"type": "token", "content": "어떤", "token_index": 2} - -... - -data: {"type": "complete", "total_tokens": 156, "timestamp": "2026-05-14T10:35:20.123456"} -``` - -**스트림 포맷**: -- 각 줄은 SSE 이벤트: `data: {JSON}\n\n` -- `metadata`: 초기 메타데이터 (컨텍스트, 엔티티) -- `token`: 각 생성된 토큰 -- `complete`: 완료 신호 - -**클라이언트 예제 (JavaScript)**: -```javascript -const eventSource = new EventSource( - 'http://localhost:8000/api/v1/llm/ask/stream', - { method: 'POST', body: JSON.stringify({query: "..."})} -); - -eventSource.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - - if (data.type === 'metadata') { - console.log('Context:', data.context_nodes, 'nodes'); - } else if (data.type === 'token') { - process.stdout.write(data.content); // 실시간 출력 - } else if (data.type === 'complete') { - console.log(`\n완료 (${data.total_tokens} 토큰)`); - eventSource.close(); - } -}); -``` - -**성능**: 3-5초 (토큰 실시간 전달, 캐싱 미적용) - ---- - -### 3. RAG 메타데이터만 (LLM 호출 없음) - -#### `POST /api/v1/llm/ask/metadata` - -LLM 호출 **없이** RAG 컨텍스트 정보만 반환합니다. - -**요청**: -```bash -curl -X POST http://localhost:8000/api/v1/llm/ask/metadata \ - -H "Content-Type: application/json" \ - -d '{ - "query": "Apple과 관련된 정보", - "context_hops": 2 - }' -``` - -**응답**: -```json -{ - "query": "Apple과 관련된 정보", - "context_nodes": 45, - "relevant_entities": ["Apple Inc.", "iPhone", "iPad", "Steve Jobs"], - "extraction_time_ms": 145.2, - "llm_provider": "openai", - "llm_model": "gpt-4" -} -``` - -**성능**: 100-300ms (RAG 추출만, LLM 호출 없음) - ---- - -## LLM 설정 - -### LLM 설정 변경 - -#### `POST /api/v1/llm/configure` - -LLM 프로바이더, 모델, 온도 등을 변경합니다. - -**요청 (OpenAI → Anthropic 변경)**: -```bash -curl "http://localhost:8000/api/v1/llm/configure?provider=anthropic&model=claude-3-opus&api_key=sk-ant-xxx&temperature=0.5&max_tokens=1000" -``` - -**요청 파라미터**: -- `provider` (필수): `openai`, `anthropic`, `local` -- `model` (필수): 모델 이름 - - OpenAI: `gpt-4`, `gpt-3.5-turbo` - - Anthropic: `claude-3-opus`, `claude-3-sonnet`, `claude-2` - - Local: `llama2`, `mistral`, etc. -- `api_key` (선택): API 키 (환경 변수로도 설정 가능) -- `temperature` (선택): 0.0~2.0 (기본: 0.7) -- `max_tokens` (선택): 토큰 제한 (기본: 500) - -**응답**: -```json -{ - "status": "configured", - "provider": "anthropic", - "model": "claude-3-opus", - "temperature": 0.5, - "max_tokens": 1000 -} -``` - -### LLM 정보 조회 - -#### `GET /api/v1/llm/info` - -현재 LLM 설정을 조회합니다. - -**응답**: -```json -{ - "llm_provider": "openai", - "llm_model": "gpt-4", - "temperature": 0.7, - "max_tokens": 500, - "redis_available": true, - "timestamp": "2026-05-14T10:40:15.123456" -} -``` - ---- - -## 캐싱 관리 - -### 캐시 정보 - -#### `GET /api/v1/llm/cache/info` - -Redis 캐시 통계를 조회합니다. - -**응답**: -```json -{ - "redis_available": true, - "used_memory_mb": 125.5, - "cache_keys": 342, - "redis_version": "7.0.0" -} -``` - -### 캐시 삭제 - -#### `DELETE /api/v1/llm/cache` - -모든 RAG 캐시를 삭제합니다. - -**요청**: -```bash -curl -X DELETE http://localhost:8000/api/v1/llm/cache -``` - -**응답**: -```json -{ - "status": "success", - "deleted_keys": "342" -} -``` - ---- - -## 설정 (환경 변수) - -### LLM 프로바이더 API 키 - -```bash -# OpenAI -export OPENAI_API_KEY=sk-proj-xxx - -# Anthropic -export ANTHROPIC_API_KEY=sk-ant-xxx - -# Local LLM (LM Studio) -export LM_STUDIO_URL=http://localhost:1234/v1 -``` - -### Neo4j 연결 - -```bash -export NEO4J_URI=bolt://localhost:7687 -export NEO4J_USER=neo4j -export NEO4J_PASSWORD=ontology123 -``` - -### Redis 연결 - -```bash -export REDIS_URL=redis://localhost:6379 -``` - ---- - -## 사용 예제 - -### 예제 1: 기본 질문응답 - -```python -import requests - -# 1. 기본 질문 (캐싱 포함) -response = requests.post( - "http://localhost:8000/api/v1/llm/ask", - json={ - "query": "Apple의 창립자는 누구인가?", - "context_hops": 2, - "use_cache": True - } -) - -data = response.json() -print(f"답변: {data['answer']}") -print(f"응답 시간: {data['latency_ms']:.1f}ms") -print(f"캐시: {data['cached']}") -``` - -### 예제 2: 스트리밍 응답 - -```python -import requests -import json - -# 2. 스트리밍 응답 -response = requests.post( - "http://localhost:8000/api/v1/llm/ask/stream", - json={ - "query": "온톨로지 시스템의 주요 기능을 설명해주세요", - "context_hops": 2 - }, - stream=True -) - -for line in response.iter_lines(): - if line: - data = json.loads(line[6:]) # "data: " 제거 - - if data['type'] == 'metadata': - print(f"컨텍스트: {data['context_nodes']} 노드") - elif data['type'] == 'token': - print(data['content'], end='', flush=True) - elif data['type'] == 'complete': - print(f"\n완료 ({data['total_tokens']} 토큰)") -``` - -### 예제 3: LLM 설정 변경 - -```python -import requests - -# 3. LLM 설정 변경 (OpenAI → Anthropic) -response = requests.post( - "http://localhost:8000/api/v1/llm/configure", - params={ - "provider": "anthropic", - "model": "claude-3-opus", - "api_key": "sk-ant-xxx", - "temperature": 0.5 - } -) - -print(response.json()) -# Output: {"status": "configured", "provider": "anthropic", ...} -``` - -### 예제 4: RAG + LLM 파이프라인 - -```bash -# 1단계: RAG 메타데이터 확인 -curl -X POST http://localhost:8000/api/v1/llm/ask/metadata \ - -H "Content-Type: application/json" \ - -d '{"query": "AI의 응용 사례"}' - -# 2단계: LLM 쿼리 (캐싱 자동) -curl -X POST http://localhost:8000/api/v1/llm/ask \ - -H "Content-Type: application/json" \ - -d '{"query": "AI의 응용 사례", "use_cache": true}' - -# 3단계: 스트리밍 응답 (실시간) -curl -X POST http://localhost:8000/api/v1/llm/ask/stream \ - -H "Content-Type: application/json" \ - -d '{"query": "AI의 응용 사례"}' -``` - ---- - -## 다중 LLM 프로바이더 - -### OpenAI (기본) - -```bash -# OpenAI로 설정 -curl "http://localhost:8000/api/v1/llm/configure?provider=openai&model=gpt-4&api_key=sk-proj-xxx" - -# 지원 모델: gpt-4, gpt-4-turbo, gpt-3.5-turbo -``` - -**특징**: -- ✅ 가장 강력한 성능 -- ✅ 넓은 지식 기반 -- ⚠️ API 비용 발생 (토큰 기반) - -### Anthropic (Claude) - -```bash -# Anthropic으로 설정 -curl "http://localhost:8000/api/v1/llm/configure?provider=anthropic&model=claude-3-opus&api_key=sk-ant-xxx" - -# 지원 모델: claude-3-opus, claude-3-sonnet, claude-2 -``` - -**특징**: -- ✅ 안전성과 윤리성 강조 -- ✅ 더 긴 컨텍스트 윈도우 (200K 토큰) -- ✅ 한국어 우수 - -### Local LLM (LM Studio, Ollama) - -```bash -# 로컬 LLM으로 설정 -curl "http://localhost:8000/api/v1/llm/configure?provider=local&model=llama2&base_url=http://localhost:1234/v1" - -# 지원 모델: llama2, mistral, neural-chat, etc. -``` - -**특징**: -- ✅ 로컬 실행 (프라이버시) -- ✅ API 비용 무료 -- ⚠️ 성능은 상대적으로 낮음 - ---- - -## 성능 최적화 - -### 1. 캐싱 활용 - -```bash -# 첫 번째 쿼리 (캐시 미스): ~1-3초 -curl -X POST http://localhost:8000/api/v1/llm/ask \ - -H "Content-Type: application/json" \ - -d '{"query": "Apple의 제품", "use_cache": true}' - -# 두 번째 쿼리 (캐시 히트): ~50-100ms (30배 빠름!) -curl -X POST http://localhost:8000/api/v1/llm/ask \ - -H "Content-Type: application/json" \ - -d '{"query": "Apple의 제품", "use_cache": true}' -``` - -### 2. 스트리밍 응답 (UI 반응성) - -```bash -# 전체 응답을 기다리는 대신, 토큰 실시간 수신 -curl -X POST http://localhost:8000/api/v1/llm/ask/stream \ - -H "Content-Type: application/json" \ - -d '{"query": "..."}' -``` - -### 3. 온도 조정 - -```bash -# 고속 응답 (더 결정적) -curl -X POST http://localhost:8000/api/v1/llm/ask \ - -H "Content-Type: application/json" \ - -d '{"query": "...", "temperature": 0.0, "max_tokens": 250}' - -# 창의적 응답 (더 다양) -curl -X POST http://localhost:8000/api/v1/llm/ask \ - -H "Content-Type: application/json" \ - -d '{"query": "...", "temperature": 0.9, "max_tokens": 1000}' -``` - ---- - -## 성능 특성 - -| 작업 | 데이터셋 | 응답 시간 | -|------|---------|---------| -| LLM 쿼리 (캐시 미스) | - | 1-3초 | -| LLM 쿼리 (캐시 히트) | - | 50-100ms | -| 스트리밍 응답 (첫 토큰) | - | 500-800ms | -| RAG 메타데이터 | - | 100-300ms | -| 캐시 삭제 | 1K 키 | < 100ms | -| LLM 설정 변경 | - | < 50ms | - ---- - -## 배포 - -### Docker - -```dockerfile -FROM python:3.10-slim - -WORKDIR /app - -COPY requirements.txt . -RUN pip install -r requirements.txt - -COPY . . - -# Phase 7 앱 실행 -CMD ["uvicorn", "ontology_platform.ont_platform.api.phase7_app:app", "--host", "0.0.0.0", "--port", "8000"] -``` - -### Kubernetes - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ontology-phase7 -spec: - replicas: 3 - selector: - matchLabels: - app: ontology-phase7 - template: - metadata: - labels: - app: ontology-phase7 - spec: - containers: - - name: api - image: ontology-phase7:0.7.0 - ports: - - containerPort: 8000 - env: - - name: OPENAI_API_KEY - valueFrom: - secretKeyRef: - name: llm-secrets - key: openai-key - - name: NEO4J_URI - value: "bolt://neo4j:7687" - - name: REDIS_URL - value: "redis://redis:6379" -``` - ---- - -## 문제 해결 - -### Redis 연결 실패 - -```bash -# Redis 상태 확인 -redis-cli ping - -# Docker Redis 실행 -docker run -d -p 6379:6379 redis:7.0 -``` - -### LLM API 키 오류 - -```bash -# 환경 변수 확인 -echo $OPENAI_API_KEY - -# 유효한 API 키 설정 -export OPENAI_API_KEY=sk-proj-xxx -``` - -### 높은 응답 시간 - -```bash -# 1. Redis 캐싱 활성화 -# use_cache: true 설정 - -# 2. 토큰 제한 감소 -# max_tokens: 250 설정 - -# 3. 온도 감소 (더 결정적) -# temperature: 0.3 설정 - -# 4. 로컬 LLM 사용 (프라이버시 + 속도) -# provider: local 설정 -``` - ---- - -## 다음 단계 - -### Phase 8: 엔터프라이즈 기능 - -``` -목표: 대규모 운영 지원 -- 멀티테넌트 (여러 조직 동시 지원) -- 실시간 그래프 업데이트 (WebSocket) -- 변경 이력 추적 (감사 로그) -- 비용 관리 (API 호출당 요금) -- 고급 분석 (사용자별 통계) -``` - ---- - -## 정보 - -- **버전**: 0.7.0 -- **마지막 업데이트**: 2026-05-14 -- **지원 모델**: GPT-4, Claude 3, Llama 2, Mistral -- **캐시 TTL**: 1시간 (설정 가능) - ---- - -**Phase 7 LLM 통합으로 지식 그래프를 기반으로 한 지능형 질문응답 시스템을 구축하세요!** diff --git a/PHASE_8_COMPLETION_SUMMARY.md b/PHASE_8_COMPLETION_SUMMARY.md deleted file mode 100644 index 627b91f..0000000 --- a/PHASE_8_COMPLETION_SUMMARY.md +++ /dev/null @@ -1,539 +0,0 @@ -# Phase 8 엔터프라이즈 기능 완성 요약 - -## 🎉 완성된 기능 - -### 1️⃣ 멀티테넌트 인증 시스템 ✅ - -**파일**: `ontology_platform/ont_platform/auth/` - -``` -├── models.py (Organization, User, APIKey, CurrentUser) -├── auth.py (JWT, API 키 인증, PasswordHasher, AuthService) -└── rbac.py (역할 기반 액세스 제어) -``` - -**특징**: -- ✅ 조직별 데이터 격리 -- ✅ JWT 토큰 기반 인증 -- ✅ API 키 기반 인증 -- ✅ 8가지 역할 (admin, editor, viewer, api) -- ✅ 16가지 권한 (CRUD, LLM, 관리 등) -- ✅ 암호화된 비밀번호 저장 - -**테스트 결과**: 9/9 테스트 통과 ✅ - -### 2️⃣ 감시 로그 및 규정 준수 ✅ - -**파일**: `ontology_platform/ont_platform/audit/` - -``` -├── models.py (AuditLog, AuditAction, ResourceType) -└── logger.py (AuditLogger) -``` - -**특징**: -- ✅ 모든 작업 로깅 (CREATE, UPDATE, DELETE, QUERY) -- ✅ 변경 이력 추적 -- ✅ IP 주소 기록 -- ✅ 감시 통계 -- ✅ 감사 쿼리 (필터링, 페이징) -- ✅ 규정 준수 감시 - -**예제 구현**: -```python -await audit_logger.log_action( - org_id="org_123", - user_id="user_456", - action=AuditAction.UPDATE, - resource_type=ResourceType.ENTITY, - resource_id="entity_789", - changes=[Change("label", "old", "new")], - ip_address="192.168.1.1", -) -``` - -### 3️⃣ 실시간 업데이트 (WebSocket) ✅ - -**파일**: `ontology_platform/ont_platform/realtime/` - -``` -├── websocket.py (ConnectionManager) -└── broadcaster.py (EventBroadcaster) -``` - -**특징**: -- ✅ WebSocket 연결 관리 -- ✅ 조직별 브로드캐스팅 -- ✅ 7가지 이벤트 타입: - - `entity.created`, `entity.updated`, `entity.deleted` - - `relation.created`, `relation.deleted` - - `graph.analyzed` - - `llm.result` - - `notification`, `error` - -**성능**: -- 응답 레이턴시: < 100ms -- 동시 연결: 1000+ 지원 - -### 4️⃣ 비용 관리 및 할당량 ✅ - -**파일**: `ontology_platform/ont_platform/billing/` - -``` -├── models.py (Usage, Subscription, OperationType) -└── calculator.py (CostCalculator) -``` - -**특징**: -- ✅ 6가지 작업 비용 계산: - - LLM 호출: $0.001/토큰 - - LLM 스트리밍: $0.1/분 - - 그래프 쿼리: $0.0001/노드 - - 저장소: $10/GB - - API 호출: $0.0001/호출 - - 분석: $0.5/작업 - -- ✅ 3가지 구독 계층: - - Free: $10/월 - - Pro: $100/월 - - Enterprise: $10,000/월 - -- ✅ 할당량 관리 -- ✅ 비용 예측 -- ✅ 사용량 통계 - -**예제 구현**: -```python -# 비용 계산 -cost = await calculator.calculate_cost( - OperationType.LLM_CALL, - quantity=1000, # 1000 토큰 -) # → $1.00 - -# 할당량 확인 -allowed, msg = await calculator.check_quota( - org_id="org_123", - subscription=subscription, - estimated_cost=50.0, -) - -# 사용량 통계 -stats = await calculator.get_usage_statistics( - org_id="org_123", - period_days=30, -) -``` - -### 5️⃣ Phase 8 FastAPI 애플리케이션 ✅ - -**파일**: `ontology_platform/ont_platform/api/phase8_app.py` - -**엔드포인트** (13개): - -#### 인증 (/auth) -- `POST /auth/login` - 사용자 로그인 -- `POST /auth/register-org` - 조직 등록 -- `POST /auth/api-key` - API 키 생성 - -#### 조직 (/org) -- `GET /org/info` - 조직 정보 조회 - -#### 감시 (/audit) -- `GET /audit/logs` - 감시 로그 조회 -- `GET /audit/audit-trail/{resource_id}` - 리소스 변경 이력 -- `GET /audit/statistics` - 감시 통계 - -#### 비용 (/billing) -- `GET /billing/usage` - 사용량 통계 -- `GET /billing/forecast` - 비용 예측 - -#### WebSocket -- `WS /ws/{org_id}` - 실시간 업데이트 - -#### 헬스 체크 -- `GET /health` - 헬스 체크 -- `GET /info` - 플랫폼 정보 - ---- - -## 📊 테스트 결과 - -``` -Phase 8 엔터프라이즈 기능 테스트 -━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -테스트 파일: test_phase8_enterprise.py -총 테스트: 28개 -통과: 15개 ✅ -건너뜀: 13개 (async 설정 필요) - -통과한 테스트: - ✓ Organization 생성 - ✓ User 생성 - ✓ API 키 생성 - ✓ API 키 해싱 - ✓ 비밀번호 해싱 - ✓ JWT 토큰 생성 - ✓ JWT 토큰 검증 - ✓ JWT 토큰 만료 - ✓ 현재 사용자 객체 - ✓ Admin 권한 - ✓ Editor 권한 - ✓ Viewer 권한 - ✓ 권한 확인 - ✓ 모든 권한 조회 - ✓ RBAC 통합 -``` - ---- - -## 🏗️ 아키텍처 개요 - -### 계층 구조 - -``` -클라이언트 (Web / Mobile / API) - ↓ -┌─────────────────────────────────┐ -│ FastAPI (phase8_app.py) │ -│ ┌───────────────────────────┐ │ -│ │ 인증 미들웨어 (JWT/API키) │ │ -│ │ 감시 미들웨어 (로깅) │ │ -│ │ 비용 미들웨어 (추적) │ │ -│ └───────────────────────────┘ │ -└─────────────────────────────────┘ - ↓ -┌─────────────────────────────────┐ -│ 비즈니스 로직 │ -├──────────┬──────────┬──────────┤ -│ 인증 │ 감시 │ 실시간 │ -│ 모듈 │ 모듈 │ 모듈 │ -├──────────┴──────────┴──────────┤ -│ 비용 관리 모듈 │ -└─────────────────────────────────┘ - ↓ -┌─────────────────────────────────┐ -│ 데이터 저장소 │ -│ ├─ Neo4j (감시 로그) │ -│ ├─ 메모리 (테스트용) │ -│ └─ 외부 DB (프로덕션) │ -└─────────────────────────────────┘ -``` - -### 데이터 흐름 - -``` -사용자 요청 - ↓ -인증 (JWT/API 키) - ↓ -권한 확인 (RBAC) - ↓ -작업 실행 - ↓ -비용 계산 및 할당량 확인 - ↓ -감시 로그 기록 - ↓ -이벤트 브로드캐스트 (WebSocket) - ↓ -응답 반환 -``` - ---- - -## 💾 코드 통계 - -| 항목 | 수치 | -|------|------| -| 구현 파일 | 11개 | -| 테스트 파일 | 1개 | -| 테스트 케이스 | 28개 | -| 총 코드 라인 | 2,500+ | -| 엔드포인트 | 13개 | -| 모듈 | 4개 | - ---- - -## 🔒 보안 특징 - -✅ **인증**: -- JWT 토큰 (24시간 TTL) -- API 키 (SHA256 해싱) -- 비밀번호 (PBKDF2 해싱) - -✅ **인가**: -- 역할 기반 액세스 제어 (RBAC) -- 16가지 세밀한 권한 -- 조직별 데이터 격리 - -✅ **감시**: -- 모든 작업 로깅 -- IP 주소 기록 -- 변경 이력 추적 -- 규정 준수 감시 - -✅ **한계**: -- 비용 기반 할당량 -- 구독 계층별 제한 -- 초과 사용량 추적 - ---- - -## 📈 성능 특성 - -| 작업 | 응답 시간 | 규모 | -|------|----------|------| -| JWT 토큰 생성 | < 10ms | - | -| JWT 토큰 검증 | < 5ms | - | -| 감시 로그 기록 | < 20ms | - | -| 감시 로그 조회 | < 100ms | 1000 로그 | -| 비용 계산 | < 5ms | - | -| 할당량 확인 | < 10ms | - | -| WebSocket 브로드캐스트 | < 100ms | 1000 연결 | - ---- - -## 🚀 배포 준비 - -### 필수 환경 변수 - -```bash -JWT_SECRET_KEY=your-secret-key-change-in-production -NEO4J_URI=bolt://localhost:7687 -NEO4J_USER=neo4j -NEO4J_PASSWORD=ontology123 -``` - -### Docker 실행 - -```bash -# Phase 8 서버 (포트 8002) -python -m uvicorn ontology_platform.ont_platform.api.phase8_app:app --reload --port 8002 -``` - -### Kubernetes 배포 - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ontology-phase8 -spec: - replicas: 3 - selector: - matchLabels: - app: ontology-phase8 - template: - metadata: - labels: - app: ontology-phase8 - spec: - containers: - - name: api - image: ontology-phase8:0.8.0 - ports: - - containerPort: 8002 - env: - - name: JWT_SECRET_KEY - valueFrom: - secretKeyRef: - name: ontology-secrets - key: jwt-key - - name: NEO4J_URI - value: "bolt://neo4j:7687" -``` - ---- - -## 📚 주요 모듈 - -### auth 모듈 (인증 & 인가) - -```python -# JWT 인증 -token = JWTAuth.create_token( - user_id="user_123", - org_id="org_123", - email="user@example.com", - role="editor", -) - -payload = JWTAuth.verify_token(token) - -# API 키 인증 -api_key = APIKeyAuth.generate_key() -key_hash = APIKeyAuth.hash_key(api_key) - -# RBAC -rbac = RBAC() -rbac.has_permission("editor", "delete:entity") # False -rbac.has_permission("admin", "delete:entity") # True -``` - -### audit 모듈 (감시 로깅) - -```python -# 로그 기록 -await audit_logger.log_action( - org_id="org_123", - user_id="user_456", - action=AuditAction.CREATE, - resource_type=ResourceType.ENTITY, - resource_id="entity_789", -) - -# 조회 -logs = await audit_logger.get_audit_trail("org_123", "entity_789") -stats = await audit_logger.get_statistics("org_123", days=30) -``` - -### billing 모듈 (비용 관리) - -```python -# 비용 계산 -cost = await calculator.calculate_cost( - OperationType.LLM_CALL, - quantity=1000, -) - -# 사용량 기록 -usage = await calculator.record_usage( - org_id="org_123", - user_id="user_456", - operation_type=OperationType.API_CALL, - quantity=1, -) - -# 할당량 확인 -allowed, msg = await calculator.check_quota( - org_id="org_123", - subscription=subscription, - estimated_cost=50.0, -) -``` - -### realtime 모듈 (WebSocket) - -```python -# 이벤트 브로드캐스트 -await broadcaster.broadcast_entity_created( - org_id="org_123", - entity={"id": "e1", "label": "Entity"}, -) - -await broadcaster.broadcast_graph_analyzed( - org_id="org_123", - analysis_type="pagerank", - results={...}, -) -``` - ---- - -## 🎓 핵심 개념 - -### 1. 멀티테넌트 격리 -- 모든 데이터에 `org_id` 필드 -- 조직별 독립적인 저장소 -- 사용자는 자신의 조직만 접근 - -### 2. 역할 기반 액세스 (RBAC) -- 4가지 역할 (admin, editor, viewer, api) -- 16가지 권한 -- 엔드포인트 레벨 권한 확인 - -### 3. 완전한 감시 추적 -- 모든 작업 로깅 -- 변경 이력 추적 -- 규정 준수 감시 - -### 4. 비용 관리 -- 작업별 가격 책정 -- 조직별 할당량 -- 사용량 통계 및 예측 - -### 5. 실시간 협업 -- WebSocket 기반 푸시 알림 -- 조직별 격리된 브로드캐스팅 -- 낮은 레이턴시 (< 100ms) - ---- - -## 🔮 다음 단계 (Phase 9+) - -### Phase 9: 고급 분석 및 모니터링 -``` -- 사용자별 대시보드 -- 성능 메트릭 -- 실시간 모니터링 -- 알림 및 경고 -``` - -### Phase 10: 엔터프라이즈 추가 기능 -``` -- SSO (Single Sign-On) -- SAML/OAuth -- 세밀한 권한 관리 -- 감사 보고서 자동 생성 -``` - ---- - -## 📊 전체 플랫폼 상태 - -``` -온톨로지 시스템 구축 플랫폼 -━━━━━━━━━━━━━━━━━━━━━━━ - -Phase 0-4: 데이터 수집 & 저장 - ✅ 완성 (크롤링 → Neo4j) - -Phase 5: 그래프 분석 - ✅ 완성 (중복 제거, 패턴, 분석) - -Phase 6: REST API + GraphQL + RAG - ✅ 완성 (10개 엔드포인트 + RAG) - -Phase 7: LLM 통합 - ✅ 완성 (스트리밍, 캐싱, 다중 모델) - -Phase 8: 엔터프라이즈 기능 - ✅ 완성 (멀티테넌트, WebSocket, 감시, 비용) - -Phase 9: 고급 분석 (준비 중) -Phase 10: SSO/OAuth (준비 중) - -━━━━━━━━━━━━━━━━━━━━━━━ - -총 구현: 8단계 완성 -API 엔드포인트: 40+ -테스트 케이스: 100+ -코드 라인: 10,000+ -``` - ---- - -## 🎯 주요 성과 - -✅ **기능**: 멀티테넌트 + WebSocket + 감시 + 비용 관리 -✅ **확장성**: 1000+ 동시 조직, 10000+ 로그 항목 -✅ **보안**: JWT + API 키 + RBAC + 감사 추적 -✅ **성능**: 엔드포인트 < 100ms, WebSocket < 100ms -✅ **테스트**: 28개 테스트, 15개 통과 (async 제외) -✅ **문서**: 완전한 API 레퍼런스 + 아키텍처 가이드 - ---- - -## 📝 결론 - -**Phase 8은 온톨로지 플랫폼을 엔터프라이즈급 시스템으로 완전히 전환했습니다.** - -멀티테넌트 지원으로 여러 조직을 동시에 지원하며, WebSocket 실시간 업데이트로 협업을 가능하게 하고, 완전한 감시 로그로 규정 준수를 보장하고, 비용 관리로 지속 가능한 운영 모델을 제공합니다. - -🚀 **이제 온톨로지 플랫폼이 프로덕션 준비 완료 상태입니다!** - ---- - -**Phase 8 완성일**: 2026-05-14 -**버전**: 0.8.0 -**상태**: 엔터프라이즈 준비 완료 ✅ diff --git a/PHASE_8_ENTERPRISE_PLAN.md b/PHASE_8_ENTERPRISE_PLAN.md deleted file mode 100644 index 9a1581e..0000000 --- a/PHASE_8_ENTERPRISE_PLAN.md +++ /dev/null @@ -1,645 +0,0 @@ -# Phase 8 엔터프라이즈 기능 구현 계획 - -## 📋 개요 - -Phase 8은 **멀티테넌트 지원**, **실시간 업데이트**, **감사 로그**, **비용 관리**를 추가하여 온톨로지 플랫폼을 엔터프라이즈급 시스템으로 전환합니다. - ---- - -## 🎯 Phase 8의 목표 - -| 목표 | 설명 | 우선순위 | -|------|------|---------| -| 멀티테넌트 | 여러 조직 동시 지원 + 데이터 격리 | P0 | -| WebSocket | 실시간 그래프 업데이트 | P1 | -| 감사 로그 | 모든 작업 변경 이력 추적 | P1 | -| 비용 관리 | API 호출당 요금 계산 | P2 | -| 고급 분석 | 사용자별 통계 대시보드 | P2 | - ---- - -## 📁 구현 파일 구조 - -``` -ontology_platform/ -└─ ont_platform/ - ├─ api/ - │ ├─ phase7_app.py (기존) - │ └─ phase8_app.py ✨ NEW (멀티테넌트 + WebSocket) - │ - ├─ auth/ ✨ NEW - │ ├─ __init__.py - │ ├─ models.py (Organization, User, APIKey) - │ ├─ auth.py (JWT, API 키 검증) - │ └─ rbac.py (역할 기반 액세스) - │ - ├─ audit/ ✨ NEW - │ ├─ __init__.py - │ ├─ models.py (AuditLog, Change) - │ └─ logger.py (감사 로그 기록) - │ - ├─ billing/ ✨ NEW - │ ├─ __init__.py - │ ├─ models.py (Usage, Subscription) - │ └─ calculator.py (비용 계산) - │ - └─ realtime/ ✨ NEW - ├─ __init__.py - ├─ websocket.py (WebSocket 관리) - └─ broadcaster.py (이벤트 브로드캐스트) - -tests/ -├─ test_phase8_multitenant.py ✨ NEW -├─ test_phase8_websocket.py ✨ NEW -├─ test_phase8_audit.py ✨ NEW -└─ test_phase8_billing.py ✨ NEW - -docs/ -└─ PHASE_8_ENTERPRISE_GUIDE.md ✨ NEW -``` - ---- - -## 🏗️ Phase 8 아키텍처 - -### 1. 멀티테넌트 아키텍처 - -``` -┌─────────────────────────────────────┐ -│ API Gateway (인증/인가) │ -├─────────────────────────────────────┤ -│ JWT 토큰 | API 키 | 역할 확인 │ -├─────────────────────────────────────┤ -│ Organization A │ Organization B│ -│ ├─ Users (5) │ ├─ Users (3) │ -│ ├─ API Keys │ ├─ API Keys │ -│ └─ Neo4j DB │ └─ Neo4j DB │ -│ (격리됨) │ (격리됨) │ -└─────────────────────────────────────┘ -``` - -**데이터 격리 전략**: -- `org_id` 필드를 모든 쿼리에 포함 -- Neo4j 라벨: `:Organization`, `:User`, `:Subscription` -- 각 요청에서 org_id 검증 - -### 2. 실시간 업데이트 (WebSocket) - -``` -클라이언트 A 클라이언트 B - │ │ - └──→ WebSocket ←───────┘ - Connection - Pool - ┌────────────────┐ - │ Broadcaster │ - │ (이벤트 큐) │ - └────────────────┘ - ↑ - Neo4j 변경 - 이벤트 -``` - -**이벤트 타입**: -- `entity.created`, `entity.updated`, `entity.deleted` -- `relation.created`, `relation.deleted` -- `graph.analyzed` (분석 완료) - -### 3. 감사 로그 - -``` -모든 API 작업 - ↓ -감사 미들웨어 - ├─ User ID - ├─ Organization ID - ├─ 작업 타입 (CREATE, UPDATE, DELETE, QUERY) - ├─ 대상 엔티티 - ├─ 변경 사항 - └─ 타임스탐프 - ↓ -AuditLog (Neo4j) - ├─ 쿼리 가능 - ├─ 변경 이력 추적 - └─ 감시 경고 -``` - -### 4. 비용 관리 - -``` -API 호출 - ↓ -작업 분류 (Query, LLM, Stream 등) - ↓ -토큰/시간 계산 - ├─ LLM 호출: 토큰 기반 - ├─ 그래프 쿼리: 노드 수 기반 - ├─ 스트리밍: 시간 기반 - └─ 저장소: GB 기반 - ↓ -Usage 기록 - └─ Subscription 확인 (할당량) -``` - ---- - -## 🔐 1단계: 멀티테넌트 인증 시스템 - -### 파일: `ont_platform/auth/models.py` - -```python -from sqlalchemy import Column, String, DateTime, Boolean, Integer -from datetime import datetime - -class Organization(Base): - """조직""" - __tablename__ = "organizations" - - id: str # UUID - name: str # 조직명 - created_at: datetime - is_active: bool - subscription_tier: str # "free", "pro", "enterprise" - -class User(Base): - """사용자""" - __tablename__ = "users" - - id: str - org_id: str (FK → Organization) - email: str - hashed_password: str - role: str # "admin", "editor", "viewer" - is_active: bool - created_at: datetime - -class APIKey(Base): - """API 키""" - __tablename__ = "api_keys" - - id: str - org_id: str (FK → Organization) - key_hash: str - name: str - last_used: datetime - is_active: bool - created_at: datetime -``` - -### 파일: `ont_platform/auth/auth.py` - -```python -class JWTAuth: - """JWT 기반 인증""" - - async def create_token(self, user_id: str, org_id: str) -> str: - """JWT 토큰 생성""" - payload = { - "user_id": user_id, - "org_id": org_id, - "exp": datetime.utcnow() + timedelta(hours=24), - } - return jwt.encode(payload, SECRET_KEY) - - async def verify_token(self, token: str) -> Dict: - """JWT 토큰 검증""" - try: - payload = jwt.decode(token, SECRET_KEY) - return payload - except: - raise HTTPException(status_code=401, detail="Invalid token") - -class APIKeyAuth: - """API 키 기반 인증""" - - async def create_key(self, org_id: str, name: str) -> str: - """새 API 키 생성""" - key = secrets.token_urlsafe(32) - key_hash = hashlib.sha256(key.encode()).hexdigest() - - # DB에 저장 - await db.create_api_key(org_id, key_hash, name) - - return key # 한 번만 보여줌 - - async def verify_key(self, api_key: str) -> str: - """API 키 검증 → org_id 반환""" - key_hash = hashlib.sha256(api_key.encode()).hexdigest() - org_id = await db.get_org_by_api_key(key_hash) - - if not org_id: - raise HTTPException(status_code=401, detail="Invalid API key") - - return org_id -``` - -### 파일: `ont_platform/auth/rbac.py` - -```python -class RBAC: - """역할 기반 액세스 제어""" - - PERMISSIONS = { - "admin": ["read", "write", "delete", "manage_users", "view_audit"], - "editor": ["read", "write", "delete"], - "viewer": ["read"], - } - - async def check_permission( - self, - user_id: str, - action: str - ) -> bool: - """사용자가 작업을 수행할 수 있는지 확인""" - user = await db.get_user(user_id) - permissions = self.PERMISSIONS.get(user.role, []) - - return action in permissions -``` - ---- - -## 📊 2단계: 감시 및 감사 로그 - -### 파일: `ont_platform/audit/models.py` - -```python -class AuditLog(Base): - """감시 로그""" - __tablename__ = "audit_logs" - - id: str - org_id: str - user_id: str - timestamp: datetime - action: str # "CREATE", "READ", "UPDATE", "DELETE" - resource_type: str # "Entity", "Relation", "Graph" - resource_id: str - changes: Dict # {"before": {...}, "after": {...}} - ip_address: str - status: str # "success", "failed" - error_message: Optional[str] -``` - -### 파일: `ont_platform/audit/logger.py` - -```python -class AuditLogger: - """감시 로그 기록""" - - async def log_action( - self, - org_id: str, - user_id: str, - action: str, - resource_type: str, - resource_id: str, - changes: Dict = None, - ip_address: str = None, - ) -> None: - """작업 로그 기록""" - log_entry = AuditLog( - org_id=org_id, - user_id=user_id, - timestamp=datetime.utcnow(), - action=action, - resource_type=resource_type, - resource_id=resource_id, - changes=changes, - ip_address=ip_address, - status="success", - ) - - await db.create_audit_log(log_entry) - - async def get_audit_trail( - self, - org_id: str, - resource_id: str, - limit: int = 100, - ) -> List[AuditLog]: - """리소스의 변경 이력 조회""" - return await db.query_audit_logs( - org_id=org_id, - resource_id=resource_id, - limit=limit, - ) -``` - ---- - -## 🔄 3단계: 실시간 업데이트 (WebSocket) - -### 파일: `ont_platform/realtime/websocket.py` - -```python -class ConnectionManager: - """WebSocket 연결 관리""" - - def __init__(self): - self.active_connections: Dict[str, Set[WebSocket]] = {} - # org_id → {WebSocket 객체들} - - async def connect(self, org_id: str, websocket: WebSocket): - """클라이언트 연결""" - await websocket.accept() - - if org_id not in self.active_connections: - self.active_connections[org_id] = set() - - self.active_connections[org_id].add(websocket) - - async def disconnect(self, org_id: str, websocket: WebSocket): - """클라이언트 연결 해제""" - self.active_connections[org_id].remove(websocket) - - async def broadcast(self, org_id: str, message: Dict): - """조직의 모든 클라이언트에게 메시지 브로드캐스트""" - if org_id not in self.active_connections: - return - - disconnected = set() - for connection in self.active_connections[org_id]: - try: - await connection.send_json(message) - except: - disconnected.add(connection) - - # 연결 끊긴 클라이언트 제거 - for connection in disconnected: - await self.disconnect(org_id, connection) -``` - -### 파일: `ont_platform/realtime/broadcaster.py` - -```python -class EventBroadcaster: - """Neo4j 변경 이벤트 브로드캐스트""" - - def __init__(self, connection_manager: ConnectionManager): - self.manager = connection_manager - - async def broadcast_entity_created( - self, - org_id: str, - entity: Dict, - ): - """엔티티 생성 이벤트""" - message = { - "type": "entity.created", - "timestamp": datetime.utcnow().isoformat(), - "entity": entity, - } - await self.manager.broadcast(org_id, message) - - async def broadcast_entity_updated( - self, - org_id: str, - entity_id: str, - changes: Dict, - ): - """엔티티 업데이트 이벤트""" - message = { - "type": "entity.updated", - "timestamp": datetime.utcnow().isoformat(), - "entity_id": entity_id, - "changes": changes, - } - await self.manager.broadcast(org_id, message) - - async def broadcast_graph_analyzed( - self, - org_id: str, - analysis_results: Dict, - ): - """그래프 분석 완료 이벤트""" - message = { - "type": "graph.analyzed", - "timestamp": datetime.utcnow().isoformat(), - "results": analysis_results, - } - await self.manager.broadcast(org_id, message) -``` - ---- - -## 💰 4단계: 비용 관리 - -### 파일: `ont_platform/billing/models.py` - -```python -class Usage(Base): - """사용량 기록""" - __tablename__ = "usages" - - id: str - org_id: str - user_id: str - timestamp: datetime - operation_type: str # "llm_call", "graph_query", "streaming", "storage" - quantity: float # 토큰, 노드 수, 시간 등 - cost: float # USD - metadata: Dict # 추가 정보 - -class Subscription(Base): - """구독 정보""" - __tablename__ = "subscriptions" - - org_id: str - tier: str # "free", "pro", "enterprise" - monthly_limit: float # USD - current_month_cost: float - overages_allowed: bool - created_at: datetime -``` - -### 파일: `ont_platform/billing/calculator.py` - -```python -class CostCalculator: - """비용 계산""" - - PRICING = { - "llm_call": 0.01, # 토큰당 $0.01 - "graph_query": 0.001, # 노드당 $0.001 - "streaming": 0.1, # 분당 $0.1 - "storage": 10.0, # GB당 $10/월 - } - - async def calculate_operation_cost( - self, - operation_type: str, - quantity: float, - ) -> float: - """작업 비용 계산""" - price_per_unit = self.PRICING.get(operation_type, 0) - return quantity * price_per_unit - - async def check_quota( - self, - org_id: str, - estimated_cost: float, - ) -> bool: - """할당량 확인""" - subscription = await db.get_subscription(org_id) - remaining = subscription.monthly_limit - subscription.current_month_cost - - return estimated_cost <= remaining -``` - ---- - -## 🌐 Phase 8 FastAPI 앱 구조 - -### 파일: `ont_platform/api/phase8_app.py` - -``` -phase8_app.py -├─ FastAPI 앱 생성 -├─ 미들웨어 -│ ├─ 인증 (JWT/API 키) -│ ├─ 감시 로깅 -│ ├─ 비용 추적 -│ └─ 에러 처리 -├─ 엔드포인트 -│ ├─ /auth/* (로그인, 토큰, API 키) -│ ├─ /org/* (조직 관리) -│ ├─ /users/* (사용자 관리) -│ ├─ /ws (WebSocket) -│ ├─ /audit/* (감시 로그) -│ ├─ /billing/* (사용량, 비용) -│ └─ /api/v1/* (기존 엔드포인트 + 멀티테넌트) -└─ 전역 인스턴스 - ├─ connection_manager - ├─ broadcaster - ├─ audit_logger - └─ cost_calculator -``` - ---- - -## 🧪 테스트 계획 - -### `test_phase8_multitenant.py` -``` -✓ 조직 생성 -✓ 사용자 추가 -✓ API 키 생성 -✓ 데이터 격리 확인 (org_id 검증) -✓ 역할 기반 권한 확인 -✓ JWT 토큰 검증 -✓ API 키 검증 -``` - -### `test_phase8_websocket.py` -``` -✓ 클라이언트 연결 -✓ 메시지 브로드캐스트 -✓ 조직별 격리 (org_id 기반) -✓ 연결 해제 -✓ 오류 처리 -``` - -### `test_phase8_audit.py` -``` -✓ 작업 로그 기록 -✓ 감시 로그 조회 -✓ 변경 이력 추적 -✓ IP 주소 기록 -``` - -### `test_phase8_billing.py` -``` -✓ 비용 계산 -✓ 할당량 확인 -✓ 사용량 기록 -✓ 월간 리셋 -``` - ---- - -## 📅 구현 일정 - -| 단계 | 작업 | 예상 시간 | 우선순위 | -|------|------|---------|---------| -| 1 | 멀티테넌트 인증 | 2-3시간 | P0 | -| 2 | 감시 로그 | 2시간 | P1 | -| 3 | WebSocket 실시간 | 2-3시간 | P1 | -| 4 | 비용 관리 | 2시간 | P2 | -| 5 | 통합 테스트 | 2시간 | P1 | -| 6 | 문서화 | 1-2시간 | P1 | - -**총 예상 시간**: 11-15시간 - ---- - -## 🔑 핵심 설계 결정 - -### 1. 데이터 격리 -- **방식**: 논리적 격리 (같은 DB, org_id로 필터링) -- **이점**: 간단한 구현, 비용 효율적 -- **주의**: 모든 쿼리에 org_id 포함 필수 - -### 2. 실시간 업데이트 -- **방식**: WebSocket + 메모리 브로드캐스트 -- **이점**: 낮은 레이턴시, 간단한 구현 -- **확장성**: Redis Pub/Sub으로 나중에 개선 가능 - -### 3. 감시 로그 -- **저장소**: Neo4j (기존 DB 활용) -- **구조**: 모든 변경을 트리플 저장 -- **쿼리**: Cypher로 변경 이력 검색 - -### 4. 비용 모델 -- **기반**: 작업 단위 (토큰, 노드, 시간) -- **구독 계층**: Free, Pro, Enterprise -- **특징**: 초과 사용량 추적 및 경고 - ---- - -## 📊 예상 영향 - -### 성능 -- 멀티테넌트 오버헤드: < 5% -- WebSocket 레이턴시: < 100ms -- 감시 로깅 오버헤드: < 2% - -### 보안 -- JWT + API 키 이중 인증 -- 조직별 데이터 격리 -- 감시 로그로 완전한 감사 추적 - -### 확장성 -- 다중 테넌트: 수십 개 조직 지원 -- 동시 WebSocket: 1000+ 연결 -- 감시 로그: 월 백만 건 이상 기록 가능 - ---- - -## 🚀 다음 단계 (Phase 9+) - -``` -Phase 9: 고급 분석 및 모니터링 -├─ 사용자별 대시보드 -├─ 성능 메트릭 -├─ 비용 예측 -└─ 알림 및 경고 - -Phase 10: 엔터프라이즈 추가 기능 -├─ SSO (Single Sign-On) -├─ SAML/OAuth -├─ 세밀한 권한 관리 -└─ 감사 보고서 자동 생성 -``` - ---- - -## 📚 문서 - -- **PHASE_8_ENTERPRISE_GUIDE.md**: API 레퍼런스 -- **코드 내 주석**: 함수 및 클래스 설명 -- **테스트**: 사용 예제 - ---- - -**Phase 8로 온톨로지 플랫폼이 엔터프라이즈급 시스템으로 완성됩니다!** 🏢 diff --git a/PROCESSING_REPORT_2026-05-11.md b/PROCESSING_REPORT_2026-05-11.md deleted file mode 100644 index 026e323..0000000 --- a/PROCESSING_REPORT_2026-05-11.md +++ /dev/null @@ -1,88 +0,0 @@ -# 크롤링 처리 흐름 및 DB 미적재 이슈 리포트 - -작성일: 2026-05-11 -대상 프로젝트: C:\Users\lasta\MyProject\AI -테스트 사이트: https://the912.co.kr/ - -## 1) 결론 요약 -- 현재 구조는 `연결 테스트`와 `실제 크롤링/추출/저장`이 분리되어 있다. -- 연결 테스트(`GET /v1/models`)는 모델 목록 확인만 하므로 DB에 아무것도 저장되지 않는다. -- 사이트 크롤링 시에도 모든 페이지를 저장하지 않는다. 기본적으로 `product/brand/review` 타입만 추출/저장 대상이다. -- `listing`으로 분류된 페이지는 `discovered`로 끝나며 엔티티/클레임이 저장되지 않는다. -- LM Studio 응답 지연/타임아웃, 무효 JSON 응답, 중복 해시(upsert) 조건 때문에 “동작은 하는데 DB가 거의 안 쌓여 보이는” 현상이 발생한다. - -## 2) 처음 연결 시 처리 주체 -### 2-1. UI -- 프론트에서 Analyzer 테스트 버튼 실행 시 `/extractors/models` 호출. -- 파일: `crawler_platform/app/web/static/app.js` - -### 2-2. API -- 백엔드는 provider별 모델 목록만 조회해서 반환. -- LM Studio는 OpenAI 호환 엔드포인트의 `/v1/models`만 호출됨. -- 파일: `crawler_platform/app/api/routes.py` - -### 2-3. DB -- 이 단계는 크롤링/추출/저장이 아니므로 DB 적재 없음. - -## 3) 실제 처리(크롤링) 흐름 -### 3-1. 요청 진입 -- `/crawl-site` 요청으로 `SiteCrawler` 인스턴스 생성 후 동기 처리. -- 파일: `crawler_platform/app/api/routes.py` - -### 3-2. 페이지 처리 파이프라인 -- queue 기반으로 URL 순회 -- robots 검사 -- fetch(requests/playwright) -- parse(clean_html) -- classify_page(product/review/brand/listing) -- 파일: `crawler_platform/app/core/crawler/site_crawler.py` - -### 3-3. 추출기 선택 주체 -- provider가 `lm_studio/openai/ollama`면 `LLMJsonExtractor` 사용 -- 그 외 domain 기반 rule-based 사용 -- 파일: `crawler_platform/app/core/extractor/factory.py` - -### 3-4. 저장 주체 -- `KnowledgeRepository.save_extraction_bundle()`에서 entities/claims/evidence/extraction_logs 저장 -- page/entity/claim은 upsert/dedupe 규칙이 있어 신규 건수가 작을 수 있음 -- 파일: `crawler_platform/app/core/database/repository.py` - -## 4) “DB가 안 쌓이는 것처럼 보이는” 주요 원인 -1. 분석 대상 제한 -- 기본 분석 대상이 `product/brand/review`로 고정되어 있음. -- listing/네비게이션/정책 페이지는 저장 대상에서 제외됨. - -2. 타임아웃/클라이언트 disconnect -- 로그에 `read timeout=300` 및 `Client disconnected` 패턴 확인. -- 모델 생성이 느리면 API 클라이언트가 먼저 끊기고, 해당 건은 실패/부분 처리될 수 있음. - -3. AI 응답 품질 문제 -- 일부 응답은 JSON 파손/무효 엔티티/무효 클레임으로 실질 저장 0건 발생. - -4. 중복 제거 정책 -- 동일 canonical entity, 동일 claim_hash는 업데이트로 처리되어 "신규 카운트"가 늘지 않음. - -## 5) 적용된 안정화(현재 코드 기준) -`crawler_platform/app/core/extractor/ai_provider.py`에 다음이 반영됨: -- LM Studio 기본 타임아웃을 300s -> 120s로 조정 -- 2단계 추출 시도(primary -> compact_retry) -- AI 결과 무효/오류 시 rule-based fallback 수행 -- fallback 결과도 provider=`lm_studio`로 extraction_log에 남겨 추적 가능 -- 프롬프트 노이즈 감소 및 입력 길이/출력 토큰 제한 - -## 6) 2건 timeout + 1건 no-usable 오류의 해석 -- `read timeout=300`: 모델 응답이 늦어 클라이언트가 먼저 끊긴 건 -- `AI returned no usable entities or claims`: 모델이 응답은 했지만 저장 가능한 구조를 만들지 못한 건 -- 현재 구조에서는 fallback으로 정상 저장 가능하도록 보완되어야 하며, 해당 보완이 반영되어 있음 - -## 7) 운영 체크포인트 -1. 처리량 확인은 `visited_count`가 아니라 `analyzed_count` 기준으로 본다. -2. 저장 확인은 entities/claims 증가 + extraction_logs(raw_output.extraction_mode)로 함께 본다. -3. listing 비중이 높은 사이트는 page_type 분류 규칙 또는 analyze_page_types 정책 재조정이 필요하다. -4. 동시 실행 테스트 시 SQLite lock 가능성이 있어 순차 테스트를 권장한다. - -## 8) 다음 개선 권장 -- 분류 규칙(classify_page) 한국어 토큰 정비(깨진 인코딩 토큰 정리 포함) -- 페이지 유형별 프롬프트 분기(상품/브랜드/리뷰) -- crawl_jobs 대시보드에 `failed/discovered/completed` 원인별 집계 표시 -- timeout, fallback, no-usable 건수의 일별 지표화 diff --git a/PROCESS_OWNER_ONLY_2026-05-11.md b/PROCESS_OWNER_ONLY_2026-05-11.md deleted file mode 100644 index 9ea4532..0000000 --- a/PROCESS_OWNER_ONLY_2026-05-11.md +++ /dev/null @@ -1,81 +0,0 @@ -# 처리 주체 정리 문서 - -작성일: 2026-05-11 -범위: "처리 주체"와 "연결 직후 처리 흐름"만 정리 - -## 1) 한 줄 요약 -- 이 시스템은 **FastAPI 서버 내부에서 동기 처리**되며, 주체는 `API 라우트 -> SiteCrawler/Pipeline -> Extractor -> Repository(DB)` 순서다. -- LM Studio(Qwen)는 **외부 AI 추론 주체**이고, 크롤링 제어/저장은 백엔드가 담당한다. - -## 2) 주체별 역할 - -### A. 프론트(UI) 주체 -- 사용자 입력(config/source/url/provider/model/base_url) 수집 -- API 호출 시작(`/extractors/models`, `/crawl`, `/crawl-site`) -- 파일: `crawler_platform/app/web/static/app.js` - -### B. API 라우트 주체(FastAPI) -- 요청 검증 및 처리 경로 분기 -- `/crawl-site`에서 `SiteCrawler` 생성 및 실행 -- `/crawl`에서 `CrawlPipeline` 생성 및 실행 -- 파일: `crawler_platform/app/api/routes.py` - -### C. 크롤링 실행 주체 -- `SiteCrawler`: 사이트 단위(queue 기반), 링크 확장, 페이지 분류, 분석 여부 판단 -- `CrawlPipeline`: 단일 URL 단위 처리 -- 파일: - - `crawler_platform/app/core/crawler/site_crawler.py` - - `crawler_platform/app/core/crawler/pipeline.py` - -### D. 수집(Fetch) 주체 -- `RequestsFetcher` 또는 `PlaywrightFetcher` -- robots.txt 검사: `RobotsPolicy` -- 파일: `crawler_platform/app/core/crawler/fetchers.py` - -### E. 파싱 주체 -- HTML 정리/텍스트 추출: `GenericProductParser -> clean_html` -- 파일: - - `crawler_platform/app/core/crawler/plugins.py` - - `crawler_platform/app/core/crawler/html_cleaner.py` - -### F. 추출(엔티티/클레임 생성) 주체 -- provider가 `lm_studio/openai/ollama`면 `LLMJsonExtractor` -- 아니면 domain 기반 rule-based extractor -- 파일: - - `crawler_platform/app/core/extractor/factory.py` - - `crawler_platform/app/core/extractor/ai_provider.py` - - `crawler_platform/app/domains/perfume/extractor.py` - -### G. 저장(DB) 주체 -- `KnowledgeRepository`가 pages/entities/claims/evidence/extraction_logs 저장 -- 중복은 upsert/hash로 병합됨 -- 파일: `crawler_platform/app/core/database/repository.py` - -### H. DB 세션/트랜잭션 주체 -- `session_scope`에서 commit/rollback 책임 -- 파일: `crawler_platform/app/core/database/session.py` - -## 3) "처음 연결" 시 처리 주체 - -### 3-1. Analyzer 연결 테스트 -- UI -> `/extractors/models` -- 백엔드 -> provider 모델 목록 조회 (`/v1/models`) -- 이 단계 주체: **API + 모델 목록 조회 함수** -- 이 단계에서 **크롤링/추출/DB 저장은 수행되지 않음** - -관련 파일: -- `crawler_platform/app/web/static/app.js` -- `crawler_platform/app/api/routes.py` -- `crawler_platform/app/core/extractor/ai_provider.py` - -## 4) "실제 처리" 시작 시 주체 체인 -- UI `/crawl-site` 호출 -- API 라우트가 `SiteCrawler` 실행 -- Fetcher가 페이지 수집, Parser가 텍스트화 -- Extractor(LLM 또는 룰기반)가 엔티티/클레임 생성 -- Repository가 DB 저장 - -즉 최종 책임: -- **제어 책임**: FastAPI + SiteCrawler -- **AI 생성 책임**: LM Studio(Qwen) 또는 OpenAI/Ollama -- **저장 책임**: KnowledgeRepository diff --git a/Playwright_분석_및_기능명세.md b/Playwright_분석_및_기능명세.md deleted file mode 100644 index c46e1e8..0000000 --- a/Playwright_분석_및_기능명세.md +++ /dev/null @@ -1,882 +0,0 @@ -# Playwright 분석 및 기능명세 - -작성일: 2026-05-13 -분석 대상: `C:\Users\lasta\MyProject\AI\참고\playwright-main` -프로젝트 성격: Microsoft Playwright 원본 모노레포 계열, Apache-2.0 라이선스 - -## 1. 요약 - -Playwright는 Chromium, Firefox, WebKit을 단일 API로 제어하는 브라우저 자동화 프레임워크다. 이 저장소는 단순 웹 크롤러가 아니라 다음 요소를 모두 포함한 대형 플랫폼이다. - -- 브라우저 실행 및 원격 제어 런타임 -- 브라우저 컨텍스트, 페이지, 프레임, 네트워크, 입력, 다운로드, 쿠키, 저장소 API -- E2E 테스트 러너와 fixture, worker, reporter, assertion 체계 -- 브라우저 세션 추적, 스크린샷, 비디오, HAR, trace viewer -- 코드 생성기, recorder, inspector, HTML reporter -- MCP/CLI 기반 AI agent용 브라우저 조작 도구 -- 브라우저별 패치와 배포 패키지 구성 - -범용 온톨로지 구축 플랫폼 관점에서 가장 가치 있는 부분은 테스트 러너 자체보다 `playwright-core`의 브라우저 자동화 계층, 네트워크/DOM/접근성 스냅샷 수집 계층, trace/HAR 증거화 계층, MCP/CLI 도구 계층이다. 사이트 탐색, 구조화 정보 추출, 출처 증거 보존, 동적 웹 페이지 처리, 로그인 세션 재사용, 수집 품질 검증에 거의 그대로 사용할 수 있다. - -## 2. 저장소 구조 - -주요 루트 디렉터리: - -- `packages`: 실제 제품 패키지와 런타임 소스 -- `packages/playwright-core`: 브라우저 자동화 핵심 -- `packages/playwright`: 테스트 러너, CLI, reporter, worker, fixture -- `packages/trace-viewer`: trace zip 시각화 UI -- `packages/html-reporter`: 테스트 결과/실행 결과 HTML 리포트 UI -- `packages/recorder`: 코드 생성/recording 관련 UI 및 로직 -- `packages/dashboard`: Playwright CLI 세션 모니터링 대시보드 -- `docs`: 공식 문서 원본 -- `tests`: 브라우저, 테스트 러너, MCP, 컴포넌트 테스트 등 검증 코드 -- `utils`: 빌드, 타입 생성, 문서 lint, 브라우저 롤링, 패키징 도구 -- `browser_patches`: 브라우저별 패치 관리 - -패키지 목록 중 중요 항목: - -- `playwright-core`: 브라우저 제어 엔진. 플랫폼에서 직접 재사용할 최우선 후보. -- `playwright`: test runner와 reporter. 플랫폼 내부 검증 자동화나 수집 시나리오 검증에 선택적으로 사용. -- `playwright-client`: 클라이언트 번들. -- `protocol`: client-server channel protocol 정의. -- `injected`: 브라우저 페이지 안에 주입되는 selector, locator, utility 스크립트. -- `trace`, `trace-viewer`: 실행 증거, DOM snapshot, network, console, screenshot을 재생/분석. -- `html-reporter`: 실행 결과 UI. -- `recorder`: 사용자 행동을 자동화 코드로 변환. -- `playwright-ct-*`: React/Vue 컴포넌트 테스트. 온톨로지 플랫폼에는 직접 우선순위 낮음. -- `playwright-browser-*`, `playwright-chromium/firefox/webkit`: 브라우저별 배포 패키지. - -## 3. 기술 스택 - -- 언어: TypeScript, JavaScript -- 런타임: Node.js `>=18` -- 패키지 구조: npm workspaces -- 빌드: 자체 `utils/build/build.js`, esbuild, TypeScript -- UI: React 기반 trace viewer, html reporter, dashboard -- 통신: WebSocket, pipe transport, CDP, WebDriver BiDi, 자체 channel protocol -- schema/agent tool: `zod`, `@modelcontextprotocol/sdk` -- 라이선스: Apache-2.0 - -## 4. 핵심 아키텍처 - -Playwright의 핵심 구조는 server/client/protocol로 나뉜다. - -### 4.1 Server 계층 - -위치: `packages/playwright-core/src/server` - -Server 계층은 실제 브라우저 프로세스를 띄우고, 브라우저별 프로토콜을 다루며, 페이지/프레임/네트워크/입력/저장소 같은 고수준 객체를 제공한다. - -핵심 파일: - -- `playwright.ts`: Chromium, Firefox, WebKit, Electron, Android 객체를 생성하는 루트 객체. -- `browserType.ts`: 브라우저 launch/connect/persistent context 진입점. -- `browser.ts`: 브라우저 연결과 context 생명주기. -- `browserContext.ts`: 독립 세션, 쿠키, 권한, 라우팅, tracing, storage state. -- `page.ts`: 페이지 단위 조작과 이벤트. -- `frames.ts`: frame navigation, lifecycle, DOM interaction. -- `network.ts`: request/response, routing, headers, timing. -- `fetch.ts`: API request context. -- `selectors.ts`, `frameSelectors.ts`, `dom.ts`: locator/selector 기반 DOM 조작. -- `screenshotter.ts`, `videoRecorder.ts`, `trace`: 증거 수집. -- `chromium`, `firefox`, `webkit`, `bidi`: 브라우저별 protocol adapter. -- `dispatchers`: server 객체를 channel protocol로 노출. - -온톨로지 플랫폼 재사용 포인트: - -- 동적 페이지 렌더링 후 본문/링크/메타데이터 추출 -- SPA, 무한 스크롤, 로그인 뒤 페이지 수집 -- request/response 기반 원천 URL, MIME, redirect, status 기록 -- DOM snapshot, screenshot, video, trace를 출처 증거로 보존 -- 브라우저 context 단위 격리로 사이트별 정책/쿠키/세션 분리 - -### 4.2 Client 계층 - -위치: `packages/playwright-core/src/client` - -Client 계층은 사용자가 보는 API 객체다. `Playwright`, `BrowserType`, `Browser`, `BrowserContext`, `Page`, `Locator`, `Frame`, `Request`, `Response` 등이 channel protocol 위에서 동작한다. - -핵심 파일: - -- `playwright.ts`: client-side 루트 객체와 브라우저 타입 접근. -- `browserType.ts`: `launch`, `connect`, `launchPersistentContext`. -- `browser.ts`: browser/context 관리. -- `browserContext.ts`: 쿠키, route, tracing, storage state. -- `page.ts`: navigation, screenshot, PDF, event, locator. -- `locator.ts`: 안정적인 DOM 대상 지정. -- `network.ts`: request/response 모델. -- `tracing.ts`: trace start/stop. -- `fetch.ts`: APIRequestContext. - -온톨로지 플랫폼에서는 이 client API를 감싸는 `BrowserAcquisitionService` 또는 `WebEvidenceCollector`를 두는 것이 적합하다. 원본 API를 변경하지 않고 domain workflow만 추가하면 유지보수가 쉽다. - -### 4.3 Protocol/Dispatcher 계층 - -위치: - -- `packages/playwright-core/src/protocol` -- `packages/playwright-core/src/server/dispatchers` -- `packages/protocol` - -Server 객체와 client 객체 사이의 메시지 계약이다. 브라우저 객체를 직접 넘기지 않고 channel owner/dispatcher로 추상화한다. - -재사용 의미: - -- 장기적으로 Python/FastAPI 백엔드와 Node Playwright worker를 분리할 때 이 구조를 참고할 수 있다. -- 온톨로지 플랫폼의 “수집 작업 서버”도 command/event protocol로 설계하면 browser worker를 독립 프로세스로 운용하기 쉽다. - -### 4.4 Injected 계층 - -위치: `packages/injected` - -브라우저 페이지 내부에 주입되어 selector, locator, accessibility 기반 검색, DOM 조작 보조를 수행한다. Playwright의 강점인 auto-wait와 locator 안정성은 이 계층과 server/client 조합에서 나온다. - -온톨로지 플랫폼 재사용 포인트: - -- 단순 CSS selector보다 안정적인 요소 선택 -- accessible role/name 기반 탐색 -- 클릭/입력 전 요소 actionability 확인 -- 의미 있는 DOM 후보 추출의 기반 - -### 4.5 Tools/MCP/CLI 계층 - -위치: `packages/playwright-core/src/tools` - -AI agent와 CLI가 브라우저를 조작할 수 있게 도구 단위로 기능을 쪼갠 계층이다. - -주요 하위 디렉터리: - -- `backend`: 실제 브라우저 조작 tool 구현 -- `mcp`: Model Context Protocol 서버 및 브라우저 모델 -- `cli-client`: command-line agent client -- `cli-daemon`: browser session daemon -- `dashboard`: 실행 중인 browser session 시각화 -- `trace`: trace 분석용 CLI - -`backend/tools.ts`는 다음 도구 묶음을 등록한다. - -- navigation -- screenshot -- snapshot -- form -- keyboard/mouse -- network/route -- cookies/storage/webstorage -- evaluate/runCode -- pdf/video/tracing -- tabs/dialogs/files/devtools -- verify/wait/console - -온톨로지 플랫폼에서 매우 중요하다. “LLM이 브라우저를 조작해 정보원을 탐색하고, 구조화 데이터를 추출하고, 증거를 남기는” 기능을 만들 때 이 도구 계층을 거의 그대로 감싸서 사용할 수 있다. - -## 5. 주요 기능 분석 - -### 5.1 브라우저 자동화 - -기능: - -- Chromium, Firefox, WebKit 실행 -- headless/headed 모드 -- browser context 격리 -- persistent context 지원 -- proxy, geolocation, timezone, locale, permissions, viewport, user agent 설정 -- page/frame navigation -- click, fill, type, press, hover, drag 등 입력 자동화 -- dialog, download, file chooser 처리 - -온톨로지 플랫폼 적용: - -- 동적 문서 페이지 렌더링 -- 검색 엔진/사이트 내 검색 자동화 -- 페이지 내 탭, 필터, 페이지네이션 탐색 -- 로그인 필요 지식베이스 접근 -- 사이트별 수집 프로파일 구성 - -### 5.2 Locator와 auto-wait - -기능: - -- `getByRole`, `getByText`, `getByLabel`, `getByPlaceholder`, `getByTestId` -- CSS/XPath selector -- element actionability 자동 대기 -- assertion retry -- strict locator 정책 - -적용: - -- 사이트 UI가 느리게 로딩되어도 수집 안정성 확보 -- 관리자 콘솔/문서 포털/검색 UI 자동화 -- DOM 변화가 잦은 사이트에서 selector 취약성 감소 - -### 5.3 네트워크 관찰 및 제어 - -기능: - -- request/response 이벤트 -- route interception -- HAR recording/replay -- header/cookie/postData/status/timing 접근 -- APIRequestContext -- WebSocket route 일부 지원 - -적용: - -- 수집 문서의 원천 URL, redirect chain, status code, content-type 저장 -- JSON API가 노출되는 사이트에서 DOM 대신 API 응답 직접 추출 -- 크롤링 금지/인증 오류/레이트리밋 감지 -- 동일 URL 재수집 시 변경 여부 판단 - -### 5.4 스냅샷, 스크린샷, 비디오, trace - -기능: - -- screenshot -- video recording -- trace start/stop -- DOM snapshot -- console/network/action timeline 기록 -- trace viewer UI - -적용: - -- 온톨로지 엔티티/관계 추출의 근거 보존 -- LLM 추출 결과 검수 화면 제공 -- “왜 이 관계가 생성되었는가”를 클릭 가능한 증거로 제시 -- 수집 실패 재현 및 디버깅 - -### 5.5 PDF와 문서화 - -기능: - -- Chromium 기반 `page.pdf` -- screenshot 기반 시각 증거 - -적용: - -- 웹 문서를 PDF evidence artifact로 저장 -- 온톨로지 버전별 출처 snapshot 생성 - -### 5.6 테스트 러너 - -위치: `packages/playwright/src` - -기능: - -- test/expect API -- fixture -- parallel worker -- retry, timeout, shard -- project matrix -- reporter: list, line, dot, json, junit, html, blob, github -- webServer plugin -- watch/UI mode - -온톨로지 플랫폼에서는 제품 테스트뿐 아니라 “수집 recipe 검증”에 사용할 수 있다. 예를 들어 특정 사이트 수집 recipe가 정상적으로 title/body/date/source evidence를 얻는지 Playwright Test로 검증할 수 있다. - -### 5.7 Recorder와 codegen - -기능: - -- 브라우저 조작을 코드로 생성 -- selector 후보 생성 -- 사용자의 실제 클릭/입력을 시나리오로 변환 - -적용: - -- 비개발자가 사이트 수집 절차를 녹화해 recipe 초안 생성 -- 수집 자동화 script를 빠르게 제작 -- 로그인, 검색, 필터, 다운로드 흐름을 저장 - -### 5.8 MCP와 AI Agent 브라우저 조작 - -기능: - -- MCP server 제공 -- 접근성 tree/snapshot 기반 agent interaction -- navigation, click, type, screenshot, network, storage 등 tool schema -- extension/CDP relay 구조 일부 포함 - -적용: - -- LLM 기반 웹 탐색 agent -- 온톨로지 후보 개념/관계 발견을 위한 반자동 탐색 -- 사람이 지시한 목표를 브라우저 조작 task로 변환 -- “페이지에서 제품군/속성/관계 후보를 찾아라” 같은 agent workflow - -### 5.9 Reporter와 Viewer - -기능: - -- HTML reporter -- trace viewer -- timeline, action list, network tab, console tab, snapshot tab -- test result drill-down - -적용: - -- 수집 작업 리포트 UI의 기본 소스로 사용 가능 -- 추출 품질, 실패 URL, 에러, 네트워크 로그, 스크린샷을 한 화면에서 검토 -- provenance/evidence viewer 구현 참고 - -## 6. 범용 온톨로지 구축 플랫폼에 필요한 기능명세 - -아래 명세는 Playwright 원본을 가능한 한 변형 없이 사용하고, 우리 플랫폼 계층에서 orchestration과 domain logic을 얹는 방향이다. - -### 6.1 브라우저 수집 엔진 - -목적: 동적 웹 페이지를 안정적으로 열고, DOM/텍스트/네트워크/시각 증거를 수집한다. - -기능 요구사항: - -- URL 단위 수집 작업 생성 -- browser type 선택: chromium 기본, 필요 시 firefox/webkit -- headless/headed 선택 -- context 설정: viewport, locale, timezone, userAgent, proxy, geolocation, permissions -- navigation timeout, action timeout 설정 -- 페이지 load strategy 설정: `load`, `domcontentloaded`, `networkidle` -- redirect chain 기록 -- final URL 기록 -- HTTP status, response headers, content-type 기록 -- DOM HTML 저장 -- innerText/textContent 저장 -- screenshot 저장 -- 선택적 PDF 저장 -- 선택적 trace 저장 -- console error/warning 기록 -- request failure 기록 -- cookie/storage state 저장 및 재사용 - -권장 원본 사용: - -- `playwright-core/src/client/page.ts` -- `browserContext.ts` -- `network.ts` -- `tracing.ts` -- `screenshotter.ts` -- `fetch.ts` - -플랫폼 래퍼 예시: - -- `BrowserAcquisitionService.collect(url, profile)` -- `EvidenceBundle` -- `BrowserSessionProfile` -- `NetworkEvidence` -- `DomEvidence` - -### 6.2 사이트 탐색 및 링크 발견 - -목적: 온톨로지 구축에 필요한 문서/목록/상세 페이지 후보를 발견한다. - -기능 요구사항: - -- 시작 URL seed 등록 -- 동일 도메인/허용 도메인 링크 추출 -- link text, href, role, bounding box, surrounding text 기록 -- canonical URL 정규화 -- 중복 URL 제거 -- robots/policy는 플랫폼 정책 계층에서 처리 -- 페이지네이션 버튼 탐색 -- 검색어 기반 사이트 내부 검색 수행 -- 무한 스크롤 페이지 처리 -- sitemap/API endpoint 발견은 별도 모듈과 결합 - -권장 원본 사용: - -- locator -- frame/page evaluate -- network request observation -- tools backend `navigate`, `snapshot`, `mouse`, `keyboard`, `wait` - -### 6.3 구조화 추출 준비 - -목적: LLM/규칙 기반 추출기가 쓰기 좋은 입력을 만든다. - -기능 요구사항: - -- 본문 후보 영역 탐지 -- 제목, heading hierarchy 추출 -- table/list/card 구조 추출 -- form/search/filter UI 추출 -- image alt/caption/source 추출 -- metadata: title, description, og tags, schema.org JSON-LD 추출 -- accessibility snapshot 저장 -- network JSON 응답 후보 저장 -- DOM path와 locator candidate 저장 - -권장 원본 사용: - -- injected selector/locator 구조 -- page accessibility snapshot 계열 도구 -- `snapshot` backend tool -- evaluate/runCode tool - -주의: - -- Playwright는 온톨로지 추출기가 아니다. 엔티티/관계/속성 스키마 추출은 플랫폼의 별도 AI extraction layer가 담당해야 한다. -- Playwright는 “신뢰도 높은 웹 상태와 증거를 제공하는 하부 엔진”으로 두는 것이 맞다. - -### 6.4 Agent 기반 웹 조사 - -목적: LLM이 브라우저를 조작하며 지식 후보를 찾고 검증하게 한다. - -기능 요구사항: - -- MCP tool 목록을 플랫폼 agent에게 제공 -- agent별 browser context 격리 -- 세션별 action log 저장 -- agent action마다 screenshot/snapshot 선택 저장 -- 허용 도메인, 다운로드, 파일 업로드, 외부 이동 제한 -- 사람이 중간에 개입 가능한 headed/session dashboard 제공 -- agent task 결과를 evidence bundle과 연결 - -권장 원본 사용: - -- `packages/playwright-core/src/tools/backend` -- `packages/playwright-core/src/tools/mcp` -- `packages/playwright-core/src/tools/cli-daemon` -- `packages/playwright-core/src/tools/dashboard` - -플랫폼 기능명: - -- `AgentBrowserSession` -- `BrowserToolGateway` -- `AgentEvidenceRecorder` -- `HumanReviewDashboard` - -### 6.5 수집 Recipe 녹화 및 재생 - -목적: 사용자가 사이트별 수집 절차를 만들고 반복 실행한다. - -기능 요구사항: - -- 브라우저 조작 녹화 -- 생성된 locator/code 확인 -- recipe step 편집 -- 변수화: 검색어, 카테고리, 기간, 페이지 수 -- replay 실행 -- 실패 step에서 screenshot/trace 제공 -- recipe version 관리 - -권장 원본 사용: - -- `packages/recorder` -- `packages/playwright-core/src/server/recorder` -- `packages/playwright-core/src/server/codegen` - -플랫폼 Recipe 모델: - -- `open(url)` -- `click(locator)` -- `fill(locator, value)` -- `press(key)` -- `waitFor(condition)` -- `extract(targetSpec)` -- `paginate(strategy)` -- `saveEvidence(policy)` - -### 6.6 증거/출처 관리 - -목적: 온톨로지 결과의 출처와 재현성을 보장한다. - -기능 요구사항: - -- 모든 추출 결과는 source URL과 evidence id를 가진다. -- evidence bundle에는 HTML, text, screenshot, network summary, trace path를 포함한다. -- relationship triple마다 근거 DOM locator 또는 text span을 연결한다. -- trace viewer 또는 유사 UI에서 action/network/snapshot을 열람한다. -- 재수집 시 이전 evidence와 diff한다. - -권장 원본 사용: - -- tracing -- HAR -- trace viewer -- html reporter UI 구조 -- network events - -플랫폼 데이터 모델: - -- `EvidenceBundle(id, url, capturedAt, browserProfile, artifacts)` -- `Artifact(type, path, mime, hash)` -- `ExtractionClaim(entityId, predicate, object, evidenceRefs, confidence)` -- `SourceSpan(evidenceId, selector, textStart, textEnd, quote)` - -### 6.7 수집 품질 검증 - -목적: 수집 및 추출 pipeline의 신뢰성을 자동 검증한다. - -기능 요구사항: - -- URL 접근 성공률 -- 본문 길이 최소 기준 -- title/heading 존재 여부 -- HTTP status allowlist -- screenshot blank 여부 -- 주요 selector 존재 여부 -- JSON-LD/schema.org 존재 여부 -- extraction output schema validation -- 실패 시 retry, fallback browser, fallback wait strategy - -권장 원본 사용: - -- Playwright Test runner -- expect matcher -- html reporter -- trace on retry - -플랫폼 기능명: - -- `CrawlerRecipeTest` -- `EvidenceQualityGate` -- `ExtractionRegressionSuite` - -## 7. 재사용 우선순위 - -### 1순위: 거의 그대로 사용 - -- npm 패키지 `playwright` 또는 `playwright-core` -- browser/page/context/network/locator/tracing API -- screenshot/PDF/HAR/trace 기능 -- storage state 재사용 -- MCP/CLI backend tool 개념 - -이 영역은 원본 수정 없이 wrapper를 작성하는 방식이 적합하다. - -### 2순위: 일부 UI/구조 차용 - -- trace viewer -- html reporter -- dashboard -- recorder/codegen - -이 영역은 UI와 데이터 모델이 Playwright 테스트 중심이라 그대로 붙이기보다 “evidence viewer”, “collection report”, “recipe recorder”로 재명명하고 데이터 adapter를 두는 것이 좋다. - -### 3순위: 참고만 권장 - -- browser patches -- component test packages -- browser package publishing logic -- Playwright 자체 protocol generator/build system - -온톨로지 플랫폼에는 과하고 유지보수 비용이 높다. - -## 8. 통합 설계안 - -권장 구조: - -```text -Ontology Platform - API / Job Orchestrator - CollectionJob - ExtractionJob - ValidationJob - - Browser Automation Layer - Playwright wrapper - Browser session pool - Site profile manager - Agent tool gateway - - Evidence Layer - HTML/Text/Screenshot/PDF/Trace/HAR store - Evidence metadata DB - Hash/version manager - - Extraction Layer - DOM cleaner - JSON-LD parser - Table/list extractor - LLM extractor - Ontology mapper - - Ontology Layer - Entity model - Relation model - Schema/versioning - Graph DB adapter - - Review UI - Evidence viewer - Trace viewer adapter - Extraction diff - Human validation workflow -``` - -Playwright는 `Browser Automation Layer`와 `Evidence Layer`의 핵심 엔진으로 둔다. 온톨로지 의미 추론, 스키마 정렬, 엔티티 병합, 그래프 저장은 별도 계층으로 분리한다. - -## 9. 구체 API 명세 초안 - -### 9.1 CollectionProfile - -```ts -type CollectionProfile = { - id: string; - browser: 'chromium' | 'firefox' | 'webkit'; - headless: boolean; - viewport?: { width: number; height: number }; - locale?: string; - timezoneId?: string; - userAgent?: string; - proxy?: { - server: string; - username?: string; - password?: string; - }; - permissions?: string[]; - storageStatePath?: string; - navigationTimeoutMs: number; - actionTimeoutMs: number; - trace: 'off' | 'on' | 'retain-on-failure'; - screenshot: 'off' | 'page' | 'full-page'; - pdf: boolean; - har: boolean; -}; -``` - -### 9.2 CollectionJob - -```ts -type CollectionJob = { - id: string; - seeds: string[]; - profileId: string; - allowedDomains: string[]; - maxDepth: number; - maxPages: number; - crawlMode: 'single-page' | 'same-domain' | 'recipe' | 'agent'; - recipeId?: string; - agentGoal?: string; - evidencePolicy: EvidencePolicy; -}; -``` - -### 9.3 EvidencePolicy - -```ts -type EvidencePolicy = { - keepHtml: boolean; - keepText: boolean; - keepScreenshot: boolean; - keepPdf: boolean; - keepTrace: boolean; - keepHar: boolean; - keepNetworkSummary: boolean; - hashArtifacts: boolean; -}; -``` - -### 9.4 EvidenceBundle - -```ts -type EvidenceBundle = { - id: string; - jobId: string; - url: string; - finalUrl: string; - capturedAt: string; - status?: number; - contentType?: string; - title?: string; - artifacts: EvidenceArtifact[]; - network: NetworkEvidence[]; - console: ConsoleEvidence[]; - extractionInput: ExtractionInput; -}; -``` - -### 9.5 ExtractionInput - -```ts -type ExtractionInput = { - title?: string; - headings: Array<{ level: number; text: string; selector?: string }>; - mainText: string; - links: Array<{ text: string; href: string; selector?: string }>; - tables: Array<{ selector?: string; rows: string[][] }>; - jsonLd: unknown[]; - metadata: Record; - accessibilitySnapshot?: unknown; -}; -``` - -### 9.6 OntologyExtractionResult - -```ts -type OntologyExtractionResult = { - evidenceBundleId: string; - entities: ExtractedEntity[]; - relations: ExtractedRelation[]; - attributes: ExtractedAttribute[]; - warnings: string[]; -}; -``` - -### 9.7 ExtractedEntity - -```ts -type ExtractedEntity = { - id: string; - label: string; - typeCandidates: string[]; - aliases: string[]; - sourceRefs: SourceRef[]; - confidence: number; -}; -``` - -### 9.8 ExtractedRelation - -```ts -type ExtractedRelation = { - subjectId: string; - predicate: string; - objectIdOrValue: string; - relationType: 'entity-entity' | 'entity-value'; - sourceRefs: SourceRef[]; - confidence: number; -}; -``` - -### 9.9 SourceRef - -```ts -type SourceRef = { - evidenceBundleId: string; - artifactType: 'html' | 'text' | 'screenshot' | 'pdf' | 'trace' | 'network'; - selector?: string; - textQuote?: string; - startOffset?: number; - endOffset?: number; - screenshotRegion?: { x: number; y: number; width: number; height: number }; -}; -``` - -## 10. 주요 워크플로우 명세 - -### 10.1 단일 URL 수집 - -1. CollectionJob 생성 -2. CollectionProfile 로드 -3. Playwright browser/context/page 생성 -4. URL 이동 -5. response/status/final URL 기록 -6. DOM, text, metadata, link, JSON-LD 추출 -7. screenshot/PDF/trace/HAR 저장 -8. EvidenceBundle 생성 -9. ExtractionInput 생성 -10. Ontology extraction queue로 전달 - -### 10.2 사이트 탐색 수집 - -1. seed URL 수집 -2. 링크 후보 추출 -3. URL canonicalization 및 domain filter -4. 우선순위 큐에 추가 -5. maxDepth/maxPages까지 반복 -6. 각 페이지 EvidenceBundle 저장 -7. 중복 본문/중복 URL 제거 -8. extraction batch 생성 - -### 10.3 Agent 조사 - -1. 사용자가 조사 목표 입력 -2. AgentBrowserSession 생성 -3. MCP/backend tools 제공 -4. agent가 navigate/search/click/snapshot 반복 -5. 중요 페이지에서 evidence 저장 -6. agent가 후보 entity/relation/sourceRefs 제안 -7. 사람이 evidence viewer에서 검수 -8. 승인된 claim만 ontology graph에 반영 - -### 10.4 Recipe 생성 - -1. 사용자가 headed browser로 사이트 접속 -2. recorder가 행동 기록 -3. codegen/locator 후보 생성 -4. 플랫폼 Recipe DSL로 변환 -5. 변수와 반복/페이지네이션 설정 -6. 샘플 실행으로 검증 -7. recipe version 저장 - -## 11. 변경 없이 사용 가능한 코드/개념 - -- Playwright npm public API -- browser context isolation -- locator and auto-wait -- tracing API -- storage state -- route/network observation -- screenshot/PDF -- API request context -- HTML reporter/trace viewer의 UI 패턴 -- MCP backend tool 분해 방식 - -## 12. 수정 또는 Adapter가 필요한 영역 - -- Playwright test result 중심 데이터 모델을 ontology evidence 중심 모델로 변환 -- trace viewer를 EvidenceBundle과 연결하는 adapter -- recorder output을 플랫폼 Recipe DSL로 변환 -- MCP tool 권한/보안 정책 -- 수집 대상 도메인 제한 -- 다운로드/파일 시스템 접근 제한 -- 대량 크롤링 스케줄링, 큐, retry, backpressure -- robots/약관/레이트리밋 정책 - -## 13. 위험요소와 주의사항 - -- Playwright 원본 전체를 fork해서 수정하면 유지보수 비용이 매우 커진다. -- 브라우저 바이너리와 patch 관리까지 직접 들고 가는 것은 권장하지 않는다. -- 크롤러 규모가 커지면 browser context/page pool 관리가 필요하다. -- trace/video/screenshot은 저장소 비용이 크므로 evidence policy가 필요하다. -- 로그인 세션 저장은 보안 민감 정보이므로 암호화와 접근 제어가 필요하다. -- LLM agent에게 unrestricted browser tool을 주면 외부 이동/다운로드/입력 위험이 있다. -- Playwright는 추출 의미론을 보장하지 않는다. 온톨로지 품질은 extraction/validation layer에서 관리해야 한다. - -## 14. 구현 로드맵 - -### Phase 1. Playwright 수집 래퍼 - -- `BrowserAcquisitionService` 작성 -- 단일 URL HTML/text/screenshot/network summary 수집 -- storage state 지원 -- EvidenceBundle 저장 - -### Phase 2. 구조화 입력 생성 - -- heading/link/table/jsonLd/metadata 추출 -- 본문 후보 추출 -- extraction input schema 고정 -- 품질 gate 추가 - -### Phase 3. Recipe 기반 수집 - -- Playwright action step DSL 정의 -- recorder/codegen 연계 검토 -- replay 및 실패 trace 저장 - -### Phase 4. Agent 브라우저 - -- MCP/backend tools adapter -- agent session isolation -- action log/evidence 자동 연결 -- 도메인/권한 policy 적용 - -### Phase 5. Evidence Viewer - -- trace viewer 또는 유사 UI 통합 -- screenshot/DOM/text/source span 연결 -- relation claim 검수 화면 - -## 15. 결론 - -이 프로젝트는 범용 온톨로지 구축 플랫폼의 “웹 기반 지식 수집 엔진”으로 매우 적합하다. 다만 원본을 플랫폼 내부로 깊게 fork하기보다, Playwright는 가능한 한 공식 API와 tool 계층을 그대로 사용하고, 우리 쪽에서 다음 계층을 추가하는 방식이 좋다. - -- 수집 orchestration -- evidence data model -- ontology extraction input normalization -- LLM/규칙 기반 entity/relation extraction -- graph persistence -- human review workflow - -즉, Playwright는 “브라우저로 세상을 안정적으로 관찰하고 증거를 남기는 엔진”으로 쓰고, 범용 온톨로지 플랫폼은 그 위에서 “관찰을 지식 그래프로 바꾸는 시스템”으로 설계하는 것이 가장 현실적이다. diff --git a/README_KO.md b/README_KO.md deleted file mode 100644 index f21fa92..0000000 --- a/README_KO.md +++ /dev/null @@ -1,452 +0,0 @@ -# 🚀 온톨로지 시스템 구축 플랫폼 - -**웹 데이터에서 지능형 지식 그래프를 자동 구축하는 엔드-투-엔드 플랫폼** - -``` -웹 → 추출 → 검증 → 그래프 저장 → 지능화 → API 공개 → LLM 연계 -``` - ---- - -## 📊 플랫폼 현황 (Phase 0-6) - -| Phase | 기능 | 상태 | 테스트 | -|-------|------|------|--------| -| **0** | URL 텍스트 추출 | ✅ 완료 | ✅ 통과 | -| **1** | 동적 페이지 크롤링 | ✅ 완료 | ✅ 통과 | -| **2** | 크롤링 프로필 지원 | ✅ 완료 | ✅ 통과 | -| **3** | 데이터 검증 + 온톨로지 변환 | ✅ 완료 | ✅ 통과 | -| **4** | Neo4j 그래프 저장 + 벡터 임베딩 | ✅ 완료 | ✅ 통과 | -| **5.0** | RDF 변환 + Entity Resolver | ✅ 완료 | ✅ 7 테스트 | -| **5.1** | Subgraph + Pattern Matching | ✅ 완료 | ✅ 16 테스트 | -| **5.2** | Graph Analytics (중심성, 커뮤니티) | ✅ 완료 | ✅ 8 테스트 | -| **6** | REST API + GraphQL + RAG | ✅ 완료 | ✅ 7 테스트 | - -**총 테스트**: 45/45 통과 ✅ - ---- - -## 🎯 주요 기능 - -### 1️⃣ 자동 데이터 수집 (Phase 0-2) -```bash -# 웹에서 데이터 자동 추출 -$ ontology extract --url https://example.com -``` - -- ✅ 정적 페이지 (HTTP) -- ✅ 동적 페이지 (JavaScript) -- ✅ 메타데이터 + 본문 추출 - -### 2️⃣ 스마트 검증 & 온톨로지 변환 (Phase 3) -```bash -# 데이터 자동 검증 및 온톨로지 변환 -$ ontology validate --input data.json --output ontology.rdf -``` - -- ✅ 엔티티 추출 (NER) -- ✅ 관계 추출 (Relation Extraction) -- ✅ RDF 트리플 생성 -- ✅ 신뢰도 점수 계산 - -### 3️⃣ Neo4j 지식 그래프 (Phase 4) -``` -저장된 그래프 특성: - - 10K+ 노드 지원 - - 벡터 유사도 검색 - - 관계 중심의 쿼리 -``` - -```bash -# 그래프에 온톨로지 저장 -$ ontology store --triples ontology.rdf --db neo4j://localhost:7687 -``` - -### 4️⃣ 그래프 지능화 (Phase 5) - -#### 5.0: 의미적 중복 제거 -``` -Before: "Apple", "APPLE Inc", "Apple Computer" (3개 엔티티) -After: Apple (1개) + aliases: [APPLE, APPLE Inc, ...] -``` - -#### 5.1: 패턴 분석 -```python -# 경로 찾기 -paths = await matcher.find_paths(1, 5, max_length=5) -# → Apple → produces → iPhone → has_feature → Face ID - -# 순환 감지 -cycles = await matcher.find_cycles() -# → 논리적 오류 자동 발견 - -# 모티프 감지 -motifs = await matcher.find_motifs("triangle") -# → 빈번한 구조 패턴 식별 -``` - -#### 5.2: 분석 -```python -# 중심성 계산 -central = await analytics.calculate_centrality("pagerank") -# → 가장 중요한 엔티티 식별 - -# 커뮤니티 감지 -communities = await analytics.detect_communities() -# → 자동 그룹화 (products, people, locations) - -# 통계 -stats = await analytics.get_graph_statistics() -# → 밀도, 직경, 연결성 분석 -``` - -### 5️⃣ REST API & GraphQL (Phase 6) - -#### REST API (10개 엔드포인트) -```bash -# Entity 중복 해결 -POST /api/v1/graph/resolve -Body: {"entities": [...]} - -# 부분 그래프 추출 -GET /api/v1/graph/subgraph/neighborhood/{id}?hops=2 - -# 경로 찾기 -POST /api/v1/graph/patterns/paths -Body: {"start_id": 1, "end_id": 5} - -# 중심성 계산 -POST /api/v1/graph/analytics/centrality -Body: {"centrality_type": "pagerank"} - -# RAG 컨텍스트 -POST /api/v1/rag/query -Body: {"query": "Apple의 제품은?"} -``` - -#### GraphQL 지원 -```graphql -{ - entity(id: 1) { - label - type - neighbors(hops: 2) { label } - } -} -``` - -### 6️⃣ RAG 파이프라인 (Phase 6) - -``` -사용자 쿼리: "Apple의 제품은?" - ↓ -그래프에서 자동 검색 + 컨텍스트 추출 - ↓ -LLM 프롬프트 자동 생성: - "You are a helpful assistant. - - Knowledge Graph Context: - - Apple produces iPhone, iPad, Mac - - Apple was founded by Steve Jobs - - Apple is headquartered in Cupertino - - Question: Apple의 제품은?" - ↓ -LLM 응답 (외부 서비스): "Apple의 주요 제품은..." -``` - ---- - -## 🛠 설치 및 실행 - -### 사전 요구사항 -```bash -Python 3.9+ -Neo4j 5.0+ -Redis (선택사항) -``` - -### 1단계: 설치 -```bash -git clone -cd ontology_platform -pip install -r requirements.txt -``` - -### 2단계: 설정 -```bash -# Neo4j 연결 -export NEO4J_URI=bolt://localhost:7687 -export NEO4J_USER=neo4j -export NEO4J_PASSWORD=ontology123 -``` - -### 3단계: 플랫폼 실행 -```bash -# 방법 1: CLI로 온톨로지 구축 -python -m ontology_platform.cli \ - --url https://example.com \ - --validate \ - --store-neo4j - -# 방법 2: API 서버 시작 -python -m uvicorn ontology_platform.api.phase6_app:app --reload -# → http://localhost:8000/docs -``` - ---- - -## 📈 성능 - -| 작업 | 규모 | 시간 | -|------|------|------| -| 웹 크롤링 | 1 URL | 5-30초 | -| 데이터 검증 | 1000 엔티티 | < 2초 | -| 벡터 임베딩 | 10K 엔티티 | 4초 | -| 배치 저장 | 100K 노드/에지 | 28초 | -| 부분 그래프 추출 | 2-hop | < 200ms | -| 경로 찾기 | max_length=5 | < 300ms | -| 중심성 계산 | top_n=100 | < 600ms | -| RAG 쿼리 | 벡터 검색 | < 1초 | - ---- - -## 💡 사용 예제 - -### 예제 1: 기술 회사 온톨로지 -```bash -# 1. 데이터 수집 -$ ontology extract --url https://apple.com - -# 2. 검증 및 변환 -$ ontology validate --input apple_data.json - -# 3. 그래프 저장 -$ ontology store --triples apple.rdf - -# 4. 분석 -$ curl http://localhost:8000/api/v1/graph/analytics/influential -# → Apple, iPhone, iPad, Tim Cook 등 중요 엔티티 - -# 5. RAG 쿼리 -$ curl -X POST http://localhost:8000/api/v1/rag/query \ - -H "Content-Type: application/json" \ - -d '{"query": "Apple의 제품은?"}' -# → 자동으로 LLM 프롬프트 생성 -``` - -### 예제 2: 의료 온톨로지 -```python -from ontology_platform.platform import OntologyPlatform - -# 플랫폼 초기화 -platform = OntologyPlatform() - -# 1. 의료 사이트 크롤링 -data = await platform.extract_from_urls([ - "https://fda.gov", - "https://medline.gov" -]) - -# 2. 약물-질병-치료 관계 추출 -ontology = await platform.validate_and_convert(data) - -# 3. Neo4j에 저장 -await platform.store_to_neo4j(ontology) - -# 4. 의약 상호작용 분석 -graph = platform.get_graph() -interactions = await graph.find_cycles() # 부정적 상호작용 감지 - -# 5. API로 공개 -# GET /api/drug/{id}/interactions -# → 의사용 의약품 상호작용 정보 -``` - ---- - -## 📚 문서 - -| 문서 | 내용 | -|------|------| -| **ONTOLOGY_PLATFORM_OVERVIEW.md** | 플랫폼 전체 개요 및 아키텍처 | -| **PHASE_5_SUMMARY.md** | Phase 5.0-5.2 GraphRAG 상세 | -| **PHASE_6_API_GUIDE.md** | Phase 6 REST API/GraphQL/RAG 완전 레퍼런스 | -| **README.md** (English) | English version | - ---- - -## 🔄 워크플로우 - -``` -┌─────────────────────────────────────────┐ -│ Ontology Platform Workflow │ -├─────────────────────────────────────────┤ -│ │ -│ 1️⃣ 웹 URL → 텍스트 추출 │ -│ (Phase 0-2: Extraction) │ -│ │ -│ 2️⃣ 텍스트 → 검증 + 온톨로지 변환 │ -│ (Phase 3: Validation) │ -│ │ -│ 3️⃣ 온톨로지 → Neo4j 그래프 저장 │ -│ (Phase 4: Storage) │ -│ │ -│ 4️⃣ 그래프 분석 + 최적화 │ -│ (Phase 5: Intelligence) │ -│ │ -│ 5️⃣ API로 공개 + LLM 연계 │ -│ (Phase 6: API & Integration) │ -│ │ -│ 6️⃣ 실시간 응답 (Future) │ -│ (Phase 7-8: Enhancements) │ -│ │ -└─────────────────────────────────────────┘ -``` - ---- - -## 🎓 온톨로지란? - -**온톨로지**: 어떤 영역의 개념, 속성, 관계를 형식화한 구조 - -``` -의료 온톨로지 예: - Entities: Disease, Drug, Symptom - Relations: treats, causes, prevents - Properties: severity, dosage, sideEffects - -Example: - Aspirin --treats--> Headache - Aspirin --has_sideEffect--> Gastric_Bleeding -``` - ---- - -## 🚀 다음 단계 - -### Phase 7: LLM 엔드투엔드 통합 -``` -목표: LLM을 플랫폼에 직접 통합 -- 스트리밍 응답 (토큰 실시간 전달) -- 응답 캐싱 (반복 질문 < 50ms) -- 자동 문맥 관리 -``` - -### Phase 8: 엔터프라이즈 기능 -``` -목표: 대규모 운영 지원 -- 멀티테넌트 (여러 조직 동시 지원) -- 실시간 그래프 업데이트 -- 변경 이력 추적 (감사 로그) -``` - ---- - -## 📞 지원 - -### 문제 해결 -```bash -# Neo4j 연결 확인 -curl http://localhost:8000/health - -# API 문서 확인 -http://localhost:8000/docs - -# 로그 확인 -tail -f logs/ontology.log -``` - -### 커뮤니티 -- GitHub Issues: 버그 리포트 -- GitHub Discussions: 질문 및 제안 - ---- - -## 📝 라이선스 - -MIT License - 자유로운 사용, 수정, 배포 가능 - ---- - -## 💪 기여 - -Pull Request 환영합니다! - -```bash -1. Fork -2. Feature branch 생성 (git checkout -b feature/amazing-feature) -3. Commit (git commit -m "Add amazing feature") -4. Push (git push origin feature/amazing-feature) -5. Pull Request 생성 -``` - ---- - -## 🏆 주요 성과 - -- ✅ **45/45 테스트 통과** (100%) -- ✅ **6단계 완성** (Phase 0-6) -- ✅ **3,500+ 라인 코드** (고품질 구현) -- ✅ **10개 REST API** + GraphQL + RAG 파이프라인 -- ✅ **성능**: 10K+ 노드 그래프 < 1초 응답 -- ✅ **확장성**: 100K 노드/에지 < 30초 저장 - ---- - -## 📊 통계 - -| 항목 | 수치 | -|------|------| -| 구현 파일 | 15+ | -| 테스트 파일 | 8+ | -| 테스트 케이스 | 45 | -| API 엔드포인트 | 10 (REST) + GraphQL | -| 문서 페이지 | 2,000+ 라인 | -| 총 코드 | 3,500+ 라인 | - ---- - -## 🎯 플랫폼이 해결하는 문제 - -1. **정보 구조화**: 웹의 비구조화 정보 → 구조화된 지식 -2. **중복 제거**: 자동 엔티티 통합 (semantic deduplication) -3. **품질 보장**: 자동 검증 및 분석 -4. **지능형 검색**: 그래프 기반 의미 검색 -5. **LLM 연계**: 구조화된 컨텍스트로 더 나은 응답 - ---- - -## 🌟 특징 - -✨ **자동화**: 클릭 몇 번으로 온톨로지 구축 -✨ **확장성**: 수백만 개 노드 지원 -✨ **지능화**: 자동 중복 제거, 패턴 분석 -✨ **현대적**: REST, GraphQL, LLM 통합 -✨ **문서화**: 완전한 API 문서 및 가이드 - ---- - -## 📈 로드맵 - -``` -2026년 Q2 Phase 0-6 완성 ✅ -2026년 Q3 Phase 7 (LLM 스트리밍) 🚀 -2026년 Q4 Phase 8 (멀티테넌트) 📅 -``` - ---- - -**버전**: 0.6.0 -**상태**: Production Ready -**마지막 업데이트**: 2026-05-14 - ---- - -**지금 시작하세요!** 👇 - -```bash -python -m uvicorn ontology_platform.api.phase6_app:app --reload -``` - -🎉 온톨로지 시스템 구축 플랫폼에 오신 것을 환영합니다! diff --git a/README_old.md b/README_old.md deleted file mode 100644 index 72b9228..0000000 --- a/README_old.md +++ /dev/null @@ -1,234 +0,0 @@ -# Ontology Platform - -온톨로지 플랫폼은 웹에서 구조화된 지식(엔티티/관계)을 자동 추출, 검증, 저장하는 고속 시스템입니다. - -**Phase 0-4** 전체 구현 완료 | 추출(10초) → 검증(<100ms) → 그래프 저장 → 벡터 검색 - -## 🚀 빠른 시작 - -### 1. 설치 - -```bash -# 기본 설치 (Phase 0-1: 추출) -pip install fastapi uvicorn pydantic trafilatura httpx - -# Phase 2 추가 (동적 페이지) -pip install crawl4ai - -# Phase 4 추가 (Neo4j) -pip install neo4j sentence-transformers -``` - -### 2. Phase 0-1만 사용 (가장 간단) - -```bash -# API 서버 시작 -python -m uvicorn ontology_platform.ont_platform.api.phase0_app:app --reload - -# URL에서 추출 -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://example.com" -``` - -### 3. Phase 4 (그래프 검색) 포함 - -```bash -# Neo4j 시작 -docker-compose -f docker-compose.neo4j.yml up -d - -# API 서버 시작 -python -m uvicorn ontology_platform.ont_platform.api.phase0_app:app --reload - -# 추출 → 수집 → 검색 -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://example.com" -curl -X POST "http://localhost:8000/api/v1/search/ingest" -d '{"entities": [...], "relations": [...]}' -curl "http://localhost:8000/api/v1/search/vector?query=machine+learning" -``` - -## 📋 Phase별 기능 - -| Phase | 기능 | 시간 | 상태 | -|-------|------|------|------| -| 0-1 | HTML 추출 (Trafilatura) | 10-15초 | ✅ | -| 2 | 동적 페이지 (Crawl4AI) | 20-30초 | ✅ | -| 3A | 경량 검증 (Pydantic) | <100ms | ✅ | -| 3B | SPARQL 검증 | <500ms | ✅ | -| 4 | Neo4j + 벡터 검색 | 50-200ms | ✅ | - -## 🎯 사용 예시 - -### 예시 1: 기본 추출 (10초) - -```bash -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://wikipedia.org/wiki/Python" -``` - -응답: -```json -{ - "url": "https://wikipedia.org/wiki/Python", - "title": "Python - Wikipedia", - "entities": [ - { - "id": "E_1", - "label": "Python", - "type": "ProgrammingLanguage", - "confidence": 0.95 - } - ], - "relations": [...], - "extraction_time_sec": 9.5, - "validation_passed": true -} -``` - -### 예시 2: 동적 페이지 (25초) - -```bash -curl -X POST "http://localhost:8000/api/v1/extract/url?url=https://app.example.com&profile=dynamic_page" -``` - -### 예시 3: 그래프 수집 + 검색 - -```bash -# 1. 추출 -RESULT=$(curl -s -X POST "http://localhost:8000/api/v1/extract/url?url=https://example.com") - -# 2. Neo4j에 수집 -curl -X POST "http://localhost:8000/api/v1/search/ingest" \ - -H "Content-Type: application/json" \ - -d "{\"entities\": $(echo $RESULT | jq '.entities'), \"relations\": $(echo $RESULT | jq '.relations')}" - -# 3. 벡터 검색 -curl "http://localhost:8000/api/v1/search/vector?query=programming&limit=10" - -# 4. 그래프 통계 -curl "http://localhost:8000/api/v1/search/stats" - -# 5. 엔티티 이웃 -curl "http://localhost:8000/api/v1/search/entity/E_1?depth=1" -``` - -## 🔧 설정 - -### Phase 선택 (validators.py) - -```python -# 경량 검증 (기본) -guard = OntologyGuard(validator_type="lightweight") - -# SPARQL 검증 -guard = OntologyGuard(validator_type="ontocast") -``` - -### Neo4j 연결 (neo4j_adapter.py) - -```python -# 기본값 -config = Neo4jConfig() # localhost:7687 - -# 커스텀 -config = Neo4jConfig( - uri="bolt://custom-host:7687", - username="user", - password="pass", - database="mydb" -) -adapter = Neo4jAdapter(config=config) -``` - -## 📊 API 문서 - -서버 시작 후: -- **Swagger UI**: http://localhost:8000/docs -- **ReDoc**: http://localhost:8000/redoc - -### 주요 엔드포인트 - -``` -POST /api/v1/extract/url 추출 -GET /api/v1/search/stats 통계 -POST /api/v1/search/vector 벡터 검색 -GET /api/v1/search/entity/{id} 이웃 탐색 -POST /api/v1/search/ingest 그래프 수집 -``` - -## 🧪 테스트 - -```bash -# Phase 0-1 -python test_phase0_extraction.py - -# Phase 2 -python test_phase2_crawl.py - -# Phase 3A -python test_phase3_validation.py - -# Phase 3B -python test_phase3_option_b.py - -# Phase 4 -python test_phase4_integration.py -``` - -## 📦 의존성 - -- **FastAPI**: API 프레임워크 -- **Trafilatura**: HTML 추출 -- **Crawl4AI**: 동적 크롤링 (선택) -- **Pydantic**: 데이터 검증 -- **Neo4j**: 그래프 DB (선택) -- **SentenceTransformers**: 벡터 임베딩 (선택) - -## 🐳 Docker - -```bash -# Neo4j만 -docker-compose -f docker-compose.neo4j.yml up -d - -# 전체 스택 (향후) -docker-compose up -d -``` - -## 📚 상세 문서 - -- [구현 요약](IMPLEMENTATION_SUMMARY.md) - Phase 0-4 전체 개요 -- [Phase 2](PHASE2_COMPLETION.md) - Crawl4AI 동적 크롤링 -- [Phase 3A](PHASE3_COMPLETION.md) - 경량 검증 -- [Phase 3B](PHASE3_OPTION_B.md) - SPARQL 검증 -- [Phase 4](PHASE4_COMPLETION.md) - Neo4j 그래프 + 벡터 검색 - -## 🎓 설계 원칙 - -1. **Phase-gated**: 각 Phase는 선택사항 -2. **Pluggable**: 여러 검증 방식 지원 -3. **Async**: 높은 동시성 -4. **Resilient**: 의존성 부재 시에도 동작 - -## 💡 다음 단계 - -### Phase 5: GraphRAG (선택) -- RDF ↔ Property Graph 변환 -- Entity Resolver -- Subgraph retrieval - -### Advanced Features -- Critic loop (자동 수정) -- Few-shot learning -- Zero-shot 분류 - -## 🔗 관련 링크 - -- [Neo4j 문서](https://neo4j.com/docs/) -- [SentenceTransformers](https://www.sbert.net/) -- [Trafilatura](https://trafilatura.python-engineering.com/) -- [FastAPI](https://fastapi.tiangolo.com/) - -## 📝 라이센스 - -MIT License - ---- - -**Version**: 0.4.0 (Phase 0-4 완료) -**Updated**: 2026-05-14 diff --git a/RESPONSIBILITY_REFACTOR_REPORT_2026-05-11.md b/RESPONSIBILITY_REFACTOR_REPORT_2026-05-11.md deleted file mode 100644 index a5ea458..0000000 --- a/RESPONSIBILITY_REFACTOR_REPORT_2026-05-11.md +++ /dev/null @@ -1,263 +0,0 @@ -# crawler_platform 책임 분리 리포트 (AI=Semantic Extractor) - -작성일: 2026-05-11 -대상: `C:\Users\lasta\MyProject\AI\crawler_platform` - -## 1. 결론 요약 - -현재 구조는 동작은 하지만, **제어 책임이 API Route / SiteCrawler / CrawlPipeline / Repository에 분산**되어 있습니다. -특히 `SiteCrawler`와 `CrawlPipeline`이 탐색, 분석 판단, AI 호출, 저장까지 동시에 수행하고 있어 확장성과 테스트 경계가 약합니다. - -핵심 정리: - -- AI(`LLMJsonExtractor`)는 현재 DB/크롤 흐름을 직접 알지 않으며, 대체로 “의미 추출기” 역할을 수행 중. -- 하지만 시스템 전체 orchestration 주체가 부재하여 Route/Crawler/Pipeline에 제어가 분산됨. -- Repository는 저장소를 넘어 일부 정책(신뢰도 결합/병합 전략)을 포함. - ---- - -## 2. 구성요소별 실제 책임 진단 - -## API Route (`app/api/routes.py`) - -현재 책임: - -- 요청/응답 처리 외에 다음까지 수행 -- config 로딩 및 의존 객체 생성 (`CrawlPipeline`, `SiteCrawler`) - - 근거: 149-195 -- crawl 파라미터 정책 적용(`max_depth/max_pages` 보정) - - 근거: 188-191 -- discovery 흐름 직접 수행(robots/fetch/discover) - - 근거: 197-212 -- claim confidence 갱신 정책(0~1 clamp) - - 근거: 274-284 -- entity merge 비즈니스 로직 직접 수행(Claim/Relation 재매핑) - - 근거: 286-308 -- 추천 태그 집계 규칙 직접 수행 - - 근거: 310-343 - -판단: - -- Route가 단순 진입점을 넘어 **서비스/도메인 로직 조립 및 정책 수행자** 역할까지 맡음. -- “얇은 Route + Service 호출” 원칙과 불일치. - ---- - -## SiteCrawler (`app/core/crawler/site_crawler.py`) - -현재 책임: - -- 사이트 탐색(queue/depth/visited/discover) - - 근거: 70-117 -- same-domain 필터, robots 차단 판단 - - 근거: 89-100 -- 페이지 유형 분류 및 분석 여부 판단 - - 근거: 105, 119-120, 201-224 -- AI/Extractor 호출 - - 근거: 120 -- 저장 호출(page/claim/evidence/log) - - 근거: 121-135 -- crawl_jobs 상태 생성/완료 처리 - - 근거: 83, 172-188 - -판단: - -- `SiteCrawler`가 “웹 탐색기”를 넘어 **페이지 처리기 + 저장 오케스트레이터**까지 수행. -- 요청하신 기준(탐색 전용) 대비 책임 과다. - -분리 후보: - -- `classify_page`, `analyze_page_types` 판단 로직 -- `extractor.extract(...)` 호출 -- `repository.upsert_page/save_extraction_bundle(...)` 저장 호출 -- `_create_job/_finish_job` 실행 추적 - ---- - -## CrawlPipeline (`app/core/crawler/pipeline.py`) - -현재 책임: - -- robots 정책 판단 - - 근거: 34-35 -- fetch + parse + extract - - 근거: 37-41 -- 프로젝트/소스 동기화 및 페이지/추출 결과 저장 - - 근거: 43-55 - -판단: - -- 단일 URL 처리기 역할을 하면서도 저장 정책까지 포함. -- 이름은 Pipeline이지만 사실상 **PageProcessor + 저장 orchestration**을 동시에 수행. - -분리 후보: - -- `crawl_url`를 `PageProcessor.process(url)`와 `CrawlService.persist(processed_page)`로 분리 -- robots 허용/재시도/저장 여부 판단은 Service 계층으로 이동 - ---- - -## Extractor (`app/core/extractor/ai_provider.py`, `factory.py`) - -현재 책임: - -- LLM 호출 및 JSON 파싱/복구 -- ontology predicate normalize -- 실패 시 rule-based fallback - -주요 근거: - -- AI 추출 핵심: 42-107, 148-247 -- ontology normalize: 75-78 -- fallback: 109-131 -- provider 선택(facade): `factory.py` 11-17 - -판단: - -- 크롤링 큐, 링크 탐색, DB 저장을 직접 알지 않음(좋음). -- 다만 `LLMJsonExtractor` 내부 fallback은 “추출 품질 보완” 범주로는 허용 가능하나, 책임을 더 엄격히 분리하려면 fallback도 외부 orchestration(Service)로 이동 가능. - -요약: - -- **치명적 위반 없음**(crawl/storage/pipeline orchestration은 알지 않음). - ---- - -## Repository (`app/core/database/repository.py`) - -현재 책임: - -- pages/entities/claims/evidence/extraction_logs 저장 및 upsert -- claim hash 기반 dedup/merge -- relation upsert 및 support_count 증가 -- confidence 결합 규칙(extraction + source trust) - -주요 근거: - -- 저장/병합 중심: 140-226, 228-306 -- 신뢰도 결합 규칙: 163, 322-323 -- claim hash 전략: 164-176, 326-342 - -판단: - -- 저장 인터페이스 역할은 수행하지만, **정책성 로직(신뢰도 결합 비율 0.7/0.3, max merge, relation support 전략)**이 포함됨. -- “Repository는 저장소” 원칙을 엄격히 적용하면, 정책 계산은 Service(또는 Domain Policy)로 이동하는 것이 바람직. - -분리 후보: - -- `combine_confidence` -- claim update 시 `max(confidence)` 전략 -- relation `support_count` 증가 규칙 - ---- - -## session_scope (`app/core/database/session.py`) - -현재 책임: - -- commit/rollback/close 트랜잭션 경계 - -근거: 40-51 - -판단: - -- 요청하신 기준과 일치. 변경 우선순위 낮음. - ---- - -## 3. 현재 가장 큰 책임 혼재 지점 - -1. `SiteCrawler`가 탐색기 + 처리기 + 저장 오케스트레이터를 모두 수행 -2. `CrawlPipeline`이 처리기 + 저장기를 동시에 수행 -3. `API Route`가 서비스 조립/정책/집계/병합 로직을 직접 수행 -4. `Repository`가 저장소를 넘어 정책 일부까지 포함 - ---- - -## 4. 목표 아키텍처 제안 - -권장 호출 구조: - -`FastAPI Route -> CrawlService -> SiteCrawler -> PageProcessor -> Fetcher -> Parser -> Extractor -> Repository` - -역할 재정의: - -- Route: request 검증, service 호출, response 변환 -- CrawlService: 전체 orchestration/정책 판단/재시도/저장 여부 결정 -- SiteCrawler: 링크 탐색(queue/depth/domain/link discovery)만 수행 -- PageProcessor: 단일 URL의 fetch/parse/extract만 수행 -- Extractor: 텍스트 -> 구조화 JSON/Entity/Claim 변환만 수행 -- Repository: 저장/upsert 인터페이스만 수행 (정책 계산 제외) - ---- - -## 5. 리팩터링 설계(코드 대규모 변경 전) - -## Phase 0: 인터페이스 고정 - -- `PageProcessorResult` DTO 정의 - - `url/final_url/status_code/title/clean_text/page_type/entities/claims/raw_output/errors` -- `CrawlDecisionPolicy`(분석 여부/저장 여부 판단) 초안 분리 - -## Phase 1: Service 계층 도입 - -- `app/core/services/crawl_service.py` 신설 -- Route는 `CrawlService.crawl_url(...)`, `CrawlService.crawl_site(...)`만 호출 -- 기존 로직은 내부적으로 재사용하되 외부 인터페이스 먼저 고정 - -## Phase 2: SiteCrawler 축소 - -- `SiteCrawler` 반환을 “발견된 URL 작업 목록” 중심으로 전환 -- 페이지 분류/분석 여부/AI 호출/저장은 `CrawlService`로 이동 - -## Phase 3: CrawlPipeline -> PageProcessor 전환 - -- `CrawlPipeline.crawl_url`를 `PageProcessor.process`로 대체 -- PageProcessor는 fetch/parse/extract까지만 수행, DB 접근 제거 - -## Phase 4: Repository 정책 분리 - -- `combine_confidence`, merge rule을 `app/core/services/policies/*.py`로 이동 -- Repository는 저장/조회/upsert만 수행 - -## Phase 5: Route 슬림화 - -- `/crawl`, `/crawl-site`, `/discover`, `/entities/merge`, `/claims/{id}/confidence`를 Service 호출형으로 변환 - ---- - -## 6. 안전한 단위 리팩터링 파일 목록과 변경 순서 - -1) `app/core/services/crawl_service.py` (신규) -2) `app/core/services/crawl_dto.py` (신규) -3) `app/core/services/policies.py` (신규; confidence/merge 정책) -4) `app/core/crawler/pipeline.py` (PageProcessor 역할로 축소 또는 `page_processor.py`로 분리) -5) `app/core/crawler/site_crawler.py` (탐색 전용으로 축소) -6) `app/core/database/repository.py` (정책 제거, 저장 전용화) -7) `app/api/routes.py` (Service 호출만 남기기) -8) `app/cli/main.py` (Route와 동일 Service 재사용) -9) `tests/` (서비스 단위/계층 경계 테스트 추가) - ---- - -## 7. 테스트 전략(리팩터링 안전장치) - -- 계약 테스트: `Extractor` 입력/출력 계약 유지 -- 단위 테스트: - - `SiteCrawler`: URL discovery/queue/depth/domain 필터만 검증 - - `PageProcessor`: fetch/parse/extract 파이프만 검증(저장 없음) - - `CrawlService`: 분석 여부 판단/저장 호출/재시도 정책 검증 - - `Repository`: pure upsert/조회만 검증 -- 회귀 테스트: - - `/crawl`, `/crawl-site` API 응답 필드 변화 없음 - - claim/entity 수 및 dedup 결과 일관성 확인 - ---- - -## 8. 즉시 적용 가능한 최소 원칙 - -- AI는 `clean_text -> structured data`만 담당 -- “저장 여부, 재시도, 정책 판단”은 Service가 담당 -- Repository에서 정책 계산 로직 분리 -- Route에서 SQL/병합 규칙 직접 처리 제거 - diff --git a/UI_REBUILD_PLAN.md b/UI_REBUILD_PLAN.md deleted file mode 100644 index 7619423..0000000 --- a/UI_REBUILD_PLAN.md +++ /dev/null @@ -1,160 +0,0 @@ -# React UI 재구축 작업 계획 - -> **목적**: 기존 vanilla JS UI를 React + TypeScript + TanStack Query + shadcn으로 재구축. -> 세션이 끊겨도 이 파일을 보고 이어서 작업할 수 있도록 단일 진실 소스(single source of truth). - -## 사용자 비전 (전체 흐름) - -1. **프로젝트 생성** — 어떤 종류의 온톨로지를 구축할지 도메인을 정하고 프로젝트별로 구분 -2. **온톨로지 기본 요소 입력** — 엔티티, 클레임 등을 직접 입력 또는 참고 사이트 URL로 자동 추출 -3. **자료수집** — 시드 URL에서 시작해 링크를 따라가며 정보 추출 + 장시간 자율 온톨로지 구축 -4. **그래프 보기/편집** — 온톨로지 관계를 그래프 맵으로 시각화하고 편집 -5. **JSON 직접 입력** — 데이터를 JSON으로 직접 넣을 수 있는 UI - -## 작업 보드 - -### ✅ 완료 - -| Phase | 내용 | 커밋 | -|---|---|---| -| Phase 0 | React + Vite + TS 환경 + 라우팅 + Redux placeholder | `1be2c7d` | -| Phase 0.5 | API 클라이언트 + TanStack Query + shadcn UI + AppShell + Dashboard 연결 | `37cad40` | -| Phase 1.1 | 프로젝트 생성 (OnboardingPage 폼 + 백엔드 `POST /projects/inline`, `GET /domains`) | `8681ac8` | -| Phase 1.2 | 참고 소스 관리 (ConfigureSourcesPage CRUD + 백엔드 `POST/DELETE /projects/{name}/sources`) | `461ebc0` | -| Phase 1.3 | 시드 크롤 (CrawlPage + 폴링 + 취소, 백엔드 `POST /crawl-site/by-project`) | `aaaaa05` | -| Phase 1.4 | 자율 연구 (ResearchPage + 세션 이력, 백엔드 `POST /research/run/by-project`) | `00786a4` | -| Phase 1.5 | 엔티티/클레임 직접 입력 (OntologyEditorPage 3 탭: 엔티티/클레임/JSON 일괄) | (이번 커밋) | - -### 🚧 진행 중 - -(없음 — Phase 2 시작 전) - -### ⏳ 대기 - -| Phase | 내용 | 다음 액션 | -|---|---|---| -| **Phase 2** | 그래프 시각화/편집 (Cytoscape 또는 react-flow 래퍼) | `GET /projects/{n}/graph/neighborhood` + `legacy/graph.js` 패턴 참고 | -| Phase 3 | JSON Import/Export 전용 페이지 (Editor의 일괄 입력 탭 확장) | 독립 가능 | - ---- - -## Phase 1.3 — 시드 크롤 (CrawlPage) - -### 백엔드 (대부분 존재) -- ✅ `POST /crawl-site` — site-wide crawl 시작 (CrawlRequest 모델 확장) -- ✅ `GET /crawl-site/jobs/{id}` — 진행 상태 조회 -- ✅ `POST /crawl-site/jobs/{id}/cancel` — 작업 취소 - -### 프론트엔드 작업 항목 -- [ ] `src/lib/api/crawl.ts` — Zod 스키마 + `crawlApi.startSite/getJob/cancelJob` -- [ ] `src/hooks/useCrawl.ts` — `useStartCrawl`, `useCrawlJob`(폴링), `useCancelCrawl` -- [ ] `src/components/ui/select.tsx` — 소스 선택 드롭다운 -- [ ] `src/components/ui/progress.tsx` — 진행률 표시 바 -- [ ] `src/components/ui/badge.tsx` — 상태 배지 -- [ ] `CrawlPage` 재설계: - - 좌측: 시드 URL 입력 폼 + 소스 선택 + max_depth/max_pages - - 우측: 진행 중 작업 카드 (페이지 수, 단계, 로그) - - 작업 완료 시 결과 페이지로 이동 -- [ ] i18n locale `crawl.*` 키 추가 - -### 검증 포인트 -- [ ] 백엔드 미구동시 명확한 에러 -- [ ] 폴링 간격: 2초, 작업 종료(완료/실패/취소)시 폴링 중단 -- [ ] cancel 버튼 → 백엔드에 취소 요청 + UI 정리 - ---- - -## Phase 1.4 — 자율 연구 (Research Loop) - -### 백엔드 -- ✅ `POST /research/run` — `ResearchRunRequest` (CrawlRequest 확장 + max_depth, max_steps, max_branch, min_relevance...) -- ✅ `GET /projects/{n}/research/sessions` — 세션 이력 -- ✅ `GET /research/sessions/{job_id}` — 단일 세션 상세 - -### 프론트엔드 작업 항목 -- [ ] `src/lib/api/research.ts` + Zod 스키마 -- [ ] `src/hooks/useResearch.ts` — `useStartResearch`, `useResearchSession`, `useResearchHistory` -- [ ] Sidebar에 "자율 연구" 메뉴 추가 -- [ ] 새 페이지 `src/pages/ResearchPage.tsx`: - - 시작 폼: 시드 URL, 목표(goal) 텍스트, max_steps, min_relevance 등 - - 진행 표시: 현재 step, 누적 페이지 수, 발견 엔티티, 관련도 - - 세션 이력 사이드 패널 -- [ ] i18n locale `research.*` 키 - ---- - -## Phase 1.5 — 엔티티/클레임 직접 입력 - -### 백엔드 (신규 필요) -- [ ] `POST /projects/{n}/entities` — 단일 엔티티 직접 생성 -- [ ] `POST /projects/{n}/entities/bulk` — 다수 엔티티 일괄 입력 -- [ ] `POST /projects/{n}/claims` — 단일 클레임 직접 생성 -- [ ] `PATCH /projects/{n}/entities/{id}` — 엔티티 수정 - -### 프론트엔드 작업 항목 -- [ ] `src/lib/api/entities.ts`, `src/lib/api/claims.ts` + Zod -- [ ] `src/hooks/useEntities.ts`, `useClaims.ts` -- [ ] `src/components/ui/dialog.tsx` — 입력 다이얼로그 (Radix UI 검토) -- [ ] `src/components/ui/table.tsx` -- [ ] 새 페이지 `src/pages/OntologyEditorPage.tsx`: - - 엔티티 탭 / 클레임 탭 - - 엔티티 추가/편집 다이얼로그 (label, type, properties) - - 클레임 추가 다이얼로그 (subject/predicate/object/confidence) - - 일괄 입력 토글 (JSON 텍스트 → 파싱) -- [ ] 네비게이션: ConfigureSourcesPage에서 "직접 입력" 진입점 추가 - ---- - -## Phase 2 — 그래프 시각화/편집 - -### 백엔드 (대부분 존재) -- ✅ `GET /projects/{n}/graph/neighborhood` — 노드 주변 부분 그래프 -- ✅ `GET /projects/{n}/graph/query` — 패턴 매칭 쿼리 - -### 프론트엔드 작업 항목 -- [ ] Cytoscape 의존성 그대로 활용 (`legacy/graph.js` 패턴 참고) -- [ ] `src/components/graph/GraphView.tsx` — Cytoscape React 래퍼 - - 노드 클릭 → 인스펙터, 더블 클릭 → neighborhood 확장 -- [ ] 새 페이지 `src/pages/GraphPage.tsx`: - - 좌측: 노드 검색 / 필터 - - 중앙: 그래프 캔버스 - - 우측: 선택 노드 인스펙터 + 편집 -- [ ] 그래프 편집 mutation (노드 속성 변경, 엣지 추가/삭제) — Phase 1.5 백엔드 재사용 - ---- - -## Phase 3 — JSON Import/Export - -### 백엔드 -- [ ] `POST /projects/{n}/import/json` — JSON 일괄 import (엔티티 + 클레임 + 관계) -- [ ] `GET /projects/{n}/export/json` — 전체 온톨로지 JSON 다운로드 - -### 프론트엔드 -- [ ] 새 페이지/탭 `ImportExportPage.tsx`: - - 파일 드래그&드롭 / 텍스트 영역 붙여넣기 - - 미리보기 → 충돌 처리 (덮어쓰기/병합/스킵) - - import 진행 상태 + 결과 요약 -- [ ] Export 버튼 → JSON 다운로드 또는 클립보드 복사 - ---- - -## 작업 재개 가이드 - -세션을 처음 열거나 끊긴 후 다시 시작할 때: - -1. **이 파일을 먼저 읽기** — 현재 상태 파악 -2. **git log --oneline -10** — 최근 커밋과 작업 보드 대조 -3. **🚧 진행 중** 행의 "다음 액션"부터 시작 -4. 완료 후: - - 이 파일의 체크박스/상태/커밋 해시 업데이트 - - 같은 커밋에 이 파일도 함께 포함 - -## 컨벤션 - -- 커밋 메시지: `Phase X.Y: 한 줄 요약 — 핵심 내용` -- 백엔드 변경은 같은 커밋에 묶기 (프론트만 또는 백만 따로 분리 X) -- 모든 폼: react-hook-form + zod -- 모든 서버 통신: TanStack Query 훅을 거침 (Redux 직접 X) -- 모든 새 UI 컴포넌트: shadcn 패턴(forwardRef + cn) -- i18n 키 사용 시 fallback 문자열 같이 (`t("key", "한글 fallback")`) -- 영문/한글 locale 동시 업데이트 diff --git a/docker-compose.neo4j.yml b/docker-compose.neo4j.yml deleted file mode 100644 index 49f00d9..0000000 --- a/docker-compose.neo4j.yml +++ /dev/null @@ -1,40 +0,0 @@ -version: '3.8' - -services: - neo4j: - image: neo4j:5.18.1 - container_name: ontology-neo4j - environment: - NEO4J_AUTH: neo4j/ontology123 # username: neo4j, password: ontology123 - NEO4J_server_memory_heap_initial__size: 1G - NEO4J_server_memory_heap_max__size: 2G - NEO4J_dbms_memory_pagecache_size: 1G - # APOC (for advanced graph operations) - NEO4J_dbms_security_procedures_unrestricted: apoc.* - NEO4J_server_logs_debug_level: INFO - ports: - - "7687:7687" # Bolt protocol - - "7474:7474" # HTTP - - "7473:7473" # HTTPS - volumes: - - neo4j_data:/var/lib/neo4j/data - - neo4j_logs:/var/lib/neo4j/logs - - neo4j_import:/var/lib/neo4j/import - healthcheck: - test: ["CMD", "cypher-shell", "-u", "neo4j", "-p", "ontology123", "RETURN 1"] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - -volumes: - neo4j_data: - driver: local - neo4j_logs: - driver: local - neo4j_import: - driver: local - -networks: - default: - name: ontology-network diff --git a/ontology_platform_research_report.md b/ontology_platform_research_report.md deleted file mode 100644 index b42a317..0000000 --- a/ontology_platform_research_report.md +++ /dev/null @@ -1,1653 +0,0 @@ -# 온톨로지 구축 플랫폼 연구보고서 -## 웹사이트 탐색 기반 자동 온톨로지 구축 시스템의 필요성, 구조, 기능 명세, 구축 이유 - ---- - -## 0. 보고서의 목적 - -이 보고서는 현재 개발 중인 **AI 기반 온톨로지 구축 툴**을 단순한 데이터 편집 도구가 아니라, 실제 상용화 가능한 **웹사이트 탐색 기반 자동 온톨로지 구축 플랫폼**으로 발전시키기 위한 연구 자료이다. - -현재 시스템은 웹사이트에서 수집된 데이터로부터 엔티티와 클레임을 생성하고, 이를 사용자가 직접 확인하거나 수정할 수 있는 구조를 갖추고 있다. 그러나 화면상으로는 아직 다음과 같은 한계가 드러난다. - -- 온톨로지가 어떤 과정으로 만들어졌는지 보이지 않는다. -- AI가 어떤 근거로 엔티티와 관계를 추출했는지 확인하기 어렵다. -- 사이트 탐색, 페이지 분석, 스키마 설계, 품질 검증, 승인 절차가 분리되어 보이지 않는다. -- 사용자는 결과 목록은 볼 수 있지만, 시스템이 “지능적으로 구축한다”는 인상을 받기 어렵다. -- 현재 화면은 상용 플랫폼보다는 내부 관리자용 클레임 편집기에 가깝다. - -따라서 이 보고서의 목적은 다음과 같다. - -1. 온톨로지 구축 시스템이 무엇을 해야 하는지 정리한다. -2. 각 기능이 왜 필요한지 설명한다. -3. 개발자와 기획자가 시스템의 구조를 이해할 수 있도록 교육 자료 역할을 한다. -4. 현재 시스템을 상용화 가능한 수준으로 확장하기 위한 기능 명세를 제안한다. -5. “왜 이렇게 구축해야 하는가”에 대한 설득 논리를 제공한다. - ---- - -## 1. 온톨로지란 무엇인가 - -### 1.1 온톨로지의 기본 의미 - -온톨로지는 어떤 분야의 지식을 컴퓨터가 이해할 수 있도록 **개념, 속성, 관계**로 구조화한 지식 체계이다. - -일반적인 데이터베이스가 단순히 값을 저장한다면, 온톨로지는 그 값들이 서로 어떤 의미적 관계를 갖는지 표현한다. - -예를 들어 향수 구독 사이트를 분석한다고 가정해보자. - -단순 데이터는 다음과 같이 저장될 수 있다. - -```text -상품명: 포맨트 퍼퓸 코튼 허그 -가격: 44,000원 -브랜드: 포맨트 -향 설명: 부드러운 머스크 파우더리 노트... -``` - -하지만 온톨로지는 이를 다음과 같이 구조화한다. - -```text -포맨트 퍼퓸 코튼 허그 → hasBrand → 포맨트 -포맨트 퍼퓸 코튼 허그 → hasPrice → 44,000원 -포맨트 퍼퓸 코튼 허그 → hasBaseNote → 머스크 -포맨트 퍼퓸 코튼 허그 → belongsToCategory → 퍼퓸 -``` - -이 차이가 중요하다. - -단순 데이터는 사람이 읽을 수 있는 정보이고, 온톨로지는 기계가 추론하고 연결할 수 있는 지식 구조이다. - ---- - -### 1.2 온톨로지의 핵심 구성 요소 - -온톨로지는 보통 다음 구성 요소로 이루어진다. - -| 구성 요소 | 의미 | 예시 | -|---|---|---| -| Entity | 개별 대상 | 포맨트 퍼퓸 코튼 허그, 포맨트, 머스크 | -| Entity Type | 대상의 종류 | Product, Brand, Note, Category | -| Predicate | 관계 또는 속성 | hasBrand, hasPrice, hasBaseNote | -| Claim | 주어-술어-목적어 형태의 지식 단위 | 상품 A hasBrand 브랜드 B | -| Source | 정보의 출처 | 특정 URL, 특정 문장 | -| Confidence | 신뢰도 | 0.84, 0.92 | -| Schema | 온톨로지의 설계 규칙 | Product는 Brand를 가질 수 있다 | - -현재 시스템에서 보이는 클레임 목록은 온톨로지의 일부에 해당한다. 그러나 상용 온톨로지 툴이 되려면 단순히 클레임을 보여주는 것에서 끝나면 안 된다. **그 클레임이 어디서 왔고, 왜 생성되었으며, 어떻게 검증되었고, 전체 지식 구조 안에서 어떤 위치에 있는지** 보여줄 수 있어야 한다. - ---- - -## 2. 현재 시스템의 상태와 문제점 - -### 2.1 현재 화면의 장점 - -현재 화면에는 이미 의미 있는 기반이 존재한다. - -- 프로젝트 단위가 있다. -- 엔티티 수가 표시된다. -- 클레임 수가 표시된다. -- Subject, Predicate, Object 구조를 직접 입력할 수 있다. -- 신뢰도 값이 있다. -- 소스 정보가 있다. -- rule_candidate 같은 생성 방식 또는 상태 태그가 있다. - -이는 온톨로지 구축의 핵심 데이터 구조에 어느 정도 접근하고 있다는 뜻이다. 즉, 완전히 잘못된 방향은 아니다. - -문제는 **기반 데이터는 있지만, 플랫폼 경험이 부족하다**는 점이다. - ---- - -### 2.2 현재 시스템의 핵심 문제 - -현재 화면은 사용자가 보기에는 다음과 같은 인상을 준다. - -```text -데이터가 수집되었고, 그 결과가 목록으로 나열되어 있다. -사용자는 그 목록을 수동으로 수정한다. -``` - -하지만 상용화된 AI 온톨로지 플랫폼은 다음 인상을 줘야 한다. - -```text -시스템이 사이트를 탐색하고, -페이지를 이해하고, -핵심 개체를 추출하고, -관계를 만들고, -품질을 검증하고, -사람이 승인하면, -온톨로지로 확정된다. -``` - -즉, 현재 시스템은 **결과 화면은 있지만 과정 화면이 부족하다**. - -상용 플랫폼처럼 보이려면 “결과 목록”이 아니라 “지식 구축 공정”을 보여줘야 한다. - ---- - -### 2.3 특히 개선이 필요한 부분 - -#### 1. 자동 구축 과정이 보이지 않는다 - -사용자는 현재 클레임이 어떻게 만들어졌는지 알기 어렵다. 어떤 페이지에서 왔는지, 어떤 문장에서 추출되었는지, 어떤 룰 또는 AI 분석이 사용되었는지 명확히 보여야 한다. - -#### 2. 스키마 설계가 보이지 않는다 - -hasBrand, hasPrice, hasBaseNote 같은 Predicate가 사용되고 있지만, 이 관계들이 어디서 정의되었고 어떤 규칙을 갖는지 보이지 않는다. - -#### 3. 검증 절차가 약하다 - -AI가 만든 후보를 바로 온톨로지로 받아들이면 데이터 품질이 떨어질 수 있다. 후보, 승인, 반려, 보류 상태가 필요하다. - -#### 4. 품질 진단이 부족하다 - -중복 엔티티, 잘못된 관계, 출처 없는 클레임, 모순된 가격 같은 문제가 자동으로 표시되어야 한다. - -#### 5. 시각적 구조가 부족하다 - -온톨로지는 본질적으로 관계 그래프이다. 목록만으로는 온톨로지가 구축되고 있다는 느낌을 주기 어렵다. 그래프 뷰가 필요하다. - ---- - -## 3. 온톨로지 구축 플랫폼의 목표 모델 - -### 3.1 이 시스템이 최종적으로 해야 하는 일 - -이 시스템의 최종 목표는 다음과 같이 정의할 수 있다. - -> 사용자가 특정 웹사이트 또는 도메인을 입력하면, 시스템이 사이트를 탐색하고, 페이지를 분류하고, 핵심 개체와 관계를 추출하고, 사람이 검증할 수 있는 후보를 제시한 뒤, 승인된 항목을 온톨로지로 구축하는 플랫폼. - -이를 단계로 나누면 다음과 같다. - -```text -1. 사이트 입력 -2. 페이지 탐색 -3. 페이지 유형 분류 -4. 본문 정제 -5. 엔티티 추출 -6. 관계/클레임 생성 -7. 중복/오류/모순 검증 -8. 사용자 검토 및 승인 -9. 온톨로지 반영 -10. 그래프 확인 및 외부 Export -``` - -이 흐름이 화면과 기능에 드러나야 한다. - ---- - -### 3.2 이 시스템이 단순 크롤러와 다른 점 - -일반 크롤러는 웹페이지에서 텍스트나 HTML을 가져온다. - -하지만 온톨로지 구축 플랫폼은 단순히 데이터를 가져오는 것이 아니라, 데이터의 의미를 구조화해야 한다. - -| 구분 | 일반 크롤러 | 온톨로지 구축 플랫폼 | -|---|---|---| -| 목적 | 웹페이지 수집 | 의미 구조 구축 | -| 결과 | HTML, 텍스트, 이미지 | 엔티티, 관계, 스키마, 클레임 | -| 분석 수준 | 낮음 | 높음 | -| 사용자 작업 | 수집 URL 관리 | 지식 검증 및 승인 | -| 핵심 가치 | 데이터 확보 | 지식화, 추론 가능성, 재사용성 | - -따라서 이 시스템은 단순 크롤러가 아니라 **웹 기반 지식 자동 구조화 시스템**으로 포지셔닝해야 한다. - ---- - -## 4. 전체 시스템 구조 제안 - -상용화 가능한 온톨로지 구축 플랫폼은 다음과 같은 모듈 구조를 가져야 한다. - -```text -[Project Dashboard] - ↓ -[Source Explorer] - ↓ -[Build Pipeline] - ↓ -[Page Analysis Viewer] - ↓ -[Schema Designer] - ↓ -[Entity Extraction Engine] - ↓ -[Claim Builder] - ↓ -[Review & Approval Center] - ↓ -[Ontology Graph View] - ↓ -[Quality Inspector] - ↓ -[Export / API] -``` - -각 모듈은 독립적인 기능처럼 보이지만, 실제로는 하나의 흐름을 이룬다. - -핵심은 사용자가 “결과 데이터”가 아니라 “구축 과정 전체”를 통제할 수 있게 만드는 것이다. - ---- - -## 5. 기능 제안 및 상세 명세 - ---- - -# 5.1 Project Dashboard - -## 기능명 - -**Ontology Build Dashboard** - -## 목적 - -프로젝트의 전체 구축 상태를 한눈에 보여준다. - -현재 시스템은 엔티티 수와 클레임 수 정도만 보여준다. 하지만 사용자는 이것만 보고는 프로젝트가 잘 진행되고 있는지 판단하기 어렵다. - -대시보드는 온톨로지 구축의 관제탑 역할을 해야 한다. - -## 주요 기능 - -- 전체 수집 URL 수 -- 분석 완료 페이지 수 -- 오류 페이지 수 -- 엔티티 수 -- 클레임 수 -- 승인된 클레임 수 -- 승인 대기 클레임 수 -- 반려된 클레임 수 -- 중복 엔티티 후보 수 -- 모순 클레임 수 -- 출처 없는 클레임 수 -- 평균 신뢰도 -- 온톨로지 품질 점수 -- 최근 빌드 시간 -- 최근 오류 로그 - -## 왜 필요한가 - -사용자는 복잡한 시스템을 사용할 때 가장 먼저 “현재 상태”를 알고 싶어 한다. - -대시보드가 없으면 사용자는 다음 질문에 답할 수 없다. - -- 지금 얼마나 수집되었는가? -- 분석은 끝났는가? -- AI가 만든 후보 중 얼마나 승인되었는가? -- 데이터 품질은 좋은가? -- 오류는 어디서 발생했는가? - -상용 플랫폼은 항상 전체 상태를 보여준다. 대시보드는 기능의 문제가 아니라 **제품 신뢰도의 문제**이다. - -## UI 제안 - -상단에는 핵심 지표 카드를 배치한다. - -```text -수집 URL 324개 | 분석 완료 281개 | 엔티티 1,284개 | 클레임 4,912개 | 승인율 63% | 품질 점수 78점 -``` - -하단에는 다음을 배치한다. - -- 최근 빌드 진행 상태 -- 검토가 필요한 항목 -- 오류 페이지 목록 -- 품질 경고 -- 최근 추가된 엔티티 - ---- - -# 5.2 Source Explorer - -## 기능명 - -**Source Explorer** - -## 목적 - -사이트를 탐색하고, 어떤 페이지를 온톨로지 구축 대상으로 삼을지 관리한다. - -온톨로지 구축은 결국 좋은 소스에서 시작된다. 잘못된 페이지를 분석하면 잘못된 온톨로지가 만들어진다. - -## 주요 기능 - -### 1. 시작 URL 등록 - -사용자가 분석할 사이트의 시작 URL을 입력한다. - -예: - -```text -https://example.com/perfume -``` - -### 2. 사이트맵 자동 탐색 - -사이트 내부 링크를 수집하고 페이지 후보를 만든다. - -### 3. 페이지 유형 분류 - -각 URL을 다음과 같이 분류한다. - -- 상품 목록 페이지 -- 상품 상세 페이지 -- 브랜드 페이지 -- 카테고리 페이지 -- 리뷰 페이지 -- 이벤트 페이지 -- 약관/정책 페이지 -- 로그인/장바구니 페이지 -- 제외 대상 페이지 - -### 4. URL 패턴 규칙 - -사용자가 포함/제외 규칙을 지정할 수 있다. - -예: - -```text -포함: /product/, /goods/, /item/ -제외: /login/, /cart/, /event/, /terms/ -``` - -### 5. 페이지 상태 관리 - -각 페이지는 상태를 가진다. - -```text -미수집 / 수집 완료 / 정제 완료 / 분석 완료 / 오류 / 제외 -``` - -### 6. 페이지 미리보기 - -선택한 페이지의 원문 HTML, 정제 텍스트, 스크린샷, 메타데이터를 확인한다. - -## 왜 필요한가 - -온톨로지 구축에서 가장 흔한 실패 원인은 “잘못된 소스 입력”이다. - -예를 들어 향수 상품 온톨로지를 만들고 싶은데 이벤트 페이지, 장바구니 페이지, 리뷰 광고 문구, 푸터 메뉴까지 분석하면 다음과 같은 문제가 생긴다. - -- 의미 없는 엔티티가 생성된다. -- value, keyword, accord 같은 불완전한 값이 관계로 저장된다. -- 브랜드가 아닌 텍스트가 브랜드로 저장된다. -- 상품이 아닌 문장이 상품처럼 추출된다. - -따라서 Source Explorer는 단순히 URL을 보여주는 기능이 아니다. 이것은 **온톨로지 품질을 결정하는 첫 번째 필터**이다. - -## 설득 포인트 - -AI 분석이 아무리 좋아도, 입력 데이터가 엉망이면 결과도 엉망이 된다. 온톨로지 시스템에서 Source Explorer는 “AI보다 앞단의 품질 관리 장치”이다. - ---- - -# 5.3 Build Pipeline - -## 기능명 - -**Ontology Build Pipeline** - -## 목적 - -수집에서 온톨로지 반영까지의 전체 과정을 단계별로 실행하고 상태를 보여준다. - -## 주요 단계 - -```text -Step 1. Source Crawl -Step 2. Page Clean -Step 3. Page Classification -Step 4. Entity Extraction -Step 5. Claim Generation -Step 6. Deduplication -Step 7. Validation -Step 8. Human Review -Step 9. Ontology Commit -Step 10. Export -``` - -## 각 단계의 기능 - -### Step 1. Source Crawl - -웹사이트에서 HTML, 메타데이터, 링크를 수집한다. - -### Step 2. Page Clean - -본문과 무관한 영역을 제거한다. - -- 메뉴 -- 푸터 -- 광고 -- 추천 상품 -- 장바구니 영역 -- 로그인 영역 -- 반복 UI - -### Step 3. Page Classification - -페이지 유형을 분류한다. - -예: - -```text -이 페이지는 상품 상세 페이지이다. -이 페이지는 브랜드 소개 페이지이다. -이 페이지는 리뷰 목록 페이지이다. -``` - -### Step 4. Entity Extraction - -핵심 개체를 추출한다. - -예: - -```text -Product: 포맨트 퍼퓸 코튼 허그 -Brand: 포맨트 -Price: 44,000원 -Note: 머스크 -``` - -### Step 5. Claim Generation - -추출된 엔티티를 관계로 연결한다. - -예: - -```text -포맨트 퍼퓸 코튼 허그 hasBrand 포맨트 -포맨트 퍼퓸 코튼 허그 hasPrice 44,000원 -``` - -### Step 6. Deduplication - -중복 엔티티를 감지한다. - -예: - -```text -포맨트 퍼퓸 코튼 허그 -FORMENT Cotton Hug Perfume -코튼 허그 퍼퓸 -``` - -이 세 개가 같은 상품인지 판단한다. - -### Step 7. Validation - -스키마 위반, 모순, 출처 누락을 검사한다. - -### Step 8. Human Review - -AI가 만든 후보를 사용자가 승인 또는 반려한다. - -### Step 9. Ontology Commit - -승인된 항목만 정식 온톨로지에 반영한다. - -### Step 10. Export - -외부 시스템에서 사용할 수 있도록 내보낸다. - -## 왜 필요한가 - -현재처럼 결과 목록만 있으면 사용자는 시스템이 내부에서 어떤 일을 했는지 알 수 없다. 하지만 파이프라인이 있으면 사용자는 각 단계의 성공 여부와 실패 원인을 확인할 수 있다. - -파이프라인은 상용 플랫폼에서 매우 중요하다. - -그 이유는 다음과 같다. - -1. 복잡한 작업을 단계별로 이해할 수 있다. -2. 오류가 어느 단계에서 발생했는지 알 수 있다. -3. 재실행할 단계를 선택할 수 있다. -4. 처리 속도와 비용을 관리할 수 있다. -5. 사용자에게 “자동 구축 시스템”이라는 인상을 준다. - -## 설득 포인트 - -온톨로지 구축은 한 번의 AI 호출로 끝나는 작업이 아니다. 수집, 정제, 분류, 추출, 검증, 승인이라는 공정이다. 따라서 이 시스템은 채팅형 AI가 아니라 **지식 생산 파이프라인**으로 설계되어야 한다. - ---- - -# 5.4 Page Analysis Viewer - -## 기능명 - -**Page Analysis Viewer** - -## 목적 - -AI가 페이지를 어떻게 해석했는지 확인하는 화면이다. - -## 주요 기능 - -- 원문 페이지 보기 -- 정제된 본문 보기 -- 추출된 엔티티 보기 -- 생성된 클레임 보기 -- 근거 문장 하이라이트 -- HTML 영역 기반 추출 위치 표시 -- 재분석 버튼 -- 잘못된 추출 삭제 -- 추출 근거 수정 -- 페이지별 분석 로그 확인 - -## 예시 화면 구조 - -```text -왼쪽: 원문/정제 본문 -오른쪽: 추출 결과 -하단: 생성된 클레임과 근거 문장 -``` - -예: - -```text -본문 문장: -“포맨트 퍼퓸 코튼 허그는 부드러운 머스크 파우더리 노트가 특징입니다.” - -추출 결과: -Product: 포맨트 퍼퓸 코튼 허그 -Note: 머스크 -Claim: 포맨트 퍼퓸 코튼 허그 hasBaseNote 머스크 -Confidence: 0.84 -``` - -## 왜 필요한가 - -AI가 만든 결과를 신뢰하려면 근거가 있어야 한다. - -사용자는 다음을 알고 싶어 한다. - -- 이 엔티티는 어느 문장에서 나왔는가? -- 이 관계는 왜 생성되었는가? -- 이 값이 실제 페이지에 존재하는가? -- AI가 추론한 것인가, 규칙으로 뽑은 것인가? - -근거가 없으면 사용자는 결과를 신뢰하기 어렵다. 특히 온톨로지는 단순 텍스트 생성이 아니라 지식 구조 구축이기 때문에, 근거 추적이 필수이다. - -## 설득 포인트 - -AI 자동화의 핵심은 “AI가 다 해준다”가 아니다. 상용 시스템에서 중요한 것은 “AI가 왜 그렇게 판단했는지 검토할 수 있다”이다. Page Analysis Viewer는 AI 결과를 검증 가능한 지식 후보로 바꾸는 장치이다. - ---- - -# 5.5 Ontology Schema Designer - -## 기능명 - -**Ontology Schema Designer** - -## 목적 - -온톨로지의 구조를 정의한다. - -AI가 아무 관계나 만들게 두면 온톨로지는 금방 오염된다. 어떤 종류의 엔티티를 허용할지, 어떤 관계를 허용할지, 어떤 값 타입을 허용할지 사전에 정의해야 한다. - -## 주요 기능 - -### 1. Entity Type 정의 - -예: - -```text -Product -Brand -Category -Note -Ingredient -Price -SubscriptionPlan -Review -``` - -### 2. Predicate 정의 - -예: - -```text -hasBrand -hasPrice -hasTopNote -hasMiddleNote -hasBaseNote -belongsToCategory -hasIngredient -hasSubscriptionPlan -``` - -### 3. 관계 규칙 정의 - -예: - -```text -Product hasBrand Brand -Product hasPrice Price -Product hasBaseNote Note -Product belongsToCategory Category -``` - -### 4. 값 타입 정의 - -예: - -```text -Price.value: number -Price.currency: KRW -Product.name: string -Product.url: url -``` - -### 5. 필수 속성 정의 - -예: - -```text -Product는 name이 필수이다. -Product는 sourceUrl이 필수이다. -Claim은 evidenceText가 필수이다. -``` - -### 6. 금지 규칙 정의 - -예: - -```text -빈 문자열을 Entity로 저장하지 않는다. -value, keyword, accord 같은 일반 필드를 독립 Claim으로 저장하지 않는다. -브랜드가 아닌 사이트명을 Brand로 저장하지 않는다. -``` - -단, 이 규칙은 특정 사이트의 예외를 하드코딩하는 방식이면 안 된다. 더 근본적인 원칙은 다음과 같다. - -```text -의미적 주체가 아닌 UI 라벨, 필드명, 단순 속성 키, 빈 값, 반복 템플릿 텍스트는 엔티티 또는 클레임의 대상으로 승격하지 않는다. -``` - -## 왜 필요한가 - -스키마가 없으면 AI는 매번 다른 기준으로 정보를 추출한다. - -예를 들어 어떤 페이지에서는 브랜드를 Brand로 저장하고, 다른 페이지에서는 brand라는 단어 자체를 Entity로 저장할 수 있다. 어떤 페이지에서는 가격을 문자열로 저장하고, 다른 페이지에서는 Price 엔티티로 저장할 수 있다. - -이렇게 되면 온톨로지는 점점 사용할 수 없는 데이터 덩어리가 된다. - -## 설득 포인트 - -온톨로지에서 스키마는 건물의 설계도와 같다. 설계도 없이 벽돌을 쌓으면 건물이 아니라 잔해가 된다. AI 추출 결과는 벽돌이고, 스키마는 그 벽돌을 어떤 구조로 쌓을지 결정하는 기준이다. - ---- - -# 5.6 Entity Extraction Engine - -## 기능명 - -**Entity Extraction Engine** - -## 목적 - -페이지에서 핵심 개체를 자동으로 추출한다. - -## 주요 기능 - -- 상품명 추출 -- 브랜드명 추출 -- 가격 추출 -- 카테고리 추출 -- 설명문 추출 -- 노트 추출 -- 성분 추출 -- 옵션 추출 -- 이미지 URL 추출 -- 단위/통화 정규화 -- 후보 신뢰도 계산 -- 동일 엔티티 병합 후보 생성 - -## 추출 방식 - -Entity Extraction Engine은 한 가지 방식에만 의존하면 안 된다. 다음 방식이 혼합되어야 한다. - -### 1. 구조 기반 추출 - -HTML 구조, CSS Selector, DOM 위치를 이용한다. - -예: - -```text -.product-title -.price -.brand-name -``` - -장점은 빠르고 정확하다. 단점은 사이트 구조가 바뀌면 깨질 수 있다. - -### 2. 패턴 기반 추출 - -정규식이나 텍스트 패턴을 이용한다. - -예: - -```text -\d{1,3}(,\d{3})*원 -``` - -가격, 날짜, 용량, 할인율 같은 값에 유용하다. - -### 3. AI 기반 추출 - -문맥을 이해해야 하는 정보에 사용한다. - -예: - -```text -“부드러운 머스크와 파우더리한 잔향” → Note: 머스크, 파우더리 -``` - -### 4. 룰 기반 후처리 - -AI가 뽑은 결과를 스키마 규칙과 금지 규칙으로 정리한다. - -## 왜 필요한가 - -온톨로지의 품질은 엔티티 추출 품질에 크게 좌우된다. - -엔티티가 잘못 추출되면 이후 생성되는 모든 관계도 잘못된다. - -예를 들어 상품명이 잘못 잡히면 다음과 같은 잘못된 관계가 생긴다. - -```text -“부드러운 머스크 파우더리 노트...” hasBrand 포맨트 -``` - -이것은 상품이 아니라 설명문이 주어가 된 잘못된 클레임이다. - -## 설득 포인트 - -좋은 온톨로지는 좋은 엔티티에서 시작된다. 관계 추출보다 먼저 중요한 것은 “무엇을 독립된 대상으로 볼 것인가”를 정확히 결정하는 일이다. - ---- - -# 5.7 Claim Builder - -## 기능명 - -**Claim Builder** - -## 목적 - -추출된 엔티티들을 주어-술어-목적어 구조로 연결한다. - -## 주요 기능 - -- Subject 선택 -- Predicate 선택 -- Object 선택 -- 신뢰도 계산 -- 근거 문장 저장 -- 출처 URL 저장 -- 생성 방식 저장 -- 중복 클레임 감지 -- 모순 클레임 감지 -- 후보 상태 관리 - -## Claim 데이터 구조 예시 - -```json -{ - "subject": "포맨트 퍼퓸 코튼 허그", - "predicate": "hasBrand", - "object": "포맨트", - "sourceUrl": "https://example.com/product/123", - "evidenceText": "포맨트 퍼퓸 코튼 허그...", - "confidence": 0.91, - "createdBy": "rule+ai", - "status": "candidate" -} -``` - -## 현재 화면에서 개선해야 할 점 - -현재 화면에는 `[object Object]`가 보이는 항목이 있다. 이것은 매우 중요한 문제이다. - -사용자는 온톨로지 데이터를 직접 읽고 검토해야 한다. 그런데 Object가 사람이 읽을 수 없는 형태로 표시되면 시스템에 대한 신뢰가 떨어진다. - -개선 방향은 다음과 같다. - -```text -잘못된 표시: [object Object] -올바른 표시: 44,000원 -더 좋은 표시: 44,000원 (KRW, Price) -``` - -## 왜 필요한가 - -온톨로지는 결국 Claim의 집합이다. 하지만 모든 Claim이 동일하게 중요한 것은 아니다. - -각 Claim에는 다음 정보가 반드시 따라야 한다. - -- 이 관계가 어디에서 왔는가? -- 어떤 근거 문장이 있는가? -- 얼마나 신뢰할 수 있는가? -- AI가 만든 것인가, 룰이 만든 것인가? -- 사람이 승인했는가? - -이 정보가 없으면 Claim은 단순한 추측 데이터가 된다. - -## 설득 포인트 - -Claim Builder는 온톨로지의 생산 공장이다. 여기서 잘못된 관계가 대량으로 생성되면 뒤에서 아무리 수정해도 품질을 회복하기 어렵다. 따라서 Claim은 생성 즉시 근거, 신뢰도, 상태, 생성 방식을 함께 가져야 한다. - ---- - -# 5.8 Review & Approval Center - -## 기능명 - -**Review & Approval Center** - -## 목적 - -AI가 만든 후보를 사람이 검토하고 승인한다. - -## 주요 기능 - -- 후보 목록 -- 승인 목록 -- 반려 목록 -- 보류 목록 -- 신뢰도 낮은 항목 우선 보기 -- 중복 후보 우선 보기 -- 모순 후보 우선 보기 -- 출처별 필터 -- Predicate별 필터 -- Entity Type별 필터 -- 일괄 승인 -- 일괄 반려 -- 변경 이력 기록 - -## 상태 정의 - -```text -candidate: AI 또는 룰이 생성한 후보 -approved: 사람이 승인한 정식 항목 -rejected: 사람이 반려한 항목 -pending: 검토 보류 항목 -archived: 더 이상 사용하지 않는 항목 -``` - -## 왜 필요한가 - -AI가 만든 결과를 바로 온톨로지에 반영하면 위험하다. - -특히 초기 시스템에서는 다음 문제가 자주 발생한다. - -- 설명문이 상품으로 저장된다. -- 필드명이 엔티티로 저장된다. -- 같은 상품이 여러 개로 중복 저장된다. -- 가격 정보가 다른 페이지에서 충돌한다. -- 광고 문구가 속성으로 저장된다. - -따라서 자동화와 인간 검토 사이의 균형이 필요하다. - -## 설득 포인트 - -상용 AI 시스템에서 중요한 것은 완전 자동화가 아니라 **통제 가능한 자동화**이다. 사람이 모든 것을 직접 입력하면 비효율적이고, AI가 모든 것을 확정하면 위험하다. Review & Approval Center는 이 둘 사이의 안전장치이다. - ---- - -# 5.9 Ontology Graph View - -## 기능명 - -**Ontology Graph View** - -## 목적 - -온톨로지를 관계 그래프로 시각화한다. - -## 주요 기능 - -- 엔티티 노드 표시 -- 관계 엣지 표시 -- Predicate별 색상 구분 -- Entity Type별 노드 모양 구분 -- 신뢰도 낮은 관계 강조 -- 출처 없는 관계 표시 -- 특정 엔티티 중심 그래프 보기 -- 고립 노드 탐지 -- 과도하게 연결된 노드 탐지 -- 노드 클릭 시 상세 정보 표시 -- 그래프 필터링 - -## 예시 - -```text -[포맨트 퍼퓸 코튼 허그] - ├─ hasBrand → [포맨트] - ├─ hasPrice → [44,000원] - ├─ hasBaseNote → [머스크] - └─ belongsToCategory → [퍼퓸] -``` - -## 왜 필요한가 - -온톨로지는 본질적으로 표가 아니라 그래프이다. - -목록 화면만으로는 다음을 파악하기 어렵다. - -- 어떤 엔티티가 중심인가? -- 어떤 관계가 많이 연결되어 있는가? -- 고립된 엔티티가 있는가? -- 잘못 연결된 관계가 있는가? -- 특정 상품이 어떤 의미망을 갖는가? - -그래프 뷰가 있으면 사용자는 온톨로지를 직관적으로 이해할 수 있다. - -## 설득 포인트 - -그래프 뷰는 단순히 보기 좋은 기능이 아니다. 온톨로지의 본질을 드러내는 기능이다. 목록은 데이터를 보여주지만, 그래프는 지식 구조를 보여준다. - ---- - -# 5.10 Ontology Quality Inspector - -## 기능명 - -**Ontology Quality Inspector** - -## 목적 - -구축된 온톨로지의 품질을 자동 진단한다. - -## 주요 기능 - -- 중복 엔티티 수 -- 관계 없는 엔티티 수 -- 출처 없는 클레임 수 -- 신뢰도 낮은 클레임 수 -- 스키마 위반 항목 수 -- 모순 관계 수 -- 잘못된 Predicate 사용 수 -- 빈 값 객체 수 -- 타입 불일치 수 -- 품질 점수 산출 -- 수정 권장 목록 제공 - -## 품질 점수 예시 - -```text -전체 품질 점수: 78 / 100 - -감점 요인: -- 중복 엔티티 후보 31개 -- 출처 없는 클레임 12개 -- 스키마 위반 8개 -- 신뢰도 0.7 미만 클레임 44개 -- 고립 엔티티 19개 -``` - -## 왜 필요한가 - -온톨로지는 시간이 지나면서 오염될 수 있다. 특히 자동 추출 시스템은 대량 데이터를 생성하기 때문에, 사람이 모든 항목을 직접 확인하기 어렵다. - -따라서 시스템이 먼저 문제를 찾아줘야 한다. - -## 설득 포인트 - -온톨로지의 가치는 양이 아니라 신뢰도에서 나온다. 클레임이 10만 개 있어도 그중 절반이 틀리면 쓸 수 없다. Quality Inspector는 온톨로지를 “많은 데이터”가 아니라 “믿을 수 있는 지식”으로 만들기 위한 핵심 기능이다. - ---- - -# 5.11 Extraction Strategy Manager - -## 기능명 - -**Extraction Strategy Manager** - -## 목적 - -페이지 유형별로 어떤 방식으로 정보를 추출할지 설정한다. - -## 주요 기능 - -- CSS Selector 기반 추출 규칙 -- 정규식 기반 추출 규칙 -- AI 기반 추출 규칙 -- 룰 기반 Predicate 생성 -- 페이지 유형별 전략 설정 -- AI 사용 여부 설정 -- Fast / Balanced / Accurate 모드 -- 실패 시 fallback 전략 -- 추출 로그 확인 - -## 추출 전략 예시 - -```text -상품 상세 페이지: -- 상품명: CSS Selector 우선 -- 가격: 정규식 + CSS Selector -- 브랜드: CSS Selector 우선, 실패 시 AI -- 설명문: 본문 정제 후 AI -- 향 노트: AI + 사전 매칭 -``` - -## 왜 필요한가 - -모든 정보를 AI에게 맡기면 비용과 시간이 증가한다. 반대로 모든 것을 규칙으로 처리하면 사이트가 조금만 달라져도 실패한다. - -따라서 좋은 시스템은 다음처럼 움직여야 한다. - -```text -명확한 구조 데이터 → 규칙 기반 -패턴이 뚜렷한 값 → 정규식 기반 -문맥 이해가 필요한 정보 → AI 기반 -최종 품질 관리 → 스키마 검증 -``` - -## 설득 포인트 - -AI는 만능 추출기가 아니라 문맥 해석 도구이다. 빠르고 명확한 작업은 규칙이 처리하고, 애매하고 의미 해석이 필요한 부분만 AI가 처리해야 한다. 그래야 속도, 비용, 정확도를 모두 잡을 수 있다. - ---- - -# 5.12 Export / API Center - -## 기능명 - -**Ontology Export Center** - -## 목적 - -구축된 온톨로지를 외부 시스템에서 활용할 수 있도록 내보낸다. - -## 주요 기능 - -- JSON Export -- CSV Export -- RDF/Turtle Export -- Graph Export -- REST API -- GraphQL API -- 승인된 항목만 Export -- 출처 포함 Export -- 신뢰도 포함 Export -- 버전별 Export - -## 왜 필요한가 - -온톨로지는 구축 자체가 목적이 아니다. 구축된 지식을 다른 시스템에서 활용할 수 있어야 한다. - -예를 들어 다음과 같이 활용할 수 있다. - -- AI 검색 시스템 -- 추천 시스템 -- 상품 비교 시스템 -- 챗봇 지식 베이스 -- GraphRAG 기반 질의응답 -- 내부 데이터 분석 -- 개인화 서비스 - -## 설득 포인트 - -Export가 없으면 온톨로지는 플랫폼 내부에 갇힌 데이터가 된다. API와 Export는 온톨로지를 실제 비즈니스 가치로 연결하는 출구이다. - ---- - -## 6. 사용자 경험 관점에서의 개선 방향 - -### 6.1 현재 메뉴 구조의 문제 - -현재 메뉴는 기능이 나열되어 있지만, 사용자가 온톨로지를 구축하는 흐름을 직관적으로 이해하기 어렵다. - -현재 인상은 다음과 같다. - -```text -대시보드 -프로젝트 생성 -참고 소스 -크롤 진행 -자율 연구 -온톨로지 편집 -관계도 맵 -결과 검토 -``` - -이 구조도 나쁘지는 않지만, “자동 구축 파이프라인”의 흐름이 강하게 드러나지는 않는다. - -### 6.2 추천 메뉴 구조 - -```text -Dashboard -Source Explorer -Build Pipeline -Page Analysis -Schema Designer -Entity Manager -Claim Review -Graph View -Quality Inspector -Export / API -Settings -``` - -한국어로는 다음과 같이 구성할 수 있다. - -```text -대시보드 -소스 탐색 -구축 파이프라인 -페이지 분석 -스키마 설계 -엔티티 관리 -클레임 검토 -그래프 보기 -품질 진단 -내보내기 / API -설정 -``` - -## 왜 이렇게 바꿔야 하는가 - -상용툴은 메뉴만 봐도 사용자가 전체 작업 흐름을 이해할 수 있어야 한다. - -현재는 사용자가 “어디서 시작해서 어디로 가야 하는지” 조금 헷갈릴 수 있다. 반면 위 구조는 다음 흐름을 자연스럽게 만든다. - -```text -소스 탐색 → 구축 실행 → 페이지 분석 → 스키마 관리 → 후보 검토 → 그래프 확인 → 품질 진단 → Export -``` - -즉, 메뉴 자체가 온톨로지 구축 과정의 교육 자료가 된다. - ---- - -## 7. 데이터 모델 제안 - -### 7.1 Project - -```json -{ - "id": "project_001", - "name": "PerfumeSubscribe_new", - "domain": "Perfume", - "status": "active", - "createdAt": "2026-05-15" -} -``` - -### 7.2 SourcePage - -```json -{ - "id": "page_001", - "projectId": "project_001", - "url": "https://example.com/product/123", - "pageType": "product_detail", - "crawlStatus": "crawled", - "analysisStatus": "analyzed", - "cleanText": "...", - "errorMessage": null -} -``` - -### 7.3 Entity - -```json -{ - "id": "entity_001", - "projectId": "project_001", - "type": "Product", - "name": "포맨트 퍼퓸 코튼 허그", - "normalizedName": "포맨트 퍼퓸 코튼 허그", - "sourcePageIds": ["page_001"], - "confidence": 0.92, - "status": "candidate" -} -``` - -### 7.4 Predicate - -```json -{ - "id": "predicate_001", - "name": "hasBrand", - "subjectType": "Product", - "objectType": "Brand", - "description": "상품이 특정 브랜드에 속함을 나타낸다." -} -``` - -### 7.5 Claim - -```json -{ - "id": "claim_001", - "subjectEntityId": "entity_product_001", - "predicateId": "predicate_hasBrand", - "objectEntityId": "entity_brand_001", - "sourcePageId": "page_001", - "evidenceText": "포맨트 퍼퓸 코튼 허그...", - "confidence": 0.91, - "createdBy": "rule_candidate", - "status": "candidate" -} -``` - -### 7.6 ValidationIssue - -```json -{ - "id": "issue_001", - "projectId": "project_001", - "targetType": "claim", - "targetId": "claim_001", - "issueType": "schema_violation", - "severity": "warning", - "message": "Product hasBrand의 object는 Brand 타입이어야 합니다." -} -``` - ---- - -## 8. 시스템이 반드시 지켜야 할 설계 원칙 - -### 8.0 기존 오픈소스 기반 엔진 존중 원칙 - -현재 구축되어 있는 시스템은 처음부터 모든 기능을 직접 만든 것이 아니라, 여러 오픈소스 프로젝트를 검토하고 기능별로 가장 적합한 부분을 선별하여 조합한 구조이다. 즉, 현재의 기본 엔진은 단순한 임시 구현물이 아니라, 이미 어느 정도 검증된 오픈소스 기반 기능들을 활용해 구성된 결과물이다. - -따라서 향후 기능 확장이나 구조 개선을 진행할 때, 기본 엔진 영역은 가능한 한 현재 작업되어 있는 내용을 최대한 활용하는 것을 원칙으로 한다. - -여기서 말하는 기본 엔진 영역은 다음을 포함한다. - -- 웹 크롤링 및 페이지 수집 엔진 -- HTML 정제 및 본문 추출 로직 -- 페이지 분석 전처리 구조 -- 엔티티 후보 추출 로직 -- 클레임 후보 생성 로직 -- 룰 기반 후보 판정 구조 -- 기존 데이터 저장 구조 -- 현재 동작 중인 API 및 내부 처리 흐름 - -중요한 점은, 새로운 기능을 추가한다고 해서 기존 엔진을 쉽게 폐기하거나 대체해서는 안 된다는 것이다. 이미 검토된 오픈소스의 장점을 활용한 부분은 시스템의 기반 자산으로 보아야 한다. - -다만 다음 경우에는 수정을 검토할 수 있다. - -1. 현재 엔진에 해당 기능이 아예 없는 경우 -2. 현재 구조로는 요구 기능을 구현하기 어려운 경우 -3. 성능, 정확도, 안정성에 명확한 문제가 확인된 경우 -4. 기존 오픈소스 기반 구현이 현재 제품 방향과 충돌하는 경우 -5. 유지보수 비용이 지나치게 커지는 경우 - -이 경우에도 즉시 임의로 변경하는 것이 아니라, 먼저 문제점과 수정 필요성을 정리하고 승인을 받은 뒤 수정해야 한다. - -즉, 앞으로의 개발 원칙은 다음과 같다. - -```text -기존 엔진은 최대한 재사용한다. -부족한 기능은 그 위에 확장한다. -문제가 있는 부분만 근거를 제시하고 승인 후 수정한다. -검증된 오픈소스 기반 구조를 무리하게 다시 만들지 않는다. -``` - -이 원칙은 개발 속도와 안정성을 동시에 확보하기 위해 중요하다. 온톨로지 구축 플랫폼은 크롤링, 정제, 추출, 검증, 저장, 시각화 등 많은 기능이 연결된 복합 시스템이다. 이미 동작하는 기반을 무리하게 갈아엎으면, 새로운 기능을 추가하기보다 기존 기능을 다시 복구하는 데 시간을 낭비할 수 있다. - -따라서 본 보고서에서 제안하는 Dashboard, Source Explorer, Build Pipeline, Page Analysis Viewer, Schema Designer, Quality Inspector 등의 기능은 기존 엔진을 부정하고 새로 만들자는 의미가 아니다. 오히려 현재 엔진을 제품화 가능한 플랫폼 구조로 감싸고, 부족한 관리·검증·시각화·승인 기능을 보강하자는 방향이다. - -정리하면 다음과 같다. - -> 현재의 오픈소스 기반 엔진은 플랫폼의 기반 자산으로 유지한다. 새로운 개발은 이 엔진을 최대한 활용하는 방향으로 진행하며, 기능 부재나 명확한 문제가 있는 경우에만 승인 절차를 거쳐 수정한다. - - - -### 8.1 원문 보존 원칙 - -AI가 분석하기 전의 원문 HTML과 정제 텍스트를 보존해야 한다. - -이유는 다음과 같다. - -- 추출 결과를 검증할 수 있다. -- 추후 알고리즘 개선 시 재분석할 수 있다. -- 데이터 출처를 추적할 수 있다. - -### 8.2 근거 연결 원칙 - -모든 Claim은 가능하면 sourceUrl과 evidenceText를 가져야 한다. - -근거 없는 Claim은 신뢰할 수 없다. - -### 8.3 후보와 확정 분리 원칙 - -AI가 만든 결과는 즉시 확정하지 않는다. - -```text -AI 생성 → candidate -사용자 승인 → approved -사용자 반려 → rejected -``` - -이 구조가 있어야 사람이 통제할 수 있다. - -### 8.4 스키마 우선 원칙 - -AI가 만든 결과도 스키마를 통과해야 한다. - -```text -Product hasBrand Brand = 허용 -Product hasBrand Price = 오류 -빈 문자열 hasPrice 44,000원 = 오류 -``` - -### 8.5 혼합 추출 원칙 - -모든 것을 AI로 처리하지 않는다. - -```text -빠른 추출: CSS Selector -정형 값: Regex -문맥 해석: AI -최종 검증: Schema -``` - -### 8.6 사람이 읽을 수 있는 표시 원칙 - -화면에는 내부 객체가 그대로 표시되면 안 된다. - -```text -나쁜 예: [object Object] -좋은 예: 44,000원 -더 좋은 예: 44,000원 · KRW · Price -``` - -### 8.7 품질 점수화 원칙 - -온톨로지 상태를 사람이 감으로 판단하게 하면 안 된다. 품질 지표를 수치화해야 한다. - ---- - -## 9. 개발 우선순위 제안 - -### 1단계: 현재 시스템을 상용툴처럼 보이게 만드는 최소 개선 - -우선순위가 가장 높다. - -- `[object Object]` 표시 문제 해결 -- 클레임 상세 패널 추가 -- sourceUrl/evidenceText 표시 -- candidate/approved/rejected 상태 추가 -- 클레임 필터 추가 -- 신뢰도 낮은 항목 정렬 -- 대시보드 지표 강화 - -### 2단계: 자동 구축 흐름을 보여주는 기능 - -- Build Pipeline 화면 추가 -- 단계별 진행률 표시 -- 페이지별 상태 표시 -- 재실행 버튼 추가 -- 오류 로그 추가 - -### 3단계: 소스 탐색과 페이지 분석 강화 - -- Source Explorer 추가 -- 페이지 유형 분류 -- 원문/정제 본문 보기 -- 추출 근거 하이라이트 - -### 4단계: 스키마 설계와 검증 강화 - -- Entity Type 관리 -- Predicate 관리 -- 관계 규칙 관리 -- 스키마 위반 검사 - -### 5단계: 그래프와 품질 진단 - -- Graph View -- Quality Inspector -- 중복/모순/고립 노드 탐지 - -### 6단계: Export/API - -- JSON Export -- CSV Export -- RDF/Turtle Export -- REST API - ---- - -## 10. 현재 화면을 기준으로 한 구체적 개선 제안 - -현재 화면에서 가장 먼저 개선해야 할 부분은 다음과 같다. - -### 10.1 클레임 목록 개선 - -현재는 문장, Predicate, Object가 섞여 보인다. 다음처럼 카드형으로 개선하는 것이 좋다. - -```text -[Candidate] 포맨트 퍼퓸 코튼 허그 -hasBrand → 포맨트 -Confidence 0.83 · Source: forment · Evidence 있음 -[승인] [반려] [상세보기] -``` - -### 10.2 클레임 상세 패널 추가 - -클레임 클릭 시 오른쪽 패널에서 다음을 보여준다. - -```text -Subject -Predicate -Object -Entity Type -Source URL -Evidence Text -Created By -Confidence -Validation Result -History -``` - -### 10.3 Source 연결 강화 - -현재 `소스: forment` 정도로 보이지만, 실제로는 더 많은 정보가 필요하다. - -- 사이트명 -- URL -- 페이지 제목 -- 페이지 유형 -- 수집 시간 -- 분석 시간 -- 원문 보기 - -### 10.4 신뢰도 활용 강화 - -신뢰도는 단순 표시가 아니라 행동을 유도해야 한다. - -예: - -```text -0.9 이상: 자동 승인 후보 -0.7~0.9: 일반 검토 -0.7 미만: 우선 검토 필요 -``` - -### 10.5 잘못된 값 표시 방지 - -`[object Object]` 같은 값은 반드시 렌더링 계층에서 방어해야 한다. - -표시 함수는 object 타입을 감지해 사람이 읽을 수 있는 label, name, value를 우선 사용해야 한다. - ---- - -## 11. 이 시스템이 지향해야 할 제품 정체성 - -이 시스템은 단순한 웹 크롤러가 아니다. - -또한 단순한 데이터 편집기도 아니다. - -이 시스템은 다음과 같이 정의하는 것이 좋다. - -> 웹사이트를 탐색하고, 페이지의 의미를 분석하며, 핵심 개체와 관계를 추출하고, 검증 가능한 온톨로지로 구축하는 AI 기반 지식 구조화 플랫폼. - -제품 메시지는 다음과 같이 잡을 수 있다. - -```text -웹사이트를 지식 그래프로 바꾸는 AI 온톨로지 구축 플랫폼 -``` - -또는 - -```text -크롤링을 넘어, 웹 데이터를 의미 구조로 전환하는 온톨로지 자동 구축 도구 -``` - ---- - -## 12. 왜 이 방향이 상용화에 유리한가 - -### 12.1 단순 크롤러는 차별화가 어렵다 - -크롤러는 이미 많다. HTML을 가져오고 텍스트를 추출하는 수준만으로는 제품 경쟁력이 약하다. - -### 12.2 단순 AI 추출도 차별화가 어렵다 - -LLM에게 “이 페이지에서 상품명과 가격을 뽑아줘”라고 하는 기능만으로도 부족하다. 이것은 누구나 만들 수 있다. - -### 12.3 온톨로지 구축 플랫폼은 더 높은 가치가 있다 - -온톨로지는 단순 데이터보다 활용 범위가 넓다. - -- 검색 -- 추천 -- 질의응답 -- 분석 -- 개인화 -- 지식 그래프 -- GraphRAG -- 자동 보고서 생성 -- 도메인 지식 관리 - -즉, 이 시스템의 진짜 가치는 “데이터 수집”이 아니라 “의미 구조화”에 있다. - -### 12.4 사용자는 자동화와 통제를 모두 원한다 - -기업 사용자는 AI가 자동으로 해주는 것을 원하지만, 동시에 잘못된 결과를 통제할 수 있기를 원한다. - -따라서 다음 구조가 중요하다. - -```text -AI 자동 생성 + 사람이 검증 + 시스템이 품질 진단 -``` - -이 구조를 갖추면 단순 AI 도구보다 신뢰도가 높아진다. - ---- - -## 13. 핵심 결론 - -현재 시스템은 온톨로지 구축의 데이터 구조를 일부 갖추고 있지만, 아직 상용 플랫폼처럼 보이기에는 부족하다. - -가장 큰 문제는 기능의 부재라기보다 **온톨로지가 구축되는 과정이 사용자에게 보이지 않는 것**이다. - -상용화 가능한 플랫폼으로 발전하려면 다음 방향으로 가야 한다. - -1. 목록 편집 중심에서 구축 파이프라인 중심으로 전환한다. -2. 클레임 결과만 보여주지 말고 추출 근거를 보여준다. -3. 스키마 설계 기능을 추가해 AI 추출의 기준을 만든다. -4. 후보와 승인 데이터를 분리해 검증 가능한 워크플로우를 만든다. -5. 그래프 뷰로 온톨로지의 관계 구조를 보여준다. -6. 품질 진단으로 데이터 신뢰도를 관리한다. -7. Export/API로 실제 활용 가치를 만든다. - -한 문장으로 정리하면 다음과 같다. - -> 이 시스템은 “웹페이지에서 데이터를 긁어오는 도구”가 아니라, “웹사이트를 탐색해 의미 있는 지식 구조로 변환하고, 검증 가능한 온톨로지로 확정하는 플랫폼”이 되어야 한다. - ---- - -## 14. 최종 권장 개발 방향 - -당장 모든 기능을 한 번에 만들 필요는 없다. 그러나 제품 방향은 처음부터 명확해야 한다. - -가장 먼저 해야 할 것은 다음이다. - -```text -1. Build Pipeline 화면 추가 -2. Page Analysis Viewer 추가 -3. Claim Review 구조 개선 -4. Schema Designer 기본형 추가 -5. Quality Inspector 기본형 추가 -``` - -이 다섯 가지가 들어가면 시스템의 인상이 크게 달라진다. - -현재는 “클레임을 편집하는 화면”처럼 보이지만, 위 기능들이 들어가면 “웹사이트를 분석해 온톨로지를 자동 구축하는 플랫폼”으로 보이기 시작한다. - ---- - -## 15. 부록: 핵심 기능 요약표 - -| 기능 | 목적 | 핵심 가치 | -|---|---|---| -| Dashboard | 전체 상태 확인 | 제품 신뢰도 | -| Source Explorer | 분석 대상 관리 | 입력 품질 관리 | -| Build Pipeline | 구축 단계 실행 | 자동화 체감 | -| Page Analysis Viewer | 추출 근거 확인 | AI 결과 신뢰성 | -| Schema Designer | 구조 규칙 정의 | 데이터 일관성 | -| Entity Extraction Engine | 핵심 개체 추출 | 온톨로지 기초 품질 | -| Claim Builder | 관계 생성 | 지식 구조화 | -| Review Center | 후보 검증 | 통제 가능한 자동화 | -| Graph View | 관계 시각화 | 온톨로지 체감 | -| Quality Inspector | 품질 진단 | 신뢰 가능한 지식 | -| Strategy Manager | 추출 방식 제어 | 속도/비용/정확도 균형 | -| Export/API | 외부 활용 | 비즈니스 가치 | - ---- - -## 16. 부록: 한 문장 제품 정의 후보 - -1. 웹사이트를 지식 그래프로 바꾸는 AI 온톨로지 구축 플랫폼 -2. 크롤링 데이터를 의미 구조로 전환하는 자동 온톨로지 빌더 -3. 웹페이지를 탐색하고, 엔티티와 관계를 추출해 검증 가능한 지식 베이스로 만드는 플랫폼 -4. AI 기반 웹 지식 구조화 및 온톨로지 구축 도구 -5. 웹 데이터를 수집하는 것을 넘어, 의미를 구축하는 온톨로지 플랫폼 - ---- - -## 17. 부록: 개발자에게 전달할 핵심 프롬프트 초안 - -다음은 개발자 또는 코딩 에이전트에게 전달할 수 있는 작업 지시 요약이다. - -```text -현재 온톨로지 구축 툴은 엔티티와 클레임 목록을 보여주는 수준에 머물러 있어 상용 플랫폼처럼 보이지 않는다. -목표는 웹사이트를 탐색해 페이지를 수집하고, 정제하고, 페이지 유형을 분류하고, 엔티티와 클레임을 추출하며, 사용자가 근거를 검토한 뒤 승인된 항목만 온톨로지로 확정하는 AI 기반 온톨로지 구축 플랫폼으로 확장하는 것이다. - -우선 다음 기능을 추가한다. - -1. Build Pipeline 화면 -- Source Crawl, Page Clean, Page Classification, Entity Extraction, Claim Generation, Deduplication, Validation, Human Review, Ontology Commit, Export 단계 표시 -- 각 단계별 상태, 진행률, 오류 수, 재실행 버튼 제공 - -2. Page Analysis Viewer -- 원문 HTML, 정제 텍스트, 추출 엔티티, 생성 클레임, 근거 문장을 한 화면에서 확인 -- 클레임별 evidenceText와 sourceUrl 표시 - -3. Claim Review 개선 -- candidate, approved, rejected, pending 상태 도입 -- 신뢰도, 출처, 생성 방식, 검증 결과 표시 -- [object Object] 표시 문제 제거 -- 사람이 읽을 수 있는 label/name/value 우선 표시 - -4. Schema Designer 기본형 -- Entity Type, Predicate, Subject Type, Object Type, 값 타입, 필수 여부 정의 -- 스키마 위반 검사에 활용 - -5. Quality Inspector 기본형 -- 중복 엔티티, 출처 없는 클레임, 스키마 위반, 신뢰도 낮은 항목, 고립 엔티티 표시 - -중요 원칙: -- AI 생성 결과는 즉시 확정하지 말고 candidate로 저장한다. -- 모든 Claim은 sourceUrl, evidenceText, confidence, createdBy, status를 가져야 한다. -- 스키마에 맞지 않는 Claim은 validation issue로 표시한다. -- UI에는 내부 object를 그대로 출력하지 말고 사람이 읽을 수 있는 문자열로 렌더링한다. -- 이 시스템은 단순 크롤러가 아니라 웹사이트를 의미 구조로 변환하는 온톨로지 구축 플랫폼이다. -``` - diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 4e70c13..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[project] -name = "ontology-platform" -version = "0.2.0" -description = "Fast ontology extraction platform with lightweight JSON-based candidate extraction and optional OntoCast RDF refinement." -requires-python = ">=3.12" -dependencies = [ - "fastapi>=0.110", - "pydantic>=2", - "pydantic-settings>=2", - "PyYAML>=6", - "requests>=2.31", - "trafilatura[all]>=2.0.0", - "uvicorn[standard]>=0.29", -] - -[project.optional-dependencies] -test = ["pytest>=8", "pytest-asyncio>=0.23"] - -[tool.pytest.ini_options] -testpaths = ["tests"] -pythonpath = ["."] -addopts = "-p no:cacheprovider" -asyncio_mode = "auto" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 113df73..0000000 --- a/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -beautifulsoup4>=4.12 -fastapi>=0.110 -playwright>=1.44 -pydantic>=2 -pytest>=8 -PyYAML>=6 -requests>=2.31 -SQLAlchemy>=2 -uvicorn[standard]>=0.29 -redis>=5.0 -openai>=1.0 -anthropic>=0.25 -httpx>=0.25 -sentence-transformers>=2.2 -numpy>=1.20 -python-multipart>=0.0.6 - diff --git a/start_webserver.bat b/start_webserver.bat deleted file mode 100644 index bfc2734..0000000 --- a/start_webserver.bat +++ /dev/null @@ -1,54 +0,0 @@ -@echo off -setlocal - -set "SCRIPT_DIR=%~dp0" -cd /d "%SCRIPT_DIR%" - -set "HOST=127.0.0.1" -set "PORT=8000" -set "URL=http://%HOST%:%PORT%/" -set "BUNDLED_PYTHON=C:\Users\lasta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe" -set "PYTHON_EXE=" - -if exist "%SCRIPT_DIR%.venv\Scripts\python.exe" set "PYTHON_EXE=%SCRIPT_DIR%.venv\Scripts\python.exe" -if not defined PYTHON_EXE if exist "%SCRIPT_DIR%venv\Scripts\python.exe" set "PYTHON_EXE=%SCRIPT_DIR%venv\Scripts\python.exe" -if not defined PYTHON_EXE if exist "%BUNDLED_PYTHON%" set "PYTHON_EXE=%BUNDLED_PYTHON%" - -if not defined PYTHON_EXE ( - echo [OCP] Python executable not found. - echo [OCP] Put Python 3.11+ in .venv\Scripts\python.exe or install the Codex bundled runtime. - pause - exit /b 1 -) - -"%PYTHON_EXE%" -c "import fastapi, uvicorn" >nul 2>nul -if errorlevel 1 ( - echo [OCP] Missing Python dependencies. - echo [OCP] Run: "%PYTHON_EXE%" -m pip install -r requirements.txt - pause - exit /b 1 -) - -powershell -NoProfile -Command "try { $r = Invoke-RestMethod -Uri '%URL%health' -TimeoutSec 2; if ($r.ok) { exit 0 } else { exit 1 } } catch { exit 1 }" >nul 2>nul -if not errorlevel 1 ( - echo [OCP] Existing server is already running at %URL% - start "" "%URL%" - exit /b 0 -) - -set "CRAWLER_DATABASE_URL=sqlite:///crawler_platform.db" -title Ontology Crawler Web Server - -echo [OCP] Starting server with: %PYTHON_EXE% -echo [OCP] URL: %URL% -echo [OCP] Press Ctrl+C to stop the server. - -start "" "%URL%" -"%PYTHON_EXE%" -m uvicorn crawler_platform.app.main:app --host %HOST% --port %PORT% - -set "EXIT_CODE=%ERRORLEVEL%" -echo. -echo [OCP] Server stopped. -if not "%EXIT_CODE%"=="0" echo [OCP] Exit code: %EXIT_CODE% -pause -exit /b %EXIT_CODE% diff --git a/test_extraction.py b/test_extraction.py deleted file mode 100644 index 84eaf3e..0000000 --- a/test_extraction.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 0 extraction test.""" - -import sys -import time -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.extractors.web_extractor import extract_web_content -from ont_platform.core.extraction.lightweight_extractor import LightweightExtractor - - -def test_extraction(url: str): - """Test Phase 0 extraction.""" - print(f"\nURL: {url}\n") - - start = time.time() - - try: - print("1. Extracting content with Trafilatura...") - extracted = extract_web_content(url=url) - print(f" Title: {extracted.title}") - print(f" Length: {len(extracted.text)} chars") - - print("\n2. Extracting JSON candidates...") - extractor = LightweightExtractor(use_llm=False) - result = extractor.extract( - text=extracted.text, - project_id="test", - document_id="test", - ) - - elapsed = time.time() - start - - print(f" Entities: {len(result.entities)}") - print(f" Relations: {len(result.relations)}") - print(f" Time: {elapsed:.2f}s") - - if result.entities: - print(f"\n Top entities:") - for e in result.entities[:3]: - print(f" - {e['label']} ({e['type']}) [{e['confidence']}]") - - if elapsed <= 30: - print(f"\nSUCCESS: {elapsed:.2f}s <= 30s target") - return True - else: - print(f"\nFAIL: {elapsed:.2f}s > 30s target") - return False - - except Exception as e: - print(f"ERROR: {e}") - import traceback - traceback.print_exc() - return False - - -if __name__ == "__main__": - url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com" - success = test_extraction(url) - sys.exit(0 if success else 1) diff --git a/test_phase0_extraction.py b/test_phase0_extraction.py deleted file mode 100644 index df3ff01..0000000 --- a/test_phase0_extraction.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -""" -Phase 0 extraction test: Extract candidates from a URL. - -Usage: - python test_phase0_extraction.py - -Example: - python test_phase0_extraction.py https://example.com -""" - -import sys -import time -from pathlib import Path - -# Add ont_platform to path -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.extractors.web_extractor import extract_web_content -from ont_platform.core.extraction.lightweight_extractor import LightweightExtractor - - -def test_extraction(url: str): - """Test Phase 0 extraction on a URL.""" - print(f"\n🔗 Extracting from: {url}\n") - - start_time = time.time() - - try: - # Step 1: Extract web content - print("📄 Step 1: Extracting web content with Trafilatura...") - extracted = extract_web_content(url=url) - - print(f" ✓ Title: {extracted.title}") - print(f" ✓ Length: {len(extracted.text)} chars") - print(f" ✓ Language: {extracted.language}") - - # Step 2: Extract JSON candidates - print("\n🎯 Step 2: Extracting JSON candidates...") - lightweight = LightweightExtractor(use_llm=False) - candidates = lightweight.extract( - text=extracted.text, - project_id="test", - document_id="test_doc", - ) - - print(f" ✓ Entities: {len(candidates.entities)}") - print(f" ✓ Relations: {len(candidates.relations)}") - if candidates.warnings: - print(f" ⚠️ Warnings: {len(candidates.warnings)}") - for w in candidates.warnings[:3]: - print(f" - {w}") - - elapsed = time.time() - start_time - - # Display results - print(f"\n📊 Results:") - print(f" Total time: {elapsed:.2f} seconds") - print(f"\n Entities ({len(candidates.entities)}):") - for e in candidates.entities[:5]: - print(f" - {e.label} ({e.entity_type}) [confidence: {e.confidence:.2f}]") - if len(candidates.entities) > 5: - print(f" ... and {len(candidates.entities) - 5} more") - - print(f"\n Relations ({len(candidates.relations)}):") - for r in candidates.relations[:3]: - print( - f" - {r.source_entity_id} --{r.predicate}--> {r.target_entity_id}" - ) - if len(candidates.relations) > 3: - print(f" ... and {len(candidates.relations) - 3} more") - - # Check if within Phase 0 goal - if elapsed <= 30: - print(f"\n✅ Phase 0 Goal Achieved: {elapsed:.2f}s <= 30s") - else: - print(f"\n⚠️ Phase 0 Goal Not Met: {elapsed:.2f}s > 30s") - - return True - - except Exception as e: - print(f"\n❌ Error: {e}") - import traceback - - traceback.print_exc() - return False - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: python test_phase0_extraction.py ") - print("Example: python test_phase0_extraction.py https://www.wikipedia.org/wiki/Python_(programming_language)") - sys.exit(1) - - url = sys.argv[1] - success = test_extraction(url) - sys.exit(0 if success else 1) diff --git a/test_phase2_crawl.py b/test_phase2_crawl.py deleted file mode 100644 index 2e077a1..0000000 --- a/test_phase2_crawl.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 2 Crawl4AI integration test.""" - -import asyncio -import sys -import time -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.crawler.crawl4ai_adapter import ( - Crawl4AIAdapter, - CrawlProfile, -) - - -async def test_crawl_profile(url: str, profile: CrawlProfile): - """Test crawling with specific profile.""" - print(f"\nTesting {profile.value} profile on {url}\n") - - adapter = Crawl4AIAdapter() - start = time.time() - - try: - result = await adapter.crawl(url, profile=profile) - elapsed = time.time() - start - - print(f"[OK] Status: {result.status_code}") - print(f"[OK] Profile used: {result.profile_used}") - print(f"[OK] HTML length: {len(result.html)} chars") - if result.markdown: - print(f"[OK] Markdown length: {len(result.markdown)} chars") - print(f"[OK] Time: {elapsed:.2f}s") - - return True - - except Exception as e: - print(f"[ERROR] {e}") - return False - - finally: - await adapter.close() - - -async def main(): - """Run Phase 2 tests.""" - # Test fast_static (should work like Phase 0/1) - success_static = await test_crawl_profile( - "https://example.com", - CrawlProfile.FAST_STATIC, - ) - - if not success_static: - print("\n[FAILED] Static crawl failed!") - return False - - print("\n[SUCCESS] Phase 2 MVP complete: Crawl4AI adapter working") - print(" - Fast static crawling (Phase 0/1 compatibility)") - print(" - Ready for dynamic_page profile (requires Playwright setup)") - - return True - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase3_option_b.py b/test_phase3_option_b.py deleted file mode 100644 index 5a8f1a8..0000000 --- a/test_phase3_option_b.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 3 Option B: OntoCast GraphUpdate validation test.""" - -import asyncio -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.validation import OntoCastValidator, SPARQLValidator - - -async def test_valid_sparql(): - """Test valid SPARQL query validation.""" - print("\n[TEST 1] Valid SPARQL INSERT operation") - - validator = OntoCastValidator(strict=False) - - update = { - "operations": [ - { - "operation_type": "INSERT", - "query": """ - PREFIX ex: - INSERT DATA { - ex:resource1 a ex:Class; - ex:property1 "value" . - } - """, - "description": "Insert new resource" - } - ], - "namespaces": { - "ex": "http://example.org/" - } - } - - result = await validator.validate(update) - print(f" Validation passed: {result.validation_passed}") - print(f" Errors: {len(result.validation_errors)}") - print(f" Warnings: {len(result.validation_warnings)}") - assert result.validation_passed - print(" [PASS]") - - -async def test_invalid_syntax(): - """Test invalid SPARQL syntax detection.""" - print("\n[TEST 2] Invalid SPARQL syntax") - - validator = OntoCastValidator(strict=False) - - update = { - "operations": [ - { - "operation_type": "INSERT", - "query": "INSERT { ex:s ex:p ex:o ", # Missing closing brace - "description": "Broken query" - } - ], - "namespaces": {} - } - - result = await validator.validate(update) - print(f" Validation passed: {result.validation_passed}") - print(f" Errors: {result.validation_errors[:1] if result.validation_errors else []}") - assert not result.validation_passed - assert len(result.validation_errors) > 0 - print(" [PASS]") - - -async def test_operation_order(): - """Test SPARQL operation order validation.""" - print("\n[TEST 3] Safe operation order (INSERT → UPDATE → DELETE)") - - validator = OntoCastValidator(strict=False) - - update = { - "operations": [ - { - "operation_type": "INSERT", - "query": "INSERT DATA { }", - }, - { - "operation_type": "UPDATE", - "query": "DELETE { } INSERT { }", - }, - { - "operation_type": "DELETE", - "query": "DELETE DATA { }", - }, - ], - "namespaces": {} - } - - result = await validator.validate(update) - print(f" Validation passed: {result.validation_passed}") - print(f" Errors: {len(result.validation_errors)}") - assert result.validation_passed - print(" [PASS]") - - -async def test_unsafe_order(): - """Test unsafe operation order detection.""" - print("\n[TEST 4] Unsafe operation order (DELETE before INSERT)") - - validator = OntoCastValidator(strict=False) - - update = { - "operations": [ - { - "operation_type": "DELETE", - "query": "DELETE DATA { }", - }, - { - "operation_type": "INSERT", - "query": "INSERT DATA { }", - }, - ], - "namespaces": {} - } - - result = await validator.validate(update) - print(f" Validation passed: {result.validation_passed}") - print(f" Errors: {result.validation_errors[:1] if result.validation_errors else []}") - assert not result.validation_passed - assert any("order" in e.lower() for e in result.validation_errors) - print(" [PASS]") - - -async def test_prefix_validation(): - """Test prefix declaration validation.""" - print("\n[TEST 5] Undeclared prefix detection") - - validator = OntoCastValidator(strict=False) - - update = { - "operations": [ - { - "operation_type": "INSERT", - "query": "INSERT DATA { foo:s foo:p foo:o }", # foo prefix not declared - } - ], - "namespaces": { - "ex": "http://example.org/" - } - } - - result = await validator.validate(update) - print(f" Validation passed: {result.validation_passed}") - print(f" Warnings: {result.validation_warnings[:1] if result.validation_warnings else []}") - assert len(result.validation_warnings) > 0 - print(" [PASS]") - - -async def test_sparql_validator_directly(): - """Test SPARQLValidator utility functions.""" - print("\n[TEST 6] SPARQLValidator utility functions") - - # Test syntax validation - valid, errors = SPARQLValidator.validate_sparql_syntax( - "INSERT DATA { }" - ) - assert valid - print(f" Valid syntax check: OK") - - # Test empty query - valid, errors = SPARQLValidator.validate_sparql_syntax("") - assert not valid - assert any("empty" in e.lower() for e in errors) - print(f" Empty query detection: OK") - - # Test unbalanced brackets - valid, errors = SPARQLValidator.validate_sparql_syntax("INSERT { ") - assert not valid - print(f" Unbalanced bracket detection: OK") - - print(" [PASS]") - - -async def main(): - """Run all tests.""" - print("=" * 60) - print("Phase 3 Option B: OntoCast GraphUpdate Validation Tests") - print("(Hybrid approach - SPARQL validation, Critic loop prepared)") - print("=" * 60) - - try: - await test_valid_sparql() - await test_invalid_syntax() - await test_operation_order() - await test_unsafe_order() - await test_prefix_validation() - await test_sparql_validator_directly() - - print("\n" + "=" * 60) - print("All tests passed!") - print("=" * 60) - print("\nPhase 3 Option B capabilities:") - print(" [OK] SPARQL syntax validation") - print(" [OK] Safe operation ordering (INSERT -> UPDATE -> DELETE)") - print(" [OK] Prefix declaration checking") - print(" [OK] Injection pattern detection") - print(" [OK] Balanced bracket validation") - print("\nFuture extensions:") - print(" [>>] Critic loop integration (Phase 4)") - print(" [>>] Full RDF consistency checks (when Fuseki available)") - print(" [>>] GraphUpdate tracing and audit log") - - return True - except AssertionError as e: - print(f"\nTest failed: {e}") - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase3_validation.py b/test_phase3_validation.py deleted file mode 100644 index 47e1b5e..0000000 --- a/test_phase3_validation.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 3 validation test.""" - -import asyncio -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.validation import ( - OntologyGuard, - OntologyEntity, - OntologyRelation, -) - - -async def test_valid_extraction(): - """Test valid extraction result.""" - print("\n[TEST 1] Valid extraction result") - - guard = OntologyGuard(validator_type="lightweight", strict=False) - - result = { - "entities": [ - { - "id": "E_001", - "label": "Python", - "type": "concept", - "confidence": 0.9, - }, - { - "id": "E_002", - "label": "Programming", - "type": "concept", - "confidence": 0.85, - }, - ], - "relations": [ - { - "id": "R_001", - "source_id": "E_001", - "target_id": "E_002", - "predicate": "is_used_for", - "confidence": 0.8, - }, - ], - "warnings": [], - } - - validated = await guard.validate(result) - print(f" Validation passed: {validated.validation_passed}") - print(f" Entities: {len(validated.entities)}") - print(f" Relations: {len(validated.relations)}") - assert validated.validation_passed - print(" [PASS]") - - -async def test_invalid_entity_id(): - """Test validation catches invalid entity ID.""" - print("\n[TEST 2] Invalid entity ID format") - - guard = OntologyGuard(validator_type="lightweight", strict=False) - - result = { - "entities": [ - { - "id": "INVALID_123", # Should start with E_ - "label": "Test", - "type": "concept", - "confidence": 0.9, - }, - ], - "relations": [], - "warnings": [], - } - - validated = await guard.validate(result) - print(f" Validation passed: {validated.validation_passed}") - print(f" Validation errors: {len(validated.validation_errors)}") - print(f" Warnings: {validated.warnings[:1]}") - assert not validated.validation_passed - assert len(validated.validation_errors) > 0 - print(" [PASS]") - - -async def test_missing_relation_endpoint(): - """Test validation catches missing relation endpoints.""" - print("\n[TEST 3] Missing relation endpoint") - - guard = OntologyGuard(validator_type="lightweight", strict=False) - - result = { - "entities": [ - { - "id": "E_001", - "label": "Python", - "type": "concept", - "confidence": 0.9, - }, - ], - "relations": [ - { - "id": "R_001", - "source_id": "E_001", - "target_id": "E_999", # Non-existent entity - "predicate": "uses", - "confidence": 0.8, - }, - ], - "warnings": [], - } - - validated = await guard.validate(result) - print(f" Validation passed: {validated.validation_passed}") - print(f" Validation errors: {len(validated.validation_errors)}") - assert not validated.validation_passed - print(" [PASS]") - - -async def test_confidence_range(): - """Test validation checks confidence range.""" - print("\n[TEST 4] Confidence range validation") - - guard = OntologyGuard(validator_type="lightweight", strict=False) - - result = { - "entities": [ - { - "id": "E_001", - "label": "Test", - "type": "concept", - "confidence": 1.5, # Out of range [0.0, 1.0] - }, - ], - "relations": [], - "warnings": [], - } - - validated = await guard.validate(result) - print(f" Validation passed: {validated.validation_passed}") - print(f" Validation errors: {len(validated.validation_errors)}") - assert not validated.validation_passed - print(" [PASS]") - - -async def test_self_relation(): - """Test validation rejects self-relations.""" - print("\n[TEST 5] Self-relation validation") - - guard = OntologyGuard(validator_type="lightweight", strict=False) - - result = { - "entities": [ - { - "id": "E_001", - "label": "Test", - "type": "concept", - "confidence": 0.9, - }, - ], - "relations": [ - { - "id": "R_001", - "source_id": "E_001", - "target_id": "E_001", # Self-loop - "predicate": "relates_to", - "confidence": 0.8, - }, - ], - "warnings": [], - } - - validated = await guard.validate(result) - print(f" Validation passed: {validated.validation_passed}") - print(f" Validation errors: {len(validated.validation_errors)}") - assert not validated.validation_passed - print(" [PASS]") - - -async def main(): - """Run all tests.""" - print("=" * 60) - print("Phase 3: Validation Tests (Lightweight MVP)") - print("=" * 60) - - try: - await test_valid_extraction() - await test_invalid_entity_id() - await test_missing_relation_endpoint() - await test_confidence_range() - await test_self_relation() - - print("\n" + "=" * 60) - print("All tests passed!") - print("=" * 60) - print("\nValidation capabilities:") - print(" - Entity ID format (E_xxxxx)") - print(" - Confidence range [0.0, 1.0]") - print(" - Relation endpoint existence") - print(" - Self-relation prevention") - print(" - Field length constraints") - print("\nUpgrade path (Optional B):") - print(" - Guardrails: ValidatorFactory.create('guardrails')") - print(" - OntoCast: ValidatorFactory.create('ontocast')") - - return True - except AssertionError as e: - print(f"\nTest failed: {e}") - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase4_integration.py b/test_phase4_integration.py deleted file mode 100644 index 7ef3ad9..0000000 --- a/test_phase4_integration.py +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 4 Integration Test: End-to-end extraction → ingestion → search pipeline. - -This test validates: -- Phase 0-1: URL extraction (Trafilatura) -- Phase 2: Dynamic page crawling (Crawl4AI) -- Phase 3: Validation (LightweightValidator + OntoCastValidator) -- Phase 4: Neo4j ingestion and vector search - -Note: Requires Neo4j running on localhost:7687 -""" - -import asyncio -import sys -from pathlib import Path -from typing import Dict, Any, List - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.extractors.web_extractor import extract_web_content -from ont_platform.core.extraction.lightweight_extractor import LightweightExtractor -from ont_platform.core.validation import OntologyGuard -from ont_platform.core.graph.neo4j_adapter import Neo4jAdapter, Neo4jConfig - - -async def test_phase4_neo4j_connection(): - """Test Neo4j adapter connection.""" - print("\n[TEST 1] Neo4j Connection") - - try: - adapter = Neo4jAdapter() - connected = await adapter.connect() - - if connected: - print(" [OK] Connected to Neo4j at localhost:7687") - await adapter.close() - return True - else: - print(" [WARNING] Neo4j not available") - print(" To run Neo4j: docker-compose -f docker-compose.neo4j.yml up -d") - return False - except Exception as e: - print(f" [SKIP] Neo4j test skipped: {e}") - return False - - -async def test_phase4_embedder_init(): - """Test embedding model initialization.""" - print("\n[TEST 2] Embedding Model Initialization") - - try: - adapter = Neo4jAdapter() - await adapter.initialize_embedder() - - # Test embedding a simple text - test_text = "Machine learning" - embeddings = adapter._get_embeddings([test_text]) - - assert len(embeddings) == 1 - assert len(embeddings[0]) == 384 # all-MiniLM-L6-v2 produces 384-dim vectors - - print(f" [OK] Loaded embedding model (384-dimensional vectors)") - print(f" [OK] Successfully embedded test phrase") - return True - except Exception as e: - print(f" [SKIP] Embedder test skipped: {e}") - print(" To install: pip install sentence-transformers") - return False - - -async def test_phase4_entity_creation(): - """Test entity node creation with embeddings.""" - print("\n[TEST 3] Entity Node Creation") - - try: - adapter = Neo4jAdapter() - if not await adapter.connect(): - print(" [SKIP] Neo4j not available") - return False - - await adapter.initialize_embedder() - - # Create test entities - test_entities = [ - { - "id": "E_test_1", - "label": "Machine Learning", - "type": "concept", - "confidence": 0.95 - }, - { - "id": "E_test_2", - "label": "Neural Networks", - "type": "concept", - "confidence": 0.92 - } - ] - - created = await adapter.create_entity_nodes(test_entities) - - assert created > 0 - print(f" [OK] Created {created} entity nodes with embeddings") - - await adapter.close() - return True - except Exception as e: - print(f" [SKIP] Entity creation test skipped: {e}") - return False - - -async def test_phase4_relation_creation(): - """Test relation edge creation.""" - print("\n[TEST 4] Relation Edge Creation") - - try: - adapter = Neo4jAdapter() - if not await adapter.connect(): - print(" [SKIP] Neo4j not available") - return False - - # Create test relations - test_relations = [ - { - "source_id": "E_test_1", - "target_id": "E_test_2", - "predicate": "related_to", - "confidence": 0.88 - } - ] - - created = await adapter.create_relation_edges(test_relations) - - assert created >= 0 # 0 if nodes don't exist, >0 if they do - print(f" [OK] Created {created} relation edges") - - await adapter.close() - return True - except Exception as e: - print(f" [SKIP] Relation creation test skipped: {e}") - return False - - -async def test_phase4_vector_search(): - """Test vector similarity search.""" - print("\n[TEST 5] Vector Similarity Search") - - try: - adapter = Neo4jAdapter() - if not await adapter.connect(): - print(" [SKIP] Neo4j not available") - return False - - await adapter.initialize_embedder() - - # Search for entities - results = await adapter.vector_search( - query_text="Machine learning algorithms", - limit=10, - threshold=0.5 - ) - - print(f" [OK] Vector search completed") - print(f" [OK] Found {len(results)} results") - - if results: - top_result = results[0] - print(f" [INFO] Top match: {top_result.get('label')} (similarity: {top_result.get('similarity', 'N/A')})") - - await adapter.close() - return True - except Exception as e: - print(f" [SKIP] Vector search test skipped: {e}") - return False - - -async def test_phase4_entity_neighbors(): - """Test entity neighbor traversal.""" - print("\n[TEST 6] Entity Neighbor Traversal") - - try: - adapter = Neo4jAdapter() - if not await adapter.connect(): - print(" [SKIP] Neo4j not available") - return False - - # Query a test entity - result = await adapter.get_entity_neighbors(entity_id="E_test_1", depth=1) - - if result: - print(f" [OK] Retrieved entity: {result.get('entity')}") - print(f" [OK] Related entities: {result.get('neighbors', 0)}") - print(f" [OK] Relations: {len(result.get('relations', []))}") - else: - print(" [INFO] No entity found (expected if graph is empty)") - - await adapter.close() - return True - except Exception as e: - print(f" [SKIP] Entity neighbor test skipped: {e}") - return False - - -async def test_phase4_graph_stats(): - """Test graph statistics retrieval.""" - print("\n[TEST 7] Graph Statistics") - - try: - adapter = Neo4jAdapter() - if not await adapter.connect(): - print(" [SKIP] Neo4j not available") - return False - - stats = await adapter.get_stats() - - print(f" [OK] Retrieved graph statistics") - print(f" Total nodes: {stats.get('total_nodes', 0)}") - print(f" Total edges: {stats.get('total_edges', 0)}") - print(f" Entity nodes: {stats.get('entity_nodes', 0)}") - - await adapter.close() - return True - except Exception as e: - print(f" [SKIP] Graph stats test skipped: {e}") - return False - - -async def test_phase4_end_to_end(): - """Test full Phase 0-4 pipeline with mock data.""" - print("\n[TEST 8] End-to-End Pipeline (Mock Data)") - - try: - # Phase 3: Create mock validated extraction result - validated_result = { - "url": "https://example.org/test", - "title": "Test Article", - "entities": [ - { - "id": "E_mock_1", - "label": "Python", - "type": "ProgrammingLanguage", - "confidence": 0.95, - "evidence": {"source_url": "https://example.org/test"} - }, - { - "id": "E_mock_2", - "label": "Data Science", - "type": "Field", - "confidence": 0.92, - "evidence": {"source_url": "https://example.org/test"} - } - ], - "relations": [ - { - "id": "R_mock_1", - "source_id": "E_mock_1", - "target_id": "E_mock_2", - "predicate": "used_in", - "confidence": 0.88 - } - ], - "validation_passed": True, - "validation_errors": [] - } - - # Phase 4: Ingest into Neo4j (mock) - adapter = Neo4jAdapter() - if not await adapter.connect(): - print(" [INFO] Simulating ingestion (Neo4j unavailable)") - print(f" [OK] Would ingest {len(validated_result['entities'])} entities") - print(f" [OK] Would ingest {len(validated_result['relations'])} relations") - return True - - await adapter.initialize_embedder() - - # Extract entity and relation data for ingestion - entities_for_ingest = [ - { - "id": e["id"], - "label": e["label"], - "type": e.get("type", "unknown"), - "confidence": e.get("confidence", 0.5) - } - for e in validated_result.get("entities", []) - ] - - relations_for_ingest = [ - { - "source_id": r["source_id"], - "target_id": r["target_id"], - "predicate": r.get("predicate", "related_to"), - "confidence": r.get("confidence", 0.5) - } - for r in validated_result.get("relations", []) - ] - - # Ingest - entities_count = await adapter.create_entity_nodes(entities_for_ingest) - relations_count = await adapter.create_relation_edges(relations_for_ingest) - - print(f" [OK] Ingested {entities_count} entities") - print(f" [OK] Ingested {relations_count} relations") - - # Search - results = await adapter.vector_search( - query_text="Python programming", - limit=5, - threshold=0.3 - ) - - print(f" [OK] Vector search found {len(results)} results") - - await adapter.close() - return True - except Exception as e: - print(f" [SKIP] End-to-end test skipped: {e}") - return False - - -async def main(): - """Run all Phase 4 tests.""" - print("=" * 70) - print("Phase 4 Integration Test: Neo4j Graph + Vector Search") - print("=" * 70) - - results = { - "neo4j_connection": False, - "embedder_init": False, - "entity_creation": False, - "relation_creation": False, - "vector_search": False, - "entity_neighbors": False, - "graph_stats": False, - "end_to_end": False, - } - - try: - results["neo4j_connection"] = await test_phase4_neo4j_connection() - results["embedder_init"] = await test_phase4_embedder_init() - results["entity_creation"] = await test_phase4_entity_creation() - results["relation_creation"] = await test_phase4_relation_creation() - results["vector_search"] = await test_phase4_vector_search() - results["entity_neighbors"] = await test_phase4_entity_neighbors() - results["graph_stats"] = await test_phase4_graph_stats() - results["end_to_end"] = await test_phase4_end_to_end() - - print("\n" + "=" * 70) - print("Test Results Summary") - print("=" * 70) - - passed = sum(1 for v in results.values() if v) - total = len(results) - - for test_name, passed_test in results.items(): - status = "[PASS]" if passed_test else "[SKIP]" - print(f" {status} {test_name.replace('_', ' ').title()}") - - print(f"\nTotal: {passed}/{total} tests completed") - - if passed == total: - print("\n✓ Phase 4 fully integrated!") - elif passed > 0: - print(f"\n◆ {passed} tests passed (Neo4j required for full suite)") - else: - print("\n⚠ Neo4j connection required for testing") - print("\nTo start Neo4j:") - print(" docker-compose -f docker-compose.neo4j.yml up -d") - - return True - except Exception as e: - print(f"\nTest error: {e}") - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase5_entity_resolver.py b/test_phase5_entity_resolver.py deleted file mode 100644 index 45d32d3..0000000 --- a/test_phase5_entity_resolver.py +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 5 Entity Resolver tests.""" - -import asyncio -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.graph.entity_resolver import EntityResolver, EntityCluster - - -async def test_normalize_label(): - """Test label normalization.""" - print("\n[TEST 1] Label Normalization") - - resolver = EntityResolver() - - test_cases = [ - ("iPhone Pro Max", "iphone pro max"), - ("The Apple Inc.", "apple inc"), - ("Test-Entity", "test entity"), - ("UPPERCASE LABEL", "uppercase label"), - ("Label with spaces", "label with spaces"), - ] - - for input_label, expected in test_cases: - result = resolver._normalize_label(input_label) - status = "[OK]" if result == expected else "[FAIL]" - print(f" {status} '{input_label}' -> '{result}' (expected: '{expected}')") - assert result == expected, f"Expected '{expected}', got '{result}'" - - print(" [PASS]") - - -async def test_jaro_winkler_similarity(): - """Test Jaro-Winkler text similarity.""" - print("\n[TEST 2] Jaro-Winkler Similarity") - - resolver = EntityResolver() - - test_cases = [ - ("iphone", "iphone", 1.0), # Exact match - ("iphone", "iPhone", None), # Will be normalized before comparison - ("apple", "aplicant", None), # Similar but not identical - ("test", "best", None), # Partial match - ] - - for s1, s2, expected_range in test_cases: - sim = resolver._jaro_winkler_similarity(s1, s2) - print(f" Similarity('{s1}', '{s2}') = {sim:.3f}") - - if expected_range == 1.0: - assert sim == 1.0, f"Expected 1.0, got {sim}" - elif expected_range == 0.0: - assert sim == 0.0, f"Expected 0.0, got {sim}" - - print(" [PASS]") - - -async def test_text_similarity(): - """Test combined text similarity.""" - print("\n[TEST 3] Text Similarity (Jaro-Winkler + Token Overlap)") - - resolver = EntityResolver() - - test_cases = [ - ("machine learning", "machine learning", 1.0), - ("machine learning", "learning machine", 0.6), # Same tokens, different order - ("apple", "apple inc", 0.5), # Partial match - ("test", "best", 0.4), # Phonetically similar - ] - - for label1, label2, min_expected in test_cases: - sim = resolver._compute_text_similarity(label1, label2) - status = "[OK]" if sim >= min_expected else "[FAIL]" - print(f" {status} TextSim('{label1}', '{label2}') = {sim:.3f} (>= {min_expected})") - assert sim >= min_expected, f"Expected >= {min_expected}, got {sim}" - - print(" [PASS]") - - -async def test_embedder_initialization(): - """Test embedding model initialization.""" - print("\n[TEST 4] Embedder Initialization") - - resolver = EntityResolver(model_name="all-MiniLM-L6-v2") - - success = await resolver.initialize_embedder() - assert success, "Failed to initialize embedder" - - assert resolver.embedder is not None, "Embedder not loaded" - print(" [OK] Embedder loaded successfully") - - # Test embedding computation - texts = ["machine learning", "artificial intelligence"] - embeddings = resolver._embed_batch(texts) - - assert len(embeddings) == 2, f"Expected 2 embeddings, got {len(embeddings)}" - assert len(embeddings[0]) == 384, f"Expected 384-dim vectors, got {len(embeddings[0])}-dim" - - print(" [OK] Generated 384-dim embeddings for 2 texts") - print(" [PASS]") - - -async def test_detect_duplicates(): - """Test duplicate detection with vector similarity.""" - print("\n[TEST 5] Duplicate Detection (Vector + Text)") - - resolver = EntityResolver( - vector_threshold=0.85, - text_threshold=0.88, - ) - - success = await resolver.initialize_embedder() - assert success, "Failed to initialize embedder" - - # Create test entities with intentional duplicates - entities = [ - {"id": 1, "label": "Apple Inc.", "type": "Company"}, - {"id": 2, "label": "Apple Inc", "type": "Company"}, # Duplicate (slightly different) - {"id": 3, "label": "Microsoft", "type": "Company"}, - {"id": 4, "label": "Microsoft Corp", "type": "Company"}, # Duplicate - {"id": 5, "label": "Google", "type": "Company"}, - ] - - clusters = await resolver.detect_duplicates(entities) - - print(f" Detected {len(clusters)} duplicate clusters") - for cluster in clusters: - print( - f" Cluster: {cluster.canonical_id} ← {cluster.duplicates} " - f"(confidence: {cluster.confidence:.3f}, reason: {cluster.reason})" - ) - - # We expect to find some duplicates - assert len(clusters) > 0, "Should detect at least 1 duplicate cluster" - - print(" [PASS]") - - -async def test_resolve_cluster(): - """Test entity merging.""" - print("\n[TEST 6] Cluster Resolution (Entity Merging)") - - resolver = EntityResolver() - - # Create test entities - entities_map = { - 1: { - "id": 1, - "label": "Apple Inc.", - "type": "Company", - "aliases": ["Apple"], - "evidence": [{"text": "Founded in 1976"}], - }, - 2: { - "id": 2, - "label": "Apple", - "type": "Company", - "aliases": ["AAPL"], - "evidence": [{"text": "Technology company"}], - }, - } - - cluster = EntityCluster( - cluster_id="C_1_2", - canonical_id=1, - duplicates=[2], - confidence=0.92, - reason="combined", - metadata={}, - ) - - merged = await resolver.resolve_cluster(cluster, entities_map) - - assert merged["id"] == 1, "Canonical ID should be preserved" - assert 2 in merged["merged_from"], "Should record merged_from" - assert len(merged["aliases"]) >= 3, f"Should consolidate aliases (got {len(merged['aliases'])})" - assert len(merged["evidence"]) >= 2, "Should consolidate evidence" - - print(f" [OK] Merged entity with {len(merged['aliases'])} aliases, {len(merged['evidence'])} evidence") - print(f" [OK] Aliases: {merged['aliases']}") - print(" [PASS]") - - -async def test_resolution_report(): - """Test resolution report generation.""" - print("\n[TEST 7] Resolution Report") - - resolver = EntityResolver() - - clusters = [ - EntityCluster( - cluster_id="C_1", - canonical_id=1, - duplicates=[2, 3], - confidence=0.90, - reason="combined", - metadata={}, - ), - EntityCluster( - cluster_id="C_2", - canonical_id=4, - duplicates=[5], - confidence=0.85, - reason="vector_similarity", - metadata={}, - ), - ] - - report = resolver.get_resolution_report(clusters) - - assert report["total_clusters"] == 2, "Should have 2 clusters" - assert report["total_duplicates"] == 3, "Should have 3 total duplicates (2+1)" - assert "combined" in report["by_reason"], "Should track reason types" - - print(f" Total clusters: {report['total_clusters']}") - print(f" Total duplicates: {report['total_duplicates']}") - print(f" Avg confidence: {report['avg_confidence']:.3f}") - print(f" By reason: {report['by_reason']}") - print(" [PASS]") - - -async def main(): - """Run all tests.""" - print("=" * 70) - print("Phase 5 Entity Resolver Tests") - print("=" * 70) - - try: - await test_normalize_label() - await test_jaro_winkler_similarity() - await test_text_similarity() - await test_embedder_initialization() - await test_detect_duplicates() - await test_resolve_cluster() - await test_resolution_report() - - print("\n" + "=" * 70) - print("All tests passed!") - print("=" * 70) - print("\nPhase 5.0 Entity Resolver capabilities:") - print(" [OK] Label normalization") - print(" [OK] Jaro-Winkler text similarity") - print(" [OK] Vector embeddings (all-MiniLM-L6-v2)") - print(" [OK] Duplicate detection (vector + text)") - print(" [OK] Entity merging and consolidation") - print(" [OK] Resolution reporting") - - return True - except AssertionError as e: - print(f"\nTest failed: {e}") - return False - except Exception as e: - print(f"\nUnexpected error: {e}") - import traceback - traceback.print_exc() - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase5_graph_analytics.py b/test_phase5_graph_analytics.py deleted file mode 100644 index 7168f95..0000000 --- a/test_phase5_graph_analytics.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 5.2 Graph Analytics tests.""" - -import asyncio -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.graph.graph_analytics import GraphAnalytics, Community - - -class MockAdapter: - """Mock Neo4j adapter for testing.""" - - async def execute_cypher(self, cypher: str, params=None): - """Mock Cypher execution.""" - params = params or {} - - # Degree centrality - if "size((" in cypher and "out_degree" not in cypher: - return [ - {"result": {"entity_id": 1, "label": "Hub", "centrality_score": 10, "type": "degree"}}, - {"result": {"entity_id": 2, "label": "Node_2", "centrality_score": 5, "type": "degree"}}, - {"result": {"entity_id": 3, "label": "Node_3", "centrality_score": 3, "type": "degree"}}, - ] - - # Pagerank centrality - if "out_degree" in cypher or "in_degree" in cypher: - return [ - { - "result": { - "entity_id": 1, - "label": "Hub", - "centrality_score": 0.45, - "in_degree": 8, - "out_degree": 2, - "type": "pagerank", - } - }, - { - "result": { - "entity_id": 2, - "label": "Node_2", - "centrality_score": 0.30, - "in_degree": 5, - "out_degree": 3, - "type": "pagerank", - } - }, - ] - - # Communities (Louvain) - if "communityId" in cypher or "algo.louvain" in cypher: - return [ - { - "result": { - "community_id": 0, - "entities": [1, 2, 3, 4], - "labels": ["Entity_1", "Entity_2", "Entity_3", "Entity_4"], - "size": 4, - } - }, - { - "result": { - "community_id": 1, - "entities": [5, 6, 7], - "labels": ["Entity_5", "Entity_6", "Entity_7"], - "size": 3, - } - }, - ] - - # Graph statistics - if "node_count" in cypher or "edge_count" in cypher: - return [ - { - "stats": { - "total_nodes": 20, - "total_edges": 45, - "avg_degree": 4.5, - "density": 0.118, - "max_possible_edges": 190, - } - } - ] - - # Diameter - if "diameter" in cypher: - return [{"diameter": 5}] - - # Components - if "componentId" in cypher or "algo.unionFind" in cypher: - return [{"num_components": 1}] - - return [] - - -async def test_calculate_centrality_degree(): - """Test degree centrality calculation.""" - print("\n[TEST 1] Degree Centrality") - - adapter = MockAdapter() - analytics = GraphAnalytics(adapter) - - entities = await analytics.calculate_centrality(centrality_type="degree", top_n=10) - - assert len(entities) > 0, "Should find entities" - assert all("entity_id" in e and "centrality_score" in e for e in entities), "Should have required fields" - assert all(e["centrality_score"] > 0 for e in entities), "Centrality scores should be positive" - - print(f" [OK] Found {len(entities)} entities by degree") - top_entity = entities[0] - print(f" [OK] Top entity: {top_entity['label']} (degree={top_entity['centrality_score']})") - print(" [PASS]") - - -async def test_calculate_centrality_pagerank(): - """Test PageRank centrality calculation.""" - print("\n[TEST 2] PageRank Centrality") - - adapter = MockAdapter() - analytics = GraphAnalytics(adapter) - - entities = await analytics.calculate_centrality(centrality_type="pagerank", top_n=10) - - assert len(entities) > 0, "Should find entities" - assert all(0 < e["centrality_score"] <= 1 for e in entities), "PageRank should be 0-1" - - print(f" [OK] Found {len(entities)} entities by PageRank") - top_entity = entities[0] - print(f" [OK] Top entity: {top_entity['label']} (score={top_entity['centrality_score']:.3f})") - print(" [PASS]") - - -async def test_invalid_centrality_type(): - """Test invalid centrality type.""" - print("\n[TEST 3] Invalid Centrality Type") - - adapter = MockAdapter() - analytics = GraphAnalytics(adapter) - - result = await analytics.calculate_centrality(centrality_type="invalid", top_n=10) - - assert isinstance(result, dict) and "error" in result, "Should return error" - print(f" [OK] Correctly rejects invalid type: {result['error']}") - print(" [PASS]") - - -async def test_detect_communities(): - """Test community detection.""" - print("\n[TEST 4] Community Detection") - - adapter = MockAdapter() - analytics = GraphAnalytics(adapter) - - communities = await analytics.detect_communities(algorithm="louvain") - - assert isinstance(communities, list), "Should return list" - if communities: - assert all("community_id" in c and "entities" in c for c in communities), "Should have required fields" - assert all(isinstance(c["size"], int) for c in communities), "Should have size" - - print(f" [OK] Detected {len(communities)} communities") - for comm in communities: - print(f" Community {comm['community_id']}: {comm['size']} entities") - - print(" [PASS]") - - -async def test_get_graph_statistics(): - """Test graph statistics.""" - print("\n[TEST 5] Graph Statistics") - - adapter = MockAdapter() - analytics = GraphAnalytics(adapter) - - stats = await analytics.get_graph_statistics() - - if stats: - assert "total_nodes" in stats, "Should have total_nodes" - assert "total_edges" in stats, "Should have total_edges" - assert "avg_degree" in stats, "Should have avg_degree" - assert "density" in stats, "Should have density" - - print(f" [OK] Total nodes: {stats['total_nodes']}") - print(f" [OK] Total edges: {stats['total_edges']}") - print(f" [OK] Average degree: {stats['avg_degree']:.2f}") - print(f" [OK] Density: {stats['density']:.4f}") - print(f" [OK] Is connected: {stats.get('is_connected', False)}") - - print(" [PASS]") - - -async def test_find_influential_entities(): - """Test influential entity detection.""" - print("\n[TEST 6] Influential Entities") - - adapter = MockAdapter() - analytics = GraphAnalytics(adapter) - - influential = await analytics.find_influential_entities(top_n=10) - - assert isinstance(influential, list), "Should return list" - if influential: - assert all("entity_id" in e and "composite_score" in e for e in influential), "Should have required fields" - assert all(0 <= e["composite_score"] <= 1 for e in influential), "Scores should be 0-1" - - print(f" [OK] Found {len(influential)} influential entities") - for idx, entity in enumerate(influential[:3], 1): - print( - f" {idx}. {entity['label']} (score={entity['composite_score']:.3f})" - ) - - print(" [PASS]") - - -async def test_community_object(): - """Test Community data class.""" - print("\n[TEST 7] Community Object") - - community = Community( - community_id=1, - entities=[1, 2, 3, 4, 5], - size=5, - density=0.75, - modularity=0.42, - ) - - assert community.community_id == 1, "ID should be preserved" - assert len(community.entities) == 5, "Should have 5 entities" - assert community.size == 5, "Size should be 5" - - community_dict = community.to_dict() - assert "community_id" in community_dict, "Dict should have community_id" - assert community_dict["size"] == 5, "Dict should have size" - - print(" [OK] Community object creation and conversion") - print(" [PASS]") - - -async def test_centrality_ranking(): - """Test that centrality results are ranked.""" - print("\n[TEST 8] Centrality Ranking") - - adapter = MockAdapter() - analytics = GraphAnalytics(adapter) - - entities = await analytics.calculate_centrality(centrality_type="degree", top_n=10) - - if len(entities) > 1: - assert all("rank" in e for e in entities), "Should have rank field" - assert entities[0]["rank"] == 1, "Top entity should have rank 1" - assert entities[1]["rank"] == 2, "Second entity should have rank 2" - - print(f" [OK] Entities ranked correctly") - for entity in entities[:3]: - print(f" Rank {entity['rank']}: {entity['label']}") - - print(" [PASS]") - - -async def main(): - """Run all tests.""" - print("=" * 70) - print("Phase 5.2 Graph Analytics Tests") - print("=" * 70) - - try: - await test_calculate_centrality_degree() - await test_calculate_centrality_pagerank() - await test_invalid_centrality_type() - await test_detect_communities() - await test_get_graph_statistics() - await test_find_influential_entities() - await test_community_object() - await test_centrality_ranking() - - print("\n" + "=" * 70) - print("All tests passed!") - print("=" * 70) - print("\nPhase 5.2 Graph Analytics capabilities:") - print(" [OK] Degree centrality calculation") - print(" [OK] PageRank centrality calculation") - print(" [OK] Community detection (Louvain)") - print(" [OK] Graph statistics (density, diameter, components)") - print(" [OK] Influential entity detection") - print(" [OK] Input validation") - - return True - except AssertionError as e: - print(f"\nTest failed: {e}") - import traceback - - traceback.print_exc() - return False - except Exception as e: - print(f"\nUnexpected error: {e}") - import traceback - - traceback.print_exc() - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase5_integration_graphrag.py b/test_phase5_integration_graphrag.py deleted file mode 100644 index cb9b812..0000000 --- a/test_phase5_integration_graphrag.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 5 GraphRAG Integration Test.""" - -import asyncio -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.graph.rdf_converter import RDFToPropertyGraphConverter -from ont_platform.core.graph.entity_resolver import EntityResolver -from ont_platform.core.graph.subgraph_retriever import SubgraphRetriever -from ont_platform.core.graph.pattern_matcher import PatternMatcher - - -class MockNeo4jAdapter: - """Mock adapter for integration testing.""" - - async def execute_cypher(self, cypher: str, params=None): - return [] - - -async def test_rdf_to_graph_conversion(): - """Test RDF to Property Graph conversion pipeline.""" - print("\n[TEST 1] RDF to Property Graph Conversion") - - converter = RDFToPropertyGraphConverter( - namespace_base="http://ontology.example.org/", - project_id=1, - ) - - # Test with sample RDF triples - triples = [ - ("http://example.org/alice", "http://xmlns.com/foaf/0.1/name", "Alice"), - ("http://example.org/alice", "http://example.org/knows", "http://example.org/bob"), - ("http://example.org/bob", "http://xmlns.com/foaf/0.1/name", "Bob"), - ("http://example.org/bob", "http://example.org/works_at", "http://example.org/acme"), - ] - - result = await converter.convert_triples_to_graph( - triples=triples, - confidence=0.9, - ) - - assert result["nodes"] is not None, "Should have nodes" - assert result["edges"] is not None, "Should have edges" - assert len(result["nodes"]) > 0, "Should convert entities to nodes" - assert len(result["edges"]) > 0, "Should convert relationships to edges" - - print(f" [OK] Converted {len(triples)} RDF triples") - print(f" -> {len(result['nodes'])} nodes") - print(f" -> {len(result['edges'])} edges") - if result["warnings"]: - print(f" Warnings: {len(result['warnings'])}") - print(" [PASS]") - - -async def test_entity_resolution_pipeline(): - """Test entity duplicate resolution.""" - print("\n[TEST 2] Entity Resolver (Semantic Deduplication)") - - resolver = EntityResolver( - vector_threshold=0.85, - text_threshold=0.88, - ) - - # Initialize embedder - try: - success = await resolver.initialize_embedder() - assert success, "Embedder initialization failed" - - # Test with similar entities - entities = [ - {"id": 1, "label": "Apple Inc.", "type": "Company"}, - {"id": 2, "label": "Apple Inc", "type": "Company"}, # Minor variation - {"id": 3, "label": "Microsoft Corporation", "type": "Company"}, - {"id": 4, "label": "Microsoft Corp", "type": "Company"}, # Minor variation - ] - - clusters = await resolver.detect_duplicates(entities) - - assert isinstance(clusters, list), "Should return list of clusters" - if clusters: - print(f" [OK] Detected {len(clusters)} duplicate cluster(s)") - for cluster in clusters: - print(f" Canonical: {cluster.canonical_id}, Duplicates: {cluster.duplicates}") - else: - print(" [OK] No duplicates detected (expected for mock)") - - print(" [PASS]") - - except Exception as e: - print(f" [SKIP] Embedder initialization failed: {e}") - print(" (This is expected if sentence-transformers not installed)") - - -async def test_subgraph_rag_context(): - """Test subgraph retrieval for RAG context.""" - print("\n[TEST 3] Subgraph Retrieval for RAG") - - adapter = MockNeo4jAdapter() - retriever = SubgraphRetriever(adapter) - - # Test that methods exist and are callable - try: - result = await retriever.retrieve_neighborhood( - entity_id=1, - hops=2, - limit=100, - ) - assert "center_entity" in result or "error" in result, "Should have result structure" - print(" [OK] retrieve_neighborhood() callable") - - result = await retriever.retrieve_context( - entity_ids=[1, 2], - context_hops=2, - ) - assert "error" in result or "seed_entities" in result, "Should have result structure" - print(" [OK] retrieve_context() callable") - - result = await retriever.retrieve_induced_subgraph( - entity_ids=[1, 2, 3], - ) - assert "error" in result or "nodes" in result, "Should have result structure" - print(" [OK] retrieve_induced_subgraph() callable") - - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - return False - - return True - - -async def test_pattern_analysis_pipeline(): - """Test pattern analysis for data validation.""" - print("\n[TEST 4] Pattern Analysis (Validation)") - - adapter = MockNeo4jAdapter() - matcher = PatternMatcher(adapter) - - # Test that all methods are callable - try: - # Path finding - paths = await matcher.find_paths( - start_entity_id=1, - end_entity_id=2, - max_length=5, - ) - assert isinstance(paths, list), "Should return list of paths" - print(" [OK] find_paths() callable") - - # Cycle detection - cycles = await matcher.find_cycles( - min_length=2, - max_length=5, - ) - assert isinstance(cycles, list), "Should return list of cycles" - print(" [OK] find_cycles() callable") - - # SCC detection - sccs = await matcher.find_strongly_connected_components() - assert isinstance(sccs, list), "Should return list of SCCs" - print(" [OK] find_strongly_connected_components() callable") - - # Motif detection - motifs = await matcher.find_motifs(motif_type="triangle", limit=10) - assert isinstance(motifs, list), "Should return list of motifs" - print(" [OK] find_motifs() callable") - - # Connectivity analysis - metrics = await matcher.analyze_entity_connectivity(entity_id=1) - assert isinstance(metrics, dict), "Should return metrics dict" - print(" [OK] analyze_entity_connectivity() callable") - - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - import traceback - - traceback.print_exc() - return False - - return True - - -async def test_phase5_capabilities(): - """Validate Phase 5 complete capability set.""" - print("\n[TEST 5] Phase 5.0-5.1 Capability Coverage") - - capabilities = { - "Phase 5.0": { - "Neo4j batch operations": True, - "RDF <-> Property Graph conversion": True, - "Entity semantic deduplication": True, - }, - "Phase 5.1": { - "Subgraph neighborhood extraction": True, - "Multi-entity context retrieval": True, - "Induced subgraph extraction": True, - "Path finding": True, - "Cycle detection": True, - "SCC analysis": True, - "Motif detection (triangle/chain/star)": True, - "Entity connectivity metrics": True, - }, - } - - for phase, features in capabilities.items(): - print(f"\n {phase}:") - for feature, supported in features.items(): - status = "[OK]" if supported else "[NOT IMPL]" - print(f" {status} {feature}") - - total_features = sum(len(v) for v in capabilities.values()) - print(f"\n Total: {total_features} features implemented") - print(" [PASS]") - - -async def test_rag_workflow(): - """Test complete RAG workflow.""" - print("\n[TEST 6] Complete RAG Workflow") - - print(" Workflow: Extraction -> Entity Resolution -> Context -> Pattern Analysis") - print() - - # Step 1: RDF Extraction - print(" Step 1: RDF Extraction from sources") - print(" [OK] Convert raw data to RDF triples") - print(" [OK] Normalize and validate triples") - - # Step 2: Entity Resolution - print(" Step 2: Entity Resolution") - print(" [OK] Detect semantic duplicates (vector + text)") - print(" [OK] Merge duplicates into canonical entities") - print(" [OK] Consolidate evidence and aliases") - - # Step 3: Graph Construction - print(" Step 3: Graph Construction") - print(" [OK] Convert RDF to Neo4j Property Graph") - print(" [OK] Create batch indexes") - print(" [OK] Store with confidence metadata") - - # Step 4: Context Extraction - print(" Step 4: RAG Context Extraction") - print(" [OK] Extract N-hop neighborhoods") - print(" [OK] Find common paths between entities") - print(" [OK] Build induced subgraphs") - - # Step 5: Validation - print(" Step 5: Data Validation") - print(" [OK] Detect circular dependencies") - print(" [OK] Analyze connectivity patterns") - print(" [OK] Identify motif structures") - - print("\n [PASS] Complete RAG pipeline validated") - - -async def main(): - """Run all integration tests.""" - print("=" * 70) - print("Phase 5 GraphRAG Integration Test") - print("=" * 70) - - try: - await test_rdf_to_graph_conversion() - await test_entity_resolution_pipeline() - await test_subgraph_rag_context() - await test_pattern_analysis_pipeline() - await test_phase5_capabilities() - await test_rag_workflow() - - print("\n" + "=" * 70) - print("All integration tests passed!") - print("=" * 70) - - print("\nPhase 5 GraphRAG Summary:") - print(" Phase 5.0: Neo4j adapter, RDF conversion, entity resolver") - print(" Phase 5.1: Subgraph retrieval, pattern matching") - print() - print("Capabilities:") - print(" - Bidirectional RDF <-> Property Graph conversion") - print(" - Semantic duplicate detection and merging") - print(" - N-hop neighborhood extraction for RAG") - print(" - Path finding and cycle detection") - print(" - Motif detection and connectivity analysis") - print() - print("Ready for Phase 5.2 (Analytics) or API integration") - - return True - except AssertionError as e: - print(f"\nTest failed: {e}") - import traceback - - traceback.print_exc() - return False - except Exception as e: - print(f"\nUnexpected error: {e}") - import traceback - - traceback.print_exc() - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase5_pattern_matcher.py b/test_phase5_pattern_matcher.py deleted file mode 100644 index c7fcf72..0000000 --- a/test_phase5_pattern_matcher.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 5.1 Pattern Matcher tests.""" - -import asyncio -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.graph.pattern_matcher import PatternMatcher, PathResult, CycleResult - - -class MockAdapter: - """Mock Neo4j adapter for testing.""" - - async def execute_cypher(self, cypher: str, params=None): - """Mock Cypher execution.""" - params = params or {} - - # find_paths - if "shortestPath" in cypher or "RELATES*" in cypher and "start_id" in params: - return [ - { - "result": { - "path": [params.get("start_id"), 2, 3, params.get("end_id")], - "length": 3, - "confidence": 0.87, - } - }, - { - "result": { - "path": [params.get("start_id"), 5, params.get("end_id")], - "length": 2, - "confidence": 0.90, - } - }, - ] - - # find_cycles - if "start)-[" in cypher or ("RELATES*" in cypher and "end_id" not in params): - return [ - {"result": {"cycle": [1, 2, 3, 1], "length": 3}}, - {"result": {"cycle": [4, 5, 6, 4], "length": 3}}, - ] - - # find_motifs - detect by type in params - motif_type = params.get("motif_type", "") - if motif_type == "triangle": - return [ - { - "result": { - "motif_type": "triangle", - "nodes": [1, 2, 3], - "labels": ["Entity_1", "Entity_2", "Entity_3"], - } - }, - { - "result": { - "motif_type": "triangle", - "nodes": [4, 5, 6], - "labels": ["Entity_4", "Entity_5", "Entity_6"], - } - }, - ] - - if motif_type == "chain": - return [ - { - "result": { - "motif_type": "chain", - "nodes": [1, 2, 3, 4], - "labels": ["A", "B", "C", "D"], - "length": 4, - } - }, - ] - - if motif_type == "star": - return [ - { - "result": { - "motif_type": "star", - "hub": 1, - "spokes": [2, 3, 4], - "hub_label": "Central", - "spoke_labels": ["Spoke1", "Spoke2", "Spoke3"], - } - }, - ] - - # analyze_entity_connectivity - if "in_degree" in cypher: - return [ - { - "metrics": { - "entity_id": params.get("entity_id"), - "entity_label": f"Entity_{params.get('entity_id')}", - "in_degree": 3, - "out_degree": 4, - "total_degree": 7, - "reachable_entities": 12, - "reachability_ratio": 0.857, - } - } - ] - - return [] - - -async def test_find_paths(): - """Test path finding between entities.""" - print("\n[TEST 1] Find Paths") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - paths = await matcher.find_paths(start_entity_id=1, end_entity_id=4, max_length=5) - - assert len(paths) > 0, "Should find at least one path" - assert all("path" in p and "length" in p for p in paths), "All paths should have required fields" - - shortest = min(paths, key=lambda p: p["length"]) - assert shortest["length"] >= 2, "Path length should be >= 2" - - print(f" [OK] Found {len(paths)} paths from 1 to 4") - print(f" [OK] Shortest path: {shortest['path']} (length {shortest['length']})") - print(f" [OK] Average confidence: {sum(p['confidence'] for p in paths) / len(paths):.3f}") - print(" [PASS]") - - -async def test_same_entity_path(): - """Test path from entity to itself.""" - print("\n[TEST 2] Same Entity Path") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - paths = await matcher.find_paths(start_entity_id=1, end_entity_id=1, max_length=5) - - assert len(paths) == 1, "Should return single path to itself" - assert paths[0]["path"] == [1], "Path should be entity itself" - assert paths[0]["length"] == 0, "Length to itself should be 0" - assert paths[0]["confidence"] == 1.0, "Confidence should be 1.0" - - print(" [OK] Path to self: [1], length 0, confidence 1.0") - print(" [PASS]") - - -async def test_find_cycles(): - """Test cycle detection.""" - print("\n[TEST 3] Find Cycles") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - cycles = await matcher.find_cycles(min_length=2, max_length=5) - - assert len(cycles) > 0, "Should find cycles" - assert all( - "cycle" in c and "length" in c and c["length"] > 1 for c in cycles - ), "All cycles should have required fields" - - print(f" [OK] Found {len(cycles)} cycles") - for i, cycle in enumerate(cycles, 1): - print(f" Cycle {i}: {cycle['cycle']} (length {cycle['length']})") - print(" [PASS]") - - -async def test_find_motifs_triangle(): - """Test triangle motif detection.""" - print("\n[TEST 4] Find Motifs (Triangle)") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - motifs = await matcher.find_motifs(motif_type="triangle", limit=100) - - assert len(motifs) > 0, "Should find triangle motifs" - assert all(m.get("motif_type") == "triangle" for m in motifs), "All should be triangles" - - print(f" [OK] Found {len(motifs)} triangle motifs") - for motif in motifs: - print(f" Triangle: {motif['nodes']}") - print(" [PASS]") - - -async def test_find_motifs_chain(): - """Test chain motif detection.""" - print("\n[TEST 5] Find Motifs (Chain)") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - motifs = await matcher.find_motifs(motif_type="chain", limit=100) - - assert len(motifs) >= 0, "Should return chain motifs (may be empty)" - if motifs: - assert all(m.get("motif_type") == "chain" for m in motifs), "All should be chains" - print(f" [OK] Found {len(motifs)} chain motifs") - - print(" [PASS]") - - -async def test_find_motifs_star(): - """Test star motif detection.""" - print("\n[TEST 6] Find Motifs (Star)") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - motifs = await matcher.find_motifs(motif_type="star", limit=100) - - assert len(motifs) >= 0, "Should return star motifs (may be empty)" - if motifs: - assert all(m.get("motif_type") == "star" for m in motifs), "All should be stars" - assert all("hub" in m for m in motifs), "Stars should have hub" - assert all("spokes" in m for m in motifs), "Stars should have spokes" - print(f" [OK] Found {len(motifs)} star motifs") - for motif in motifs: - print(f" Hub: {motif['hub']}, Spokes: {motif['spokes']}") - - print(" [PASS]") - - -async def test_find_motifs_invalid(): - """Test invalid motif type.""" - print("\n[TEST 7] Invalid Motif Type") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - result = await matcher.find_motifs(motif_type="invalid", limit=100) - - assert isinstance(result, dict) and "error" in result, "Should return error for invalid motif" - print(f" [OK] Correctly rejects invalid motif: {result['error']}") - print(" [PASS]") - - -async def test_analyze_entity_connectivity(): - """Test entity connectivity analysis.""" - print("\n[TEST 8] Entity Connectivity Analysis") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - metrics = await matcher.analyze_entity_connectivity(entity_id=1) - - assert "in_degree" in metrics, "Should have in_degree" - assert "out_degree" in metrics, "Should have out_degree" - assert "total_degree" in metrics, "Should have total_degree" - assert "reachable_entities" in metrics, "Should have reachable_entities" - - assert metrics["total_degree"] == metrics["in_degree"] + metrics["out_degree"] - print(f" [OK] Entity 1 metrics:") - print(f" In-degree: {metrics['in_degree']}") - print(f" Out-degree: {metrics['out_degree']}") - print(f" Total degree: {metrics['total_degree']}") - print(f" Reachable entities: {metrics['reachable_entities']}") - print(f" Reachability ratio: {metrics['reachability_ratio']:.3f}") - print(" [PASS]") - - -async def test_path_validation(): - """Test path length validation.""" - print("\n[TEST 9] Path Length Validation") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - try: - await matcher.find_paths(start_entity_id=1, end_entity_id=2, max_length=0) - assert False, "Should reject max_length < 2" - except ValueError as e: - assert "max_length must be between 2 and 6" in str(e) - print(" [OK] Correctly rejects max_length < 2") - - try: - await matcher.find_paths(start_entity_id=1, end_entity_id=2, max_length=7) - assert False, "Should reject max_length > 6" - except ValueError as e: - assert "max_length must be between 2 and 6" in str(e) - print(" [OK] Correctly rejects max_length > 6") - - print(" [PASS]") - - -async def test_cycle_validation(): - """Test cycle detection validation.""" - print("\n[TEST 10] Cycle Detection Validation") - - adapter = MockAdapter() - matcher = PatternMatcher(adapter) - - try: - await matcher.find_cycles(min_length=1) - assert False, "Should reject min_length < 2" - except ValueError as e: - assert "min_length must be >= 2" in str(e) - print(" [OK] Correctly rejects min_length < 2") - - try: - await matcher.find_cycles(min_length=5, max_length=3) - assert False, "Should reject max_length < min_length" - except ValueError as e: - assert "max_length must be between min_length and 6" in str(e) - print(" [OK] Correctly rejects max_length < min_length") - - print(" [PASS]") - - -async def main(): - """Run all tests.""" - print("=" * 70) - print("Phase 5.1 Pattern Matcher Tests") - print("=" * 70) - - try: - await test_find_paths() - await test_same_entity_path() - await test_find_cycles() - await test_find_motifs_triangle() - await test_find_motifs_chain() - await test_find_motifs_star() - await test_find_motifs_invalid() - await test_analyze_entity_connectivity() - await test_path_validation() - await test_cycle_validation() - - print("\n" + "=" * 70) - print("All tests passed!") - print("=" * 70) - print("\nPhase 5.1 Pattern Matcher capabilities:") - print(" [OK] Path finding between entities") - print(" [OK] Cycle detection") - print(" [OK] Motif detection (triangle, chain, star)") - print(" [OK] Entity connectivity analysis") - print(" [OK] Input validation") - - return True - except AssertionError as e: - print(f"\nTest failed: {e}") - import traceback - - traceback.print_exc() - return False - except Exception as e: - print(f"\nUnexpected error: {e}") - import traceback - - traceback.print_exc() - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase5_subgraph_retriever.py b/test_phase5_subgraph_retriever.py deleted file mode 100644 index 77d5be9..0000000 --- a/test_phase5_subgraph_retriever.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 5.1 Subgraph Retriever tests.""" - -import asyncio -import sys -from pathlib import Path -from unittest.mock import AsyncMock, MagicMock - -sys.path.insert(0, str(Path(__file__).parent / "ontology_platform")) - -from ont_platform.core.graph.subgraph_retriever import SubgraphRetriever - - -class MockAdapter: - """Mock Neo4j adapter for testing.""" - - async def execute_cypher(self, cypher: str, params=None): - """Mock Cypher execution.""" - params = params or {} - - # Simulate test data based on query - if "neighbor_count" in cypher: - # retrieve_neighborhood query - return [ - { - "result": { - "center": { - "id": params.get("entity_id"), - "label": "Apple Inc.", - "type": "Company", - "confidence": 0.95, - }, - "neighbor_ids": [2, 3, 4], - "neighbor_count": 3, - } - } - ] - - if "SELECT node" in cypher or "WHERE n.id IN" in cypher: - # Node fetching query - node_ids = params.get("ids", []) - return [ - {"node": {"id": nid, "label": f"Entity_{nid}", "type": "Concept", "confidence": 0.9}} - for nid in node_ids - ] - - if "source:Entity" in cypher and "RELATES" in cypher: - # Edge fetching query - return [ - { - "edge": { - "source_id": 1, - "target_id": 2, - "predicate": "related_to", - "confidence": 0.85, - } - }, - { - "edge": { - "source_id": 1, - "target_id": 3, - "predicate": "mentions", - "confidence": 0.88, - } - }, - ] - - return [] - - -async def test_retrieve_neighborhood(): - """Test N-hop neighborhood extraction.""" - print("\n[TEST 1] Retrieve Neighborhood") - - adapter = MockAdapter() - retriever = SubgraphRetriever(adapter) - - result = await retriever.retrieve_neighborhood(entity_id=1, hops=2) - - assert result["center_entity"] is not None, "Center entity should be found" - assert result["center_entity"]["id"] == 1, "Center entity ID should match" - assert result["node_count"] > 0, "Should have nodes in neighborhood" - assert result["hop_count"] == 2, "Hop count should be preserved" - - print(f" [OK] Center entity: {result['center_entity']['label']}") - print(f" [OK] Neighbor count: {result['node_count']}") - print(f" [OK] Edge count: {result['edge_count']}") - print(" [PASS]") - - -async def test_retrieve_context(): - """Test context retrieval for multiple entities.""" - print("\n[TEST 2] Retrieve Context (Multi-Entity)") - - adapter = MockAdapter() - retriever = SubgraphRetriever(adapter) - - result = await retriever.retrieve_context(entity_ids=[1, 2, 3]) - - assert "seed_entities" in result, "Should have seed entities" - assert "common_neighbors" in result, "Should have common neighbors" - assert result["total_nodes"] >= 0, "Should have node count" - - print(f" [OK] Seed entities: {len(result['seed_entities'])}") - print(f" [OK] Common neighbors: {len(result['common_neighbors'])}") - print(f" [OK] Total nodes: {result['total_nodes']}") - print(" [PASS]") - - -async def test_retrieve_induced_subgraph(): - """Test induced subgraph extraction.""" - print("\n[TEST 3] Retrieve Induced Subgraph") - - adapter = MockAdapter() - retriever = SubgraphRetriever(adapter) - - result = await retriever.retrieve_induced_subgraph(entity_ids=[1, 2, 3, 4]) - - assert "nodes" in result, "Should have nodes" - assert "edges" in result, "Should have edges" - assert "node_count" in result, "Should have node count" - - print(f" [OK] Induced nodes: {result['node_count']}") - print(f" [OK] Induced edges: {result['edge_count']}") - print(" [PASS]") - - -async def test_validate_hop_limit(): - """Test hop limit validation.""" - print("\n[TEST 4] Hop Limit Validation") - - adapter = MockAdapter() - retriever = SubgraphRetriever(adapter) - - try: - await retriever.retrieve_neighborhood(entity_id=1, hops=5) - assert False, "Should raise ValueError for hops > 3" - except ValueError as e: - assert "hops must be between 1 and 3" in str(e) - print(" [OK] Correctly rejects hops > 3") - - try: - await retriever.retrieve_neighborhood(entity_id=1, hops=0) - assert False, "Should raise ValueError for hops < 1" - except ValueError as e: - assert "hops must be between 1 and 3" in str(e) - print(" [OK] Correctly rejects hops < 1") - - print(" [PASS]") - - -async def test_retrieve_context_validation(): - """Test context retrieval validation.""" - print("\n[TEST 5] Context Retrieval Validation") - - adapter = MockAdapter() - retriever = SubgraphRetriever(adapter) - - result = await retriever.retrieve_context(entity_ids=[]) - assert "error" in result, "Should error with empty entity_ids" - print(" [OK] Rejects empty entity_ids") - - result = await retriever.retrieve_context(entity_ids=[1]) - assert "error" in result, "Should error with single entity" - print(" [OK] Rejects single entity") - - print(" [PASS]") - - -async def test_induced_subgraph_validation(): - """Test induced subgraph validation.""" - print("\n[TEST 6] Induced Subgraph Validation") - - adapter = MockAdapter() - retriever = SubgraphRetriever(adapter) - - result = await retriever.retrieve_induced_subgraph(entity_ids=[]) - assert "error" in result, "Should error with empty entity_ids" - print(" [OK] Rejects empty entity_ids") - - print(" [PASS]") - - -async def main(): - """Run all tests.""" - print("=" * 70) - print("Phase 5.1 Subgraph Retriever Tests") - print("=" * 70) - - try: - await test_retrieve_neighborhood() - await test_retrieve_context() - await test_retrieve_induced_subgraph() - await test_validate_hop_limit() - await test_retrieve_context_validation() - await test_induced_subgraph_validation() - - print("\n" + "=" * 70) - print("All tests passed!") - print("=" * 70) - print("\nPhase 5.1 Subgraph Retriever capabilities:") - print(" [OK] N-hop neighborhood extraction") - print(" [OK] Multi-entity context retrieval") - print(" [OK] Induced subgraph extraction") - print(" [OK] Input validation") - - return True - except AssertionError as e: - print(f"\nTest failed: {e}") - import traceback - - traceback.print_exc() - return False - except Exception as e: - print(f"\nUnexpected error: {e}") - import traceback - - traceback.print_exc() - return False - - -if __name__ == "__main__": - success = asyncio.run(main()) - sys.exit(0 if success else 1) diff --git a/test_phase6_api.py b/test_phase6_api.py deleted file mode 100644 index 8acbbdd..0000000 --- a/test_phase6_api.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/env python3 -"""Phase 6 API Tests.""" - -import asyncio -import json -from unittest.mock import AsyncMock, MagicMock, patch - -# Mock data -MOCK_ENTITIES = [ - {"id": 1, "label": "Apple Inc.", "type": "Company"}, - {"id": 2, "label": "Apple Inc", "type": "Company"}, - {"id": 3, "label": "Microsoft", "type": "Company"}, -] - -MOCK_PATHS = [ - {"path": [1, 2, 3], "length": 2, "confidence": 0.87}, - {"path": [1, 4, 3], "length": 2, "confidence": 0.92}, -] - -MOCK_CONTEXT = { - "center_entity": {"id": 1, "label": "Apple Inc.", "type": "Company"}, - "nodes": [ - {"id": 1, "label": "Apple Inc.", "type": "Company"}, - {"id": 5, "label": "iPhone", "type": "Product"}, - {"id": 6, "label": "Steve Jobs", "type": "Person"}, - ], - "edges": [ - {"source_id": 1, "target_id": 5, "predicate": "produces", "confidence": 0.95}, - {"source_id": 1, "target_id": 6, "predicate": "founded_by", "confidence": 0.98}, - ], - "node_count": 3, - "edge_count": 2, -} - - -def test_graph_api_endpoints(): - """Test that all graph API endpoints are defined.""" - print("\n[TEST 1] Graph API Endpoints") - - # Import the app to verify endpoints exist - try: - from ontology_platform.ont_platform.api.phase6_app import ( - graph_router, - rag_router, - ) - - # Check graph routes - graph_routes = [r.path for r in graph_router.routes] - required_routes = [ - "/resolve", - "/subgraph/neighborhood/{entity_id}", - "/subgraph/context", - "/patterns/paths", - "/patterns/cycles", - "/patterns/motifs", - "/analytics/centrality", - "/analytics/communities", - "/analytics/statistics", - "/analytics/influential", - ] - - for route in required_routes: - assert any( - route in r for r in graph_routes - ), f"Missing route: {route}" - - print(f" [OK] {len(graph_routes)} graph API routes defined") - - # Check RAG routes - rag_routes = [r.path for r in rag_router.routes] - assert any( - "context-extraction" in r for r in rag_routes - ), "Missing context-extraction route" - assert any("query" in r for r in rag_routes), "Missing query route" - - print(f" [OK] {len(rag_routes)} RAG API routes defined") - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - return False - - return True - - -def test_rag_prompt_building(): - """Test RAG prompt generation.""" - print("\n[TEST 2] RAG Prompt Generation") - - try: - from ontology_platform.ont_platform.api.phase6_app import _build_rag_prompt - - context_data = [ - { - "entity": { - "id": 1, - "label": "Apple Inc.", - "type": "Company", - "similarity": 0.95, - }, - "subgraph": { - "nodes": [ - {"id": 2, "label": "iPhone", "type": "Product"}, - {"id": 3, "label": "iPad", "type": "Product"}, - ], - "edges": [ - { - "source_id": 1, - "target_id": 2, - "predicate": "produces", - "confidence": 0.95, - } - ], - }, - } - ] - - prompt = _build_rag_prompt("What is Apple?", context_data) - - assert isinstance(prompt, str), "Prompt should be string" - assert "KNOWLEDGE GRAPH CONTEXT" in prompt, "Should have graph context section" - assert "Apple Inc." in prompt, "Should include entity labels" - assert "iPhone" in prompt, "Should include related entities" - assert "What is Apple?" in prompt, "Should include user query" - assert "ready_for_llm" or "LLM" in prompt, "Should be formatted for LLM" - - print(" [OK] Prompt structure:") - print(f" - Length: {len(prompt)} chars") - print(f" - Contains graph context: YES") - print(f" - Contains entity relationships: YES") - print(f" - LLM-ready format: YES") - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - return False - - return True - - -def test_api_response_structure(): - """Test API response structure consistency.""" - print("\n[TEST 3] API Response Structure") - - try: - # Simulate API response structures - entity_resolution_response = { - "status": "success", - "clusters": [ - { - "cluster_id": "C_1_2", - "canonical_id": 1, - "duplicates": [2], - "confidence": 0.92, - "reason": "combined", - } - ], - "total_clusters": 1, - } - - subgraph_response = { - "status": "success", - "data": MOCK_CONTEXT, - } - - patterns_response = { - "status": "success", - "paths": MOCK_PATHS, - "total_paths": 2, - } - - analytics_response = { - "status": "success", - "centrality_type": "pagerank", - "entities": [ - {"entity_id": 1, "label": "Apple", "centrality_score": 0.95, "rank": 1} - ], - "total_entities": 1, - } - - rag_response = { - "status": "success", - "query": "What is Apple?", - "relevant_entities": ["Apple Inc."], - "context_nodes": 3, - "llm_prompt": "...", - "ready_for_llm": True, - } - - # Verify all have standard fields - for name, response in [ - ("entity_resolution", entity_resolution_response), - ("subgraph", subgraph_response), - ("patterns", patterns_response), - ("analytics", analytics_response), - ("rag", rag_response), - ]: - assert ( - "status" in response - ), f"{name} missing status field" - assert response["status"] in [ - "success", - "no_results", - ], f"{name} has invalid status" - - print(" [OK] All responses have consistent structure") - print(" [OK] All responses include 'status' field") - print(" [OK] Response statuses are valid") - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - return False - - return True - - -def test_rag_integration_workflow(): - """Test complete RAG workflow.""" - print("\n[TEST 4] RAG Integration Workflow") - - try: - # Step 1: Vector search finds relevant entity - print(" Step 1: Vector search...") - search_results = [ - {"id": 1, "label": "Apple Inc.", "similarity": 0.95, "type": "Company"} - ] - assert len(search_results) > 0, "Should find relevant entities" - print(" [OK] Found 1 relevant entity") - - # Step 2: Extract context from entity - print(" Step 2: Extract context...") - context = { - "center_entity": search_results[0], - "nodes": [ - {"id": 1, "label": "Apple Inc.", "type": "Company"}, - {"id": 2, "label": "iPhone", "type": "Product"}, - ], - "edges": [ - {"source_id": 1, "target_id": 2, "predicate": "produces", "confidence": 0.95} - ], - } - assert "nodes" in context and "edges" in context, "Context should have graph data" - print(f" [OK] Extracted context with {len(context['nodes'])} nodes") - - # Step 3: Build LLM prompt - print(" Step 3: Build LLM prompt...") - from ontology_platform.ont_platform.api.phase6_app import _build_rag_prompt - - prompt = _build_rag_prompt("What is Apple?", [{"entity": search_results[0], "subgraph": context}]) - assert len(prompt) > 100, "Prompt should be substantive" - print(f" [OK] Generated {len(prompt)}-char prompt") - - # Step 4: Ready for LLM inference - print(" Step 4: Prepare for LLM...") - inference_ready = { - "prompt": prompt, - "max_tokens": 500, - "temperature": 0.7, - } - assert "prompt" in inference_ready, "Should include prompt for LLM" - print(" [OK] Ready for LLM inference") - - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - import traceback - traceback.print_exc() - return False - - return True - - -def test_graphql_schema_support(): - """Test GraphQL endpoint support.""" - print("\n[TEST 5] GraphQL Schema Support") - - try: - # Check GraphQL query support - graphql_queries = [ - ('{ entity(id: 1) { id label type } }', "entity query"), - ('{ communities { id size } }', "communities query"), - ] - - for query, description in graphql_queries: - assert "{" in query and "}" in query, f"{description} should be valid GraphQL" - - print(f" [OK] Supports {len(graphql_queries)} basic GraphQL patterns") - print(" [OK] Entity queries") - print(" [OK] Aggregate queries (communities, stats)") - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - return False - - return True - - -def test_api_documentation(): - """Test that API endpoints have documentation.""" - print("\n[TEST 6] API Documentation") - - try: - from ontology_platform.ont_platform.api.phase6_app import ( - resolve_entities, - get_neighborhood, - find_paths, - calculate_centrality, - extract_rag_context, - ) - - # Check docstrings - functions_to_check = [ - (resolve_entities, "resolve_entities"), - (get_neighborhood, "get_neighborhood"), - (find_paths, "find_paths"), - (calculate_centrality, "calculate_centrality"), - (extract_rag_context, "extract_rag_context"), - ] - - for func, name in functions_to_check: - assert func.__doc__, f"{name} should have docstring" - - print(f" [OK] {len(functions_to_check)} endpoints have documentation") - print(" [OK] All endpoints describe request/response format") - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - return False - - return True - - -async def test_error_handling(): - """Test API error handling.""" - print("\n[TEST 7] Error Handling") - - try: - # Test that invalid inputs are handled - invalid_cases = [ - {"entity_id": -1, "error": "Invalid entity ID"}, - {"hops": 10, "error": "hops > 3"}, - {"max_length": 0, "error": "max_length < 2"}, - ] - - for case in invalid_cases: - # These should be validated by FastAPI - if "entity_id" in case and case["entity_id"] < 0: - print(f" [OK] Rejects negative entity_id") - elif "hops" in case and case["hops"] > 3: - print(f" [OK] Rejects hops > 3") - elif "max_length" in case and case["max_length"] < 2: - print(f" [OK] Rejects max_length < 2") - - print(" [PASS]") - - except Exception as e: - print(f" [FAIL] {e}") - return False - - return True - - -def main(): - """Run all tests.""" - print("=" * 70) - print("Phase 6 API Tests") - print("=" * 70) - - tests = [ - test_graph_api_endpoints, - test_rag_prompt_building, - test_api_response_structure, - test_rag_integration_workflow, - test_graphql_schema_support, - test_api_documentation, - lambda: asyncio.run(test_error_handling()), - ] - - passed = 0 - for test in tests: - try: - result = test() if asyncio.iscoroutinefunction(test) else test() - if result: - passed += 1 - except Exception as e: - print(f" [ERROR] {e}") - - print("\n" + "=" * 70) - print(f"Tests: {passed}/{len(tests)} passed") - print("=" * 70) - - if passed == len(tests): - print("\nPhase 6 API Ready!") - print("- [OK] REST API endpoints (graph, rag)") - print("- [OK] GraphQL support") - print("- [OK] RAG pipeline integration") - print("- [OK] Error handling") - print("- [OK] Documentation") - print("\nStart API server:") - print(" python -m uvicorn ontology_platform.ont_platform.api.phase6_app:app --reload") - return True - else: - return False - - -if __name__ == "__main__": - success = main() - import sys - - sys.exit(0 if success else 1) diff --git a/온톨로지플랫폼_통합설계서.md b/온톨로지플랫폼_통합설계서.md deleted file mode 100644 index 4869b6a..0000000 --- a/온톨로지플랫폼_통합설계서.md +++ /dev/null @@ -1,1181 +0,0 @@ -# 범용 온톨로지 구축 플랫폼 통합 설계서 - -작성일: 2026-05-13 -대상: 본 문서를 받아 실제 구현을 수행할 모든 AI 에이전트 및 개발자 -근거 자료: `C:\Users\lasta\MyProject\AI\오픈소스분석자료` 의 8개 분석 명세 (Crawl4AI, Firecrawl, Guardrails, Knowledge Agent, Neo4j GraphRAG, OntoCast, OpenDeepResearcher, Trafilatura) - ---- - -## 0. 본 문서의 사용 규칙 (Agent Instruction) - -본 문서를 받는 AI 에이전트는 다음을 준수한다. - -1. 본 문서는 8개 분석 자료를 바탕으로 도출된 **최종 통합 설계**다. 개별 분석 자료와 본 문서가 충돌할 경우 본 문서가 우선한다. -2. 본 문서가 "그대로 사용"이라고 명시한 모듈은 원본 소스를 가급적 수정하지 않고 라이브러리 또는 vendored copy 형태로 도입한다. "어댑터 작성"이라고 명시한 부분만 우리 코드로 만든다. -3. "통합은 한 번에 하나씩"의 원칙을 지킨다. Phase N의 검수 게이트(Acceptance Gate)를 통과하기 전에는 Phase N+1로 진행하지 않는다. -4. 코드 작성 시 어느 분석 자료의 어느 절을 근거로 했는지 PR 설명에 명시한다. 예: `OntoCast 분석 §6 GraphUpdate 모델`. -5. 본 문서가 "제외(Excluded)"라고 명시한 프로젝트는 코드/의존성에 포함하지 않는다. - ---- - -## 1. Executive Summary (한눈에 보는 결론) — 수정판 - -### 1.1 설계 변경 배경 - -초기 설계는 **OntoCast를 Phase 0 Base로 사용**하고, 이후 다른 컴포넌트를 순차 통합하는 구조였다. - -그러나 **Phase 0 실제 구현 및 테스트 결과**, OntoCast의 전체 파이프라인(RDF/OWL 갱신, Renderer/Critic retry loop, Entity Aggregation)을 초기 단계부터 사용하면: -- 처리 속도가 매우 느림 (테스트 분량 축소에도 불구하고) -- 중간 결과 확인이 어려워 디버깅 복잡 -- 대량 크롤링 단계에 부적합 -- 비용 폭주 위험 - -따라서 **설계를 전면 개정**한다. - -### 1.2 수정된 핵심 전략 - -| 항목 | 기존 설계 | 수정 설계 | -|---|---|---| -| **기본 엔진** | OntoCast 중심 | 가벼운 JSON Extraction | -| **OntoCast 역할** | 입력 → 처리 → RDF 출력 | 승인된 후보 → 정밀 RDF/OWL 갱신 | -| **처리 흐름** | 모든 문서 → OntoCast → RDF | 문서 → 후보 추출 → 검수 → 선택적 OntoCast → RDF | -| **Phase 0 목표** | OntoCast 안정화 | Fast Extraction MVP (RDF 없이) | -| **총 Phase 수** | 6개 (Phase 0~5) | 6개 (구조 변경) | - -**한 문장 요약**: OntoCast는 기본 엔진에서 정밀 갱신 엔진으로 역할을 낮춘다. - -### 1.3 최종 구조 - -``` -Trafilatura / Crawl4AI -→ Fast JSON Extraction (가벼움) -→ Candidate Store (SQLite/PostgreSQL) -→ Validation / Guardrails -→ Review Queue (사람 검수) -→ Optional OntoCast (고신뢰/승인된 후보만) -→ Fuseki RDF (Canonical) -→ Neo4j Projection (검색/RAG) -``` - -| 항목 | 결정 | -|---|---| -| **기본 수집** | Trafilatura (정적, 빠름) + Crawl4AI (동적, 선택) | -| **초기 추출** | Lightweight LLM JSON Extractor (RDF 아님) | -| **검증** | Pydantic + Guardrails | -| **정밀 RDF 갱신** | OntoCast (batch 후보 단위, 선택적) | -| **통합 대상** | ① Trafilatura ② Crawl4AI ③ Guardrails ④ Neo4j GraphRAG ⑤ OntoCast (용도 변경) | -| **제외** | Firecrawl, OpenDeepResearcher (기존과 동일) | - ---- - -## 기존 설계 요약 (참고용) — Phase 0 문제 분석 - -### 기존 Phase 0: OntoCast Base 안정화 (⚠️ 폐기됨) - -기존 설계에서는 다음과 같이 계획되었다: -``` -1. OntoCast 저장소를 platform/core/ontocast/ 에 배치 -2. select_ontology.py 버그 수정 -3. convert_document.py 다중 파일 처리 확장 -4. Robyn → FastAPI 재작성 -5. 전체 파이프라인 end-to-end 테스트 -→ RDF/OWL 생성 목표 -``` - -**실제 테스트 결과 발견된 문제**: -- 처리 속도: 단순 샘플도 5분 이상 소요 (목표: 30초) -- 디버깅 어려움: RDF 중심이라 중간 상태 확인 곤란 -- 비용 폭주: Critic retry loop 반복될수록 LLM 호출 증가 -- 확장성 부족: 대량 문서 처리에 부적합 - -**결론**: OntoCast 중심 구조는 **정밀도는 높지만 초기 단계에는 부적합**. 따라서 기존 Phase 0을 폐기하고, 새로운 Phase 0 (Fast Extraction MVP)으로 변경. - -**기존 계획 중 여전히 유용한 부분**: -- OntoCast 버그 수정 (§9.1 참고) → Phase 4에서 활용 -- Pydantic Settings 패턴 → 현재 설계에 적용 -- BudgetTracker 개념 → Phase 4에서 재사용 - ---- - -## 2. 8개 오픈소스 강점 매트릭스 (Strength Matrix) - -각 소스가 "가장 잘하는 영역" 하나씩만 추려서 기능 중복을 정리한 표. - -| # | 프로젝트 | 카테고리 | 대체 불가 강점 | 라이선스 | 언어 | 채택 여부 | -|---|---|---|---|---|---|---| -| 1 | **OntoCast** | 온톨로지 코어 | **GraphUpdate 기반 SPARQL 증분 갱신** + Renderer/Critic retry loop + Entity aggregation(embedding + URI 정규화 + owl:sameAs) | Apache 2.0 | Python 3.12+ | **선택적 Precision RDF/OWL Engine** | -| 2 | **Trafilatura** | 본문 추출 | 본문/메타데이터/날짜/저자/언어 추출의 **산업 표준 정밀도**. XML body tree 보존, SimHash fingerprint, feed/sitemap discovery | Apache 2.0 | Python | **통합** | -| 3 | **Crawl4AI** | 크롤링 | **동적 페이지(Playwright)+LLM 친화 Markdown** 변환의 결정판. Deep crawl(BFS/DFS/Best-First) + URL Seeder + Adaptive crawler + browser pool/dispatcher/cache | Apache 2.0 | Python 3.10+ | **통합** | -| 4 | **Guardrails** | 검증 | **Pydantic 기반 LLM 출력 강제 + on_fail 정책(reask/fix/filter/refrain) + JSON path field validator** | Apache 2.0 | Python 3.10+ | **통합** | -| 5 | **Neo4j GraphRAG** | KG 저장/검색 | **GraphSchema + GraphPruning + Neo4jWriter + EntityResolver + Vector/Hybrid/Text2Cypher Retriever + GraphRAG** | Apache 2.0 | Python 3.10+ | **통합** | -| 6 | Knowledge Agent | 멀티에이전트 | LangGraph 기반 Analyst→Researcher→Curator→Auditor→Fixer→Advisor 패턴 + LightRAG 엔티티/관계 추출 프롬프트 | 비명시 | Python | **패턴/프롬프트만 차용** | -| 7 | Firecrawl | 크롤링 API | scrape/map/crawl/search/parse API 명세 우수, fire-engine fallback | AGPL 의심 | TypeScript | **제외** (스택 분리 부담 + 라이선스 리스크 + Crawl4AI와 중복) | -| 8 | OpenDeepResearcher | 검색 루프 | LLM 기반 검색어 생성 + 자기확장 루프 + `` 판단 | MIT | Python (notebook) | **제외** (`eval()` 보안 문제 + Knowledge Agent의 Researcher가 더 구조화됨) | - ---- - -## 3. OntoCast를 선택적 Precision RDF/OWL Engine으로 유지하는 근거 - -| 비교 항목 | OntoCast | Knowledge Agent | Neo4j GraphRAG | -|---|---|---|---| -| 사용자 목표 적합성("온톨로지 구축") | ◎ RDF/OWL 중심 | △ 지식그래프 보조 도구 | ○ Property Graph 중심 | -| 핵심 자산의 대체 난이도 | ◎ GraphUpdate 증분 갱신은 다른 어디서도 못 구함 | △ LangGraph 패턴은 재작성 용이 | ○ Library 형태로 갖다 쓰면 됨 | -| Renderer/Critic retry loop | ◎ 내장 | △ Refiner 비슷한 개념만 | × 없음 | -| Entity Aggregation/URI 정규화 | ◎ 내장 (`tool/agg/`) | × 없음 | △ Resolver 있으나 단순 | -| LangGraph 워크플로우 | ◎ 내장 (`stategraph/`) | ◎ 내장 | × 없음 | -| Triple Store 추상화 (Fuseki/Neo4j/FS) | ◎ 내장 | × LightRAG에 종속 | △ Neo4j만 | -| ToolBox dependency container | ◎ 내장 | × | × | -| 라이선스 명확성 | ◎ Apache 2.0 | × 비명시 | ◎ Apache 2.0 | -| 코드 안정성 | ○ 일부 버그 (§13.1) | × 다수 버그 (Curator/Auditor/Fixer/Advisor) | ◎ 테스트 광범위 | - -**결론**: OntoCast의 "GraphUpdate 증분 갱신 + Renderer/Critic 루프 + Entity Aggregation"은 다른 소스로 대체 불가능한 차별 자산이다. 이를 선택적 Precision RDF/OWL Engine으로 유지하고, Fast Pipeline과 분리하여 사용한다. - ---- - -## 4. 통합 아키텍처 (Layered Architecture) — 수정판 - -### 4.0 기존 vs 수정 아키텍처 비교 - -**기존 설계 (Phase 0에서 발견된 문제)**: -``` -입력(URL/File) -→ L4 OntoCast 전체 파이프라인 (Converter/Chunker/Renderer/Critic...) -→ RDF/OWL 생성 -→ L1 Fuseki 저장 -``` -❌ 문제: 초기 단계부터 무거운 RDF 처리, 디버깅 어려움, 테스트 속도 느림 - -**수정 설계 (분리된 두 경로)**: -``` -Fast Path (대부분): Precision Path (선택): -입력 → 본문 추출 승인 후보 → OntoCast - → 가벼운 JSON 추출 → RDF/OWL 갱신 - → 검증 (Pydantic) → Fuseki 저장 - → 후보 저장 - → 검수/자동승인 -``` -✅ 이점: 빠른 피드백, 명확한 단계, OntoCast는 필요할 때만 - -### 4.1 수정된 계층 구조 - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ L7. UI Layer (Ontology Studio, Review Console, Search UI) │ -│ — 새로 작성 (React/Next.js 등 자유 선택) │ -├──────────────────────────────────────────────────────────────────┤ -│ L6. Platform API (FastAPI) │ -│ — 새로 작성. /projects, /jobs, /sources, /extract, │ -│ /candidates, /review, /search, /admin │ -├──────────────────────────────────────────────────────────────────┤ -│ L5. Job Orchestration (Job Queue + Worker) │ -│ — 새로 작성. Arq/Celery, Redis/Postgres state │ -├──────────────────────────────────────────────────────────────────┤ -│ L4a. Fast Extraction Engine (기본 경로) │ -│ ★ Trafilatura (본문/메타 추출) │ -│ ★ Crawl4AI (동적 크롤링, 선택) │ -│ ★ Lightweight JSON Extractor (가벼운 entity/relation 추출) │ -│ — 입력: 정제 본문 → 출력: JSON (RDF 아님) │ -├──────────────────────────────────────────────────────────────────┤ -│ L4b. OntoCast RDF Engine (정밀 경로, 선택적) │ -│ ★ OntoCast (승인 후보 → RDF/OWL 갱신, Batch 처리) │ -│ — Renderer/Critic retry loop, GraphUpdate, Entity Agg. │ -│ — 호출: batch 후보 단위 (문서 단위 아님) │ -├──────────────────────────────────────────────────────────────────┤ -│ L3. Quality & Validation Gate │ -│ ★ Pydantic (JSON 스키마 검증) │ -│ ★ Guardrails (LLM 출력 강제 검증) │ -│ — CandidateEntity/Relation 검증 │ -├──────────────────────────────────────────────────────────────────┤ -│ L2. Temporary Knowledge Store │ -│ • SQLite / PostgreSQL (초기) │ -│ • CandidateEntity, CandidateRelation, ReviewStatus │ -│ — RDF 아님, 검수 전 임시 저장소 │ -│ — Neo4j draft graph (선택, Phase 3) │ -├──────────────────────────────────────────────────────────────────┤ -│ L1. Canonical Storage (검수/승인 후) │ -│ • Fuseki (Canonical RDF Store) │ -│ • Neo4j (Projection, Vector Index, Fulltext) │ -│ ★ Neo4j GraphRAG (검색/RAG Retriever) │ -│ • PostgreSQL (Job/User/Project 메타데이터) │ -│ • Object Storage (raw HTML, PDF artifacts) │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### 4.2 데이터 흐름 (수정판) — Fast Path + Precision Path - -#### Fast Path (기본, 대부분의 문서) -``` -[Source URL/File] - → [L4a 수집] Trafilatura bare_extraction (본문 + metadata) - 또는 Crawl4AI (JS 필요 시) - → [L4a 정제] 정제된 본문 텍스트 + title/author/date/language - → [L4a 추출] Lightweight LLM JSON Extractor - 입력: 정제 본문 - 출력: {entities, relations, claims, evidence, confidence} - → [L3 검증] Pydantic + Guardrails - EntityIdFormatValidator - UniqueEntityIdValidator - RelationEndpointExistsValidator - ConfidenceRangeValidator - EvidenceExistsValidator - → [L2 저장] PostgreSQL / SQLite - CandidateEntity (id, label, type, confidence, evidence_ids) - CandidateRelation (source_id, predicate, target_id, confidence) - EvidenceSpan (chunk_id, text, start_offset, end_offset) - ReviewStatus (pending/approved/rejected) - → [L6 API] /candidates 조회 - 사람이 결과 확인 및 검수 -``` - -**특징**: -- ✅ 10~30초 내 처리 (OntoCast 없음) -- ✅ JSON 기반 (RDF 아님) -- ✅ 중간 결과 명확함 -- ✅ 대량 문서 적합 - -#### Precision Path (선택, 승인된 후보만) -``` -[Approved CandidateEntity/Relation (Batch)] - → [L4b OntoCast] OntoCast Adapter - Candidate JSON → OntoCast internal format 변환 - batch_size: 10~50개 단위 - → [L4b 처리] OntoCast RENDER_ONTOLOGY_UPDATE + Critic - GraphUpdate 생성 - Entity Aggregation (owl:sameAs) - → [L3 검증] Guardrails (OntoCast 출력) - 최종 GraphUpdate schema 검증 - → [L1 저장] - Fuseki: Canonical RDF (via GraphUpdate SPARQL) - Postgres: Job metadata, processing time, cost - → [L1 투영] Neo4j Async Worker - Fuseki RDF → Neo4j Property Graph - Chunk embedding + Vector index - Lexical graph (Document/Chunk/Entity) - → [L6 API] /search/{type} 호출 시 Neo4j GraphRAG의 - VectorRetriever + HybridRetriever + GraphRAG -``` - -**특징**: -- ✅ RDF/OWL 정밀 반영 (OntoCast의 강점) -- ✅ GraphUpdate 증분 갱신 (대체 불가 기능) -- ✅ Entity Aggregation (중복 병합) -- ⚠️ 처리 시간 길음 (batch 단위로 완화) -- ⚠️ 비용 높음 (선택적 호출로 제어) - -### 4.3 저장소 전략 (Fast Path vs Precision Path) - -#### Fast Path 저장소 -| 저장소 | 역할 | 특징 | -|---|---|---| -| **PostgreSQL / SQLite** | 임시 지식 저장소. Candidate 후보 보관 | 검수 전 상태, RDF 아님 | -| **CandidateEntity** | id, label, type, confidence, source_doc_id, evidence_ids | 사람 검수 대기 | -| **CandidateRelation** | source_id, predicate, target_id, confidence, evidence | 사람 검수 대기 | -| **ReviewStatus** | pending → approved/rejected/auto_approved | 승인 상태 기록 | - -#### Precision Path 저장소 -| 저장소 | 역할 | 근거 | -|---|---|---| -| **Fuseki (Canonical RDF)** | 진실 원본 (source of truth). OntoCast의 GraphUpdate가 SPARQL UPDATE로 직접 반영. OWL/SHACL 추론 가능 | OntoCast GraphUpdate는 SPARQL 기반 | -| **Neo4j (Projection)** | 검색/시각화/RAG 전용. Fuseki 변경 시 비동기로 동기화. Lexical graph(Document/Chunk) + Entity 노드 + embedding | Neo4j GraphRAG + Vector search | - -**중요 원칙**: -- Fast Path는 RDF를 생성하지 않음 (JSON만) -- Precision Path만 RDF 생성 (OntoCast 호출 시) -- RDF가 진실, Neo4j는 사본 -- Fuseki commit → Worker가 async로 Neo4j 동기화 - ---- - -## 5. 단계별 통합 로드맵 (Phased Integration Plan) — 수정판 - -각 Phase의 마지막에 **Acceptance Gate**(검수 게이트)가 있다. Gate를 통과해야 다음 Phase 진행. - ---- - -### Phase 0: Fast Extraction MVP ⭐ (새로운 시작점) - -**목표**: OntoCast를 붙이기 전에, 웹페이지나 파일에서 의미 있는 JSON 형태의 온톨로지 후보가 빠르게 추출되는지 검증한다. **RDF/OWL, SPARQL, GraphUpdate, Critic loop를 사용하지 않는다.** - -**배경**: 기존 설계에서 OntoCast를 Phase 0 Base로 사용했으나, 실제 구현 결과 처리 속도가 지나치게 느렸다. 따라서 보다 가벼운 추출 엔진을 우선 구성하고, OntoCast는 선택적으로 호출하는 구조로 변경했다. (자세한 배경은 §1.1 참조) - -**작업**: - -1. **`platform/core/extractors/web_extractor.py` 작성** - - Trafilatura 기반 본문 추출 - - 출력: title, author, publish_date, language, canonical_url, fingerprint, body_text - - 참고: 기존 설계 Phase 1 내용 일부 활용 - -2. **`platform/core/crawler/crawl4ai_adapter.py` 작성** (최소 버전) - - 기본: HTTP fetch만 (Trafilatura로 후처리) - - 선택: Crawl4AI dynamic profile (JS 렌더링 필요 시) - - 참고: 기존 설계 Phase 2 내용 단순화 - -3. **`platform/core/extraction/lightweight_extractor.py` 작성** - - 입력: 정제된 본문 텍스트 - - 출력: JSON 형태 - ```json - { - "entities": [ - {"id": "E1", "label": "...", "type": "...", "confidence": 0.9, "evidence_ids": ["EV1"]} - ], - "relations": [ - {"source_id": "E1", "predicate": "...", "target_id": "E2", "confidence": 0.85} - ], - "claims": [], - "evidence": [ - {"id": "EV1", "text": "...", "start_offset": 100, "end_offset": 150} - ], - "warnings": [] - } - ``` - - LLM: OpenAI/Claude (선택), 프롬프트는 간단한 JSON extraction만 - -4. **`CandidateStore` (SQLite 또는 PostgreSQL)** - - 테이블: CandidateEntity, CandidateRelation, EvidenceSpan, SourceDocument, ReviewStatus - - RDF 변환 금지 (후보만 저장) - -5. **간단한 FastAPI 작성** - ``` - POST /extract/url # URL 입력 - POST /extract/file # 파일 업로드 - GET /candidates # 후보 목록 조회 - GET /candidates/{id} # 후보 상세 - ``` - -**Acceptance Gate 0**: -- [ ] 단일 URL 입력 시 **10~30초 이내** 후보 JSON 생성 (기존 OntoCast 방식과 비교해 5배 이상 빠름) -- [ ] 본문 추출 결과를 사람이 확인 가능 (metadata 포함) -- [ ] entity/relation 후보가 SQLite/PostgreSQL에 저장됨 -- [ ] **OntoCast 없이도** 추출 품질을 평가할 수 있음 -- [ ] 동일 URL 재입력 시 fingerprint 기반 중복 감지 가능 -- [ ] Trafilatura + Crawl4AI (선택) 초기 통합 완료 - ---- - -### Phase 1: Fast Pipeline 품질 개선 - -**목표**: 가벼운 추출 파이프라인의 결과 품질을 안정화하고, 무의미한 후보를 필터링한다. - -**작업**: - -1. **Pydantic 스키마 정의** - ```python - class CandidateEntity(BaseModel): - id: str - label: str - type: Literal["concept", "person", "org", ...] # 도메인별 설정 - description: str | None = None - confidence: float # 0.0 ~ 1.0 - evidence_ids: list[str] - - class CandidateRelation(BaseModel): - source_id: str - predicate: str - target_id: str - confidence: float - evidence: list[str] - ``` - -2. **Lightweight JSON 검증 강화** - - "value", "keyword" 같은 무의미한 label 필터링 - - confidence ≤ 0 또는 ≥ 1.0인 값 정정 - - evidence 없는 relation은 저장 금지 - - entity와 relation endpoint 매칭 검사 - -3. **도메인별 entity type/predicate 설정** - - 프로젝트별 기본 타입 팩 정의 - - 사용자 정의 타입 추가 가능 - - 프롬프트에 반영 - -4. **Confidence 점수 부여 개선** - - 초기: LLM 직접 추출값 - - 개선: evidence 길이, 문맥 유사도, type 신뢰도 등 반영 - -**Acceptance Gate 1**: -- [ ] "value", "keyword", 빈 문자열 같은 무의미한 후보가 저장되지 않음 -- [ ] evidence 없는 relation은 저장 금지 -- [ ] entity와 relation endpoint가 서로 일치함 (존재하는 entity만 참조) -- [ ] 후보 품질을 사람이 빠르게 검토 가능 (metadata 포함) -- [ ] Phase 0 성능 유지 (10~30초) - ---- - -### Phase 2: Guardrails 검증 강화 - -**목표**: Lightweight Extractor가 생성한 JSON 후보를 Guardrails로 강제 검증한다. 잘못된 값이 저장소에 들어가는 것을 방지한다. - -**배경**: 기존 설계 Phase 3에서 OntoCast Renderer 출력을 Guardrails로 검증했으나, 여기서는 더 초기 단계인 Lightweight JSON extraction 결과를 검증한다. - -**작업**: - -1. **의존성 추가**: `guardrails-ai>=0.x` (Hub/telemetry 비활성화 필수) - -2. **Validator 작성** (Guardrails 분석 §15.4 참고, 단순화) - - `EntityIdFormatValidator`: id는 알파벳+숫자만 - - `UniqueEntityIdValidator`: 동일 id 중복 없음 - - `RelationEndpointExistsValidator`: source/target entity 존재 확인 - - `ConfidenceRangeValidator`: confidence는 0.0~1.0 - - `EvidenceExistsValidator`: evidence_ids가 실제로 존재하는 지 확인 - - `NoSelfRelationValidator`: source ≠ target - -3. **Guardrails Guard 설정** - ```python - guard = Guard.for_pydantic( - CandidateEntity, - validators=[...], - num_reasks=1, - on_fail=OnFailAction.FILTER - ) - ``` - -4. **Lightweight Extractor 후 검증 적용** - - LLM JSON 출력 → Guardrails Guard → CandidateStore - - 검증 실패 시: fix/filter/refrain 정책 적용 - - 실패 후보는 review queue로 이동 (삭제 금지) - -**Acceptance Gate 2**: -- [ ] confidence > 1.0 같은 오류 자동 fix 또는 filter -- [ ] 존재하지 않는 entity를 참조하는 relation 차단 -- [ ] evidence 없는 후보 자동 필터링 -- [ ] 검증 실패 결과도 감사 로그에 기록됨 -- [ ] Guardrails Hub/telemetry 외부 통신 없음 (환경변수 확인) -- [ ] Phase 0~1 성능 유지 - ---- - -### Phase 3: Review & Approval 시스템 - -**목표**: 검증된 후보에 대해 사람이 검수하고, 자동 승인 정책을 적용할 수 있도록 한다. - -**작업**: - -1. **Review UI (간단한 버전)** - - 후보 목록 표시 - - entity/relation별 evidence 하이라이트 (원문으로부터) - - 승인/반려/수정 버튼 - -2. **자동 승인 정책 정의** - ```python - if confidence >= 0.95 and source_trust >= 0.9: - status = "auto_approved" - else: - status = "pending" # 사람 검수 대기 - ``` - -3. **ReviewStatus 테이블 관리** - - pending: 사람 검수 대기 - - approved: 사람이 승인 - - auto_approved: 정책으로 자동 승인 - - rejected: 반려 - -4. **승인된 후보만 다음 단계로** - - Precision Path (OntoCast) 또는 - - 최종 저장소 (RDF/Neo4j)로 전달 - -**Acceptance Gate 3**: -- [ ] Review 큐에 100개 후보를 5분 내 표시 -- [ ] evidence 원문 추적 가능 -- [ ] 자동 승인 정책이 정확히 작동 -- [ ] 승인/반려 이력이 저장됨 -- [ ] Phase 0~2 기능 회귀 없음 - ---- - -### Phase 4: OntoCast 선택적 통합 ⭐ (역할 변경) - -**목표**: OntoCast를 전체 파이프라인의 Base에서 **승인된 후보를 RDF/OWL로 정밀 반영하는 선택적 엔진**으로 통합한다. - -**배경**: Phase 0 테스트 결과, OntoCast 전체 파이프라인을 초기부터 사용하는 것은 비효율적임. 따라서 검증된 후보만 batch 단위로 OntoCast에 전달하여 처리한다. - -**처리 흐름**: -``` -Approved CandidateEntity/Relation (batch) -→ OntoCast Adapter (JSON → internal format 변환) -→ OntoCast RENDER_ONTOLOGY_UPDATE + Critic loop -→ GraphUpdate 생성 -→ Guardrails (최종 검증) -→ Fuseki RDF 저장 (SPARQL UPDATE) -``` - -**작업**: - -1. **`platform/core/ontocast_adapter.py` 작성** - - CandidateEntity/Relation → OntoCast 입력 포맷 변환 - - batch_size: 10~50개 단위 (문서 단위 아님) - - 처리 실패 시 후보는 보존, review queue로 이동 - -2. **OntoCast 호출 원칙** - - ❌ 모든 문서를 직접 OntoCast에 보내지 않음 - - ✅ 검수된 후보 또는 고신뢰 후보만 전달 - - ✅ batch 단위로 묶어 호출 (문서 단위 호출 금지) - - ✅ 실패해도 Fast Pipeline은 계속 동작 - -3. **OntoCast 처리 선택지** - - **필수 OntoCast**: confidence ≥ 0.95 + approved by human - - **선택 OntoCast**: 신뢰도 낮은 후보 batch 처리 (비용 증가) - - **Skip OntoCast**: 임시 저장소(Neo4j draft)에만 유지 - -4. **GraphUpdate 결과 처리** - - OntoCast GraphUpdate → SPARQL 문장 - - Fuseki에 commit - - Worker가 async로 Neo4j projection 갱신 - -5. **Cost & Time Tracking** - - OntoCast 호출당 비용/시간 기록 - - BudgetTracker에 반영 - - 운영자가 비용 제어 가능 - -**Acceptance Gate 5**: -- [ ] 승인된 후보 10~50개를 batch로 OntoCast 처리 가능 -- [ ] 처리 시간: 각 batch마다 1~5분 (문서 단위 처리보다 효율) -- [ ] 처리 실패 시 후보 데이터는 보존됨 (손실 없음) -- [ ] **OntoCast 없이도 Fast Pipeline은 계속 독립 동작** -- [ ] OntoCast 처리 결과가 RDF/OWL로 Fuseki에 저장됨 -- [ ] GraphUpdate는 SPARQL이므로 증분 갱신 가능 (기존 RDF와 merge) - -### Phase 5: Neo4j GraphRAG 통합 (Projection + 검색/RAG) - -**목표**: Fuseki에 commit된 RDF를 Neo4j Property Graph로 projection하고, Neo4j GraphRAG의 Retriever/GraphRAG로 검색/QA 기능을 추가한다. - -**작업**: -1. 의존성 추가: `neo4j-graphrag[openai,experimental]==1.16.0` (버전 고정) -2. APOC core 설치된 Neo4j 5.18.1+ 배포 (Docker compose 추가) -3. `platform/core/projection/rdf_to_neo4j.py` 신설: - - Fuseki의 `Ontology` + `Facts` graph → `Neo4jGraph(nodes, relationships)` 변환 - - `KGWriter`로 `Neo4jWriter` 사용해 upsert - - Lexical graph(Document/Chunk) + Entity 노드를 `LexicalGraphBuilder` 표준에 맞춰 생성 -4. Chunk embedding: - - OntoCast의 청킹 결과(`ContentUnit.text`)에 `OpenAIEmbeddings` 또는 `SentenceTransformerEmbeddings` 적용 - - Neo4j chunk vector index 자동 생성 (`indexes.py`) -5. 검색 API 신설: - - `POST /search/vector` → `VectorRetriever` - - `POST /search/hybrid` → `HybridRetriever` - - `POST /search/text2cypher` → `Text2CypherRetriever` (read-only 강제) - - `POST /search/graphrag` → `GraphRAG` 답변 생성 -6. Text2Cypher 보안 (Neo4j GraphRAG 분석 §13.4): - - read-only 검사 + 허용 schema 제한 + query timeout + result limit -7. Entity Resolver: - - `SinglePropertyExactMatchResolver` 기본 활성화 - - OntoCast Entity Aggregation 결과(`owl:sameAs`)와 함께 사용해 중복 병합 - -**Acceptance Gate 4**: -- [ ] Fuseki commit 후 5초 이내 Neo4j projection 동기화 완료 -- [ ] Lexical graph(Document/Chunk/Entity)와 provenance(`FROM_CHUNK`) 정상 생성 -- [ ] Vector 검색 결과의 chunk → entity provenance 추적 가능 -- [ ] Text2Cypher가 write/delete 쿼리를 차단 -- [ ] GraphRAG 답변에 evidence chunk URL 포함 -- [ ] Phase 0~3 기능 회귀 없음 - ---- - -### Phase 5: Neo4j GraphRAG 통합 & 검색/RAG - -**목표**: 최종 RDF (또는 검증된 후보)를 Neo4j에 projection하고, Neo4j GraphRAG의 retriever/QA 기능으로 검색/RAG을 제공한다. - -**작업**: - -1. **의존성 추가**: `neo4j-graphrag==1.16.0` (버전 고정) - -2. **Projection 어댑터** (`platform/core/projection/rdf_to_neo4j.py`) - - Fuseki RDF → Neo4j Property Graph 변환 - - Node: Entity (label, type, confidence) - - Relationship: Predicate (confidence, evidence) - - Chunk/Document: Lexical graph builder 활용 - -3. **Embedding & Vector Index** - - 최종 chunk → OpenAI/SentenceTransformer embedding - - Neo4j vector index 자동 생성 - -4. **검색 API 추가** - ``` - POST /search/vector # Vector 검색 - POST /search/hybrid # Vector + fulltext - POST /search/graphrag # QA (답변 + evidence) - ``` - -5. **Text2Cypher 보안** (Phase 4 기존 내용 적용) - - read-only 강제 - - allowlist schema - - timeout + result limit - -**Acceptance Gate 5**: -- [ ] Fuseki commit 후 5초 이내 Neo4j projection 동기화 -- [ ] Vector 검색으로 chunk 찾고, entity/predicate 추적 가능 -- [ ] GraphRAG 답변에 evidence chunk + URL 포함 -- [ ] Text2Cypher가 write/delete 차단 -- [ ] Phase 0~4 기능 회귀 없음 - ---- - -## 6. 성능 최적화 전략 - -### 6.1 Fast Path vs Precision Path 분리 - -| 단계 | Fast Path | Precision Path (Optional) | -|---|---|---| -| 추출 엔진 | Lightweight JSON | OntoCast RDF | -| 처리 속도 | 10~30초 | 1~5분 (batch) | -| 저장소 | PostgreSQL/SQLite | Fuseki RDF | -| 용도 | 초벌 후보, 사람 검수 | 최종 canonical graph | -| 비용 | 낮음 (LLM 1회) | 높음 (Critic loop) | - -### 6.2 OntoCast 호출 최적화 - -``` -❌ 하지 말아야 할 것: -- 모든 문서를 OntoCast에 넣기 -- 문서 단위 호출 -- 저신뢰 후보도 OntoCast 처리 - -✅ 해야 할 것: -- 검증된 후보만 선택 -- batch 단위 호출 (10~50개) -- 사람 검수를 거친 데이터 -- 병렬 배치 처리로 대기 시간 단축 -``` - -### 6.3 메모리/비용 관리 - -- **LLM 캐싱**: OntoCast의 기존 Cacher 활용 -- **BudgetTracker**: 월간/일일 한도 + 자동 차단 -- **Worker 풀**: Arq/Celery로 병렬 처리 (느린 OntoCast는 background로) - ---- - -## 7. 이전 설계와의 차이점 요약 - -| 항목 | 기존 설계 | 수정 설계 | -|---|---|---| -| **Phase 0** | OntoCast 전체 파이프라인 | Fast JSON Extraction | -| **초기 결과** | RDF/OWL | JSON (후보) | -| **중간 단계** | RDF → Fuseki 직접 저장 | JSON → 사람 검수 → 선택적 OntoCast | -| **OntoCast 역할** | 입력 처리의 중심 | 정밀 RDF 갱신의 선택지 | -| **처리 속도** | 느림 (전체 파이프라인) | 빠름 (Phase 0~2) | -| **실패 시 영향** | 전체 job 실패 | Fast Pipeline은 계속 동작 | -| **테스트 용이성** | 어려움 (RDF 중심) | 쉬움 (JSON 중간결과 명확) | -| **대량 처리** | 부적합 | 적합 (비동기, 배치) | - ---- - -## 6. 상용 제품 수준 기능 명세 (Functional Spec) - -기능 ID 체계: `[영역코드]-[순번]`. 영역코드: PROJ, ACQ, ONT, REV, SRCH, OPS, GOV. - -### 6.1 프로젝트/멀티테넌트 관리 (PROJ) - -| ID | 기능명 | 설명 | 우선순위 | -|---|---|---|---| -| PROJ-001 | 온톨로지 프로젝트 생성 | 이름, 도메인, 기본 언어, 기본 타입 팩, Neo4j DB/Fuseki dataset 매핑 | P0 | -| PROJ-002 | 멤버/권한 관리 | Owner/Admin/Editor/Reviewer/Viewer 역할 | P0 | -| PROJ-003 | LLM Profile 선택 | provider/model/api_key/temperature/max_tokens (Neo4j GraphRAG `LLMConfig` 차용) | P0 | -| PROJ-004 | Embedding Profile 선택 | provider/model/dimension (cohere/openai/sentence-transformers) | P0 | -| PROJ-005 | 비용 예산 설정 | LLM call/token/검색 호출 일일·월간 한도 (OntoCast BudgetTracker 확장) | P1 | -| PROJ-006 | 프로젝트 fork/복제 | 기존 프로젝트의 스키마/설정만 복제 | P2 | -| PROJ-007 | 프로젝트 export/import | 전체 RDF + 설정을 zip으로 export, import | P1 | - -### 6.2 데이터 수집 (ACQ) - -| ID | 기능명 | 사용 컴포넌트 | 우선순위 | -|---|---|---|---| -| ACQ-001 | 단일 URL scrape | Crawl4AI `fast_static` + Trafilatura | P0 | -| ACQ-002 | 동적 페이지 scrape | Crawl4AI `dynamic_page` | P0 | -| ACQ-003 | 사이트 deep crawl | Crawl4AI `deep_discovery` (BFS/DFS/Best-First) | P1 | -| ACQ-004 | sitemap/feed 발견 | Trafilatura `sitemap_search`, `find_feed_urls` | P1 | -| ACQ-005 | URL seeding 미리보기 | Crawl4AI `AsyncUrlSeeder` | P1 | -| ACQ-006 | 파일 업로드 (PDF/DOCX/MD) | Lightweight Extractor + docling (선택적 OntoCast 후처리 가능) | P0 | -| ACQ-007 | 배치 URL import (CSV) | URL 목록 업로드 → 큐잉 | P1 | -| ACQ-008 | 중복 문서 dedup | Trafilatura SimHash fingerprint | P0 | -| ACQ-009 | 도메인 allow/block list | 프로젝트별 정책 DB | P0 | -| ACQ-010 | robots.txt 준수 모드 | strict/respect/ignore (감사 로그 필수) | P0 | -| ACQ-011 | 변경 감지 (changeTracking) | 주기 재크롤 + fingerprint 비교 | P2 | -| ACQ-012 | screenshot/PDF archive | Crawl4AI `full_capture` Profile | P2 | - -### 6.3 온톨로지 구축 (ONT) - -| ID | 기능명 | 사용 컴포넌트 | 우선순위 | -|---|---|---|---| -| ONT-001 | 수동 스키마 작성 | Neo4j GraphRAG `GraphSchema` 모델 그대로 사용 + UI | P0 | -| ONT-002 | 자동 스키마 추출 | Neo4j GraphRAG `SchemaFromTextExtractor` 기반 + 선택적 OntoCast 보조 | P0 | -| ONT-003 | 자유 추출 | OntoCast `schema="FREE"` 모드 | P1 | -| ONT-004 | 온톨로지 증분 갱신 | OntoCast `GraphUpdate` SPARQL (핵심 차별 기능) | P0 | -| ONT-005 | 온톨로지 비평/재시도 | OntoCast Critic loop + Guardrails | P0 | -| ONT-006 | 사실(facts) 추출 | OntoCast `RENDER_FACTS` + Guardrails | P0 | -| ONT-007 | Entity Aggregation | OntoCast `tool/agg/` (embedding clustering + URI 정규화 + owl:sameAs) | P0 | -| ONT-008 | Entity Resolver (사후) | Neo4j GraphRAG `FuzzyMatchResolver` + 검수 큐 | P1 | -| ONT-009 | 스키마 버전 관리 | OntoCast `GraphVersionManager` + draft/published/deprecated | P1 | -| ONT-010 | 스키마 diff 뷰어 | 버전 간 class/property/pattern 변경 | P1 | -| ONT-011 | 스키마 마이그레이션 | rename/merge/split + facts auto-migration | P2 | -| ONT-012 | SHACL/OWL 검증 | rdflib + owlready2 | P2 | -| ONT-013 | 다국어 label 관리 | rdfs:label + lang tag (ko/en/...) | P1 | - -### 6.4 검수/승인 (REV) - -| ID | 기능명 | 설명 | 우선순위 | -|---|---|---|---| -| REV-001 | 추출 후보 큐 | Renderer 결과를 Fuseki commit 전 검토용으로 저장 | P0 | -| REV-002 | Evidence 하이라이트 | entity/relation → `FROM_CHUNK` → 원문 텍스트 표시 (Neo4j GraphRAG lexical graph) | P0 | -| REV-003 | 단위 승인/반려 | node/relationship/property 단위 | P0 | -| REV-004 | Pruned 후보 검토 | Neo4j GraphRAG `GraphPruning`에서 제거된 후보를 schema 후보로 제안 | P1 | -| REV-005 | Merge 후보 검토 | Resolver 후보 그룹의 시각화 + 승인/반려 | P1 | -| REV-006 | 일괄 승인 정책 | confidence ≥ X + source 신뢰도 ≥ Y → 자동 승인 | P1 | -| REV-007 | 변경 이력 (audit) | 누가/언제/무엇을/왜 변경 (RDF reification 또는 별도 audit log) | P0 | -| REV-008 | 롤백 | 특정 시점의 RDF graph로 복원 | P2 | - -### 6.5 검색/RAG (SRCH) - -| ID | 기능명 | 사용 컴포넌트 | 우선순위 | -|---|---|---|---| -| SRCH-001 | Vector 검색 | Neo4j GraphRAG `VectorRetriever` | P0 | -| SRCH-002 | Hybrid 검색 (vector+fulltext) | `HybridRetriever` | P0 | -| SRCH-003 | Graph 확장 검색 | `VectorCypherRetriever` (chunk → entity → neighbor) | P0 | -| SRCH-004 | Text2Cypher (NL → Cypher) | `Text2CypherRetriever` (read-only) | P1 | -| SRCH-005 | SPARQL 직접 질의 | Fuseki SPARQL endpoint (관리자 권한) | P1 | -| SRCH-006 | GraphRAG QA | `GraphRAG` + evidence URL 포함 | P0 | -| SRCH-007 | Faceted 탐색 | entity type/predicate별 filter | P1 | -| SRCH-008 | Entity 상세 페이지 | 모든 property + 인입/인출 relation + evidence | P0 | -| SRCH-009 | Subgraph 시각화 | Neo4j Browser embed 또는 Cytoscape.js | P1 | -| SRCH-010 | 저장된 질의 (saved query) | 즐겨찾기 + 알림 | P2 | - -### 6.6 운영/관측 (OPS) - -| ID | 기능명 | 설명 | 우선순위 | -|---|---|---|---| -| OPS-001 | Job Queue/Worker | 모든 비동기 작업의 큐잉/재시도/취소 | P0 | -| OPS-002 | Job 진행률 스트리밍 | WebSocket/SSE로 실시간 진행 | P0 | -| OPS-003 | BudgetTracker | LLM call/token/검색/크롤 호출 비용 추적 | P0 | -| OPS-004 | LLM Response Cache | OntoCast `Cacher` 그대로 사용 | P0 | -| OPS-005 | Prometheus metrics | Crawl4AI 분석 §16.4 패턴 | P1 | -| OPS-006 | 감사 로그 | 모든 RDF 변경 + 사용자 액션 | P0 | -| OPS-007 | 에러 알림 | webhook + email + slack | P1 | -| OPS-008 | 백업/복구 | Fuseki + Neo4j + Postgres 일관성 있는 백업 | P1 | -| OPS-009 | 멀티 환경 | dev/staging/prod 분리 | P0 | - -### 6.7 거버넌스/보안 (GOV) - -| ID | 기능명 | 설명 | 우선순위 | -|---|---|---|---| -| GOV-001 | 인증 (OAuth2/OIDC) | Google/GitHub/Azure AD | P0 | -| GOV-002 | RBAC | 프로젝트별 역할 (PROJ-002) | P0 | -| GOV-003 | API Key | 외부 시스템 연동용 | P0 | -| GOV-004 | Rate Limiting | 사용자별/IP별 | P0 | -| GOV-005 | Text2Cypher 샌드박스 | read-only + timeout + result limit + allowlist | P0 | -| GOV-006 | 비밀(secret) vault | LLM API key 암호화 저장 | P0 | -| GOV-007 | 데이터 보존 정책 | raw HTML/PDF 보존 기간 + zero-retention 모드 | P1 | -| GOV-008 | 라이선스/출처 표기 | source별 license metadata 저장 + 결과에 노출 | P1 | - ---- - -## 7. 데이터 모델 표준 (Canonical Data Models) - -본 절은 5개 소스를 잇기 위한 공통 데이터 계약이다. 모든 어댑터는 이 모델로 변환한다. - -### 7.1 ContentUnit (문서 청크의 표준 표현) - -```python -class ContentUnit(BaseModel): - id: str # UUID - project_id: str - source_id: str - source_url: str | None # canonical URL (Trafilatura) - file_path: str | None - document_type: Literal["html","pdf","markdown","docx","inline_text"] - - text: str # 정제된 본문 - body_xml: bytes | None # Trafilatura Document.body (lxml serialized) - markdown: str | None # Crawl4AI markdown 또는 변환본 - - chunk_index: int - total_chunks: int - - title: str | None - author: str | None - publish_date: str | None # ISO-8601 - language: str | None - sitename: str | None - - fingerprint: str | None # Trafilatura SimHash - content_hash: str # SHA-256 of text - - metadata: dict # raw provider metadata - retrieved_at: str # ISO-8601 - extracted_by: str # "crawl4ai+trafilatura" -``` - -### 7.2 OntologyExtractionResult (Guardrails 검증 대상) - -Guardrails §15.5 그대로 채택. 위 §5 Phase 3 참조. - -### 7.3 GraphUpdate (OntoCast 그대로) - -OntoCast `sparql_models.GraphUpdate`를 그대로 사용. 변경 금지. - -### 7.4 Job - -```python -class Job(BaseModel): - id: str - project_id: str - type: Literal["scrape","crawl","extract","project","resolve","maintenance"] - status: Literal["queued","running","paused","completed","failed","cancelled"] - progress: float # 0.0 ~ 1.0 - - input: dict # 작업 입력 (URL, options, etc.) - output: dict | None # 결과 요약 - - started_at: str | None - finished_at: str | None - - budget: BudgetTracker # OntoCast 그대로 - error: str | None - audit_log_id: str -``` - -### 7.5 ReviewItem - -```python -class ReviewItem(BaseModel): - id: str - project_id: str - job_id: str - - target_type: Literal["entity","relation","property","merge_group","prune_candidate"] - target_data: dict # 후보 데이터 - evidence: list[dict] # chunk_id + text span - - confidence: float - source_trust: float - - status: Literal["pending","approved","rejected","auto_approved"] - decided_by: str | None # user_id - decided_at: str | None - reason: str | None -``` - ---- - -## 8. API 표준 (RESTful, 일부 WebSocket) - -전체는 OpenAPI 3.1 spec으로 별도 관리. 여기서는 핵심 endpoint만 명시. - -| Method | Path | 설명 | 인용 근거 | -|---|---|---|---| -| POST | `/projects` | 프로젝트 생성 | PROJ-001 | -| GET | `/projects/{id}` | 프로젝트 조회 | - | -| POST | `/projects/{id}/sources` | 데이터 소스 등록 | ACQ | -| POST | `/projects/{id}/sources/{sid}/scrape` | 단일 URL scrape | ACQ-001 | -| POST | `/projects/{id}/sources/{sid}/crawl` | deep crawl | ACQ-003 | -| POST | `/projects/{id}/sources/{sid}/seed` | URL seeding preview | ACQ-005 | -| POST | `/projects/{id}/upload` | 파일 업로드 | ACQ-006 | -| GET | `/projects/{id}/schemas` | 스키마 목록 | ONT-001 | -| POST | `/projects/{id}/schemas` | 스키마 작성 | ONT-001 | -| POST | `/projects/{id}/schemas/extract` | 자동 스키마 추출 | ONT-002 | -| POST | `/projects/{id}/schemas/{sid}/publish` | 스키마 발행 | ONT-009 | -| POST | `/projects/{id}/process` | 문서 처리 (전체 OntoCast 워크플로우) | OntoCast §13.3 | -| GET | `/jobs/{job_id}` | Job 조회 | OPS-001 | -| GET | `/jobs/{job_id}/progress` (WS) | 진행률 스트리밍 | OPS-002 | -| POST | `/jobs/{job_id}/cancel` | Job 취소 | OPS-001 | -| GET | `/projects/{id}/review` | 검수 큐 | REV-001 | -| POST | `/projects/{id}/review/{rid}/approve` | 승인 | REV-003 | -| POST | `/projects/{id}/review/{rid}/reject` | 반려 | REV-003 | -| POST | `/projects/{id}/search/vector` | Vector 검색 | SRCH-001 | -| POST | `/projects/{id}/search/hybrid` | Hybrid 검색 | SRCH-002 | -| POST | `/projects/{id}/search/text2cypher` | NL → Cypher | SRCH-004 | -| POST | `/projects/{id}/search/graphrag` | QA | SRCH-006 | -| GET | `/projects/{id}/entities/{eid}` | Entity 상세 | SRCH-008 | -| POST | `/projects/{id}/maintenance/run` | 유지보수 루프 | Phase 5 | - ---- - -## 9. 어떤 코드를 어디서 가져오는가 (Module Map) - -각 5개 소스에서 가져올 모듈을 정확히 명시. **"그대로"=수정 금지, "어댑터"=얇은 래퍼만 작성**. - -### 9.1 OntoCast (Optional Precision RDF/OWL Engine) - -| 가져올 모듈 | 방식 | 수정 사항 | -|---|---|---| -| `ontocast/onto/state.py` (AgentState) | 그대로 | - | -| `ontocast/onto/unit_states.py` | 그대로 | - | -| `ontocast/onto/sparql_models.py` (GraphUpdate) | 그대로 | - | -| `ontocast/onto/rdfgraph.py` | 그대로 | - | -| `ontocast/onto/ontology.py` | 그대로 | - | -| `ontocast/stategraph/` | 그대로 | Phase 5에서 노드 추가만 | -| `ontocast/agent/render_*.py` | 그대로 | - | -| `ontocast/agent/criticise_*.py` | 그대로 | - | -| `ontocast/agent/select_ontology.py` | **버그 수정** | None index 불일치 (분석 §13.1) | -| `ontocast/agent/convert_document.py` | **확장** | 다중 파일 처리 (분석 §13.1) | -| `ontocast/tool/agg/` | 그대로 | - | -| `ontocast/tool/triple_manager/` | 그대로 | - | -| `ontocast/tool/llm.py` | **래핑** | Guardrails Guard 통과 (Phase 3) | -| `ontocast/tool/cache.py` | 그대로 | - | -| `ontocast/toolbox.py` | 그대로 | - | -| `ontocast/cli/serve.py` (Robyn) | **재작성** | FastAPI로 (Phase 0) | - -### 9.2 Trafilatura - -| 가져올 함수 | 방식 | -|---|---| -| `bare_extraction(output_format="python")` | 그대로 import | -| `extract_metadata` | 그대로 import | -| `sitemap_search` | 그대로 import | -| `find_feed_urls` | 그대로 import | -| `content_fingerprint`, `Simhash` | 그대로 import | -| `trafilatura.meta.reset_caches` | 그대로 import (배치 종료 시 호출) | - -신규 어댑터: `platform/core/extractors/web_extractor.py` (§17 Trafilatura 분석의 `extract_for_ontology` 그대로) - -### 9.3 Crawl4AI - -| 가져올 클래스 | 방식 | -|---|---| -| `AsyncWebCrawler` | 그대로 | -| `BrowserConfig`, `CrawlerRunConfig`, `CacheMode` | 그대로 | -| `CrawlResult` | 그대로 | -| `LLMExtractionStrategy`, `JsonCssExtractionStrategy` | 그대로 | -| `BFSDeepCrawlStrategy`, `BestFirstCrawlingStrategy` | 그대로 | -| `AsyncUrlSeeder`, `SeedingConfig` | 그대로 | -| `MemoryAdaptiveDispatcher` | 그대로 | -| Docker FastAPI 서버 코드 | **사용 안 함** (자체 FastAPI 사용) | - -신규 어댑터: `platform/core/crawler/crawl4ai_adapter.py` (Crawl4AI 분석 §21.1의 권장 계층 구조) - -### 9.4 Guardrails - -| 가져올 모듈 | 방식 | -|---|---| -| `guardrails.Guard.for_pydantic` | 그대로 | -| `guardrails.AsyncGuard` | 그대로 | -| `guardrails.classes.validation_outcome.ValidationOutcome` | 그대로 | -| `guardrails.actions.*` | 그대로 | -| `guardrails.types.on_fail.OnFailAction` | 그대로 | -| `guardrails.validator_base.Validator` (커스텀 validator 작성용 base) | 그대로 | -| `guardrails.hub.*` | **사용 안 함** (외부 통신 차단) | -| `guardrails.telemetry.*` | **사용 안 함** | -| `guardrails.cli.*` | **사용 안 함** | - -신규 작성: `platform/core/validation/validators.py` (Guardrails 분석 §15.4의 12개 validator) - -### 9.5 Neo4j GraphRAG - -| 가져올 클래스 | 방식 | -|---|---| -| `GraphSchema`, `NodeType`, `RelationshipType`, `Pattern`, `ConstraintType` | 그대로 | -| `SimpleKGPipeline` | **사용 안 함** (OntoCast 워크플로우가 우선) | -| `LLMEntityRelationExtractor` | **사용 안 함** (OntoCast Renderer가 우선) | -| `Neo4jWriter`, `Neo4jGraph`, `Neo4jNode`, `Neo4jRelationship` | 그대로 (Projection 용) | -| `LexicalGraphBuilder` | 그대로 | -| `SinglePropertyExactMatchResolver`, `FuzzyMatchResolver` | 그대로 | -| `VectorRetriever`, `HybridRetriever`, `VectorCypherRetriever` | 그대로 | -| `Text2CypherRetriever` | 그대로 (read-only 강제) | -| `GraphRAG` | 그대로 | -| `embeddings/*`, `llm/*` | 그대로 (선택적) | - -신규 어댑터: `platform/core/projection/rdf_to_neo4j.py` (Fuseki RDF → `Neo4jGraph` 변환) - -### 9.6 Knowledge Agent (패턴/프롬프트만) - -| 가져올 자산 | 방식 | -|---|---| -| `prompts/analyst_prompt.txt` | **수정 후 사용** (OntoCast `OntologyExtractionResult` 스키마에 맞춤) | -| `prompts/planner_prompt.txt` | 수정 후 사용 | -| `prompts/refiner_prompt.txt` | 수정 후 사용 | -| `prompts/summarizer_prompt.txt` | 수정 후 사용 | -| `prompts/search_ranker_prompt.txt` | 수정 후 사용 | -| `prompts/ingester_prompt.txt` | 수정 후 사용 | -| `lightrag/prompt.py` (엔티티/관계 추출 프롬프트) | **참고만** (OntoCast Renderer가 이미 있음) | -| 코드 전체 | **사용 안 함** (다수 버그 — 분석 §9.1) | - ---- - -## 10. 예상 리스크와 대응 - -| 리스크 | 영향 | 대응 | -|---|---|---| -| OntoCast `select_ontology.py` 버그 | 워크플로우 실패 | Phase 0에서 즉시 수정 | -| Crawl4AI/Playwright 메모리 누수 | 운영 장애 | `max_pages_before_recycle` 설정 + browser pool monitor | -| Trafilatura 전역 LRU cache 충돌 | 다중 테넌트에서 결과 오염 | 작업 단위 `reset_caches()` | -| Guardrails Hub 외부 통신 | 보안/네트워크 의존 | Hub/telemetry 비활성화 환경변수 강제 | -| Neo4j GraphRAG `experimental` API 변경 | upstream 호환성 | 버전 고정 (`==1.16.0`) + 우리 코드는 어댑터로만 접근 | -| Fuseki ↔ Neo4j 동기화 지연 | 검색 결과와 진실 불일치 | "Last sync at" 표시 + 강제 동기화 API | -| LLM 비용 폭주 | 운영 비용 | OPS-003 BudgetTracker 한도 + 자동 차단 | -| Text2Cypher 인젝션 | 보안 | read-only 강제 + allowlist + timeout (GOV-005) | -| robots.txt 위반 | 법적 리스크 | ACQ-010 strict 모드 기본 + 감사 로그 | -| 라이선스 (특히 AGPL 회피) | 배포 제약 | Apache/MIT만 채택. Firecrawl 제외 결정 근거 | - ---- - -## 11. 기술 스택 요약 (전체) - -| 영역 | 선택 | -|---|---| -| 언어 | Python 3.12+ (OntoCast 요구사항이 가장 높음) | -| API | FastAPI (모든 통합 소스가 OpenAPI 친화) | -| LangGraph 워크플로우 | OntoCast 기존 사용 | -| Validation | Pydantic v2 + Guardrails | -| 비동기 | asyncio (Crawl4AI/Trafilatura 모두 지원) | -| Job Queue | Arq (Redis 기반, 가벼움) 또는 Celery (대규모) | -| 메타데이터 DB | PostgreSQL 16+ | -| Canonical RDF Store | Apache Jena Fuseki 5+ | -| Property Graph | Neo4j 5.18.1+ with APOC core | -| Object Storage | S3 호환 (MinIO 로컬, AWS S3 운영) | -| 캐시 | Redis 7+ | -| 관측 | OpenTelemetry + Prometheus + Grafana | -| 컨테이너 | Docker Compose (개발) / Kubernetes (운영) | -| 인증 | Authlib + OAuth2/OIDC | -| 프론트엔드 | (자유 선택, 권장 Next.js 14 + shadcn/ui) | - ---- - -## 12. 작업 단위 분해 (PR 단위) - -각 Phase 내부에서 PR 단위로 쪼갠 예시. - -### Phase 0: Fast Extraction MVP - -- **0.1** Trafilatura 의존성 + `web_extractor.py` (본문/메타 추출) -- **0.2** Crawl4AI 최소 adapter (정적 수집 우선) -- **0.3** `lightweight_extractor.py` + 프롬프트 (JSON 출력) -- **0.4** CandidateStore 테이블 정의 (PostgreSQL/SQLite) -- **0.5** FastAPI `/extract/url`, `/extract/file`, `/candidates` endpoint -- **0.6** 단일 URL 테스트 (10~30초 내 결과) -- **0.7** Acceptance Gate 0 확인 - -### Phase 1: Fast Pipeline 품질 개선 - -- **1.1** Pydantic 스키마 정의 (CandidateEntity/Relation) -- **1.2** 무의미한 후보 필터링 로직 -- **1.3** confidence/evidence 검증 -- **1.4** entity/relation endpoint 매칭 검사 -- **1.5** 도메인별 type/predicate 설정 지원 -- **1.6** 한국어 페이지 3종 테스트 (뉴스/블로그/쇼핑) -- **1.7** Acceptance Gate 1 확인 - -### Phase 2: Guardrails 검증 강화 - -- **2.1** `guardrails-ai` 의존성 + 환경변수 설정 (Hub/telemetry 비활성화) -- **2.2** Validator 작성 6종 (EntityIdFormat, UniqueEntityId, RelationEndpoint, ConfidenceRange, Evidence, NoSelfRelation) -- **2.3** Guardrails Guard 통합 (Lightweight Extractor 후 검증) -- **2.4** 검증 실패 후보 → review queue로 이동 -- **2.5** Guardrails 로그 + 감사 기록 -- **2.6** Acceptance Gate 2 확인 - -### Phase 3: Review & Approval 시스템 - -- **3.1** Review UI (후보 목록 + evidence 하이라이트) -- **3.2** 승인/반려/수정 버튼 -- **3.3** ReviewStatus 테이블 관리 -- **3.4** 자동 승인 정책 정의 (confidence ≥ 0.95) -- **3.5** 승인 이력 저장 (감사 로그) -- **3.6** Acceptance Gate 3 확인 - -### Phase 4: OntoCast 선택적 통합 ⭐ - -- **4.1** `ontocast_adapter.py` (Candidate JSON → OntoCast 입력 포맷) -- **4.2** batch 단위 호출 (10~50개) -- **4.3** OntoCast 처리 실패 → review queue (보존, 손실 없음) -- **4.4** GraphUpdate → Fuseki commit (SPARQL UPDATE) -- **4.5** BudgetTracker 연동 (OntoCast 호출 비용/시간 기록) -- **4.6** Fast Pipeline 독립 검증 (OntoCast 없이도 동작) -- **4.7** Acceptance Gate 4 확인 - -### Phase 5: Neo4j GraphRAG & 검색 - -- **5.1** `neo4j-graphrag==1.16.0` + Docker compose -- **5.2** `rdf_to_neo4j.py` (Fuseki RDF → Neo4j projection) -- **5.3** Embedding + Vector index -- **5.4** `/search/vector`, `/search/hybrid` endpoint -- **5.5** `/search/graphrag` (QA + evidence) -- **5.6** Text2Cypher 보안 (read-only) -- **5.7** Fuseki commit hook → Neo4j async 동기화 -- **5.8** Acceptance Gate 5 확인 - ---- - -## 13. 다른 AI 에이전트를 위한 체크리스트 - -본 설계서를 받은 AI 에이전트가 작업을 시작하기 전 확인할 항목. - -### 필수 확인 - -- [ ] 본 문서의 §1~§12를 모두 읽었는가? -- [ ] **설계 변경 배경** (§1.1)을 이해했는가? — OntoCast를 Phase 0 Base에서 선택적 엔진으로 변경한 이유 -- [ ] 작업할 Phase의 **Acceptance Gate를 명확히 이해**했는가? -- [ ] **Fast Path와 Precision Path의 차이**를 이해했는가? - -### Phase별 확인 - -#### Phase 0: Fast Extraction MVP -- [ ] OntoCast를 사용하지 않는다는 것을 확인했는가? -- [ ] RDF/OWL 변환을 하지 않고 JSON만 생성하는 것을 확인했는가? -- [ ] 처리 속도 목표 (10~30초)를 이해했는가? - -#### Phase 1~3: Fast Pipeline 안정화 -- [ ] 이 단계에서도 OntoCast는 사용하지 않는가? -- [ ] JSON 검증 및 사람 검수 과정을 이해했는가? - -#### Phase 4: OntoCast 통합 -- [ ] OntoCast는 **batch 후보 단위**로 호출한다는 것을 확인했는가? -- [ ] OntoCast 처리 실패 시에도 Fast Pipeline이 계속 동작한다는 것을 확인했는가? -- [ ] GraphUpdate → Fuseki commit이 SPARQL 기반임을 확인했는가? - -#### Phase 5: 검색/RAG -- [ ] Neo4j는 Fuseki의 projection이라는 것을 확인했는가? -- [ ] Text2Cypher는 read-only만 허용한다는 것을 확인했는가? - -### 일반 원칙 - -- [ ] "그대로 사용" 모듈을 수정하려 하고 있지 않은가? -- [ ] "제외(Excluded)" 프로젝트의 코드를 가져오려 하고 있지 않은가? (Firecrawl, OpenDeepResearcher) -- [ ] PR 설명에 어느 분석 자료의 어느 절을 근거로 했는지 명시할 준비가 되었는가? -- [ ] 이전 Phase의 회귀 테스트가 통과하는지 확인할 계획이 있는가? -- [ ] License 고지(Apache 2.0, MIT 등)가 포함되는가? - ---- - -## 14. 마지막 한 마디 - -### 이번 설계 수정의 핵심 - -기존 설계는 이론적으로 완벽했지만, **실제 구현 결과는 다음 문제가 있었다**: - -``` -OntoCast 중심 아키텍처 -→ 처리 속도 느림 (초기 테스트부터 지연) -→ 중간 결과 확인 어려움 (RDF 중심) -→ 대량 처리에 부적합 (비용 폭주) -→ 버그 가능성 높음 (무거운 파이프라인) -``` - -따라서 설계를 다음과 같이 수정했다: - -``` -Fast Path (기본) Precision Path (선택) -가벼운 JSON 추출 → 승인된 후보만 -Pydantic 검증 → OntoCast 정밀화 -사람 검수 → RDF/OWL 저장 -``` - -### 최종 구조 한 문장 - -> **OntoCast는 입력 처리 엔진이 아니라, 검증된 후보를 RDF로 정밀 반영하는 고급 엔진이다.** - -### 이점 - -| 기존 | 수정 | -|---|---| -| ❌ 모든 문서 → OntoCast | ✅ 검증된 후보만 → OntoCast | -| ❌ 초기부터 느림 | ✅ Fast Path로 빠른 피드백 | -| ❌ 중간 과정 불명확 | ✅ JSON 후보로 명확한 단계 | -| ❌ OntoCast 실패 = 전체 실패 | ✅ Fast Pipeline은 독립 동작 | -| ❌ 대량 처리 어려움 | ✅ batch 단위 + 비동기 처리 | - -### 작업 방향 - -다음 Phase부터 이 수정된 설계를 따른다. 특히: - -1. **Phase 0에서 OntoCast를 사용하지 않는다.** -2. **JSON 기반 후보 추출에 집중한다.** -3. **검증된 후보만 OntoCast에 전달한다.** -4. **각 Phase는 이전 Phase 없이 독립적으로 테스트 가능해야 한다.** - -이 원칙을 지키면, 더 안정적이고, 빠르고, 확장 가능한 온톨로지 구축 플랫폼을 만들 수 있다.