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,544 @@
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 异步配置生成任务 API 路由模块
17
+
18
+ 功能概述:
19
+ - 创建异步配置生成任务,避免大文件场景阻塞请求
20
+ - 查询任务执行状态和进度
21
+ - 支持取消正在运行的任务
22
+ - 项目本地持久化任务状态(支持后端重启后恢复)
23
+
24
+ 架构设计:
25
+ - 使用 asyncio.create_task 在后台执行生成任务
26
+ - 通过 AgentJobStorage 持久化任务状态和 checkpoint
27
+ - 通过内存字典 _job_tasks 保存运行中的 Task 句柄
28
+ - 支持进度回调,实时更新任务阶段和进度百分比
29
+ - 超过最大任务数时自动清理已完成任务
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import asyncio
35
+ import logging
36
+ import threading
37
+ import uuid
38
+ from collections.abc import AsyncIterator, Callable
39
+ from datetime import UTC, datetime
40
+ from typing import Any
41
+
42
+ from fastapi import Depends, Header, HTTPException
43
+ from fastapi.responses import StreamingResponse
44
+
45
+ from app.api.dependencies import get_project_config_path
46
+ from app.shared.services.ai.job_storage import AgentJobStorage
47
+ from app.shared.services.llm.generation import (
48
+ CancelledError,
49
+ ConfigGenerationService,
50
+ GenerationOptions,
51
+ GenerationParseError,
52
+ ProfilingOptions,
53
+ )
54
+
55
+ from .models import ConfigGenerateJobCreateResponse, ConfigGenerateJobStatus, ConfigGenerateRequest
56
+ from .router import router
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+ # 内存中的任务句柄(仅用于取消运行中的任务)
61
+ _job_tasks: dict[str, asyncio.Task] = {}
62
+ _job_tasks_lock = threading.Lock()
63
+
64
+ # 持久化 job 状态清理参数
65
+ _JOB_TTL_HOURS = 24.0
66
+
67
+
68
+ def _get_storage(config_path: str) -> AgentJobStorage:
69
+ """获取指定项目的持久化存储实例。"""
70
+ return AgentJobStorage(config_path)
71
+
72
+
73
+ def _serialize_result(result: dict[str, Any] | None) -> dict[str, Any] | None:
74
+ """将生成结果序列化为可 JSON 序列化的字典。"""
75
+ if result is None:
76
+ return None
77
+ # 只保留必要字段,避免循环引用或不可序列化对象
78
+ return {
79
+ "success": result.get("success", False),
80
+ "yaml_preview": result.get("yaml_preview", ""),
81
+ "manifest": result.get("manifest"),
82
+ "schemas": result.get("schemas", {}),
83
+ "constraints": result.get("constraints", {}),
84
+ "regex_nodes": result.get("regex_nodes", {}),
85
+ "warnings": result.get("warnings", []),
86
+ "error": result.get("error"),
87
+ }
88
+
89
+
90
+ def _now_iso() -> str:
91
+ """返回当前 UTC 时间 ISO 字符串。"""
92
+ return datetime.now(UTC).isoformat()
93
+
94
+
95
+ @router.post(
96
+ "/config/generate/jobs",
97
+ response_model=ConfigGenerateJobCreateResponse,
98
+ summary="创建异步配置生成任务",
99
+ responses={
100
+ 500: {"description": "服务器内部错误"},
101
+ },
102
+ )
103
+ async def create_generate_job(
104
+ payload: ConfigGenerateRequest,
105
+ config_path: str = Depends(get_project_config_path),
106
+ ) -> ConfigGenerateJobCreateResponse:
107
+ """
108
+ 创建异步配置生成任务
109
+
110
+ 适用于大文件或批量生成场景,避免阻塞请求
111
+ """
112
+ job_id = f"job_{uuid.uuid4().hex[:12]}"
113
+ now = _now_iso()
114
+
115
+ storage = _get_storage(config_path)
116
+ storage.cleanup_old_jobs(max_age_hours=_JOB_TTL_HOURS)
117
+
118
+ job_status = ConfigGenerateJobStatus(
119
+ job_id=job_id,
120
+ status="pending",
121
+ stage="initializing",
122
+ progress=0.0,
123
+ iterations=0,
124
+ max_iterations=payload.options.max_iterations,
125
+ created_at=now,
126
+ updated_at=now,
127
+ warnings=[],
128
+ )
129
+ storage.save_status(job_id, job_status.model_dump())
130
+
131
+ # 在后台运行任务(保存 Task 句柄以便取消)
132
+ task = asyncio.create_task(_run_job(job_id, payload, config_path))
133
+ with _job_tasks_lock:
134
+ _job_tasks[job_id] = task
135
+
136
+ return ConfigGenerateJobCreateResponse(job_id=job_id)
137
+
138
+
139
+ async def _run_job(
140
+ job_id: str,
141
+ payload: ConfigGenerateRequest,
142
+ config_path: str,
143
+ emit: Callable[[str, dict[str, Any]], None] | None = None,
144
+ initial_checkpoint: dict[str, Any] | None = None,
145
+ ) -> None:
146
+ """后台执行任务
147
+
148
+ 参数:
149
+ emit: 可选的 SSE 事件推送回调 (event_type, data_dict)。
150
+ 传入时,update_status/终态会同时推 SSE 事件;不传则只持久化(兼容现有轮询)。
151
+ """
152
+
153
+ def emit_status(event_type: str, **fields: Any) -> None:
154
+ """持久化状态变更的同时推 SSE 事件(若 emit 已注入)。"""
155
+ if emit is not None:
156
+ emit(event_type, {k: v for k, v in fields.items() if v is not None})
157
+
158
+ storage = _get_storage(config_path)
159
+
160
+ def update_status(
161
+ status: str,
162
+ stage: str | None = None,
163
+ progress: float | None = None,
164
+ message: str | None = None,
165
+ iterations: int | None = None,
166
+ metrics: dict[str, Any] | None = None,
167
+ current_plan: list[dict[str, Any]] | None = None,
168
+ warnings: list[str] | None = None,
169
+ error: str | None = None,
170
+ result: dict[str, Any] | None = None,
171
+ ) -> ConfigGenerateJobStatus:
172
+ """更新并持久化任务状态。"""
173
+ existing = storage.load_status(job_id) or {}
174
+ data = {
175
+ "job_id": job_id,
176
+ "status": status,
177
+ "stage": stage if stage is not None else existing.get("stage", "initializing"),
178
+ "message": message if message is not None else existing.get("message"),
179
+ "progress": progress if progress is not None else existing.get("progress"),
180
+ "iterations": iterations if iterations is not None else existing.get("iterations", 0),
181
+ "max_iterations": payload.options.max_iterations,
182
+ "metrics": metrics if metrics is not None else existing.get("metrics"),
183
+ "current_plan": current_plan if current_plan is not None else existing.get("current_plan"),
184
+ "created_at": existing.get("created_at", _now_iso()),
185
+ "updated_at": _now_iso(),
186
+ "warnings": warnings if warnings is not None else existing.get("warnings", []),
187
+ "error": error if error is not None else existing.get("error"),
188
+ }
189
+ if result is not None:
190
+ data["result"] = result
191
+ job_status = ConfigGenerateJobStatus(**data)
192
+ storage.save_status(job_id, job_status.model_dump())
193
+
194
+ # 同时推 SSE 事件(若 emit 已注入)
195
+ # 终态用对应事件类型,中间态用 progress 事件
196
+ if status == "completed":
197
+ emit_status("completed", reply="", result=data.get("result"), iterations=iterations)
198
+ elif status == "failed":
199
+ emit_status("error", message=error or "生成失败")
200
+ elif status == "cancelled":
201
+ emit_status("cancelled", completed_turns=iterations or 0, partial=True)
202
+ else:
203
+ # running 中间态
204
+ emit_status(
205
+ "progress",
206
+ stage=stage,
207
+ progress=progress,
208
+ message=message,
209
+ iterations=iterations,
210
+ metrics=metrics,
211
+ current_plan=current_plan,
212
+ )
213
+ return job_status
214
+
215
+ update_status("running", stage="initializing", progress=0.0)
216
+
217
+ # 存储 payload 元数据供 resume 重建
218
+ # B18 修复:保存全部 options(过去仅保存 max_iterations,resume 时其余 options 回退默认值)
219
+ raw = storage._load_raw(job_id)
220
+ raw["payload"] = {
221
+ "file_paths": payload.file_paths,
222
+ "project_name": payload.project_name,
223
+ "project_id": payload.project_id,
224
+ "provider_id": payload.provider_id,
225
+ "max_iterations": payload.options.max_iterations,
226
+ # 完整保存 options 字典,resume 时整体重建
227
+ "options": payload.options.model_dump(),
228
+ }
229
+ storage._save_raw(job_id, raw)
230
+
231
+ service = ConfigGenerationService(provider_id=payload.provider_id)
232
+
233
+ profiling_opts = ProfilingOptions(
234
+ sample_rows=payload.options.sample_rows,
235
+ sample_values_per_column=payload.options.sample_values_per_column,
236
+ max_files=payload.options.max_files,
237
+ max_cell_chars=payload.options.max_cell_chars,
238
+ )
239
+
240
+ gen_opts = GenerationOptions(
241
+ generate_schemas=payload.options.generate_schemas,
242
+ generate_constraints=payload.options.generate_constraints,
243
+ generate_regex_nodes=payload.options.generate_regex_nodes,
244
+ keep_existing=payload.options.keep_existing,
245
+ )
246
+
247
+ def progress_callback(stage: str, progress: float, extra: dict[str, Any] | None = None) -> None:
248
+ """
249
+ @methoddesc 同步进度回调,更新任务状态
250
+
251
+ 业务用途:
252
+ - 由 ConfigGenerationService 在生成过程中周期性调用
253
+ - 同时检查任务是否被取消,若是则调用 service.cancel() 触发中止
254
+
255
+ 参数:
256
+ stage: 当前阶段名称(如 "profiling" / "generating")
257
+ progress: 当前进度(0.0 - 1.0)
258
+ extra: 附加信息(如迭代轮数、metrics)
259
+ """
260
+ # 检查是否被取消
261
+ current_status = storage.load_status(job_id)
262
+ if current_status and current_status.get("status") == "cancelled":
263
+ service.cancel()
264
+
265
+ extra = extra or {}
266
+ update_status(
267
+ "running",
268
+ stage=stage,
269
+ progress=min(progress * 100.0, 100.0),
270
+ iterations=extra.get("iterations"),
271
+ metrics=extra.get("metrics"),
272
+ current_plan=extra.get("current_plan"),
273
+ )
274
+
275
+ def on_checkpoint(cp: dict[str, Any]) -> None:
276
+ """checkpoint 落盘回调,异常不中断任务。"""
277
+ try:
278
+ storage.save_checkpoint(job_id, cp)
279
+ except Exception:
280
+ logger.warning("checkpoint 落盘失败", exc_info=True)
281
+
282
+ try:
283
+ from .models import ConfigGenerateResponse
284
+
285
+ if payload.options.agent_mode:
286
+ result = await service.generate_with_agent(
287
+ file_paths=payload.file_paths,
288
+ project_name=payload.project_name,
289
+ project_id=payload.project_id,
290
+ config_path=config_path,
291
+ profiling_options=profiling_opts,
292
+ generation_options=gen_opts,
293
+ max_iterations=payload.options.max_iterations,
294
+ validation_sample_size=payload.options.validation_sample_size,
295
+ auto_chunking=payload.options.auto_chunking,
296
+ chunk_max_columns=payload.options.chunk_max_columns,
297
+ chunk_max_files=payload.options.chunk_max_files,
298
+ progress_callback=progress_callback,
299
+ checkpoint_callback=on_checkpoint,
300
+ initial_checkpoint=initial_checkpoint,
301
+ )
302
+ else:
303
+ result = await service.generate(
304
+ file_paths=payload.file_paths,
305
+ project_name=payload.project_name,
306
+ project_id=payload.project_id,
307
+ config_path=config_path,
308
+ profiling_options=profiling_opts,
309
+ generation_options=gen_opts,
310
+ progress_callback=progress_callback,
311
+ )
312
+
313
+ response_result = ConfigGenerateResponse(
314
+ success=result["success"],
315
+ yaml_preview=result["yaml_preview"],
316
+ manifest=result["manifest"],
317
+ schemas=result["schemas"],
318
+ constraints=result["constraints"],
319
+ regex_nodes=result["regex_nodes"],
320
+ warnings=result.get("warnings", []),
321
+ error=result.get("error"),
322
+ )
323
+
324
+ # B17 修复:按 result["success"] 区分 completed/failed,
325
+ # 过去无论成功失败都标记 completed,导致失败任务在前端显示为成功
326
+ final_status = "completed" if result.get("success") else "failed"
327
+ update_status(
328
+ final_status,
329
+ stage=final_status,
330
+ progress=100.0,
331
+ iterations=result.get("iterations"),
332
+ metrics=result.get("metrics"),
333
+ warnings=result.get("warnings", []),
334
+ result=response_result.model_dump(),
335
+ error=result.get("error") if not result.get("success") else None,
336
+ )
337
+
338
+ except CancelledError:
339
+ update_status(
340
+ "cancelled",
341
+ stage="cancelled",
342
+ progress=0.0,
343
+ error="任务已取消",
344
+ )
345
+ except GenerationParseError as e:
346
+ update_status(
347
+ "failed",
348
+ stage="error",
349
+ error=f"配置解析失败: {e}",
350
+ )
351
+ except Exception as e:
352
+ update_status(
353
+ "failed",
354
+ stage="error",
355
+ error=str(e),
356
+ )
357
+ finally:
358
+ with _job_tasks_lock:
359
+ _job_tasks.pop(job_id, None)
360
+
361
+
362
+ @router.get(
363
+ "/config/generate/jobs/{job_id}",
364
+ response_model=ConfigGenerateJobStatus,
365
+ summary="获取任务状态",
366
+ responses={
367
+ 404: {"description": "任务未找到"},
368
+ },
369
+ )
370
+ async def get_generate_job(job_id: str, config_path: str = Depends(get_project_config_path)) -> ConfigGenerateJobStatus:
371
+ """获取任务状态"""
372
+ storage = _get_storage(config_path)
373
+ status_data = storage.load_status(job_id)
374
+ if not status_data:
375
+ raise HTTPException(status_code=404, detail=f"未找到对应的任务,可能已完成或已过期(ID: {job_id})")
376
+ return ConfigGenerateJobStatus(**status_data)
377
+
378
+
379
+ @router.post(
380
+ "/config/generate/stream",
381
+ summary="AI 配置生成流式接口(SSE)",
382
+ )
383
+ async def generate_stream(
384
+ payload: ConfigGenerateRequest,
385
+ config_path: str = Depends(get_project_config_path),
386
+ last_event_id: int = Header(default=0, alias="Last-Event-ID"),
387
+ ) -> StreamingResponse:
388
+ """@methoddesc 配置生成流式端点
389
+
390
+ 创建 generate job task 并通过 SSE 实时推送进度与终态。
391
+ 复用 _run_job 逻辑,通过 emit 回调桥接到 EventJournal + 事件队列。
392
+ 支持断线重连续传(Last-Event-ID)。
393
+ """
394
+ from fastapi.responses import StreamingResponse
395
+
396
+ from app.shared.services.ai.streaming.event_journal import EventJournal
397
+ from app.shared.services.ai.streaming.sse_response import sse_event_stream
398
+
399
+ job_id = f"job_{uuid.uuid4().hex[:12]}"
400
+ now = _now_iso()
401
+
402
+ storage = _get_storage(config_path)
403
+ storage.cleanup_old_jobs(max_age_hours=_JOB_TTL_HOURS)
404
+ job_status = ConfigGenerateJobStatus(
405
+ job_id=job_id,
406
+ status="pending",
407
+ stage="initializing",
408
+ progress=0.0,
409
+ iterations=0,
410
+ max_iterations=payload.options.max_iterations,
411
+ created_at=now,
412
+ updated_at=now,
413
+ warnings=[],
414
+ )
415
+ storage.save_status(job_id, job_status.model_dump())
416
+
417
+ # 创建 EventJournal + 事件队列
418
+ journal_dir = _journal_dir_for(config_path)
419
+ journal = EventJournal(job_id=job_id, journal_dir=journal_dir)
420
+ event_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
421
+
422
+ # emit 回调:把状态变更追加到 journal + 推事件队列
423
+ def emit(event: str, data: dict[str, Any]) -> None:
424
+ eid = journal.append(event, data)
425
+ try:
426
+ event_queue.put_nowait({"id": eid, "event": event, "data": data})
427
+ except asyncio.QueueFull:
428
+ pass
429
+
430
+ # 后台执行 job task,注入 emit
431
+ task = asyncio.create_task(_run_job(job_id, payload, config_path, emit=emit))
432
+ with _job_tasks_lock:
433
+ _job_tasks[job_id] = task
434
+
435
+ async def _sse_generator() -> AsyncIterator[str]:
436
+ async for frame in sse_event_stream(
437
+ journal=journal,
438
+ last_event_id=last_event_id,
439
+ event_queue=event_queue,
440
+ ):
441
+ yield frame
442
+
443
+ return StreamingResponse(_sse_generator(), media_type="text/event-stream")
444
+
445
+
446
+ def _journal_dir_for(config_path: str) -> str:
447
+ """根据项目路径返回 journal 目录(复用 .precis/stream_jobs 约定)。"""
448
+ import os
449
+
450
+ if config_path:
451
+ return os.path.join(config_path, ".precis", "stream_jobs")
452
+ return os.path.join(os.path.expanduser("~"), ".precis", "stream_jobs")
453
+
454
+
455
+ @router.post(
456
+ "/config/generate/jobs/{job_id}/cancel",
457
+ response_model=ConfigGenerateJobStatus,
458
+ summary="取消任务",
459
+ responses={
460
+ 404: {"description": "任务未找到"},
461
+ },
462
+ )
463
+ async def cancel_generate_job(
464
+ job_id: str, config_path: str = Depends(get_project_config_path)
465
+ ) -> ConfigGenerateJobStatus:
466
+ """取消任务"""
467
+ storage = _get_storage(config_path)
468
+ status_data = storage.load_status(job_id)
469
+ if not status_data:
470
+ raise HTTPException(status_code=404, detail=f"未找到对应的任务,可能已完成或已过期(ID: {job_id})")
471
+
472
+ if status_data.get("status") in ("completed", "failed", "cancelled"):
473
+ return ConfigGenerateJobStatus(**status_data)
474
+
475
+ status_data["status"] = "cancelled"
476
+ status_data["stage"] = "cancelled"
477
+ status_data["updated_at"] = _now_iso()
478
+ storage.save_status(job_id, status_data)
479
+
480
+ # 真正取消运行中的任务
481
+ with _job_tasks_lock:
482
+ task = _job_tasks.get(job_id)
483
+ if task and not task.done():
484
+ task.cancel()
485
+
486
+ return ConfigGenerateJobStatus(**status_data)
487
+
488
+
489
+ @router.post(
490
+ "/config/generate/jobs/{job_id}/resume",
491
+ summary="从最近 checkpoint 续跑任务",
492
+ responses={
493
+ 404: {"description": "无可用 checkpoint"},
494
+ },
495
+ )
496
+ async def resume_job(
497
+ job_id: str,
498
+ config_path: str = Depends(get_project_config_path),
499
+ ) -> dict[str, Any]:
500
+ """从最近 checkpoint 续跑任务
501
+
502
+ 加载最新的 checkpoint 并在后台重启 executor.run(initial_checkpoint=cp)。
503
+ """
504
+ storage = _get_storage(config_path)
505
+ cp = storage.load_latest_checkpoint(job_id)
506
+ if not cp:
507
+ raise HTTPException(status_code=404, detail="无可用 checkpoint")
508
+
509
+ status_data = storage.load_status(job_id)
510
+ if not status_data:
511
+ raise HTTPException(status_code=404, detail=f"未找到对应的任务,可能已完成或已过期(ID: {job_id})")
512
+
513
+ # 从持久化的 payload 元数据重建请求
514
+ raw = storage.load_full(job_id)
515
+ meta = raw.get("payload", {})
516
+ from .models import ConfigGenerateOptions
517
+
518
+ # B18 修复:优先使用完整保存的 options 字典重建,回退到仅 max_iterations(兼容旧数据)
519
+ saved_options = meta.get("options")
520
+ if isinstance(saved_options, dict):
521
+ options = ConfigGenerateOptions(**saved_options)
522
+ else:
523
+ options = ConfigGenerateOptions(max_iterations=meta.get("max_iterations", 2))
524
+
525
+ payload = ConfigGenerateRequest(
526
+ file_paths=meta.get("file_paths", []),
527
+ project_name=meta.get("project_name", ""),
528
+ project_id=meta.get("project_id", ""),
529
+ provider_id=meta.get("provider_id"),
530
+ options=options,
531
+ )
532
+
533
+ # 更新状态为 resuming
534
+ status_data["status"] = "running"
535
+ status_data["stage"] = "resuming"
536
+ status_data["updated_at"] = _now_iso()
537
+ storage.save_status(job_id, status_data)
538
+
539
+ # 后台重启任务
540
+ task = asyncio.create_task(_run_job(job_id, payload, config_path, initial_checkpoint=cp))
541
+ with _job_tasks_lock:
542
+ _job_tasks[job_id] = task
543
+
544
+ return {"status": "resuming", "turn": cp.get("turn", 0)}