"""V2 API routes - Enhanced implementation with structured results.""" from __future__ import annotations import time from flask import Blueprint, jsonify, request from concurrent.futures import ThreadPoolExecutor from lawrisk.services.lawrisk_v2_service import search_v2, list_regions from lawrisk.services.licensing_repo import ( list_permits_for_region, load_permits_and_risks, list_region_theme_options, load_theme_payload, create_checkpoint, list_checkpoints, restore_checkpoint, delete_checkpoint, ) from lawrisk.services.lawrisk_service import suggest_questions_embed v2_bp = Blueprint('lawrisk_v2', __name__, url_prefix='/fs-ai-asistant/api/workflow/lawrisk') @v2_bp.route('/v2', methods=['POST', 'GET']) def lawrisk_search_v2(): """V2 search endpoint with structured results.""" query, debug_flag, top_k_int, _mode, region_filter = _extract_params() if not query or not isinstance(query, str): return jsonify({"error": "query is required"}), 400 try: t0 = time.time() with ThreadPoolExecutor(max_workers=2) as ex: fut_subject = ex.submit(search_v2, query, debug_flag, region_filter) fut_questions = ex.submit(suggest_questions_embed, query, max(1, top_k_int)) result_v2 = fut_subject.result() rec_questions = fut_questions.result() or [] risk_subject = result_v2.get("risk_subject", []) if isinstance(result_v2, dict) else [] found = bool(risk_subject) exec_time = int((time.time() - t0) * 1000) data = { "llmRespond": "" if found else "抱歉,无法检索到相关答案", "lawRisk": "", "questionExtend": rec_questions, "conversationId": "", "messageId": "", "roundNumber": 0, "conversationInfo": {}, "knowledgeSources": [], "totalKnowledgeSources": 0, "executionTime": exec_time, "workflowStatus": "ok" if found else "no_match", "executionSteps": [], "costStatistics": {}, "workflowTrackingId": "", "risk_subject": risk_subject, "debug": result_v2.get("debug", {}) if (debug_flag and isinstance(result_v2, dict)) else {}, } resp = {"success": True, "message": "OK", "data": data} return jsonify(resp) except Exception as e: print(f"lawrisk_search_v2 error: {e}") return jsonify({"success": False, "message": str(e), "data": {}}), 500 @v2_bp.route('/v2/regions', methods=['GET']) def lawrisk_regions(): """Get list of available regions.""" try: regions = list_regions() return jsonify({"success": True, "data": {"regions": regions}}) except Exception as exc: print(f"lawrisk_regions error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/getPermits', methods=['GET', 'POST']) def lawrisk_get_permits(): """Get permits for a specific region.""" if request.method == "GET": region_value = request.args.get("region") or request.args.get("region_id") else: if request.is_json: payload = request.get_json(silent=True) or {} else: payload = request.form.to_dict(flat=True) if request.form else {} region_value = payload.get("region") or payload.get("region_id") if not region_value or (isinstance(region_value, str) and not region_value.strip()): return jsonify({"success": False, "message": "region is required", "data": {}}), 400 region_token = region_value.strip() if isinstance(region_value, str) else str(region_value) try: permits = list_permits_for_region(region_token) return jsonify({"success": True, "data": {"region": region_token, "permits": permits}}) except Exception as exc: print(f"lawrisk_get_permits error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 def _extract_params(): """Extract parameters from request.""" if request.method == "GET": query = request.args.get("query") or request.args.get("q") or request.args.get("text") debug_flag = request.args.get("debug") in {"1", "true", "yes", "on"} top_k = request.args.get("top") try: top_k_int = int(top_k) if top_k else 5 except Exception: top_k_int = 5 mode_value = (request.args.get("mode") or "llm").lower() region_value = request.args.get("region") or request.args.get("region_id") region_list = request.args.getlist("region") if region_list and (not region_value or len(region_list) > 1): region_value = region_list else: if request.is_json: payload = request.get_json(silent=True) or {} else: payload = request.form.to_dict(flat=True) if request.form else {} query = payload.get("query") or payload.get("q") or payload.get("text") debug_flag = str(payload.get("debug", "")).strip().lower() in {"1", "true", "yes", "on"} try: top_k_int = int(payload.get("top", 5)) except Exception: top_k_int = 5 mode_value = str(payload.get("mode", "llm")).lower() region_value = payload.get("region") or payload.get("region_id") return query, debug_flag, top_k_int, mode_value, region_value @v2_bp.route('/admin/test', methods=['GET']) def admin_test(): """Simple test route.""" return jsonify({"success": True, "message": "Test route works!"}) @v2_bp.route('/test-simple', methods=['GET']) def test_simple(): """Very simple test.""" return jsonify({"status": "ok"}) @v2_bp.route('/admin/regions', methods=['GET']) def admin_regions(): """Get all regions for database maintenance.""" try: regions = list_regions() return jsonify({"success": True, "data": {"regions": regions}}) except Exception as exc: print(f"admin_regions error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/admin/themes', methods=['GET']) def admin_themes(): """Get themes for a specific region.""" region_value = request.args.get("region") or request.args.get("region_id") if not region_value or (isinstance(region_value, str) and not region_value.strip()): return jsonify({"success": False, "message": "region is required", "data": {}}), 400 region_token = region_value.strip() if isinstance(region_value, str) else str(region_value) try: catalog = list_region_theme_options() region_id_lower = region_token.lower() themes = [] seen_theme_ids = set() for item in catalog: if (item["region_id"] == region_token or item["region_id"].lower() == region_id_lower or item["region_name"].lower() == region_id_lower): theme_id = item["theme_id"] if theme_id not in seen_theme_ids: seen_theme_ids.add(theme_id) themes.append({ "id": theme_id, "name": item["theme_name"], "option_id": item["option_id"] }) return jsonify({"success": True, "data": {"region": region_token, "themes": themes}}) except Exception as exc: print(f"admin_themes error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/admin/permits', methods=['GET']) def admin_permits(): """Get permits for a specific region-theme combination.""" region_value = request.args.get("region") or request.args.get("region_id") theme_value = request.args.get("theme") or request.args.get("theme_id") if not region_value or not theme_value: return jsonify({"success": False, "message": "region and theme are required", "data": {}}), 400 region_token = region_value.strip() if isinstance(region_value, str) else str(region_value) theme_token = theme_value.strip() if isinstance(theme_value, str) else str(theme_value) try: permits = load_permits_and_risks(region_token, theme_token) return jsonify({ "success": True, "data": { "region": region_token, "theme": theme_token, "permits": permits } }) except Exception as exc: print(f"admin_permits error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/admin/permit-details', methods=['GET']) def admin_permit_details(): """Get detailed information for a specific permit.""" region_value = request.args.get("region") or request.args.get("region_id") theme_value = request.args.get("theme") or request.args.get("theme_id") permit_value = request.args.get("permit") or request.args.get("permit_id") if not region_value or not theme_value or not permit_value: return jsonify({"success": False, "message": "region, theme, and permit are required", "data": {}}), 400 region_token = region_value.strip() if isinstance(region_value, str) else str(region_value) theme_token = theme_value.strip() if isinstance(theme_value, str) else str(theme_value) permit_token = permit_value.strip() if isinstance(permit_value, str) else str(permit_value) try: permits = load_permits_and_risks(region_token, theme_token, permit_token) if not permits: return jsonify({"success": False, "message": "Permit not found", "data": {}}), 404 return jsonify({ "success": True, "data": { "region": region_token, "theme": theme_token, "permit": permits[0] } }) except Exception as exc: print(f"admin_permit_details error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/admin/checkpoints', methods=['GET']) def admin_list_checkpoints(): """List all available checkpoints.""" try: checkpoints = list_checkpoints() return jsonify({"success": True, "data": {"checkpoints": checkpoints}}) except Exception as exc: print(f"admin_list_checkpoints error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/admin/checkpoints', methods=['POST']) def admin_create_checkpoint(): """Create a new checkpoint.""" if request.is_json: payload = request.get_json(silent=True) or {} else: payload = request.form.to_dict(flat=True) if request.form else {} description = payload.get("description", "") try: checkpoint = create_checkpoint(description) return jsonify({"success": True, "data": checkpoint}) except Exception as exc: print(f"admin_create_checkpoint error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500 @v2_bp.route('/admin/checkpoints//restore', methods=['POST']) def admin_restore_checkpoint(checkpoint_id): """ ⚠️ DANGEROUS OPERATION ⚠️ 恢复数据库从checkpoint。 此操作将会: 1. 永久删除当前数据库中的所有数据 2. 从指定的checkpoint恢复数据 3. 如果失败,可能导致数据丢失 建议在生产环境中: - 确保已创建最新备份 - 在低峰期操作 - 启用自动备份功能 (create_auto_backup=true) POST参数: - create_auto_backup: 是否在恢复前自动备份当前状态 (默认: true) - batch_size: 批量插入的批次大小,每批插入行数 (默认: 1000,范围: 100-10000) """ import logging logger = logging.getLogger(__name__) logger.info("=" * 80) logger.info(f"[API] Received checkpoint restore request: {checkpoint_id}") logger.info("=" * 80) try: # 获取请求参数 if request.is_json: payload = request.get_json(silent=True) or {} else: payload = request.form.to_dict(flat=True) if request.form else {} logger.info(f"[API] Request parameters: {payload}") # 默认启用自动备份 create_auto_backup = str(payload.get("create_auto_backup", "true")).lower() in {"1", "true", "yes", "on"} logger.info(f"[API] Auto-backup enabled: {create_auto_backup}") # 解析批量大小参数 try: batch_size = int(payload.get("batch_size", "1000")) if batch_size < 100 or batch_size > 10000: raise ValueError(f"batch_size must be between 100 and 10000, got {batch_size}") except (ValueError, TypeError) as e: logger.warning(f"[API] Invalid batch_size: {e}, using default 1000") batch_size = 1000 logger.info(f"[API] Batch size: {batch_size} rows/batch") # 执行恢复 logger.info(f"[API] Starting restore operation...") restore_result = restore_checkpoint(checkpoint_id, create_auto_backup=create_auto_backup, batch_size=batch_size) # 检查恢复状态 if restore_result.get("status") == "success": logger.info(f"[API] Restore completed successfully") logger.info(f"[API] Tables restored: {restore_result['summary']['tables_restored']}") logger.info(f"[API] Total rows restored: {restore_result['summary']['total_rows_restored']}") return jsonify({ "success": True, "message": restore_result["message"], "data": { "checkpoint_id": restore_result["summary"]["checkpoint_id"], "tables_restored": restore_result["summary"]["tables_restored"], "total_rows": restore_result["summary"]["total_rows_restored"], "auto_backup": restore_result["summary"].get("auto_backup"), "table_details": restore_result["summary"]["table_details"] } }) else: # 恢复失败 logger.error(f"[API] Restore failed: {restore_result.get('message')}") errors = restore_result["summary"].get("errors", []) if errors: logger.error(f"[API] Errors encountered: {errors}") response_data = { "success": False, "message": restore_result["message"], "data": { "errors": errors, "auto_backup_available": restore_result.get("auto_backup_available", False) } } # 如果有自动备份,提供恢复建议 if restore_result.get("recovery_suggestion"): logger.warning(f"[API] Recovery suggestion: {restore_result['recovery_suggestion']}") response_data["data"]["recovery_suggestion"] = restore_result["recovery_suggestion"] return jsonify(response_data), 500 except Exception as exc: logger.error(f"[API] Restore operation failed with exception: {str(exc)}", exc_info=True) return jsonify({ "success": False, "message": f"Restore failed: {str(exc)}", "data": { "error_type": type(exc).__name__, "auto_backup_available": None # 未知,因为异常发生在备份之前 } }), 500 @v2_bp.route('/admin/checkpoints/', methods=['DELETE']) def admin_delete_checkpoint(checkpoint_id): """Delete a checkpoint.""" try: success = delete_checkpoint(checkpoint_id) if success: return jsonify({"success": True, "message": "Checkpoint deleted"}) else: return jsonify({"success": False, "message": "Checkpoint not found"}), 404 except Exception as exc: print(f"admin_delete_checkpoint error: {exc}") return jsonify({"success": False, "message": str(exc)}), 500