fs-lawrisk/lawrisk/api/v2.py

407 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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/<checkpoint_id>/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/<checkpoint_id>', 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