precis-cli 0.1.3__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.
Files changed (442) hide show
  1. app/__init__.py +16 -0
  2. app/api/__init__.py +32 -0
  3. app/api/dependencies.py +185 -0
  4. app/api/main.py +301 -0
  5. app/api/middleware/__init__.py +34 -0
  6. app/api/middleware/exception_handler.py +126 -0
  7. app/api/middleware/request_logging.py +97 -0
  8. app/api/middleware/token_auth.py +215 -0
  9. app/api/models/__init__.py +151 -0
  10. app/api/models/connection_rules.py +98 -0
  11. app/api/models/files.py +99 -0
  12. app/api/models/full_validation.py +554 -0
  13. app/api/models/project.py +117 -0
  14. app/api/models/projects.py +80 -0
  15. app/api/models/schema.py +121 -0
  16. app/api/models/v2_responses.py +46 -0
  17. app/api/models/validation.py +303 -0
  18. app/api/models/workspace.py +118 -0
  19. app/api/routers/__init__.py +56 -0
  20. app/api/routers/ai/__init__.py +36 -0
  21. app/api/routers/ai/chat.py +243 -0
  22. app/api/routers/ai/generate.py +156 -0
  23. app/api/routers/ai/hardware.py +199 -0
  24. app/api/routers/ai/jobs.py +544 -0
  25. app/api/routers/ai/migrate.py +471 -0
  26. app/api/routers/ai/models.py +352 -0
  27. app/api/routers/ai/ollama.py +108 -0
  28. app/api/routers/ai/providers.py +491 -0
  29. app/api/routers/ai/router.py +44 -0
  30. app/api/routers/ai/stream.py +281 -0
  31. app/api/routers/ai/utils.py +182 -0
  32. app/api/routers/core/__init__.py +34 -0
  33. app/api/routers/core/connection_rules.py +188 -0
  34. app/api/routers/core/data_sources.py +374 -0
  35. app/api/routers/core/regex.py +333 -0
  36. app/api/routers/core/reporting.py +263 -0
  37. app/api/routers/files/__init__.py +24 -0
  38. app/api/routers/files/ops.py +179 -0
  39. app/api/routers/files/transfer.py +101 -0
  40. app/api/routers/preview/__init__.py +35 -0
  41. app/api/routers/preview/content_mode.py +264 -0
  42. app/api/routers/preview/header_row.py +154 -0
  43. app/api/routers/preview/models.py +81 -0
  44. app/api/routers/preview/path_mode.py +440 -0
  45. app/api/routers/preview/router.py +30 -0
  46. app/api/routers/project/__init__.py +61 -0
  47. app/api/routers/project/base.py +111 -0
  48. app/api/routers/project/constraint.py +345 -0
  49. app/api/routers/project/full_config.py +411 -0
  50. app/api/routers/project/full_config_writer.py +327 -0
  51. app/api/routers/project/helpers.py +167 -0
  52. app/api/routers/project/inspection_fix.py +330 -0
  53. app/api/routers/project/manifest.py +605 -0
  54. app/api/routers/project/models.py +177 -0
  55. app/api/routers/project/pattern.py +268 -0
  56. app/api/routers/project/regex.py +358 -0
  57. app/api/routers/project/scanner.py +157 -0
  58. app/api/routers/project/schema.py +567 -0
  59. app/api/routers/project/schema_helpers.py +149 -0
  60. app/api/routers/project/settings.py +399 -0
  61. app/api/routers/project/template.py +325 -0
  62. app/api/routers/project/validation.py +227 -0
  63. app/api/routers/project/view.py +177 -0
  64. app/api/routers/project/workspaces.py +167 -0
  65. app/api/routers/projects/__init__.py +25 -0
  66. app/api/routers/projects/create.py +121 -0
  67. app/api/routers/projects/open.py +103 -0
  68. app/api/routers/projects/scan.py +100 -0
  69. app/api/routers/validation/__init__.py +39 -0
  70. app/api/routers/validation/common.py +263 -0
  71. app/api/routers/validation/content_mode.py +401 -0
  72. app/api/routers/validation/history.py +235 -0
  73. app/api/routers/validation/inline_mode.py +196 -0
  74. app/api/routers/validation/path_mode.py +314 -0
  75. app/api/routers/validation/router.py +51 -0
  76. app/api/services/full_validation_response_builder.py +412 -0
  77. app/api/services/io_error_messages.py +41 -0
  78. app/api/services/preview_service.py +248 -0
  79. app/cli/__init__.py +18 -0
  80. app/cli/__main__.py +30 -0
  81. app/cli/shared_services/__init__.py +42 -0
  82. app/cli/shared_services/config_ops.py +581 -0
  83. app/cli/shared_services/generation_ops.py +160 -0
  84. app/cli/shared_services/project_ops.py +288 -0
  85. app/cli/shell/__init__.py +48 -0
  86. app/cli/shell/commands/__init__.py +63 -0
  87. app/cli/shell/commands/ai/__init__.py +260 -0
  88. app/cli/shell/commands/ai/chat.py +331 -0
  89. app/cli/shell/commands/ai/delete.py +165 -0
  90. app/cli/shell/commands/ai/diff.py +70 -0
  91. app/cli/shell/commands/ai/display.py +428 -0
  92. app/cli/shell/commands/ai/executor.py +225 -0
  93. app/cli/shell/commands/ai/executor_utils.py +271 -0
  94. app/cli/shell/commands/ai/generate.py +285 -0
  95. app/cli/shell/commands/ai/interaction.py +262 -0
  96. app/cli/shell/commands/ai/migrate.py +281 -0
  97. app/cli/shell/commands/ai/resolver.py +224 -0
  98. app/cli/shell/commands/ai/status.py +101 -0
  99. app/cli/shell/commands/ai/switch.py +158 -0
  100. app/cli/shell/commands/ai/utils.py +52 -0
  101. app/cli/shell/commands/base.py +343 -0
  102. app/cli/shell/commands/config/__init__.py +131 -0
  103. app/cli/shell/commands/config/base.py +80 -0
  104. app/cli/shell/commands/config/check.py +246 -0
  105. app/cli/shell/commands/config/edit.py +148 -0
  106. app/cli/shell/commands/config/get.py +111 -0
  107. app/cli/shell/commands/config/init.py +123 -0
  108. app/cli/shell/commands/config/inspect.py +164 -0
  109. app/cli/shell/commands/config/list.py +104 -0
  110. app/cli/shell/commands/config/set.py +119 -0
  111. app/cli/shell/commands/config/show.py +150 -0
  112. app/cli/shell/commands/exit.py +67 -0
  113. app/cli/shell/commands/help.py +104 -0
  114. app/cli/shell/commands/infer_schema.py +156 -0
  115. app/cli/shell/commands/open.py +201 -0
  116. app/cli/shell/commands/project.py +215 -0
  117. app/cli/shell/commands/provider.py +619 -0
  118. app/cli/shell/commands/system.py +170 -0
  119. app/cli/shell/commands/validate.py +455 -0
  120. app/cli/shell/completer.py +231 -0
  121. app/cli/shell/config_storage.py +191 -0
  122. app/cli/shell/exceptions.py +90 -0
  123. app/cli/shell/formatter.py +371 -0
  124. app/cli/shell/interactive_menu.py +348 -0
  125. app/cli/shell/main.py +286 -0
  126. app/cli/shell/parser.py +266 -0
  127. app/cli/start.py +180 -0
  128. app/cli_main.py +52 -0
  129. app/mcp_server.py +311 -0
  130. app/shared/core/__init__.py +18 -0
  131. app/shared/core/app_version.py +59 -0
  132. app/shared/core/config/__init__.py +119 -0
  133. app/shared/core/config/server.py +193 -0
  134. app/shared/core/data_source/__init__.py +106 -0
  135. app/shared/core/data_source/loader.py +348 -0
  136. app/shared/core/data_source/loaders/__init__.py +213 -0
  137. app/shared/core/data_source/loaders/base.py +215 -0
  138. app/shared/core/data_source/loaders/converter.py +337 -0
  139. app/shared/core/data_source/loaders/csv_loader.py +252 -0
  140. app/shared/core/data_source/loaders/excel_loader.py +666 -0
  141. app/shared/core/data_source/loaders/extractor.py +268 -0
  142. app/shared/core/data_source/loaders/json_loader.py +354 -0
  143. app/shared/core/data_source/loaders/registry.py +88 -0
  144. app/shared/core/data_source/loaders/sql_loader.py +313 -0
  145. app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
  146. app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
  147. app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
  148. app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
  149. app/shared/core/data_source/schema_info.py +50 -0
  150. app/shared/core/data_source/specs/__init__.py +128 -0
  151. app/shared/core/data_source/specs/base.py +258 -0
  152. app/shared/core/data_source/specs/csv_source.py +137 -0
  153. app/shared/core/data_source/specs/excel_source.py +141 -0
  154. app/shared/core/data_source/specs/file_base.py +252 -0
  155. app/shared/core/data_source/specs/json_source.py +226 -0
  156. app/shared/core/data_source/specs/sql_source.py +161 -0
  157. app/shared/core/encoding.py +36 -0
  158. app/shared/core/io/__init__.py +24 -0
  159. app/shared/core/io/yaml.py +379 -0
  160. app/shared/core/manifest_schema/__init__.py +52 -0
  161. app/shared/core/manifest_schema/types.py +99 -0
  162. app/shared/core/manifest_schema/version.py +175 -0
  163. app/shared/core/patterns/__init__.py +24 -0
  164. app/shared/core/patterns/loader.py +134 -0
  165. app/shared/core/patterns/writer.py +244 -0
  166. app/shared/core/project/__init__.py +47 -0
  167. app/shared/core/project/constraint/builders/__init__.py +50 -0
  168. app/shared/core/project/constraint/builders/base.py +100 -0
  169. app/shared/core/project/constraint/builders/composite.py +77 -0
  170. app/shared/core/project/constraint/builders/conditional.py +67 -0
  171. app/shared/core/project/constraint/builders/foreign_key.py +53 -0
  172. app/shared/core/project/constraint/builders/registry.py +64 -0
  173. app/shared/core/project/constraint/builders/scripted.py +51 -0
  174. app/shared/core/project/constraint/builders/single_column.py +86 -0
  175. app/shared/core/project/constraint/builders/unique.py +53 -0
  176. app/shared/core/project/constraint/factory.py +170 -0
  177. app/shared/core/project/constraint/reader.py +214 -0
  178. app/shared/core/project/constraint/registry.py +233 -0
  179. app/shared/core/project/constraint/types/__init__.py +63 -0
  180. app/shared/core/project/constraint/types/constraint_file.py +261 -0
  181. app/shared/core/project/constraint/types/refs.py +460 -0
  182. app/shared/core/project/constraint/types.py +28 -0
  183. app/shared/core/project/constraint/writer.py +181 -0
  184. app/shared/core/project/loader/__init__.py +56 -0
  185. app/shared/core/project/loader/loader.py +30 -0
  186. app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
  187. app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
  188. app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
  189. app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
  190. app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
  191. app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
  192. app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
  193. app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
  194. app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
  195. app/shared/core/project/loader/loader_parts/main.py +432 -0
  196. app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
  197. app/shared/core/project/loader/loader_parts/runtime.py +125 -0
  198. app/shared/core/project/loader/types.py +246 -0
  199. app/shared/core/project/manifest/coverage.py +391 -0
  200. app/shared/core/project/manifest/reader.py +260 -0
  201. app/shared/core/project/manifest/types.py +91 -0
  202. app/shared/core/project/manifest/types_parts/__init__.py +60 -0
  203. app/shared/core/project/manifest/types_parts/constants.py +40 -0
  204. app/shared/core/project/manifest/types_parts/data_source.py +68 -0
  205. app/shared/core/project/manifest/types_parts/info.py +59 -0
  206. app/shared/core/project/manifest/types_parts/manifest.py +294 -0
  207. app/shared/core/project/manifest/types_parts/refs.py +153 -0
  208. app/shared/core/project/manifest/types_parts/settings.py +92 -0
  209. app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
  210. app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
  211. app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
  212. app/shared/core/project/manifest/types_parts/template.py +66 -0
  213. app/shared/core/project/manifest/writer.py +262 -0
  214. app/shared/core/project/manual_data/__init__.py +27 -0
  215. app/shared/core/project/manual_data/types.py +75 -0
  216. app/shared/core/project/regex/reader.py +197 -0
  217. app/shared/core/project/regex/types.py +405 -0
  218. app/shared/core/project/regex/writer.py +123 -0
  219. app/shared/core/project/schema/reader.py +170 -0
  220. app/shared/core/project/schema/types.py +47 -0
  221. app/shared/core/project/schema/types_parts/__init__.py +36 -0
  222. app/shared/core/project/schema/types_parts/column.py +174 -0
  223. app/shared/core/project/schema/types_parts/column_utils.py +72 -0
  224. app/shared/core/project/schema/types_parts/constraint.py +165 -0
  225. app/shared/core/project/schema/types_parts/schema_id.py +66 -0
  226. app/shared/core/project/schema/types_parts/source.py +255 -0
  227. app/shared/core/project/schema/types_parts/source_options.py +347 -0
  228. app/shared/core/project/schema/types_parts/table.py +230 -0
  229. app/shared/core/project/schema/writer.py +139 -0
  230. app/shared/core/project/schema_ref_check.py +95 -0
  231. app/shared/core/project/template/__init__.py +27 -0
  232. app/shared/core/project/template/expander.py +263 -0
  233. app/shared/core/project/template/reader.py +120 -0
  234. app/shared/core/project/template/types.py +114 -0
  235. app/shared/core/project/transform/reader.py +76 -0
  236. app/shared/core/project/transform/types.py +116 -0
  237. app/shared/core/project/transform/writer.py +84 -0
  238. app/shared/core/pydantic_messages.py +59 -0
  239. app/shared/core/reporter/__init__.py +46 -0
  240. app/shared/core/reporter/reporter.py +220 -0
  241. app/shared/core/reporter/reporters/__init__.py +65 -0
  242. app/shared/core/reporter/reporters/base.py +188 -0
  243. app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
  244. app/shared/core/reporter/reporters/email_reporter.py +271 -0
  245. app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
  246. app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
  247. app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
  248. app/shared/core/utils/__init__.py +18 -0
  249. app/shared/core/utils/path_utils.py +60 -0
  250. app/shared/core/utils/regex_extract.py +113 -0
  251. app/shared/domain/__init__.py +114 -0
  252. app/shared/domain/constraints/__init__.py +76 -0
  253. app/shared/domain/constraints/allowed_values.py +211 -0
  254. app/shared/domain/constraints/base.py +141 -0
  255. app/shared/domain/constraints/charset.py +337 -0
  256. app/shared/domain/constraints/composite.py +174 -0
  257. app/shared/domain/constraints/condition_registry.py +130 -0
  258. app/shared/domain/constraints/conditional.py +629 -0
  259. app/shared/domain/constraints/date_logic.py +731 -0
  260. app/shared/domain/constraints/foreign_key.py +261 -0
  261. app/shared/domain/constraints/key_normalization.py +56 -0
  262. app/shared/domain/constraints/not_null.py +185 -0
  263. app/shared/domain/constraints/range.py +360 -0
  264. app/shared/domain/constraints/regex.py +218 -0
  265. app/shared/domain/constraints/scripted.py +276 -0
  266. app/shared/domain/constraints/unique.py +233 -0
  267. app/shared/domain/data_engine.py +384 -0
  268. app/shared/domain/data_types.py +113 -0
  269. app/shared/domain/data_types_parts/__init__.py +67 -0
  270. app/shared/domain/data_types_parts/base.py +204 -0
  271. app/shared/domain/data_types_parts/composite.py +250 -0
  272. app/shared/domain/data_types_parts/expression.py +202 -0
  273. app/shared/domain/data_types_parts/extracted.py +106 -0
  274. app/shared/domain/data_types_parts/json_types.py +234 -0
  275. app/shared/domain/data_types_parts/scalars.py +719 -0
  276. app/shared/domain/data_types_parts/sequence.py +129 -0
  277. app/shared/domain/dataset_schema.py +48 -0
  278. app/shared/domain/eval_sandbox.py +63 -0
  279. app/shared/domain/expression_system.py +366 -0
  280. app/shared/domain/regex_flags.py +53 -0
  281. app/shared/domain/schema/builder.py +223 -0
  282. app/shared/domain/schema/models.py +339 -0
  283. app/shared/domain/transforms/__init__.py +28 -0
  284. app/shared/domain/transforms/aggregate.py +141 -0
  285. app/shared/domain/transforms/base.py +171 -0
  286. app/shared/domain/transforms/cast_type.py +120 -0
  287. app/shared/domain/transforms/concat.py +103 -0
  288. app/shared/domain/transforms/conditional_assign.py +114 -0
  289. app/shared/domain/transforms/date_format.py +81 -0
  290. app/shared/domain/transforms/digits.py +77 -0
  291. app/shared/domain/transforms/drop_duplicates.py +94 -0
  292. app/shared/domain/transforms/fill_na.py +94 -0
  293. app/shared/domain/transforms/filter_rows.py +97 -0
  294. app/shared/domain/transforms/lookup.py +78 -0
  295. app/shared/domain/transforms/lower_case.py +70 -0
  296. app/shared/domain/transforms/map_value.py +90 -0
  297. app/shared/domain/transforms/math_expr.py +112 -0
  298. app/shared/domain/transforms/modulo.py +82 -0
  299. app/shared/domain/transforms/regex_extract.py +103 -0
  300. app/shared/domain/transforms/registry.py +95 -0
  301. app/shared/domain/transforms/replace.py +88 -0
  302. app/shared/domain/transforms/sort_rows.py +94 -0
  303. app/shared/domain/transforms/string_split.py +84 -0
  304. app/shared/domain/transforms/strip.py +73 -0
  305. app/shared/domain/transforms/substring.py +92 -0
  306. app/shared/domain/transforms/upper_case.py +70 -0
  307. app/shared/domain/transforms/weighted_sum.py +104 -0
  308. app/shared/domain/validation_constraints.py +69 -0
  309. app/shared/services/__init__.py +62 -0
  310. app/shared/services/ai/__init__.py +48 -0
  311. app/shared/services/ai/agent/__init__.py +40 -0
  312. app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
  313. app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
  314. app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
  315. app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
  316. app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
  317. app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
  318. app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
  319. app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
  320. app/shared/services/ai/agent/executor.py +554 -0
  321. app/shared/services/ai/agent/memory.py +256 -0
  322. app/shared/services/ai/agent/planner.py +282 -0
  323. app/shared/services/ai/agent/tool_registry.py +296 -0
  324. app/shared/services/ai/agent/tools/__init__.py +32 -0
  325. app/shared/services/ai/agent/tools/config_generate.py +133 -0
  326. app/shared/services/ai/agent/tools/config_refine.py +109 -0
  327. app/shared/services/ai/agent/tools/config_validate.py +403 -0
  328. app/shared/services/ai/agent/tools/merge_results.py +136 -0
  329. app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
  330. app/shared/services/ai/agent/tools/script_parse.py +359 -0
  331. app/shared/services/ai/agent/types.py +138 -0
  332. app/shared/services/ai/chat_agent_runner.py +596 -0
  333. app/shared/services/ai/chat_orchestrator.py +725 -0
  334. app/shared/services/ai/failure_messages.py +65 -0
  335. app/shared/services/ai/job_storage.py +274 -0
  336. app/shared/services/ai/migrate_service.py +550 -0
  337. app/shared/services/ai/streaming/__init__.py +65 -0
  338. app/shared/services/ai/streaming/event_journal.py +197 -0
  339. app/shared/services/ai/streaming/orchestrator.py +262 -0
  340. app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
  341. app/shared/services/ai/streaming/sse_response.py +148 -0
  342. app/shared/services/ai/streaming/types.py +73 -0
  343. app/shared/services/ai/types.py +213 -0
  344. app/shared/services/ai/utils.py +322 -0
  345. app/shared/services/diff/config_diff.py +320 -0
  346. app/shared/services/hardware.py +237 -0
  347. app/shared/services/llm/__init__.py +62 -0
  348. app/shared/services/llm/actions/__init__.py +42 -0
  349. app/shared/services/llm/actions/_canvas_validator.py +147 -0
  350. app/shared/services/llm/actions/_constraint_validator.py +401 -0
  351. app/shared/services/llm/actions/_regex_validator.py +85 -0
  352. app/shared/services/llm/actions/_schema_validator.py +82 -0
  353. app/shared/services/llm/actions/_settings_validator.py +76 -0
  354. app/shared/services/llm/actions/_transform_validator.py +78 -0
  355. app/shared/services/llm/actions/action_handlers.py +400 -0
  356. app/shared/services/llm/actions/action_parser.py +63 -0
  357. app/shared/services/llm/actions/action_processor.py +468 -0
  358. app/shared/services/llm/actions/action_validator.py +311 -0
  359. app/shared/services/llm/actions/diff_compute.py +195 -0
  360. app/shared/services/llm/actions/regex_handlers.py +284 -0
  361. app/shared/services/llm/actions/registry.py +336 -0
  362. app/shared/services/llm/actions/schema_handlers.py +335 -0
  363. app/shared/services/llm/actions/settings_handlers.py +180 -0
  364. app/shared/services/llm/actions/specs.py +259 -0
  365. app/shared/services/llm/actions/transform_handlers.py +279 -0
  366. app/shared/services/llm/actions/validation_types.py +147 -0
  367. app/shared/services/llm/cache/__init__.py +18 -0
  368. app/shared/services/llm/cache/response_cache.py +80 -0
  369. app/shared/services/llm/chat/__init__.py +35 -0
  370. app/shared/services/llm/chat/chat_system_prompt.py +561 -0
  371. app/shared/services/llm/chat/response_parser.py +296 -0
  372. app/shared/services/llm/config/__init__.py +45 -0
  373. app/shared/services/llm/config/crypto.py +135 -0
  374. app/shared/services/llm/config/loader.py +258 -0
  375. app/shared/services/llm/config/models.py +202 -0
  376. app/shared/services/llm/config/presets.py +128 -0
  377. app/shared/services/llm/config_generator.py +163 -0
  378. app/shared/services/llm/constraints/__init__.py +41 -0
  379. app/shared/services/llm/constraints/constraint_builder.py +177 -0
  380. app/shared/services/llm/constraints/constraint_deletion.py +84 -0
  381. app/shared/services/llm/constraints/constraint_id.py +174 -0
  382. app/shared/services/llm/constraints/frontend_instructions.py +333 -0
  383. app/shared/services/llm/constraints/inline_batch.py +279 -0
  384. app/shared/services/llm/discovery/__init__.py +35 -0
  385. app/shared/services/llm/discovery/scanner.py +181 -0
  386. app/shared/services/llm/generation/__init__.py +42 -0
  387. app/shared/services/llm/generation/agent_wiring.py +156 -0
  388. app/shared/services/llm/generation/config_builder.py +386 -0
  389. app/shared/services/llm/generation/errors.py +36 -0
  390. app/shared/services/llm/generation/existing_config.py +99 -0
  391. app/shared/services/llm/generation/profiler.py +279 -0
  392. app/shared/services/llm/generation/prompt_builder.py +199 -0
  393. app/shared/services/llm/generation/response_parser.py +144 -0
  394. app/shared/services/llm/generation/service.py +622 -0
  395. app/shared/services/llm/models.py +104 -0
  396. app/shared/services/llm/providers/__init__.py +48 -0
  397. app/shared/services/llm/providers/base.py +310 -0
  398. app/shared/services/llm/providers/cached_provider.py +90 -0
  399. app/shared/services/llm/providers/ollama.py +498 -0
  400. app/shared/services/llm/providers/openai.py +334 -0
  401. app/shared/services/llm/providers/registry.py +108 -0
  402. app/shared/services/llm/schema_resolver.py +141 -0
  403. app/shared/services/llm/suggestion_utils.py +219 -0
  404. app/shared/services/llm/validate_executor.py +163 -0
  405. app/shared/services/llm/yaml_io.py +406 -0
  406. app/shared/services/preview/__init__.py +18 -0
  407. app/shared/services/preview/loader.py +145 -0
  408. app/shared/services/preview/path_validation.py +138 -0
  409. app/shared/services/project_loader.py +57 -0
  410. app/shared/services/schema_inference.py +260 -0
  411. app/shared/services/schema_runtime_builder.py +120 -0
  412. app/shared/services/validation/__init__.py +52 -0
  413. app/shared/services/validation/chunked_loader.py +509 -0
  414. app/shared/services/validation/dag/__init__.py +35 -0
  415. app/shared/services/validation/dag/builder.py +141 -0
  416. app/shared/services/validation/dag/executor.py +225 -0
  417. app/shared/services/validation/dag/sorter.py +77 -0
  418. app/shared/services/validation/data_loader.py +231 -0
  419. app/shared/services/validation/engine.py +446 -0
  420. app/shared/services/validation/executor.py +1030 -0
  421. app/shared/services/validation/extractors.py +266 -0
  422. app/shared/services/validation/history.py +209 -0
  423. app/shared/services/validation/json_payload.py +136 -0
  424. app/shared/services/validation/loader.py +152 -0
  425. app/shared/services/validation/memory_monitor.py +179 -0
  426. app/shared/services/validation/postprocess.py +208 -0
  427. app/shared/services/validation/progress.py +64 -0
  428. app/shared/services/validation/report_export.py +222 -0
  429. app/shared/services/validation/resolver.py +180 -0
  430. app/shared/services/validation/service.py +418 -0
  431. app/shared/services/validation/types.py +238 -0
  432. app/shared/services/validation/validators/__init__.py +36 -0
  433. app/shared/services/validation/validators/adapter.py +182 -0
  434. app/shared/services/validation/validators/base.py +332 -0
  435. app/shared/services/validation/validators/composite.py +233 -0
  436. app/shared/services/validation/validators/date_logic.py +276 -0
  437. app/start_server.py +133 -0
  438. precis_cli-0.1.3.dist-info/METADATA +180 -0
  439. precis_cli-0.1.3.dist-info/RECORD +442 -0
  440. precis_cli-0.1.3.dist-info/WHEEL +5 -0
  441. precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
  442. precis_cli-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,284 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """@fileoverview Regex 动作处理器模块
17
+
18
+ 功能概述:
19
+ - 处理 AI 生成的 Regex CRUD 动作(ADD_REGEX / UPDATE_REGEX / DELETE_REGEX)
20
+ - 通过 core 层 writer 持久化 Regex YAML 文件
21
+ - 同步更新 project.precis.yaml 中的 RegexRef
22
+
23
+ 架构设计:
24
+ - 复用 core/project/regex/writer.py 的 save_regex_node()
25
+ - 复用 core/project/manifest/writer.py 的 ensure_regex_ref() / save_manifest()
26
+ - 使用 RegexNodeFile Pydantic 模型确保类型安全
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ import os
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ import yaml
37
+
38
+ from app.shared.core.project.manifest.reader import load_manifest
39
+ from app.shared.core.project.manifest.writer import ensure_regex_ref, save_manifest
40
+ from app.shared.core.project.regex.types import RegexNodeFile, RegexSourceRef
41
+ from app.shared.core.project.regex.writer import save_regex_node
42
+ from app.shared.services.llm.yaml_io import FileLock, atomic_write_yaml
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ VALID_MATCH_MODES = {"full", "partial", "extract"}
47
+
48
+
49
+ def _sanitize_resource_id(resource_id: str) -> str:
50
+ cleaned = os.path.basename(resource_id)
51
+ if "/" in resource_id or "\\" in resource_id or ".." in resource_id:
52
+ raise ValueError(f"非法的资源 ID: {resource_id!r}")
53
+ return cleaned
54
+
55
+
56
+ def process_regex_action(action: dict[str, Any], workspace_path: str) -> dict[str, Any]:
57
+ """
58
+ @methoddesc 处理 Regex 动作
59
+
60
+ 根据 actionType 分发到对应的处理函数。
61
+
62
+ 参数:
63
+ action: 动作字典,包含 actionType 和 regexSpec
64
+ workspace_path: 项目工作区路径
65
+
66
+ 返回:
67
+ 处理结果字典 {"success": bool, "message": str}
68
+ """
69
+ action_type = action.get("actionType", "")
70
+ spec = action.get("regexSpec", {})
71
+
72
+ if action_type == "ADD_REGEX":
73
+ return _add_regex(spec, workspace_path)
74
+ elif action_type == "UPDATE_REGEX":
75
+ return _update_regex(spec, workspace_path)
76
+ elif action_type == "DELETE_REGEX":
77
+ return _delete_regex(spec, workspace_path)
78
+ else:
79
+ return {"success": False, "message": f"未知的 Regex 动作类型: {action_type}"}
80
+
81
+
82
+ def _add_regex(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
83
+ """创建新的 Regex YAML 文件"""
84
+ regex_name = spec.get("name", "")
85
+ regex_id = spec.get("regexId") or spec.get("id") or regex_name
86
+ try:
87
+ regex_id = _sanitize_resource_id(regex_id)
88
+ except ValueError:
89
+ return {"success": False, "message": f"非法的 Regex ID: {regex_id}"}
90
+ pattern = spec.get("pattern", "")
91
+ match_mode = spec.get("matchMode", "full")
92
+ case_sensitive = spec.get("caseSensitive", False)
93
+ description = spec.get("description")
94
+ target_node_id = spec.get("targetNodeId")
95
+ target_column = spec.get("targetColumn")
96
+
97
+ if not regex_name:
98
+ return {"success": False, "message": "Regex 名称不能为空"}
99
+ if not pattern:
100
+ return {"success": False, "message": "Regex 模式不能为空"}
101
+
102
+ if match_mode not in VALID_MATCH_MODES:
103
+ match_mode = "full"
104
+
105
+ workspace = Path(workspace_path)
106
+ # V2 标准:regex 文件统一存放在 regex/ 目录(manifest 登记与全仓读取方均认此目录),
107
+ # 旧版写入 regex_nodes/ 会导致 manifest 引用的路径与实际文件位置漂移
108
+ regex_dir = workspace / "regex"
109
+ regex_dir.mkdir(parents=True, exist_ok=True)
110
+
111
+ regex_file = regex_dir / f"{regex_id}.regex.yaml"
112
+ # 已存在检测同时覆盖新旧目录(历史版本曾写入 regex_nodes/),避免跨目录产生同 ID 重复文件
113
+ if regex_file.exists() or _find_regex_file(workspace_path, regex_id):
114
+ # 提示 Agent 改用 ADD_TO_CANVAS(同 schema_handlers 的引导逻辑)
115
+ return {
116
+ "success": False,
117
+ "message": (
118
+ f"Regex 文件已存在: {regex_id}.regex.yaml。"
119
+ f"若用户想把已存在的资源显示到画布,请改用 actionType=ADD_TO_CANVAS"
120
+ f"(canvasSpec.resourceKind='regex'),它不会重复创建文件。"
121
+ ),
122
+ }
123
+
124
+ # 构建 source_ref
125
+ source_ref = None
126
+ if target_node_id and target_column:
127
+ source_ref = RegexSourceRef(table_id=target_node_id, column_id=target_column)
128
+
129
+ try:
130
+ regex_node = RegexNodeFile(
131
+ version=2,
132
+ id=regex_id,
133
+ name=regex_name,
134
+ pattern=pattern,
135
+ match_mode=match_mode,
136
+ case_sensitive=case_sensitive,
137
+ description=description,
138
+ enabled=True,
139
+ uses_pattern=None,
140
+ flags="",
141
+ input_from_node=None,
142
+ input_column=None,
143
+ source_ref=source_ref,
144
+ source_column_name=target_column,
145
+ )
146
+ save_regex_node(regex_node, regex_file)
147
+ except Exception as e:
148
+ return {"success": False, "message": f"写入 Regex 文件失败: {e}"}
149
+
150
+ # 更新 manifest —— §2.7: 登记失败时回滚删除已写文件(对齐 Schema 路径),
151
+ # 否则磁盘留下孤儿 .regex.yaml:重启后不被加载、不报错、画布节点还在
152
+ try:
153
+ _ensure_manifest_regex_ref(workspace_path, regex_id)
154
+ except Exception as e:
155
+ Path(regex_file).unlink(missing_ok=True)
156
+ return {"success": False, "message": f"更新 manifest 引用失败(已回滚 Regex 文件): {e}"}
157
+
158
+ logger.info(f"[RegexHandler] 创建 Regex: {regex_id}")
159
+ return {"success": True, "message": regex_id}
160
+
161
+
162
+ def _update_regex(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
163
+ """更新现有 Regex"""
164
+ regex_id = spec.get("regexId") or spec.get("id") or spec.get("name", "")
165
+ try:
166
+ regex_id = _sanitize_resource_id(regex_id)
167
+ except ValueError:
168
+ return {"success": False, "message": f"非法的 Regex ID: {regex_id}"}
169
+
170
+ if not regex_id:
171
+ return {"success": False, "message": "缺少 Regex ID"}
172
+
173
+ regex_file = _find_regex_file(workspace_path, regex_id)
174
+ if not regex_file:
175
+ return {"success": False, "message": f"Regex 文件不存在: {regex_id}"}
176
+
177
+ try:
178
+ with FileLock(str(regex_file)):
179
+ with open(regex_file, encoding="utf-8") as f:
180
+ data = yaml.safe_load(f) or {}
181
+
182
+ if spec.get("pattern"):
183
+ data["pattern"] = spec["pattern"]
184
+ if spec.get("name"):
185
+ data["name"] = spec["name"]
186
+ if spec.get("matchMode"):
187
+ data["match_mode"] = spec["matchMode"]
188
+ if "caseSensitive" in spec:
189
+ data["case_sensitive"] = spec["caseSensitive"]
190
+ if "description" in spec:
191
+ data["description"] = spec["description"]
192
+ if spec.get("targetNodeId") and spec.get("targetColumn"):
193
+ data["source_ref"] = {"table_id": spec["targetNodeId"], "column_id": spec["targetColumn"]}
194
+ data["source_column_name"] = spec["targetColumn"]
195
+
196
+ # §2.10: UPDATE 显式整体替换语义(与 UPDATE_TRANSFORM/UPDATE_SCHEMA 同族对齐)
197
+ atomic_write_yaml(regex_file, data, preserve_format=False)
198
+
199
+ except Exception as e:
200
+ return {"success": False, "message": f"更新 Regex 失败: {e}"}
201
+
202
+ logger.info(f"[RegexHandler] 更新 Regex: {regex_id}")
203
+ return {"success": True, "message": regex_id}
204
+
205
+
206
+ def _delete_regex(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
207
+ """删除 Regex 文件"""
208
+ regex_id = spec.get("regexId") or spec.get("id") or spec.get("name", "")
209
+ try:
210
+ regex_id = _sanitize_resource_id(regex_id)
211
+ except ValueError:
212
+ return {"success": False, "message": f"非法的 Regex ID: {regex_id}"}
213
+
214
+ if not regex_id:
215
+ return {"success": False, "message": "缺少 Regex ID"}
216
+
217
+ regex_file = _find_regex_file(workspace_path, regex_id)
218
+ if not regex_file:
219
+ return {"success": False, "message": f"Regex 文件不存在: {regex_id}"}
220
+
221
+ try:
222
+ regex_file.unlink()
223
+ except OSError as e:
224
+ return {"success": False, "message": f"删除 Regex 文件失败: {e}"}
225
+
226
+ _remove_manifest_regex_ref(workspace_path, regex_id)
227
+
228
+ logger.info(f"[RegexHandler] 删除 Regex: {regex_id}")
229
+ return {"success": True, "message": regex_id}
230
+
231
+
232
+ def _find_regex_file(workspace_path: str, regex_id: str) -> Path | None:
233
+ """在工作区中查找 regex 文件"""
234
+ # 检查多个可能的目录名
235
+ for dirname in ("regex_nodes", "regex"):
236
+ regex_dir = Path(workspace_path) / dirname
237
+ if not regex_dir.exists():
238
+ continue
239
+ # 按 ID 精确匹配
240
+ for rf in regex_dir.glob("*.yaml"):
241
+ try:
242
+ with open(rf, encoding="utf-8") as f:
243
+ data = yaml.safe_load(f) or {}
244
+ if data.get("id") == regex_id or data.get("name") == regex_id:
245
+ return rf
246
+ except Exception:
247
+ continue
248
+ # 按文件名匹配
249
+ candidate = regex_dir / f"{regex_id}.regex.yaml"
250
+ if candidate.exists():
251
+ return candidate
252
+ candidate = regex_dir / f"{regex_id}.yaml"
253
+ if candidate.exists():
254
+ return candidate
255
+
256
+ return None
257
+
258
+
259
+ def _ensure_manifest_regex_ref(workspace_path: str, regex_id: str) -> None:
260
+ """确保 manifest 中包含指定 Regex 引用。
261
+
262
+ §2.7: 异常向上传播(不再吞掉只留 warning)——调用方据此回滚已写的 Regex 文件。
263
+ """
264
+ manifest_path = Path(workspace_path) / "project.precis.yaml"
265
+ if not manifest_path.exists():
266
+ return
267
+
268
+ manifest = load_manifest(manifest_path)
269
+ ensure_regex_ref(manifest, regex_id)
270
+ save_manifest(manifest, manifest_path)
271
+
272
+
273
+ def _remove_manifest_regex_ref(workspace_path: str, regex_id: str) -> None:
274
+ """从 manifest 中移除指定 Regex 引用"""
275
+ manifest_path = Path(workspace_path) / "project.precis.yaml"
276
+ if not manifest_path.exists():
277
+ return
278
+
279
+ try:
280
+ manifest = load_manifest(manifest_path)
281
+ manifest.regex_nodes = [r for r in manifest.regex_nodes if r.id != regex_id]
282
+ save_manifest(manifest, manifest_path)
283
+ except Exception as e:
284
+ logger.warning(f"[RegexHandler] 更新 manifest 引用失败: {e}")
@@ -0,0 +1,336 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """@fileoverview AI 动作注册表 — 单一事实源
17
+
18
+ 本模块是所有 AI 动作类型(actionType)的唯一权威定义处。
19
+ 其他模块(解析、校验、执行、前端指令生成、提示词)一律从此派生,
20
+ 禁止重复硬编码动作类型集合,以消灭"同步遗漏"类 bug(如计数写错、
21
+ enum 漏条目、spec 字段映射不一致)。
22
+
23
+ 三类内容在此收敛:
24
+ 1. 动作类型(15种)及其 spec 字段、分类、读写性
25
+ 2. 派生集合:ALL_ACTION_TYPES / BY_CATEGORY / READ_ONLY_TYPES / SPEC_FIELD_FOR 等
26
+ 3. 子类型白名单:约束类型、转换子类型、数据类型、设置分类(原散落 2-4 处)
27
+
28
+ 设计原则:
29
+ - 注册表只描述"是什么"(声明性),不描述"怎么做"(执行逻辑仍在各 handler)。
30
+ - 派生属性用普通函数/常量,导入即用,零运行时开销。
31
+ - 新增一个动作 = 在 ACTIONS 加一行 + 配套 validator/handler,其余全自动派生。
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from dataclasses import dataclass
37
+
38
+ # =============================================================================
39
+ # 子类型白名单(规范来源)——原散落于 action_validator / response_parser /
40
+ # suggestion_utils / transform_handlers / _schema_validator / settings_handlers 等
41
+ # =============================================================================
42
+
43
+ # 约束类型(PascalCase 正名)——与前端 CONSTRAINT_TYPE_MAP 的 value 侧一致
44
+ # 注意:仅含"标准名",大写别名(NOT_NULL 等)由 ALIAS_CONSTRAINT_TYPES 单独管理
45
+ CONSTRAINT_TYPES: frozenset[str] = frozenset(
46
+ {
47
+ "NotNull",
48
+ "Unique",
49
+ "Range",
50
+ "AllowedValues",
51
+ "ForeignKey",
52
+ "Conditional",
53
+ "Scripted",
54
+ "DateLogic",
55
+ "Charset",
56
+ "Composite",
57
+ }
58
+ )
59
+
60
+ # 约束类型大写别名(LLM 可能输出)→ 标准名。
61
+ # 这是唯一的别名映射表,取代散落各处的重复副本。
62
+ # Scripted 的别名 REGEX 保留(历史兼容:旧约束用 REGEX 表示正则脚本约束)。
63
+ CONSTRAINT_TYPE_ALIASES: dict[str, str] = {
64
+ "NOT_NULL": "NotNull",
65
+ "UNIQUE": "Unique",
66
+ "RANGE": "Range",
67
+ "ALLOWED_VALUES": "AllowedValues",
68
+ "FOREIGN_KEY": "ForeignKey",
69
+ "CONDITIONAL": "Conditional",
70
+ "DATE_LOGIC": "DateLogic",
71
+ "CHARSET": "Charset",
72
+ "COMPOSITE": "Composite",
73
+ "REGEX": "Scripted",
74
+ }
75
+
76
+ # 约束类型全集(标准名 + 别名),供校验白名单使用
77
+ ALL_CONSTRAINT_TYPES: frozenset[str] = CONSTRAINT_TYPES | frozenset(CONSTRAINT_TYPE_ALIASES.keys())
78
+
79
+ # 需要特定参数的约束类型(供参数完整性校验)
80
+ # 注意:Range 的 min/max 实际允许"至少一个"(见 _constraint_validator 特殊处理),
81
+ # 此处仅声明"需要关注的参数键",具体宽松度由 validator 决定。
82
+ CONSTRAINT_REQUIRED_PARAMS: dict[str, list[str]] = {
83
+ "Range": ["min", "max"],
84
+ "AllowedValues": ["allowedValues"],
85
+ "ForeignKey": ["toTableId", "toColumnId"],
86
+ }
87
+
88
+ # 转换子类型白名单(22种)——原 response_parser(21条) 与 transform_handlers(22条) 不一致,
89
+ # 以 transform_handlers 的 22 条为准(response_parser 缺 FillNA,实际是有效类型)。
90
+ TRANSFORM_SUB_TYPES: frozenset[str] = frozenset(
91
+ {
92
+ "StringSplit",
93
+ "RegexExtract",
94
+ "MathExpr",
95
+ "DateFormat",
96
+ "Lookup",
97
+ "Strip",
98
+ "UpperCase",
99
+ "LowerCase",
100
+ "Replace",
101
+ "FillNA",
102
+ "FilterRows",
103
+ "DropDuplicates",
104
+ "CastType",
105
+ "Concat",
106
+ "Substring",
107
+ "Aggregate",
108
+ "ConditionalAssign",
109
+ "SortRows",
110
+ "Digits",
111
+ "WeightedSum",
112
+ "Modulo",
113
+ "MapValue",
114
+ }
115
+ )
116
+
117
+ # Schema 列数据类型白名单
118
+ DATA_TYPES: frozenset[str] = frozenset({"string", "integer", "decimal", "boolean", "datetime", "date", "time", "float"})
119
+
120
+ # 设置分类白名单
121
+ SETTINGS_CATEGORIES: frozenset[str] = frozenset({"validation", "fileProcessing", "scriptSecurity"})
122
+
123
+ # ADD_TO_CANVAS 支持的资源类型
124
+ CANVAS_RESOURCE_KINDS: frozenset[str] = frozenset({"schema", "regex", "constraint", "transform"})
125
+
126
+
127
+ # =============================================================================
128
+ # 动作类型定义
129
+ # =============================================================================
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class ActionTypeDef:
134
+ """单个动作类型的声明性定义。
135
+
136
+ Attributes:
137
+ type: actionType 字符串,如 "ADD_SCHEMA"
138
+ spec_field: 对应的 spec 字段名,如 "schemaSpec"(None 表示无 spec,如 VALIDATE_PROJECT)
139
+ category: 动作家族,用于分类桶:constraint/schema/regex/transform/settings/canvas/validate
140
+ read_only: True=不写盘(纯读),False=写盘动作
141
+ """
142
+
143
+ type: str
144
+ spec_field: str | None
145
+ category: str
146
+ read_only: bool
147
+
148
+
149
+ # 动作类型唯一注册表——新增动作只需在此加一行,其余派生属性自动更新
150
+ ACTIONS: dict[str, ActionTypeDef] = {
151
+ # 约束 CRUD(写盘)
152
+ "ADD_CONSTRAINT_NODE": ActionTypeDef("ADD_CONSTRAINT_NODE", "constraintSpec", "constraint", False),
153
+ "UPDATE_CONSTRAINT_NODE": ActionTypeDef("UPDATE_CONSTRAINT_NODE", "constraintSpec", "constraint", False),
154
+ "DELETE_CONSTRAINT_NODE": ActionTypeDef("DELETE_CONSTRAINT_NODE", "constraintSpec", "constraint", False),
155
+ # Schema CRUD(写盘)
156
+ "ADD_SCHEMA": ActionTypeDef("ADD_SCHEMA", "schemaSpec", "schema", False),
157
+ "UPDATE_SCHEMA": ActionTypeDef("UPDATE_SCHEMA", "schemaSpec", "schema", False),
158
+ "DELETE_SCHEMA": ActionTypeDef("DELETE_SCHEMA", "schemaSpec", "schema", False),
159
+ # Regex CRUD(写盘)
160
+ "ADD_REGEX": ActionTypeDef("ADD_REGEX", "regexSpec", "regex", False),
161
+ "UPDATE_REGEX": ActionTypeDef("UPDATE_REGEX", "regexSpec", "regex", False),
162
+ "DELETE_REGEX": ActionTypeDef("DELETE_REGEX", "regexSpec", "regex", False),
163
+ # Transform CRUD(写盘)
164
+ "ADD_TRANSFORM": ActionTypeDef("ADD_TRANSFORM", "transformSpec", "transform", False),
165
+ "UPDATE_TRANSFORM": ActionTypeDef("UPDATE_TRANSFORM", "transformSpec", "transform", False),
166
+ "DELETE_TRANSFORM": ActionTypeDef("DELETE_TRANSFORM", "transformSpec", "transform", False),
167
+ # 设置(写盘)
168
+ "UPDATE_SETTINGS": ActionTypeDef("UPDATE_SETTINGS", "settingsSpec", "settings", False),
169
+ # 校验(只读)
170
+ "VALIDATE_PROJECT": ActionTypeDef("VALIDATE_PROJECT", "constraintSpec", "validate", True),
171
+ # 显示到画布(只读,不写盘)
172
+ "ADD_TO_CANVAS": ActionTypeDef("ADD_TO_CANVAS", "canvasSpec", "canvas", True),
173
+ }
174
+
175
+
176
+ # =============================================================================
177
+ # 派生属性(自动生成,禁止手动维护)
178
+ # =============================================================================
179
+
180
+ # 全部动作类型列表(顺序稳定,供 LLM enum 与提示词清单)
181
+ ALL_ACTION_TYPES: list[str] = [a.type for a in ACTIONS.values()]
182
+
183
+ # LLM 工具 schema 的 enum 数组
184
+ ACTION_ENUM: list[str] = ALL_ACTION_TYPES
185
+
186
+ # 动作总数(消灭硬编码 "15种")
187
+ ACTION_COUNT: int = len(ACTIONS)
188
+
189
+ # 按 category 分组的动作集合(先收集到普通 set,再冻结为 frozenset)
190
+ _category_buckets: dict[str, set[str]] = {}
191
+ for _a in ACTIONS.values():
192
+ _category_buckets.setdefault(_a.category, set()).add(_a.type)
193
+ BY_CATEGORY: dict[str, frozenset[str]] = {k: frozenset(v) for k, v in _category_buckets.items()}
194
+
195
+ # 家族集合的便捷别名(最常引用的几个)
196
+ CONSTRAINT_ACTION_TYPES: frozenset[str] = BY_CATEGORY["constraint"]
197
+ SCHEMA_ACTION_TYPES: frozenset[str] = BY_CATEGORY["schema"]
198
+ REGEX_ACTION_TYPES: frozenset[str] = BY_CATEGORY["regex"]
199
+ TRANSFORM_ACTION_TYPES: frozenset[str] = BY_CATEGORY["transform"]
200
+
201
+ # 只读动作集合(不写盘)
202
+ READ_ONLY_ACTION_TYPES: frozenset[str] = frozenset(a.type for a in ACTIONS.values() if a.read_only)
203
+
204
+ # 写盘动作集合(READ_ONLY 的补集)
205
+ WRITE_ACTION_TYPES: frozenset[str] = frozenset(a.type for a in ACTIONS.values() if not a.read_only)
206
+
207
+ # actionType → spec 字段映射(None 表示无 spec 字段要求)
208
+ SPEC_FIELD_FOR: dict[str, str | None] = {a.type: a.spec_field for a in ACTIONS.values()}
209
+
210
+
211
+ def get_action_def(action_type: str) -> ActionTypeDef | None:
212
+ """查询动作定义,不存在返回 None。"""
213
+ return ACTIONS.get(action_type)
214
+
215
+
216
+ def is_read_only(action_type: str) -> bool:
217
+ """判断动作是否只读(不写盘)。未知动作视为写(保守)。"""
218
+ d = ACTIONS.get(action_type)
219
+ return d.read_only if d else False
220
+
221
+
222
+ def is_known_action(action_type: str) -> bool:
223
+ """判断是否为已注册的动作类型。"""
224
+ return action_type in ACTIONS
225
+
226
+
227
+ # =============================================================================
228
+ # 提示词文本派生(从注册表生成 LLM 可见的动作清单,消灭手写源文本漂移)
229
+ # =============================================================================
230
+
231
+ # category 的中文标签(用于提示词动作清单的分组标题)
232
+ _CATEGORY_LABELS: dict[str, str] = {
233
+ "constraint": "约束",
234
+ "schema": "Schema",
235
+ "regex": "正则",
236
+ "transform": "转换",
237
+ "settings": "设置",
238
+ "validate": "校验",
239
+ "canvas": "显示到画布",
240
+ }
241
+
242
+
243
+ def build_action_type_list_text() -> str:
244
+ """生成 actionType 可选值清单(按 category 分组),供系统提示词使用。
245
+
246
+ 新增动作后自动出现在此清单,无需手动同步提示词文本。
247
+ """
248
+ lines: list[str] = []
249
+ for category, types in BY_CATEGORY.items():
250
+ label = _CATEGORY_LABELS.get(category, category)
251
+ types_sorted = " / ".join(sorted(types))
252
+ suffix = ""
253
+ # ADD_TO_CANVAS 特殊标注"不写盘"
254
+ if category == "canvas":
255
+ suffix = "(把已存在的配置显示到画布,不写盘)"
256
+ lines.append(f"- {label}: {types_sorted}{suffix}")
257
+ return "\n".join(lines)
258
+
259
+
260
+ def build_spec_field_mapping_text() -> str:
261
+ """生成 actionType → spec 字段映射清单,供系统提示词使用。
262
+
263
+ 从 SPEC_FIELD_FOR 派生,新增动作后自动更新。
264
+ """
265
+ # spec 字段的中文说明(用于提示词)
266
+ _spec_notes: dict[str, str] = {
267
+ "constraintSpec": "含 type, tableName, targetColumn, isInline, params 等",
268
+ "schemaSpec": "含 name, columns",
269
+ "regexSpec": "含 name, pattern, matchMode",
270
+ "transformSpec": "含 type, inputColumn, params, outputColumns",
271
+ "settingsSpec": "含 category, settings",
272
+ "canvasSpec": "含 resourceKind: schema/regex/constraint/transform, resourceId 或 resourceName",
273
+ }
274
+ lines: list[str] = []
275
+ for category, types in BY_CATEGORY.items():
276
+ label = _CATEGORY_LABELS.get(category, category)
277
+ # 取该 category 第一个动作的 spec 字段(同 category 共享 spec 字段)
278
+ sample_type = sorted(types)[0]
279
+ spec_field = SPEC_FIELD_FOR.get(sample_type)
280
+ if spec_field is None:
281
+ continue
282
+ note = _spec_notes.get(spec_field, "")
283
+ # 特殊:校验动作用 constraintSpec 但 tableName 可选
284
+ if category == "validate":
285
+ note = "含 tableName,可选"
286
+ lines.append(f"- {label}动作 → {spec_field} ({note})" if note else f"- {label}动作 → {spec_field}")
287
+ return "\n".join(lines)
288
+
289
+
290
+ # =============================================================================
291
+ # Codegen 导出(供 frontend/scripts/codegen.mjs 通过 subprocess 读取)
292
+ # =============================================================================
293
+
294
+
295
+ def export_for_codegen() -> dict[str, object]:
296
+ """将动作类型单一事实源序列化为 JSON-friendly dict,供前端 codegen 生成 TS 类型。
297
+
298
+ 设计要点:
299
+ - frozenset 统一转 sorted list,保证顺序稳定(生成的 TS 文件可 diff)
300
+ - 仅导出 codegen 所需字段(type/spec_field/category/read_only + 派生集合)
301
+ - 纯函数、零 IO、零副作用,可在 subprocess 中安全 import 调用
302
+
303
+ 返回结构(JSON 兼容)::
304
+
305
+ {
306
+ "actions": [ {type, spec_field, category, read_only}, ... ], # 按 ACTIONS 插入序
307
+ "all_action_types": [...],
308
+ "by_category": { "constraint": [...], ... },
309
+ "read_only_action_types": [...],
310
+ "write_action_types": [...],
311
+ "constraint_types": [...], # 约束类型标准名(PascalCase,sorted)
312
+ "constraint_type_aliases": {...}, # 大写别名 → 标准名
313
+ }
314
+
315
+ constraint_types/constraint_type_aliases 供前端 codegen 生成约束类型映射,
316
+ 使前端 AI 指令处理器与后端提示词/写盘路径消费同一份约束类型清单,
317
+ 消灭"新增约束类型时前端 map/提示词漏同步"类漂移。
318
+ """
319
+ return {
320
+ # 按 ACTIONS 插入序保留(dict 保序),便于生成稳定 TS
321
+ "actions": [
322
+ {
323
+ "type": a.type,
324
+ "spec_field": a.spec_field,
325
+ "category": a.category,
326
+ "read_only": a.read_only,
327
+ }
328
+ for a in ACTIONS.values()
329
+ ],
330
+ "all_action_types": list(ALL_ACTION_TYPES),
331
+ "by_category": {k: sorted(v) for k, v in BY_CATEGORY.items()},
332
+ "read_only_action_types": sorted(READ_ONLY_ACTION_TYPES),
333
+ "write_action_types": sorted(WRITE_ACTION_TYPES),
334
+ "constraint_types": sorted(CONSTRAINT_TYPES),
335
+ "constraint_type_aliases": dict(sorted(CONSTRAINT_TYPE_ALIASES.items())),
336
+ }