diff --git a/ontology_platform/crawler_platform/app/api/routes.py b/ontology_platform/crawler_platform/app/api/routes.py index bfb20d3..6a59e25 100644 --- a/ontology_platform/crawler_platform/app/api/routes.py +++ b/ontology_platform/crawler_platform/app/api/routes.py @@ -44,6 +44,100 @@ from crawler_platform.app.core.research.memory_store import ResearchMemoryStore, DOMAIN_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]{1,79}$") SITE_CRAWL_CANCEL_REQUESTS: set[int] = set() +COMPARISON_KEYS = ("both_agree", "rule_only", "llm_only", "conflict", "rejected_by_validation") + + +def extraction_log_summary(raw_output: dict[str, Any]) -> dict[str, Any]: + candidate_claims = raw_output.get("candidate_claims") + if not isinstance(candidate_claims, list): + candidate_claims = [] + + raw_comparison = raw_output.get("comparison") + if not isinstance(raw_comparison, dict): + raw_comparison = {} + comparison = {key: int(raw_comparison.get(key) or 0) for key in COMPARISON_KEYS} + if not any(comparison.values()): + comparison.update(comparison_from_candidate_claims(candidate_claims)) + validation = raw_output.get("validation") + if isinstance(validation, dict) and comparison["rejected_by_validation"] == 0: + comparison["rejected_by_validation"] = number_or_default(validation.get("rejected_claim_count"), 0) + + return { + "candidate_count": len(candidate_claims), + "comparison": comparison, + "rule_entity_count": number_or_none(raw_output.get("rule_entity_count")), + "rule_claim_count": number_or_derived( + raw_output.get("rule_claim_count"), + candidate_claims, + source="rule", + ), + "llm_entity_count": number_or_none(raw_output.get("llm_entity_count")), + "llm_claim_count": number_or_derived( + raw_output.get("llm_claim_count"), + candidate_claims, + source="llm", + ), + "agreement_claim_count": number_or_default(raw_output.get("agreement_claim_count"), comparison["both_agree"]), + "rule_only_claim_count": number_or_default(raw_output.get("rule_only_claim_count"), comparison["rule_only"]), + "llm_only_claim_count": number_or_default(raw_output.get("llm_only_claim_count"), comparison["llm_only"]), + "conflict_claim_count": number_or_default(raw_output.get("conflict_claim_count"), comparison["conflict"]), + } + + +def comparison_from_candidate_claims(candidate_claims: list[Any]) -> dict[str, int]: + comparison = {key: 0 for key in COMPARISON_KEYS} + for claim in candidate_claims: + metadata = claim_metadata(claim) + agreement = str(metadata.get("agreement") or metadata.get("claim_kind") or "").lower() + if agreement == "rule_and_llm": + comparison["both_agree"] += 1 + elif agreement == "rule_only": + comparison["rule_only"] += 1 + elif agreement == "llm_only": + comparison["llm_only"] += 1 + elif agreement == "conflict": + comparison["conflict"] += 1 + return comparison + + +def claim_metadata(claim: Any) -> dict[str, Any]: + if not isinstance(claim, dict): + return {} + metadata = claim.get("metadata") + return metadata if isinstance(metadata, dict) else {} + + +def number_or_none(value: Any) -> int | None: + if isinstance(value, bool): + return None + if isinstance(value, (int, float)): + return int(value) + return None + + +def number_or_default(value: Any, default: int) -> int: + parsed = number_or_none(value) + return default if parsed is None else parsed + + +def number_or_derived(value: Any, candidate_claims: list[Any], *, source: str) -> int: + parsed = number_or_none(value) + if parsed is not None: + return parsed + return sum(1 for claim in candidate_claims if claim_matches_source(claim, source)) + + +def claim_matches_source(claim: Any, source: str) -> bool: + metadata = claim_metadata(claim) + extraction_source = str(metadata.get("extraction_source") or "").lower() + agreement = str(metadata.get("agreement") or "").lower() + if extraction_source == source: + return True + if source == "rule": + return agreement in {"rule_only", "rule_and_llm"} + if source == "llm": + return agreement in {"llm_only", "rule_and_llm"} + return False class CrawlRequest(BaseModel): @@ -2490,35 +2584,37 @@ def register_routes(app, database_url: str) -> None: .order_by(models.ExtractionLog.created_at.desc()) .limit(limit) ).all() - return [ - { + payload = [] + for log, page in rows: + raw_output = log.raw_output or {} + summary = extraction_log_summary(raw_output) + payload.append({ "id": log.id, "page_url": page.url if page else None, "extractor_name": log.extractor_name, "provider": log.provider, "error": log.error, "created_at": log.created_at.isoformat(), - "validation": (log.raw_output or {}).get("validation"), - "page_context": (log.raw_output or {}).get("page_context"), - "candidate_count": len((log.raw_output or {}).get("candidate_claims") or []), - "extraction_mode": (log.raw_output or {}).get("extraction_mode"), - "effective_extraction_mode": (log.raw_output or {}).get("effective_extraction_mode"), - "comparison": (log.raw_output or {}).get("comparison"), - "rule_entity_count": (log.raw_output or {}).get("rule_entity_count"), - "rule_claim_count": (log.raw_output or {}).get("rule_claim_count"), - "llm_entity_count": (log.raw_output or {}).get("llm_entity_count"), - "llm_claim_count": (log.raw_output or {}).get("llm_claim_count"), - "agreement_claim_count": (log.raw_output or {}).get("agreement_claim_count"), - "rule_only_claim_count": (log.raw_output or {}).get("rule_only_claim_count"), - "llm_only_claim_count": (log.raw_output or {}).get("llm_only_claim_count"), - "conflict_claim_count": (log.raw_output or {}).get("conflict_claim_count"), - "llm_skipped": (log.raw_output or {}).get("llm_skipped"), - "llm_skip_reason": (log.raw_output or {}).get("llm_skip_reason"), - "fallback": (log.raw_output or {}).get("fallback"), + "validation": raw_output.get("validation"), + "page_context": raw_output.get("page_context"), + "candidate_count": summary["candidate_count"], + "extraction_mode": raw_output.get("extraction_mode"), + "effective_extraction_mode": raw_output.get("effective_extraction_mode"), + "comparison": summary["comparison"], + "rule_entity_count": summary["rule_entity_count"], + "rule_claim_count": summary["rule_claim_count"], + "llm_entity_count": summary["llm_entity_count"], + "llm_claim_count": summary["llm_claim_count"], + "agreement_claim_count": summary["agreement_claim_count"], + "rule_only_claim_count": summary["rule_only_claim_count"], + "llm_only_claim_count": summary["llm_only_claim_count"], + "conflict_claim_count": summary["conflict_claim_count"], + "llm_skipped": raw_output.get("llm_skipped"), + "llm_skip_reason": raw_output.get("llm_skip_reason"), + "fallback": raw_output.get("fallback"), "raw_output": log.raw_output, - } - for log, page in rows - ] + }) + return payload @app.patch("/claims/{claim_id}/confidence") def update_claim_confidence(claim_id: int, request: UpdateClaimConfidenceRequest): diff --git a/ontology_platform/crawler_platform/app/core/crawler/page_classifier.py b/ontology_platform/crawler_platform/app/core/crawler/page_classifier.py index 024cb7b..efd759c 100644 --- a/ontology_platform/crawler_platform/app/core/crawler/page_classifier.py +++ b/ontology_platform/crawler_platform/app/core/crawler/page_classifier.py @@ -3,6 +3,14 @@ from __future__ import annotations from urllib.parse import urlparse from crawler_platform.app.core.crawler.content_zone import is_claim_allowed_zone +from crawler_platform.app.core.crawler.page_type_taxonomy import ( + PageClassificationResult, + build_classification_result_from_legacy, + get_legacy_page_type, + normalize_page_type, +) +from crawler_platform.app.core.crawler.page_signal_extractor import extract_page_signals_from_page +from crawler_platform.app.core.crawler.page_type_scorer import score_page_type PRODUCT_DETAIL_PREDICATES = { @@ -95,6 +103,38 @@ def classify_page( return "UnknownPage" +def classify_page_semantic( + url: str, + title: str | None = None, + text: str = "", + html: str | None = None, + source_zones: list[dict[str, object]] | None = None, +) -> PageClassificationResult: + signals = extract_page_signals_from_page( + url=url, + title=title, + text=text, + html=html, + source_zones=source_zones, + ) + result = score_page_type(url, signals) + if result.primary_page_type == "UnknownPage" and not result.alternatives: + legacy_page_type = classify_page( + url=url, + title=title, + text=text, + html=html, + source_zones=source_zones, + ) + return build_classification_result_from_legacy( + url=url, + legacy_page_type=legacy_page_type, + confidence=0.35, + source="legacy_classifier_fallback", + ) + return result + + def relation_allowed_for_page_type(page_type: str | None, predicate: str) -> bool: if page_type in PRODUCT_DETAIL_PAGE_TYPES: return True @@ -109,26 +149,9 @@ def claim_allowed_for_context(page_type: str | None, predicate: str, zone_type: return relation_allowed_for_page_type(page_type, predicate) and is_claim_allowed_zone(zone_type) -def normalize_page_type(value: str | None) -> str: - aliases = { - "product": "ProductPage", - "brand": "BrandStoryPage", - "review": "ReviewPage", - "listing": "CategoryPage", - "category": "CategoryPage", - "community": "BoardPage", - "board": "BoardPage", - "communitypage": "BoardPage", - "listingpage": "CategoryPage", - "promotionpage": "PromotionPage", - } - clean = str(value or "").strip() - return aliases.get(clean.lower(), aliases.get(clean, clean or "UnknownPage")) - - -def should_analyze_page(page_type: str, analyze_page_types: set[str]) -> bool: +def should_analyze_page(page_type: object, analyze_page_types: set[str] | None) -> bool: normalized_page_type = normalize_page_type(page_type) - normalized = {normalize_page_type(item) for item in analyze_page_types} + normalized = {normalize_page_type(item) for item in (analyze_page_types or set())} return normalized_page_type in normalized diff --git a/ontology_platform/crawler_platform/app/core/extractor/hybrid.py b/ontology_platform/crawler_platform/app/core/extractor/hybrid.py index f5cde9a..17e03e0 100644 --- a/ontology_platform/crawler_platform/app/core/extractor/hybrid.py +++ b/ontology_platform/crawler_platform/app/core/extractor/hybrid.py @@ -82,6 +82,13 @@ class HybridExtractor(Extractor): "effective_extraction_mode": "rule_only", "llm_skipped": True, "llm_skip_reason": "rule_only mode", + **count_payload( + rule_entity_count=len(rule_bundle.entities), + rule_claim_count=len(rule_bundle.claims), + llm_entity_count=0, + llm_claim_count=0, + comparison=comparison_payload(rule_only=len(rule_bundle.claims)), + ), } return rule_bundle @@ -103,8 +110,13 @@ class HybridExtractor(Extractor): **llm_bundle.raw_output, "extraction_mode": self.mode, "effective_extraction_mode": "llm_only", - "rule_entity_count": len(rule_bundle.entities), - "rule_claim_count": len(rule_bundle.claims), + **count_payload( + rule_entity_count=len(rule_bundle.entities), + rule_claim_count=len(rule_bundle.claims), + llm_entity_count=len(llm_bundle.entities), + llm_claim_count=len(llm_bundle.claims), + comparison=comparison_payload(llm_only=len(llm_bundle.claims)), + ), } return llm_bundle @@ -202,6 +214,13 @@ def fallback_bundle(rule_bundle: ExtractionBundle, extractor: HybridExtractor, e "ai_model": extractor.model, "ai_warning": reason, "fallback": "rule_based", + **count_payload( + rule_entity_count=len(bundle.entities), + rule_claim_count=len(bundle.claims), + llm_entity_count=0, + llm_claim_count=0, + comparison=comparison_payload(rule_only=len(bundle.claims)), + ), } for entity in bundle.entities: entity.metadata["ai_fallback_reason"] = reason @@ -265,6 +284,44 @@ def llm_skipped_bundle( return bundle +def comparison_payload( + *, + both_agree: int = 0, + rule_only: int = 0, + llm_only: int = 0, + conflict: int = 0, + rejected_by_validation: int = 0, +) -> dict[str, int]: + return { + "both_agree": both_agree, + "rule_only": rule_only, + "llm_only": llm_only, + "conflict": conflict, + "rejected_by_validation": rejected_by_validation, + } + + +def count_payload( + *, + rule_entity_count: int, + rule_claim_count: int, + llm_entity_count: int, + llm_claim_count: int, + comparison: dict[str, int], +) -> dict[str, Any]: + return { + "rule_entity_count": rule_entity_count, + "rule_claim_count": rule_claim_count, + "llm_entity_count": llm_entity_count, + "llm_claim_count": llm_claim_count, + "agreement_claim_count": comparison["both_agree"], + "rule_only_claim_count": comparison["rule_only"], + "llm_only_claim_count": comparison["llm_only"], + "conflict_claim_count": comparison["conflict"], + "comparison": comparison, + } + + def llm_skip_reason( context: ExtractionPageContext | None, page_text: str, diff --git a/ontology_platform/data/crawler_platform.db b/ontology_platform/data/crawler_platform.db index b546ca1..5e445ee 100644 Binary files a/ontology_platform/data/crawler_platform.db and b/ontology_platform/data/crawler_platform.db differ diff --git a/ontology_platform/docs/phases/26_05_22_PHASE_INDEX_engine_respect_backup.md b/ontology_platform/docs/phases/26_05_22_PHASE_INDEX_engine_respect_backup.md new file mode 100644 index 0000000..e51c766 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_PHASE_INDEX_engine_respect_backup.md @@ -0,0 +1,100 @@ +# PHASE INDEX - ontology_platform engine-respect roadmap + +?묒꽦?? 2026-05-19 + +踰붿쐞: `ontology_platform` ?꾩슜. `crawler_platform`?€ ?대쾲 ?묒뾽 踰붿쐞?먯꽌 ?쒖쇅?쒕떎. + +湲곗? 臾몄꽌: +- `ontology_platform/docs/?듯빀?ㅺ퀎??md` +- `ontology_platform/README.md` +- `ontology_platform/docs/phases/PHASE0_ACCEPTANCE_GATE.md` +- `ontology_platform/docs/phases/PHASE1_NEXT_STEPS.md` +- `ontology_platform/docs/phases/PHASE1_ACCEPTANCE_GATE.md` +- `ontology_platform/docs/phases/PHASE2_ACCEPTANCE_GATE.md` + +?듭떖 ?먯튃: +- OntoCast??Base ?붿쭊?쇰줈 議댁쨷?쒕떎. +- vendored OntoCast 肄붿뼱???듯빀?ㅺ퀎?쒓? ?덉슜??踰붿쐞 ?몄뿉???섏젙?섏? ?딅뒗?? +- Trafilatura, Crawl4AI, Guardrails, Neo4j GraphRAG??吏곸젒 ?ш뎄?꾪븯吏€ ?딄퀬 ?뉗? adapter/facade濡?媛먯떬?? +- Firecrawl, OpenDeepResearcher 肄붾뱶???ы븿?섏? ?딅뒗?? +- Acceptance Gate瑜??듦낵?섍린 ???ㅼ쓬 ?듯빀?쇰줈 ?섏뼱媛€吏€ ?딅뒗?? + +--- + +PHASE 0. ?붿쭊 寃쎄퀎 媛먯궗 諛?Phase Gate 蹂듦뎄 +FILE: ./26_05_19_engine_respect_plan/phase_00_001_engine_boundary_gate.md + +1) ?꾩옱 `ont_platform` 紐⑤뱢??Base/Adapter/Draft/Excluded 梨낆엫?쇰줈 遺꾨쪟 [?꾨즺] +2) Phase 0?먯꽌 誘몃옒 Phase ?섏〈?깆씠 import?섏뼱 ???쒖옉??源⑥? ?딅룄濡?寃뚯씠???뺣━ [?꾨즺] +3) Phase 0 unit/integration 寃€利??덉감 怨좎젙 [?꾨즺] +4) `PHASE0_ACCEPTANCE_GATE.md` 媛깆떊 湲곗? ?뺣━ [?꾨즺] + +--- + +PHASE 1. Trafilatura 湲곕컲 URL/HTML ?낅젰 ?뺣젹 +FILE: ./26_05_19_engine_respect_plan/phase_01_001_trafilatura_ingestion.md + +1) `web_extractor.py`瑜?Trafilatura adapter 梨낆엫?쇰줈 ?뺣━ [?꾨즺] +2) `SourceDocument`, `EvidenceSpan`, Content metadata ?€??寃쎄퀎 ?곌껐 [?꾨즺] +3) `/process/url` ?먮뒗 ?숇벑??URL ?낅젰 API ?ㅺ퀎 [?꾨즺] +4) ?쒓뎅??URL/HTML fixture 湲곕컲 異붿텧 ?뚯뒪?몄? dedup 湲곗? ?묒꽦 [?꾨즺] + +--- + +PHASE 2. Candidate Storage 諛?Review 梨낆엫 寃쎄퀎 +FILE: ./26_05_19_engine_respect_plan/phase_02_001_candidate_review_boundary.md + +1) `storage/models.py`???꾨낫 紐⑤뜽???뺤떇 Review Queue 怨꾩빟?쇰줈 ?뺤젙 [?꾨즺] +2) OntoCast 寃곌낵?€ lightweight extraction 寃곌낵???€??寃쎈줈 遺꾨━ [?꾨즺] +3) ?뱀씤/諛섎젮/?먮룞?뱀씤 ?곹깭 ?꾩씠 洹쒖튃 ?뺤쓽 [?꾨즺] +4) evidence ?녿뒗 ?꾨낫媛€ ?뺤젙 graph濡??ㅼ뼱媛€吏€ 紐삵븯寃?李⑤떒 [?꾨즺] + +--- + +PHASE 3. Crawl4AI ?섏쭛 怨꾩링 諛?Job Orchestration +FILE: ./26_05_19_engine_respect_plan/phase_03_001_crawl4ai_acquisition_jobs.md + +1) `crawl4ai_adapter.py`瑜??숈쟻/?€???섏쭛 adapter濡??쒗븳 [?꾨즺] +2) crawler profile, robots policy, cache policy瑜??ㅼ젙 湲곕컲?쇰줈 遺꾨━ [?꾨즺] +3) Job ?곹깭 紐⑤뜽怨?progress API/WebSocket 寃쎄퀎 ?뺣━ [?꾨즺] +4) Trafilatura ?꾩쿂由ъ? SourceDocument ?€?μ쑝濡??곌껐 [?꾨즺] + +--- + +PHASE 4. Guardrails Validation Gate +FILE: ./26_05_19_engine_respect_plan/phase_04_001_guardrails_validation_gate.md + +1) `core/validation`??Pydantic lightweight?€ Guardrails facade濡?遺꾨━ [?꾨즺] +2) OntoCast LLM 異쒕젰 ?섑븨 吏€?먯쓣 vendored ?섏젙 ?놁씠 ?곗꽑 ?ㅺ퀎 [?꾨즺] +3) schema violation, endpoint missing, confidence range ?뚯뒪???묒꽦 [?꾨즺] +4) Guard ?ㅽ뙣 寃곌낵瑜?candidate/review issue濡??€??[?꾨즺] + +--- + +PHASE 5. Neo4j Projection 諛?GraphRAG 寃€??FILE: ./26_05_19_engine_respect_plan/phase_05_001_neo4j_projection_graphrag.md + +1) RDF/Fuseki瑜?canonical store, Neo4j瑜?projection/search store濡?怨좎젙 [?꾨즺] +2) `core/graph` 湲곗〈 紐⑤뱢??projection/search adapter 梨낆엫?쇰줈 ?щ텇瑜?[?꾨즺] +3) read-only Text2Cypher?€ vector/hybrid retriever API ?ㅺ퀎 [?꾨즺] +4) provenance媛€ search result源뚯? ?댁뼱吏€??寃€利?湲곗? ?묒꽦 [?꾨즺] + +--- + +PHASE 6. Maintenance Loop 諛??댁쁺 湲곕뒫 ?뺣━ +FILE: ./26_05_19_engine_respect_plan/phase_06_001_maintenance_loop_operations.md + +1) Knowledge Agent??肄붾뱶媛€ ?꾨땲???꾨\?꾪듃/?뚰겕?뚮줈???⑦꽩留?李⑥슜 [?꾨즺] +2) Analyst/Researcher/Curator/Auditor/Fixer/Advisor 梨낆엫 ?뺤쓽 [?꾨즺] +3) `auth`, `audit`, `billing`, `realtime` 珥덉븞 紐⑤뱢???댁쁺 寃쎄퀎 ?뺣━ [?꾨즺] +4) destructive fix???щ엺 ?뱀씤 寃뚯씠?몃? 諛섎뱶???듦낵?섎룄濡??ㅺ퀎 [?꾨즺] + +--- + +PHASE 7. Hybrid Rule + LLM Extraction +FILE: ./26_05_19_engine_respect_plan/phase_07_001_hybrid_rule_llm_extraction.md + +1) rule baseline, LLM extraction, fallback, validation, Review UI 흐름을 기준선으로 고정 [신규] +2) `rule_only`, `llm_only`, `hybrid`, `compare` extraction mode 계약 정의 [신규] +3) product backend에 명시적 HybridExtractor와 rule/LLM agreement metadata 추가 [신규] +4) confidence breakdown에 rule agreement와 conflict/review 정책 반영 [신규] +5) Crawl/Research UI에서 mode/provider/model/base URL 선택 지원 [신규] diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_01_001_current_flow_boundary.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_01_001_current_flow_boundary.md new file mode 100644 index 0000000..d37afc2 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_01_001_current_flow_boundary.md @@ -0,0 +1,51 @@ +# PHASE 1. 현재 흐름 기준선 고정 및 영향 범위 정리 + +## 목표 + +`Semantic Page Understanding Layer`를 얹기 전에 현재 page classification 흐름과 page_type 문자열 의존 지점을 정확히 고정한다. + +## 기준 문서 + +- `README.md` +- `ontology_platform/README.md` +- `ontology_platform/docs/semantic_page_classification_codex_spec.md` + +## 작업 범위 + +1. `crawler_platform/app/core/crawler/page_classifier.py`의 현재 public API 확인 + - `classify_page(...)` + - `normalize_page_type(...)` + - `should_analyze_page(...)` + - `relation_allowed_for_page_type(...)` + - `claim_allowed_for_context(...)` + +2. crawler 호출부 확인 + - `site_crawler.py` + - `pipeline.py` + - `page_cleaner.py` + - `domain_discovery.py` + - `relevance_engine.py` + +3. extractor 연결 확인 + - `ExtractionPageContext.page_type` + - `HybridExtractor.llm_skip_reason(...)` + - `LLM_PAGE_TYPES`, `SKIP_LLM_PAGE_TYPES`, `RULE_ONLY_PAGE_TYPES` + - validation metadata의 `page_type` + +4. 기존 page_type 문자열 기대 코드 목록화 + - config `analyze_page_types` + - ontology relation rule `allowed_page_types` + - frontend display + - tests + - adapters + +## 수정 금지 + +- 이 phase에서는 구현 변경을 하지 않는다. +- 기존 page_type 문자열 의미를 변경하지 않는다. +- 기존 테스트 기대값을 바꾸지 않는다. + +## 완료 기준 + +- 현재 흐름과 의존 지점이 다음 phase 구현의 기준선으로 정리되어 있어야 한다. +- 신규 semantic API를 추가할 때 깨뜨리면 안 되는 legacy contract가 명확해야 한다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_01_current_flow_boundary_report.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_01_current_flow_boundary_report.md new file mode 100644 index 0000000..cc9cfee --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_01_current_flow_boundary_report.md @@ -0,0 +1,276 @@ +# Phase 1 Current Flow Boundary Report + +작성일: 2026-05-22 + +범위: Semantic Page Classification Layer 구현 전, 현재 `page_type` 문자열 흐름과 의존 지점을 고정한다. + +## 1. page_classifier.py public API + +파일: `ontology_platform/crawler_platform/app/core/crawler/page_classifier.py` + +현재 public API와 contract: + +- `classify_page(url, title=None, text="", html=None, source_zones=None) -> str` + - 단일 문자열 page_type을 반환한다. + - 호출부는 반환값이 `PageClassificationResult`가 아니라 `str`이라고 가정한다. +- `normalize_page_type(value: str | None) -> str` + - 짧은 alias를 legacy page_type 문자열로 변환한다. + - 현재 alias 예: `product -> ProductPage`, `listing/category -> CategoryPage`, `community/board -> BoardPage`. +- `should_analyze_page(page_type: str, analyze_page_types: set[str]) -> bool` + - page_type과 allowlist를 각각 normalize한 뒤 포함 여부만 본다. +- `relation_allowed_for_page_type(page_type, predicate) -> bool` + - `ProductPage`는 product detail predicate를 모두 허용한다. + - `UnknownPage`, `SearchPage`, `CategoryPage`, `BoardPage`는 non-merge page로 간주한다. + - `BrandStoryPage`, `NoticePage`, `EventPage`, `PromotionPage`는 content page로 간주한다. +- `claim_allowed_for_context(page_type, predicate, zone_type) -> bool` + - page_type relation policy와 source zone policy를 함께 적용한다. + +현재 legacy page_type 문자열: + +- `ProductPage` +- `CategoryPage` +- `SearchPage` +- `BoardPage` +- `NoticePage` +- `BrandStoryPage` +- `PromotionPage` +- `EventPage` +- `ReviewPage` +- `UnknownPage` +- 보조/외부 상태 문자열: `unknown`, `external`, `entity` + +Phase 2 이후에도 기존 `classify_page(...) -> str` contract는 유지해야 한다. 신규 semantic result는 별도 API로 추가하거나 compatibility wrapper 뒤에 두어야 한다. + +## 2. crawler 호출 흐름 + +### site_crawler.py + +파일: `ontology_platform/crawler_platform/app/core/crawler/site_crawler.py` + +흐름: + +1. fetcher가 HTML을 가져온다. +2. parser가 `analysis_html`을 parse한다. +3. `classify_page(final_url or url, title, raw_text or text, analysis_html, source_zones)`를 호출한다. +4. 반환된 `page_type` 문자열을 page metadata와 `SiteCrawlPageResult`에 저장한다. +5. `should_analyze_page(page_type, analyze_page_types)`가 true일 때만 `ExtractionPageContext`를 만들고 extractor를 실행한다. +6. context의 `page_type` 문자열이 HybridExtractor, validation, repository 저장까지 이어진다. + +현재 기본 analyze allowlist: + +```txt +ProductPage +BrandStoryPage +ReviewPage +``` + +따라서 현재 기본 흐름에서는 `CategoryPage`, `SearchPage`, `BoardPage`, `UnknownPage`가 extractor 실행에서 빠진다. + +### pipeline.py + +파일: `ontology_platform/crawler_platform/app/core/crawler/pipeline.py` + +흐름: + +1. 단일 URL fetch/parse 후 `classify_page(...)`를 호출한다. +2. `metadata["page_type"]`에 문자열을 저장한다. +3. fetch/extraction 실패가 아니면 `ExtractionPageContext.page_type`에 같은 문자열을 넣고 extractor를 실행한다. + +주의: + +- `pipeline.py`에는 `should_analyze_page()` gate가 없다. +- 단일 URL pipeline은 현재 모든 정상 parse page를 extractor로 보낸다. + +### page_cleaner.py / html_cleaner.py / plugins.py + +파일: + +- `ontology_platform/crawler_platform/app/core/crawler/page_cleaner.py` +- `ontology_platform/crawler_platform/app/core/crawler/html_cleaner.py` +- `ontology_platform/crawler_platform/app/core/crawler/plugins.py` + +흐름: + +- `PageCleaner.clean()`은 page_type이 없으면 내부에서 `classify_page()`를 호출한다. +- `ZONE_PRIORITY_BY_PAGE_TYPE`가 legacy page_type 문자열에 의존한다. +- `ProductPage`, `BrandStoryPage`, `NoticePage`, `BoardPage`, `EventPage`, `PromotionPage`, `CategoryPage`별로 source zone 우선순위가 다르다. + +Phase 6에서 semantic type을 추가할 때 `ProductDetailPage -> ProductPage`, `CategoryListingPage -> CategoryPage` 같은 zone compatibility가 필요하다. + +## 3. research/discovery 호출 흐름 + +### graph_research_loop.py + +파일: `ontology_platform/crawler_platform/app/core/research/graph_research_loop.py` + +흐름: + +1. 기본 analyze allowlist는 `ProductPage`, `BrandStoryPage`, `ReviewPage`다. +2. explored page에서 `classify_page(...)`를 호출한다. +3. metadata에 `page_type`을 저장한다. +4. relevance score와 `should_analyze_page(page_type, analyze_page_types)`를 모두 통과해야 extractor를 실행한다. +5. link 후보 metadata에도 relevance engine이 산출한 `score.page_type`을 저장한다. + +### relevance_engine.py + +파일: `ontology_platform/crawler_platform/app/core/research/relevance_engine.py` + +흐름: + +- `score_url()` 내부에서 `classify_page(...)`를 호출한다. +- `HIGH_VALUE_PAGE_TYPES` 점수표가 legacy page_type 문자열에 의존한다. +- 신규 semantic page type이 들어오면 점수표 또는 legacy normalization이 필요하다. + +현재 주요 점수: + +- `ProductPage`: 0.95 +- `BrandStoryPage`: 0.88 +- `ReviewPage`: 0.82 +- `CategoryPage`: 0.34 +- `SearchPage`: 0.22 +- `BoardPage`: 0.18 +- `UnknownPage`: 0.25 + +### domain_discovery.py + +파일: `ontology_platform/crawler_platform/app/core/ontology/domain_discovery.py` + +흐름: + +- discovery job에서 `classify_page(...)`를 호출한다. +- `mine_schema_candidates(..., page_type=page_type)`로 page_type을 evidence metadata에 넣는다. +- `entity_type_from_page_type()`가 legacy mapping을 사용한다. + +현재 mapping: + +- `ProductPage -> Product` +- `CategoryPage -> Category` +- `BrandStoryPage -> Brand` +- `NoticePage -> Notice` +- `PromotionPage -> Promotion` +- `ReviewPage -> Review` +- `BoardPage -> Article` + +## 4. Extractor / HybridExtractor 연결 흐름 + +### ExtractionPageContext + +파일: `ontology_platform/crawler_platform/app/core/extractor/base.py` + +현재 `ExtractionPageContext.page_type: str`는 필수 문자열 필드다. `to_payload()`도 `page_type`을 그대로 내보낸다. + +Phase 6에서 이 필드는 유지해야 하며, semantic 정보는 `metadata`에 추가하는 방식이 가장 안전하다. + +### HybridExtractor + +파일: `ontology_platform/crawler_platform/app/core/extractor/hybrid.py` + +현재 LLM routing 상수: + +- `LLM_PAGE_TYPES = {"ProductPage", "BrandStoryPage", "ReviewPage"}` +- `SKIP_LLM_PAGE_TYPES = {"CategoryPage", "SearchPage", "ListingPage"}` +- `RULE_ONLY_PAGE_TYPES = {"BoardPage", "CommunityPage", "UnknownPage"}` + +현재 `llm_skip_reason()` 판단 순서: + +1. mode가 `hybrid`가 아니거나 context가 없으면 skip 판단 없음. +2. page_type이 `SKIP_LLM_PAGE_TYPES`면 LLM skip. +3. page_type이 `RULE_ONLY_PAGE_TYPES`면 rule-only. +4. page_type이 `LLM_PAGE_TYPES`에 없으면 LLM allowlist 밖으로 skip. +5. clean text 길이가 너무 짧거나 길면 skip. + +Phase 5/6에서 `LLMPolicy`가 있으면 이를 우선하고, 없으면 이 legacy fallback을 유지해야 한다. + +### ai_provider.py + +파일: `ontology_platform/crawler_platform/app/core/extractor/ai_provider.py` + +LLM prompt에 page_type semantics가 직접 들어간다. + +현재 prompt contract: + +- `ProductPage`는 product detail claim 가능 +- `CategoryPage`는 detailed product claim 제한 +- `BrandStoryPage`는 product price/note claim 제한 +- `UnknownPage`는 ontology claim을 반환하지 않도록 지시 + +신규 semantic type을 추가할 때 prompt가 `ProductDetailPage`, `CategoryListingPage`를 이해하도록 하거나, prompt에는 legacy page_type을 계속 넘기는 compatibility가 필요하다. + +### validation.py + +파일: `ontology_platform/crawler_platform/app/core/extractor/validation.py` + +흐름: + +- `attach_page_context()`가 entity/claim metadata에 `page_type`을 저장한다. +- `claim_status_for_bundle()`은 `UnknownPage`, `SearchPage`, `CategoryPage`, `BoardPage`를 candidate claim으로 낮춘다. +- `invalid_claim_reason()`은 relation schema compatibility에 `page_type`을 넘긴다. + +신규 semantic type은 validation status와 relation schema의 allowed page type 검증에 영향을 준다. + +## 5. 기존 page_type 문자열 기대 지점 + +### API request/response + +파일: `ontology_platform/crawler_platform/app/api/routes.py` + +- 여러 request model의 기본 `analyze_page_types`가 `["ProductPage", "BrandStoryPage", "ReviewPage"]`다. +- relation/quality 관련 request에 `allowed_page_types`가 있다. +- API response 및 progress payload에서 `page_type` 문자열을 그대로 노출한다. + +### Config + +파일: `ontology_platform/configs/perfume_subscription.yaml` + +- `analyze_page_types` 또는 ontology rule에서 legacy page type이 쓰일 수 있다. +- target entity/ontology에는 `ProductPage`, `BrandStoryPage`, `ListingPage`, `PromotionPage`, `ReviewPage` 같은 page entity type이 포함되어 있다. + +### Adapters + +파일: `ontology_platform/crawler_platform/app/adapters/ecommerce/perfume.py` + +- relation rules가 `page_types={"ProductPage"}` 또는 `{"ProductPage", "ReviewPage"}` 같은 legacy set에 의존한다. + +### Ontology relation schema + +파일: `ontology_platform/crawler_platform/app/core/ontology/relation_schema.py` + +- configured relation rule의 `allowed_page_types`/`page_types`를 set으로 읽는다. +- `relation_schema_compatible()`은 `page_type not in rule.page_types`면 reject한다. +- semantic type 도입 시 legacy alias expansion 없이는 기존 relation rules가 거부될 수 있다. + +### Frontend + +주요 파일: + +- `ontology_platform/web/frontend/src/pages/CrawlPage.tsx` +- `ontology_platform/web/frontend/src/pages/ReviewPage.tsx` +- `ontology_platform/web/frontend/src/pages/BuildPipelinePage.tsx` +- `ontology_platform/web/frontend/src/pages/QualityInspectorPage.tsx` +- `ontology_platform/web/frontend/src/lib/api/crawl.ts` +- `ontology_platform/web/frontend/src/lib/api/claims.ts` +- `ontology_platform/web/frontend/src/lib/api/platform.ts` +- legacy JS files under `web/frontend/src/legacy` + +현재 frontend는 `page_type`을 optional string으로 표시하거나, allowed page types mismatch 검사에 사용한다. + +### Tests + +주요 테스트: + +- `ontology_platform/tests/unit/test_phase7_hybrid_extraction.py` + - `CategoryPage`가 LLM skip 되는 기존 behavior를 검증한다. +- integration/e2e tests는 API response의 `page_type` 문자열 contract에 간접 의존한다. + +Phase 8에서 semantic classifier 전용 테스트를 추가하되, 기존 문자열 contract 회귀 테스트도 유지해야 한다. + +## 6. Phase 2 이후 유지해야 할 legacy contract + +1. `classify_page(...)`는 계속 `str`을 반환해야 한다. +2. 신규 API는 `classify_page_semantic(...) -> PageClassificationResult`처럼 분리하는 편이 안전하다. +3. `normalize_page_type()`은 `PageClassificationResult`도 받을 수 있게 확장하되, 기존 string 입력 결과를 바꾸면 안 된다. +4. `should_analyze_page(str, set)` 기존 호출은 계속 동작해야 한다. +5. API/metadata의 `page_type` 필드는 legacy string으로 유지하고, semantic 결과는 별도 `page_classification` payload로 저장한다. +6. `ExtractionPageContext.page_type`은 legacy string으로 유지하고, `analyze_strategy`/`llm_policy`/evidence는 `metadata`로 전달한다. +7. relation schema, adapter page_types, frontend allowed page type 검증에는 legacy/semantic alias compatibility가 필요하다. +8. `CategoryPage`, `SearchPage`, `BoardPage`, `UnknownPage`의 기존 skip/rule-only 동작은 Phase 5에서 strategy 기반으로 확장하되, LLM 호출이 늘지 않도록 `LLMPolicy`를 우선한다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_02_001_taxonomy_result_model.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_02_001_taxonomy_result_model.md new file mode 100644 index 0000000..959450a --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_02_001_taxonomy_result_model.md @@ -0,0 +1,53 @@ +# PHASE 2. Taxonomy와 Classification Result 모델 추가 + +## 목표 + +기존 단일 문자열 page_type을 대체하지 않고, 그 위에 semantic classification result 모델을 추가한다. + +## 작업 범위 + +1. 신규 taxonomy 모듈 추가 + - 권장 위치: `crawler_platform/app/core/crawler/page_type_taxonomy.py` + - 기존 프로젝트 구조와 import 경계를 우선한다. + +2. 다음 상수 또는 enum 정의 + - `PageDomain` + - `PageArchetype` + - `PageType` + - `EntityType` + - `ActionIntent` + - `GraphRole` + - `AnalyzeStrategy` + - `LLMPolicy` + +3. classification result 모델 추가 + - `EvidenceItem` + - `PageClassificationResult` + +4. legacy compatibility helper 추가 + - `normalize_page_type(result_or_page_type)` + - `get_legacy_page_type(result_or_page_type)` + - `classify_page_semantic(...)` + - 기존 `classify_page(...) -> str` 유지 + +5. 기존 page type alias 유지 + - `ProductPage` + - `CategoryPage` + - `SearchPage` + - `BoardPage` + - `NoticePage` + - `BrandStoryPage` + - `PromotionPage` + - `UnknownPage` + - `ReviewPage` + +## 수정 금지 + +- 기존 `classify_page(...)` 호출부를 한 번에 모두 semantic result 기반으로 바꾸지 않는다. +- legacy 문자열을 제거하지 않는다. + +## 완료 기준 + +- 기존 호출부가 string page_type을 그대로 받을 수 있어야 한다. +- semantic result API를 신규 테스트에서 직접 호출할 수 있어야 한다. +- legacy alias mapping이 테스트로 보호되어야 한다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_03_001_signal_extraction_layer.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_03_001_signal_extraction_layer.md new file mode 100644 index 0000000..23b9c79 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_03_001_signal_extraction_layer.md @@ -0,0 +1,93 @@ +# PHASE 3. Raw Snapshot 및 Signal Extraction 레이어 추가 + +## 목표 + +URL substring 중심 분류를 줄이고, HTML/DOM/metadata/link/form/text 기반 signal을 별도 레이어에서 추출한다. + +## 작업 범위 + +1. `RawPageSnapshot` 모델 추가 + + * `url`, `final_url`, `status_code`, `content_type` + * `title`, `text`, `html`, `rendered_html` + * metadata, structured data, headings, links, images, forms, buttons, inputs, tables + * source_zones, screenshot_path + +2. `PageSignals` 모델 추가 + + * structured data signals + * commerce signals + * listing signals + * editorial signals + * community signals + * knowledge/docs signals + * corporate/legal signals + * transaction/protected signals + * graph/link signals + * text/layout keyword signals + * visual/layout block signals + * external collector signals + +3. signal extractor 추가 + + * 권장 위치: `crawler_platform/app/core/crawler/page_signal_extractor.py` + * BeautifulSoup 사용 가능 시 DOM parsing + * BeautifulSoup 미설치/HTML 깨짐 시 regex/text fallback + +4. 다국어 확장 고려 + + * 한국어/영어 키워드 dictionary를 분리 가능한 구조로 둔다. + * 인코딩 깨짐이 있어도 예외 없이 동작한다. + +5. visual/layout signal 세부화 고려 + + * screenshot 기반 정밀 분석은 이번 phase의 필수 구현 범위가 아니지만, 향후 visual block classification을 붙일 수 있도록 signal 구조를 열어둔다. + * DOM class/id/role/aria/heading 구조와 반복 레이아웃을 통해 가능한 범위에서 visual/layout block 후보를 추출한다. + * visual/layout block signal 예시는 다음과 같다. + + * hero block + * product card grid + * article body block + * left filter sidebar + * top navigation + * footer navigation + * sticky buy box + * review/comment block + * FAQ accordion + * media player area + * map area + * calendar/availability grid + * dashboard card grid + * form wizard / stepper + * pricing table + * comparison table + * 초기 구현은 실제 computer vision까지 요구하지 않는다. + * 다만 `PageSignals`에는 visual/layout 후보를 담을 수 있는 필드를 둔다. + * 예시 필드: + + * `layout_blocks: list[str]` + * `has_hero_block: bool` + * `has_card_grid: bool` + * `has_filter_sidebar: bool` + * `has_sticky_action_box: bool` + * `has_media_player_area: bool` + * `has_map_area: bool` + * `has_calendar_grid: bool` + * `has_pricing_table: bool` + * `has_comparison_table: bool` + * visual/layout signal은 page type을 단독 확정하지 않고, evidence scoring의 보조 신호로 사용한다. + +## 수정 금지 + +* signal extractor에서 page_type을 확정 반환하지 않는다. +* 이 phase에서 extractor나 crawler의 분석 여부 정책을 바꾸지 않는다. +* Crawl4AI, Firecrawl, Trafilatura 같은 외부 도구에 핵심 classifier가 강하게 종속되도록 만들지 않는다. +* screenshot 또는 visual analysis가 없다는 이유로 기본 signal extraction이 실패하면 안 된다. + +## 완료 기준 + +* HTML이 비어도 `PageSignals`가 생성되어야 한다. +* JSON-LD/OpenGraph/form/input/button/link/text signal이 evidence scorer에서 사용할 수 있는 형태로 정리되어야 한다. +* 외부 수집 도구 결과를 `RawPageSnapshot`에 매핑할 수 있는 구조 또는 adapter hook이 있어야 한다. +* visual/layout block 후보를 담을 수 있는 `PageSignals` 필드가 있어야 한다. +* visual/layout signal이 없어도 기존 signal extraction과 scoring 흐름은 정상 동작해야 한다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_04_001_evidence_scoring_classifier.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_04_001_evidence_scoring_classifier.md new file mode 100644 index 0000000..98339b4 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_04_001_evidence_scoring_classifier.md @@ -0,0 +1,99 @@ +# PHASE 4. Evidence Scoring 기반 Semantic Classification 구현 + +## 목표 + +단일 if-return 방식이 아니라 signal별 evidence weight를 합산해 semantic page type을 결정한다. + +## 작업 범위 + +1. `page_type_scorer.py` 추가 + + * score accumulator + * evidence item 생성 helper + * normalize 및 confidence 계산 + * alternatives 산출 + +2. 최소 구현 page type + + * `ProductDetailPage` + * `CategoryListingPage` + * `SearchResultsPage` + * `ArticlePage` + * `BlogPostPage` + * `QAPage` + * `FAQPage` + * `ForumBoardPage` + * `ForumThreadPage` + * `BrandStoryPage` + * `AboutPage` + * `ContactPage` + * `DocumentationPage` + * `APIReferencePage` + * `JobPostingPage` + * `PricingPage` + * `LoginPage` + * `CheckoutPage` + * `PaymentPage` + * `TermsPage` + * `PrivacyPolicyPage` + * `SitemapPage` + * `RSSFeedPage` + * `ErrorPage` + * `AccessDeniedPage` + * `CaptchaPage` + * `UnknownPage` + +3. 범용 taxonomy 확장 기준 + + * 위의 최소 구현 page type은 1차 구현 범위로 본다. + * 장기 목표는 인터넷에 존재하는 다양한 페이지를 포괄할 수 있는 전체 taxonomy catalog를 유지하는 것이다. + * 따라서 `page_type_scorer.py`와 taxonomy 정의는 아래 계열을 나중에 확장할 수 있는 구조로 작성한다. + + * Site / Navigation + * Commerce / Marketplace + * Editorial / Article + * Community / UGC + * Knowledge / Documentation + * Corporate / Organization + * Local / Place / Travel / Real Estate + * Education / Learning + * Jobs / Career + * Media / Entertainment + * Software / SaaS / App + * Finance / Legal / Government + * Healthcare / Medical + * Transaction / Account / Protected + * System / Technical / Machine-readable + * 현재 phase에서는 위 전체 계열을 모두 scoring 구현하지 않아도 된다. + * 다만 enum, alias, mapping, scorer 구조는 특정 몇 개 타입에 고정하지 말고, 전체 taxonomy catalog가 추가되어도 깨지지 않도록 확장 가능해야 한다. + * Phase 4의 최소 구현 page type은 1차 안정화 대상이며, 전체 taxonomy catalog는 별도 taxonomy 문서 또는 후속 phase에서 보강한다. + * `UnknownPage`는 전체 taxonomy에 아직 포함되지 않은 신규 페이지 패턴을 발견하기 위한 후보로 유지한다. + +4. result enrichment + + * `domain` + * `archetype` + * `main_entity_type` + * `action_intents` + * `graph_roles` + * `confidence` + * `alternatives` + * `evidence` + +5. low confidence 처리 + + * threshold 아래는 `UnknownPage` + * alternatives/evidence는 유지 + +## 수정 금지 + +* URL 문자열 조건만 추가해서 바로 return하지 않는다. +* Product/Category/Search/Board만 처리하는 구조로 고정하지 않는다. +* 1차 최소 구현 page type만을 전체 taxonomy의 전부로 간주하지 않는다. + +## 완료 기준 + +* classification result가 항상 evidence를 포함해야 한다. +* 보호 페이지가 commerce/detail page로 오분류되지 않아야 한다. +* category/search/board 계열은 skip 여부가 아니라 semantic type과 graph role이 남아야 한다. +* 최소 구현 page type은 동작해야 하며, 전체 taxonomy catalog를 후속 확장할 수 있는 구조여야 한다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_05_001_analysis_llm_policy.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_05_001_analysis_llm_policy.md new file mode 100644 index 0000000..3951896 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_05_001_analysis_llm_policy.md @@ -0,0 +1,45 @@ +# PHASE 5. Analyze Strategy 및 LLM Policy 분리 + +## 목표 + +분석 여부를 단순 boolean allowlist에서 page type별 strategy와 LLM policy로 분리한다. + +## 작업 범위 + +1. `page_analysis_policy.py` 추가 + - `decide_analyze_strategy(result)` + - `decide_llm_policy(result)` + - `is_protected_strategy(strategy)` + - `is_noise_strategy(strategy)` + +2. strategy mapping + - `ProductDetailPage`, `ArticlePage`, `FAQPage`, `QAPage`, `BrandStoryPage` -> `AnalyzeFull` + - `CategoryListingPage`, `ForumBoardPage` -> `AnalyzeRelationsOnly` + - `SearchResultsPage`, `SitemapPage` -> `AnalyzeDiscoveryOnly` + - `TermsPage`, `PrivacyPolicyPage` -> `AnalyzeDocumentOnly` + - `LoginPage`, `CheckoutPage`, `PaymentPage`, `CaptchaPage`, `AccessDeniedPage` -> `SkipProtected` + - `ErrorPage`, `NotFoundPage` -> `SkipNoise` + - `UnknownPage` -> `AnalyzeMetadataOnly` + +3. LLM policy mapping + - full content page -> `LLMFull` 또는 `LLMLight` + - listing/search/board -> `LLMForAmbiguityOnly` 또는 `RuleOnly` + - sitemap/rss/robots/protected -> `RuleOnly`, `NoLLM`, 또는 `Skip` + +4. compatibility + - `should_analyze_page(result_or_page_type, analyze_page_types=None)` + - 기존 allowlist가 들어오면 legacy behavior를 최대한 유지하되 protected/noise는 안전하게 skip + - semantic result가 들어오면 strategy 우선 + +## 수정 금지 + +- CategoryPage/SearchPage/BoardPage를 무조건 skip하지 않는다. +- protected page에서 extractor/LLM이 개인정보를 추출하도록 두지 않는다. + +## 완료 기준 + +- `CategoryListingPage.should_analyze == True` +- `SearchResultsPage.should_analyze == True` +- `ForumBoardPage.should_analyze == True` +- `LoginPage`, `CheckoutPage`, `PaymentPage`는 `should_analyze == False` +- LLM 호출이 기존보다 불필요하게 증가하지 않도록 policy 테스트가 있어야 한다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_06_001_pipeline_integration.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_06_001_pipeline_integration.md new file mode 100644 index 0000000..8d8f763 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_06_001_pipeline_integration.md @@ -0,0 +1,45 @@ +# PHASE 6. Crawler, Cleaner, Extractor, Discovery/Relevance 통합 + +## 목표 + +Semantic classification result를 crawler와 extractor 흐름에 연결하되, 기존 page_type 문자열 기반 contract는 유지한다. + +## 작업 범위 + +1. `site_crawler.py` + - `classify_page_semantic(...)` 호출 + - 기존 `page_type` 필드는 legacy string으로 유지 + - metadata에 `page_classification` 저장 + - `should_analyze_page(result, analyze_page_types)` 사용 + +2. `pipeline.py` + - 단일 URL 처리에서도 semantic payload 저장 + - `ExtractionPageContext.metadata`에 strategy/policy 전달 + +3. `ExtractionPageContext` + - 기존 `page_type: str`는 유지 + - metadata 기반 `analyze_strategy`, `llm_policy`, evidence payload 전달 + +4. `HybridExtractor` + - metadata의 `llm_policy`를 우선 사용 + - 없으면 기존 `LLM_PAGE_TYPES`, `SKIP_LLM_PAGE_TYPES`, `RULE_ONLY_PAGE_TYPES` fallback + - protected/skip policy는 LLM 호출 금지 + +5. compatibility update + - `page_cleaner.py` zone priority에 semantic aliases 추가 + - `domain_discovery.py` page_type entity mapping에 semantic aliases 추가 + - `relevance_engine.py` high value score mapping에 semantic aliases 추가 + - relation schema allowed page type 검증에서 legacy/semantic alias 고려 + +## 수정 금지 + +- extractor 전체를 새 구조로 갈아엎지 않는다. +- DB schema 변경을 필수로 만들지 않는다. +- frontend 표시용 기존 `page_type` 필드를 제거하지 않는다. + +## 완료 기준 + +- 기존 API response의 `page_type`은 문자열로 유지된다. +- semantic classification payload가 metadata에 남는다. +- HybridExtractor가 LLMPolicy에 따라 LLM 호출을 줄일 수 있다. +- 기존 page_type 문자열 기반 relation rule이 신규 semantic type 때문에 깨지지 않는다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_07_001_unknown_pattern_storage.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_07_001_unknown_pattern_storage.md new file mode 100644 index 0000000..94e0339 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_07_001_unknown_pattern_storage.md @@ -0,0 +1,47 @@ +# PHASE 7. Unknown Pattern 저장 기반 추가 + +## 목표 + +`UnknownPage`를 분석 실패나 폐기 대상으로 두지 않고, taxonomy 확장 후보로 저장 가능한 evidence payload를 만든다. + +## 작업 범위 + +1. unknown payload 정의 + - url + - title + - text sample + - html fingerprint + - dom fingerprint + - schema types + - link pattern summary + - button labels + - forms summary + - top keywords + - alternatives + - evidence + +2. fingerprint hook + - text fingerprint + - html/dom fingerprint + - link pattern fingerprint + +3. 저장 위치 + - 초기 구현은 `metadata_json["page_classification"]["unknown_pattern"]` + - DB migration 없이 시작 + +4. 향후 확장 hook + - embedding input 생성 함수 + - cluster candidate payload 생성 함수 + - 실제 clustering은 이번 phase 범위에서 제외 + +## 수정 금지 + +- UnknownPage를 무조건 extractor 대상에서 제외하지 않는다. +- low confidence 결과의 evidence와 alternatives를 버리지 않는다. +- 이 phase에서 clustering 알고리즘을 새로 도입하지 않는다. + +## 완료 기준 + +- UnknownPage result가 evidence를 가진다. +- metadata에 unknown pattern summary가 저장 가능하다. +- 향후 clustering/관리자 검토 UI로 넘길 수 있는 구조다. diff --git a/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_08_001_tests_regression.md b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_08_001_tests_regression.md new file mode 100644 index 0000000..7a669e5 --- /dev/null +++ b/ontology_platform/docs/phases/26_05_22_semantic_page_classification/phase_08_001_tests_regression.md @@ -0,0 +1,59 @@ +# PHASE 8. 테스트 Fixture 및 회귀 검증 + +## 목표 + +Semantic Page Understanding Layer가 기존 흐름을 깨지 않고, page type별 strategy와 LLM policy를 정확히 산출하는지 검증한다. + +## 작업 범위 + +1. fixture 추가 + - `tests/fixtures/pages/product_detail.html` + - `tests/fixtures/pages/category_listing.html` + - `tests/fixtures/pages/search_results.html` + - `tests/fixtures/pages/article.html` + - `tests/fixtures/pages/qapage.html` + - `tests/fixtures/pages/faq.html` + - `tests/fixtures/pages/forum_thread.html` + - `tests/fixtures/pages/documentation.html` + - `tests/fixtures/pages/job_posting.html` + - `tests/fixtures/pages/login.html` + - `tests/fixtures/pages/checkout.html` + - `tests/fixtures/pages/terms.html` + - `tests/fixtures/pages/sitemap.xml` + - `tests/fixtures/pages/unknown.html` + +2. unit test 추가 + - primary_page_type 확인 + - confidence 최소 기준 확인 + - evidence non-empty 확인 + - analyze_strategy 확인 + - llm_policy 확인 + - protected page skip 확인 + - UnknownPage 예외 없는 처리 확인 + +3. compatibility test 추가 + - 기존 `classify_page(...) -> str` + - 신규 `classify_page_semantic(...) -> PageClassificationResult` + - `should_analyze_page(str, set)` + - `should_analyze_page(result, set)` + - legacy aliases + +4. extractor policy 회귀 테스트 + - HybridExtractor가 metadata `llm_policy`를 우선 사용 + - `Skip`, `NoLLM`, `RuleOnly`에서 LLM 호출 금지 + - 기존 legacy page_type fallback 유지 + +5. 회귀 테스트 실행 + - 기본 명령: `pytest` + - 필요 시 변경 범위 우선: `pytest tests/unit` + +## 수정 금지 + +- 기존 테스트 기대값을 불필요하게 변경하지 않는다. +- LLM live 호출이 필요한 테스트를 기본 회귀 테스트에 포함하지 않는다. + +## 완료 기준 + +- 새 semantic classifier 테스트가 통과한다. +- 기존 unit/integration 테스트가 통과한다. +- 테스트 결과와 미실행 사유가 작업 완료 보고에 명확히 기록된다. diff --git a/ontology_platform/docs/phases/PHASE0_ACCEPTANCE_GATE.md b/ontology_platform/docs/phases/PHASE0_ACCEPTANCE_GATE.md deleted file mode 100644 index f909b13..0000000 --- a/ontology_platform/docs/phases/PHASE0_ACCEPTANCE_GATE.md +++ /dev/null @@ -1,132 +0,0 @@ -# Phase 0 — Acceptance Gate 결과 - -본 문서는 통합설계서 §5 Phase 0의 Acceptance Gate를 객관적으로 점검한 결과다. Phase 1 진입 전에 모든 체크가 통과되어야 한다. - -## 결과 요약 - -| # | Acceptance Gate 항목 | 상태 | 검증 방법 | -|---|---|---|---| -| 1 | 단일 PDF/JSON 입력 → ontology TTL + facts TTL이 filesystem에 생성됨 | ⚠️ **e2e 검증 대기** (로컬 LLM/API 키 필요) | `tests/e2e/test_phase0_full_pipeline.py` | -| 2 | `/health`, `/info`, `/process` (FastAPI) 정상 동작 | ✅ **통합 테스트 11/11 통과** (2026-05-19) | `tests/integration/test_api_smoke.py` | -| 3 | BudgetTracker가 LLM call/triple count를 정확히 기록 | ⚠️ **e2e 검증 대기** (mock 검증은 통합 테스트로 통과) | e2e 테스트가 실제 검증 | -| 4 | LangGraph 워크플로우 (CONVERT→CHUNK→...→SERIALIZE) 전 노드 traceable | ✅ **OntoCast 원본 워크플로우 무수정 채택** | `vendored/ontocast/ontocast/stategraph/` 그대로 사용 | - -추가로 **단위 테스트 16/16 통과** (test_convert_document 7, test_platform_config 5, test_select_ontology 4). -자동 검증 기준으로는 **unit + integration 27/27 통과**가 현재 Phase 0 기본선이다. - -**현재 진척 (2026-05-19)**: -- Python 3.14.5 `.venv` 환경에서 unit + integration 27/27 통과 -- `python-multipart`를 Phase 0 FastAPI multipart upload 필수 의존성으로 추가 -- Phase 0 production app에서 Phase 1 Trafilatura route가 기본 mount되지 않도록 lazy phase route gate 적용 -- `pip install -e ".[dev]"` 또는 동등한 의존성 설치 필요 -- `pip install -e vendored/ontocast` 로 OntoCast 의존성 설치 완료 -- 패키지 이름 충돌 수정: `platform/` → `ont_platform/` (Python 내장 `platform` 모듈과 충돌) -- **남은 작업**: e2e 테스트 (Acceptance Gate #1, #3) 실행 — 로컬 Ollama 또는 OpenAI 키 필요 - -## 다음 작업자가 실행할 검증 절차 - -### 1) 환경 준비 - -```powershell -# Python 3.12+ 설치 (예: https://www.python.org/downloads/) -python --version # Python 3.12.x 이상 확인 - -cd C:\Users\lasta\MyProject\AI\ontology_platform - -# 가상환경 + 의존성 설치 -python -m venv .venv -.venv\Scripts\activate -pip install --upgrade pip -pip install -e ".[dev]" - -# .env 생성 (실제 LLM 키 채우기) -Copy-Item .env.example .env -# 그 다음 .env 파일을 편집하여 LLM_API_KEY 등 채움 -``` - -### 2) 자동 검증 (Acceptance Gate #2) - -```powershell -# 단위 + 통합 테스트만 (LLM 호출 없음, 빠름) -.venv\Scripts\python.exe -m pytest tests/unit tests/integration -v -``` - -**기대 결과**: 모든 케이스 PASS. - -- `tests/unit/test_select_ontology.py` (4 케이스) — Phase 0.2 검증 -- `tests/unit/test_convert_document.py` (7 케이스) — Phase 0.3 검증 -- `tests/unit/test_platform_config.py` (5 케이스) — Phase 0.5 검증 -- `tests/integration/test_api_smoke.py` (11 케이스) — Phase 0.4 + 0.6 mock 검증, Phase 0 future dependency route gate 검증 - -Windows에서 `%TEMP%` 권한 문제 또는 `.pytest_cache` 쓰기 문제가 발생하면 아래처럼 pytest temp/cache 위치를 workspace 내부로 고정한다. - -```powershell -$env:TMP=(Join-Path (Resolve-Path '.').Path 'pytest_tmp') -$env:TEMP=$env:TMP -New-Item -ItemType Directory -Force -Path $env:TMP | Out-Null -.venv\Scripts\python.exe -m pytest tests/unit tests/integration -v --basetemp "$env:TMP\basetemp" -o cache_dir="$env:TMP\cache" -``` - -### 3) End-to-end 검증 (Acceptance Gate #1, #3, #4) - -LLM 호출이 실제로 일어남. OpenAI는 비용 발생, Ollama는 로컬에서 무료. - -```powershell -# (A) Ollama 로컬 사용 (권장 — 비용 무료) -# 사전: Ollama 설치 후 `ollama pull qwen2.5` -$env:LLM_PROVIDER = "ollama" -$env:LLM_MODEL_NAME = "qwen2.5" -$env:LLM_BASE_URL = "http://localhost:11434" -pytest tests/e2e -m e2e -v - -# (B) OpenAI 사용 -$env:LLM_PROVIDER = "openai" -$env:LLM_MODEL_NAME = "gpt-4o-mini" -$env:LLM_API_KEY = "sk-..." -pytest tests/e2e -m e2e -v -``` - -**기대 결과**: -- `test_full_pipeline_writes_ontology_and_facts` PASS -- 응답에서 ontology TTL과 facts TTL이 비어 있지 않음 -- `metadata.budget.calls_count > 0` -- `metadata.budget.ontology_triples_generated > 0` 또는 `facts_triples_generated > 0` -- `tmp_path / "work"` 아래 `.ttl` 또는 `.rdf` 파일 생성됨 - -### 4) 수동 smoke (선택) - -```powershell -# 서버 기동 -uvicorn ont_platform.api.main:app --reload - -# 다른 셸에서 -curl http://localhost:8000/health -curl http://localhost:8000/info -curl -X POST http://localhost:8000/process ` - -H "Content-Type: application/json" ` - -d '{"text":"Alice works at Acme in Berlin."}' -``` - -## 통과 시 처리 - -위 모든 검증을 통과하면 **이 문서의 표 상태 컬럼을 ✅로 갱신**하고 git에 commit한다. - -이후 Phase 1 작업은 [PHASE1_NEXT_STEPS.md](PHASE1_NEXT_STEPS.md)를 따른다. - -## 실패 시 처리 - -- **단위 테스트 실패**: 어느 케이스가 실패했는지 확인. Phase 0.2/0.3/0.5의 vendored 수정 또는 platform/ 코드에 회귀가 발생했을 가능성. PR 단위로 롤백 후 재시도. -- **통합 테스트 실패**: FastAPI 라우팅/의존성 주입 문제. `platform/api/main.py` 또는 `platform/api/deps.py` 확인. -- **E2E 테스트 실패**: - - `LLM_API_KEY`, `LLM_PROVIDER`, `LLM_MODEL_NAME` 환경변수 확인 - - 워크플로우가 timeout: `ServerConfig.base_recursion_limit` 조정 검토 - - OntoCast `select_ontology.py` 또는 `convert_document.py` 수정에 회귀가 있는지 점검 (VENDORED_MODIFICATIONS.md 참조) - -## 검증 이력 - -| 일자 | 검증자 | 결과 | -|---|---|---| -| 2026-05-13 | (코드 작성: ontology-platform agent) | 코드 준비 완료. 실 환경 검증 보류. | -| 2026-05-14 | lasta + Claude | **unit 16/16, integration 10/10 통과** (Gate #2 ✅). 패키지 이름 충돌 수정 (`platform`→`ont_platform`). e2e는 LLM 필요로 대기. | -| 2026-05-19 | Codex | **unit 16/16, integration 11/11, 총 27/27 통과**. Phase 0 route gate 추가로 Trafilatura route는 PHASE>=1에서만 lazy mount. e2e는 LLM 필요로 대기. | -| ____-__-__ | ________________ | __________________________________ | diff --git a/ontology_platform/docs/phases/PHASE0_NEXT_STEPS.md b/ontology_platform/docs/phases/PHASE0_NEXT_STEPS.md deleted file mode 100644 index acffaf8..0000000 --- a/ontology_platform/docs/phases/PHASE0_NEXT_STEPS.md +++ /dev/null @@ -1,124 +0,0 @@ -# Phase 0 — 다음 작업자 핸드오프 - -본 문서는 이 프로젝트를 이어받는 AI 에이전트 또는 개발자가 즉시 작업을 시작하기 위한 핸드오프 노트다. - -## 현재 상태 (지금까지 완료된 것) - -- [x] **0.1 일부**: 폴더 골격, `pyproject.toml`, `README.md`, `.env.example`, `.gitignore`, `NOTICE` 생성 -- [x] **설계 문서**: [../통합설계서.md](../통합설계서.md) 배치 완료 (모든 작업의 기준) - -## 즉시 시작할 작업 (순서대로) - -### 0.1 (잔여): OntoCast vendored copy - -**근거**: 통합설계서 §9.1, OntoCast 분석 §13.1 - -```powershell -# 1. 원본을 vendored/ontocast로 복사 (.git 제외) -Copy-Item -Path "C:\Users\lasta\MyProject\AI\참고\ontocast-main\*" ` - -Destination "C:\Users\lasta\MyProject\AI\ontology_platform\vendored\ontocast\" ` - -Recurse -Exclude ".git",".github",".venv","node_modules" - -# 2. 원본 LICENSE 및 NOTICE를 vendored/ontocast/ 안에 그대로 유지 -# 3. NOTICE 파일의 "(원본 저장소 URL 기입)" 부분을 실제 URL로 채우기 -# 4. git init (아직 안 했다면) -cd C:\Users\lasta\MyProject\AI\ontology_platform -git init -git add . -git commit -m "Initial scaffold: folder skeleton, design doc, NOTICE" -``` - -**확인 사항**: -- [ ] `vendored/ontocast/` 안에 원본 LICENSE 파일이 있어야 한다 -- [ ] `vendored/ontocast/pyproject.toml`은 그대로 두되, 우리 `pyproject.toml`이 우선 -- [ ] NOTICE 파일의 OntoCast 항목에 실제 source URL 기입 - -### 0.2: `select_ontology.py` 버그 수정 - -**근거**: 통합설계서 §5 Phase 0, OntoCast 분석 §13.1 / §9.1 (1번) - -**문제**: `vendored/ontocast/ontocast/agent/select_ontology.py`에서 None 선택 인덱스 불일치. -- 코드는 `answer_index == 0`을 None으로 처리 -- 그러나 dynamic model은 `1..num_ontologies+1` 범위 사용 -- 실제 None 선택은 `num_ontologies + 1`이어야 자연스러움 - -**조치**: -1. 해당 함수의 분기 로직을 `answer_index == num_ontologies + 1` 또는 동등한 표현으로 수정 -2. **수정 사실을 파일 상단 주석으로 명시** (Apache 2.0 의무): 예) `# MODIFIED 2026-MM-DD: Fixed None index inconsistency, see docs/통합설계서.md §5 Phase 0` -3. 회귀 테스트 작성: `tests/unit/test_select_ontology.py` - - 케이스 1: ontology가 0개일 때 → None 반환 - - 케이스 2: ontology가 N개, LLM이 1~N 선택 → 해당 ontology 반환 - - 케이스 3: ontology가 N개, LLM이 N+1 선택 → None 반환 - -### 0.3: `convert_document.py` 다중 파일 처리 확장 - -**근거**: 통합설계서 §5 Phase 0, OntoCast 분석 §13.1 (3번) / §21.1 (4번) - -**문제**: `convert_document()`가 "processing only one file"로 주석 처리되어 있고, 다중 파일 처리 시 마지막 파일 기준으로만 상태가 업데이트됨. - -**조치**: -1. 입력 파일 목록을 순회하며 각 파일을 독립 `ContentUnit`으로 만들어 `AgentState.content_units`에 누적 -2. 동일 corpus 내 파일들이 함께 처리되도록 보장 (각 파일이 별도 doc IRI를 가짐) -3. 회귀 테스트: 2개 PDF를 한 번에 처리 → 둘 다 처리되어야 함 - -### 0.4: Robyn → FastAPI 재작성 - -**근거**: 통합설계서 §11 (기술 스택), OntoCast 분석 §13 (API 명세) - -**조치**: -1. `platform/api/main.py` 생성 (FastAPI app 인스턴스) -2. OntoCast 분석 §13.1~§13.4의 4개 endpoint를 FastAPI로 동일 시맨틱 재작성: - - `GET /health` - - `GET /info` - - `POST /process` (JSON + multipart) - - `POST /flush` (관리자 권한 + confirmation token, 분석 §21.1 #6) -3. OntoCast의 `ToolBox` 의존성 주입은 FastAPI `Depends`로 변환 -4. `uvicorn platform.api.main:app --reload`로 기동 가능해야 함 - -**중요**: OntoCast 코어 모듈(`stategraph/`, `agent/`, `onto/`, `tool/`)은 **건드리지 않는다**. API 레이어만 재작성. - -### 0.5: Pydantic Settings 정리 - -**근거**: 통합설계서 §5 Phase 0 (5번), OntoCast 분석 §15 - -**조치**: -1. `platform/config.py` 생성 -2. `pydantic-settings`의 `BaseSettings`로 `.env` 로딩 -3. **Phase 0에서는 filesystem 모드만 활성화** (Fuseki/Neo4j는 Phase 4에서): - - `STORAGE_BACKEND=filesystem` 강제 - - Neo4j/Fuseki 변수가 채워져 있어도 무시 -4. OntoCast의 기존 `Config` 클래스는 우리 `Settings`에서 만들어 주입 - -### 0.6: End-to-end 통합 테스트 - -**근거**: 통합설계서 §5 Phase 0 Acceptance Gate - -**조치**: -1. `vendored/ontocast/data/`의 예제 JSON 또는 PDF 1개를 fixture로 복사 → `tests/fixtures/` -2. `tests/integration/test_phase0_e2e.py` 작성: - - FastAPI `TestClient`로 `/process` 호출 - - 응답에 `ontology` TTL과 `facts` TTL 둘 다 포함 - - `working_directory/`에 ontology/facts 파일 생성 확인 - - BudgetTracker가 LLM call/triple count를 0보다 큰 값으로 기록 - -### 0.7: Acceptance Gate 0 체크 - -통합설계서 §5 Phase 0 Acceptance Gate의 4개 체크박스를 PR에 인용하며 모두 확인: - -- [ ] 단일 PDF 또는 JSON 입력 → ontology TTL + facts TTL이 filesystem에 생성됨 -- [ ] `/health`, `/info`, `/process` (FastAPI 버전) 정상 동작 -- [ ] BudgetTracker가 LLM call/triple count를 정확히 기록 -- [ ] LangGraph 워크플로우(CONVERT→CHUNK→...→SERIALIZE) 전 노드가 traceable - -## 작업 시 준수사항 - -1. **PR 단위**: 위의 0.1~0.7 각각을 별도 PR/커밋으로 분리. 하나의 PR에 여러 단계를 섞지 않는다. -2. **PR 설명에 근거 인용**: 예) "통합설계서 §5 Phase 0 (3번)에 따라 다중 파일 처리 확장. OntoCast 분석 §13.1 인용." -3. **vendored/ 수정 시 라이선스 의무**: - - 수정한 파일 상단에 `# MODIFIED YYYY-MM-DD: <한 줄 설명>` 주석 추가 - - 원본 LICENSE/NOTICE 파일은 절대 삭제하지 않는다 -4. **Phase 1로 넘어가지 말 것**: Acceptance Gate 0 통과 전까지 Trafilatura/Crawl4AI/Guardrails/Neo4j GraphRAG 의존성을 활성화하거나 import하지 않는다. (`pyproject.toml`에 명시되어 있더라도 코드에서 사용 금지) - -## Phase 1 이후 핸드오프 - -Phase 0 완료 후, 본 폴더에 `PHASE1_NEXT_STEPS.md`를 작성하여 다음 작업자에게 동일한 형식으로 핸드오프한다. 통합설계서 §12 Phase 1 작업 단위(1.1~1.7)를 참조. diff --git a/ontology_platform/docs/phases/PHASE1_ACCEPTANCE_GATE.md b/ontology_platform/docs/phases/PHASE1_ACCEPTANCE_GATE.md deleted file mode 100644 index 0975e62..0000000 --- a/ontology_platform/docs/phases/PHASE1_ACCEPTANCE_GATE.md +++ /dev/null @@ -1,33 +0,0 @@ -# Phase 1 Acceptance Gate 결과 - -작성일: 2026-05-19 - -범위: Trafilatura 기반 URL/HTML 입력 정렬, SourceDocument/EvidenceSpan 계약, URL 입력 API, fixture 기반 dedup 검증. - -## 결과 요약 - -| # | Acceptance Gate 항목 | 상태 | 검증 방법 | -|---|---|---|---| -| 1 | URL/HTML 입력이 정제 문서로 변환됨 | 통과 | `tests/unit/test_web_extractor.py` | -| 2 | source URL, title, language, content hash, fingerprint 보존 | 통과 | `test_extract_from_korean_html_preserves_document_contract` | -| 3 | `SourceDocument`, `EvidenceSpan`, Content metadata 경계 연결 | 통과 | `test_extracted_content_maps_to_source_document_and_evidence_spans`, `test_content_unit.py` | -| 4 | `/process/url`, `/api/v1/extract/url` URL 입력 API 제공 | 통과 | `tests/integration/test_url_ingest.py` | -| 5 | 같은 본문 중복 입력은 fingerprint 기반으로 skip | 통과 | `test_same_clean_body_gets_same_hash_and_fingerprint`, `test_process_url_skips_duplicate_payload_by_fingerprint` | -| 6 | Phase 0 회귀 없음 | 통과 | `python -m pytest tests/unit tests/integration -q` | - -## 검증 이력 - -| 일자 | 검증자 | 결과 | -|---|---|---| -| 2026-05-19 | Codex | Phase 1 신규 테스트 7/7 통과. 전체 unit/integration 34/34 통과. | - -## 구현 메모 - -- `ont_platform/core/extractors/web_extractor.py`는 Trafilatura 2.x `bare_extraction`을 사용하되, local HTML fixture에서 Trafilatura fingerprint가 비어 있는 경우 normalized text 기반 `sha1:` fingerprint를 생성한다. -- `ont_platform/storage/models.py`의 SQLAlchemy 예약어 충돌을 피하기 위해 DB 컬럼명은 `metadata`로 유지하고 Python attribute는 `metadata_`로 정리했다. -- `/process/url`, `/api/v1/process/url`, `/api/v1/extract/url`은 같은 Phase 1 응답 계약을 사용한다. -- OntoCast vendored core는 수정하지 않았다. - -## 다음 Gate - -Phase 2는 Candidate Storage 및 Review 책임 경계를 다룬다. 진행 전 `PHASE_INDEX.md`에서 Phase 2 항목만 명시적으로 선택해 작업한다. diff --git a/ontology_platform/docs/phases/PHASE1_NEXT_STEPS.md b/ontology_platform/docs/phases/PHASE1_NEXT_STEPS.md deleted file mode 100644 index e5e0bad..0000000 --- a/ontology_platform/docs/phases/PHASE1_NEXT_STEPS.md +++ /dev/null @@ -1,162 +0,0 @@ -# Phase 1 — Trafilatura 통합 (다음 작업자 핸드오프) - -본 문서는 Phase 0이 완료된 시점에서 Phase 1 작업을 이어받는 AI 에이전트 또는 개발자가 즉시 작업을 시작하기 위한 핸드오프 노트다. - -## 시작 전 확인 사항 - -- [ ] **Phase 0 Acceptance Gate**가 모두 ✅인가? [PHASE0_ACCEPTANCE_GATE.md](PHASE0_ACCEPTANCE_GATE.md) 참조. 통과 전에는 Phase 1 진행 금지. -- [ ] `tests/unit`와 `tests/integration` 전체가 PASS인가? -- [ ] git log에 Phase 0 commit들이 PR 단위로 분리되어 있는가? (0.1 vendored / 0.2 bug fix / 0.3 multi-file / 0.4 FastAPI / 0.5 config / 0.6 e2e tests / 0.7 gate) - -## Phase 1 목표 - -URL이 입력일 때 원본 페이지에서 본문, 제목, 저자, 날짜, 언어, canonical URL을 정확히 뽑아 OntoCast의 `ContentUnit` metadata에 채워 넣는다. - -**근거**: 통합설계서 §5 Phase 1, Trafilatura 분석 §11~§17. - -**왜 Trafilatura를 가장 먼저 통합하는가**: 가장 작은 통합 — 단일 함수 호출(`bare_extraction`)만으로 끝남. 의존성도 명확하며 라이선스 동일 (Apache 2.0). - -## 작업 단위 (PR 분해) - -### 1.1: Trafilatura 의존성 활성화 - -`pyproject.toml`에 이미 `trafilatura[all]>=2.0.0`이 명시되어 있다. 활성화 절차: - -```powershell -pip install -e ".[dev]" # 의존성 재설치 시 trafilatura 자동 설치 -python -c "import trafilatura; print(trafilatura.__version__)" -``` - -**확인**: `2.0.0` 이상이 출력되어야 한다. - -### 1.2: `web_extractor.py` 어댑터 작성 - -**위치**: `platform/core/extractors/web_extractor.py` - -**근거**: Trafilatura 분석 §17의 `extract_for_ontology` 함수를 거의 그대로 사용. - -**필수 동작**: -- 입력: `html: str`, `url: str`, `lang: str | None = None` -- 출력: `ExtractedWebDocument` (dataclass) - - `url`, `title`, `author`, `date`, `sitename`, `description` - - `text` (정제 본문) - - `body_xml` (Trafilatura `Document.body`) - - `metadata` (raw dict) - - `fingerprint` (SimHash) -- 실패 시 `None` 반환 - -**호출 옵션** (Trafilatura 분석 §12 권장값 그대로): -```python -Extractor( - output_format="python", - url=url, - with_metadata=True, - comments=False, - tables=True, - formatting=True, - links=True, - images=True, - dedup=True, - lang=lang, -) -``` - -### 1.3: `ContentUnit` 모델 확장 - -OntoCast의 `vendored/ontocast/ontocast/onto/content_unit.py`를 **직접 수정하지 말고**, 우리 쪽에 wrapper 모델을 만든다. - -**위치**: `platform/models/content_unit.py` - -**필드** (통합설계서 §7.1 참조): -- 기존 OntoCast 필드 (`text`, `index`, `doc_iri`, `graph`, `type`, `iri`) 유지/위임 -- 추가: `source_url`, `title`, `author`, `publish_date`, `language`, `sitename`, `fingerprint`, `content_hash`, `metadata`, `retrieved_at`, `extracted_by` - -**호환성**: 기존 OntoCast 코드가 받는 `ContentUnit`과 인터페이스 호환되도록 `as_ontocast()` 메서드 제공. - -### 1.4: OntoCast `ConverterTool` 분기 추가 (URL/HTML 입력) - -**문제**: OntoCast `ConverterTool`은 PDF/DOCX/MD만 처리. URL 또는 HTML 입력은 처리 못 함. - -**조치 옵션**: -- **옵션 A (권장)**: OntoCast의 `convert_document.py` 모듈에 새 분기 추가 — `.html`, `.htm` 확장자 또는 `state.source_url`이 있으면 Trafilatura로 처리. **vendored 수정이지만 매우 작음**. -- **옵션 B**: API 레이어(`platform/api/`)에서 입력이 URL이면 미리 fetch + Trafilatura 처리한 뒤 그 결과를 JSON envelope로 ToolBox에 넘김. - -**권장**: 옵션 B. vendored 수정을 늘리지 않고 platform 코드로 끝낼 수 있음. - -새 endpoint: -- `POST /process/url` — body: `{"url": "...", "ontology_user_instruction": "...", ...}` — 내부적으로 `web_extractor`로 본문 추출 후 OntoCast workflow 실행. - -### 1.5: Fingerprint 기반 dedup - -- `tests/fixtures/`에 같은 본문의 두 URL fixture 만들기 -- `web_extractor` 결과의 `fingerprint`가 일치하면 OntoCast 처리 skip -- 저장 위치: 일단 in-memory set (`platform/storage/dedup_cache.py`), Phase 2에서 Redis로 이전 - -### 1.6: 한국어 페이지 3종 추출 검증 - -**테스트 fixture 수집**: -- 한국어 뉴스 1개 (예: 연합뉴스/조선/한겨레) -- 한국어 블로그 1개 (예: 네이버 블로그) -- 한국어 쇼핑 페이지 1개 (예: 쿠팡 상품 페이지) - -각각 raw HTML을 `tests/fixtures/korean/`에 저장 (실제 fetch는 운영 환경에서 한 번만, 그 결과를 fixture로 박제). - -**테스트**: `tests/integration/test_web_extractor_korean.py` -- 본문 길이 > 200자 -- title 추출 성공 -- language 감지: `ko` -- author 또는 date 중 하나 이상 추출 - -### 1.7: Acceptance Gate 1 체크 - -통합설계서 §5 Phase 1 Acceptance Gate 4개 항목: - -- [ ] URL 입력 → 본문/메타데이터가 정확히 추출되어 `ContentUnit`에 저장됨 -- [ ] 한국어 뉴스/블로그/쇼핑 페이지 각각 1개씩 본문 추출 정확도 수동 검증 -- [ ] 동일 URL 재입력 시 fingerprint 기반 dedup으로 skip -- [ ] Phase 0의 모든 기능이 여전히 정상 동작 (회귀 없음) - -Phase 0의 `tests/unit/`, `tests/integration/` 전체가 여전히 PASS여야 함. - -## Phase 1에서 만들 새 산출물 - -``` -platform/ - core/ - extractors/ - web_extractor.py ← 1.2 - models/ - content_unit.py ← 1.3 - storage/ - dedup_cache.py ← 1.5 - api/ - routes/ - url_ingest.py ← 1.4 (POST /process/url) -tests/ - fixtures/ - korean/ ← 1.6 - news_yonhap.html - blog_naver.html - shop_coupang.html - unit/ - test_web_extractor.py ← 1.2 - test_dedup_cache.py ← 1.5 - integration/ - test_url_ingest.py ← 1.4 - test_web_extractor_korean.py ← 1.6 -docs/ - phases/ - PHASE1_ACCEPTANCE_GATE.md ← 1.7 (PHASE0과 동일 형식) - PHASE2_NEXT_STEPS.md ← 다음 작업자에게 넘김 -``` - -## 작업 시 준수사항 (PHASE0과 동일) - -1. **PR 단위 분리**: 1.1~1.7 각각 별도 PR/커밋. -2. **PR 설명에 근거 인용**: 예) "통합설계서 §5 Phase 1 (1.2)에 따라 Trafilatura adapter 작성. Trafilatura 분석 §17 인용." -3. **vendored/ontocast/** 수정 최소화. 본 Phase에서는 옵션 B 사용 시 vendored 수정 0건이 목표. -4. **Phase 2로 넘어가지 말 것**: Acceptance Gate 1 통과 전까지 Crawl4AI 의존성을 코드에서 import하지 않는다. - -## Phase 2 이후 핸드오프 - -Phase 1 완료 후 다음 작업자에게 동일한 형식의 `PHASE2_NEXT_STEPS.md`를 작성한다. 통합설계서 §12 Phase 2 작업 단위(2.1~2.8)를 참조. diff --git a/ontology_platform/docs/phases/PHASE2_ACCEPTANCE_GATE.md b/ontology_platform/docs/phases/PHASE2_ACCEPTANCE_GATE.md deleted file mode 100644 index 7bd0a5b..0000000 --- a/ontology_platform/docs/phases/PHASE2_ACCEPTANCE_GATE.md +++ /dev/null @@ -1,35 +0,0 @@ -# Phase 2 Acceptance Gate 결과 - -작성일: 2026-05-19 - -범위: Candidate Storage 및 Review 책임 경계. Lightweight/OntoCast 후보 저장 경로, review 상태 전이, audit trail, evidence 기반 promotion gate. - -## 결과 요약 - -| # | Acceptance Gate 항목 | 상태 | 검증 방법 | -|---|---|---|---| -| 1 | extraction 결과가 candidate로 저장됨 | 통과 | `tests/unit/test_candidate_repository.py` | -| 2 | lightweight와 OntoCast 저장 경로가 분리됨 | 통과 | `test_repository_saves_lightweight_candidates_with_evidence`, `test_repository_saves_ontocast_candidates_on_separate_source_path` | -| 3 | 승인/반려/자동승인 상태 변경 이력이 남음 | 통과 | `tests/unit/test_review_service.py` | -| 4 | evidence 없는 항목은 승인 및 graph commit 대상이 아님 | 통과 | `test_candidate_without_evidence_cannot_be_approved`, `test_promotion_plan_blocks_approved_candidate_without_evidence` | -| 5 | Review API가 ingest/list/detail/approve/reject/promote 흐름을 제공함 | 통과 | `tests/integration/test_review_api.py` | -| 6 | Phase 0-1 회귀 없음 | 통과 | `python -m pytest tests/unit tests/integration -q` | - -## 검증 이력 - -| 일자 | 검증자 | 결과 | -|---|---|---| -| 2026-05-19 | Codex | Phase 2 신규 테스트 9/9 통과. 전체 unit/integration 43/43 통과. 변경 파일 대상 ruff 통과. | - -## 구현 메모 - -- `CandidateEntity`, `CandidateRelation`에 `source_type`, `created_by`, `validation_passed`, `promoted_at`을 추가해 review queue 계약을 명확히 했다. -- `ReviewDecision`으로 상태 변경 audit trail을 남긴다. -- `CandidateRepository.save_lightweight_result()`와 `save_ontocast_result()`를 분리해 두 입력 경로가 같은 candidate contract로 정규화되되, 출처는 유지된다. -- `ReviewService`는 `pending -> approved/rejected/auto_approved`, `approved/auto_approved -> rejected`만 허용한다. -- `CandidatePromotionService`는 `approved` 또는 `auto_approved`이면서 evidence가 실제 존재하는 후보만 commit plan에 포함한다. -- OntoCast vendored core는 수정하지 않았다. - -## 다음 Gate - -Phase 3은 Crawl4AI 수집 계층 및 Job Orchestration이다. 진행 전 `PHASE_INDEX.md`에서 Phase 3 항목만 명시적으로 선택해 작업한다. diff --git a/ontology_platform/docs/phases/PHASE2_NEXT_STEPS.md b/ontology_platform/docs/phases/PHASE2_NEXT_STEPS.md deleted file mode 100644 index fa02845..0000000 --- a/ontology_platform/docs/phases/PHASE2_NEXT_STEPS.md +++ /dev/null @@ -1,27 +0,0 @@ -# Phase 2 — Candidate Storage 및 Review 책임 경계 - -본 문서는 Phase 1 완료 후 다음 작업자가 Phase 2를 시작할 때 참고할 핸드오프 노트다. 자동으로 Phase 2를 진행하지 않는다. - -## 시작 전 확인 - -- `PHASE_INDEX.md`에서 Phase 2 진행 요청이 명시되어 있는지 확인한다. -- `PHASE1_ACCEPTANCE_GATE.md`의 unit/integration 34/34 통과 상태를 기준선으로 삼는다. -- vendored OntoCast core는 계속 직접 수정하지 않는다. - -## Phase 2 목표 - -추출 결과를 바로 확정 그래프로 보내지 않고, 사람이 검토할 수 있는 candidate/review queue 계약으로 분리한다. SourceDocument와 EvidenceSpan이 없는 후보는 확정 graph로 들어가지 못하게 한다. - -## 작업 범위 - -1. `storage/models.py`의 `CandidateEntity`, `CandidateRelation`을 review queue 계약으로 확정한다. -2. OntoCast 결과와 lightweight extraction 결과의 저장 경로를 분리한다. -3. `pending`, `approved`, `auto_approved`, `rejected` 상태 전이 규칙을 문서와 테스트로 고정한다. -4. evidence 없는 후보가 확정 graph로 승격되지 못하도록 validation boundary를 둔다. - -## 권장 테스트 - -- 후보 생성 시 `document_id`와 `evidence_ids`가 필수로 연결되는지 검증한다. -- 승인/반려/자동승인 상태 전이가 허용된 경로로만 움직이는지 검증한다. -- evidence 없는 entity/relation이 commit 단계에 도달하지 못하는지 검증한다. -- Phase 1 URL/HTML ingestion 테스트가 계속 통과하는지 회귀 검증한다. diff --git a/ontology_platform/docs/phases/PHASE3_NEXT_STEPS.md b/ontology_platform/docs/phases/PHASE3_NEXT_STEPS.md deleted file mode 100644 index 568cf6c..0000000 --- a/ontology_platform/docs/phases/PHASE3_NEXT_STEPS.md +++ /dev/null @@ -1,28 +0,0 @@ -# Phase 3 — Crawl4AI 수집 계층 및 Job Orchestration - -본 문서는 Phase 2 완료 후 다음 작업자가 Phase 3을 시작할 때 참고할 핸드오프 노트다. 자동으로 Phase 3을 진행하지 않는다. - -## 시작 전 확인 - -- `PHASE_INDEX.md`에서 Phase 3 진행 요청이 명시되어 있는지 확인한다. -- `PHASE2_ACCEPTANCE_GATE.md`의 unit/integration 43/43 통과 상태를 기준선으로 삼는다. -- 수집 계층은 SourceDocument 생성 전 단계까지만 책임진다. Candidate 저장과 Review Queue는 Phase 2 계약을 사용한다. -- vendored OntoCast core는 계속 직접 수정하지 않는다. - -## Phase 3 목표 - -정적 URL 1건 처리를 넘어 동적 페이지와 대량 수집을 job 단위로 관리한다. Crawl4AI는 acquisition adapter로 감싸고, 본문 정제는 Phase 1 Trafilatura adapter, 후보 저장은 Phase 2 Review Queue로 넘긴다. - -## 작업 범위 - -1. `crawl4ai_adapter.py`를 동적/대량 수집 adapter로 제한한다. -2. crawler profile, robots policy, cache policy를 설정 기반으로 분리한다. -3. Job 상태 모델과 progress API/WebSocket 경계를 정리한다. -4. 수집 결과를 Trafilatura 후처리와 SourceDocument 저장으로 연결한다. - -## 권장 테스트 - -- 정적 HTML/동적 페이지 profile이 같은 SourceDocument 계약으로 이어지는지 검증한다. -- robots/cache policy가 설정값에 따라 선택되는지 검증한다. -- job 상태가 pending/running/completed/failed로 전이되는지 검증한다. -- Phase 1 extraction 및 Phase 2 review queue 테스트가 계속 통과하는지 회귀 검증한다. diff --git a/ontology_platform/docs/phases/PHASE_INDEX.md b/ontology_platform/docs/phases/PHASE_INDEX.md index e51c766..c5ba6f5 100644 --- a/ontology_platform/docs/phases/PHASE_INDEX.md +++ b/ontology_platform/docs/phases/PHASE_INDEX.md @@ -1,100 +1,101 @@ -# PHASE INDEX - ontology_platform engine-respect roadmap +# PHASE INDEX - Semantic Page Classification Layer -?묒꽦?? 2026-05-19 +작성일: 2026-05-22 -踰붿쐞: `ontology_platform` ?꾩슜. `crawler_platform`?€ ?대쾲 ?묒뾽 踰붿쐞?먯꽌 ?쒖쇅?쒕떎. +범위: `ontology_platform`의 `crawler_platform.app.core.crawler.page_classifier` 및 page classification과 직접 연결된 crawler/extractor 흐름. -湲곗? 臾몄꽌: -- `ontology_platform/docs/?듯빀?ㅺ퀎??md` +기준 문서: +- `README.md` +- `docs/PHASE_PLANNING.md` - `ontology_platform/README.md` -- `ontology_platform/docs/phases/PHASE0_ACCEPTANCE_GATE.md` -- `ontology_platform/docs/phases/PHASE1_NEXT_STEPS.md` -- `ontology_platform/docs/phases/PHASE1_ACCEPTANCE_GATE.md` -- `ontology_platform/docs/phases/PHASE2_ACCEPTANCE_GATE.md` +- `ontology_platform/docs/semantic_page_classification_codex_spec.md` -?듭떖 ?먯튃: -- OntoCast??Base ?붿쭊?쇰줈 議댁쨷?쒕떎. -- vendored OntoCast 肄붿뼱???듯빀?ㅺ퀎?쒓? ?덉슜??踰붿쐞 ?몄뿉???섏젙?섏? ?딅뒗?? -- Trafilatura, Crawl4AI, Guardrails, Neo4j GraphRAG??吏곸젒 ?ш뎄?꾪븯吏€ ?딄퀬 ?뉗? adapter/facade濡?媛먯떬?? -- Firecrawl, OpenDeepResearcher 肄붾뱶???ы븿?섏? ?딅뒗?? -- Acceptance Gate瑜??듦낵?섍린 ???ㅼ쓬 ?듯빀?쇰줈 ?섏뼱媛€吏€ ?딅뒗?? +핵심 원칙: +- 기존 엔진을 폐기하거나 대규모로 교체하지 않는다. +- 기존 `classify_page(...) -> str` 호출부가 깨지지 않도록 legacy compatibility를 유지한다. +- 기존 `ProductPage`, `CategoryPage`, `SearchPage`, `BoardPage`, `BrandStoryPage`, `UnknownPage` 문자열은 alias 또는 compatibility mapping으로 유지한다. +- URL substring 중심 if-return 확장이 아니라 signal extraction -> evidence scoring -> classification result -> analyze strategy -> LLM policy 구조로 확장한다. +- protected page는 안전하게 skip하고, UnknownPage는 evidence와 confidence를 남긴다. +- pytest 또는 현재 프로젝트 테스트 명령으로 회귀 검증한다. --- -PHASE 0. ?붿쭊 寃쎄퀎 媛먯궗 諛?Phase Gate 蹂듦뎄 -FILE: ./26_05_19_engine_respect_plan/phase_00_001_engine_boundary_gate.md +PHASE 1. 현재 흐름 기준선 고정 및 영향 범위 정리 +FILE: ./26_05_22_semantic_page_classification/phase_01_001_current_flow_boundary.md -1) ?꾩옱 `ont_platform` 紐⑤뱢??Base/Adapter/Draft/Excluded 梨낆엫?쇰줈 遺꾨쪟 [?꾨즺] -2) Phase 0?먯꽌 誘몃옒 Phase ?섏〈?깆씠 import?섏뼱 ???쒖옉??源⑥? ?딅룄濡?寃뚯씠???뺣━ [?꾨즺] -3) Phase 0 unit/integration 寃€利??덉감 怨좎젙 [?꾨즺] -4) `PHASE0_ACCEPTANCE_GATE.md` 媛깆떊 湲곗? ?뺣━ [?꾨즺] +1) `page_classifier.py`의 현재 public API와 legacy page_type 문자열 목록 고정 [완료] +2) `should_analyze_page()` 호출부와 crawler의 `classify_page()` 호출 위치 문서화 [완료] +3) Extractor/HybridExtractor에서 page_type과 LLM skip 정책이 연결되는 흐름 정리 [완료] +4) 기존 page_type 문자열을 기대하는 테스트, config, adapter, ontology rule 경로 목록화 [완료] --- -PHASE 1. Trafilatura 湲곕컲 URL/HTML ?낅젰 ?뺣젹 -FILE: ./26_05_19_engine_respect_plan/phase_01_001_trafilatura_ingestion.md +PHASE 2. Taxonomy와 Classification Result 모델 추가 +FILE: ./26_05_22_semantic_page_classification/phase_02_001_taxonomy_result_model.md -1) `web_extractor.py`瑜?Trafilatura adapter 梨낆엫?쇰줈 ?뺣━ [?꾨즺] -2) `SourceDocument`, `EvidenceSpan`, Content metadata ?€??寃쎄퀎 ?곌껐 [?꾨즺] -3) `/process/url` ?먮뒗 ?숇벑??URL ?낅젰 API ?ㅺ퀎 [?꾨즺] -4) ?쒓뎅??URL/HTML fixture 湲곕컲 異붿텧 ?뚯뒪?몄? dedup 湲곗? ?묒꽦 [?꾨즺] +1) PageDomain/PageArchetype/PageType/EntityType/ActionIntent/GraphRole/AnalyzeStrategy/LLMPolicy 상수 또는 enum 추가 [완료] +2) `EvidenceItem`, `PageClassificationResult` dataclass 추가 [완료] +3) legacy alias 및 normalize helper 추가 [완료] +4) 기존 `classify_page()` 문자열 반환 호환을 유지하면서 semantic result API 추가 [완료] --- -PHASE 2. Candidate Storage 諛?Review 梨낆엫 寃쎄퀎 -FILE: ./26_05_19_engine_respect_plan/phase_02_001_candidate_review_boundary.md +PHASE 3. Raw Snapshot 및 Signal Extraction 레이어 추가 +FILE: ./26_05_22_semantic_page_classification/phase_03_001_signal_extraction_layer.md -1) `storage/models.py`???꾨낫 紐⑤뜽???뺤떇 Review Queue 怨꾩빟?쇰줈 ?뺤젙 [?꾨즺] -2) OntoCast 寃곌낵?€ lightweight extraction 寃곌낵???€??寃쎈줈 遺꾨━ [?꾨즺] -3) ?뱀씤/諛섎젮/?먮룞?뱀씤 ?곹깭 ?꾩씠 洹쒖튃 ?뺤쓽 [?꾨즺] -4) evidence ?녿뒗 ?꾨낫媛€ ?뺤젙 graph濡??ㅼ뼱媛€吏€ 紐삵븯寃?李⑤떒 [?꾨즺] +1) `RawPageSnapshot`와 `PageSignals` 모델 추가 [완료] +2) JSON-LD, OpenGraph, Twitter Card, meta, headings, links, forms, buttons, inputs 추출 [완료] +3) commerce/listing/editorial/community/docs/corporate/protected/system signal 추출 [완료] +4) HTML 일부가 깨지거나 필드가 누락되어도 예외 없이 빈 값으로 처리 [완료] --- -PHASE 3. Crawl4AI ?섏쭛 怨꾩링 諛?Job Orchestration -FILE: ./26_05_19_engine_respect_plan/phase_03_001_crawl4ai_acquisition_jobs.md +PHASE 4. Evidence Scoring 기반 Semantic Classification 구현 +FILE: ./26_05_22_semantic_page_classification/phase_04_001_evidence_scoring_classifier.md -1) `crawl4ai_adapter.py`瑜??숈쟻/?€???섏쭛 adapter濡??쒗븳 [?꾨즺] -2) crawler profile, robots policy, cache policy瑜??ㅼ젙 湲곕컲?쇰줈 遺꾨━ [?꾨즺] -3) Job ?곹깭 紐⑤뜽怨?progress API/WebSocket 寃쎄퀎 ?뺣━ [?꾨즺] -4) Trafilatura ?꾩쿂由ъ? SourceDocument ?€?μ쑝濡??곌껐 [?꾨즺] +1) 주요 page type별 scoring function과 evidence recording 구조 추가 [완료] +2) 최소 20개 semantic page type 분류 구현 [완료] +3) confidence, alternatives, secondary_page_types 산출 [완료] +4) low confidence 또는 모호한 결과를 evidence 포함 UnknownPage로 처리 [완료] --- -PHASE 4. Guardrails Validation Gate -FILE: ./26_05_19_engine_respect_plan/phase_04_001_guardrails_validation_gate.md +PHASE 5. Analyze Strategy 및 LLM Policy 분리 +FILE: ./26_05_22_semantic_page_classification/phase_05_001_analysis_llm_policy.md -1) `core/validation`??Pydantic lightweight?€ Guardrails facade濡?遺꾨━ [?꾨즺] -2) OntoCast LLM 異쒕젰 ?섑븨 吏€?먯쓣 vendored ?섏젙 ?놁씠 ?곗꽑 ?ㅺ퀎 [?꾨즺] -3) schema violation, endpoint missing, confidence range ?뚯뒪???묒꽦 [?꾨즺] -4) Guard ?ㅽ뙣 寃곌낵瑜?candidate/review issue濡??€??[?꾨즺] +1) PageClassificationResult 기반 `decide_analyze_strategy()` 추가 [TODO] +2) PageClassificationResult 기반 `decide_llm_policy()` 추가 [TODO] +3) `should_analyze_page(result_or_page_type, analyze_page_types=None)` compatibility 구현 [TODO] +4) Category/Search/Board 계열을 무조건 skip하지 않고 strategy 기반으로 처리 [TODO] +5) Login/Checkout/Payment/Captcha/AccessDenied 계열은 SkipProtected/Skip 정책으로 처리 [TODO] --- -PHASE 5. Neo4j Projection 諛?GraphRAG 寃€??FILE: ./26_05_19_engine_respect_plan/phase_05_001_neo4j_projection_graphrag.md +PHASE 6. Crawler, Cleaner, Extractor, Discovery/Relevance 통합 +FILE: ./26_05_22_semantic_page_classification/phase_06_001_pipeline_integration.md -1) RDF/Fuseki瑜?canonical store, Neo4j瑜?projection/search store濡?怨좎젙 [?꾨즺] -2) `core/graph` 湲곗〈 紐⑤뱢??projection/search adapter 梨낆엫?쇰줈 ?щ텇瑜?[?꾨즺] -3) read-only Text2Cypher?€ vector/hybrid retriever API ?ㅺ퀎 [?꾨즺] -4) provenance媛€ search result源뚯? ?댁뼱吏€??寃€利?湲곗? ?묒꽦 [?꾨즺] +1) `site_crawler.py`와 `pipeline.py` metadata에 semantic classification payload 저장 [TODO] +2) `ExtractionPageContext` 또는 metadata를 통해 analyze_strategy/llm_policy 전달 [TODO] +3) `HybridExtractor`가 LLMPolicy를 우선 사용하고 legacy page_type fallback을 유지하도록 수정 [TODO] +4) `page_cleaner.py`, `domain_discovery.py`, `relevance_engine.py`의 legacy page_type 기대 경로와 신규 semantic type을 호환 [TODO] --- -PHASE 6. Maintenance Loop 諛??댁쁺 湲곕뒫 ?뺣━ -FILE: ./26_05_19_engine_respect_plan/phase_06_001_maintenance_loop_operations.md +PHASE 7. Unknown Pattern 저장 기반 추가 +FILE: ./26_05_22_semantic_page_classification/phase_07_001_unknown_pattern_storage.md -1) Knowledge Agent??肄붾뱶媛€ ?꾨땲???꾨\?꾪듃/?뚰겕?뚮줈???⑦꽩留?李⑥슜 [?꾨즺] -2) Analyst/Researcher/Curator/Auditor/Fixer/Advisor 梨낆엫 ?뺤쓽 [?꾨즺] -3) `auth`, `audit`, `billing`, `realtime` 珥덉븞 紐⑤뱢???댁쁺 寃쎄퀎 ?뺣━ [?꾨즺] -4) destructive fix???щ엺 ?뱀씤 寃뚯씠?몃? 諛섎뱶???듦낵?섎룄濡??ㅺ퀎 [?꾨즺] +1) UnknownPage 또는 low confidence 페이지의 evidence payload 정의 [TODO] +2) text/html/link/schema/button/form summary와 fingerprint hook 추가 [TODO] +3) DB schema 변경 없이 metadata_json에 저장 가능한 초기 구조 구현 [TODO] +4) 향후 clustering/embedding 확장을 위한 hook만 추가하고 실제 clustering은 이번 범위에서 제외 [TODO] --- -PHASE 7. Hybrid Rule + LLM Extraction -FILE: ./26_05_19_engine_respect_plan/phase_07_001_hybrid_rule_llm_extraction.md +PHASE 8. 테스트 Fixture 및 회귀 검증 +FILE: ./26_05_22_semantic_page_classification/phase_08_001_tests_regression.md -1) rule baseline, LLM extraction, fallback, validation, Review UI 흐름을 기준선으로 고정 [신규] -2) `rule_only`, `llm_only`, `hybrid`, `compare` extraction mode 계약 정의 [신규] -3) product backend에 명시적 HybridExtractor와 rule/LLM agreement metadata 추가 [신규] -4) confidence breakdown에 rule agreement와 conflict/review 정책 반영 [신규] -5) Crawl/Research UI에서 mode/provider/model/base URL 선택 지원 [신규] +1) 최소 10개 이상의 HTML fixture 추가 [TODO] +2) ProductDetailPage, CategoryListingPage, SearchResultsPage, ArticlePage, QAPage, FAQPage, ForumThreadPage, DocumentationPage, JobPostingPage, LoginPage, CheckoutPage, TermsPage, SitemapPage, UnknownPage 단위 테스트 추가 [TODO] +3) legacy `classify_page()`와 `should_analyze_page()` 호환성 테스트 추가 [TODO] +4) HybridExtractor LLMPolicy 회귀 테스트 추가 [TODO] +5) pytest 또는 현재 프로젝트 테스트 명령 실행 및 결과 기록 [TODO] diff --git a/ontology_platform/tests/unit/test_phase7_hybrid_extraction.py b/ontology_platform/tests/unit/test_phase7_hybrid_extraction.py index 3db3fb5..0f6b195 100644 --- a/ontology_platform/tests/unit/test_phase7_hybrid_extraction.py +++ b/ontology_platform/tests/unit/test_phase7_hybrid_extraction.py @@ -1,6 +1,7 @@ from crawler_platform.app.config.loader import ProjectConfig +from crawler_platform.app.api.routes import extraction_log_summary from crawler_platform.app.core.extractor.base import ExtractedClaim, ExtractionBundle, ExtractionPageContext -from crawler_platform.app.core.extractor.hybrid import HybridExtractor, mark_bundle, merge_bundles +from crawler_platform.app.core.extractor.hybrid import HybridExtractor, fallback_bundle, mark_bundle, merge_bundles from crawler_platform.app.core.extractor.validation import validate_extraction_bundle @@ -142,3 +143,61 @@ def test_hybrid_smart_routing_skips_llm_for_category_page() -> None: assert bundle.raw_output["llm_skipped"] is True assert bundle.raw_output["effective_extraction_mode"] == "rule_only" assert "CategoryPage" in bundle.raw_output["llm_skip_reason"] + + +def test_rule_only_mode_records_rule_llm_summary_counts() -> None: + rule_bundle = ExtractionBundle( + claims=[ + ExtractedClaim("A", "Thing", "color", object_value="red"), + ExtractedClaim("A", "Thing", "size", object_value="large"), + ] + ) + + class FixedRuleExtractor: + def extract(self, page_text, project_config): # noqa: ANN001 + return rule_bundle + + def extract_from_context(self, context, project_config): # noqa: ANN001 + return rule_bundle + + extractor = HybridExtractor("generic", mode="rule_only") + extractor.rule_extractor = FixedRuleExtractor() + + bundle = extractor.extract("Alpha color red size large", project_config()) + + assert bundle.raw_output["rule_claim_count"] == 2 + assert bundle.raw_output["llm_claim_count"] == 0 + assert bundle.raw_output["comparison"]["rule_only"] == 2 + + +def test_fallback_records_rule_counts_and_comparison() -> None: + rule_bundle = ExtractionBundle( + claims=[ + ExtractedClaim("A", "Thing", "color", object_value="red"), + ExtractedClaim("A", "Thing", "size", object_value="large"), + ] + ) + mark_bundle(rule_bundle, source="rule", mode="hybrid") + + bundle = fallback_bundle(rule_bundle, HybridExtractor("generic", mode="hybrid"), RuntimeError("boom")) + + assert bundle.raw_output["rule_claim_count"] == 2 + assert bundle.raw_output["llm_claim_count"] == 0 + assert bundle.raw_output["comparison"]["rule_only"] == 2 + + +def test_extraction_log_summary_backfills_legacy_rule_only_counts() -> None: + summary = extraction_log_summary( + { + "validation": {"rejected_claim_count": 4}, + "candidate_claims": [ + {"metadata": {"agreement": "rule_only", "extraction_source": "rule"}}, + {"metadata": {"agreement": "rule_only", "extraction_source": "rule"}}, + ] + } + ) + + assert summary["rule_claim_count"] == 2 + assert summary["llm_claim_count"] == 0 + assert summary["comparison"]["rule_only"] == 2 + assert summary["comparison"]["rejected_by_validation"] == 4