mcp-server warning clear
This commit is contained in:
@@ -10,6 +10,7 @@ ExperionCrawler Unified MCP Server
|
||||
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import httpx
|
||||
@@ -19,19 +20,19 @@ from mcp.server.fastmcp import FastMCP
|
||||
logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
|
||||
|
||||
# ── 설정 ──────────────────────────────────────────────────────────────────────
|
||||
QDRANT_URL = "http://localhost:6333"
|
||||
OLLAMA_URL = "http://localhost:11434"
|
||||
EMBED_MODEL = "nomic-embed-text" # 768-dim, Roo Code 인덱스와 동일
|
||||
VLLM_BASE_URL = "http://localhost:8000/v1"
|
||||
VLLM_MODEL = "Qwen3.6-27B-FP8"
|
||||
QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333")
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
||||
EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text")
|
||||
VLLM_BASE_URL = os.environ.get("VLLM_BASE_URL", "http://localhost:8000/v1")
|
||||
VLLM_MODEL = os.environ.get("VLLM_MODEL", "Qwen3.6-27B-FP8")
|
||||
|
||||
# Qdrant 컬렉션
|
||||
COL_CODEBASE = "ws-65f457145aee80b2" # ExperionCrawler 소스코드
|
||||
COL_OPC_DOCS = "experion-opc-docs" # Experion HS R530 OPC UA 공식 문서 (266 chunks)
|
||||
|
||||
# PostgreSQL 연결
|
||||
DB_CONNECTION_STRING = "postgresql://postgres:postgres@localhost:5432/iiot_platform"
|
||||
DB_TIMEOUT = 10 # 초
|
||||
DB_CONNECTION_STRING = os.environ.get("DB_CONNECTION_STRING", "postgresql://postgres:postgres@localhost:5432/iiot_platform")
|
||||
DB_TIMEOUT = int(os.environ.get("DB_TIMEOUT", "10"))
|
||||
|
||||
# C# McpClient(localhost:5001)와 통신: json_response+stateless로 단순 POST→JSON 방식
|
||||
mcp = FastMCP(
|
||||
@@ -47,158 +48,7 @@ from pipeline.topology import PidTopologyBuilder
|
||||
from pipeline.mapper import IntelligentMapper
|
||||
from pipeline.analyzer import PidAnalysisEngine
|
||||
import networkx as nx
|
||||
import os
|
||||
import asyncio
|
||||
import subprocess
|
||||
import atexit
|
||||
import signal
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
from functools import cache
|
||||
|
||||
# ── ProcessManager ─────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class WorkerProcess:
|
||||
process: subprocess.Popen
|
||||
port: int
|
||||
status: str # "running", "stopped", "error"
|
||||
one_shot: bool = False # 요청 후 프로세스 종료 여부 (P&ID 워커용)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""워커 프로세스 관리자."""
|
||||
|
||||
def __init__(self):
|
||||
self.workers: Dict[str, WorkerProcess] = {}
|
||||
self._locks: Dict[str, asyncio.Lock] = {}
|
||||
self._pid_locks: Dict[str, asyncio.Lock] = {} # 파일/ID별 세부 Lock
|
||||
self._worker_ports = {"rag": 5002, "nl2sql": 5003, "pid": 5004}
|
||||
|
||||
# 정리 훅 등록
|
||||
atexit.register(self._cleanup)
|
||||
signal.signal(signal.SIGTERM, lambda *_: self._cleanup())
|
||||
signal.signal(signal.SIGINT, lambda *_: self._cleanup())
|
||||
|
||||
def _get_available_port(self, worker_type: str) -> int:
|
||||
"""워커 타입에 대한 포트 반환."""
|
||||
return self._worker_ports.get(worker_type, 5002)
|
||||
|
||||
def _classify_tool(self, tool_name: str) -> str:
|
||||
"""도구 이름을 워커 타입으로 분류."""
|
||||
rag_tools = {"search_codebase", "search_r530_docs", "ask_iiot_llm", "rag_query"}
|
||||
nl2sql_tools = {"run_sql", "query_pv_history", "get_tag_metadata", "list_drawings", "query_with_nl"}
|
||||
pid_tools = {
|
||||
"extract_pid_tags", "match_pid_tags", "parse_pid_dxf", "parse_pid_pdf",
|
||||
"parse_pid_drawing", "build_pid_graph_parallel", "analyze_pid_impact"
|
||||
}
|
||||
|
||||
if tool_name in rag_tools:
|
||||
return "rag"
|
||||
elif tool_name in nl2sql_tools:
|
||||
return "nl2sql"
|
||||
elif tool_name in pid_tools:
|
||||
return "pid"
|
||||
else:
|
||||
return "default"
|
||||
|
||||
async def start_worker(self, worker_type: str, one_shot: bool = False) -> WorkerProcess:
|
||||
"""서브 프로세스 시작.
|
||||
|
||||
Args:
|
||||
worker_type: 워커 타입 (rag, nl2sql, pid)
|
||||
one_shot: True일 경우 요청 후 프로세스 종료 (P&ID 워커용)
|
||||
"""
|
||||
port = self._get_available_port(worker_type)
|
||||
cmd = [
|
||||
sys.executable,
|
||||
f"worker/{worker_type}_worker.py",
|
||||
str(port)
|
||||
]
|
||||
|
||||
# 로그 파일 열기
|
||||
log_dir = os.path.join(os.path.dirname(__file__), "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = open(os.path.join(log_dir, f"{worker_type}_worker.log"), "a")
|
||||
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
|
||||
# 헬스체크 루프 (최대 15초 대기)
|
||||
for _ in range(30): # 0.5초 * 30 = 15초
|
||||
await asyncio.sleep(0.5)
|
||||
if proc.poll() is not None:
|
||||
log_file.close()
|
||||
raise RuntimeError(f"{worker_type} 워커가 시작 직후 종료됨")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=1) as client:
|
||||
await client.get(f"http://localhost:{port}/health")
|
||||
break # 헬스체크 성공
|
||||
except Exception:
|
||||
continue
|
||||
else:
|
||||
proc.kill()
|
||||
log_file.close()
|
||||
raise RuntimeError(f"{worker_type} 워커 시작 타임아웃")
|
||||
|
||||
worker = WorkerProcess(
|
||||
process=proc,
|
||||
port=port,
|
||||
status="running",
|
||||
one_shot=one_shot
|
||||
)
|
||||
self.workers[worker_type] = worker
|
||||
log_file.close()
|
||||
return worker
|
||||
|
||||
async def stop_worker(self, worker_type: str):
|
||||
"""서브 프로세스 종료."""
|
||||
if worker_type in self.workers:
|
||||
proc = self.workers[worker_type].process
|
||||
proc.terminate()
|
||||
await asyncio.sleep(0.5)
|
||||
if proc.poll() is None:
|
||||
proc.kill()
|
||||
del self.workers[worker_type]
|
||||
|
||||
async def get_worker(self, tool_name: str, one_shot: bool = False) -> WorkerProcess:
|
||||
"""도구 이름에 해당하는 워커 프로세스 반환 (자동 시작).
|
||||
|
||||
Args:
|
||||
tool_name: 도구 이름
|
||||
one_shot: True일 경우 요청 후 프로세스 종료 (P&ID 워커용)
|
||||
"""
|
||||
worker_type = self._classify_tool(tool_name)
|
||||
|
||||
if worker_type not in self._locks:
|
||||
self._locks[worker_type] = asyncio.Lock()
|
||||
|
||||
async with self._locks[worker_type]:
|
||||
if worker_type not in self.workers:
|
||||
return await self.start_worker(worker_type, one_shot)
|
||||
|
||||
proc = self.workers[worker_type].process
|
||||
if proc.poll() is not None:
|
||||
del self.workers[worker_type]
|
||||
return await self.start_worker(worker_type, one_shot)
|
||||
|
||||
return self.workers[worker_type]
|
||||
|
||||
def _cleanup(self):
|
||||
"""모든 워커 프로세스 정리."""
|
||||
for wtype, worker in list(self.workers.items()):
|
||||
try:
|
||||
worker.process.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
self.workers.clear()
|
||||
|
||||
|
||||
# 전역 ProcessManager 인스턴스
|
||||
process_manager = ProcessManager()
|
||||
|
||||
# ── 임베딩 (Ollama) ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -444,7 +294,7 @@ PostgreSQL 시계열 데이터베이스 스키마
|
||||
|
||||
테이블: tag_metadata (태그 메타데이터 - 변경 드묾)
|
||||
base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124')
|
||||
attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor', ...)
|
||||
attribute TEXT - 속성명 ('desc', 'area')
|
||||
value TEXT - 메타데이터 값
|
||||
node_id TEXT - OPC UA 노드 ID
|
||||
loaded_at TIMESTAMPTZ - 마지막 로드 시각
|
||||
@@ -459,20 +309,16 @@ PostgreSQL 시계열 데이터베이스 스키마
|
||||
instate2 TEXT - 상태 비트 2 (true/false)
|
||||
description TEXT - 장비 설명 (tag_metadata.desc)
|
||||
area TEXT - 소속 플랜트 (tag_metadata.area)
|
||||
state0_descriptor TEXT - 비트 0 의미 (예: "Run/Stop")
|
||||
state1_descriptor TEXT - 비트 1 의미 (예: "Remote/Local")
|
||||
state2_descriptor TEXT - 비트 2 의미 (예: "Trip/Normal")
|
||||
|
||||
새로운 태그 타입:
|
||||
- 아날로그: ficq-6101.pv/sp/op (Double)
|
||||
- 디지털 XV: xv-6124.pv/op (Int32), xv-6124.instate0~7 (Boolean)
|
||||
- Pump: p-6102.pv/op (Int32), p-6102.instate0~7 (Boolean)
|
||||
- 메타데이터: desc (String), area (Enum), state0descriptor~7 (String)
|
||||
- 메타데이터: desc (String), area (Enum)
|
||||
|
||||
BCD 상태 조회 팁:
|
||||
- instate0~7은 Boolean (true/false)
|
||||
- state0descriptor~7은 해당 비트의 의미 설명
|
||||
- instate0=true이고 state0descriptor="Run/Stop"이면 → "Run" 상태
|
||||
- pv 값이 EnumValueType 형식인 경우 `{코드 | DisplayName | }`에서 DisplayName으로 상태 확인 가능
|
||||
- v_tag_summary 뷰를 사용하면 실시간값+메타데이터 한 번에 조회 가능
|
||||
|
||||
N분 간격 집계 공식 (time_bucket 금지, date_trunc 사용):
|
||||
@@ -500,7 +346,7 @@ N분 간격 집계 공식 (time_bucket 금지, date_trunc 사용):
|
||||
# ── RAG 도구 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def search_codebase(query: str, top_k: int = 6) -> str:
|
||||
async def search_codebase(query: str, top_k: int = 6) -> str:
|
||||
"""ExperionCrawler 프로젝트 소스코드 검색 (우리가 개발한 .NET 8 C# 코드).
|
||||
Experion HS R530 공식 문서가 아닌, ExperionCrawler 구현 코드를 검색함.
|
||||
|
||||
@@ -511,11 +357,11 @@ def search_codebase(query: str, top_k: int = 6) -> str:
|
||||
query: 검색어 (예: "OPC UA 구독 시작", "히스토리 스냅샷", "TextToSql 서비스")
|
||||
top_k: 반환 결과 수 (기본 6)
|
||||
"""
|
||||
return _search(COL_CODEBASE, query, top_k)
|
||||
return await _search(COL_CODEBASE, query, top_k)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def search_r530_docs(query: str, top_k: int = 5) -> str:
|
||||
async def search_r530_docs(query: str, top_k: int = 5) -> str:
|
||||
"""Honeywell Experion HS R530 공식 제품 문서 검색.
|
||||
ExperionCrawler 코드가 아닌, Honeywell 공식 HTM 문서를 검색함.
|
||||
|
||||
@@ -526,7 +372,7 @@ def search_r530_docs(query: str, top_k: int = 5) -> str:
|
||||
query: 검색어 (예: "certificate configuration", "endpoint security policy")
|
||||
top_k: 반환 결과 수 (기본 5)
|
||||
"""
|
||||
return _search(COL_OPC_DOCS, query, top_k)
|
||||
return await _search(COL_OPC_DOCS, query, top_k)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -559,7 +405,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:
|
||||
async def rag_query(question: str, search_code: bool = False, search_docs: bool = True) -> str:
|
||||
"""검색 → Qwen3.6-27B-FP8 답변 생성 (통합 RAG).
|
||||
|
||||
기본값: Experion HS R530 공식 문서만 검색 (search_docs=True, search_code=False).
|
||||
@@ -572,9 +418,9 @@ def rag_query(question: str, search_code: bool = False, search_docs: bool = True
|
||||
"""
|
||||
context_parts: list[str] = []
|
||||
if search_docs:
|
||||
context_parts.append(f"=== Experion HS R530 공식 문서 ===\n{_search(COL_OPC_DOCS, question, 4)}")
|
||||
context_parts.append(f"=== Experion HS R530 공식 문서 ===\n{await _search(COL_OPC_DOCS, question, 4)}")
|
||||
if search_code:
|
||||
context_parts.append(f"=== ExperionCrawler 구현 코드 ===\n{_search(COL_CODEBASE, question, 3)}")
|
||||
context_parts.append(f"=== ExperionCrawler 구현 코드 ===\n{await _search(COL_CODEBASE, question, 3)}")
|
||||
return ask_iiot_llm(question, "\n\n".join(context_parts))
|
||||
|
||||
|
||||
@@ -620,7 +466,7 @@ async def run_sql(sql: str) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def query_pv_history(tag_names: list[str], time_from: str, time_to: str, limit: int = 100) -> str:
|
||||
async def query_pv_history(tag_names: list[str], time_from: str, time_to: str, limit: int = 100) -> str:
|
||||
"""과거 값(PV) 히스토리 조회.
|
||||
|
||||
Args:
|
||||
@@ -635,7 +481,7 @@ def query_pv_history(tag_names: list[str], time_from: str, time_to: str, limit:
|
||||
conn = None
|
||||
try:
|
||||
limit = min(limit, 5000)
|
||||
conn = _get_db_connection()
|
||||
conn = await _get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT tagname, recorded_at, value
|
||||
@@ -663,7 +509,7 @@ def query_pv_history(tag_names: list[str], time_from: str, time_to: str, limit:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_tag_metadata(query: str, limit: int = 10) -> str:
|
||||
async def get_tag_metadata(query: str, limit: int = 10) -> str:
|
||||
"""태그 메타데이터 검색 (realtime_table 기반).
|
||||
|
||||
Args:
|
||||
@@ -675,7 +521,7 @@ def get_tag_metadata(query: str, limit: int = 10) -> str:
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = _get_db_connection()
|
||||
conn = await _get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT tagname, livevalue, timestamp, node_id
|
||||
@@ -698,7 +544,7 @@ def get_tag_metadata(query: str, limit: int = 10) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_drawings(unit_no: str | None = None) -> str:
|
||||
async def list_drawings(unit_no: str | None = None) -> str:
|
||||
"""단위별 도면 목록 조회 (node_map_master.name 기반).
|
||||
|
||||
Args:
|
||||
@@ -709,7 +555,7 @@ def list_drawings(unit_no: str | None = None) -> str:
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = _get_db_connection()
|
||||
conn = await _get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
if unit_no:
|
||||
cur.execute(
|
||||
@@ -1248,15 +1094,13 @@ async def build_pid_graph_parallel(filepath: str) -> str:
|
||||
# 실제로는 get_tag_metadata 등을 통해 전체 태그 리스트를 확보해야 함
|
||||
system_tags = []
|
||||
try:
|
||||
def _fetch_system_tags():
|
||||
conn = _get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT tagname FROM realtime_table")
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
system_tags = await asyncio.to_thread(_fetch_system_tags)
|
||||
conn = await _get_db_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT tagname FROM realtime_table")
|
||||
system_tags = [r[0] for r in cur.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to fetch system tags: {e}")
|
||||
|
||||
@@ -1378,223 +1222,6 @@ async def parse_pid_drawing(filepath: str) -> str:
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
# ── 워커 요청 전달 ────────────────────────────────────────────────────────────
|
||||
|
||||
async def _forward_request(port: int, tool_name: str, params: dict, one_shot: bool = False) -> str:
|
||||
"""HTTP를 통해 워커 프로세스로 요청 전달.
|
||||
|
||||
Args:
|
||||
port: 워커 포트
|
||||
tool_name: 도구 이름
|
||||
params: 요청 파라미터
|
||||
one_shot: True일 경우 요청 완료 후 워커 종료
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=600) as client: # 5분 타임아웃 (대용량 DXF 처리용)
|
||||
endpoint = "/execute/one_shot" if one_shot else "/execute"
|
||||
response = await client.post(
|
||||
f"http://localhost:{port}{endpoint}",
|
||||
json={"tool": tool_name, "params": params}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
# ── 요청 라우팅 (워커 프로세스 사용) ───────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
async def search_codebase(query: str, top_k: int = 6) -> str:
|
||||
"""RAG 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("search_codebase")
|
||||
return await _forward_request(worker.port, "search_codebase", {
|
||||
"query": query,
|
||||
"top_k": top_k
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def search_r530_docs(query: str, top_k: int = 5) -> str:
|
||||
"""RAG 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("search_r530_docs")
|
||||
return await _forward_request(worker.port, "search_r530_docs", {
|
||||
"query": query,
|
||||
"top_k": top_k
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def ask_iiot_llm(question: str, context: str = "") -> str:
|
||||
"""RAG 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("ask_iiot_llm")
|
||||
return await _forward_request(worker.port, "ask_iiot_llm", {
|
||||
"question": question,
|
||||
"context": context
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def rag_query(question: str, search_code: bool = False, search_docs: bool = True) -> str:
|
||||
"""RAG 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("rag_query")
|
||||
return await _forward_request(worker.port, "rag_query", {
|
||||
"question": question,
|
||||
"search_code": search_code,
|
||||
"search_docs": search_docs
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def run_sql(sql: str) -> str:
|
||||
"""NL2SQL 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("run_sql")
|
||||
return await _forward_request(worker.port, "run_sql", {"sql": sql})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def query_pv_history(tag_names: list[str], time_from: str, time_to: str, limit: int = 100) -> str:
|
||||
"""NL2SQL 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("query_pv_history")
|
||||
return await _forward_request(worker.port, "query_pv_history", {
|
||||
"tag_names": tag_names,
|
||||
"time_from": time_from,
|
||||
"time_to": time_to,
|
||||
"limit": limit
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_tag_metadata(query: str, limit: int = 10) -> str:
|
||||
"""NL2SQL 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("get_tag_metadata")
|
||||
return await _forward_request(worker.port, "get_tag_metadata", {
|
||||
"query": query,
|
||||
"limit": limit
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_drawings(unit_no: str = None) -> str:
|
||||
"""NL2SQL 워커로 요청 전달."""
|
||||
worker = await process_manager.get_worker("list_drawings")
|
||||
return await _forward_request(worker.port, "list_drawings", {
|
||||
"unit_no": unit_no
|
||||
})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def parse_pid_dxf(filepath: str) -> str:
|
||||
"""P&ID 워커로 요청 전달 (one_shot: 요청 후 종료)."""
|
||||
# 파일 경로 기반으로 Lock 획득하여 동일 파일 중복 처리 방지 및 다른 파일 병렬 처리 허용
|
||||
lock_key = os.path.basename(filepath)
|
||||
if lock_key not in process_manager._pid_locks:
|
||||
process_manager._pid_locks[lock_key] = asyncio.Lock()
|
||||
|
||||
async with process_manager._pid_locks[lock_key]:
|
||||
worker = await process_manager.get_worker("parse_pid_dxf", one_shot=True)
|
||||
return await _forward_request(worker.port, "parse_pid_dxf", {"filepath": filepath}, one_shot=True)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def parse_pid_pdf(filepath: str, use_ocr: bool = True) -> str:
|
||||
"""P&ID 워커로 요청 전달 (one_shot: 요청 후 종료)."""
|
||||
lock_key = os.path.basename(filepath)
|
||||
if lock_key not in process_manager._pid_locks:
|
||||
process_manager._pid_locks[lock_key] = asyncio.Lock()
|
||||
|
||||
async with process_manager._pid_locks[lock_key]:
|
||||
worker = await process_manager.get_worker("parse_pid_pdf", one_shot=True)
|
||||
return await _forward_request(worker.port, "parse_pid_pdf", {
|
||||
"filepath": filepath,
|
||||
"use_ocr": use_ocr
|
||||
}, one_shot=True)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def parse_pid_drawing(filepath: str) -> str:
|
||||
"""P&ID 워커로 요청 전달 (one_shot: 요청 후 종료)."""
|
||||
lock_key = os.path.basename(filepath)
|
||||
if lock_key not in process_manager._pid_locks:
|
||||
process_manager._pid_locks[lock_key] = asyncio.Lock()
|
||||
|
||||
async with process_manager._pid_locks[lock_key]:
|
||||
worker = await process_manager.get_worker("parse_pid_drawing", one_shot=True)
|
||||
return await _forward_request(worker.port, "parse_pid_drawing", {"filepath": filepath}, one_shot=True)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def extract_pid_tags(text: str, source_type: str) -> str:
|
||||
"""P&ID 워커로 요청 전달 (one_shot: 요청 후 종료)."""
|
||||
# 텍스트 추출/매핑은 특정 파일에 종속되지 않으므로 전역 Lock 사용 (또는 세마포어 유지)
|
||||
# 여기서는 단순화를 위해 전역 Lock 하나를 사용하거나,
|
||||
# 텍스트 기반 작업은 병렬 처리가 가능하므로 Lock을 제거할 수도 있으나,
|
||||
# 워커 리소스 보호를 위해 'global_text' 키로 Lock 관리
|
||||
lock_key = "global_text_processing"
|
||||
if lock_key not in process_manager._pid_locks:
|
||||
process_manager._pid_locks[lock_key] = asyncio.Lock()
|
||||
|
||||
async with process_manager._pid_locks[lock_key]:
|
||||
worker = await process_manager.get_worker("extract_pid_tags", one_shot=True)
|
||||
return await _forward_request(worker.port, "extract_pid_tags", {
|
||||
"text": text,
|
||||
"source_type": source_type
|
||||
}, one_shot=True)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def match_pid_tags(pid_tags: list[str], experion_tags: list[str]) -> str:
|
||||
"""P&ID 워커로 요청 전달 (one_shot: 요청 후 종료)."""
|
||||
lock_key = "global_matching"
|
||||
if lock_key not in process_manager._pid_locks:
|
||||
process_manager._pid_locks[lock_key] = asyncio.Lock()
|
||||
|
||||
async with process_manager._pid_locks[lock_key]:
|
||||
worker = await process_manager.get_worker("match_pid_tags", one_shot=True)
|
||||
return await _forward_request(worker.port, "match_pid_tags", {
|
||||
"pid_tags": pid_tags,
|
||||
"experion_tags": experion_tags
|
||||
}, one_shot=True)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def build_pid_graph_parallel(filepath: str) -> str:
|
||||
"""P&ID 워커로 요청 전달 (one_shot: 요청 후 종료)."""
|
||||
lock_key = os.path.basename(filepath)
|
||||
if lock_key not in process_manager._pid_locks:
|
||||
process_manager._pid_locks[lock_key] = asyncio.Lock()
|
||||
|
||||
async with process_manager._pid_locks[lock_key]:
|
||||
worker = await process_manager.get_worker("build_pid_graph_parallel", one_shot=True)
|
||||
return await _forward_request(worker.port, "build_pid_graph_parallel", {"filepath": filepath}, one_shot=True)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def analyze_pid_impact(graph_id: str, start_node_id: str) -> str:
|
||||
"""P&ID 워커로 요청 전달 (one_shot: 요청 후 종료)."""
|
||||
# graph_id 기반으로 Lock 관리
|
||||
lock_key = graph_id
|
||||
if lock_key not in process_manager._pid_locks:
|
||||
process_manager._pid_locks[lock_key] = asyncio.Lock()
|
||||
|
||||
async with process_manager._pid_locks[lock_key]:
|
||||
worker = await process_manager.get_worker("analyze_pid_impact", one_shot=True)
|
||||
return await _forward_request(worker.port, "analyze_pid_impact", {
|
||||
"graph_id": graph_id,
|
||||
"start_node_id": start_node_id
|
||||
}, one_shot=True)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_worker_status() -> str:
|
||||
"""모든 워커 프로세스 상태 조회."""
|
||||
status = {}
|
||||
for name, worker in process_manager.workers.items():
|
||||
status[name] = {
|
||||
"pid": worker.process.pid,
|
||||
"status": worker.status,
|
||||
"port": worker.port,
|
||||
"one_shot": worker.one_shot
|
||||
}
|
||||
return json.dumps(status, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ── 엔트리포인트 ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user