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,333 @@
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
+ - 根据动作类型生成前端渲染指令
21
+ - 支持约束、Schema、Regex、Transform、Settings 全部操作
22
+ - 统一指令格式,便于前端 dispatcher 分发处理
23
+ - 约束指令的 targetNodeId 走与写 YAML 相同的名称→ID fallback 解析,
24
+ 保证"写文件"和"生成前端指令"两条出口的 ID 解析能力对等
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ import yaml
34
+
35
+ from app.shared.services.llm.constraints.constraint_id import _generate_constraint_id
36
+ from app.shared.services.llm.schema_resolver import _resolve_id_from_name
37
+ from app.shared.services.llm.suggestion_utils import normalize_constraint_type
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ def _read_yaml_file(path: Path) -> dict[str, Any] | None:
43
+ """安全读取 YAML 文件,失败返回 None(仅用于 UPDATE 指令重读磁盘真实结果)。"""
44
+ try:
45
+ if not path.exists():
46
+ return None
47
+ with open(path, encoding="utf-8") as f:
48
+ return yaml.safe_load(f) or {}
49
+ except Exception as e:
50
+ logger.warning(f"[frontend_instructions] 重读文件失败 {path}: {e}")
51
+ return None
52
+
53
+
54
+ # 动作分类集合从注册表派生(单一事实源),避免本地硬编码与注册表不同步
55
+ from app.shared.services.llm.actions.registry import (
56
+ CONSTRAINT_ACTION_TYPES,
57
+ REGEX_ACTION_TYPES,
58
+ SCHEMA_ACTION_TYPES,
59
+ TRANSFORM_ACTION_TYPES,
60
+ )
61
+
62
+
63
+ def generate_frontend_instructions(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
64
+ """
65
+ @methoddesc 生成前端渲染指令(通用入口)
66
+
67
+ 根据 actionType 分发到对应的指令生成器。
68
+ workspace_path 用于约束指令的名称→ID fallback 解析,
69
+ 保证 targetNodeId 与写 YAML 路径使用同一套解析逻辑。
70
+
71
+ 参数:
72
+ action: 动作字典,包含 actionType 和对应的 spec
73
+ workspace_path: 项目工作区路径,启用约束指令的 ID 解析(默认空=原样透传)
74
+
75
+ 返回:
76
+ 前端渲染指令字典
77
+ """
78
+ action_type = action.get("actionType", "")
79
+
80
+ if action_type in CONSTRAINT_ACTION_TYPES:
81
+ return _generate_constraint_instruction(action, workspace_path)
82
+ elif action_type in SCHEMA_ACTION_TYPES:
83
+ return _generate_schema_instruction(action, workspace_path)
84
+ elif action_type in REGEX_ACTION_TYPES:
85
+ return _generate_regex_instruction(action, workspace_path)
86
+ elif action_type in TRANSFORM_ACTION_TYPES:
87
+ return _generate_transform_instruction(action, workspace_path)
88
+ elif action_type == "UPDATE_SETTINGS":
89
+ return _generate_settings_instruction(action)
90
+ elif action_type == "ADD_TO_CANVAS":
91
+ return _generate_canvas_instruction(action, workspace_path)
92
+ else:
93
+ return {"actionType": action_type}
94
+
95
+
96
+ def _generate_constraint_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
97
+ """
98
+ 生成约束类前端指令
99
+
100
+ 与 _build_constraint_refs 保持对等的 ID 解析能力:
101
+ 当 targetNodeId 为空或可疑时,用 _resolve_id_from_name 从 schema 文件
102
+ 解析出确定性的 table_id / column_id,避免前端拿到 AI 原话找不到节点。
103
+ """
104
+ action_type = action.get("actionType")
105
+ constraint_spec = action.get("constraintSpec", {})
106
+
107
+ # 标准化为 PascalCase 正名再下发:提示词教 AI 输出 PascalCase(NotNull),
108
+ # 但 LLM 也可能回大写下划线(NOT_NULL)。写盘路径已做同样的标准化,
109
+ # 此处对齐可保证前端指令与已写入的约束文件类型一致,避免前端拿到
110
+ # 自己无法识别的类型字符串而静默不建节点(幽灵约束)。
111
+ constraint_type = normalize_constraint_type(constraint_spec.get("type", ""))
112
+ target_node_id = constraint_spec.get("targetNodeId", "")
113
+ target_column_id = constraint_spec.get("targetColumnId", "")
114
+ table_name = constraint_spec.get("tableName", "")
115
+ target_column = constraint_spec.get("targetColumn", "")
116
+ is_inline = constraint_spec.get("isInline", False)
117
+ # params 原样透传(min/max/allowedValues/expression/toTableId 等):
118
+ # 前端建独立约束节点时须据此填充节点 data,否则保存链路会用空 params
119
+ # 覆盖后端已写入的参数(静默数据丢失)。
120
+ params = constraint_spec.get("params", {})
121
+
122
+ # 关键:与写 YAML 路径(_build_constraint_refs)相同的 fallback 解析
123
+ # 当 workspace_path 可用时,把 tableName/targetColumn 解析为确定性的 schema/column ID
124
+ if workspace_path and table_name:
125
+ fallback_table_id, fallback_column_id = _resolve_id_from_name(
126
+ workspace_path, table_name, target_column or target_column_id
127
+ )
128
+ # 仅在 AI 未给出明确 ID、或给出空值时,用 fallback 覆盖
129
+ # (AI 明确给出的非空 ID 优先,尊重其意图)
130
+ if fallback_table_id and not target_node_id:
131
+ target_node_id = fallback_table_id
132
+ if fallback_column_id and not target_column_id:
133
+ target_column_id = fallback_column_id
134
+
135
+ filename_table = table_name or target_node_id or "unknown"
136
+ filename_column = target_column or target_column_id or "unknown"
137
+
138
+ constraint_id = _generate_constraint_id(constraint_type, filename_table or "inline", filename_column)
139
+
140
+ return {
141
+ "actionType": action_type,
142
+ "constraintSpec": {
143
+ "type": constraint_type,
144
+ "targetNodeId": target_node_id,
145
+ "targetColumnId": target_column_id,
146
+ "tableName": table_name,
147
+ "targetColumn": target_column,
148
+ "constraintId": constraint_id,
149
+ "isInline": is_inline,
150
+ "params": params,
151
+ },
152
+ }
153
+
154
+
155
+ def _generate_schema_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
156
+ """生成 Schema 类前端指令。
157
+
158
+ ADD:回声输入(输入即落盘结果)。
159
+ UPDATE:重读磁盘,返回合并后的真实完整列/源(避免回声仅含 LLM 增量导致画布丢列)。
160
+ DELETE:只需 id/name 用于前端定位,不重读(文件已删)。
161
+ """
162
+ action_type = action.get("actionType")
163
+ spec = action.get("schemaSpec", {})
164
+ schema_id = spec.get("schemaId") or spec.get("id", "")
165
+ name = spec.get("name", "")
166
+
167
+ columns = spec.get("columns", [])
168
+ source = spec.get("source")
169
+
170
+ # UPDATE 时重读磁盘真实结果(_update_schema 会保留未列出的列,回声会丢列)
171
+ if action_type == "UPDATE_SCHEMA" and workspace_path:
172
+ schemas_dir = Path(workspace_path) / "schemas"
173
+ # 按与 handler 一致的方式定位文件:先按 id,再按 name
174
+ candidates = list(schemas_dir.glob("*.yaml")) if schemas_dir.exists() else []
175
+ for sf in candidates:
176
+ data = _read_yaml_file(sf)
177
+ if data and (data.get("id") == schema_id or (name and data.get("name") == name)):
178
+ columns = data.get("columns", columns)
179
+ source = data.get("source", source)
180
+ break
181
+
182
+ return {
183
+ "actionType": action_type,
184
+ "schemaSpec": {
185
+ "name": name,
186
+ "schemaId": schema_id,
187
+ "columns": columns,
188
+ "source": source,
189
+ },
190
+ }
191
+
192
+
193
+ def _generate_regex_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
194
+ """生成 Regex 类前端指令。
195
+
196
+ ADD:回声输入。UPDATE:重读磁盘真实 pattern/matchMode 等(整段替换,回声≈结果,重读为一致性)。
197
+ DELETE:只需 id/name 定位,不重读。
198
+ """
199
+ action_type = action.get("actionType")
200
+ spec = action.get("regexSpec", {})
201
+ regex_id = spec.get("regexId") or spec.get("id", "")
202
+ name = spec.get("name", "")
203
+
204
+ pattern = spec.get("pattern", "")
205
+ match_mode = spec.get("matchMode", "full")
206
+ case_sensitive = spec.get("caseSensitive", False)
207
+ description = spec.get("description")
208
+
209
+ if action_type == "UPDATE_REGEX" and workspace_path and regex_id:
210
+ data = _read_yaml_file(Path(workspace_path) / "regex" / f"{regex_id}.regex.yaml")
211
+ if data:
212
+ pattern = data.get("pattern", pattern)
213
+ match_mode = data.get("matchMode", match_mode)
214
+ case_sensitive = data.get("caseSensitive", case_sensitive)
215
+ description = data.get("description", description)
216
+
217
+ return {
218
+ "actionType": action_type,
219
+ "regexSpec": {
220
+ "name": name,
221
+ "regexId": regex_id,
222
+ "pattern": pattern,
223
+ "matchMode": match_mode,
224
+ "caseSensitive": case_sensitive,
225
+ "targetNodeId": spec.get("targetNodeId"),
226
+ "targetColumn": spec.get("targetColumn"),
227
+ "description": description,
228
+ },
229
+ }
230
+
231
+
232
+ def _generate_transform_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
233
+ """生成 Transform 类前端指令。
234
+
235
+ ADD:回声输入。UPDATE:重读磁盘真实 params 等(整段替换,回声≈结果,重读为一致性)。
236
+ DELETE:只需 id 定位,不重读。
237
+ """
238
+ action_type = action.get("actionType")
239
+ spec = action.get("transformSpec", {})
240
+ transform_id = spec.get("transformId") or spec.get("id", "")
241
+
242
+ params = spec.get("params", {})
243
+ output_columns = spec.get("outputColumns", [])
244
+ description = spec.get("description")
245
+
246
+ if action_type == "UPDATE_TRANSFORM" and workspace_path and transform_id:
247
+ data = _read_yaml_file(Path(workspace_path) / "transforms" / f"{transform_id}.transform.yaml")
248
+ if data:
249
+ params = data.get("params", params)
250
+ output_columns = data.get("outputColumns", output_columns)
251
+ description = data.get("description", description)
252
+
253
+ return {
254
+ "actionType": action_type,
255
+ "transformSpec": {
256
+ "transformId": transform_id,
257
+ "type": spec.get("type", ""),
258
+ "description": description,
259
+ "inputFromNode": spec.get("inputFromNode"),
260
+ "inputColumn": spec.get("inputColumn"),
261
+ "params": params,
262
+ "outputColumns": output_columns,
263
+ },
264
+ }
265
+
266
+
267
+ def _generate_settings_instruction(action: dict[str, Any]) -> dict[str, Any]:
268
+ """生成 Settings 类前端指令"""
269
+ action_type = action.get("actionType")
270
+ spec = action.get("settingsSpec", {})
271
+ return {
272
+ "actionType": action_type,
273
+ "settingsSpec": {
274
+ "category": spec.get("category", ""),
275
+ "settings": spec.get("settings", {}),
276
+ },
277
+ }
278
+
279
+
280
+ def _generate_canvas_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
281
+ """生成 ADD_TO_CANVAS 前端指令(重读磁盘真实配置)
282
+
283
+ 与 ADD_*(回声输入)不同:ADD_TO_CANVAS 的语义是"显示已存在的配置到画布",
284
+ 故重读磁盘拿到真实数据(columns/source/pattern 等),保证画布显示与配置一致,
285
+ 而非依赖 LLM 输入(可能不完整或与磁盘不同步)。
286
+
287
+ 前端收到后委托 graphStore.importV2ResourceToCanvas 创建节点(幂等)。
288
+ """
289
+ spec = action.get("canvasSpec", {})
290
+ resource_kind = spec.get("resourceKind", "")
291
+ resource_id = spec.get("resourceId", "")
292
+ resource_name = spec.get("resourceName") or spec.get("name", "")
293
+
294
+ # 定位磁盘配置文件并重读真实数据
295
+ # dir_map 与 _canvas_validator._list_existing_resource_ids 保持一致
296
+ dir_map = {
297
+ "schema": ("schemas", "*.schema.yaml"),
298
+ "regex": ("regex", "*.regex.yaml"),
299
+ "constraint": ("constraints", "*.constraint.yaml"),
300
+ "transform": ("transforms", "*.transform.yaml"),
301
+ }
302
+ real_config: dict[str, Any] = {}
303
+ if workspace_path and resource_kind in dir_map:
304
+ subdir, pattern = dir_map[resource_kind]
305
+ target_dir = Path(workspace_path) / subdir
306
+ if target_dir.exists():
307
+ for f in target_dir.glob(pattern):
308
+ data = _read_yaml_file(f)
309
+ if not data:
310
+ continue
311
+ # 按 id 或 name 匹配目标资源
312
+ fid = data.get("id", "")
313
+ fname = data.get("name", "")
314
+ if (resource_id and str(fid) == str(resource_id)) or (
315
+ resource_name and str(fname) == str(resource_name)
316
+ ):
317
+ real_config = data
318
+ # 同步补全 resourceId(若 LLM 只给了 name)
319
+ if not resource_id and fid:
320
+ resource_id = str(fid)
321
+ break
322
+
323
+ return {
324
+ "actionType": "ADD_TO_CANVAS",
325
+ "canvasSpec": {
326
+ "resourceKind": resource_kind,
327
+ "resourceId": resource_id,
328
+ "name": resource_name,
329
+ # 透传重读的真实配置,前端可据此直接构建节点(importV2ResourceToCanvas 也会独立重读,
330
+ # 但携带 config 可让前端在不依赖 API 往返时也能渲染)
331
+ "config": real_config,
332
+ },
333
+ }
@@ -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
+ """
17
+ @fileoverview 内联约束批量处理模块
18
+
19
+ 功能概述:
20
+ - 批量处理同一 schema 的多个内联约束
21
+ - 核心优化:只读取和写入 schema 文件一次,避免多次 IO
22
+ - 所有约束先在内存中处理,最后统一原子性写入
23
+
24
+ 输入示例:
25
+ results = process_inline_batch(actions, "/workspace")
26
+
27
+ 输出示例:
28
+ [
29
+ {"action": action, "success": True, "message": "inline:notnull_users_email", "frontendInstructions": {...}},
30
+ ...
31
+ ]
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import logging
37
+ from pathlib import Path
38
+ from typing import Any
39
+
40
+ import yaml
41
+
42
+ from app.shared.services.llm.constraints.constraint_builder import (
43
+ CONSTRAINT_TYPE_MAP,
44
+ _build_constraint_params,
45
+ )
46
+ from app.shared.services.llm.constraints.constraint_id import _generate_constraint_id
47
+ from app.shared.services.llm.constraints.frontend_instructions import generate_frontend_instructions
48
+ from app.shared.services.llm.yaml_io import FileLock, atomic_write_yaml
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ def _collect_target_schema_id(action: dict[str, Any]) -> str | None:
54
+ """
55
+ @methoddesc 从 action 中提取目标 schema ID(用于批量处理时分组)
56
+
57
+ 参数:
58
+ action: 动作字典
59
+
60
+ 返回:
61
+ schema ID 字符串,如果无法识别则返回 None
62
+ """
63
+ spec = action.get("constraintSpec", {})
64
+ # 优先使用 tableName,其次是 targetNodeId
65
+ value = spec.get("tableName") or spec.get("targetNodeId")
66
+ return value if isinstance(value, str) else None
67
+
68
+
69
+ def _is_inline_action(action: dict[str, Any]) -> bool:
70
+ """
71
+ @methoddesc 判断动作是否为内联约束操作
72
+
73
+ 内联约束直接存储在 schema 文件中,而非独立的 .constraint.yaml 文件。
74
+
75
+ 参数:
76
+ action: 动作字典
77
+
78
+ 返回:
79
+ True 表示内联约束,False 表示独立约束
80
+ """
81
+ spec = action.get("constraintSpec", {})
82
+ return bool(spec.get("isInline", False))
83
+
84
+
85
+ def process_inline_batch(actions: list[dict[str, Any]], workspace_path: str) -> list[dict[str, Any]]:
86
+ """
87
+ @methoddesc 批量处理同一 schema 的多个内联约束
88
+
89
+ 核心优化:只读取和写入 schema 文件一次,避免多个独立约束操作导致多次 IO。
90
+ 所有约束先在内存中处理,最后统一原子性写入。
91
+
92
+ 参数:
93
+ actions: 同一 schema 的内联约束操作列表
94
+ workspace_path: 项目工作区路径
95
+
96
+ 返回:
97
+ 每个 action 的处理结果列表
98
+
99
+ 示例:
100
+ >>> results = process_inline_batch([
101
+ ... {"actionType": "ADD_CONSTRAINT_NODE", "constraintSpec": {"isInline": True, ...}},
102
+ ... {"actionType": "ADD_CONSTRAINT_NODE", "constraintSpec": {"isInline": True, ...}},
103
+ ... ], "/workspace")
104
+ """
105
+ if not actions:
106
+ return []
107
+
108
+ # 从第一个 action 中提取 schema 标识(假设同一批的 schema 相同)
109
+ first_action = actions[0]
110
+ spec = first_action.get("constraintSpec", {})
111
+ target_node_id = spec.get("targetNodeId", "")
112
+ table_name = spec.get("tableName", "")
113
+ target_table_id = target_node_id or table_name
114
+
115
+ results = []
116
+
117
+ try:
118
+ schemas_dir = Path(workspace_path) / "schemas"
119
+ schema_file = None
120
+
121
+ # 在 schemas 目录中查找匹配的 schema 文件
122
+ for sf in schemas_dir.glob("*.yaml"):
123
+ with open(sf, encoding="utf-8") as f:
124
+ sd = yaml.safe_load(f) or {}
125
+ if sd.get("id") == target_table_id or sd.get("name") == table_name:
126
+ schema_file = sf
127
+ break
128
+
129
+ if not schema_file:
130
+ # schema 文件不存在,所有操作标记为失败
131
+ for action in actions:
132
+ results.append(
133
+ {
134
+ "action": action,
135
+ "success": False,
136
+ "message": f"未找到 schema 文件: {target_table_id}",
137
+ "frontendInstructions": None,
138
+ }
139
+ )
140
+ return results
141
+
142
+ # 使用文件锁保护,一次性处理所有约束
143
+ with FileLock(str(schema_file)):
144
+ # 重新读取文件(确保获取最新内容)
145
+ with open(schema_file, encoding="utf-8") as f:
146
+ schema_data = yaml.safe_load(f) or {}
147
+
148
+ columns = schema_data.get("columns", [])
149
+ if "constraints" not in schema_data:
150
+ schema_data["constraints"] = []
151
+
152
+ # 逐个处理约束(全部在内存中修改,不立即写入)
153
+ has_delete = False
154
+ for action in actions:
155
+ try:
156
+ spec = action.get("constraintSpec", {})
157
+ action_type = action.get("actionType", "")
158
+ constraint_type = spec.get("type", "")
159
+ target_column = spec.get("targetColumn", "")
160
+ target_column_id = spec.get("targetColumnId", "")
161
+
162
+ # 将约束类型别名标准化
163
+ std_type = CONSTRAINT_TYPE_MAP.get(constraint_type, constraint_type)
164
+
165
+ # 查找目标列的 ID(优先使用传入的 ID,否则按名称查找)
166
+ column_id = target_column_id
167
+ if not column_id:
168
+ for col in columns:
169
+ if col.get("name") == target_column:
170
+ column_id = col.get("id")
171
+ break
172
+
173
+ if not column_id:
174
+ results.append(
175
+ {
176
+ "action": action,
177
+ "success": False,
178
+ "message": f"未找到列 ID: {target_column}",
179
+ "frontendInstructions": None,
180
+ }
181
+ )
182
+ continue
183
+
184
+ # 生成约束 ID
185
+ filename_table = table_name or target_node_id or "unknown"
186
+ constraint_id = _generate_constraint_id(std_type, filename_table, target_column)
187
+
188
+ # DELETE 动作:从内存中的 constraints 移除「同列 + 同类型」项,绝不新增。
189
+ # 旧实现不区分 actionType,删除动作会被当作添加处理(只增不删还报成功)。
190
+ if action_type == "DELETE_CONSTRAINT_NODE":
191
+ has_delete = True
192
+ remaining = [
193
+ c
194
+ for c in schema_data["constraints"]
195
+ if not (c.get("column") == column_id and c.get("type") == std_type)
196
+ ]
197
+ if len(remaining) == len(schema_data["constraints"]):
198
+ results.append(
199
+ {
200
+ "action": action,
201
+ "success": False,
202
+ "message": f"未找到内联约束: {std_type} on {filename_table}.{target_column}",
203
+ "frontendInstructions": None,
204
+ }
205
+ )
206
+ continue
207
+ schema_data["constraints"] = remaining
208
+ logger.info(f"[批量处理] 删除内联约束: {constraint_id}")
209
+ results.append(
210
+ {
211
+ "action": action,
212
+ "success": True,
213
+ "message": f"inline:{constraint_id}",
214
+ "frontendInstructions": generate_frontend_instructions(action, workspace_path),
215
+ }
216
+ )
217
+ continue
218
+
219
+ # 构建内联约束结构(字段与其他约束保持一致)
220
+ constraint_description = spec.get("description") or f"{constraint_id}"
221
+ inline_constraint = {
222
+ "id": constraint_id,
223
+ "column": column_id,
224
+ "type": std_type,
225
+ "enabled": True,
226
+ "description": constraint_description,
227
+ }
228
+ params = _build_constraint_params(std_type, spec)
229
+ if params:
230
+ inline_constraint["params"] = params
231
+
232
+ # 检查是否已存在相同列和类型的约束
233
+ existing_idx = None
234
+ for idx, existing in enumerate(schema_data["constraints"]):
235
+ if existing.get("column") == column_id and existing.get("type") == std_type:
236
+ existing_idx = idx
237
+ break
238
+
239
+ if existing_idx is not None:
240
+ # 更新现有约束
241
+ schema_data["constraints"][existing_idx] = inline_constraint
242
+ logger.info(f"[批量处理] 更新内联约束: {constraint_id}")
243
+ else:
244
+ # 添加新约束
245
+ schema_data["constraints"].append(inline_constraint)
246
+ logger.info(f"[批量处理] 添加内联约束: {constraint_id}")
247
+
248
+ results.append(
249
+ {
250
+ "action": action,
251
+ "success": True,
252
+ "message": f"inline:{constraint_id}",
253
+ "frontendInstructions": generate_frontend_instructions(action, workspace_path),
254
+ }
255
+ )
256
+
257
+ except Exception as e:
258
+ logger.error(f"[批量处理] 处理约束失败: {e}")
259
+ results.append(
260
+ {"action": action, "success": False, "message": str(e), "frontendInstructions": None}
261
+ )
262
+
263
+ # 只要有成功的操作,就一次性写入所有修改
264
+ if any(r["success"] for r in results):
265
+ # 含删除动作时必须全量替换写入:atomic_write_yaml 的 preserve_format
266
+ # 路径按 id 合并列表(只更新/追加、不删除),会把已删除的约束"复活"
267
+ atomic_write_yaml(schema_file, schema_data, preserve_format=not has_delete)
268
+ logger.info(f"[批量处理] 成功保存 {len([r for r in results if r['success']])} 个约束到 schema")
269
+
270
+ except Exception as e:
271
+ logger.error(f"[批量处理] 批量处理失败: {e}")
272
+ # 为尚未记录结果的操作标记为失败
273
+ for i, action in enumerate(actions):
274
+ if len(results) < i + 1:
275
+ results.append(
276
+ {"action": action, "success": False, "message": f"批量处理失败: {e}", "frontendInstructions": None}
277
+ )
278
+
279
+ return results
@@ -0,0 +1,35 @@
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 LLM 服务发现模块入口
17
+
18
+ 功能概述:
19
+ - 导出本地 LLM 服务扫描相关类(ServiceScanner、DiscoveredService)
20
+ - 供 API 层调用以自动发现本地运行的 AI 服务
21
+
22
+ 架构设计:
23
+ - 聚合导出 scanner.py 中的公共类和全局实例
24
+ - 通过 __all__ 控制对外暴露的接口
25
+
26
+ 输入示例:
27
+ from app.shared.services.llm.discovery import scanner, DiscoveredService
28
+
29
+ 输出示例:
30
+ services = await scanner.scan()
31
+ """
32
+
33
+ from .scanner import DiscoveredService, ServiceScanner, scanner
34
+
35
+ __all__ = ["ServiceScanner", "DiscoveredService", "scanner"]