pyDMNrules-enhanced 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyDMNrules/DMNrules.py +6585 -0
- pyDMNrules/__init__.py +2 -0
- pydmnrules_enhanced-1.5.0.dist-info/METADATA +358 -0
- pydmnrules_enhanced-1.5.0.dist-info/RECORD +10 -0
- pydmnrules_enhanced-1.5.0.dist-info/WHEEL +5 -0
- pydmnrules_enhanced-1.5.0.dist-info/licenses/LICENSE +24 -0
- pydmnrules_enhanced-1.5.0.dist-info/licenses/LICENSE_MCP +22 -0
- pydmnrules_enhanced-1.5.0.dist-info/top_level.txt +2 -0
- pydmnrules_mcp/__init__.py +28 -0
- pydmnrules_mcp/server.py +632 -0
pydmnrules_mcp/server.py
ADDED
@@ -0,0 +1,632 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
pyDMNrules MCP Server - DMN Decision Engine with FastMCP
|
4
|
+
|
5
|
+
pyDMNrules 엔진을 사용하여 DMN XML 파일을 로드하고 의사결정을 실행하는 MCP 서버입니다.
|
6
|
+
DMN XML 파일을 관리하고, 규칙별 입력 스키마를 제공하며, LLM이 적절한 형태로 추론을 호출할 수 있도록 합니다.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import asyncio
|
10
|
+
import json
|
11
|
+
import os
|
12
|
+
import time
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any, Dict, List, Optional, Union
|
15
|
+
|
16
|
+
import aiofiles
|
17
|
+
from fastmcp import FastMCP
|
18
|
+
from pydantic import BaseModel, Field
|
19
|
+
|
20
|
+
# pyDMNrules import
|
21
|
+
try:
|
22
|
+
from pyDMNrules import DMN
|
23
|
+
PYDMNRULES_AVAILABLE = True
|
24
|
+
except ImportError:
|
25
|
+
PYDMNRULES_AVAILABLE = False
|
26
|
+
print("Error: pyDMNrules not available. Please install it: pip install pydmnrules-enhanced")
|
27
|
+
|
28
|
+
|
29
|
+
class DecisionResult(BaseModel):
|
30
|
+
"""의사결정 결과를 나타내는 모델"""
|
31
|
+
result: Dict[str, Any] = Field(description="의사결정 결과")
|
32
|
+
trace: List[Dict[str, Any]] = Field(description="의사결정 경로 추적")
|
33
|
+
input_context: Dict[str, Any] = Field(description="입력 컨텍스트 (key-value)")
|
34
|
+
rule_name: str = Field(description="사용된 규칙 이름")
|
35
|
+
execution_time: Optional[float] = Field(None, description="실행 시간 (초)")
|
36
|
+
rule_schema: Optional[Dict[str, Any]] = Field(None, description="규칙별 입력 스키마")
|
37
|
+
engine_used: Optional[str] = Field(None, description="사용된 엔진 (pyDMNrules)")
|
38
|
+
|
39
|
+
|
40
|
+
class PyDMNrulesEngine:
|
41
|
+
"""pyDMNrules 엔진 - DMN XML 파싱 및 실행"""
|
42
|
+
|
43
|
+
def __init__(self):
|
44
|
+
self.dmn_models: Dict[str, DMN] = {}
|
45
|
+
self.model_metadata: Dict[str, Dict[str, Any]] = {}
|
46
|
+
|
47
|
+
def load_dmn_xml(self, rule_name: str, xml_content: str) -> Dict[str, Any]:
|
48
|
+
"""DMN XML을 로드하고 규칙 엔진을 구축합니다"""
|
49
|
+
|
50
|
+
if not PYDMNRULES_AVAILABLE:
|
51
|
+
raise RuntimeError("pyDMNrules is not available. Please install it.")
|
52
|
+
|
53
|
+
try:
|
54
|
+
# DMN 모델 인스턴스 생성
|
55
|
+
dmn_model = DMN()
|
56
|
+
|
57
|
+
# XML 문자열로부터 모델 로드
|
58
|
+
status = dmn_model.useXML(xml_content)
|
59
|
+
|
60
|
+
# 에러 체크
|
61
|
+
if 'errors' in status and len(status['errors']) > 0:
|
62
|
+
return {
|
63
|
+
"status": "error",
|
64
|
+
"message": f"Failed to load rule '{rule_name}'",
|
65
|
+
"errors": status['errors']
|
66
|
+
}
|
67
|
+
|
68
|
+
# 모델 저장
|
69
|
+
self.dmn_models[rule_name] = dmn_model
|
70
|
+
|
71
|
+
# 메타데이터 추출 - Glossary로부터 입력/출력 변수 정보 추출
|
72
|
+
metadata = self._extract_metadata(dmn_model, rule_name)
|
73
|
+
self.model_metadata[rule_name] = metadata
|
74
|
+
|
75
|
+
return {
|
76
|
+
"status": "success",
|
77
|
+
"message": f"Rule '{rule_name}' loaded successfully with pyDMNrules",
|
78
|
+
"engine_type": "pyDMNrules",
|
79
|
+
"inputs": metadata.get("inputs", {}),
|
80
|
+
"outputs": metadata.get("outputs", {}),
|
81
|
+
"decision_tables": metadata.get("decision_tables", [])
|
82
|
+
}
|
83
|
+
|
84
|
+
except Exception as e:
|
85
|
+
return {
|
86
|
+
"status": "error",
|
87
|
+
"message": f"Failed to load DMN XML: {str(e)}",
|
88
|
+
"errors": [str(e)]
|
89
|
+
}
|
90
|
+
|
91
|
+
def _extract_metadata(self, dmn_model: DMN, rule_name: str) -> Dict[str, Any]:
|
92
|
+
"""DMN 모델로부터 메타데이터를 추출합니다"""
|
93
|
+
metadata = {
|
94
|
+
"loaded_at": time.time(),
|
95
|
+
"engine_type": "pyDMNrules",
|
96
|
+
"rule_name": rule_name,
|
97
|
+
"inputs": {},
|
98
|
+
"outputs": {},
|
99
|
+
"decision_tables": [],
|
100
|
+
"glossary": {},
|
101
|
+
"variable_names": [] # LLM이 사용해야 할 정확한 변수 이름 목록
|
102
|
+
}
|
103
|
+
|
104
|
+
# Glossary 정보 추출
|
105
|
+
if hasattr(dmn_model, 'glossary') and dmn_model.glossary:
|
106
|
+
for variable, info in dmn_model.glossary.items():
|
107
|
+
item = info.get('item', '')
|
108
|
+
concept = info.get('concept', '')
|
109
|
+
|
110
|
+
glossary_entry = {
|
111
|
+
"variable": variable,
|
112
|
+
"item": item,
|
113
|
+
"concept": concept,
|
114
|
+
"type": "string", # 기본 타입 (pyDMNrules에서 타입 정보가 제한적임)
|
115
|
+
"feel_name": item # FEEL 표현식에서 사용되는 이름
|
116
|
+
}
|
117
|
+
|
118
|
+
metadata["glossary"][variable] = glossary_entry
|
119
|
+
metadata["variable_names"].append(variable)
|
120
|
+
|
121
|
+
# 입력/출력 구분은 decision tables를 분석해야 정확하지만,
|
122
|
+
# 일단 모든 glossary 항목을 입력으로 간주
|
123
|
+
metadata["inputs"][variable] = glossary_entry
|
124
|
+
|
125
|
+
# Decision Tables 정보 추출
|
126
|
+
if hasattr(dmn_model, 'decisionTables') and dmn_model.decisionTables:
|
127
|
+
for table_name, table_info in dmn_model.decisionTables.items():
|
128
|
+
decision_table = {
|
129
|
+
"name": table_name,
|
130
|
+
"hit_policy": table_info.get('hitPolicy', 'U'),
|
131
|
+
"description": table_info.get('name', table_name)
|
132
|
+
}
|
133
|
+
metadata["decision_tables"].append(decision_table)
|
134
|
+
|
135
|
+
# Decisions 정보 추출
|
136
|
+
if hasattr(dmn_model, 'decisions') and dmn_model.decisions:
|
137
|
+
metadata["decisions"] = []
|
138
|
+
for decision in dmn_model.decisions:
|
139
|
+
if isinstance(decision, (list, tuple)) and len(decision) > 2:
|
140
|
+
metadata["decisions"].append({
|
141
|
+
"description": decision[1] if len(decision) > 1 else "",
|
142
|
+
"table": decision[2] if len(decision) > 2 else ""
|
143
|
+
})
|
144
|
+
|
145
|
+
return metadata
|
146
|
+
|
147
|
+
def execute_dmn_rule(self, rule_name: str, input_context: Dict[str, Any]) -> DecisionResult:
|
148
|
+
"""DMN 규칙을 실행합니다"""
|
149
|
+
start_time = time.time()
|
150
|
+
|
151
|
+
if rule_name not in self.dmn_models:
|
152
|
+
execution_time = time.time() - start_time
|
153
|
+
return DecisionResult(
|
154
|
+
result={"error": f"Rule '{rule_name}' not loaded"},
|
155
|
+
trace=[{"step": 1, "rule": "error", "condition": "rule_not_loaded", "result": "error"}],
|
156
|
+
input_context=input_context,
|
157
|
+
rule_name=rule_name,
|
158
|
+
execution_time=execution_time,
|
159
|
+
rule_schema={"error": "Rule not loaded"},
|
160
|
+
engine_used="pyDMNrules"
|
161
|
+
)
|
162
|
+
|
163
|
+
try:
|
164
|
+
dmn_model = self.dmn_models[rule_name]
|
165
|
+
|
166
|
+
# pyDMNrules의 decide() 함수 호출
|
167
|
+
(status, result_data) = dmn_model.decide(input_context)
|
168
|
+
|
169
|
+
# 에러 체크
|
170
|
+
if 'errors' in status and len(status['errors']) > 0:
|
171
|
+
execution_time = time.time() - start_time
|
172
|
+
return DecisionResult(
|
173
|
+
result={"error": "Decision execution failed", "details": status['errors']},
|
174
|
+
trace=[{"step": 1, "error": str(status['errors'])}],
|
175
|
+
input_context=input_context,
|
176
|
+
rule_name=rule_name,
|
177
|
+
execution_time=execution_time,
|
178
|
+
rule_schema=self.model_metadata.get(rule_name, {}),
|
179
|
+
engine_used="pyDMNrules"
|
180
|
+
)
|
181
|
+
|
182
|
+
# 결과 파싱 및 trace 생성
|
183
|
+
trace = self._build_trace(result_data, input_context)
|
184
|
+
parsed_result = self._parse_result(result_data)
|
185
|
+
|
186
|
+
execution_time = time.time() - start_time
|
187
|
+
|
188
|
+
return DecisionResult(
|
189
|
+
result=parsed_result,
|
190
|
+
trace=trace,
|
191
|
+
input_context=input_context,
|
192
|
+
rule_name=rule_name,
|
193
|
+
execution_time=execution_time,
|
194
|
+
rule_schema=self.model_metadata.get(rule_name, {}),
|
195
|
+
engine_used="pyDMNrules"
|
196
|
+
)
|
197
|
+
|
198
|
+
except Exception as e:
|
199
|
+
execution_time = time.time() - start_time
|
200
|
+
return DecisionResult(
|
201
|
+
result={"error": f"Rule execution failed: {str(e)}"},
|
202
|
+
trace=[{"step": 1, "rule": "error", "condition": "execution_failed", "result": str(e)}],
|
203
|
+
input_context=input_context,
|
204
|
+
rule_name=rule_name,
|
205
|
+
execution_time=execution_time,
|
206
|
+
rule_schema=self.model_metadata.get(rule_name, {}),
|
207
|
+
engine_used="pyDMNrules"
|
208
|
+
)
|
209
|
+
|
210
|
+
def _parse_result(self, result_data: Any) -> Dict[str, Any]:
|
211
|
+
"""pyDMNrules 결과를 파싱합니다"""
|
212
|
+
|
213
|
+
# result_data는 단일 decision dict이거나 decision dict의 list일 수 있음
|
214
|
+
if isinstance(result_data, list):
|
215
|
+
# 여러 decision tables가 실행된 경우
|
216
|
+
# 마지막 결과를 primary result로, 전체를 all_results로 저장
|
217
|
+
all_results = []
|
218
|
+
for item in result_data:
|
219
|
+
if isinstance(item, dict) and 'Result' in item:
|
220
|
+
all_results.append(item['Result'])
|
221
|
+
|
222
|
+
return {
|
223
|
+
"final_result": all_results[-1] if all_results else {},
|
224
|
+
"all_results": all_results,
|
225
|
+
"decision_count": len(all_results)
|
226
|
+
}
|
227
|
+
|
228
|
+
elif isinstance(result_data, dict):
|
229
|
+
# 단일 decision table 결과
|
230
|
+
if 'Result' in result_data:
|
231
|
+
return result_data['Result']
|
232
|
+
else:
|
233
|
+
return result_data
|
234
|
+
|
235
|
+
else:
|
236
|
+
return {"raw_result": str(result_data)}
|
237
|
+
|
238
|
+
def _build_trace(self, result_data: Any, input_context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
239
|
+
"""의사결정 경로 추적 정보를 생성합니다"""
|
240
|
+
trace = []
|
241
|
+
|
242
|
+
# 입력 정보 추가
|
243
|
+
trace.append({
|
244
|
+
"step": 1,
|
245
|
+
"action": "input",
|
246
|
+
"data": input_context,
|
247
|
+
"description": "Input context provided"
|
248
|
+
})
|
249
|
+
|
250
|
+
# result_data 분석
|
251
|
+
if isinstance(result_data, list):
|
252
|
+
# 여러 decision tables가 실행된 경우
|
253
|
+
for idx, item in enumerate(result_data):
|
254
|
+
step_info = {
|
255
|
+
"step": idx + 2,
|
256
|
+
"action": "decision_table",
|
257
|
+
"index": idx
|
258
|
+
}
|
259
|
+
|
260
|
+
if isinstance(item, dict):
|
261
|
+
if 'Executed Rule' in item:
|
262
|
+
executed_rule = item['Executed Rule']
|
263
|
+
if isinstance(executed_rule, tuple) and len(executed_rule) >= 3:
|
264
|
+
step_info["description"] = executed_rule[0]
|
265
|
+
step_info["table"] = executed_rule[1]
|
266
|
+
step_info["rule_id"] = executed_rule[2]
|
267
|
+
|
268
|
+
if 'Result' in item:
|
269
|
+
step_info["result"] = item['Result']
|
270
|
+
|
271
|
+
if 'RuleAnnotations' in item:
|
272
|
+
step_info["annotations"] = item['RuleAnnotations']
|
273
|
+
|
274
|
+
trace.append(step_info)
|
275
|
+
|
276
|
+
elif isinstance(result_data, dict):
|
277
|
+
# 단일 decision table 결과
|
278
|
+
step_info = {
|
279
|
+
"step": 2,
|
280
|
+
"action": "decision_table"
|
281
|
+
}
|
282
|
+
|
283
|
+
if 'Executed Rule' in result_data:
|
284
|
+
executed_rule = result_data['Executed Rule']
|
285
|
+
if isinstance(executed_rule, tuple) and len(executed_rule) >= 3:
|
286
|
+
step_info["description"] = executed_rule[0]
|
287
|
+
step_info["table"] = executed_rule[1]
|
288
|
+
step_info["rule_id"] = executed_rule[2]
|
289
|
+
|
290
|
+
if 'Result' in result_data:
|
291
|
+
step_info["result"] = result_data['Result']
|
292
|
+
|
293
|
+
if 'RuleAnnotations' in result_data:
|
294
|
+
step_info["annotations"] = result_data['RuleAnnotations']
|
295
|
+
|
296
|
+
trace.append(step_info)
|
297
|
+
|
298
|
+
return trace
|
299
|
+
|
300
|
+
def get_rule_schema(self, rule_name: str) -> Dict[str, Any]:
|
301
|
+
"""규칙의 스키마 정보를 반환합니다"""
|
302
|
+
if rule_name in self.model_metadata:
|
303
|
+
metadata = self.model_metadata[rule_name]
|
304
|
+
|
305
|
+
# LLM이 이해하기 쉬운 형태로 스키마 구성
|
306
|
+
schema = {
|
307
|
+
"rule_name": rule_name,
|
308
|
+
"engine_type": "pyDMNrules",
|
309
|
+
"loaded_at": metadata.get("loaded_at", 0),
|
310
|
+
"inputs": {},
|
311
|
+
"outputs": {},
|
312
|
+
"decision_tables": metadata.get("decision_tables", []),
|
313
|
+
"example_input": {},
|
314
|
+
"variable_names": metadata.get("variable_names", []),
|
315
|
+
"important_note": "⚠️ Use EXACT variable names from 'variable_names' list as dictionary keys in context_input"
|
316
|
+
}
|
317
|
+
|
318
|
+
# 입력 필드 정보
|
319
|
+
for var_name, var_info in metadata.get("inputs", {}).items():
|
320
|
+
schema["inputs"][var_name] = {
|
321
|
+
"variable_name": var_name, # 정확한 변수 이름 (공백 포함 가능)
|
322
|
+
"feel_name": var_info.get('feel_name', ''), # FEEL 표현식 이름
|
323
|
+
"description": f"{var_info.get('concept', '')}.{var_info.get('item', '')}",
|
324
|
+
"type": var_info.get("type", "string"),
|
325
|
+
"required": True
|
326
|
+
}
|
327
|
+
schema["example_input"][var_name] = None
|
328
|
+
|
329
|
+
# 출력 필드 정보
|
330
|
+
for var_name, var_info in metadata.get("outputs", {}).items():
|
331
|
+
schema["outputs"][var_name] = {
|
332
|
+
"variable_name": var_name,
|
333
|
+
"feel_name": var_info.get('feel_name', ''),
|
334
|
+
"description": f"{var_info.get('concept', '')}.{var_info.get('item', '')}",
|
335
|
+
"type": var_info.get("type", "string")
|
336
|
+
}
|
337
|
+
|
338
|
+
return schema
|
339
|
+
else:
|
340
|
+
return {
|
341
|
+
"error": f"Rule '{rule_name}' not loaded",
|
342
|
+
"inputs": {},
|
343
|
+
"outputs": {}
|
344
|
+
}
|
345
|
+
|
346
|
+
|
347
|
+
class DMNModel:
|
348
|
+
"""DMN 모델을 관리하는 클래스 (pyDMNrules)"""
|
349
|
+
|
350
|
+
def __init__(self, rules_dir: str = "rules"):
|
351
|
+
self.rules_dir = Path(rules_dir)
|
352
|
+
self.rules_dir.mkdir(exist_ok=True)
|
353
|
+
self.dmn_engine = PyDMNrulesEngine()
|
354
|
+
|
355
|
+
async def load_rule(self, rule_name: str) -> str:
|
356
|
+
"""DMN 규칙을 로드합니다"""
|
357
|
+
rule_file = self.rules_dir / f"{rule_name}.dmn"
|
358
|
+
|
359
|
+
if not rule_file.exists():
|
360
|
+
# .dmn.xml 확장자도 시도
|
361
|
+
rule_file = self.rules_dir / f"{rule_name}.dmn.xml"
|
362
|
+
if not rule_file.exists():
|
363
|
+
raise FileNotFoundError(f"Rule '{rule_name}' not found (tried .dmn and .dmn.xml)")
|
364
|
+
|
365
|
+
async with aiofiles.open(rule_file, 'r', encoding='utf-8') as f:
|
366
|
+
content = await f.read()
|
367
|
+
|
368
|
+
# pyDMNrules 엔진으로 XML 로드
|
369
|
+
result = self.dmn_engine.load_dmn_xml(rule_name, content)
|
370
|
+
|
371
|
+
if result.get("status") == "error":
|
372
|
+
error_msg = result.get("message", "Unknown error")
|
373
|
+
errors = result.get("errors", [])
|
374
|
+
raise ValueError(f"{error_msg}: {errors}")
|
375
|
+
|
376
|
+
return result["message"]
|
377
|
+
|
378
|
+
async def save_rule(self, rule_name: str, xml_content: str) -> str:
|
379
|
+
"""DMN 규칙을 저장합니다"""
|
380
|
+
rule_file = self.rules_dir / f"{rule_name}.dmn"
|
381
|
+
|
382
|
+
# XML 유효성 검사
|
383
|
+
try:
|
384
|
+
import xml.etree.ElementTree as ET
|
385
|
+
ET.fromstring(xml_content)
|
386
|
+
except ET.ParseError as e:
|
387
|
+
raise ValueError(f"Invalid DMN XML: {e}")
|
388
|
+
|
389
|
+
async with aiofiles.open(rule_file, 'w', encoding='utf-8') as f:
|
390
|
+
await f.write(xml_content)
|
391
|
+
|
392
|
+
# 저장 후 즉시 로드 시도
|
393
|
+
try:
|
394
|
+
self.dmn_engine.load_dmn_xml(rule_name, xml_content)
|
395
|
+
except Exception as e:
|
396
|
+
print(f"Warning: Failed to load saved XML: {e}")
|
397
|
+
|
398
|
+
return f"Rule '{rule_name}' saved successfully"
|
399
|
+
|
400
|
+
async def list_rules(self) -> List[str]:
|
401
|
+
"""저장된 규칙 목록을 반환합니다"""
|
402
|
+
if not self.rules_dir.exists():
|
403
|
+
return []
|
404
|
+
|
405
|
+
rules = set()
|
406
|
+
|
407
|
+
# .dmn 파일 찾기
|
408
|
+
for file_path in self.rules_dir.glob("*.dmn"):
|
409
|
+
if not file_path.name.endswith('.xml'):
|
410
|
+
rules.add(file_path.stem)
|
411
|
+
|
412
|
+
# .dmn.xml 파일 찾기
|
413
|
+
for file_path in self.rules_dir.glob("*.dmn.xml"):
|
414
|
+
rule_name = file_path.name.replace('.dmn.xml', '')
|
415
|
+
rules.add(rule_name)
|
416
|
+
|
417
|
+
return sorted(list(rules))
|
418
|
+
|
419
|
+
async def delete_rule(self, rule_name: str) -> str:
|
420
|
+
"""DMN 규칙을 삭제합니다"""
|
421
|
+
deleted = False
|
422
|
+
|
423
|
+
# .dmn 파일 삭제 시도
|
424
|
+
rule_file = self.rules_dir / f"{rule_name}.dmn"
|
425
|
+
if rule_file.exists():
|
426
|
+
rule_file.unlink()
|
427
|
+
deleted = True
|
428
|
+
|
429
|
+
# .dmn.xml 파일 삭제 시도
|
430
|
+
rule_file_xml = self.rules_dir / f"{rule_name}.dmn.xml"
|
431
|
+
if rule_file_xml.exists():
|
432
|
+
rule_file_xml.unlink()
|
433
|
+
deleted = True
|
434
|
+
|
435
|
+
if not deleted:
|
436
|
+
raise FileNotFoundError(f"Rule '{rule_name}' not found")
|
437
|
+
|
438
|
+
# 엔진에서도 제거
|
439
|
+
if rule_name in self.dmn_engine.dmn_models:
|
440
|
+
del self.dmn_engine.dmn_models[rule_name]
|
441
|
+
if rule_name in self.dmn_engine.model_metadata:
|
442
|
+
del self.dmn_engine.model_metadata[rule_name]
|
443
|
+
|
444
|
+
return f"Rule '{rule_name}' deleted successfully"
|
445
|
+
|
446
|
+
async def get_rule_schema(self, rule_name: str) -> Dict[str, Any]:
|
447
|
+
"""규칙의 입력 스키마를 반환합니다"""
|
448
|
+
return self.dmn_engine.get_rule_schema(rule_name)
|
449
|
+
|
450
|
+
async def evaluate_decision(self, rule_name: str, input_context: Dict[str, Any]) -> DecisionResult:
|
451
|
+
"""DMN 규칙을 실행하여 의사결정을 수행합니다"""
|
452
|
+
return self.dmn_engine.execute_dmn_rule(rule_name, input_context)
|
453
|
+
|
454
|
+
|
455
|
+
# FastMCP 서버 초기화
|
456
|
+
mcp = FastMCP("pydmnrules-mcp-server")
|
457
|
+
dmn_model = DMNModel()
|
458
|
+
|
459
|
+
|
460
|
+
@mcp.tool()
|
461
|
+
async def load_rule(rule_name: str) -> str:
|
462
|
+
"""
|
463
|
+
지정된 이름의 DMN XML 규칙을 로드합니다. pyDMNrules 엔진을 사용합니다.
|
464
|
+
|
465
|
+
Args:
|
466
|
+
rule_name: 로드할 규칙의 이름 (확장자 제외)
|
467
|
+
|
468
|
+
Returns:
|
469
|
+
로드 결과 메시지
|
470
|
+
"""
|
471
|
+
try:
|
472
|
+
return await dmn_model.load_rule(rule_name)
|
473
|
+
except Exception as e:
|
474
|
+
return f"Error loading rule '{rule_name}': {str(e)}"
|
475
|
+
|
476
|
+
|
477
|
+
@mcp.tool()
|
478
|
+
async def save_rule(rule_name: str, xml_content: str) -> str:
|
479
|
+
"""
|
480
|
+
새로운 DMN 규칙을 저장합니다.
|
481
|
+
|
482
|
+
Args:
|
483
|
+
rule_name: 저장할 규칙의 이름
|
484
|
+
xml_content: DMN XML 내용
|
485
|
+
|
486
|
+
Returns:
|
487
|
+
저장 결과 메시지
|
488
|
+
"""
|
489
|
+
try:
|
490
|
+
return await dmn_model.save_rule(rule_name, xml_content)
|
491
|
+
except Exception as e:
|
492
|
+
return f"Error saving rule '{rule_name}': {str(e)}"
|
493
|
+
|
494
|
+
|
495
|
+
@mcp.tool()
|
496
|
+
async def list_rules() -> List[str]:
|
497
|
+
"""
|
498
|
+
등록된 DMN 규칙 목록을 조회합니다.
|
499
|
+
|
500
|
+
Returns:
|
501
|
+
규칙 이름 목록
|
502
|
+
"""
|
503
|
+
try:
|
504
|
+
return await dmn_model.list_rules()
|
505
|
+
except Exception as e:
|
506
|
+
return [f"Error listing rules: {str(e)}"]
|
507
|
+
|
508
|
+
|
509
|
+
@mcp.tool()
|
510
|
+
async def delete_rule(rule_name: str) -> str:
|
511
|
+
"""
|
512
|
+
DMN 규칙을 삭제합니다.
|
513
|
+
|
514
|
+
Args:
|
515
|
+
rule_name: 삭제할 규칙의 이름
|
516
|
+
|
517
|
+
Returns:
|
518
|
+
삭제 결과 메시지
|
519
|
+
"""
|
520
|
+
try:
|
521
|
+
return await dmn_model.delete_rule(rule_name)
|
522
|
+
except Exception as e:
|
523
|
+
return f"Error deleting rule '{rule_name}': {str(e)}"
|
524
|
+
|
525
|
+
|
526
|
+
@mcp.tool()
|
527
|
+
async def get_rule_schema(rule_name: str) -> Dict[str, Any]:
|
528
|
+
"""
|
529
|
+
규칙의 입력 스키마를 조회합니다.
|
530
|
+
|
531
|
+
Args:
|
532
|
+
rule_name: 규칙 이름
|
533
|
+
|
534
|
+
Returns:
|
535
|
+
규칙의 입력 스키마 (엔진별 메타데이터 포함)
|
536
|
+
"""
|
537
|
+
try:
|
538
|
+
return await dmn_model.get_rule_schema(rule_name)
|
539
|
+
except Exception as e:
|
540
|
+
return {"error": f"Error getting schema for rule '{rule_name}': {str(e)}"}
|
541
|
+
|
542
|
+
|
543
|
+
@mcp.tool()
|
544
|
+
async def infer_decision(rule_name: str, context_input: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
|
545
|
+
"""
|
546
|
+
지정된 DMN 규칙을 기반으로 의사결정을 실행합니다. pyDMNrules 엔진을 사용합니다.
|
547
|
+
|
548
|
+
Args:
|
549
|
+
rule_name: 사용할 DMN 규칙의 이름
|
550
|
+
context_input: key-value 딕셔너리 형태의 입력 데이터
|
551
|
+
|
552
|
+
Returns:
|
553
|
+
의사결정 결과 (result, trace, input_context, rule_name, execution_time, rule_schema, engine_used 포함)
|
554
|
+
"""
|
555
|
+
try:
|
556
|
+
# 입력 컨텍스트 생성
|
557
|
+
if isinstance(context_input, str):
|
558
|
+
try:
|
559
|
+
input_context = json.loads(context_input)
|
560
|
+
except json.JSONDecodeError:
|
561
|
+
input_context = {"raw_input": context_input}
|
562
|
+
else:
|
563
|
+
input_context = context_input
|
564
|
+
|
565
|
+
# 의사결정 실행
|
566
|
+
result = await dmn_model.evaluate_decision(rule_name, input_context)
|
567
|
+
|
568
|
+
return {
|
569
|
+
"result": result.result,
|
570
|
+
"trace": result.trace,
|
571
|
+
"input_context": result.input_context,
|
572
|
+
"rule_name": result.rule_name,
|
573
|
+
"execution_time": result.execution_time,
|
574
|
+
"rule_schema": result.rule_schema,
|
575
|
+
"engine_used": result.engine_used
|
576
|
+
}
|
577
|
+
except Exception as e:
|
578
|
+
return {
|
579
|
+
"error": f"Error executing decision: {str(e)}",
|
580
|
+
"result": {},
|
581
|
+
"trace": [],
|
582
|
+
"input_context": {},
|
583
|
+
"rule_name": rule_name,
|
584
|
+
"execution_time": 0,
|
585
|
+
"rule_schema": None,
|
586
|
+
"engine_used": "error"
|
587
|
+
}
|
588
|
+
|
589
|
+
|
590
|
+
@mcp.tool()
|
591
|
+
async def check_engine_status() -> Dict[str, Any]:
|
592
|
+
"""
|
593
|
+
pyDMNrules 엔진의 상태를 확인합니다.
|
594
|
+
|
595
|
+
Returns:
|
596
|
+
엔진 상태 정보
|
597
|
+
"""
|
598
|
+
return {
|
599
|
+
"pydmnrules_available": PYDMNRULES_AVAILABLE,
|
600
|
+
"message": f"pyDMNrules Engine - Available: {PYDMNRULES_AVAILABLE}",
|
601
|
+
"loaded_models": list(dmn_model.dmn_engine.dmn_models.keys()),
|
602
|
+
"total_loaded_models": len(dmn_model.dmn_engine.dmn_models),
|
603
|
+
"rules_directory": str(dmn_model.rules_dir.absolute())
|
604
|
+
}
|
605
|
+
|
606
|
+
|
607
|
+
def main():
|
608
|
+
"""메인 함수 - 서버 실행"""
|
609
|
+
__version__ = "1.0.0"
|
610
|
+
|
611
|
+
print(f"Starting pyDMNrules MCP Server v{__version__}")
|
612
|
+
print(f"pyDMNrules available: {PYDMNRULES_AVAILABLE}")
|
613
|
+
print("Supports DMN XML files (.dmn, .dmn.xml)")
|
614
|
+
print(f"Rules directory: {dmn_model.rules_dir.absolute()}")
|
615
|
+
|
616
|
+
if not PYDMNRULES_AVAILABLE:
|
617
|
+
print("\n⚠️ WARNING: pyDMNrules is not installed!")
|
618
|
+
print("Install with: pip install pydmnrules-enhanced")
|
619
|
+
return
|
620
|
+
|
621
|
+
try:
|
622
|
+
# stdio 모드로 실행
|
623
|
+
mcp.run()
|
624
|
+
except KeyboardInterrupt:
|
625
|
+
print("\nServer stopped by user")
|
626
|
+
except Exception as e:
|
627
|
+
print(f"\nServer error: {e}")
|
628
|
+
|
629
|
+
|
630
|
+
if __name__ == "__main__":
|
631
|
+
main()
|
632
|
+
|