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,322 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview AI 服务通用工具函数
18
+
19
+ 功能概述:
20
+ - 估算文本 Token 数量(支持中英文混合)
21
+ - 按 Token 上限截断聊天历史记录
22
+ - 扫描项目目录生成项目概览(Schema / Constraint 列表)
23
+
24
+ 架构设计:
25
+ - 纯函数设计,无副作用,便于单元测试
26
+ - Token 估算采用字符分类加权策略:中文单字 + 英文单词 + 数字 + 标点
27
+ - 项目概览扫描递归读取 schemas/ 和 constraints/ 目录下的 YAML 文件
28
+
29
+ 输入示例:
30
+ estimate_tokens("Hello 世界")
31
+ truncate_history_by_tokens(history, "系统提示", max_tokens=120000)
32
+ get_project_overview("/path/to/project")
33
+
34
+ 输出示例:
35
+ 7 # Token 估算值
36
+ [{"role": "user", "content": "..."}] # 截断后的历史
37
+ {"schemas": [...], "constraints": [...]} # 项目概览
38
+ """
39
+
40
+ import logging
41
+ import re
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ import yaml
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ def estimate_tokens(text: str) -> int:
51
+ """
52
+ @methoddesc 估算文本的 Token 数量
53
+
54
+ 采用简单的字符分类加权策略,适合快速估算:
55
+ - 中文字符:每个字算 1 个 Token
56
+ - 英文单词:每个单词算 1 个 Token
57
+ - 数字串:每组连续数字算 1 个 Token
58
+ - 标点符号:每个算 1 个 Token
59
+ 最后加上 10 个 Token 的固定开销(用于角色标记等)。
60
+
61
+ 参数:
62
+ text: 要估算的文本字符串
63
+
64
+ 返回:
65
+ 估算的 Token 数量(整数)
66
+ """
67
+ if not text:
68
+ return 0
69
+
70
+ # 按字符类型分别统计数量
71
+ chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", text)) # 中文字符
72
+ english_words = len(re.findall(r"[a-zA-Z]+", text)) # 英文单词
73
+ numbers = len(re.findall(r"\d+", text)) # 数字串
74
+ punctuations = len(re.findall(r"[^\w\s]", text)) # 标点符号
75
+
76
+ total = chinese_chars + english_words + numbers + punctuations
77
+ # 加上固定开销(系统提示、消息格式等消耗的额外 Token)
78
+ return total + 10
79
+
80
+
81
+ def truncate_history_by_tokens(
82
+ chat_history: list[dict[str, str]],
83
+ system_prompt: str,
84
+ max_tokens: int = 120000,
85
+ ) -> list[dict[str, str]]:
86
+ """
87
+ @methoddesc 按 Token 上限截断聊天历史记录
88
+
89
+ 从最近的消息开始往前累加,直到接近 max_tokens 上限为止。
90
+ 至少保留最近 2 条消息(确保对话连贯性)。
91
+
92
+ 参数:
93
+ chat_history: 历史消息列表,每条消息格式为 {"role": "user", "content": "..."}
94
+ system_prompt: 系统提示词文本(用于计算初始 Token 占用)
95
+ max_tokens: 允许的最大 Token 数量,默认 120000
96
+
97
+ 返回:
98
+ 截断后的历史消息列表
99
+ """
100
+ if not chat_history:
101
+ return []
102
+
103
+ # 计算系统提示词占用的 Token 数
104
+ system_tokens = estimate_tokens(system_prompt)
105
+
106
+ # 为每条历史消息估算 Token 数
107
+ history_with_tokens = []
108
+ for msg in chat_history:
109
+ tokens = estimate_tokens(msg.get("content", ""))
110
+ history_with_tokens.append((msg, tokens))
111
+
112
+ total_tokens = system_tokens
113
+ keep_count = 0
114
+
115
+ # 从最新的消息往前累加,直到超出上限
116
+ for msg, tokens in reversed(history_with_tokens):
117
+ if total_tokens + tokens > max_tokens:
118
+ break
119
+ total_tokens += tokens
120
+ keep_count += 1
121
+
122
+ # 至少保留最近 2 条消息(一问一答),避免截断后完全失去上下文
123
+ keep_count = max(keep_count, min(2, len(chat_history)))
124
+
125
+ truncated = chat_history[-keep_count:] if keep_count < len(chat_history) else chat_history
126
+
127
+ if len(truncated) < len(chat_history):
128
+ removed = len(chat_history) - len(truncated)
129
+ logger.info(
130
+ f"[truncate_history] 截断 {removed} 条历史,保留 {len(truncated)} 条,"
131
+ f"估算 Token: {total_tokens}/{max_tokens}"
132
+ )
133
+
134
+ return truncated
135
+
136
+
137
+ def get_project_overview(project_path: str) -> dict[str, Any]:
138
+ """
139
+ @methoddesc 扫描项目目录,生成项目概览信息
140
+
141
+ 读取项目下的 schemas/ 和 constraints/ 目录中的 YAML 文件,
142
+ 汇总所有表结构、字段信息以及约束规则,供 AI 上下文使用。
143
+
144
+ 本函数以 manifest 为权威来源:磁盘上存在但未登记到 manifest 的孤儿文件
145
+ 会标注 unlisted=True,避免 AI 视角与用户资源树视角产生偏差(前者纯 glob,
146
+ 后者走 manifest)。
147
+
148
+ 参数:
149
+ project_path: 项目根目录路径
150
+
151
+ 返回:
152
+ 项目概览字典,包含 schemas 和 constraints 两个列表
153
+ """
154
+ overview: dict[str, Any] = {"schemas": [], "constraints": [], "transforms": [], "regex_nodes": [], "settings": {}}
155
+
156
+ if not project_path:
157
+ return overview
158
+
159
+ project_root = Path(project_path)
160
+
161
+ # 读取 manifest 的 schemas 白名单,用于标注孤儿文件
162
+ listed_schema_paths: set[str] = set()
163
+ manifest_path = project_root / "project.precis.yaml"
164
+ if manifest_path.exists():
165
+ try:
166
+ with open(manifest_path, encoding="utf-8") as f:
167
+ manifest_data = yaml.safe_load(f) or {}
168
+ for ref in manifest_data.get("schemas", []) or []:
169
+ p = ref.get("path") if isinstance(ref, dict) else None
170
+ if isinstance(p, str) and p:
171
+ listed_schema_paths.add(p.replace("\\", "/").lower())
172
+ except Exception as e:
173
+ logger.debug(f"读取 manifest schemas 失败 {manifest_path}: {e}")
174
+
175
+ schemas_dir = project_root / "schemas"
176
+ if schemas_dir.exists():
177
+ for schema_file in schemas_dir.glob("*.yaml"):
178
+ try:
179
+ with open(schema_file, encoding="utf-8") as f:
180
+ schema_data = yaml.safe_load(f) or {}
181
+
182
+ table_name = schema_data.get("name", "")
183
+ table_id = schema_data.get("id", table_name)
184
+ columns = schema_data.get("columns", [])
185
+ inline_constraints = schema_data.get("constraints", [])
186
+
187
+ column_list = []
188
+ column_map = {}
189
+ for c in columns:
190
+ col_name = c.get("name", "")
191
+ col_id = c.get("id", col_name)
192
+ col_type = c.get("type", "")
193
+ column_list.append({"id": col_id, "name": col_name, "type": col_type})
194
+ column_map[col_id] = col_name
195
+
196
+ if table_name or table_id:
197
+ # 标注该 schema 是否登记在 manifest 中,孤儿文件 unlisted=True
198
+ rel_path = f"schemas/{schema_file.name}".replace("\\", "/").lower()
199
+ overview["schemas"].append(
200
+ {
201
+ "id": table_id,
202
+ "name": table_name,
203
+ "columns": column_list,
204
+ "unlisted": rel_path not in listed_schema_paths,
205
+ "path": f"schemas/{schema_file.name}",
206
+ }
207
+ )
208
+
209
+ for ic in inline_constraints:
210
+ col_id = ic.get("column", "")
211
+ col_name = column_map.get(col_id, col_id)
212
+ constraint_type = ic.get("type", "")
213
+ params = ic.get("params", {})
214
+
215
+ overview["constraints"].append(
216
+ {
217
+ "id": ic.get("id", ""),
218
+ "type": constraint_type,
219
+ "description": f"{table_name}.{col_name} - {constraint_type} 约束 (内联)",
220
+ # §2.13: 读取磁盘真实 enabled(无字段默认 true,与运行时语义一致)——
221
+ # 原硬编码 True 使 AI 对已禁用约束误判"在生效"
222
+ "enabled": ic.get("enabled", True),
223
+ "table_id": table_id,
224
+ "table_name": table_name,
225
+ "column_id": col_id,
226
+ "column_name": col_name,
227
+ "params": params,
228
+ "is_inline": True,
229
+ }
230
+ )
231
+ except Exception as e:
232
+ logger.debug(f"读取 schema 文件失败 {schema_file}: {e}")
233
+
234
+ constraints_dir = project_root / "constraints"
235
+ if constraints_dir.exists():
236
+ for constraint_file in constraints_dir.glob("*.constraint.yaml"):
237
+ try:
238
+ with open(constraint_file, encoding="utf-8") as f:
239
+ constraint_data = yaml.safe_load(f) or {}
240
+
241
+ constraint_id = constraint_data.get("id", "")
242
+ constraint_type = constraint_data.get("type", "")
243
+ description = constraint_data.get("description", "")
244
+ enabled = constraint_data.get("enabled", True)
245
+ refs = constraint_data.get("refs", {})
246
+ params = constraint_data.get("params", {})
247
+
248
+ table_id = refs.get("table_id", "")
249
+ column_id = refs.get("column_id", "")
250
+ column_ids = refs.get("column_ids", [])
251
+
252
+ overview["constraints"].append(
253
+ {
254
+ "id": constraint_id,
255
+ "type": constraint_type,
256
+ "description": description,
257
+ "enabled": enabled,
258
+ "table_id": table_id,
259
+ "column_id": column_id,
260
+ "column_ids": column_ids,
261
+ "params": params,
262
+ "is_inline": False,
263
+ }
264
+ )
265
+ except Exception as e:
266
+ logger.warning(f"读取 constraint 文件失败 {constraint_file}: {e}")
267
+
268
+ # 扫描 Regex 节点
269
+ for dirname in ("regex_nodes", "regex"):
270
+ regex_dir = project_root / dirname
271
+ if regex_dir.exists():
272
+ for regex_file in regex_dir.glob("*.yaml"):
273
+ try:
274
+ with open(regex_file, encoding="utf-8") as f:
275
+ regex_data = yaml.safe_load(f) or {}
276
+
277
+ overview["regex_nodes"].append(
278
+ {
279
+ "id": regex_data.get("id", ""),
280
+ "name": regex_data.get("name", ""),
281
+ "pattern": regex_data.get("pattern", ""),
282
+ "match_mode": regex_data.get("match_mode", "full"),
283
+ "enabled": regex_data.get("enabled", True),
284
+ "source_ref": regex_data.get("source_ref"),
285
+ }
286
+ )
287
+ except Exception as e:
288
+ logger.debug(f"读取 regex 文件失败 {regex_file}: {e}")
289
+
290
+ # 扫描 Transform 节点
291
+ transforms_dir = project_root / "transforms"
292
+ if transforms_dir.exists():
293
+ for transform_file in transforms_dir.glob("*.yaml"):
294
+ try:
295
+ with open(transform_file, encoding="utf-8") as f:
296
+ transform_data = yaml.safe_load(f) or {}
297
+
298
+ overview["transforms"].append(
299
+ {
300
+ "id": transform_data.get("id", ""),
301
+ "type": transform_data.get("type", ""),
302
+ "enabled": transform_data.get("enabled", True),
303
+ "input_from_node": transform_data.get("input_from_node"),
304
+ "input_column": transform_data.get("input_column"),
305
+ "output_columns": transform_data.get("output_columns", []),
306
+ }
307
+ )
308
+ except Exception as e:
309
+ logger.debug(f"读取 transform 文件失败 {transform_file}: {e}")
310
+
311
+ # 读取项目设置
312
+ manifest_path = project_root / "project.precis.yaml"
313
+ if manifest_path.exists():
314
+ try:
315
+ with open(manifest_path, encoding="utf-8") as f:
316
+ manifest_data = yaml.safe_load(f) or {}
317
+
318
+ overview["settings"] = manifest_data.get("settings", {})
319
+ except Exception as e:
320
+ logger.debug(f"读取 manifest 设置失败 {manifest_path}: {e}")
321
+
322
+ return overview
@@ -0,0 +1,320 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview 配置差异比对模块
18
+
19
+ 功能概述:
20
+ - 比较两份项目配置(manifest/schemas/constraints/regex_nodes)的差异
21
+ - 支持新增、修改、删除三种差异类型的识别
22
+ - 为 AI 配置生成后的合并确认提供数据支持
23
+
24
+ 架构设计:
25
+ - 服务类模式: ConfigDiffService 提供类方法 compare 作为统一入口
26
+ - 递归比对: _diff_recursive 处理 dict/list 嵌套结构的深层差异
27
+ - 资源级比对: _compare_resources 处理 schemas/constraints/regex_nodes 的增删改
28
+ - 属性级比对: _build_property_diff 生成 PropertyDiff 变更列表
29
+
30
+ 输入示例:
31
+ old = {"manifest": {...}, "schemas": {"users": old_schema}}
32
+ new = {"manifest": {...}, "schemas": {"users": new_schema}}
33
+ result = ConfigDiffService.compare(old, new)
34
+
35
+ 输出示例:
36
+ ConfigDiffResult(
37
+ manifest=[{"path": ["project", "name"], "old_value": "A", "new_value": "B", "diff_type": "modified"}],
38
+ schemas=[ConfigItemDiff(id="users", type=DiffType.MODIFIED, changes=[...])],
39
+ constraints=[],
40
+ regex_nodes=[]
41
+ )
42
+ """
43
+
44
+ import logging
45
+ from enum import Enum
46
+ from typing import Any
47
+
48
+ from pydantic import BaseModel, Field
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ class DiffType(str, Enum):
54
+ """
55
+ @classdesc 差异类型枚举
56
+
57
+ 定义配置比对时可能出现的四种差异状态:
58
+ - ADDED: 新增内容
59
+ - MODIFIED: 修改内容
60
+ - DELETED: 删除内容
61
+ - UNCHANGED: 未变更(通常不展示)
62
+ """
63
+
64
+ ADDED = "added"
65
+ MODIFIED = "modified"
66
+ DELETED = "deleted"
67
+ UNCHANGED = "unchanged"
68
+
69
+
70
+ class PropertyDiff(BaseModel):
71
+ """
72
+ @classdesc 属性级差异模型
73
+
74
+ 记录单个配置属性的变更详情,包括属性名、旧值、新值和变更类型。
75
+ """
76
+
77
+ key: str
78
+ oldValue: Any | None = None
79
+ newValue: Any | None = None
80
+ type: DiffType
81
+
82
+
83
+ class ConfigItemDiff(BaseModel):
84
+ """
85
+ @classdesc 配置项差异模型
86
+
87
+ 记录单个配置资源(如 schema、constraint)的整体变更,
88
+ 包含资源 ID、名称、变更类型以及具体的属性变更列表。
89
+ """
90
+
91
+ id: str
92
+ name: str
93
+ type: DiffType
94
+ original: dict[str, Any] | None = None
95
+ generated: dict[str, Any] | None = None
96
+ changes: list[PropertyDiff] = Field(default_factory=list)
97
+
98
+
99
+ class ConfigDiffResult(BaseModel):
100
+ """
101
+ @classdesc 配置差异结果模型
102
+
103
+ 聚合整个项目配置的差异信息,分别存储 manifest、schemas、constraints、regex_nodes 的变更列表。
104
+ """
105
+
106
+ manifest: list[Any] = Field(default_factory=list)
107
+ schemas: list[ConfigItemDiff] = Field(default_factory=list)
108
+ constraints: list[ConfigItemDiff] = Field(default_factory=list)
109
+ regex_nodes: list[ConfigItemDiff] = Field(default_factory=list)
110
+
111
+
112
+ class ConfigDiffService:
113
+ """
114
+ @classdesc 配置差异比对服务
115
+
116
+ 提供类方法 compare 作为统一入口,支持递归比对 dict/list 嵌套结构,
117
+ 识别新增、修改、删除三种差异类型。
118
+ """
119
+
120
+ @classmethod
121
+ def compare(cls, old_config: dict[str, Any], new_config: dict[str, Any]) -> ConfigDiffResult:
122
+ """
123
+ @methoddesc 比较两份完整配置并返回差异结果
124
+
125
+ 参数:
126
+ old_config: 旧配置字典
127
+ new_config: 新配置字典
128
+
129
+ 返回:
130
+ ConfigDiffResult 差异结果对象
131
+ """
132
+ logger.debug("Starting config comparison")
133
+ result = ConfigDiffResult()
134
+
135
+ old_manifest = old_config.get("manifest", {})
136
+ new_manifest = new_config.get("manifest", {})
137
+ if hasattr(old_manifest, "model_dump"):
138
+ old_manifest = old_manifest.model_dump(exclude_none=True)
139
+ if hasattr(new_manifest, "model_dump"):
140
+ new_manifest = new_manifest.model_dump(exclude_none=True)
141
+
142
+ result.manifest = cls._diff_recursive(old_manifest, new_manifest, [])
143
+
144
+ result.schemas = cls._compare_resources(old_config.get("schemas", {}), new_config.get("schemas", {}))
145
+ result.constraints = cls._compare_resources(
146
+ old_config.get("constraints", {}), new_config.get("constraints", {})
147
+ )
148
+ result.regex_nodes = cls._compare_resources(
149
+ old_config.get("regex_nodes", {}), new_config.get("regex_nodes", {})
150
+ )
151
+
152
+ total_changes = len(result.manifest) + len(result.schemas) + len(result.constraints) + len(result.regex_nodes)
153
+ logger.debug(f"Config comparison completed: {total_changes} changes detected")
154
+ return result
155
+
156
+ @classmethod
157
+ def _compare_resources(cls, old_resources: dict[str, Any], new_resources: dict[str, Any]) -> list[ConfigItemDiff]:
158
+ """
159
+ @methoddesc 比对资源字典(schemas/constraints/regex_nodes)
160
+
161
+ 识别新增、删除和修改的资源项。
162
+
163
+ 参数:
164
+ old_resources: 旧资源字典
165
+ new_resources: 新资源字典
166
+
167
+ 返回:
168
+ ConfigItemDiff 列表
169
+ """
170
+ result: list[ConfigItemDiff] = []
171
+
172
+ old_keys = set(old_resources.keys())
173
+ new_keys = set(new_resources.keys())
174
+
175
+ added_keys = new_keys - old_keys
176
+ deleted_keys = old_keys - new_keys
177
+ common_keys = old_keys.intersection(new_keys)
178
+
179
+ # 旧配置存在、新配置缺失的资源必须以 DELETED 呈现——
180
+ # 否则 AI 重新生成后的合并确认界面看不到"将被删除"的资源
181
+ for key in deleted_keys:
182
+ old_val = old_resources[key]
183
+ if hasattr(old_val, "model_dump"):
184
+ old_val = old_val.model_dump(exclude_none=True)
185
+ result.append(
186
+ ConfigItemDiff(id=key, name=key, type=DiffType.DELETED, original=old_val, generated=None, changes=[])
187
+ )
188
+
189
+ for key in added_keys:
190
+ new_val = new_resources[key]
191
+ if hasattr(new_val, "model_dump"):
192
+ new_val = new_val.model_dump(exclude_none=True)
193
+ result.append(
194
+ ConfigItemDiff(id=key, name=key, type=DiffType.ADDED, original=None, generated=new_val, changes=[])
195
+ )
196
+
197
+ for key in common_keys:
198
+ old_val = old_resources[key]
199
+ new_val = new_resources[key]
200
+
201
+ if hasattr(old_val, "model_dump"):
202
+ old_val = old_val.model_dump(exclude_none=True)
203
+ if hasattr(new_val, "model_dump"):
204
+ new_val = new_val.model_dump(exclude_none=True)
205
+
206
+ changes = cls._build_property_diff(old_val, new_val, [])
207
+ if changes:
208
+ result.append(
209
+ ConfigItemDiff(
210
+ id=key, name=key, type=DiffType.MODIFIED, original=old_val, generated=new_val, changes=changes
211
+ )
212
+ )
213
+
214
+ return result
215
+
216
+ @classmethod
217
+ def _build_property_diff(cls, old: dict[str, Any], new: dict[str, Any], path: list[str]) -> list[PropertyDiff]:
218
+ """
219
+ @methoddesc 递归构建属性级差异列表
220
+
221
+ 深度遍历字典结构,对比每个键值对的变更。
222
+
223
+ 参数:
224
+ old: 旧字典
225
+ new: 新字典
226
+ path: 当前路径前缀
227
+
228
+ 返回:
229
+ PropertyDiff 列表
230
+ """
231
+ changes: list[PropertyDiff] = []
232
+
233
+ if not isinstance(old, dict) or not isinstance(new, dict):
234
+ return changes
235
+
236
+ old_keys = set(old.keys())
237
+ new_keys = set(new.keys())
238
+
239
+ for key in new_keys - old_keys:
240
+ changes.append(
241
+ PropertyDiff(
242
+ key=".".join(path + [key]) if path else key, oldValue=None, newValue=new[key], type=DiffType.ADDED
243
+ )
244
+ )
245
+
246
+ for key in old_keys - new_keys:
247
+ changes.append(
248
+ PropertyDiff(
249
+ key=".".join(path + [key]) if path else key, oldValue=old[key], newValue=None, type=DiffType.DELETED
250
+ )
251
+ )
252
+
253
+ for key in old_keys.intersection(new_keys):
254
+ old_v = old[key]
255
+ new_v = new[key]
256
+
257
+ if isinstance(old_v, dict) and isinstance(new_v, dict):
258
+ changes.extend(cls._build_property_diff(old_v, new_v, path + [key]))
259
+ elif old_v != new_v:
260
+ changes.append(
261
+ PropertyDiff(
262
+ key=".".join(path + [key]) if path else key,
263
+ oldValue=old_v,
264
+ newValue=new_v,
265
+ type=DiffType.MODIFIED,
266
+ )
267
+ )
268
+
269
+ return changes
270
+
271
+ @classmethod
272
+ def _diff_recursive(cls, old: Any, new: Any, path: list[str | int]) -> list[Any]:
273
+ """
274
+ @methoddesc 递归比对任意数据结构
275
+
276
+ 支持 dict、list 和基本类型的深层比对。
277
+
278
+ 参数:
279
+ old: 旧值
280
+ new: 新值
281
+ path: 当前路径
282
+
283
+ 返回:
284
+ 变更字典列表
285
+ """
286
+ changes = []
287
+
288
+ if isinstance(old, dict) and isinstance(new, dict):
289
+ old_keys = set(old.keys())
290
+ new_keys = set(new.keys())
291
+
292
+ for key in new_keys - old_keys:
293
+ changes.append({"path": path + [key], "old_value": None, "new_value": new[key], "diff_type": "added"})
294
+
295
+ for key in old_keys - new_keys:
296
+ changes.append({"path": path + [key], "old_value": old[key], "new_value": None, "diff_type": "removed"})
297
+
298
+ for key in old_keys.intersection(new_keys):
299
+ changes.extend(cls._diff_recursive(old[key], new[key], path + [key]))
300
+
301
+ elif isinstance(old, list) and isinstance(new, list):
302
+ len_old = len(old)
303
+ len_new = len(new)
304
+
305
+ for i in range(min(len_old, len_new)):
306
+ changes.extend(cls._diff_recursive(old[i], new[i], path + [i]))
307
+
308
+ if len_new > len_old:
309
+ for i in range(len_old, len_new):
310
+ changes.append({"path": path + [i], "old_value": None, "new_value": new[i], "diff_type": "added"})
311
+
312
+ if len_old > len_new:
313
+ for i in range(len_new, len_old):
314
+ changes.append({"path": path + [i], "old_value": old[i], "new_value": None, "diff_type": "removed"})
315
+
316
+ else:
317
+ if old != new:
318
+ changes.append({"path": path, "old_value": old, "new_value": new, "diff_type": "modified"})
319
+
320
+ return changes