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,259 @@
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 动作 Spec Pydantic 模型 — 结构校验单一事实源
17
+
18
+ 用类型系统编码动作 spec 的**结构性**约束(枚举、必填等),
19
+ 取代散落各处的手写 dict.get() + if 校验。
20
+
21
+ 设计原则:
22
+ - 只做**结构校验**(不依赖项目运行时状态):枚举值、字段存在性。
23
+ - **上下文校验**(表/列/FK 是否存在、类型兼容性、参数完整性)仍留在各 validator 函数——
24
+ 它们需要运行时加载的 schema,Pydantic 表达不了。
25
+ - 与 registry.py 互补:registry 描述"有哪些动作",specs 描述"每个动作的 spec 长什么样"。
26
+ - 约束参数的完整性校验(如 Range min/max、Scripted expression/pattern)由
27
+ _constraint_validator 与 constraint_builder 负责,不在 Pydantic 层做。
28
+
29
+ 风格参照 config/models.py(领域 Pydantic 模型,非 API DTO):
30
+ - ConfigDict(extra="ignore") 容忍 LLM 多余字段(不严格 forbid,避免误杀)
31
+ - populate_by_name 支持 camelCase(LLM 可能输出各种大小写)
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import Any
37
+
38
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
39
+
40
+ from app.shared.core.pydantic_messages import localize_pydantic_msg
41
+
42
+
43
+ class EmptyParams(BaseModel):
44
+ """无参数约束(NotNull/Unique/Charset/Composite)的空参数模型。"""
45
+
46
+ model_config = ConfigDict(extra="ignore")
47
+
48
+
49
+ # =============================================================================
50
+ # 各动作 Spec 模型
51
+ # =============================================================================
52
+
53
+
54
+ class ConstraintSpec(BaseModel):
55
+ """约束动作的 spec(constraintSpec)。ADD/UPDATE/DELETE_CONSTRAINT_NODE 用。"""
56
+
57
+ model_config = ConfigDict(extra="ignore")
58
+
59
+ # type 设为可选(非 ...):缺失 type 时 validator 用 missing_constraint_type 报错(有建议提示),
60
+ # 而非被 Pydantic 拦截为 spec_structure_invalid(无建议)。
61
+ type: str | None = Field(default=None, description="约束类型(标准名或别名)")
62
+ tableName: str | None = Field(default=None, description="目标表名")
63
+ targetNodeId: str | None = Field(default=None, description="目标表 ID")
64
+ targetColumn: str | None = Field(default=None, description="目标列名")
65
+ targetColumnId: str | None = Field(default=None, description="目标列 ID")
66
+ constraintId: str | None = Field(default=None, description="约束 ID")
67
+ # §2.11: 默认对齐消费方口径(get("isInline", False))——原默认 True 与
68
+ # update_yaml_config/frontend_instructions 的 get 默认 False 漂移,谁改任一侧即炸。
69
+ # 消费方均读原始 dict,本默认值当前不参与运行时判定(行为锁定测试见 B4 回归)。
70
+ isInline: bool = Field(default=False, description="是否内联约束(默认 false=独立约束文件)")
71
+ params: dict[str, Any] | None = Field(default=None, description="约束参数")
72
+
73
+ # 注意:约束类型白名单、表信息存在性、参数完整性均不在 Pydantic 层校验——这些由
74
+ # _constraint_validator 负责(它有更精准的 "did you mean" 建议和 missing_required_param 错误类型),
75
+ # Scripted 空表达式等参数关系校验由 constraint_builder 落实。
76
+ # Pydantic 层对 ConstraintSpec 只做字段类型解析。
77
+
78
+
79
+ class ValidateSpec(BaseModel):
80
+ """VALIDATE_PROJECT 的 spec(复用 constraintSpec 字段,但 type 可选、表信息可选)。
81
+
82
+ 与 ConstraintSpec 区别:VALIDATE_PROJECT 可空壳(校验全部表),或只给 tableName。
83
+ """
84
+
85
+ model_config = ConfigDict(extra="ignore")
86
+ tableName: str | None = Field(default=None, description="目标表名(可选)")
87
+ targetNodeId: str | None = Field(default=None, description="目标表 ID(可选)")
88
+ tables: list[str] | None = Field(default=None, description="多表校验列表")
89
+ tableIds: list[str] | None = Field(default=None, description="多表校验列表(别名)")
90
+
91
+
92
+ class SchemaColumnSpec(BaseModel):
93
+ """Schema 列定义。"""
94
+
95
+ model_config = ConfigDict(extra="ignore")
96
+ name: str = Field(..., description="列名")
97
+ type: str = Field(default="string", description="数据类型")
98
+
99
+
100
+ class SchemaSpec(BaseModel):
101
+ """Schema 动作的 spec(schemaSpec)。"""
102
+
103
+ model_config = ConfigDict(extra="ignore")
104
+ name: str | None = Field(default=None, description="表名")
105
+ schemaId: str | None = Field(default=None, description="表 ID")
106
+ columns: list[SchemaColumnSpec] | None = Field(default=None, description="列定义")
107
+
108
+
109
+ class RegexSpec(BaseModel):
110
+ """Regex 动作的 spec(regexSpec)。"""
111
+
112
+ model_config = ConfigDict(extra="ignore")
113
+ name: str | None = Field(default=None, description="正则节点名")
114
+ regexId: str | None = Field(default=None, description="正则节点 ID")
115
+ pattern: str | None = Field(default=None, description="正则表达式")
116
+ matchMode: str | None = Field(default="full", description="匹配模式 full/partial/extract")
117
+
118
+
119
+ class TransformSpec(BaseModel):
120
+ """Transform 动作的 spec(transformSpec)。
121
+
122
+ 注意:转换类型白名单不在 Pydantic 层校验——由 _transform_validator 负责(有建议)。
123
+ Pydantic 此处仅做字段类型与结构解析。
124
+ """
125
+
126
+ model_config = ConfigDict(extra="ignore")
127
+ type: str | None = Field(default=None, description="转换类型")
128
+ transformId: str | None = Field(default=None, description="转换节点 ID")
129
+ inputColumn: str | None = Field(default=None, description="输入列")
130
+ params: dict[str, Any] | None = Field(default=None, description="转换参数")
131
+ outputColumns: list[str] | None = Field(default=None, description="输出列")
132
+
133
+
134
+ class SettingsSpec(BaseModel):
135
+ """Settings 动作的 spec(settingsSpec)。
136
+
137
+ 注意:设置分类白名单不在 Pydantic 层校验——由 _settings_validator 负责(有建议)。
138
+ """
139
+
140
+ model_config = ConfigDict(extra="ignore")
141
+ category: str = Field(..., description="设置分类")
142
+ settings: dict[str, Any] = Field(..., description="设置键值")
143
+
144
+
145
+ class CanvasSpec(BaseModel):
146
+ """ADD_TO_CANVAS 动作的 spec(canvasSpec)。
147
+
148
+ 注意:资源类型白名单与存在性不在 Pydantic 层校验——由 _canvas_validator 负责
149
+ (查磁盘资源是否真实存在)。Pydantic 此处仅做字段结构解析。
150
+ """
151
+
152
+ model_config = ConfigDict(extra="ignore")
153
+ resourceKind: str = Field(..., description="资源类型 schema/regex/constraint/transform")
154
+ resourceId: str | None = Field(default=None, description="资源 ID")
155
+ resourceName: str | None = Field(default=None, description="资源名(兜底匹配)")
156
+ name: str | None = Field(default=None, description="资源名(别名)")
157
+
158
+
159
+ # =============================================================================
160
+ # 统一解析入口(适配层)
161
+ # =============================================================================
162
+
163
+
164
+ # actionType → spec 模型类的映射(从 registry 的 SPEC_FIELD_FOR 派生)
165
+ _SPEC_MODEL_FOR: dict[str, type[BaseModel]] = {
166
+ "ADD_CONSTRAINT_NODE": ConstraintSpec,
167
+ "UPDATE_CONSTRAINT_NODE": ConstraintSpec,
168
+ "DELETE_CONSTRAINT_NODE": ConstraintSpec,
169
+ "VALIDATE_PROJECT": ValidateSpec, # VALIDATE_PROJECT 的 constraintSpec 字段更宽松(type 可选)
170
+ "ADD_SCHEMA": SchemaSpec,
171
+ "UPDATE_SCHEMA": SchemaSpec,
172
+ "DELETE_SCHEMA": SchemaSpec,
173
+ "ADD_REGEX": RegexSpec,
174
+ "UPDATE_REGEX": RegexSpec,
175
+ "DELETE_REGEX": RegexSpec,
176
+ "ADD_TRANSFORM": TransformSpec,
177
+ "UPDATE_TRANSFORM": TransformSpec,
178
+ "DELETE_TRANSFORM": TransformSpec,
179
+ "UPDATE_SETTINGS": SettingsSpec,
180
+ "ADD_TO_CANVAS": CanvasSpec,
181
+ }
182
+
183
+
184
+ class SpecParseError(Exception):
185
+ """Spec 结构校验失败。message 含人类可读的错误描述。"""
186
+
187
+ def __init__(self, message: str, errors: list[str] | None = None):
188
+ super().__init__(message)
189
+ self.message = message
190
+ self.errors = errors or [message]
191
+
192
+
193
+ # spec 字段名(constraintSpec 等内部名)→ 用户可读名称
194
+ _SPEC_FIELD_ZH: dict[str, str] = {
195
+ "schemaSpec": "表结构配置",
196
+ "constraintSpec": "约束配置",
197
+ "regexSpec": "正则配置",
198
+ "transformSpec": "数据转换配置",
199
+ "settingsSpec": "项目设置",
200
+ "canvasSpec": "画布配置",
201
+ }
202
+
203
+
204
+ def _spec_field_zh(spec_field: str) -> str:
205
+ return _SPEC_FIELD_ZH.get(spec_field, spec_field)
206
+
207
+
208
+ def parse_action_spec(action: dict[str, Any]) -> BaseModel:
209
+ """解析动作的 spec 字段并做**结构校验**。
210
+
211
+ 根据 actionType 从 registry 查 spec 字段名,取出 spec dict,
212
+ 用对应 Pydantic 模型校验。校验失败抛 SpecParseError(含全部错误列表)。
213
+
214
+ 本函数只做结构校验;上下文校验(表/列/FK 是否存在)仍由各 validator 负责。
215
+
216
+ 参数:
217
+ action: 动作字典,含 actionType 和对应 spec 字段
218
+
219
+ 返回:
220
+ 校验通过的 spec 模型实例
221
+
222
+ 抛出:
223
+ SpecParseError: 结构校验失败(枚举非法、必填缺失、Range min>max 等)
224
+ """
225
+ from app.shared.services.llm.actions.registry import SPEC_FIELD_FOR
226
+
227
+ action_type = action.get("actionType", "")
228
+ spec_field = SPEC_FIELD_FOR.get(action_type)
229
+ model_cls = _SPEC_MODEL_FOR.get(action_type)
230
+
231
+ # 无 spec 字段要求的动作(理论上目前所有动作都有 spec,此分支为防御)
232
+ if spec_field is None or model_cls is None:
233
+ return EmptyParams() # 返回空模型占位
234
+
235
+ spec_data = action.get(spec_field)
236
+ if spec_data is None:
237
+ # spec 缺失:对于 VALIDATE_PROJECT 是合法的(可空壳),其余视为结构错误
238
+ if action_type == "VALIDATE_PROJECT":
239
+ return EmptyParams()
240
+ raise SpecParseError(f"动作 {_spec_field_zh(spec_field)}缺失,请补全后重试")
241
+
242
+ if not isinstance(spec_data, dict):
243
+ raise SpecParseError(f"{_spec_field_zh(spec_field)}必须是对象,实际为 {type(spec_data).__name__}")
244
+
245
+ try:
246
+ return model_cls.model_validate(spec_data)
247
+ except Exception as e: # noqa: BLE001 Pydantic ValidationError 及子类
248
+ # 提取 Pydantic 的字段级错误信息(英文校验消息做常见项中文映射,
249
+ # 未命中的保留原文;spec 字段名同步翻译,避免 constraintSpec 等内部名外露)
250
+ if isinstance(e, ValidationError):
251
+ messages = [
252
+ f"{'.'.join(str(x) for x in err['loc'])}: {localize_pydantic_msg(err['msg'])}" for err in e.errors()
253
+ ]
254
+ else:
255
+ messages = [str(e)]
256
+ raise SpecParseError(
257
+ f"{_spec_field_zh(spec_field)}校验失败: {'; '.join(localize_pydantic_msg(m) for m in messages)}",
258
+ messages,
259
+ ) from e
@@ -0,0 +1,279 @@
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 Transform 动作处理器模块
17
+
18
+ 功能概述:
19
+ - 处理 AI 生成的 Transform CRUD 动作(ADD_TRANSFORM / UPDATE_TRANSFORM / DELETE_TRANSFORM)
20
+ - 通过 core 层 writer 持久化 Transform YAML 文件
21
+ - 同步更新 project.precis.yaml 中的 TransformRef
22
+
23
+ 架构设计:
24
+ - 复用 core/project/transform/writer.py 的 save_transform()
25
+ - 复用 TransformFile Pydantic 模型确保类型安全
26
+ - 使用 atomic_write_yaml 保证文件写入安全
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.types import TransformRef
40
+ from app.shared.core.project.manifest.writer import save_manifest
41
+ from app.shared.core.project.transform.types import TransformFile
42
+ from app.shared.core.project.transform.writer import save_transform
43
+ from app.shared.services.llm.yaml_io import FileLock, atomic_write_yaml
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # 转换子类型白名单从注册表派生(单一事实源)
48
+ from app.shared.services.llm.actions.registry import TRANSFORM_SUB_TYPES as VALID_TRANSFORM_TYPES
49
+
50
+
51
+ def _sanitize_resource_id(resource_id: str) -> str:
52
+ cleaned = os.path.basename(resource_id)
53
+ if "/" in resource_id or "\\" in resource_id or ".." in resource_id:
54
+ raise ValueError(f"非法的资源 ID: {resource_id!r}")
55
+ return cleaned
56
+
57
+
58
+ def process_transform_action(action: dict[str, Any], workspace_path: str) -> dict[str, Any]:
59
+ """
60
+ @methoddesc 处理 Transform 动作
61
+
62
+ 根据 actionType 分发到对应的处理函数。
63
+
64
+ 参数:
65
+ action: 动作字典,包含 actionType 和 transformSpec
66
+ workspace_path: 项目工作区路径
67
+
68
+ 返回:
69
+ 处理结果字典 {"success": bool, "message": str}
70
+ """
71
+ action_type = action.get("actionType", "")
72
+ spec = action.get("transformSpec", {})
73
+
74
+ if action_type == "ADD_TRANSFORM":
75
+ return _add_transform(spec, workspace_path)
76
+ elif action_type == "UPDATE_TRANSFORM":
77
+ return _update_transform(spec, workspace_path)
78
+ elif action_type == "DELETE_TRANSFORM":
79
+ return _delete_transform(spec, workspace_path)
80
+ else:
81
+ return {"success": False, "message": f"未知的 Transform 动作类型: {action_type}"}
82
+
83
+
84
+ def _add_transform(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
85
+ """创建新的 Transform YAML 文件"""
86
+ transform_type = spec.get("type", "")
87
+ transform_id = spec.get("transformId") or spec.get("id") or _generate_transform_id(transform_type)
88
+ try:
89
+ transform_id = _sanitize_resource_id(transform_id)
90
+ except ValueError:
91
+ return {"success": False, "message": f"非法的 Transform ID: {transform_id}"}
92
+ description = spec.get("description")
93
+ input_from_node = spec.get("inputFromNode")
94
+ input_column = spec.get("inputColumn")
95
+ params = spec.get("params", {})
96
+ output_columns = spec.get("outputColumns", [])
97
+
98
+ if not transform_type:
99
+ return {"success": False, "message": "Transform 类型不能为空"}
100
+
101
+ if transform_type not in VALID_TRANSFORM_TYPES:
102
+ return {"success": False, "message": f"不支持的 Transform 类型: {transform_type}"}
103
+
104
+ workspace = Path(workspace_path)
105
+ transforms_dir = workspace / "transforms"
106
+ transforms_dir.mkdir(parents=True, exist_ok=True)
107
+
108
+ transform_file = transforms_dir / f"{transform_id}.transform.yaml"
109
+ if transform_file.exists():
110
+ # 提示 Agent 改用 ADD_TO_CANVAS(同 schema_handlers 的引导逻辑)
111
+ return {
112
+ "success": False,
113
+ "message": (
114
+ f"Transform 文件已存在: {transform_id}.transform.yaml。"
115
+ f"若用户想把已存在的资源显示到画布,请改用 actionType=ADD_TO_CANVAS"
116
+ f"(canvasSpec.resourceKind='transform'),它不会重复创建文件。"
117
+ ),
118
+ }
119
+
120
+ try:
121
+ transform = TransformFile(
122
+ version=2,
123
+ id=transform_id,
124
+ type=transform_type,
125
+ enabled=True,
126
+ description=description,
127
+ input_from_node=input_from_node,
128
+ input_column=input_column,
129
+ params=params,
130
+ output_columns=output_columns,
131
+ )
132
+ save_transform(transform, transform_file)
133
+ except Exception as e:
134
+ return {"success": False, "message": f"写入 Transform 文件失败: {e}"}
135
+
136
+ # 更新 manifest
137
+ _ensure_manifest_transform_ref(workspace_path, transform_id)
138
+
139
+ logger.info(f"[TransformHandler] 创建 Transform: {transform_id}")
140
+ return {"success": True, "message": transform_id}
141
+
142
+
143
+ def _update_transform(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
144
+ """更新现有 Transform"""
145
+ transform_id = spec.get("transformId") or spec.get("id", "")
146
+ try:
147
+ transform_id = _sanitize_resource_id(transform_id)
148
+ except ValueError:
149
+ return {"success": False, "message": f"非法的 Transform ID: {transform_id}"}
150
+
151
+ if not transform_id:
152
+ return {"success": False, "message": "缺少 Transform ID"}
153
+
154
+ transform_file = _find_transform_file(workspace_path, transform_id)
155
+ if not transform_file:
156
+ return {"success": False, "message": f"Transform 文件不存在: {transform_id}"}
157
+
158
+ try:
159
+ with FileLock(str(transform_file)):
160
+ with open(transform_file, encoding="utf-8") as f:
161
+ data = yaml.safe_load(f) or {}
162
+
163
+ if spec.get("type") and spec["type"] in VALID_TRANSFORM_TYPES:
164
+ data["type"] = spec["type"]
165
+ if "description" in spec:
166
+ data["description"] = spec["description"]
167
+ if "params" in spec:
168
+ data["params"] = spec["params"]
169
+ if "inputFromNode" in spec:
170
+ data["input_from_node"] = spec["inputFromNode"]
171
+ if "inputColumn" in spec:
172
+ data["input_column"] = spec["inputColumn"]
173
+ if "outputColumns" in spec:
174
+ data["output_columns"] = spec["outputColumns"]
175
+
176
+ # §2.10: UPDATE 显式整体替换语义——preserve_format 默认 True 的递归合并
177
+ # "只增不删",旧输出列 C 会残留在文件里(用户想把输出改成 A、B 结果是 A/B/C)
178
+ atomic_write_yaml(transform_file, data, preserve_format=False)
179
+
180
+ except Exception as e:
181
+ return {"success": False, "message": f"更新 Transform 失败: {e}"}
182
+
183
+ logger.info(f"[TransformHandler] 更新 Transform: {transform_id}")
184
+ return {"success": True, "message": transform_id}
185
+
186
+
187
+ def _delete_transform(spec: dict[str, Any], workspace_path: str) -> dict[str, Any]:
188
+ """删除 Transform 文件"""
189
+ transform_id = spec.get("transformId") or spec.get("id", "")
190
+ try:
191
+ transform_id = _sanitize_resource_id(transform_id)
192
+ except ValueError:
193
+ return {"success": False, "message": f"非法的 Transform ID: {transform_id}"}
194
+
195
+ if not transform_id:
196
+ return {"success": False, "message": "缺少 Transform ID"}
197
+
198
+ transform_file = _find_transform_file(workspace_path, transform_id)
199
+ if not transform_file:
200
+ return {"success": False, "message": f"Transform 文件不存在: {transform_id}"}
201
+
202
+ try:
203
+ transform_file.unlink()
204
+ except OSError as e:
205
+ return {"success": False, "message": f"删除 Transform 文件失败: {e}"}
206
+
207
+ _remove_manifest_transform_ref(workspace_path, transform_id)
208
+
209
+ logger.info(f"[TransformHandler] 删除 Transform: {transform_id}")
210
+ return {"success": True, "message": transform_id}
211
+
212
+
213
+ def _find_transform_file(workspace_path: str, transform_id: str) -> Path | None:
214
+ """在工作区中查找 transform 文件"""
215
+ transforms_dir = Path(workspace_path) / "transforms"
216
+ if not transforms_dir.exists():
217
+ return None
218
+
219
+ for tf in transforms_dir.glob("*.yaml"):
220
+ try:
221
+ with open(tf, encoding="utf-8") as f:
222
+ data = yaml.safe_load(f) or {}
223
+ if data.get("id") == transform_id:
224
+ return tf
225
+ except Exception:
226
+ continue
227
+
228
+ candidate = transforms_dir / f"{transform_id}.transform.yaml"
229
+ if candidate.exists():
230
+ return candidate
231
+
232
+ return None
233
+
234
+
235
+ def _generate_transform_id(transform_type: str) -> str:
236
+ """生成 Transform ID"""
237
+ import re
238
+
239
+ safe = re.sub(r"[^a-z0-9_]", "_", transform_type.lower())
240
+ return f"{safe}_{_short_hash()}"
241
+
242
+
243
+ def _short_hash() -> str:
244
+ """生成短随机哈希(§2.5: uuid4 前 8 位十六进制——原 random.randint(100,999)
245
+ 仅 900 个桶,批量建 transform 时生日悖论约 35 个即碰撞过半,碰撞报"文件已存在"
246
+ 会诱导 LLM 走错误的 ADD_TO_CANVAS/放弃路径)"""
247
+ import uuid
248
+
249
+ return uuid.uuid4().hex[:8]
250
+
251
+
252
+ def _ensure_manifest_transform_ref(workspace_path: str, transform_id: str) -> None:
253
+ """确保 manifest 中包含指定 Transform 引用"""
254
+ manifest_path = Path(workspace_path) / "project.precis.yaml"
255
+ if not manifest_path.exists():
256
+ return
257
+
258
+ try:
259
+ manifest = load_manifest(manifest_path)
260
+ existing = next((t for t in manifest.transforms if t.id == transform_id), None)
261
+ if not existing:
262
+ manifest.transforms.append(TransformRef(id=transform_id, path=f"transforms/{transform_id}.transform.yaml"))
263
+ save_manifest(manifest, manifest_path)
264
+ except Exception as e:
265
+ logger.warning(f"[TransformHandler] 更新 manifest 引用失败: {e}")
266
+
267
+
268
+ def _remove_manifest_transform_ref(workspace_path: str, transform_id: str) -> None:
269
+ """从 manifest 中移除指定 Transform 引用"""
270
+ manifest_path = Path(workspace_path) / "project.precis.yaml"
271
+ if not manifest_path.exists():
272
+ return
273
+
274
+ try:
275
+ manifest = load_manifest(manifest_path)
276
+ manifest.transforms = [t for t in manifest.transforms if t.id != transform_id]
277
+ save_manifest(manifest, manifest_path)
278
+ except Exception as e:
279
+ logger.warning(f"[TransformHandler] 更新 manifest 引用失败: {e}")
@@ -0,0 +1,147 @@
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
+ """
17
+ @fileoverview 动作验证器数据类型与格式化工具
18
+
19
+ 定义:
20
+ - ValidationError: 验证错误信息数据类
21
+ - ValidationResult: 验证结果聚合数据类
22
+ - format_validation_result: 将结果格式化为可读文本
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, field
28
+ from typing import Any
29
+
30
+
31
+ @dataclass
32
+ class ValidationError:
33
+ """验证错误信息
34
+
35
+ Attributes:
36
+ action_index: 动作在列表中的索引
37
+ action_type: 动作类型
38
+ error_type: 错误类型标识
39
+ message: 错误描述
40
+ suggestion: 修正建议(可选)
41
+ auto_fixable: 是否可以自动修正
42
+ """
43
+
44
+ action_index: int
45
+ action_type: str
46
+ error_type: str
47
+ message: str
48
+ suggestion: str | None = None
49
+ auto_fixable: bool = False
50
+
51
+
52
+ @dataclass
53
+ class ValidationResult:
54
+ """验证结果
55
+
56
+ Attributes:
57
+ errors: 错误列表(阻止执行)
58
+ warnings: 警告列表(不阻止执行)
59
+ valid_actions: 有效的动作列表
60
+ invalid_action_indices: 无效的动作索引集合
61
+ """
62
+
63
+ errors: list[ValidationError] = field(default_factory=list)
64
+ warnings: list[ValidationError] = field(default_factory=list)
65
+ valid_actions: list[dict[str, Any]] = field(default_factory=list)
66
+ invalid_action_indices: set[int] = field(default_factory=set)
67
+
68
+ @property
69
+ def has_errors(self) -> bool:
70
+ """是否有错误"""
71
+ return len(self.errors) > 0
72
+
73
+ @property
74
+ def has_warnings(self) -> bool:
75
+ """是否有警告"""
76
+ return len(self.warnings) > 0
77
+
78
+ @property
79
+ def all_valid(self) -> bool:
80
+ """是否全部有效"""
81
+ return len(self.errors) == 0 and len(self.invalid_action_indices) == 0
82
+
83
+ @property
84
+ def partial_valid(self) -> bool:
85
+ """是否部分有效(有错误但也有有效动作)"""
86
+ return len(self.errors) > 0 and len(self.valid_actions) > 0
87
+
88
+
89
+ def action_display_name(action_type: str) -> str:
90
+ """把动作类型枚举(ADD_SCHEMA 等)翻译成用户可读的中文名。
91
+
92
+ 派生规则:动词前缀 + registry 里的 category;特殊只读动作单独映射;
93
+ 未登记的枚举回退原文(新增动作漏配时不会显示空白)。
94
+ """
95
+ special = {"VALIDATE_PROJECT": "校验项目", "ADD_TO_CANVAS": "添加到画布"}
96
+ if action_type in special:
97
+ return special[action_type]
98
+ verbs = {"ADD": "新增", "UPDATE": "修改", "DELETE": "删除"}
99
+ categories = {
100
+ "constraint": "约束",
101
+ "schema": "表结构",
102
+ "regex": "正则",
103
+ "transform": "数据转换",
104
+ "settings": "项目设置",
105
+ }
106
+ from app.shared.services.llm.actions.registry import get_action_def
107
+
108
+ d = get_action_def(action_type)
109
+ if d is not None:
110
+ verb = verbs.get(action_type.split("_", 1)[0], "")
111
+ category = categories.get(d.category)
112
+ if verb and category:
113
+ return f"{verb}{category}"
114
+ return action_type
115
+
116
+
117
+ def format_validation_result(result: ValidationResult) -> str:
118
+ """格式化验证结果为可读文本
119
+
120
+ 参数:
121
+ result: 验证结果
122
+
123
+ 返回:
124
+ 格式化后的文本
125
+ """
126
+ lines = []
127
+
128
+ if result.all_valid and not result.warnings:
129
+ return "所有操作验证通过"
130
+
131
+ if result.errors:
132
+ lines.append(f"发现 {len(result.errors)} 个问题:")
133
+ for error in result.errors:
134
+ lines.append(f"\n [{error.action_index + 1}] {action_display_name(error.action_type)}")
135
+ lines.append(f" 问题: {error.message}")
136
+ if error.suggestion:
137
+ lines.append(f" 建议: {error.suggestion}")
138
+
139
+ if result.warnings:
140
+ lines.append(f"\n另有 {len(result.warnings)} 个提醒:")
141
+ for warning in result.warnings:
142
+ lines.append(f" - {warning.message}")
143
+
144
+ if result.partial_valid:
145
+ lines.append(f"\n其中 {len(result.valid_actions)} 个操作有效,{len(result.invalid_action_indices)} 个操作无效")
146
+
147
+ return "\n".join(lines)