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,471 @@
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
+ - 创建异步配置迁移任务(从旧脚本迁移生成 Precis 配置)
20
+ - 复用 Agent 内核和任务持久化
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import logging
27
+ import threading
28
+ import uuid
29
+ from collections.abc import AsyncIterator, Callable
30
+ from datetime import UTC, datetime
31
+ from typing import Any
32
+
33
+ from fastapi import Depends, Header, HTTPException
34
+ from fastapi.responses import StreamingResponse
35
+
36
+ from app.api.dependencies import get_project_config_path
37
+ from app.shared.services.ai.job_storage import AgentJobStorage
38
+ from app.shared.services.ai.migrate_service import ConfigMigrationService
39
+ from app.shared.services.llm.generation import (
40
+ CancelledError,
41
+ GenerationOptions,
42
+ GenerationParseError,
43
+ ProfilingOptions,
44
+ )
45
+
46
+ from .models import (
47
+ ConfigGenerateJobCreateResponse,
48
+ ConfigGenerateJobStatus,
49
+ ConfigGenerateResponse,
50
+ ConfigMigrateRequest,
51
+ )
52
+ from .router import router
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+ _job_tasks: dict[str, asyncio.Task] = {}
57
+ _job_tasks_lock = threading.Lock()
58
+ _JOB_TTL_HOURS = 24.0
59
+
60
+
61
+ def _get_storage(config_path: str) -> AgentJobStorage:
62
+ """获取指定项目的持久化存储实例。"""
63
+ return AgentJobStorage(config_path)
64
+
65
+
66
+ def _now_iso() -> str:
67
+ """返回当前 UTC 时间 ISO 字符串。"""
68
+ return datetime.now(UTC).isoformat()
69
+
70
+
71
+ @router.post(
72
+ "/config/migrate/jobs",
73
+ response_model=ConfigGenerateJobCreateResponse,
74
+ summary="创建异步配置迁移任务",
75
+ )
76
+ async def create_migrate_job(
77
+ payload: ConfigMigrateRequest,
78
+ config_path: str = Depends(get_project_config_path),
79
+ ) -> ConfigGenerateJobCreateResponse:
80
+ """创建异步配置迁移任务"""
81
+ job_id = f"job_migrate_{uuid.uuid4().hex[:12]}"
82
+ now = _now_iso()
83
+
84
+ storage = _get_storage(config_path)
85
+ storage.cleanup_old_jobs(max_age_hours=_JOB_TTL_HOURS)
86
+
87
+ job_status = ConfigGenerateJobStatus(
88
+ job_id=job_id,
89
+ status="pending",
90
+ stage="initializing",
91
+ progress=0.0,
92
+ iterations=0,
93
+ max_iterations=payload.options.max_iterations,
94
+ created_at=now,
95
+ updated_at=now,
96
+ warnings=[],
97
+ )
98
+ storage.save_status(job_id, job_status.model_dump())
99
+
100
+ task = asyncio.create_task(_run_migrate_job(job_id, payload, config_path))
101
+ with _job_tasks_lock:
102
+ _job_tasks[job_id] = task
103
+
104
+ return ConfigGenerateJobCreateResponse(job_id=job_id)
105
+
106
+
107
+ async def _run_migrate_job(
108
+ job_id: str,
109
+ payload: ConfigMigrateRequest,
110
+ config_path: str,
111
+ emit: Callable[[str, dict[str, Any]], None] | None = None,
112
+ initial_checkpoint: dict[str, Any] | None = None,
113
+ ) -> None:
114
+ """后台执行迁移任务
115
+
116
+ 参数:
117
+ emit: 可选的 SSE 事件推送回调 (event_type, data_dict)。
118
+ """
119
+
120
+ def emit_status(event_type: str, **fields: Any) -> None:
121
+ """持久化状态变更的同时推 SSE 事件(若 emit 已注入)。"""
122
+ if emit is not None:
123
+ emit(event_type, {k: v for k, v in fields.items() if v is not None})
124
+
125
+ storage = _get_storage(config_path)
126
+
127
+ def update_status(
128
+ status: str,
129
+ stage: str | None = None,
130
+ progress: float | None = None,
131
+ message: str | None = None,
132
+ iterations: int | None = None,
133
+ metrics: dict[str, Any] | None = None,
134
+ warnings: list[str] | None = None,
135
+ error: str | None = None,
136
+ result: dict[str, Any] | None = None,
137
+ ) -> ConfigGenerateJobStatus:
138
+ existing = storage.load_status(job_id) or {}
139
+ data = {
140
+ "job_id": job_id,
141
+ "status": status,
142
+ "stage": stage if stage is not None else existing.get("stage", "initializing"),
143
+ "message": message if message is not None else existing.get("message"),
144
+ "progress": progress if progress is not None else existing.get("progress"),
145
+ "iterations": iterations if iterations is not None else existing.get("iterations", 0),
146
+ "max_iterations": payload.options.max_iterations,
147
+ "metrics": metrics if metrics is not None else existing.get("metrics"),
148
+ "created_at": existing.get("created_at", _now_iso()),
149
+ "updated_at": _now_iso(),
150
+ "warnings": warnings if warnings is not None else existing.get("warnings", []),
151
+ "error": error if error is not None else existing.get("error"),
152
+ }
153
+ if result is not None:
154
+ data["result"] = result
155
+ job_status = ConfigGenerateJobStatus(**data)
156
+ storage.save_status(job_id, job_status.model_dump())
157
+
158
+ # 同时推 SSE 事件(若 emit 已注入)
159
+ if status == "completed":
160
+ emit_status("completed", reply="", result=data.get("result"), iterations=iterations)
161
+ elif status == "failed":
162
+ emit_status("error", message=error or "迁移失败")
163
+ elif status == "cancelled":
164
+ emit_status("cancelled", completed_turns=iterations or 0, partial=True)
165
+ else:
166
+ emit_status(
167
+ "progress",
168
+ stage=stage,
169
+ progress=progress,
170
+ message=message,
171
+ iterations=iterations,
172
+ metrics=metrics,
173
+ )
174
+ return job_status
175
+
176
+ update_status("running", stage="initializing", progress=0.0)
177
+
178
+ # 存储 payload 元数据供 resume 重建
179
+ # B18 对齐修复:完整保存 options 字典(与 jobs.py 一致)。过去仅保存 max_iterations,
180
+ # resume 时其余 options(agent_mode/keep_existing/分块参数等)全部回退默认值。
181
+ raw = storage._load_raw(job_id)
182
+ raw["payload"] = {
183
+ "script_content": payload.script_content,
184
+ "language": payload.language,
185
+ "file_paths": payload.file_paths,
186
+ "project_name": payload.project_name,
187
+ "project_id": payload.project_id,
188
+ "provider_id": payload.provider_id,
189
+ "max_iterations": payload.options.max_iterations,
190
+ # 完整保存 options 字典,resume 时整体重建
191
+ "options": payload.options.model_dump(),
192
+ }
193
+ storage._save_raw(job_id, raw)
194
+
195
+ service = ConfigMigrationService(provider_id=payload.provider_id)
196
+
197
+ profiling_opts = ProfilingOptions(
198
+ sample_rows=payload.options.sample_rows,
199
+ sample_values_per_column=payload.options.sample_values_per_column,
200
+ max_files=payload.options.max_files,
201
+ max_cell_chars=payload.options.max_cell_chars,
202
+ )
203
+
204
+ gen_opts = GenerationOptions(
205
+ generate_schemas=payload.options.generate_schemas,
206
+ generate_constraints=payload.options.generate_constraints,
207
+ generate_regex_nodes=payload.options.generate_regex_nodes,
208
+ keep_existing=payload.options.keep_existing,
209
+ )
210
+
211
+ def progress_callback(stage: str, progress: float, extra: dict[str, Any] | None = None) -> None:
212
+ current_status = storage.load_status(job_id)
213
+ if current_status and current_status.get("status") == "cancelled":
214
+ service.cancel()
215
+ return
216
+ extra = extra or {}
217
+ update_status(
218
+ "running",
219
+ stage=stage,
220
+ progress=min(progress * 100.0, 100.0),
221
+ iterations=extra.get("iterations"),
222
+ metrics=extra.get("metrics"),
223
+ )
224
+
225
+ def on_checkpoint(cp: dict[str, Any]) -> None:
226
+ try:
227
+ storage.save_checkpoint(job_id, cp)
228
+ except Exception:
229
+ logger.warning("checkpoint 落盘失败", exc_info=True)
230
+
231
+ try:
232
+ result = await service.migrate_from_script(
233
+ script_content=payload.script_content,
234
+ language=payload.language,
235
+ file_paths=payload.file_paths,
236
+ project_name=payload.project_name,
237
+ project_id=payload.project_id,
238
+ config_path=config_path,
239
+ profiling_options=profiling_opts,
240
+ generation_options=gen_opts,
241
+ max_iterations=payload.options.max_iterations,
242
+ validation_sample_size=payload.options.validation_sample_size,
243
+ progress_callback=progress_callback,
244
+ checkpoint_callback=on_checkpoint,
245
+ initial_checkpoint=initial_checkpoint,
246
+ sources=[s.model_dump() for s in payload.sources] if payload.sources else None,
247
+ )
248
+
249
+ response_result = ConfigGenerateResponse(
250
+ success=result["success"],
251
+ yaml_preview=result.get("yaml_preview", ""),
252
+ manifest=result.get("manifest"),
253
+ schemas=result.get("schemas"),
254
+ constraints=result.get("constraints"),
255
+ regex_nodes=result.get("regex_nodes"),
256
+ warnings=result.get("warnings", []),
257
+ error=result.get("error"),
258
+ iterations=result.get("iterations"),
259
+ metrics=result.get("metrics"),
260
+ )
261
+
262
+ if result.get("success"):
263
+ update_status(
264
+ "completed",
265
+ stage="completed",
266
+ progress=100.0,
267
+ iterations=result.get("iterations"),
268
+ metrics=result.get("metrics"),
269
+ warnings=result.get("warnings", []),
270
+ result=response_result.model_dump(),
271
+ )
272
+ else:
273
+ update_status(
274
+ "failed",
275
+ stage="error",
276
+ progress=100.0,
277
+ error=result.get("error") or "迁移失败",
278
+ iterations=result.get("iterations"),
279
+ metrics=result.get("metrics"),
280
+ warnings=result.get("warnings", []),
281
+ result=response_result.model_dump(),
282
+ )
283
+
284
+ except CancelledError:
285
+ update_status("cancelled", stage="cancelled", error="任务已取消")
286
+ except GenerationParseError as e:
287
+ update_status("failed", stage="error", error=f"配置解析失败: {e}")
288
+ except Exception as e:
289
+ update_status("failed", stage="error", error=str(e))
290
+ finally:
291
+ with _job_tasks_lock:
292
+ _job_tasks.pop(job_id, None)
293
+
294
+
295
+ @router.get(
296
+ "/config/migrate/jobs/{job_id}",
297
+ response_model=ConfigGenerateJobStatus,
298
+ summary="获取迁移任务状态",
299
+ responses={
300
+ 404: {"description": "任务未找到"},
301
+ },
302
+ )
303
+ async def get_migrate_job(job_id: str, config_path: str = Depends(get_project_config_path)) -> ConfigGenerateJobStatus:
304
+ """获取迁移任务状态"""
305
+ storage = _get_storage(config_path)
306
+ status_data = storage.load_status(job_id)
307
+ if not status_data:
308
+ raise HTTPException(status_code=404, detail=f"未找到对应的任务,可能已完成或已过期(ID: {job_id})")
309
+ return ConfigGenerateJobStatus(**status_data)
310
+
311
+
312
+ @router.post(
313
+ "/config/migrate/stream",
314
+ summary="AI 配置迁移流式接口(SSE)",
315
+ )
316
+ async def migrate_stream(
317
+ payload: ConfigMigrateRequest,
318
+ config_path: str = Depends(get_project_config_path),
319
+ last_event_id: int = Header(default=0, alias="Last-Event-ID"),
320
+ ) -> StreamingResponse:
321
+ """@methoddesc 脚本迁移流式端点
322
+
323
+ 创建 migrate job task 并通过 SSE 实时推送进度与终态。
324
+ 复用 _run_migrate_job 逻辑,通过 emit 回调桥接到 EventJournal + 事件队列。
325
+ """
326
+ from fastapi.responses import StreamingResponse
327
+
328
+ from app.shared.services.ai.streaming.event_journal import EventJournal
329
+ from app.shared.services.ai.streaming.sse_response import sse_event_stream
330
+
331
+ job_id = f"job_migrate_{uuid.uuid4().hex[:12]}"
332
+ now = _now_iso()
333
+
334
+ storage = _get_storage(config_path)
335
+ storage.cleanup_old_jobs(max_age_hours=_JOB_TTL_HOURS)
336
+ job_status = ConfigGenerateJobStatus(
337
+ job_id=job_id,
338
+ status="pending",
339
+ stage="initializing",
340
+ progress=0.0,
341
+ iterations=0,
342
+ max_iterations=payload.options.max_iterations,
343
+ created_at=now,
344
+ updated_at=now,
345
+ warnings=[],
346
+ )
347
+ storage.save_status(job_id, job_status.model_dump())
348
+
349
+ journal_dir = _journal_dir_for(config_path)
350
+ journal = EventJournal(job_id=job_id, journal_dir=journal_dir)
351
+ event_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
352
+
353
+ def emit(event: str, data: dict[str, Any]) -> None:
354
+ eid = journal.append(event, data)
355
+ try:
356
+ event_queue.put_nowait({"id": eid, "event": event, "data": data})
357
+ except asyncio.QueueFull:
358
+ pass
359
+
360
+ task = asyncio.create_task(_run_migrate_job(job_id, payload, config_path, emit=emit))
361
+ with _job_tasks_lock:
362
+ _job_tasks[job_id] = task
363
+
364
+ async def _sse_generator() -> AsyncIterator[str]:
365
+ async for frame in sse_event_stream(
366
+ journal=journal,
367
+ last_event_id=last_event_id,
368
+ event_queue=event_queue,
369
+ ):
370
+ yield frame
371
+
372
+ return StreamingResponse(_sse_generator(), media_type="text/event-stream")
373
+
374
+
375
+ def _journal_dir_for(config_path: str) -> str:
376
+ """根据项目路径返回 journal 目录(复用 .precis/stream_jobs 约定)。"""
377
+ import os
378
+
379
+ if config_path:
380
+ return os.path.join(config_path, ".precis", "stream_jobs")
381
+ return os.path.join(os.path.expanduser("~"), ".precis", "stream_jobs")
382
+
383
+
384
+ @router.post(
385
+ "/config/migrate/jobs/{job_id}/cancel",
386
+ response_model=ConfigGenerateJobStatus,
387
+ summary="取消迁移任务",
388
+ responses={
389
+ 404: {"description": "任务未找到"},
390
+ },
391
+ )
392
+ async def cancel_migrate_job(
393
+ job_id: str, config_path: str = Depends(get_project_config_path)
394
+ ) -> ConfigGenerateJobStatus:
395
+ """取消迁移任务"""
396
+ storage = _get_storage(config_path)
397
+ status_data = storage.load_status(job_id)
398
+ if not status_data:
399
+ raise HTTPException(status_code=404, detail=f"未找到对应的任务,可能已完成或已过期(ID: {job_id})")
400
+
401
+ if status_data.get("status") in ("completed", "failed", "cancelled"):
402
+ return ConfigGenerateJobStatus(**status_data)
403
+
404
+ status_data["status"] = "cancelled"
405
+ status_data["stage"] = "cancelled"
406
+ status_data["updated_at"] = _now_iso()
407
+ storage.save_status(job_id, status_data)
408
+
409
+ # 真正取消运行中的任务
410
+ with _job_tasks_lock:
411
+ task = _job_tasks.get(job_id)
412
+ if task and not task.done():
413
+ task.cancel()
414
+
415
+ return ConfigGenerateJobStatus(**status_data)
416
+
417
+
418
+ @router.post(
419
+ "/config/migrate/jobs/{job_id}/resume",
420
+ summary="从最近 checkpoint 续跑迁移任务",
421
+ responses={
422
+ 404: {"description": "无可用 checkpoint"},
423
+ },
424
+ )
425
+ async def resume_migrate_job(
426
+ job_id: str,
427
+ config_path: str = Depends(get_project_config_path),
428
+ ) -> dict[str, Any]:
429
+ """从最近 checkpoint 续跑迁移任务"""
430
+ storage = _get_storage(config_path)
431
+ cp = storage.load_latest_checkpoint(job_id)
432
+ if not cp:
433
+ raise HTTPException(status_code=404, detail="无可用 checkpoint")
434
+
435
+ status_data = storage.load_status(job_id)
436
+ if not status_data:
437
+ raise HTTPException(status_code=404, detail=f"未找到对应的任务,可能已完成或已过期(ID: {job_id})")
438
+
439
+ # 从持久化的 payload 元数据重建请求
440
+ raw = storage.load_full(job_id)
441
+ meta = raw.get("payload", {})
442
+ from .models import ConfigGenerateOptions
443
+
444
+ # B18 对齐修复:优先使用完整保存的 options 字典重建(与 jobs.py 一致),
445
+ # 回退到仅 max_iterations(兼容修复前落盘的旧任务数据)
446
+ saved_options = meta.get("options")
447
+ if isinstance(saved_options, dict):
448
+ options = ConfigGenerateOptions(**saved_options)
449
+ else:
450
+ options = ConfigGenerateOptions(max_iterations=meta.get("max_iterations", 2))
451
+
452
+ payload = ConfigMigrateRequest(
453
+ script_content=meta.get("script_content", ""),
454
+ language=meta.get("language", "python"),
455
+ file_paths=meta.get("file_paths", []),
456
+ project_name=meta.get("project_name", ""),
457
+ project_id=meta.get("project_id", ""),
458
+ provider_id=meta.get("provider_id"),
459
+ options=options,
460
+ )
461
+
462
+ status_data["status"] = "running"
463
+ status_data["stage"] = "resuming"
464
+ status_data["updated_at"] = _now_iso()
465
+ storage.save_status(job_id, status_data)
466
+
467
+ task = asyncio.create_task(_run_migrate_job(job_id, payload, config_path, initial_checkpoint=cp))
468
+ with _job_tasks_lock:
469
+ _job_tasks[job_id] = task
470
+
471
+ return {"status": "resuming", "turn": cp.get("turn", 0)}