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,725 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """@fileoverview AI Chat 统一编排服务模块
17
+
18
+ 功能概述:
19
+ - 为前端 API 和 CLI 提供统一的 AI Chat 底层能力
20
+ - 管理对话历史、上下文构建、响应解析与动作执行
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import logging
27
+ from collections.abc import Callable
28
+ from dataclasses import dataclass, field
29
+ from typing import Any, Protocol
30
+
31
+ from app.shared.services.ai.failure_messages import describe_ai_failure
32
+ from app.shared.services.llm.actions.action_parser import (
33
+ ActionParseError,
34
+ process_actions,
35
+ )
36
+ from app.shared.services.llm.actions.action_validator import ActionValidator
37
+ from app.shared.services.llm.chat.chat_system_prompt import build_system_prompt
38
+ from app.shared.services.llm.chat.response_parser import ActionParser
39
+ from app.shared.services.llm.config.models import AIProvider
40
+ from app.shared.services.llm.providers.base import ChatMessage, ChatRequest
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ # =============================================================================
46
+ # 回调协议(用于 CLI 交互)
47
+ # =============================================================================
48
+
49
+
50
+ class ConfirmCallback(Protocol):
51
+ """操作确认回调协议"""
52
+
53
+ def __call__(self, actions: list[dict[str, Any]], reply: str) -> bool: ...
54
+
55
+
56
+ class AmbiguityResolverCallback(Protocol):
57
+ """歧义解析回调协议"""
58
+
59
+ def __call__(self, actions: list[dict[str, Any]], project_path: str) -> bool: ...
60
+
61
+
62
+ class ProgressCallback(Protocol):
63
+ """进度通知回调协议"""
64
+
65
+ def __call__(self, stage: str, message: str, data: dict | None = None) -> None: ...
66
+
67
+
68
+ # =============================================================================
69
+ # 数据类
70
+ # =============================================================================
71
+
72
+
73
+ @dataclass
74
+ class ChatOptions:
75
+ """Chat 执行选项"""
76
+
77
+ # 对话相关
78
+ history: list[dict[str, str]] = field(default_factory=list)
79
+ max_history_tokens: int = 120000
80
+ temperature: float = 0.1
81
+
82
+ # 交互相关(CLI 使用)
83
+ enable_interactive: bool = False
84
+ confirm_callback: ConfirmCallback | None = None
85
+ ambiguity_resolver: AmbiguityResolverCallback | None = None
86
+ progress_callback: ProgressCallback | None = None
87
+ pause_callback: Callable[[], None] | None = None
88
+
89
+ # 功能开关
90
+ skip_action_validation: bool = False
91
+ skip_action_processing: bool = False
92
+ return_frontend_instructions: bool = True
93
+ agent_mode: bool = False
94
+ # 默认 5 轮:覆盖典型多步工作流(read_canvas → read_project → apply_actions → validate)
95
+ max_agent_iterations: int = 5
96
+ # 画布节点快照(前端请求体携带,供 read_canvas 工具查询画布真实状态)
97
+ canvas_nodes: list[dict[str, Any]] = field(default_factory=list)
98
+
99
+
100
+ @dataclass
101
+ class ChatExecutionResult:
102
+ """Chat 执行结果"""
103
+
104
+ success: bool
105
+ reply: str
106
+ actions: list[dict[str, Any]] = field(default_factory=list)
107
+ frontend_instructions: list[Any] = field(default_factory=list)
108
+ error: str | None = None
109
+ updated_history: list[dict[str, str]] = field(default_factory=list)
110
+ validation_result: dict[str, Any] | None = None
111
+ action_results: list[dict[str, Any]] = field(default_factory=list) # 动作执行结果
112
+ iterations: int = 0 # Agent 模式下的实际迭代轮数
113
+ tool_steps: list[dict[str, Any]] = field(default_factory=list) # Agent 工具调用轨迹
114
+
115
+
116
+ # =============================================================================
117
+ # 核心编排器
118
+ # =============================================================================
119
+
120
+
121
+ class AIChatOrchestrator:
122
+ """
123
+ @classdesc AI Chat 统一编排器
124
+
125
+ 职责:
126
+ 1. 统一处理 API 和 CLI 的 AI Chat 请求
127
+ 2. 管理对话历史和 Token 截断
128
+ 3. 处理动作验证、歧义解析、执行
129
+ 4. 生成前端渲染指令
130
+ """
131
+
132
+ def __init__(self, provider: AIProvider):
133
+ """
134
+ 初始化编排器
135
+
136
+ Args:
137
+ provider: AIProvider 配置对象(包含 api_key、base_url、model 等信息)
138
+ """
139
+ self._provider = provider
140
+
141
+ async def execute_chat(
142
+ self,
143
+ message: str,
144
+ project_path: str | None,
145
+ context_nodes: list[dict[str, Any]],
146
+ options: ChatOptions | None = None,
147
+ ) -> ChatExecutionResult:
148
+ """
149
+ @methoddesc 执行 AI Chat(统一入口)
150
+
151
+ 完整处理流程:
152
+ 1. 构建上下文数据和系统提示词
153
+ 2. 组装消息列表(含历史截断)
154
+ 3. 调用 LLM 获取回复
155
+ 4. 解析响应中的 reply 和 actions
156
+ 5. 更新对话历史
157
+ 6. 如需则执行动作(歧义解析 → 预验证 → 执行 → 收集前端指令)
158
+
159
+ 参数:
160
+ message: 用户输入的消息文本
161
+ project_path: 项目路径(用于读取 schema、执行动作),可为 None
162
+ context_nodes: 前端选中的上下文节点列表
163
+ options: Chat 执行选项,None 则使用默认选项
164
+
165
+ 返回:
166
+ ChatExecutionResult: 包含回复、动作、执行结果等的完整结果
167
+ """
168
+ options = options or ChatOptions()
169
+
170
+ # =====================================================================
171
+ # Agent 模式:走真正的工具循环(read/apply/validate 四件套)
172
+ # 与下方旧路径(LLM 直接输出 {reply,actions})互斥,提前返回
173
+ # =====================================================================
174
+ if options.agent_mode and project_path:
175
+ return await self._execute_with_agent(message, project_path, context_nodes, options)
176
+
177
+ # 请求了 agent 模式但没有 project_path:无法运行工具循环,静默降级到旧路径。
178
+ # 记一条 warning 方便排查"为什么 agent 模式没生效"(如配置路径未透传)。
179
+ if options.agent_mode and not project_path:
180
+ logger.warning("agent_mode=True 但 project_path 为空,降级为非 agent 路径")
181
+
182
+ # 步骤 1: 构建上下文数据(项目概览 + 选中节点)
183
+ context_data = self._build_context_data(message, context_nodes, project_path)
184
+ system_prompt = build_system_prompt(context_data)
185
+
186
+ # 步骤 2: 构建消息列表(系统提示 + 截断历史 + 当前用户消息)
187
+ messages = self._build_messages(
188
+ system_prompt=system_prompt,
189
+ history=options.history,
190
+ user_message=message,
191
+ max_tokens=options.max_history_tokens,
192
+ )
193
+
194
+ # 步骤 3: 调用 LLM
195
+ # 直接 await provider.chat(),与 agent 路径(_execute_with_agent / agent/executor.py)对齐。
196
+ # 不再走 ChatLLMService 同步包装层 —— 后者在 async 上下文里会开子线程跑 asyncio.run(),
197
+ # 导致 httpx 连接池清理任务绑定到子线程的临时 loop,loop 关闭后泄漏
198
+ # "Task exception was never retrieved: RuntimeError: Event loop is closed"。
199
+ # create 用延迟导入,与 _execute_with_agent 一致,方便测试 patch providers.create。
200
+ self._notify_progress(options, "llm_calling", "正在调用 AI...")
201
+ try:
202
+ from app.shared.services.llm.providers import create
203
+
204
+ provider = create(self._provider)
205
+ chat_messages = [ChatMessage(role=m["role"], content=m["content"]) for m in messages]
206
+ req = ChatRequest(
207
+ messages=chat_messages,
208
+ model=self._provider.model,
209
+ temperature=options.temperature,
210
+ )
211
+ resp = await provider.chat(req)
212
+ # resp.content 类型为 str | None(空响应场景),空字符串兜底交给后续解析阶段处理
213
+ llm_response = resp.content or ""
214
+ except Exception as e:
215
+ logger.error(f"LLM 调用失败: {e}")
216
+ return ChatExecutionResult(success=False, reply="", error=describe_ai_failure(e))
217
+
218
+ # 步骤 4: 解析响应(提取 reply 和 actions)
219
+ self._notify_progress(options, "parsing", "解析 AI 响应...")
220
+ parsed = self._parse_llm_response(llm_response)
221
+
222
+ if not parsed["valid"]:
223
+ return ChatExecutionResult(
224
+ success=False, reply=parsed.get("reply", ""), error=parsed.get("error", "解析响应失败")
225
+ )
226
+
227
+ reply = parsed.get("reply", "")
228
+ actions = parsed.get("actions", []) or []
229
+
230
+ # 步骤 5: 更新历史(供 CLI 多轮对话使用)
231
+ updated_history = self._update_history(options.history, message, reply)
232
+
233
+ # 步骤 6: 处理动作(如果存在且项目路径有效)
234
+ frontend_instructions: list[Any] = []
235
+ validation_result: dict | None = None
236
+ action_results: list[dict[str, Any]] = []
237
+
238
+ if actions and project_path and not options.skip_action_processing:
239
+ process_result = await self._process_actions(actions, project_path, reply, options, updated_history)
240
+ if isinstance(process_result, ChatExecutionResult):
241
+ return process_result
242
+ actions, validation_result, action_results, frontend_instructions = process_result
243
+
244
+ # 非 agent 模式:process_actions 执行后直接返回(不再有 agent followup)
245
+ return ChatExecutionResult(
246
+ success=True,
247
+ reply=reply,
248
+ actions=actions,
249
+ frontend_instructions=frontend_instructions,
250
+ updated_history=updated_history,
251
+ validation_result=validation_result,
252
+ action_results=action_results,
253
+ )
254
+
255
+ def _build_context_data(
256
+ self,
257
+ message: str,
258
+ context_nodes: list[dict[str, Any]],
259
+ project_path: str | None,
260
+ ) -> dict[str, Any]:
261
+ """
262
+ @methoddesc 构建 AI 所需的上下文数据字典
263
+
264
+ 包含用户消息、是否有选中节点、节点详情以及项目概览。
265
+
266
+ 参数:
267
+ message: 用户原始消息
268
+ context_nodes: 前端选中的节点列表
269
+ project_path: 项目路径
270
+
271
+ 返回:
272
+ 上下文数据字典
273
+ """
274
+ from app.shared.services.ai.utils import get_project_overview
275
+
276
+ # 如果项目路径存在,扫描项目结构生成概览
277
+ project_overview = {}
278
+ if project_path:
279
+ project_overview = get_project_overview(project_path)
280
+
281
+ return {
282
+ "message": message,
283
+ "context": {
284
+ "hasContext": len(context_nodes) > 0,
285
+ "selectedNodes": context_nodes,
286
+ },
287
+ "projectOverview": project_overview,
288
+ }
289
+
290
+ def _build_messages(
291
+ self,
292
+ system_prompt: str,
293
+ history: list[dict[str, str]],
294
+ user_message: str,
295
+ max_tokens: int,
296
+ ) -> list[dict[str, str]]:
297
+ """
298
+ @methoddesc 构建最终发送给 LLM 的消息列表
299
+
300
+ 顺序为:系统提示词 → 截断后的历史对话 → 当前用户消息。
301
+ 历史记录按 Token 上限截断,避免超出模型上下文窗口。
302
+
303
+ 参数:
304
+ system_prompt: 系统提示词文本
305
+ history: 历史对话列表
306
+ user_message: 当前用户消息
307
+ max_tokens: 历史记录的最大 Token 限制
308
+
309
+ 返回:
310
+ 组装好的消息列表
311
+ """
312
+ from app.shared.services.ai.utils import truncate_history_by_tokens
313
+
314
+ messages: list[dict[str, str]] = []
315
+
316
+ # 系统提示词放在首位
317
+ if system_prompt:
318
+ messages.append({"role": "system", "content": system_prompt})
319
+
320
+ # 截断并添加历史(保留最近的对话)
321
+ if history:
322
+ truncated = truncate_history_by_tokens(
323
+ history,
324
+ system_prompt,
325
+ max_tokens=max_tokens,
326
+ )
327
+ messages.extend(truncated)
328
+
329
+ # 当前用户消息放在最后
330
+ messages.append({"role": "user", "content": user_message})
331
+
332
+ return messages
333
+
334
+ def _parse_llm_response(self, response_text: str) -> dict[str, Any]:
335
+ """
336
+ @methoddesc 解析 LLM 返回的响应文本
337
+
338
+ 先尝试多种策略解析 JSON,再验证必须字段(reply 和 actions)。
339
+
340
+ 参数:
341
+ response_text: LLM 返回的原始文本
342
+
343
+ 返回:
344
+ 包含 valid、reply、actions、error 的字典
345
+ """
346
+ try:
347
+ parsed = ActionParser.parse_llm_response(response_text)
348
+ except ActionParseError as e:
349
+ logger.error(f"解析 LLM 响应失败: {e}")
350
+ return {
351
+ "valid": False,
352
+ "reply": "抱歉,我无法理解您的请求。请尝试使用更清晰的描述。",
353
+ "error": str(e),
354
+ "actions": [],
355
+ }
356
+
357
+ if not ActionParser.validate_response(parsed):
358
+ logger.warning(f"LLM 响应格式验证失败: {parsed}")
359
+ return {
360
+ "valid": False,
361
+ "reply": parsed.get("reply", "响应格式不符合要求,请重试。"),
362
+ "error": "响应格式验证失败",
363
+ "actions": [],
364
+ }
365
+
366
+ return {
367
+ "valid": True,
368
+ "reply": parsed.get("reply", ""),
369
+ "actions": parsed.get("actions", []),
370
+ }
371
+
372
+ def _update_history(
373
+ self,
374
+ history: list[dict[str, str]],
375
+ user_message: str,
376
+ assistant_reply: str,
377
+ ) -> list[dict[str, str]]:
378
+ """
379
+ @methoddesc 将当前轮次的对话追加到历史记录中
380
+
381
+ 参数:
382
+ history: 原有历史记录
383
+ user_message: 用户消息
384
+ assistant_reply: AI 回复
385
+
386
+ 返回:
387
+ 新的历史记录列表(不修改原列表)
388
+ """
389
+ new_history = history.copy()
390
+ new_history.append({"role": "user", "content": user_message})
391
+ new_history.append({"role": "assistant", "content": assistant_reply})
392
+ return new_history
393
+
394
+ def _notify_progress(
395
+ self,
396
+ options: ChatOptions,
397
+ stage: str,
398
+ message: str,
399
+ data: dict | None = None,
400
+ ) -> None:
401
+ """
402
+ @methoddesc 触发进度通知
403
+
404
+ 如果配置了进度回调,则调用回调函数通知当前执行阶段。
405
+
406
+ 参数:
407
+ options: Chat 执行选项
408
+ stage: 当前阶段标识(如 "llm_calling", "validating")
409
+ message: 进度描述文本
410
+ data: 附加数据(可选)
411
+ """
412
+ if options.progress_callback:
413
+ options.progress_callback(stage, message, data)
414
+
415
+ async def _execute_with_agent(
416
+ self,
417
+ message: str,
418
+ project_path: str,
419
+ context_nodes: list[dict[str, Any]],
420
+ options: ChatOptions,
421
+ ) -> ChatExecutionResult:
422
+ """
423
+ @methoddesc Agent 模式执行路径
424
+
425
+ 走真正的工具循环:ChatAgentRunner 组装 5 个 chat 工具
426
+ (read_project/read_table/apply_actions/validate_table/read_canvas),
427
+ 调用 AgentExecutor 跑 plan→act→observe 循环,
428
+ 对外仍返回 ChatExecutionResult,保持契约不变。
429
+
430
+ 参数:
431
+ message: 用户消息
432
+ project_path: 项目路径
433
+ context_nodes: 选中上下文节点
434
+ options: Chat 执行选项
435
+
436
+ 返回:
437
+ ChatExecutionResult: agent 产出的 reply + frontend_instructions
438
+ """
439
+ # 延迟导入,避免非 agent 模式下的额外加载
440
+ from app.shared.services.ai.chat_agent_runner import ChatAgentRunner
441
+ from app.shared.services.llm.providers import create
442
+
443
+ self._notify_progress(options, "agent_running", "AI 正在分析并操作...")
444
+
445
+ # self._provider 是 AIProvider 配置对象,AgentExecutor 需要的是 Provider 实例(有 chat 方法)
446
+ # 此处与 ConfigGenerationService._get_provider() 模式一致:用 create() 实例化
447
+ # 注意:create() 可能抛出 ImportError(如 openai 未安装),需在此捕获,
448
+ # 否则异常会穿透到 endpoint 的 try/except 导致 502,而非返回 200 + status=error
449
+ try:
450
+ runner = ChatAgentRunner(
451
+ provider=create(self._provider),
452
+ project_path=project_path,
453
+ context_nodes=context_nodes,
454
+ max_iterations=options.max_agent_iterations,
455
+ max_history_tokens=options.max_history_tokens,
456
+ canvas_nodes=options.canvas_nodes,
457
+ )
458
+ except Exception as e:
459
+ logger.error(f"Agent 模式初始化失败: {e}")
460
+ return ChatExecutionResult(
461
+ success=False,
462
+ reply="",
463
+ error=f"AI 服务初始化失败: {e}",
464
+ )
465
+
466
+ run_result = await runner.run(
467
+ message=message,
468
+ history=options.history,
469
+ )
470
+
471
+ if not run_result.success:
472
+ return ChatExecutionResult(
473
+ success=False,
474
+ reply=run_result.reply,
475
+ frontend_instructions=run_result.frontend_instructions,
476
+ error=run_result.error,
477
+ )
478
+
479
+ # 更新对话历史(供 CLI 多轮对话使用)
480
+ updated_history = self._update_history(options.history, message, run_result.reply)
481
+
482
+ self._notify_progress(options, "completed", "AI 处理完成")
483
+
484
+ return ChatExecutionResult(
485
+ success=True,
486
+ reply=run_result.reply,
487
+ actions=run_result.actions,
488
+ frontend_instructions=run_result.frontend_instructions,
489
+ updated_history=updated_history,
490
+ iterations=run_result.iterations,
491
+ tool_steps=run_result.tool_steps,
492
+ )
493
+
494
+ async def _process_actions(
495
+ self,
496
+ actions: list[dict[str, Any]],
497
+ project_path: str,
498
+ reply: str,
499
+ options: ChatOptions,
500
+ updated_history: list[dict[str, str]],
501
+ ) -> tuple[list[dict[str, Any]], dict[str, Any] | None, list[dict[str, Any]], list[Any]] | ChatExecutionResult:
502
+ """
503
+ @methoddesc 处理 AI 返回的动作列表
504
+
505
+ 阶段编排(细节在各 _xxx_gate 阶段方法):
506
+ 6.1 歧义解析门(交互式 CLI)→ 6.2 预验证门(含交互确认与 all-or-nothing 拒绝)
507
+ → 6.3 fail-closed 确认门检查 + 执行 → 6.4 收集前端指令。
508
+
509
+ 参数:
510
+ actions: 动作列表
511
+ project_path: 项目路径
512
+ reply: AI 回复文本
513
+ options: Chat 执行选项
514
+ updated_history: 更新后的历史记录
515
+
516
+ 返回:
517
+ (actions, validation_result, action_results, frontend_instructions)
518
+ 或提前返回的 ChatExecutionResult
519
+ """
520
+ frontend_instructions: list[Any] = []
521
+ validation_result: dict[str, Any] | None = None
522
+ action_results: list[dict[str, Any]] = []
523
+
524
+ # 6.1 歧义解析(交互式 CLI 使用)
525
+ early = self._resolve_ambiguity_gate(actions, project_path, reply, options, updated_history)
526
+ if early is not None:
527
+ return early
528
+
529
+ # 6.2 动作预验证
530
+ if not options.skip_action_validation:
531
+ early, validated = self._validate_actions_gate(actions, project_path, reply, options, updated_history)
532
+ if early is not None:
533
+ return early
534
+ actions, validation_result = validated
535
+
536
+ # 6.3 执行动作
537
+ # B-sec: 无确认门环境(API 模式 enable_interactive=False 且无 confirm_callback)对写盘
538
+ # 动作 fail-closed,与 agent 路径(apply_actions.py 的 two_phase=False 分支)语义对齐。
539
+ # 此前 legacy 路径会无确认直接 process_actions 写盘,恶意网页借宽松 CORS 可远程
540
+ # 触发无确认配置改写。skip_action_processing=True(显式只生成不执行)不受影响。
541
+ has_confirm_gate = bool(options.enable_interactive and options.confirm_callback)
542
+ if actions and project_path and not options.skip_action_processing and not has_confirm_gate:
543
+ logger.warning("[chat_orchestrator] 无确认环境拒绝写盘动作(fail-closed), 共 %d 个动作", len(actions))
544
+ return ChatExecutionResult(
545
+ success=False,
546
+ reply=reply,
547
+ actions=actions,
548
+ error="此环境不支持自动写盘(无确认门),请使用流式对话界面操作以获得用户确认。",
549
+ updated_history=updated_history,
550
+ validation_result=validation_result,
551
+ )
552
+
553
+ # B-async: process_actions 做大量同步文件 I/O(mkdtemp/读写多 YAML/快照/回滚),
554
+ # 在 async 编排器中直接调用会阻塞事件循环。agent 路径(apply_actions.py:441)
555
+ # 已用 asyncio.to_thread 包裹同一函数,这里对齐。
556
+ self._notify_progress(options, "executing", "执行操作...")
557
+ process_result = await asyncio.to_thread(process_actions, actions, project_path)
558
+ action_results = process_result.get("results", [])
559
+
560
+ if not process_result.get("success", False):
561
+ error_messages = [r.get("message", "未知错误") for r in action_results if not r.get("success", False)]
562
+ return ChatExecutionResult(
563
+ success=False,
564
+ reply=reply,
565
+ actions=actions,
566
+ error="; ".join(error_messages),
567
+ updated_history=updated_history,
568
+ validation_result=validation_result,
569
+ action_results=action_results,
570
+ )
571
+
572
+ # 6.4 收集前端指令
573
+ if options.return_frontend_instructions:
574
+ for result in action_results:
575
+ instructions = result.get("frontendInstructions")
576
+ if instructions:
577
+ frontend_instructions.append(instructions)
578
+
579
+ return actions, validation_result, action_results, frontend_instructions
580
+
581
+ def _resolve_ambiguity_gate(
582
+ self,
583
+ actions: list[dict[str, Any]],
584
+ project_path: str,
585
+ reply: str,
586
+ options: ChatOptions,
587
+ updated_history: list[dict[str, str]],
588
+ ) -> ChatExecutionResult | None:
589
+ """
590
+ @methoddesc 6.1 歧义解析门(交互式 CLI 使用)
591
+
592
+ 调用方可选注入的 ambiguity_resolver 决定是否继续(如表名歧义时人工选择)。
593
+
594
+ 返回:
595
+ 调用方要求中止时返回"成功但不执行"的 ChatExecutionResult;继续执行返回 None
596
+ """
597
+ if not (options.enable_interactive and options.ambiguity_resolver):
598
+ return None
599
+
600
+ self._notify_progress(options, "resolving", "解析表名歧义...")
601
+ should_continue = options.ambiguity_resolver(actions, project_path)
602
+ if not should_continue:
603
+ return ChatExecutionResult(
604
+ success=True,
605
+ reply=reply,
606
+ actions=actions,
607
+ updated_history=updated_history,
608
+ )
609
+ return None
610
+
611
+ def _validate_actions_gate(
612
+ self,
613
+ actions: list[dict[str, Any]],
614
+ project_path: str,
615
+ reply: str,
616
+ options: ChatOptions,
617
+ updated_history: list[dict[str, str]],
618
+ ) -> tuple[ChatExecutionResult | None, tuple[list[dict[str, Any]], dict[str, Any]]]:
619
+ """
620
+ @methoddesc 6.2 动作预验证门
621
+
622
+ 预验证动作合法性 → 交互确认(CLI)→ all-or-nothing 拒绝(有 error 不执行任何动作)。
623
+
624
+ 返回:
625
+ (提前返回的 ChatExecutionResult 或 None, (过滤后的合法动作, 验证结果摘要))
626
+ """
627
+ self._notify_progress(options, "validating", "验证操作...")
628
+ validator = ActionValidator(project_path)
629
+ validation_result_obj = validator.validate(actions)
630
+ validation_result = {
631
+ "has_errors": validation_result_obj.has_errors,
632
+ "has_warnings": validation_result_obj.has_warnings,
633
+ "valid_actions": validation_result_obj.valid_actions,
634
+ }
635
+
636
+ # 交互确认
637
+ if options.enable_interactive and options.confirm_callback:
638
+ if options.pause_callback:
639
+ options.pause_callback()
640
+ confirmed = options.confirm_callback(actions, reply)
641
+ if not confirmed:
642
+ return (
643
+ ChatExecutionResult(
644
+ success=True,
645
+ reply=reply,
646
+ actions=actions,
647
+ updated_history=updated_history,
648
+ validation_result=validation_result,
649
+ ),
650
+ (actions, validation_result),
651
+ )
652
+
653
+ # 校验失败处理:有 error 时直接返回失败 + 错误清单,不执行(all-or-nothing 语义)
654
+ # 修复 #5:旧逻辑 `if valid_actions:` 在全部非法时(valid_actions==[])走 else
655
+ # 把原始的全部非法动作交给 process_actions 写盘,造成校验绕过。
656
+ if validation_result_obj.has_errors:
657
+ from app.shared.services.llm.actions.validation_types import format_validation_result
658
+
659
+ formatted = format_validation_result(validation_result_obj)
660
+ logger.warning(f"[chat_orchestrator] 预校验失败,拒绝执行:\n{formatted}")
661
+ return (
662
+ ChatExecutionResult(
663
+ success=False,
664
+ reply=f"这批修改没有全部通过检查,项目未做任何改动:\n{formatted}",
665
+ actions=[],
666
+ error=formatted,
667
+ updated_history=updated_history,
668
+ validation_result=validation_result,
669
+ ),
670
+ (actions, validation_result),
671
+ )
672
+
673
+ return None, (validation_result_obj.valid_actions, validation_result)
674
+
675
+
676
+ # =============================================================================
677
+ # 便捷函数
678
+ # =============================================================================
679
+
680
+
681
+ async def execute_ai_chat_unified(
682
+ message: str,
683
+ project_path: str | None,
684
+ provider: AIProvider,
685
+ context_nodes: list[dict[str, Any]] | None = None,
686
+ history: list[dict[str, str]] | None = None,
687
+ agent_mode: bool = True,
688
+ canvas_nodes: list[dict[str, Any]] | None = None,
689
+ max_agent_iterations: int = 5,
690
+ ) -> ChatExecutionResult:
691
+ """
692
+ @methoddesc 便捷的统一 AI Chat 执行函数(用于 API 场景)
693
+
694
+ 参数:
695
+ max_agent_iterations: Agent 最大迭代轮数,默认 5(覆盖多步工作流:
696
+ read_canvas → read_project → apply_actions → validate)。
697
+
698
+ 参数:
699
+ message: 用户消息
700
+ project_path: 项目路径
701
+ provider: AIProvider 配置对象
702
+ context_nodes: 上下文节点(用户右键选中的少数节点)
703
+ history: 对话历史
704
+ agent_mode: 是否启用 Agent 深度模式
705
+ canvas_nodes: 画布全部业务节点快照(供 read_canvas 工具查询)
706
+
707
+ 返回:
708
+ ChatExecutionResult: 执行结果
709
+ """
710
+ orchestrator = AIChatOrchestrator(provider)
711
+ options = ChatOptions(
712
+ history=history or [],
713
+ enable_interactive=False, # API 模式禁用交互
714
+ return_frontend_instructions=True,
715
+ agent_mode=agent_mode,
716
+ max_agent_iterations=max_agent_iterations,
717
+ canvas_nodes=canvas_nodes or [],
718
+ )
719
+
720
+ return await orchestrator.execute_chat(
721
+ message=message,
722
+ project_path=project_path,
723
+ context_nodes=context_nodes or [],
724
+ options=options,
725
+ )