routes.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. """Governed control-plane endpoints for AI-authored data rules."""
  2. from __future__ import annotations
  3. from typing import Any
  4. from flask import current_app, g, jsonify, request
  5. from app import db
  6. from app.api.data_rules import bp
  7. from app.core.data_rules.authoring import (
  8. OpenAICompatibleRuleModel,
  9. RuleAuthoringAgent,
  10. )
  11. from app.core.data_rules.contracts import (
  12. dataflow_spec_hash,
  13. rule_spec_hash,
  14. standard_spec_hash,
  15. validate_dataflow_spec,
  16. validate_rule_spec,
  17. validate_standard_spec,
  18. )
  19. from app.core.data_rules.production_line import resolve_production_line
  20. from app.core.data_rules.release import ProductionLineReleaseService
  21. from app.core.data_rules.repository import DataRuleRepository
  22. from app.models.result import failed, success
  23. _VALIDATORS = {
  24. "rule": (validate_rule_spec, rule_spec_hash),
  25. "standard": (validate_standard_spec, standard_spec_hash),
  26. "dataflow": (validate_dataflow_spec, dataflow_spec_hash),
  27. }
  28. def _body() -> dict[str, Any]:
  29. value = request.get_json(silent=True)
  30. if not isinstance(value, dict):
  31. raise ValueError("request body must be an object")
  32. return value
  33. def _bad_request(message: str = "规则请求无效"):
  34. return jsonify(failed(message, code=400)), 400
  35. def _closed_body(allowed: set[str]) -> dict[str, Any]:
  36. body = _body()
  37. if set(body) - allowed:
  38. raise ValueError("request contains unsupported fields")
  39. return body
  40. def _repository() -> DataRuleRepository:
  41. configured = current_app.extensions.get("data_rule_repository")
  42. if configured is not None:
  43. return configured
  44. return DataRuleRepository(db.session)
  45. def _release_service() -> ProductionLineReleaseService:
  46. configured = current_app.extensions.get(
  47. "production_line_release_service"
  48. )
  49. if configured is not None:
  50. return configured
  51. return ProductionLineReleaseService(_repository())
  52. @bp.get("/capabilities")
  53. def capabilities():
  54. return jsonify(
  55. success(
  56. {
  57. "natural_language_authoring": True,
  58. "schema_constrained_candidates": True,
  59. "production_line_preview": True,
  60. "immutable_asset_versions": True,
  61. "server_side_publishing": True,
  62. "production_line_release": True,
  63. "data_factory_activation": False,
  64. }
  65. )
  66. )
  67. @bp.post("/validate")
  68. def validate_asset():
  69. try:
  70. body = _body()
  71. asset_type = body.get("asset_type")
  72. if asset_type not in _VALIDATORS:
  73. raise ValueError("unsupported asset type")
  74. validator, hasher = _VALIDATORS[asset_type]
  75. normalized = validator(body.get("spec"))
  76. return jsonify(
  77. success(
  78. {
  79. "asset_type": asset_type,
  80. "normalized": normalized,
  81. "spec_hash": hasher(normalized),
  82. }
  83. )
  84. )
  85. except (TypeError, ValueError):
  86. return _bad_request("规则定义无效")
  87. def _authoring_agent() -> RuleAuthoringAgent:
  88. configured = current_app.extensions.get("data_rule_authoring_agent")
  89. if configured is not None:
  90. return configured
  91. api_key = current_app.config.get("LLM_API_KEY") or current_app.config.get(
  92. "DEEPSEEK_API_KEY"
  93. )
  94. if not api_key:
  95. raise RuntimeError("rule authoring model is not configured")
  96. agent = RuleAuthoringAgent(model=OpenAICompatibleRuleModel())
  97. current_app.extensions["data_rule_authoring_agent"] = agent
  98. return agent
  99. @bp.post("/interpret")
  100. def interpret_rule():
  101. try:
  102. body = _body()
  103. result = _authoring_agent().interpret(
  104. source_text=body.get("source_text"),
  105. authoring_surface=body.get("authoring_surface"),
  106. context=body.get("context", {}),
  107. )
  108. audit = _repository().record_generation_run(evidence=result)
  109. db.session.commit()
  110. result = {
  111. **result,
  112. "generation_run_id": audit["id"],
  113. "correlation_id": audit["correlation_id"],
  114. }
  115. return jsonify(success(result))
  116. except (TypeError, ValueError):
  117. db.session.rollback()
  118. return _bad_request("自然语言规则描述无效")
  119. except RuntimeError:
  120. db.session.rollback()
  121. return jsonify(failed("AI 规则解析服务未配置", code=503)), 503
  122. except Exception:
  123. db.session.rollback()
  124. current_app.logger.exception("AI rule interpretation failed")
  125. return jsonify(failed("AI 规则解析暂时不可用", code=503)), 503
  126. @bp.post("/rule-versions")
  127. def create_rule_version():
  128. try:
  129. body = _closed_body(
  130. {
  131. "rule_spec",
  132. "source_text",
  133. "category",
  134. "source_language",
  135. "generated_kind",
  136. }
  137. )
  138. result = _repository().create_rule_version(
  139. rule_spec=body.get("rule_spec"),
  140. source_text=body.get("source_text"),
  141. category=body.get("category", "general"),
  142. source_language=body.get("source_language", "zh-CN"),
  143. generated_kind=body.get("generated_kind", "rulespec"),
  144. created_by=g.current_user["id"],
  145. )
  146. db.session.commit()
  147. return jsonify(success(result, "规则版本创建成功")), 201
  148. except (TypeError, ValueError):
  149. db.session.rollback()
  150. return _bad_request("规则版本定义无效")
  151. except Exception:
  152. db.session.rollback()
  153. current_app.logger.exception("create rule version failed")
  154. return jsonify(failed("规则版本创建失败", code=500)), 500
  155. @bp.post("/rule-versions/<version_id>/publish")
  156. def publish_rule_version(version_id: str):
  157. try:
  158. if request.get_data(cache=True) and request.get_json(silent=True) not in (
  159. None,
  160. {},
  161. ):
  162. raise ValueError("publish request must not contain fields")
  163. result = _repository().publish_rule_version(
  164. version_id=version_id,
  165. published_by=g.current_user["id"],
  166. )
  167. db.session.commit()
  168. return jsonify(success(result, "规则版本发布成功"))
  169. except (TypeError, ValueError):
  170. db.session.rollback()
  171. return jsonify(failed("规则版本无法发布", code=409)), 409
  172. except Exception:
  173. db.session.rollback()
  174. current_app.logger.exception("publish rule version failed")
  175. return jsonify(failed("规则版本发布失败", code=500)), 500
  176. @bp.post("/standard-versions")
  177. def create_standard_version():
  178. try:
  179. body = _closed_body({"standard_spec", "source_text"})
  180. result = _repository().create_standard_version(
  181. standard_spec=body.get("standard_spec"),
  182. source_text=body.get("source_text"),
  183. created_by=g.current_user["id"],
  184. )
  185. db.session.commit()
  186. return jsonify(success(result, "数据标准版本创建成功")), 201
  187. except (TypeError, ValueError):
  188. db.session.rollback()
  189. return _bad_request("数据标准版本定义无效")
  190. except Exception:
  191. db.session.rollback()
  192. current_app.logger.exception("create standard version failed")
  193. return jsonify(failed("数据标准版本创建失败", code=500)), 500
  194. @bp.post("/standard-versions/<version_id>/publish")
  195. def publish_standard_version(version_id: str):
  196. try:
  197. if request.get_data(cache=True) and request.get_json(silent=True) not in (
  198. None,
  199. {},
  200. ):
  201. raise ValueError("publish request must not contain fields")
  202. result = _repository().publish_standard_version(
  203. version_id=version_id,
  204. published_by=g.current_user["id"],
  205. )
  206. db.session.commit()
  207. return jsonify(success(result, "数据标准版本发布成功"))
  208. except (TypeError, ValueError):
  209. db.session.rollback()
  210. return jsonify(failed("数据标准版本无法发布", code=409)), 409
  211. except Exception:
  212. db.session.rollback()
  213. current_app.logger.exception("publish standard version failed")
  214. return jsonify(failed("数据标准版本发布失败", code=500)), 500
  215. @bp.post("/production-lines/resolve")
  216. def resolve_production_line_preview():
  217. try:
  218. body = _body()
  219. package = resolve_production_line(
  220. body.get("dataflow_spec"),
  221. body.get("standard_versions"),
  222. body.get("rule_versions"),
  223. component_binding_ids=body.get("component_binding_ids"),
  224. )
  225. return jsonify(
  226. success(
  227. {
  228. "preview": True,
  229. "release_ready": False,
  230. "package": package,
  231. }
  232. )
  233. )
  234. except (TypeError, ValueError):
  235. return _bad_request("数据生产线定义无效")
  236. @bp.post("/production-lines/<dataflow_uid>/release")
  237. def release_production_line(dataflow_uid: str):
  238. try:
  239. body = _closed_body(
  240. {
  241. "dataflow_spec",
  242. "source_text",
  243. "input_schema_hashes",
  244. "output_schema_hash",
  245. }
  246. )
  247. result = _release_service().release(
  248. dataflow_uid=dataflow_uid,
  249. dataflow_spec=body.get("dataflow_spec"),
  250. source_text=body.get("source_text"),
  251. input_schema_hashes=body.get("input_schema_hashes"),
  252. output_schema_hash=body.get("output_schema_hash"),
  253. created_by=g.current_user["id"],
  254. )
  255. db.session.commit()
  256. return jsonify(success(result, "数据生产线发布成功")), 201
  257. except (TypeError, ValueError):
  258. db.session.rollback()
  259. return jsonify(failed("数据生产线无法发布", code=409)), 409
  260. except Exception:
  261. db.session.rollback()
  262. current_app.logger.exception("release production line failed")
  263. return jsonify(failed("数据生产线发布失败", code=500)), 500