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,592 @@
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 apply_actions 工具
17
+
18
+ Chat mini-agent 可调用的工具:执行配置修改动作。
19
+
20
+ 支持两种模式:
21
+ 1. 两阶段确认(dry_run_enabled=True + confirm_controller 注入):
22
+ dry-run shadow-copy → diff → await 用户确认 → 落盘
23
+ 2. legacy 直写(dry_run_enabled=False 或无 controller):
24
+ 直接 process_actions 写盘,行为与改造前一致。
25
+
26
+ 关键机制:工具内部把 process_actions 产出的 frontendInstructions
27
+ 旁路累积到 runner.collected_instructions,供 orchestrator 最终
28
+ 注入 ChatExecutionResult.frontend_instructions,实现前端画布双写。
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import asyncio
34
+ import logging
35
+ from collections.abc import Callable
36
+ from dataclasses import dataclass
37
+ from typing import Any
38
+
39
+ from app.shared.services.llm.actions.action_processor import process_actions
40
+ from app.shared.services.llm.actions.action_validator import ActionValidator
41
+ from app.shared.services.llm.actions.diff_compute import compute_action_diff
42
+ from app.shared.services.llm.actions.registry import (
43
+ ACTION_COUNT,
44
+ ACTION_ENUM,
45
+ READ_ONLY_ACTION_TYPES,
46
+ )
47
+ from app.shared.services.llm.actions.validation_types import format_validation_result
48
+
49
+ # 延迟导入以避免循环依赖(streaming/__init__ → orchestrator → apply_actions → streaming)
50
+ # 在 _run_two_phase 内部 import ConfirmController / get_global_pending_interaction_store
51
+
52
+ logger = logging.getLogger(__name__)
53
+
54
+ # 默认以 legacy 模式运行(dry_run_enabled=False),确保未注入时不改变行为
55
+ _DRY_RUN_ENABLED_DEFAULT = False
56
+
57
+
58
+ @dataclass
59
+ class ApplyCallbacks:
60
+ """apply_actions 工具的回调集合,由 orchestrator 注入以实现事件桥接。"""
61
+
62
+ on_apply_pending: Callable[[dict[str, Any]], None] | None = None
63
+ on_apply_confirmed: Callable[[dict[str, Any]], None] | None = None
64
+ on_apply_rejected: Callable[[dict[str, Any]], None] | None = None
65
+ # 流式画布生长:确认落盘后逐条 emit 单条 frontend_instruction,
66
+ # payload 形如 {"instruction": {...单条指令对象...}},前端收到即执行 + fitView。
67
+ on_frontend_instruction: Callable[[dict[str, Any]], None] | None = None
68
+
69
+
70
+ def _summarize_results(raw_results: list[dict[str, Any]]) -> list[dict[str, Any]]:
71
+ """从 process_actions 原始结果提取 LLM 摘要(不含 frontendInstructions)。"""
72
+ summarized = []
73
+ for r in raw_results:
74
+ action = r.get("action", {})
75
+ summarized.append(
76
+ {
77
+ "actionType": action.get("actionType", "unknown"),
78
+ "success": r.get("success", False),
79
+ "message": r.get("message", ""),
80
+ }
81
+ )
82
+ for r in raw_results:
83
+ if r.get("validate_details"):
84
+ summarized.append({"validate_details": r["validate_details"]})
85
+ return summarized
86
+
87
+
88
+ # 纯读动作(无副作用),允许在无确认环境(非流式 /chat、CLI)直接执行。
89
+ # 从动作注册表派生,避免硬编码集合与注册表不同步。
90
+ _READ_ONLY_ACTION_TYPES = READ_ONLY_ACTION_TYPES
91
+
92
+
93
+ def _collect_instructions(raw_results: list[dict[str, Any]], target_list: list[Any]) -> None:
94
+ """从 raw_results 中提取 frontendInstructions 并追加到 target_list。"""
95
+ for r in raw_results:
96
+ fi = r.get("frontendInstructions")
97
+ if fi:
98
+ target_list.append(fi)
99
+
100
+
101
+ class ApplyActionsTool:
102
+ """
103
+ @classdesc 执行配置修改动作工具
104
+
105
+ 封装 process_actions,同时旁路收集 frontendInstructions。
106
+ 支持两阶段确认模式。
107
+ """
108
+
109
+ NAME = "apply_actions"
110
+
111
+ def __init__(
112
+ self,
113
+ project_path: str,
114
+ collected_instructions: list[Any],
115
+ dry_run_enabled: bool = _DRY_RUN_ENABLED_DEFAULT,
116
+ confirm_controller: Any | None = None,
117
+ apply_callbacks: ApplyCallbacks | None = None,
118
+ job_id: str = "",
119
+ user_message: str = "",
120
+ ):
121
+ """
122
+ @methoddesc 初始化工具
123
+
124
+ 参数:
125
+ project_path: 当前项目配置目录路径
126
+ collected_instructions: runner 持有的累积列表引用,工具会把
127
+ frontendInstructions 追加到此列表(共享可变引用)
128
+ dry_run_enabled: 是否启用两阶段确认模式
129
+ confirm_controller: (已废弃)旧的单 job 控制器;保留兼容但不再用于门控
130
+ apply_callbacks: 事件回调集合(两阶段模式用)
131
+ job_id: 当前任务 ID,用于生成 apply_id("{job_id}#{seq}")键控每次 apply 的独立确认
132
+ user_message: 用户当前输入的原始消息,用于意图范围校验(防止 LLM 添加无关修改)
133
+ """
134
+ self.project_path = project_path
135
+ self._collected_instructions = collected_instructions
136
+ self._dry_run_enabled = dry_run_enabled
137
+ self._apply_callbacks = apply_callbacks or ApplyCallbacks()
138
+ self._job_id = job_id
139
+ # user_message 保留注入(向后兼容 chat_agent_runner 调用),但 P2-1 后意图校验
140
+ # 改用 LLM 自填的 intent_scope 做一致性比对,不再依赖 user_message 做关键词匹配。
141
+ self._user_message = user_message or ""
142
+ # 每次 _run_two_phase 自增,保证同一 job 内多次 apply 各有独立 apply_id
143
+ self._apply_counter = 0
144
+
145
+ def _extract_action_target(self, action: dict[str, Any]) -> tuple[str | None, str | None]:
146
+ """从动作中提取目标表名和列名(如可提取)。"""
147
+ action_type = action.get("actionType", "")
148
+ spec: dict[str, Any] = {}
149
+ table: str | None = None
150
+ column: str | None = None
151
+
152
+ if action_type in (
153
+ "ADD_CONSTRAINT_NODE",
154
+ "UPDATE_CONSTRAINT_NODE",
155
+ "DELETE_CONSTRAINT_NODE",
156
+ ):
157
+ spec = action.get("constraintSpec", {}) or {}
158
+ table = spec.get("tableName") or spec.get("targetNodeId")
159
+ column = spec.get("targetColumn") or spec.get("targetColumnId")
160
+ elif action_type in ("ADD_SCHEMA", "UPDATE_SCHEMA", "DELETE_SCHEMA"):
161
+ spec = action.get("schemaSpec", {}) or {}
162
+ table = spec.get("name") or spec.get("schemaId") or spec.get("id")
163
+ elif action_type in ("ADD_TRANSFORM", "UPDATE_TRANSFORM", "DELETE_TRANSFORM"):
164
+ spec = action.get("transformSpec", {}) or {}
165
+ table = spec.get("inputFromNode") or spec.get("inputNodeId")
166
+ column = spec.get("inputColumn")
167
+ elif action_type in ("ADD_REGEX", "UPDATE_REGEX", "DELETE_REGEX"):
168
+ # Regex 节点通常不直接绑定到具体表/列,不做强校验
169
+ return None, None
170
+
171
+ # 清洗字符串
172
+ if table and isinstance(table, str):
173
+ table = table.strip()
174
+ if column and isinstance(column, str):
175
+ column = column.strip()
176
+ return table, column
177
+
178
+ def _check_actions_match_intent(
179
+ self, actions: list[dict[str, Any]], intent_scope: dict[str, Any] | None
180
+ ) -> tuple[bool, str]:
181
+ """意图范围校验(P2-1):基于 LLM 自填的 intent_scope 做一致性比对。
182
+
183
+ 设计原则(替代旧的 user_message 关键词匹配):
184
+ - LLM 在调用 apply_actions 时显式声明 intent_scope(它理解的用户意图所涉及的表/列),
185
+ 后端只做确定性比对:每个写动作的 target 必须落在 LLM 自报的 scope 内。
186
+ - 把"理解自然语言意图"的工作交给 LLM(它的强项),后端只做机械比对,彻底解决
187
+ 旧方案在中文场景("邮箱"≠"email")、通用词列名(id/name)、Regex 绕过等问题。
188
+ - 幻觉性越界的 LLM 同样会老实填 intent_scope(它不知道自己在越界),后端比对恰好能抓出。
189
+ - 只阻断写动作;只读动作(VALIDATE_PROJECT / ADD_TO_CANVAS)不阻断。
190
+ - intent_scope 为空/未填时跳过校验(保持向后兼容,不破坏老调用)。
191
+ - 校验失败时返回明确错误,帮助 LLM 自我修正(配合 P0-1 错误回灌)。
192
+
193
+ 参数:
194
+ actions: 待执行的动作列表
195
+ intent_scope: LLM 自报的意图范围,形如
196
+ {"tables": ["users"], "columns": [{"table": "users", "column": "email"}]}
197
+
198
+ 返回:
199
+ (ok, error):ok=False 时 error 含越界详情
200
+ """
201
+ # intent_scope 未填或为空 → 不限制(保持向后兼容)
202
+ if not intent_scope:
203
+ return True, ""
204
+
205
+ scope_tables_raw = intent_scope.get("tables") or []
206
+ scope_columns_raw = intent_scope.get("columns") or []
207
+
208
+ # 归一化 scope 表名集合(小写比对,消除大小写差异)
209
+ scope_tables: set[str] = {str(t).strip().lower() for t in scope_tables_raw if str(t).strip()}
210
+ # 归一化 scope 列集合:{(table_lower, column_lower)}
211
+ scope_columns: set[tuple[str, str]] = set()
212
+ for col_entry in scope_columns_raw:
213
+ if isinstance(col_entry, dict):
214
+ tbl = str(col_entry.get("table", "")).strip().lower()
215
+ col = str(col_entry.get("column", "")).strip().lower()
216
+ if tbl and col:
217
+ scope_columns.add((tbl, col))
218
+ elif isinstance(col_entry, str):
219
+ # 宽容格式:"table.column"
220
+ if "." in col_entry:
221
+ tbl, col = col_entry.split(".", 1)
222
+ scope_columns.add((tbl.strip().lower(), col.strip().lower()))
223
+
224
+ # scope 全空(LLM 填了字段但没内容)→ 不限制,避免误拦
225
+ if not scope_tables and not scope_columns:
226
+ return True, ""
227
+
228
+ # 检查每个写动作的 target 是否落在 scope 内
229
+ for action in actions:
230
+ action_type = action.get("actionType", "")
231
+ if action_type in _READ_ONLY_ACTION_TYPES:
232
+ continue
233
+
234
+ target_table, target_column = self._extract_action_target(action)
235
+ # 无法提取目标(如 Regex 独立节点)→ 不参与 scope 比对,放行
236
+ if not target_table and not target_column:
237
+ continue
238
+
239
+ target_table_lower = (target_table or "").lower()
240
+ target_column_lower = (target_column or "").lower()
241
+
242
+ # 命中规则(按 scope 粒度从严到宽):
243
+ # 1) scope 含列级声明 + 动作有列目标 → 必须精确命中列级 (table, column)。
244
+ # 表级放行在此场景无效,否则"用户提到 email 列"时 age 列会因同表被误放行。
245
+ # 2) 动作只有表级目标(如 ADD_SCHEMA)→ 命中 scope_tables,或在 scope_columns 的表集合内。
246
+ # 3) 动作有列目标但 scope 只声明了表级 → 命中 scope_tables 即放行(列级未声明不强制)。
247
+ scope_column_tables = {tbl for tbl, _ in scope_columns}
248
+ in_column_scope = (
249
+ bool(target_table_lower)
250
+ and bool(target_column_lower)
251
+ and (target_table_lower, target_column_lower) in scope_columns
252
+ )
253
+ in_table_scope = bool(target_table_lower) and target_table_lower in scope_tables
254
+ in_column_table_scope = bool(target_table_lower) and target_table_lower in scope_column_tables
255
+
256
+ # scope 含列级声明 且 动作有列目标 → 必须命中列级(最严)
257
+ requires_column_match = bool(scope_columns) and bool(target_column_lower)
258
+ ok = in_column_scope if requires_column_match else (in_table_scope or in_column_table_scope)
259
+
260
+ if not ok:
261
+ # 构造越界错误信息,列出 LLM 自报的 scope 帮助它理解边界
262
+ scope_desc_parts: list[str] = []
263
+ if scope_tables:
264
+ scope_desc_parts.append(f"表 {sorted(scope_tables)}")
265
+ if scope_columns:
266
+ cols_desc = [f"{t}.{c}" for t, c in sorted(scope_columns)]
267
+ scope_desc_parts.append(f"列 {cols_desc}")
268
+ scope_desc = "、".join(scope_desc_parts)
269
+ target_desc = target_column_lower or target_table_lower
270
+ return (
271
+ False,
272
+ f"动作 {action_type} 针对的 '{target_desc}' 不在你声明的 intent_scope 内"
273
+ f"(scope: {scope_desc})。请只修改 intent_scope 内的资源,"
274
+ "或更新 intent_scope 以覆盖该动作的目标。",
275
+ )
276
+
277
+ return True, ""
278
+
279
+ def get_definition(self) -> dict[str, Any]:
280
+ """返回 OpenAI tool 定义。"""
281
+ return {
282
+ "type": "function",
283
+ "function": {
284
+ "name": self.NAME,
285
+ "description": (
286
+ "执行配置修改动作。当用户明确要求添加/修改/删除约束、表结构、"
287
+ "正则节点、转换节点、把已有资源放到画布上或修改项目设置时调用此工具。"
288
+ "actions 数组中的每个元素必须包含 actionType 和对应的 spec 字段。"
289
+ "执行成功后,改动会立即写入项目文件并同步到画布。"
290
+ "如果是纯查询类问题(如'有哪些表'),不要调用此工具,改用 read_project。"
291
+ "注意:项目配置文件里存在的表/约束/正则,不一定已经在画布上显示;"
292
+ "当用户说'拖到画布'、'放到画布'、'显示在画布上'时,"
293
+ "必须显式使用 actionType=ADD_TO_CANVAS(不是 ADD_SCHEMA/ADD_REGEX 等)。"
294
+ "重要约束:actions 列表只能包含用户当前明确请求的修改,"
295
+ "禁止主动添加、修改或删除无关的约束、表、正则节点或转换节点。"
296
+ ),
297
+ "parameters": {
298
+ "type": "object",
299
+ "properties": {
300
+ "actions": {
301
+ "type": "array",
302
+ "description": (
303
+ "要执行的动作列表。每个元素必须含 actionType 和对应的 spec 字段:\n"
304
+ "- 约束动作(actionType=ADD/UPDATE/DELETE_CONSTRAINT_NODE)→ constraintSpec\n"
305
+ "- Schema 动作(actionType=ADD/UPDATE/DELETE_SCHEMA)→ schemaSpec\n"
306
+ "- 正则动作(actionType=ADD/UPDATE/DELETE_REGEX)→ regexSpec\n"
307
+ "- 转换动作(actionType=ADD/UPDATE/DELETE_TRANSFORM)→ transformSpec\n"
308
+ "- 设置动作(actionType=UPDATE_SETTINGS)→ settingsSpec\n"
309
+ "- 校验动作(actionType=VALIDATE_PROJECT)→ constraintSpec(含 tableName)\n"
310
+ "- 显示到画布(actionType=ADD_TO_CANVAS)→ canvasSpec(把已存在的配置显示到画布,不写盘)\n"
311
+ "注意字段名是 constraintSpec/schemaSpec 等(不是 spec)。"
312
+ ),
313
+ "items": {
314
+ "type": "object",
315
+ "properties": {
316
+ "actionType": {
317
+ "type": "string",
318
+ # enum 从注册表派生,新增动作自动出现,消灭"漏条目"bug
319
+ "enum": list(ACTION_ENUM),
320
+ "description": f"动作类型({ACTION_COUNT} 种)。",
321
+ },
322
+ },
323
+ "required": ["actionType"],
324
+ },
325
+ },
326
+ "intent_scope": {
327
+ "type": "object",
328
+ "description": (
329
+ "本次请求的目标范围声明(强烈建议填写,用于防止越界修改)。"
330
+ "声明你理解的用户意图所涉及的表和列,后端会校验 actions 中的写动作"
331
+ "目标是否全部落在此范围内——这是防止你误加无关修改的安全门。\n"
332
+ "例:用户说'给 email 加格式校验',intent_scope 应填 "
333
+ '{"tables":["users"],"columns":[{"table":"users","column":"email"}]}\n'
334
+ "注意:把自然语言理解(如中文'邮箱'='email')的工作放在你这里完成,"
335
+ "后端只做表名/列名的精确比对。"
336
+ ),
337
+ "properties": {
338
+ "tables": {
339
+ "type": "array",
340
+ "items": {"type": "string"},
341
+ "description": "涉及的表名(schema 的 name 或 id)",
342
+ },
343
+ "columns": {
344
+ "type": "array",
345
+ "items": {
346
+ "type": "object",
347
+ "properties": {
348
+ "table": {"type": "string", "description": "表名"},
349
+ "column": {"type": "string", "description": "列名"},
350
+ },
351
+ "required": ["table", "column"],
352
+ },
353
+ "description": "涉及的精确列(table + column)",
354
+ },
355
+ },
356
+ },
357
+ },
358
+ "required": ["actions"],
359
+ },
360
+ },
361
+ }
362
+
363
+ async def run(self, arguments: dict[str, Any]) -> dict[str, Any]:
364
+ """
365
+ @methoddesc 执行配置修改动作
366
+
367
+ 参数:
368
+ arguments: tool 参数,包含 actions 列表
369
+
370
+ 返回:
371
+ {"success": bool, "results": [...], "error": str}
372
+ results 为每个动作的执行结果摘要(不含 frontendInstructions,避免污染 observation)
373
+ """
374
+ actions = arguments.get("actions", [])
375
+ if not isinstance(actions, list) or not actions:
376
+ return {"success": False, "error": "actions 必须是非空数组", "results": []}
377
+
378
+ if not self.project_path:
379
+ return {"success": False, "error": "未配置项目路径", "results": []}
380
+
381
+ # 意图范围校验(P2-1):基于 LLM 自填的 intent_scope 做一致性比对,
382
+ # 防止 LLM 越界修改无关资源。intent_scope 为空时跳过(向后兼容)。
383
+ intent_scope = arguments.get("intent_scope")
384
+ intent_ok, intent_error = self._check_actions_match_intent(actions, intent_scope)
385
+ if not intent_ok:
386
+ logger.warning(f"[apply_actions] 意图范围校验失败:{intent_error}")
387
+ return {
388
+ "success": False,
389
+ "error": f"动作超出用户请求范围:{intent_error}",
390
+ "results": [],
391
+ }
392
+
393
+ # 预验证:在 dry-run / 写盘前拦截非法动作(表/列不存在、约束类型不支持、参数缺失等)。
394
+ # 采用全有或全无语义——任一动作有 error 即整批拒绝,把含 "did you mean" 建议的错误清单
395
+ # 回灌给 LLM 以便自我修正。warnings 不阻止执行(对齐 ValidationResult 设计)。
396
+ validator = ActionValidator(self.project_path)
397
+ validation = validator.validate(actions)
398
+ if validation.has_errors:
399
+ formatted = format_validation_result(validation)
400
+ logger.warning(f"[apply_actions] 预验证失败,拒绝执行:\n{formatted}")
401
+ return {
402
+ "success": False,
403
+ "error": f"动作预验证失败,未执行任何修改:\n{formatted}",
404
+ "results": [],
405
+ "validation_errors": len(validation.errors),
406
+ }
407
+
408
+ # 判断是否进入两阶段确认模式(dry_run_enabled 即两阶段;不再依赖单 job controller)
409
+ two_phase = self._dry_run_enabled
410
+
411
+ # 分流只读动作(ADD_TO_CANVAS / VALIDATE_PROJECT):无论是否 two_phase,
412
+ # 只读动作都不写盘、不需用户确认,直接执行。
413
+ # 这避免"拖入画布"这类只读操作弹出不必要的确认框(且确认框内容为空 diff,令人困惑)。
414
+ read_only_actions = [a for a in actions if a.get("actionType", "") in _READ_ONLY_ACTION_TYPES]
415
+ write_actions = [a for a in actions if a.get("actionType", "") not in _READ_ONLY_ACTION_TYPES]
416
+
417
+ # 先执行只读动作(如果有),结果累积到 combined
418
+ combined_results: list[dict[str, Any]] = []
419
+ read_only_result: dict[str, Any] | None = None
420
+ if read_only_actions:
421
+ read_only_result = await self._run_legacy(read_only_actions)
422
+ if read_only_result.get("success"):
423
+ combined_results.extend(read_only_result.get("results", []))
424
+ else:
425
+ # 只读动作失败则整体返回(全有或全无语义)
426
+ return read_only_result
427
+
428
+ # 无写盘动作:只读动作已执行完毕,直接返回合并结果
429
+ if not write_actions:
430
+ return {
431
+ "success": True,
432
+ "results": combined_results,
433
+ "instructions_collected": len(self._collected_instructions),
434
+ }
435
+
436
+ if not two_phase:
437
+ # 无确认环境(非流式 /chat、CLI ai ask):对写操作 fail-closed
438
+ logger.warning("[apply_actions] 无确认环境拒绝写操作(fail-closed)")
439
+ return {
440
+ "success": False,
441
+ "error": "此环境不支持自动写盘(无确认门),请在对话界面操作以获得用户确认。",
442
+ "results": combined_results,
443
+ }
444
+
445
+ # 写盘动作走两阶段确认;只读动作的结果已累积,最终合并返回
446
+ write_result = await self._run_two_phase(write_actions)
447
+ # 合并只读 + 写盘的结果
448
+ if combined_results:
449
+ merged_results = combined_results + write_result.get("results", [])
450
+ write_result["results"] = merged_results
451
+ return write_result
452
+
453
+ async def _run_legacy(self, actions: list[dict[str, Any]]) -> dict[str, Any]:
454
+ """legacy 直写模式:仅用于纯读动作(VALIDATE_PROJECT),直接执行。"""
455
+ try:
456
+ process_result = await asyncio.to_thread(process_actions, actions, self.project_path)
457
+ except Exception as e:
458
+ logger.exception("apply_actions 工具执行失败")
459
+ return {"success": False, "error": f"执行动作失败: {e}", "results": []}
460
+
461
+ all_success = process_result.get("success", False)
462
+ raw_results = process_result.get("results", [])
463
+
464
+ _collect_instructions(raw_results, self._collected_instructions)
465
+ summarized_results = _summarize_results(raw_results)
466
+
467
+ return {
468
+ "success": all_success,
469
+ "results": summarized_results,
470
+ "instructions_collected": len(self._collected_instructions),
471
+ }
472
+
473
+ async def _run_two_phase(self, actions: list[dict[str, Any]]) -> dict[str, Any]:
474
+ """两阶段确认模式:dry-run → await 用户决策 → 落盘/跳过。
475
+
476
+ 每次 apply_actions 调用创建独立的 ConfirmController(按 apply_id 键控),
477
+ 避免旧实现"每 job 一个 controller + Event 单次锁存"导致第 2 个 apply 免确认。
478
+
479
+ 阶段 1: shadow-copy dry-run 计算 diff(不碰真实项目)
480
+ 阶段 2: 用户 confirm 后真实写盘;reject 则不写
481
+ """
482
+ # 为本次 apply 生成独立 apply_id,创建全新 controller(不复用旧决策)
483
+ self._apply_counter += 1
484
+ # 加 #apply# 类型前缀,与 ask 的 "{job_id}#ask#{seq}" 对称,便于 store 按 job 维度批量清理
485
+ apply_id = f"{self._job_id}#apply#{self._apply_counter}" if self._job_id else f"apply#{self._apply_counter}"
486
+ # 延迟导入避免循环依赖(streaming 包 init 链 → apply_actions)
487
+ from app.shared.services.ai.streaming.pending_interaction_store import (
488
+ ConfirmController,
489
+ get_global_pending_interaction_store,
490
+ )
491
+
492
+ controller = ConfirmController(request_id=apply_id)
493
+ pending_store = get_global_pending_interaction_store()
494
+ pending_store.put(apply_id, controller)
495
+
496
+ try:
497
+ # 阶段 1: dry-run 计算 diff
498
+ try:
499
+ diff_result = await asyncio.to_thread(compute_action_diff, actions, self.project_path)
500
+ except Exception as e:
501
+ logger.exception("apply_actions dry-run 失败")
502
+ return {"success": False, "error": f"dry-run 失败: {e}", "results": []}
503
+
504
+ if not diff_result.success:
505
+ return {"success": False, "error": diff_result.error or "dry-run 失败", "results": []}
506
+
507
+ # 构建 pending payload,发给前端展示(含 apply_id 供前端回传)
508
+ pending_payload: dict[str, Any] = {
509
+ "apply_id": apply_id,
510
+ "files": [
511
+ {
512
+ "path": f.path,
513
+ "status": f.status,
514
+ "diff": f.diff,
515
+ "before_preview": f.before_preview,
516
+ "after_preview": f.after_preview,
517
+ }
518
+ for f in diff_result.files
519
+ ],
520
+ "summary": diff_result.summary,
521
+ "success": diff_result.success,
522
+ "error": diff_result.error,
523
+ }
524
+
525
+ # 通过回调 emit apply_pending 事件(非阻塞)
526
+ if self._apply_callbacks.on_apply_pending:
527
+ self._apply_callbacks.on_apply_pending(pending_payload)
528
+
529
+ # 等待用户决策(协程挂起)——每次调用独立 controller,不受历史决策影响
530
+ decision = await controller.await_decision()
531
+
532
+ if decision != "confirm":
533
+ # 拒绝/超时:不写盘,返回明确的非成功状态(success=False)
534
+ # 避免 LLM 看到 success=True 误报"已为您添加约束"
535
+ if self._apply_callbacks.on_apply_rejected:
536
+ self._apply_callbacks.on_apply_rejected({"reason": "user_rejected", "decision": decision})
537
+ return {
538
+ "success": False,
539
+ "skipped": True,
540
+ "reason": f"用户选择{decision},未写入文件",
541
+ "results": [],
542
+ }
543
+
544
+ # 确认:真实写盘
545
+ try:
546
+ process_result = await asyncio.to_thread(process_actions, actions, self.project_path)
547
+ except Exception as e:
548
+ logger.exception("apply_actions 确认后写盘失败")
549
+ return {"success": False, "error": f"写盘失败: {e}", "results": []}
550
+
551
+ all_success = process_result.get("success", False)
552
+ raw_results = process_result.get("results", [])
553
+
554
+ _collect_instructions(raw_results, self._collected_instructions)
555
+ summarized_results = _summarize_results(raw_results)
556
+
557
+ # 收集 dry-run 阶段的 frontend_instructions(写盘后可能已变化,但仍保留)
558
+ for fi in diff_result.frontend_instructions:
559
+ self._collected_instructions.append(fi)
560
+
561
+ # 流式画布生长:逐条 emit 写盘产出的 frontend_instruction。
562
+ # 仅取 raw_results 中实际落盘的单条指令(已 disk-committed),
563
+ # 每条 payload 形如 {"instruction": {...}},前端收到即 processFrontendInstructions + fitView。
564
+ # 注意:completed 事件仍携带全量 instructions 作为兜底,前端负责去重避免重复应用。
565
+ if self._apply_callbacks.on_frontend_instruction:
566
+ for r in raw_results:
567
+ fi = r.get("frontendInstructions")
568
+ if fi:
569
+ self._apply_callbacks.on_frontend_instruction({"instruction": fi})
570
+
571
+ # 通过回调 emit apply_confirmed 事件
572
+ # 注意:用 .get("actionType", "validate") 容缺(validate_details 条目无 actionType,否则 KeyError)
573
+ if self._apply_callbacks.on_apply_confirmed:
574
+ self._apply_callbacks.on_apply_confirmed(
575
+ {
576
+ "success": all_success,
577
+ "actions_count": len(actions),
578
+ "results": [
579
+ {"actionType": r.get("actionType", "validate"), "success": r.get("success", False)}
580
+ for r in summarized_results
581
+ ],
582
+ }
583
+ )
584
+
585
+ return {
586
+ "success": all_success,
587
+ "results": summarized_results,
588
+ "instructions_collected": len(self._collected_instructions),
589
+ }
590
+ finally:
591
+ # 无论 confirm/reject/timeout/异常,都清理本次 apply 的 controller(避免 store 泄漏)
592
+ pending_store.pop(apply_id)