feat: P&ID 그래프 파이프라인 및 MCP 서버 개선
- P&ID 그래프 파이프라인 구현 (py) - pid_geometric_extractor.py: 기하학적 특징 추출 - pid_intelligent_mapper.py: 태그 매핑 - pid_topology_builder.py: 위상 구축 - test_pipeline_phase2.py, test_pipeline_phase3.py: 테스트 - MCP 서버 개선 - server.py: 멀티프로세싱 지원 - pipeline/: 분석, 추출, 매핑, 위상 모듈 추가 - C# P&ID 그래프 서비스 - PidGraphDtos.cs: DTO 정의 - PidGraphService.cs: 비즈니스 로직 - PidGraphController.cs: API 컨트롤러 - OPC UA 서비스 개선 - ExperionOpcServerService.cs - ExperionRealtimeService.cs - ExperionFastService.cs - MCP 클라이언트 및 호스팅 서비스 개선 - McpClient.cs - McpServerHostedService.cs - 웹 UI 개선 - pid_graph_view.html: P&ID 그래프 뷰어 - pid-viewer.js: 뷰어 로직 - app.js: 메인 앱 - pid_graph.css: 스타일 - 프로젝트 설정 업데이트 - ExperionCrawler.csproj - Program.cs
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ExperionCrawler Unified MCP Server
|
||||
- RAG: Qdrant + Ollama nomic-embed-text + vLLM Qwen3-Coder-Next-FP8
|
||||
- RAG: Qdrant + Ollama nomic-embed-text + vLLM Qwen/Qwen3-Coder-Next-FP8
|
||||
- NL2SQL: 자연어 → LLM SQL 생성 → PostgreSQL 실행
|
||||
- 사용처:
|
||||
stdio 모드 (기본): Claude Code MCP / Roo Code MCP
|
||||
@@ -41,6 +41,15 @@ mcp = FastMCP(
|
||||
stateless_http=True,
|
||||
)
|
||||
|
||||
# Pipeline Imports
|
||||
from pipeline.extractor import PidGeometricExtractor
|
||||
from pipeline.topology import PidTopologyBuilder
|
||||
from pipeline.mapper import IntelligentMapper
|
||||
from pipeline.analyzer import PidAnalysisEngine
|
||||
import networkx as nx
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
# ── 임베딩 (Ollama) ───────────────────────────────────────────────────────────
|
||||
|
||||
def _embed(text: str) -> list[float]:
|
||||
@@ -53,7 +62,7 @@ def _embed(text: str) -> list[float]:
|
||||
resp.raise_for_status()
|
||||
return resp.json()["embedding"]
|
||||
|
||||
# ── LLM (vLLM / Qwen3-Coder-Next-FP8) ───────────────────────────────────────
|
||||
# ── LLM (vLLM / Qwen/Qwen3-Coder-Next-FP8) ─────────────────────────────────────
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _llm():
|
||||
@@ -302,7 +311,7 @@ def search_r530_docs(query: str, top_k: int = 5) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def ask_iiot_llm(question: str, context: str = "") -> str:
|
||||
"""Qwen3-Coder-Next에게 IIoT/OPC UA 질문 (컨텍스트 없이 LLM 직접 질문).
|
||||
"""Qwen/Qwen3-Coder-Next-FP8에게 IIoT/OPC UA 질문 (컨텍스트 없이 LLM 직접 질문).
|
||||
|
||||
사용 시점: search_codebase 또는 search_r530_docs 결과를 context로 넘겨
|
||||
종합 분석·답변이 필요할 때. 또는 일반 IIoT/OPC UA 개념 질문.
|
||||
@@ -331,7 +340,7 @@ def ask_iiot_llm(question: str, context: str = "") -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def rag_query(question: str, search_code: bool = False, search_docs: bool = True) -> str:
|
||||
"""검색 → Qwen3-Coder-Next 답변 생성 (통합 RAG).
|
||||
"""검색 → Qwen/Qwen3-Coder-Next-FP8 답변 생성 (통합 RAG).
|
||||
|
||||
기본값: Experion HS R530 공식 문서만 검색 (search_docs=True, search_code=False).
|
||||
ExperionCrawler 코드도 함께 보려면 search_code=True 추가.
|
||||
@@ -938,6 +947,108 @@ def parse_pid_pdf(filepath: str, use_ocr: bool = True) -> str:
|
||||
return json.dumps({"success": False, "error": f"PDF 파싱 실패: {e}"}, ensure_ascii=False)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def build_pid_graph_parallel(filepath: str) -> str:
|
||||
"""
|
||||
분산 처리 기법을 적용한 P&ID 그래프 생성 툴.
|
||||
전처리 -> 병렬 분산 추출 -> 위상 모델링 -> 저장 과정을 수행합니다.
|
||||
"""
|
||||
try:
|
||||
# 1. 전처리 (Phase 1: Geometric Extraction)
|
||||
extractor = PidGeometricExtractor(filepath)
|
||||
geo_data_path = f"mcp-server/storage/{os.path.basename(filepath)}_geo.json"
|
||||
geo_data_list = extractor.extract_and_save(geo_data_path)
|
||||
|
||||
# geo_data_list는 경로를 반환하므로 다시 로드
|
||||
with open(geo_data_path, 'r', encoding='utf-8') as f:
|
||||
geo_data = json.load(f)
|
||||
|
||||
# 2. 병렬 분산 추출 (Phase 3: Intelligent Mapping)
|
||||
# 시스템 태그 목록 가져오기 (DB에서 조회하는 로직 필요, 여기서는 예시로 빈 리스트 또는 기본값)
|
||||
# 실제로는 get_tag_metadata 등을 통해 전체 태그 리스트를 확보해야 함
|
||||
system_tags = []
|
||||
try:
|
||||
conn = _get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT tagname FROM realtime_table")
|
||||
system_tags = [r[0] for r in cur.fetchall()]
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to fetch system tags: {e}")
|
||||
|
||||
# 그래프 임시 생성 (Mapper가 위상 정보를 사용하므로 필요)
|
||||
builder = PidTopologyBuilder(geo_data)
|
||||
builder.build_graph()
|
||||
|
||||
# Mapper 설정
|
||||
from openai import AsyncOpenAI
|
||||
api_client = AsyncOpenAI(base_url=VLLM_BASE_URL, api_key="dummy")
|
||||
mapper = IntelligentMapper(builder.G, system_tags, api_client=api_client)
|
||||
|
||||
# 분류별 노드 분리
|
||||
nodes = list(builder.G.nodes())
|
||||
transmitter_nodes = [n for n, d in builder.G.nodes(data=True) if d.get('value', '').upper() in ['FIT', 'FT', 'LT', 'PT', 'TE']] # 단순화된 필터
|
||||
valve_nodes = [n for n, d in builder.G.nodes(data=True) if d.get('value', '').upper() in ['FCV', 'LCV', 'TCV', 'PCV', 'XV']]
|
||||
equipment_nodes = [n for n, d in builder.G.nodes(data=True) if d.get('type') not in ['TEXT', 'LINE', 'LWPOLYLINE']]
|
||||
|
||||
# 병렬 호출 (vLLM Batching 유도)
|
||||
tasks = [
|
||||
mapper.extract_transmitters(transmitter_nodes),
|
||||
mapper.extract_valves(valve_nodes),
|
||||
mapper.extract_equipment(equipment_nodes)
|
||||
]
|
||||
extracted_results = await asyncio.gather(*tasks)
|
||||
|
||||
# 결과 통합
|
||||
all_mapped_tags = []
|
||||
for res_dict in extracted_results:
|
||||
for node_id, mapping in res_dict.items():
|
||||
if mapping.resolved_tag != "UNKNOWN":
|
||||
# TopologyBuilder가 기대하는 형식으로 변환
|
||||
node_data = builder.G.nodes[node_id]
|
||||
all_mapped_tags.append({
|
||||
"entity_id": node_id,
|
||||
"tagName": mapping.resolved_tag,
|
||||
"bbox": node_data['bbox'].bounds if hasattr(node_data['bbox'], 'bounds') else node_data['bbox'],
|
||||
"clean_value": mapping.resolved_tag
|
||||
})
|
||||
|
||||
# 3. 최종 위상 모델링 (Phase 2)
|
||||
final_builder = PidTopologyBuilder(geo_data, all_extracted_tags=all_mapped_tags)
|
||||
final_builder.build_graph()
|
||||
|
||||
# 4. 저장
|
||||
graph_id = os.path.basename(filepath).replace(".dxf", "_graph.json")
|
||||
graph_path = f"mcp-server/storage/{graph_id}"
|
||||
final_builder.save_graph(graph_path)
|
||||
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"graph_id": graph_id,
|
||||
"graph_path": graph_path,
|
||||
"nodes": final_builder.G.number_of_nodes(),
|
||||
"edges": final_builder.G.number_of_edges()
|
||||
}, ensure_ascii=False)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"build_pid_graph_parallel failed: {e}")
|
||||
return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False)
|
||||
|
||||
@mcp.tool()
|
||||
def analyze_pid_impact(graph_id: str, start_node_id: str) -> str:
|
||||
"""
|
||||
구축된 그래프를 기반으로 특정 설비 장애 시 영향도 분석을 수행합니다.
|
||||
"""
|
||||
try:
|
||||
graph_path = f"mcp-server/storage/{graph_id}"
|
||||
mapping_path = graph_path.replace("_graph.json", "_mapping.json") # 매핑 파일이 따로 저장된다고 가정
|
||||
|
||||
analyzer = PidAnalysisEngine(graph_path, mapping_path)
|
||||
result = analyzer.analyze_impact(start_node_id)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": f"Impact analysis failed: {e}"}, ensure_ascii=False)
|
||||
|
||||
@mcp.tool()
|
||||
def parse_pid_drawing(filepath: str) -> str:
|
||||
"""확장자 자동 감지하여 P&ID 도면 파싱.
|
||||
|
||||
Reference in New Issue
Block a user