diff --git a/README.md b/README.md index adf941e..0b0ee80 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,32 @@ pip install -e . ### 启动服务 +#### REST API ```bash -python -m rule_engine.api -# 默认 http://0.0.0.0:8000 +python3 -c " +import sys; sys.path.insert(0, 'src') +from rule_engine.api import create_app +from rule_engine.store import RuleStore +srv = create_app(RuleStore('rules.db')) +print('API on http://0.0.0.0:8000') +srv.serve_forever() +" ``` +#### Web UI(推荐) +```bash +python3 -c " +import sys; sys.path.insert(0, 'src') +from rule_engine.web import create_web_app +from rule_engine.store import RuleStore +srv = create_web_app(RuleStore('rules.db')) +print('Web UI on http://0.0.0.0:8080') +srv.serve_forever() +" +``` + +访问 http://localhost:8080 + ### API 接口 #### 创建规则 @@ -84,7 +105,8 @@ src/rule_engine/ ├── executor.py # 沙箱执行器 ├── matcher.py # 规则匹配 ├── models.py # 数据模型 -└── store.py # SQLite 存储 +├── store.py # SQLite 存储 +└── web.py # Web UI(可选) ``` ## 配置 diff --git a/src/rule_engine/web.py b/src/rule_engine/web.py new file mode 100644 index 0000000..61eeded --- /dev/null +++ b/src/rule_engine/web.py @@ -0,0 +1,507 @@ +"""Web UI for rule engine management.""" +import json +import os +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse, parse_qs +from typing import Optional, Dict, Any + +from rule_engine.store import RuleStore +from rule_engine.models import CreateRuleRequest +from rule_engine.matcher import RuleMatcher +from rule_engine.executor import RuleExecutor +from rule_engine.callback import LLMEallback, MockLLMClient +from rule_engine.compiler import RuleCompiler +from rule_engine.conflict import ConflictDetector + + +class RuleEngineWebApp: + """Web 应用处理逻辑。""" + + def __init__(self, store: RuleStore, enable_callback: bool = False): + self.store = store + self.executor = RuleExecutor() + self.matcher = RuleMatcher(store, self.executor) + self.conflict_detector = ConflictDetector() + + if enable_callback: + self.callback = LLMEallback() + else: + mock_client = MockLLMClient() + compiler = RuleCompiler(llm_client=mock_client) + self.callback = LLMEallback(compiler=compiler) + + def handle_request(self, path: str, method: str, body: Optional[Dict] = None) -> tuple: + """处理请求并返回 (status, content_type, body)。""" + if path == "/" or path == "/index.html": + return 200, "text/html", self._render_index() + elif path == "/api/rules" and method == "GET": + return self._handle_list_rules() + elif path == "/api/rules" and method == "POST": + return self._handle_create_rule(body) + elif path.startswith("/api/rules/") and method == "GET": + rule_id = path.split("/")[-1] + return self._handle_get_rule(rule_id) + elif path.startswith("/api/rules/") and method == "DELETE": + rule_id = path.split("/")[-1] + return self._handle_delete_rule(rule_id) + elif path == "/api/rules/evaluate" and method == "POST": + return self._handle_evaluate(body) + elif path == "/api/rules/compile" and method == "POST": + return self._handle_compile(body) + elif path == "/api/conflicts": + return self._handle_check_conflicts() + else: + return 404, "application/json", json.dumps({"error": "Not found"}) + + def _render_index(self) -> str: + return HTML_CONTENT + + def _handle_list_rules(self) -> tuple: + rules = self.store.list_rules() + return 200, "application/json", json.dumps({ + "rules": [r.to_dict() for r in rules] + }) + + def _handle_create_rule(self, body: Optional[Dict]) -> tuple: + if not body: + return 400, "application/json", json.dumps({"error": "Body required"}) + try: + request = CreateRuleRequest( + name=body.get("name", ""), + condition_template=body.get("condition_template", ""), + description=body.get("description"), + priority=body.get("priority", 0) + ) + except (TypeError, ValueError) as e: + return 400, "application/json", json.dumps({"error": f"Invalid request: {e}"}) + + if not request.name or not request.condition_template: + return 400, "application/json", json.dumps({"error": "name and condition_template required"}) + + code = body.get("code", "def rule(facts):\n return None") + + # 检查冲突 + existing = [r.to_dict() for r in self.store.list_rules()] + new_rule_dict = { + "id": "new", "name": request.name, "code": code, + "priority": request.priority, "is_active": True + } + conflicts = self.conflict_detector.check_rule_with_existing(new_rule_dict, existing) + + rule = self.store.create_rule( + name=request.name, + condition_template=request.condition_template, + code=code, + description=request.description, + priority=request.priority + ) + + result = rule.to_dict() + if conflicts: + result["conflict_warning"] = conflicts + + return 201, "application/json", json.dumps(result) + + def _handle_get_rule(self, rule_id: str) -> tuple: + rule = self.store.get_rule(rule_id) + if not rule: + return 404, "application/json", json.dumps({"error": "Not found"}) + return 200, "application/json", json.dumps(rule.to_dict()) + + def _handle_delete_rule(self, rule_id: str) -> tuple: + deleted = self.store.delete_rule(rule_id) + if not deleted: + return 404, "application/json", json.dumps({"error": "Not found"}) + return 200, "application/json", json.dumps({"deleted": True}) + + def _handle_evaluate(self, body: Optional[Dict]) -> tuple: + if not body: + return 400, "application/json", json.dumps({"error": "facts required"}) + facts = body.get("facts") + if not facts: + return 400, "application/json", json.dumps({"error": "facts required"}) + + rule_id = body.get("rule_id") + matched_rule_id, result = self.matcher.find_matching_rule(facts, rule_id) + + if matched_rule_id and result is not None: + self.store.record_execution(matched_rule_id, facts, result, True) + return 200, "application/json", json.dumps({ + "matched": True, "rule_id": matched_rule_id, "result": result + }) + + self.store.record_execution(matched_rule_id or "", facts, result, False) + return 200, "application/json", json.dumps({ + "matched": False, "rule_id": matched_rule_id, "result": None + }) + + def _handle_compile(self, body: Optional[Dict]) -> tuple: + if not body: + return 400, "application/json", json.dumps({"error": "condition_template required"}) + template = body.get("condition_template") + if not template: + return 400, "application/json", json.dumps({"error": "condition_template required"}) + + from rule_engine.compiler import build_compile_prompt + prompt = build_compile_prompt(template) + + # 使用 mock 返回(真实环境需要配置 LLM API key) + response = MockLLMClient().complete(prompt) + from rule_engine.compiler import extract_code_block + code = extract_code_block(response) + + if not code: + return 500, "application/json", json.dumps({"error": "Failed to generate code"}) + + return 200, "application/json", json.dumps({ + "condition_template": template, + "generated_code": code + }) + + def _handle_check_conflicts(self) -> tuple: + rules = [r.to_dict() for r in self.store.list_rules()] + conflicts = self.conflict_detector.detect_conflicts(rules) + return 200, "application/json", json.dumps({"conflicts": conflicts}) + + +class WebUIHandler(BaseHTTPRequestHandler): + app: Optional[RuleEngineWebApp] = None + + def do_GET(self): + parsed = urlparse(self.path) + path = parsed.path + + if path.startswith("/api/") or path == "/": + status, content_type, body = self.app.handle_request(path, "GET") + self._send_response(status, content_type, body) + else: + self._send_response(404, "application/json", json.dumps({"error": "Not found"})) + + def do_POST(self): + parsed = urlparse(self.path) + path = parsed.path + + if path.startswith("/api/"): + content_length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(content_length)) if content_length > 0 else None + status, content_type, response = self.app.handle_request(path, "POST", body) + self._send_response(status, content_type, response) + else: + self._send_response(404, "application/json", json.dumps({"error": "Not found"})) + + def do_DELETE(self): + parsed = urlparse(self.path) + path = parsed.path + + if path.startswith("/api/rules/"): + rule_id = path.split("/")[-1] + status, content_type, response = self.app.handle_request(path, "DELETE") + self._send_response(status, content_type, response) + else: + self._send_response(404, "application/json", json.dumps({"error": "Not found"})) + + def _send_response(self, status: int, content_type: str, body: str): + self.send_response(status) + self.send_header("Content-Type", f"{content_type}; charset=utf-8") + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(body.encode("utf-8")) + + def log_message(self, format, *args): + print(f"[{self.log_date_time_string()}] {format % args}") + + +HTML_CONTENT = """ + + + + +AI 规则引擎 + + + +

AI 规则引擎 管理后台

+
+
+ + + + +
+ +
+
+

规则列表

+ +
+
+
+ +
+
+

创建规则

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +
+
+
+ +
+
+

规则测试

+
+
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+

冲突检测

+ +
+
+
+
+ + + + +""" + + +def create_web_app(store: RuleStore, host: str = "0.0.0.0", port: int = 8080, enable_callback: bool = False) -> HTTPServer: + """创建 Web UI 服务器。""" + WebUIHandler.app = RuleEngineWebApp(store, enable_callback) + server = HTTPServer((host, port), WebUIHandler) + return server