# 🧠 Graph Pipeline Phase 3: μ§€λŠ₯ν˜• λ§€ν•‘ 및 검증 (Intelligent Mapping & Validation) 이 λ¬Έμ„œλŠ” P&ID Graph Pipeline의 μ„Έ 번째 단계인 **μ§€λŠ₯ν˜• λ§€ν•‘ 및 검증**의 상세 κ΅¬ν˜„ κ³„νšμ„ λ‹€λ£Ήλ‹ˆλ‹€. 2λ‹¨κ³„μ—μ„œ κ΅¬μΆ•ν•œ μœ„μƒ κ·Έλž˜ν”„(Topology Graph)λ₯Ό ν™œμš©ν•˜μ—¬, 도면 μƒμ˜ 가상 λ…Έλ“œλ“€μ„ μ‹€μ œ Experion μ‹œμŠ€ν…œμ˜ **μ‹€μ‹œκ°„ νƒœκ·Έ(Real-time Tags)**와 μ •λ°€ν•˜κ²Œ μ—°κ²°ν•˜κ³  κ·Έ 타당성을 κ²€μ¦ν•˜λŠ” 것이 λͺ©ν‘œμž…λ‹ˆλ‹€. --- ## 🚩 [Supervisor's Audit] κ°λ…μž 진단 κ²°κ³Ό 및 μˆ˜μ • 사항 λ³Έ ν”„λ‘œκ·Έλž¨ 섀계에 λŒ€ν•΄ κ°λ…μž κ΄€μ μ—μ„œ μ •λ°€ 진단을 μˆ˜ν–‰ν•˜μ˜€μœΌλ©°, λ‹€μŒκ³Ό 같은 취약점과 κ°œμ„  사항을 λ°œκ²¬ν•˜μ—¬ λ°˜μ˜ν•˜μ˜€μŠ΅λ‹ˆλ‹€. ### 1. 진단 κ²°κ³Ό (Audit Findings) | ν•­λͺ© | 진단 λ‚΄μš© | 심각도 | μˆ˜μ • λ°©ν–₯ | |---|---|---|---| | **μ—λŸ¬ 처리** | LLM 응닡이 JSON ν˜•μ‹μ΄ μ•„λ‹ˆκ±°λ‚˜ `UNKNOWN`일 λ•Œμ˜ μ˜ˆμ™Έ 처리 둜직 λΆ€μ‘± | HIGH | κ΅¬μ‘°ν™”λœ 좜λ ₯(JSON) κ°•μ œ 및 Fallback μ „λž΅ μΆ”κ°€ | | **μ„±λŠ₯/λΉ„μš©** | λͺ¨λ“  λ…Έλ“œμ— λŒ€ν•΄ κ°œλ³„ LLM 호좜 μ‹œ API λΉ„μš© 급증 및 속도 μ €ν•˜ | MED | 배치(Batch) 처리 및 1μ°¨ 필터링 κ°•ν™” | | **검증 정밀도** | λ‹¨μˆœ ν‚€μ›Œλ“œ λ§€μΉ­ 기반 검증은 μ˜€νƒ(False Positive) κ°€λŠ₯성이 λ†’μŒ | MED | 데이터 νƒ€μž… 및 μ—”μ§€λ‹ˆμ–΄λ§ μœ λ‹›(EU)의 μ—„κ²©ν•œ 비ꡐ 둜직 μΆ”κ°€ | | **데이터 μ •ν•©μ„±** | λ§€ν•‘ 결과의 이λ ₯ 관리 및 μ‚¬λžŒμ΄ μˆ˜λ™μœΌλ‘œ μˆ˜μ •ν•  수 μžˆλŠ” ν”Όλ“œλ°± 루프 λΆ€μž¬ | LOW | λ§€ν•‘ κ²°κ³Ό μ €μž₯ μŠ€ν‚€λ§ˆμ— `confidence` 및 `manual_override` ν•„λ“œ μΆ”κ°€ | ### 2. μˆ˜μ • 이유 (Rationale) - **μ•ˆμ •μ„± 확보:** LLM은 비결정둠적 νŠΉμ„±μ΄ μžˆμœΌλ―€λ‘œ, ν”„λ‘œκ·Έλž¨μ΄ λŸ°νƒ€μž„μ— μ€‘λ‹¨λ˜μ§€ μ•Šλ„λ‘ Pydantic을 μ΄μš©ν•œ μ—„κ²©ν•œ μŠ€ν‚€λ§ˆ 검증이 ν•„μˆ˜μ μž…λ‹ˆλ‹€. - **νš¨μœ¨μ„± μ΅œμ ν™”:** 수천 개의 νƒœκ·Έλ₯Ό κ°œλ³„ ν˜ΈμΆœν•˜λŠ” 것은 λΉ„νš¨μœ¨μ μž…λ‹ˆλ‹€. μœ μ‚¬λ„ 기반으둜 후보ꡰ을 쒁히고, μœ μ‚¬ 그룹을 λ¬Άμ–΄ 배치 μ²˜λ¦¬ν•¨μœΌλ‘œμ¨ λΉ„μš©μ„ μ ˆκ°ν•©λ‹ˆλ‹€. - **신뒰도 ν–₯상:** λ‹¨μˆœ ν…μŠ€νŠΈ 맀칭을 λ„˜μ–΄ μ‹€μ œ μ‹œμŠ€ν…œμ˜ 메타데이터(Unit, Range λ“±)λ₯Ό ꡐ차 검증해야 μ—”μ§€λ‹ˆμ–΄λ§ κ΄€μ μ—μ„œ μ‹ λ’°ν•  수 μžˆλŠ” κ²°κ³Όκ°€ λ©λ‹ˆλ‹€. --- ## πŸ“¦ 1. ν•„μˆ˜ νŒ¨ν‚€μ§€ 및 ν™˜κ²½ μ„€μ • ### 1.1 Python νŒ¨ν‚€μ§€ | νŒ¨ν‚€μ§€ | μš©λ„ | λΉ„κ³  | |---|---|---| | `openai` / `langchain` | LLM API 연동 및 ν”„λ‘¬ν”„νŠΈ 체이닝 | λ§€ν•‘ μΆ”λ‘  및 검증 핡심 | | `fuzzywuzzy` / `rapidfuzz` | νƒœκ·Έ 이름 κ°„μ˜ λ¬Έμžμ—΄ μœ μ‚¬λ„ 계산 | 1μ°¨ 후보ꡰ μΆ”μΆœμš© | | `networkx` | κ·Έλž˜ν”„ 기반 인접 λ…Έλ“œ(Context) μΆ”μΆœ | 2단계 κ·Έλž˜ν”„ ν™œμš© | | `pydantic` | λ§€ν•‘ 결과의 ꡬ쑰화 및 μœ νš¨μ„± 검사 | **[κ°•ν™”]** 데이터 μ •κ·œν™” 및 λŸ°νƒ€μž„ νƒ€μž… 체크 | | `requests` | ExperionCrawler API (C#)와 톡신 | μ‹€μ œ νƒœκ·Έ 리슀트 쑰회 | ### 1.2 μ„€μΉ˜ λͺ…λ Ήμ–΄ ```bash pip install openai langchain rapidfuzz networkx pydantic requests ``` --- ## πŸ“ 2. 상세 섀계 ꡬ쑰 ### 2.1 λ§€ν•‘ νŒŒμ΄ν”„λΌμΈ (Mapping Pipeline) λ‹¨μˆœ 이름 맀칭의 ν•œκ³„λ₯Ό κ·Ήλ³΅ν•˜κΈ° μœ„ν•΄ **[후보 μΆ”μΆœ $\rightarrow$ λ§₯락 뢄석 $\rightarrow$ LLM ν™•μ • $\rightarrow$ μŠ€ν‚€λ§ˆ 검증]**의 4단계 ν”„λ‘œμ„ΈμŠ€λ₯Ό κ±°μΉ©λ‹ˆλ‹€. 1. **1μ°¨ 후보 μΆ”μΆœ (Candidate Generation):** * λ„λ©΄μ˜ νƒœκ·Έ ν…μŠ€νŠΈμ™€ Experion μ‹œμŠ€ν…œμ˜ 전체 νƒœκ·Έ 리슀트λ₯Ό `RapidFuzz`둜 λΉ„κ΅ν•˜μ—¬ μœ μ‚¬λ„ μƒμœ„ N개λ₯Ό μΆ”μΆœν•©λ‹ˆλ‹€. 2. **λ§₯락 정보 μˆ˜μ§‘ (Context Gathering):** * ν•΄λ‹Ή λ…Έλ“œμ˜ κ·Έλž˜ν”„ 상 인접 λ…Έλ“œ(1-hop, 2-hop) 정보λ₯Ό μˆ˜μ§‘ν•©λ‹ˆλ‹€. * 예: "ν˜„μž¬ λ…Έλ“œλŠ” `PT-101`이며, 상λ₯˜μ— `P-101(Pump)`이 있고 ν•˜λ₯˜μ— `V-101(Valve)`이 있음." 3. **LLM 기반 μ΅œμ’… λ§€ν•‘ (LLM-based Resolution):** * 후보 νƒœκ·Έ λ¦¬μŠ€νŠΈμ™€ μœ„μƒ λ§₯락을 LLMμ—κ²Œ μ „λ‹¬ν•˜μ—¬ κ°€μž₯ νƒ€λ‹Ήν•œ νƒœκ·Έλ₯Ό μ„ νƒν•˜κ²Œ ν•©λ‹ˆλ‹€. * **[κ°œμ„ ]** JSON Modeλ₯Ό μ‚¬μš©ν•˜μ—¬ `{"tag": "...", "reason": "...", "confidence": 0.9}` ν˜•νƒœλ‘œ 응닡을 κ°•μ œν•©λ‹ˆλ‹€. 4. **ꡬ쑰적 검증 (Structural Validation):** * Pydantic λͺ¨λΈμ„ 톡해 LLM μ‘λ‹΅μ˜ ν˜•μ‹μ„ κ²€μ¦ν•˜κ³ , μ‹€νŒ¨ μ‹œ `UNKNOWN` 처리 및 둜그λ₯Ό λ‚¨κΉλ‹ˆλ‹€. ### 2.2 μƒν˜Έ 검증 둜직 (Cross-Validation) λ§€ν•‘λœ κ²°κ³Όκ°€ μ‹€μ œ 곡정 데이터와 μΌμΉ˜ν•˜λŠ”μ§€ κ²€μ¦ν•©λ‹ˆλ‹€. * **μœ„μƒμ  일관성:** λ„λ©΄μ—μ„œ `A $\rightarrow$ B` μˆœμ„œλΌλ©΄, μ‹€μ œ λ°μ΄ν„°μ—μ„œλ„ `A`의 λ³€ν™”κ°€ `B`에 영ν–₯을 μ£ΌλŠ”μ§€ 상관관계 뢄석. * **속성 μΌμΉ˜μ„±:** λ„λ©΄μ˜ 심볼 νƒ€μž…(예: Pressure Transmitter)κ³Ό μ‹€μ œ νƒœκ·Έμ˜ 속성(예: Engineering Unit = 'bar' λ˜λŠ” 'psi')이 μΌμΉ˜ν•˜λŠ”μ§€ 확인. **[κ°•ν™”]** λ‹¨μˆœ ν‚€μ›Œλ“œκ°€ μ•„λ‹Œ Unit λ§€ν•‘ ν…Œμ΄λΈ”μ„ ν†΅ν•œ μ—„κ²©ν•œ 비ꡐ. --- ## πŸ’» 3. μ‹€μ œ κ΅¬ν˜„ μ½”λ”© κ°€μ΄λ“œ (Example) ### 3.1 λ§₯락 기반 λ§€ν•‘ μ—”μ§„ ```python import networkx as nx import asyncio import json from typing import List, Optional from pydantic import BaseModel, Field from rapidfuzz import process, fuzz from openai import AsyncOpenAI # --- [μΆ”κ°€] 응닡 ꡬ쑰화λ₯Ό μœ„ν•œ Pydantic λͺ¨λΈ --- class MappingResult(BaseModel): resolved_tag: str = Field(..., description="The final mapped system tag") reason: str = Field(..., description="Reason for this mapping based on context") confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score from 0 to 1") client = AsyncOpenAI(api_key="your-api-key") class IntelligentMapper: def __init__(self, graph, system_tags): self.graph = graph # Phase 2μ—μ„œ μƒμ„±λœ NetworkX κ·Έλž˜ν”„ self.system_tags = system_tags # Experion μ‹œμŠ€ν…œμ˜ 전체 νƒœκ·Έ 리슀트 def get_node_context(self, node_id): """λ…Έλ“œμ˜ μ£Όλ³€ μœ„μƒ 정보λ₯Ό ν…μŠ€νŠΈλ‘œ λ³€ν™˜""" neighbors = list(self.graph.neighbors(node_id)) context = [] for n in neighbors: attr = self.graph.nodes[n] context.append(f"Connected to {attr.get('value', n)} (Type: {attr.get('type')})") return ", ".join(context) async def _resolve_generic(self, node_id, category_prompt): """곡톡 λ§€ν•‘ 둜직 (비동기 + ꡬ쑰화 응닡)""" tag_text = self.graph.nodes[node_id].get('value', '') candidates = process.extract(tag_text, self.system_tags, scorer=fuzz.WRatio, limit=5) context = self.get_node_context(node_id) prompt = f""" {category_prompt} P&ID λ„λ©΄μ˜ νƒœκ·Έ '{tag_text}'λ₯Ό μ‹€μ œ μ‹œμŠ€ν…œ νƒœκ·Έμ™€ λ§€ν•‘ν•΄μ•Ό ν•©λ‹ˆλ‹€. μœ„μƒ λ§₯락: {context} 후보 리슀트: {candidates} λ°˜λ“œμ‹œ λ‹€μŒ JSON ν˜•μ‹μœΌλ‘œλ§Œ μ‘λ‹΅ν•˜μ„Έμš”: {{ "resolved_tag": "νƒœκ·Έλͺ… λ˜λŠ” UNKNOWN", "reason": "λ§€ν•‘ 이유", "confidence": 0.0~1.0 }} """ try: response = await client.chat.completions.create( model="gpt-4-turbo", messages=[{"role": "user", "content": prompt}], response_format={ "type": "json_object" } # JSON λͺ¨λ“œ κ°•μ œ ) raw_content = response.choices[0].message.content # Pydantic을 ν†΅ν•œ μœ νš¨μ„± 검사 return MappingResult.model_validate_json(raw_content) except Exception as e: print(f"Error resolving node {node_id}: {e}") return MappingResult(resolved_tag="UNKNOWN", reason=f"Error: {str(e)}", confidence=0.0) # --- μ „λ¬Έν™”λœ Worker ν•¨μˆ˜λ“€ --- async def extract_transmitters(self, node_ids): prompt = "당신은 계츑기 μ „λ¬Έ μ—”μ§€λ‹ˆμ–΄μž…λ‹ˆλ‹€. 특히 Pressure/Flow/Level Transmitter 맀핑에 νŠΉν™”λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€." return {nid: await self._resolve_generic(nid, prompt) for nid in node_ids} async def extract_valves(self, node_ids): prompt = "당신은 밸브 및 앑좔에이터 μ „λ¬Έ μ—”μ§€λ‹ˆμ–΄μž…λ‹ˆλ‹€. 밸브의 개폐 μƒνƒœ 및 μ œμ–΄ νƒœκ·Έ 맀핑에 νŠΉν™”λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€." return {nid: await self._resolve_generic(nid, prompt) for nid in node_ids} async def extract_equipment(self, node_ids): prompt = "당신은 곡정 μ„€λΉ„ μ „λ¬Έ μ—”μ§€λ‹ˆμ–΄μž…λ‹ˆλ‹€. νŽŒν”„, 탱크, μ—΄κ΅ν™˜κΈ° λ“±μ˜ 메인 μ„€λΉ„ νƒœκ·Έ 맀핑에 νŠΉν™”λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€." return {nid: await self._resolve_generic(nid, prompt) for nid in node_ids} # μ‚¬μš© μ˜ˆμ‹œ async def main(): # 가상 데이터 graph = nx.Graph() graph.add_node("node_1", value="PT-101", type="Pressure Transmitter") graph.add_node("node_2", value="P-101", type="Pump") graph.add_edge("node_1", "node_2") mapper = IntelligentMapper(graph, ["PT-101.PV", "PT-102.PV", "P-101.STATUS"]) results = await asyncio.gather( mapper.extract_transmitters(["node_1"]), mapper.extract_equipment(["node_2"]) ) final_mapping = {**results[0], **results[1]} print(f"Parallel Resolved Mapping: {final_mapping}") asyncio.run(main()) ``` ### 3.2 검증 μœ ν‹Έλ¦¬ν‹°: 속성 일치 확인 (κ°•ν™” 버전) ```python def validate_mapping(resolved_tag, symbol_type, tag_metadata): """심볼 νƒ€μž…κ³Ό μ‹€μ œ νƒœκ·Έ λ©”νƒ€λ°μ΄ν„°μ˜ μ—„κ²©ν•œ 일치 μ—¬λΆ€ 검증""" # λ‹¨μˆœ ν‚€μ›Œλ“œκ°€ μ•„λ‹Œ ν—ˆμš© λ‹¨μœ„(Unit) μ •μ˜ unit_map = { "Pressure Transmitter": ["bar", "psi", "kPa", "Pa"], "Flow Meter": ["m3/h", "lpm", "kg/h"], "Temperature Sensor": ["Β°C", "C", "K", "Β°F"] } actual_unit = tag_metadata.get('unit', '').strip() allowed_units = unit_map.get(symbol_type, []) # 1. λ‹¨μœ„ 일치 확인 (μ΅œμš°μ„ ) if actual_unit and actual_unit in allowed_units: return True, "Unit Match" # 2. λ‹¨μœ„κ°€ μ—†λŠ” 경우 μ„€λͺ…(Description) 기반 2μ°¨ 검증 actual_desc = tag_metadata.get('description', '').lower() expected_keywords = { "Pressure Transmitter": ["pressure", "press"], "Flow Meter": ["flow", "flowrate"], "Temperature Sensor": ["temp", "temperature"] } keywords = expected_keywords.get(symbol_type, []) if any(kw in actual_desc for kw in keywords): return True, "Description Match (Unit Missing)" return False, "Mismatch: Symbol type and Tag metadata do not align" ``` --- ## πŸš€ 4. Phase 3 μ™„λ£Œ κΈ°μ€€ (Definition of Done) - [ ] λͺ¨λ“  도면 λ…Έλ“œμ— λŒ€ν•΄ **1μ°¨ 후보ꡰ(Candidates)**이 μžλ™μœΌλ‘œ μƒμ„±λ˜λŠ”κ°€? - [ ] `NetworkX` κ·Έλž˜ν”„λ₯Ό 톡해 **인접 λ…Έλ“œ λ§₯락(Context)**이 μ •ν™•νžˆ μΆ”μΆœλ˜λŠ”κ°€? - [ ] LLM이 **JSON ν˜•μ‹**으둜 μ΅œμ’… νƒœκ·Έλ₯Ό κ²°μ •ν•˜κ³ , κ·Έ 근거와 신뒰도λ₯Ό μ œμ‹œν•˜λŠ”κ°€? - [ ] **Pydantic**을 톡해 LLM μ‘λ‹΅μ˜ ꡬ쑰적 μœ νš¨μ„±μ΄ κ²€μ¦λ˜λŠ”κ°€? - [ ] λ§€ν•‘λœ νƒœκ·Έμ˜ **μ—”μ§€λ‹ˆμ–΄λ§ μœ λ‹›(Unit)**κ³Ό 도면 심볼 νƒ€μž… κ°„μ˜ μΌμΉ˜μ„±μ΄ μ—„κ²©νžˆ κ²€μ¦λ˜λŠ”κ°€? - [ ] μ΅œμ’… λ§€ν•‘ κ²°κ³Όκ°€ `(λ„λ©΄λ…Έλ“œID, μ‹œμŠ€ν…œνƒœκ·Έ, 신뒰도, 검증결과, λ§€ν•‘κ·Όκ±°)` ν˜•νƒœλ‘œ μ €μž₯λ˜λŠ”κ°€?