feat: P&ID 연결 분석, LLM 에이전트 모드, KB 확장, MCP 서버 리팩토링

- P&ID: 연결 분석 API, Prefix 규칙 관리, 카테고리 분류, DXF 그래프 빌드
- LLM: 대화 요약, tool card 영구 보존, 시계열 차트(uPlot), 에이전트 모드
- KB: 청크 미리보기, Field Instrument Inference, 인증/Qdrant 클라이언트
- MCP: 서버 기능 확장, 파이프라인 수정, timeout 개선
- Frontend: P&ID UI, LLM UI, KB UI, OPC UA Write 탭 추가
- 설정: AGENTS.md, plant_context, README, opencode.json 업데이트
- 정리: 진단 체크리스트 문서 삭제
This commit is contained in:
windpacer
2026-05-21 23:36:57 +09:00
parent 960bda4a3c
commit 302183c97e
142 changed files with 2432231 additions and 1082 deletions

View File

@@ -13,7 +13,7 @@ class MappingResult(BaseModel):
confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score from 0 to 1")
class IntelligentMapper:
def __init__(self, graph: nx.Graph, system_tags: List[str], api_client: Optional[AsyncOpenAI] = None, model_name: str = "Qwen3.6-27B-FP8"):
def __init__(self, graph: nx.Graph, system_tags: List[str], api_client: Optional[AsyncOpenAI] = None, model_name: str = "Qwen3.6-35B-A3B-FP8"):
self.graph = graph
self.system_tags = system_tags
self.client = api_client

View File

@@ -114,6 +114,12 @@ class PidTopologyBuilder:
grid.add(nid, self.G.nodes[nid]['bbox'])
return grid
# 시그널 레이어 이름 집합 (ELECTRIC SIGNAL, INSTRUMENT signal선 등)
_SIGNAL_LAYERS = frozenset({'ELECTRIC SIGNAL', 'SIGNAL', 'ELEC', 'CABLE', 'WIRE'})
def _relation_for_layer(self, layer: str) -> str:
return 'signal' if (layer or '').upper() in {s.upper() for s in self._SIGNAL_LAYERS} else 'pipe'
def build_graph(self):
# 1. 모든 객체를 노드로 추가
for item in self.data:
@@ -150,7 +156,7 @@ class PidTopologyBuilder:
if best_match:
self.G.add_edge(tag, best_match, relation='associated_with')
# 4. 배관 기반 물리적 연결 (Pipe) — SpatialGrid 사용
# 4. 배관/시그널 기반 연결 — SpatialGrid 사용
lines = [n for n, d in self.G.nodes(data=True) if d['type'] in ['LINE', 'LWPOLYLINE']]
for line_id in lines:
@@ -161,6 +167,8 @@ class PidTopologyBuilder:
coords = original_item['coordinates']
line_geom = LineString(coords)
line_bbox = line_geom.bounds
layer = original_item.get('layer', '')
relation = self._relation_for_layer(layer)
# SpatialGrid로 후보 집합 조회 (O(1) 그리드 셀 기반)
nearby_equipment_ids = eq_grid.query(
@@ -183,27 +191,22 @@ class PidTopologyBuilder:
connected_nodes = list(set(connected_nodes))
if len(connected_nodes) >= 2:
# 개선: 단순 순서가 아닌, 기하학적 좌표 기반의 흐름 방향 추론 (왼쪽 -> 오른쪽, 위 -> 아래 우선)
# 실제 공정 도면의 일반적인 흐름 방향을 반영
node0_bbox = self.G.nodes[connected_nodes[0]]['bbox']
node1_bbox = self.G.nodes[connected_nodes[1]]['bbox']
center0 = ((node0_bbox.bounds[0] + node0_bbox.bounds[2])/2, (node0_bbox.bounds[1] + node0_bbox.bounds[3])/2)
center1 = ((node1_bbox.bounds[0] + node1_bbox.bounds[2])/2, (node1_bbox.bounds[1] + node1_bbox.bounds[3])/2)
# X축 차이가 Y축 차이보다 크면 X축 기준, 아니면 Y축 기준으로 방향 결정
if abs(center1[0] - center0[0]) > abs(center1[1] - center0[1]):
# X축 기준: 왼쪽 -> 오른쪽
if center0[0] < center1[0]:
self.G.add_edge(connected_nodes[0], connected_nodes[1], relation='pipe', flow_direction='forward')
self.G.add_edge(connected_nodes[0], connected_nodes[1], relation=relation, flow_direction='forward')
else:
self.G.add_edge(connected_nodes[1], connected_nodes[0], relation='pipe', flow_direction='forward')
self.G.add_edge(connected_nodes[1], connected_nodes[0], relation=relation, flow_direction='forward')
else:
# Y축 기준: 위 -> 아래 (도면 좌표계에 따라 다를 수 있으나 일반적인 관례 적용)
if center0[1] > center1[1]:
self.G.add_edge(connected_nodes[0], connected_nodes[1], relation='pipe', flow_direction='forward')
self.G.add_edge(connected_nodes[0], connected_nodes[1], relation=relation, flow_direction='forward')
else:
self.G.add_edge(connected_nodes[1], connected_nodes[0], relation='pipe', flow_direction='forward')
self.G.add_edge(connected_nodes[1], connected_nodes[0], relation=relation, flow_direction='forward')
elif len(connected_nodes) == 1:
# 단일 연결의 경우, 일단 엣지를 생성하되 방향은 미정(undirected-like)으로 처리하거나
# 추후 전파 로직에서 결정하도록 함