# ๐Ÿง  Graph Pipeline Phase 3: ์ง€๋Šฅํ˜• ๋งคํ•‘ ๋ฐ ๊ฒ€์ฆ (Intelligent Mapping & Validation) ์ด ๋ฌธ์„œ๋Š” P&ID Graph Pipeline์˜ ์„ธ ๋ฒˆ์งธ ๋‹จ๊ณ„์ธ **์ง€๋Šฅํ˜• ๋งคํ•‘ ๋ฐ ๊ฒ€์ฆ**์˜ ์ƒ์„ธ ๊ตฌํ˜„ ๊ณ„ํš์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค. 2๋‹จ๊ณ„์—์„œ ๊ตฌ์ถ•ํ•œ ์œ„์ƒ ๊ทธ๋ž˜ํ”„(Topology Graph)๋ฅผ ํ™œ์šฉํ•˜์—ฌ, ๋„๋ฉด ์ƒ์˜ ๊ฐ€์ƒ ๋…ธ๋“œ๋“ค์„ ์‹ค์ œ Experion ์‹œ์Šคํ…œ์˜ **์‹ค์‹œ๊ฐ„ ํƒœ๊ทธ(Real-time Tags)**์™€ ์ •๋ฐ€ํ•˜๊ฒŒ ์—ฐ๊ฒฐํ•˜๊ณ  ๊ทธ ํƒ€๋‹น์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์ž…๋‹ˆ๋‹ค. --- ## ๐Ÿ“ฆ 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 ํ™•์ •]**์˜ 3๋‹จ๊ณ„ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฑฐ์นฉ๋‹ˆ๋‹ค. 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์—๊ฒŒ ์ „๋‹ฌํ•˜์—ฌ ๊ฐ€์žฅ ํƒ€๋‹นํ•œ ํƒœ๊ทธ๋ฅผ ์„ ํƒํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ### 2.2 ์ƒํ˜ธ ๊ฒ€์ฆ ๋กœ์ง (Cross-Validation) ๋งคํ•‘๋œ ๊ฒฐ๊ณผ๊ฐ€ ์‹ค์ œ ๊ณต์ • ๋ฐ์ดํ„ฐ์™€ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. * **์œ„์ƒ์  ์ผ๊ด€์„ฑ:** ๋„๋ฉด์—์„œ `A $\rightarrow$ B` ์ˆœ์„œ๋ผ๋ฉด, ์‹ค์ œ ๋ฐ์ดํ„ฐ์—์„œ๋„ `A`์˜ ๋ณ€ํ™”๊ฐ€ `B`์— ์˜ํ–ฅ์„ ์ฃผ๋Š”์ง€ ์ƒ๊ด€๊ด€๊ณ„ ๋ถ„์„. * **์†์„ฑ ์ผ์น˜์„ฑ:** ๋„๋ฉด์˜ ์‹ฌ๋ณผ ํƒ€์ž…(์˜ˆ: Pressure Transmitter)๊ณผ ์‹ค์ œ ํƒœ๊ทธ์˜ ์†์„ฑ(์˜ˆ: Engineering Unit = 'bar' ๋˜๋Š” 'psi')์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ. --- ## ๐Ÿ’ป 3. ์‹ค์ œ ๊ตฌํ˜„ ์ฝ”๋”ฉ ๊ฐ€์ด๋“œ (Example) ### 3.1 ๋งฅ๋ฝ ๊ธฐ๋ฐ˜ ๋งคํ•‘ ์—”์ง„ ```python import networkx as nx import asyncio from rapidfuzz import process, fuzz from openai import AsyncOpenAI # ๋น„๋™๊ธฐ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณ€๊ฒฝ 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} ์œ„ ๋งฅ๋ฝ์„ ๊ณ ๋ คํ•  ๋•Œ ๊ฐ€์žฅ ์ ์ ˆํ•œ ์‹œ์Šคํ…œ ํƒœ๊ทธ ํ•˜๋‚˜๋งŒ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. ์ด์œ ๊ฐ€ ๋ถˆ๋ถ„๋ช…ํ•˜๋ฉด 'UNKNOWN'์„ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”. """ response = await client.chat.completions.create( model="gpt-4-turbo", messages=[{"role": "user", "content": prompt}] ) return response.choices[0].message.content # --- ์ „๋ฌธํ™”๋œ Worker ํ•จ์ˆ˜๋“ค (Phase 5 ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๋ฐ˜์˜) --- async def extract_transmitters(self, node_ids): """์ „์†ก๊ธฐ(Transmitter) ์ „๋ฌธ ๋งคํ•‘ Worker""" 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): """๋ฐธ๋ธŒ(Valve) ์ „๋ฌธ ๋งคํ•‘ Worker""" prompt = "๋‹น์‹ ์€ ๋ฐธ๋ธŒ ๋ฐ ์•ก์ถ”์—์ดํ„ฐ ์ „๋ฌธ ์—”์ง€๋‹ˆ์–ด์ž…๋‹ˆ๋‹ค. ๋ฐธ๋ธŒ์˜ ๊ฐœํ ์ƒํƒœ ๋ฐ ์ œ์–ด ํƒœ๊ทธ ๋งคํ•‘์— ํŠนํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค." return {nid: await self._resolve_generic(nid, prompt) for nid in node_ids} async def extract_equipment(self, node_ids): """์ฃผ์š” ์„ค๋น„(Pump, Tank ๋“ฑ) ์ „๋ฌธ ๋งคํ•‘ Worker""" prompt = "๋‹น์‹ ์€ ๊ณต์ • ์„ค๋น„ ์ „๋ฌธ ์—”์ง€๋‹ˆ์–ด์ž…๋‹ˆ๋‹ค. ํŽŒํ”„, ํƒฑํฌ, ์—ด๊ตํ™˜๊ธฐ ๋“ฑ์˜ ๋ฉ”์ธ ์„ค๋น„ ํƒœ๊ทธ ๋งคํ•‘์— ํŠนํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค." return {nid: await self._resolve_generic(nid, prompt) for nid in node_ids} # ์‚ฌ์šฉ ์˜ˆ์‹œ (Phase 5 Orchestrator ๊ด€์ ) async def main(): mapper = IntelligentMapper(graph, ["FIC-101.PV", "PT-101.PV", "P-101.STATUS"]) # ๋ถ„๋ฅ˜๋ณ„๋กœ ๋…ธ๋“œ ๊ทธ๋ฃนํ™” (์˜ˆ์‹œ) transmitter_nodes = ["node_1", "node_2"] valve_nodes = ["node_3", "node_4"] equipment_nodes = ["node_5"] # asyncio.gather๋ฅผ ํ†ตํ•œ ๋ณ‘๋ ฌ ํ˜ธ์ถœ results = await asyncio.gather( mapper.extract_transmitters(transmitter_nodes), mapper.extract_valves(valve_nodes), mapper.extract_equipment(equipment_nodes) ) # ๊ฒฐ๊ณผ ํ†ตํ•ฉ (flatten) final_mapping = {**results[0], **results[1], **results[2]} print(f"Parallel Resolved Mapping: {final_mapping}") asyncio.run(main()) ``` ### 3.2 ๊ฒ€์ฆ ์œ ํ‹ธ๋ฆฌํ‹ฐ: ์†์„ฑ ์ผ์น˜ ํ™•์ธ ```python def validate_mapping(resolved_tag, symbol_type, tag_metadata): """์‹ฌ๋ณผ ํƒ€์ž…๊ณผ ์‹ค์ œ ํƒœ๊ทธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ ์ผ์น˜ ์—ฌ๋ถ€ ๊ฒ€์ฆ""" type_map = { "Pressure Transmitter": ["pressure", "bar", "psi", "pa"], "Flow Meter": ["flow", "m3/h", "lpm"], "Temperature Sensor": ["temp", "celsius", "k"] } expected_keywords = type_map.get(symbol_type, []) actual_desc = tag_metadata.get('description', '').lower() # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค๋ช…์— ๊ธฐ๋Œ€ ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ is_valid = any(kw in actual_desc for kw in expected_keywords) return is_valid ``` --- ## ๐Ÿš€ 4. Phase 3 ์™„๋ฃŒ ๊ธฐ์ค€ (Definition of Done) - [ ] ๋ชจ๋“  ๋„๋ฉด ๋…ธ๋“œ์— ๋Œ€ํ•ด **1์ฐจ ํ›„๋ณด๊ตฐ(Candidates)**์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋Š”๊ฐ€? - [ ] `NetworkX` ๊ทธ๋ž˜ํ”„๋ฅผ ํ†ตํ•ด **์ธ์ ‘ ๋…ธ๋“œ ๋งฅ๋ฝ(Context)**์ด ์ •ํ™•ํžˆ ์ถ”์ถœ๋˜๋Š”๊ฐ€? - [ ] LLM์ด ๋งฅ๋ฝ์„ ๋ฐ˜์˜ํ•˜์—ฌ **์ตœ์ข… ํƒœ๊ทธ๋ฅผ ๊ฒฐ์ •**ํ•˜๊ณ  ๊ทธ ๊ทผ๊ฑฐ๋ฅผ ์ œ์‹œํ•˜๋Š”๊ฐ€? - [ ] ๋งคํ•‘๋œ ํƒœ๊ทธ์˜ **๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(Unit, Description)**์™€ ๋„๋ฉด ์‹ฌ๋ณผ ํƒ€์ž… ๊ฐ„์˜ ์ผ์น˜์„ฑ์ด ๊ฒ€์ฆ๋˜๋Š”๊ฐ€? - [ ] ์ตœ์ข… ๋งคํ•‘ ๊ฒฐ๊ณผ๊ฐ€ `(๋„๋ฉด๋…ธ๋“œID, ์‹œ์Šคํ…œํƒœ๊ทธ, ์‹ ๋ขฐ๋„, ๊ฒ€์ฆ๊ฒฐ๊ณผ)` ํ˜•ํƒœ๋กœ ์ €์žฅ๋˜๋Š”๊ฐ€?