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,279 @@
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 数据文件画像
17
+
18
+ 读取数据文件的前 N 行,提取列名、数据类型、空值数和样本值,生成供
19
+ Prompt 构建与 Agent 任务消息使用的画像数据。纯数据加工逻辑,无 Provider
20
+ 依赖;取消检查通过 is_cancelled 回调注入,保持模块无状态。
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import logging
27
+ import os
28
+ from collections.abc import Callable
29
+ from dataclasses import dataclass
30
+ from typing import Any
31
+
32
+ from .errors import CancelledError
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass
38
+ class ProfilingOptions:
39
+ """数据画像选项"""
40
+
41
+ sample_rows: int = 100
42
+ sample_values_per_column: int = 100
43
+ max_files: int = 50
44
+ max_cell_chars: int = 500
45
+
46
+
47
+ async def profile_files(
48
+ file_paths: list[str], options: ProfilingOptions, is_cancelled: Callable[[], bool]
49
+ ) -> list[dict]:
50
+ """分析数据文件获取画像。
51
+
52
+ 读取各类数据文件的前 N 行,提取列名、数据类型、空值数和样本值。
53
+ Excel 多 sheet 时每个 sheet 产出独立的 table 画像。
54
+
55
+ 参数:
56
+ file_paths: 数据文件路径列表
57
+ options: 画像选项(采样行数/每列样本数/文件数上限/单元格截断长度)
58
+ is_cancelled: 取消状态探测回调,返回 True 时抛出 CancelledError
59
+
60
+ 返回:
61
+ 画像结果列表,每项含 path/table_name/sheet_name/columns
62
+ """
63
+ import pandas as pd
64
+
65
+ results = []
66
+
67
+ # 限制最多处理的文件数量
68
+ for path in file_paths[: options.max_files]:
69
+ if is_cancelled():
70
+ raise CancelledError("Generation cancelled")
71
+ if not os.path.exists(path):
72
+ continue
73
+
74
+ ext = os.path.splitext(path)[1].lower()
75
+ # 使用文件名(不含扩展名)作为默认表名
76
+ table_name = os.path.splitext(os.path.basename(path))[0]
77
+
78
+ try:
79
+ # 收集该文件的所有 sheets:(sheet_name, df) 列表。
80
+ # Excel 可能多 sheet,每个 sheet 作为独立 table 画像;其他类型单 sheet(sheet_name=None)
81
+ sheets: list[tuple[str | None, pd.DataFrame]] = []
82
+ if ext in [".xlsx", ".xls"]:
83
+ xl = pd.ExcelFile(path)
84
+ if not xl.sheet_names:
85
+ continue
86
+ # 读取所有 sheet(修复:原仅读 sheet_names[0],多 sheet Excel 会丢数据)
87
+ for sn in xl.sheet_names:
88
+ sheet_df = pd.read_excel(path, sheet_name=sn, nrows=options.sample_rows)
89
+ sheets.append((sn, sheet_df))
90
+ elif ext == ".csv":
91
+ df = pd.read_csv(path, nrows=options.sample_rows, encoding="utf-8")
92
+ sheets.append((None, df))
93
+ elif ext == ".json":
94
+ # JSON 文件处理
95
+ with open(path, encoding="utf-8") as f:
96
+ data = json.load(f)
97
+ # 处理嵌套结构
98
+ if isinstance(data, dict):
99
+ # 查找数据记录数组
100
+ records = find_json_records(data)
101
+ if records:
102
+ df = pd.json_normalize(records)
103
+ else:
104
+ df = pd.json_normalize([data])
105
+ elif isinstance(data, list):
106
+ df = pd.json_normalize(data)
107
+ else:
108
+ continue
109
+ # 限制行数
110
+ sheets.append((None, df.head(options.sample_rows)))
111
+ elif ext == ".jsonl":
112
+ # JSON Lines 文件处理
113
+ records = []
114
+ with open(path, encoding="utf-8") as f:
115
+ for i, line in enumerate(f):
116
+ if i >= options.sample_rows:
117
+ break
118
+ line = line.strip()
119
+ if line:
120
+ records.append(json.loads(line))
121
+ sheets.append((None, pd.json_normalize(records)))
122
+ else:
123
+ # 不支持的文件类型
124
+ continue
125
+
126
+ # 每个 sheet 独立构建画像结果(Excel 多 sheet 会产出多个 table 画像)
127
+ multi_sheet = len(sheets) > 1
128
+ for sheet_name, df in sheets:
129
+ if df is None or df.empty:
130
+ continue
131
+ # 分析列信息
132
+ columns = []
133
+ for col_name in df.columns:
134
+ if is_cancelled():
135
+ raise CancelledError("Generation cancelled")
136
+ col_data = df[col_name]
137
+
138
+ # 获取样本值(处理复杂类型如列表、字典)
139
+ # 使用 dict.fromkeys 保序去重,避免 list 的 O(n) in 查找导致的 O(n²) 开销
140
+ seen: dict[str, None] = {}
141
+ max_samples = options.sample_values_per_column
142
+ for v in col_data.dropna():
143
+ if len(seen) >= max_samples:
144
+ break
145
+ # 处理复杂类型并截断
146
+ v_str = str(v)[: options.max_cell_chars]
147
+ if v_str not in seen:
148
+ seen[v_str] = None
149
+ sample_values: list[str] = list(seen.keys())
150
+
151
+ columns.append(
152
+ {
153
+ "name": str(col_name),
154
+ "dtype": str(col_data.dtype),
155
+ "null_count": int(col_data.isna().sum()),
156
+ "sample_values": sample_values,
157
+ }
158
+ )
159
+
160
+ # 多 sheet Excel 时用 "文件名_sheet名" 作为 table_name 区分,避免多表同名
161
+ effective_table_name = f"{table_name}_{sheet_name}" if (multi_sheet and sheet_name) else table_name
162
+ result = {
163
+ "path": path,
164
+ "table_name": effective_table_name,
165
+ "sheet_name": sheet_name,
166
+ "columns": columns,
167
+ }
168
+ results.append(result)
169
+
170
+ except Exception as e:
171
+ # 读取失败,记录警告并跳过,不影响其他文件的处理。
172
+ # 注意:与原实现一致,列循环内取消抛出的 CancelledError 也会走到这里被记为
173
+ # 警告跳过——取消最终由调用方的阶段边界检查(_check_cancelled)兜底。
174
+ logger.warning(f"无法分析文件 {path}: {e}")
175
+ continue
176
+
177
+ return results
178
+
179
+
180
+ def find_json_records(data: dict) -> list[dict] | None:
181
+ """在嵌套 JSON 对象中查找数据记录数组。
182
+
183
+ 策略:递归遍历 JSON 的所有节点,找到最长的、元素为字典的数组。
184
+
185
+ 参数:
186
+ data: 已解析的 JSON 对象
187
+
188
+ 返回:
189
+ 最长的记录数组;不存在时返回 None
190
+ """
191
+ best_array = None
192
+ best_length = 0
193
+
194
+ def search(obj: Any) -> None:
195
+ nonlocal best_array, best_length
196
+
197
+ if isinstance(obj, dict):
198
+ for key, value in obj.items():
199
+ if isinstance(value, list):
200
+ # 检查是否是数据记录数组(元素是字典)
201
+ if value and isinstance(value[0], dict):
202
+ if len(value) > best_length:
203
+ best_length = len(value)
204
+ best_array = value
205
+ # 继续搜索数组内部(处理嵌套列表)
206
+ for item in value:
207
+ if isinstance(item, (dict, list)):
208
+ search(item)
209
+ elif isinstance(value, (dict, list)):
210
+ search(value)
211
+ elif isinstance(obj, list):
212
+ for item in obj:
213
+ if isinstance(item, (dict, list)):
214
+ search(item)
215
+
216
+ search(data)
217
+ return best_array
218
+
219
+
220
+ def filter_profiling(
221
+ profiling_data: list[dict],
222
+ table_names: list[str] | None,
223
+ columns_filter: list[str] | None,
224
+ ) -> list[dict]:
225
+ """根据 scope 过滤画像数据。
226
+
227
+ 参数:
228
+ profiling_data: 完整画像数据
229
+ table_names: 表名白名单,None 表示不过滤表
230
+ columns_filter: 列过滤,支持 "table.column" 或裸列名,None 表示不过滤列
231
+
232
+ 返回:
233
+ 过滤后的画像数据(浅拷贝,columns 为新列表)
234
+ """
235
+ result = []
236
+ table_col_map: dict[str, list[str]] = {}
237
+ if columns_filter:
238
+ for tc in columns_filter:
239
+ if "." in tc:
240
+ t, c = tc.split(".", 1)
241
+ table_col_map.setdefault(t, []).append(c)
242
+ else:
243
+ table_col_map.setdefault("", []).append(tc)
244
+
245
+ for item in profiling_data:
246
+ table_name = item.get("table_name", "")
247
+ if table_names and table_name not in table_names:
248
+ continue
249
+
250
+ columns = item.get("columns", [])
251
+ if columns_filter:
252
+ allowed = set(table_col_map.get(table_name, []) + table_col_map.get("", []))
253
+ if allowed:
254
+ columns = [c for c in columns if c.get("name", "") in allowed]
255
+
256
+ if columns:
257
+ filtered = dict(item)
258
+ filtered["columns"] = columns
259
+ result.append(filtered)
260
+ return result
261
+
262
+
263
+ def format_profiling_for_agent(profiling_data: list[dict]) -> str:
264
+ """将画像数据格式化为 Agent 任务消息中的文本段落。"""
265
+ lines = []
266
+ for item in profiling_data:
267
+ table_name = item.get("table_name", "")
268
+ lines.append(f"### {table_name}")
269
+ lines.append(f"文件: {item.get('path', '')}")
270
+ if item.get("sheet_name"):
271
+ lines.append(f"Sheet: {item['sheet_name']}")
272
+ for col in item.get("columns", []):
273
+ samples = col.get("sample_values", [])[:3]
274
+ samples_str = ", ".join(str(s)[:30] for s in samples)
275
+ line = f"- {col['name']}: {col['dtype']}, 空值{col['null_count']}"
276
+ if samples_str:
277
+ line += f", 例: {samples_str}"
278
+ lines.append(line)
279
+ return "\n".join(lines)
@@ -0,0 +1,199 @@
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 Prompt 构建器
17
+
18
+ 功能概述:
19
+ - 根据数据画像结果构建 LLM 提示词
20
+ - 包含 Schema、Constraint、Regex Node 的格式说明
21
+ - 支持 prompt 长度预算:超限后自动降级(减少列数 → 减少文件数)
22
+
23
+ 输入示例:
24
+ profiling_data = [{"table_name": "users", "columns": [...]}]
25
+ prompt, warnings = build_prompt(profiling_data, "电商数据校验")
26
+
27
+ 输出示例:
28
+ (prompt_string, ["部分文件因 prompt 过长被省略: ..."])
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import logging
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # 默认 prompt 长度预算(字符数)。DeepSeek v4-flash 支持 1M 上下文,
38
+ # 但给 system prompt、输出和 overhead 留余量,预算设为 80K 字符。
39
+ DEFAULT_MAX_PROMPT_CHARS = 80_000
40
+
41
+
42
+ def _estimate_prompt_len(
43
+ profiling_data: list[dict],
44
+ project_name: str,
45
+ max_columns_per_table: int,
46
+ max_sample_values: int,
47
+ max_sample_length: int,
48
+ ) -> int:
49
+ """估算 prompt 长度(不实际构建完整字符串)"""
50
+ length = len(project_name) + 200 # 固定开销(header、格式说明等)
51
+ for item in profiling_data:
52
+ length += 100 # 表头开销
53
+ length += len(item.get("table_name", ""))
54
+ length += len(item.get("path", ""))
55
+ if item.get("sheet_name"):
56
+ length += 50
57
+ columns = item.get("columns", [])[:max_columns_per_table]
58
+ for col in columns:
59
+ length += 80 + len(col["name"]) + len(str(col["dtype"]))
60
+ samples = col.get("sample_values", [])[:max_sample_values]
61
+ length += sum(min(len(str(s)), max_sample_length) for s in samples)
62
+ remaining = len(item.get("columns", [])) - max_columns_per_table
63
+ if remaining > 0:
64
+ length += 40
65
+ return length
66
+
67
+
68
+ def build_prompt(
69
+ profiling_data: list[dict],
70
+ project_name: str,
71
+ max_prompt_chars: int = DEFAULT_MAX_PROMPT_CHARS,
72
+ extra_context: list[str] | None = None,
73
+ ) -> tuple[str, list[str]]:
74
+ """
75
+ @methoddesc 构建生成提示词(带长度预算和自动降级)
76
+
77
+ 策略(渐进式降级):
78
+ 1. 先用较宽松的列数限制(20 列/表,2 样本值)估算长度
79
+ 2. 若超限,先降低列数限制(20 → 10 → 5)
80
+ 3. 若仍超限,减少文件数(保留前 N 个)
81
+ 4. 最终返回 truncation 警告列表
82
+
83
+ 参数:
84
+ profiling_data: 数据画像结果列表
85
+ project_name: 项目名称
86
+ max_prompt_chars: prompt 最大字符数预算
87
+ extra_context: 额外上下文文本列表(如上一轮结果、校验问题等)
88
+
89
+ 返回:
90
+ (prompt 字符串, truncation 警告列表)
91
+ """
92
+ max_columns_per_table = 20
93
+ max_sample_values = 2
94
+ max_sample_length = 30
95
+ warnings: list[str] = []
96
+
97
+ # 阶段 1: 估算长度,尝试逐步降级
98
+ # 降级策略:列数限制 [20, 10, 5, 3],然后截断文件数
99
+ for columns_limit in (20, 10, 5, 3):
100
+ max_columns_per_table = columns_limit
101
+ estimated = _estimate_prompt_len(
102
+ profiling_data, project_name, max_columns_per_table, max_sample_values, max_sample_length
103
+ )
104
+ if estimated <= max_prompt_chars:
105
+ if columns_limit < 20:
106
+ warnings.append(f"数据文件较多,prompt 长度受限,每表仅展示前 {columns_limit} 列")
107
+ break
108
+ else:
109
+ # 即使 3 列也超限,需要截断文件数
110
+ # 二分查找最大可处理的文件数
111
+ lo, hi = 1, len(profiling_data)
112
+ best_files = 1
113
+ while lo <= hi:
114
+ mid = (lo + hi) // 2
115
+ estimated = _estimate_prompt_len(
116
+ profiling_data[:mid], project_name, 3, max_sample_values, max_sample_length
117
+ )
118
+ if estimated <= max_prompt_chars:
119
+ best_files = mid
120
+ lo = mid + 1
121
+ else:
122
+ hi = mid - 1
123
+
124
+ # 先记录总数再切片:旧实现在切片后取 len,omitted 恒为 0,
125
+ # 超预算文件被静默丢弃且无任何警告
126
+ total_files = len(profiling_data)
127
+ profiling_data = profiling_data[:best_files]
128
+ omitted = total_files - best_files
129
+ if omitted > 0:
130
+ warnings.append(f"数据文件过多(共 {total_files} 个),仅分析前 {best_files} 个文件")
131
+ max_columns_per_table = 3
132
+
133
+ # 阶段 2: 实际构建 prompt
134
+ prompt = f"""为项目 "{project_name}" 生成数据验证配置。
135
+
136
+ ## 数据画像
137
+
138
+ """
139
+ for item in profiling_data:
140
+ table_name = item.get("table_name", "")
141
+ prompt += f"### {table_name}\n"
142
+ prompt += f"文件: {item.get('path', '')}\n"
143
+ if item.get("sheet_name"):
144
+ prompt += f"Sheet: {item['sheet_name']}\n"
145
+
146
+ columns = item.get("columns", [])[:max_columns_per_table]
147
+ for col in columns:
148
+ samples = col.get("sample_values", [])[:max_sample_values]
149
+ samples_str = ", ".join(str(s)[:max_sample_length] for s in samples)
150
+ prompt += f"- {col['name']}: {col['dtype']}, 空值{col['null_count']}"
151
+ if samples_str:
152
+ prompt += f", 例: {samples_str}"
153
+ prompt += "\n"
154
+
155
+ remaining = len(item.get("columns", [])) - max_columns_per_table
156
+ if remaining > 0:
157
+ prompt += f"- ... 还有 {remaining} 列未显示\n"
158
+
159
+ # 追加额外上下文
160
+ if extra_context:
161
+ prompt += "\n## 额外上下文\n\n"
162
+ for ctx in extra_context:
163
+ prompt += f"{ctx}\n\n"
164
+
165
+ prompt += """
166
+ ## 输出要求
167
+
168
+ 返回 JSON 配置,包含 schemas、constraints、regex_nodes 三个字段。
169
+
170
+ Schema 格式(请为每个 schema 提供语义化、唯一的 id,使用小写英文+下划线,如 "users"、"order_items"。该 id 会作为约束和正则节点引用的主键):
171
+ {"id": "users", "name": "用户表", "source": {"mode": "relative_file", "path": "data/users.xlsx", "header_row": 0}, "columns": [{"id": "email", "name": "邮箱", "type": "string", "primary_key": true, "nullable": false}]}
172
+
173
+ Constraint 格式(V2,refs/params 分离,table_id 必须与 schema 的 id 一致):
174
+ {"id": "users_email_notnull", "type": "NotNull", "enabled": true, "refs": {"table_id": "users", "column_id": "email"}, "params": {}}
175
+
176
+ 支持类型:NotNull, Unique, AllowedValues(refs+params{allowed_values}), Range(refs+params{min,max}), ForeignKey, Conditional。
177
+
178
+ Regex Node 格式:
179
+ {"id": "email_regex", "name": "邮箱格式", "pattern": "^[^@]+@[^@]+$", "match_mode": "full", "source_ref": {"table_id": "users", "column_id": "email"}}
180
+
181
+ 建议:
182
+ - email/手机号/身份证 → regex_nodes
183
+ - status/gender → AllowedValues
184
+ - 主键 → Unique
185
+ - 非空列 → NotNull
186
+ - 数值列 → Range
187
+
188
+ 直接返回 JSON,不要解释。"""
189
+
190
+ actual_len = len(prompt)
191
+ logger.info(
192
+ "[build_prompt] files=%d, columns_limit=%d, estimated=%d, actual=%d",
193
+ len(profiling_data),
194
+ max_columns_per_table,
195
+ _estimate_prompt_len(profiling_data, project_name, max_columns_per_table, max_sample_values, max_sample_length),
196
+ actual_len,
197
+ )
198
+
199
+ return prompt, warnings
@@ -0,0 +1,144 @@
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 LLM 响应 JSON 提取与解析
17
+
18
+ 从 LLM 返回的自由文本中可靠地提取 JSON 配置对象。核心是字符串感知的
19
+ 大括号配平扫描:JSON 字符串值内的 '{' / '}' 不参与配平,避免字符串值
20
+ 包含 '}' 时把合法 JSON 截断(如 {"a":"x}y"} 被截成 {"a":"x})。
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import re
27
+
28
+ from .errors import GenerationParseError
29
+
30
+
31
+ def find_json_object_end(text: str, start: int) -> int | None:
32
+ """从 start(应指向 '{')开始做字符串感知的大括号配平扫描。
33
+
34
+ 跟踪扫描位置是否处于双引号字符串内部(处理反斜杠转义):
35
+ 字符串内的 '{' / '}' 不参与配平,避免 JSON 字符串值包含 '}' 时
36
+ 把合法 JSON 截断(如 {"a":"x}y"} 被截成 {"a":"x})。
37
+
38
+ 参数:
39
+ text: 待扫描文本
40
+ start: 起始索引(text[start] 应为 '{')
41
+
42
+ 返回:
43
+ 匹配闭括号的索引(含);扫描不到配平闭括号时返回 None
44
+ """
45
+ balance = 0
46
+ in_string = False
47
+ escaped = False
48
+ for i in range(start, len(text)):
49
+ char = text[i]
50
+ if in_string:
51
+ if escaped:
52
+ # 转义序列的第二个字符被消耗,不参与边界判断
53
+ escaped = False
54
+ elif char == "\\":
55
+ escaped = True
56
+ elif char == '"':
57
+ in_string = False
58
+ continue
59
+ if char == '"':
60
+ in_string = True
61
+ elif char == "{":
62
+ balance += 1
63
+ elif char == "}":
64
+ balance -= 1
65
+ if balance == 0:
66
+ return i
67
+ return None
68
+
69
+
70
+ def parse_llm_response(content: str) -> dict:
71
+ """解析 LLM 返回的文本,提取其中的 JSON 配置。
72
+
73
+ 处理步骤:
74
+ 1. 尝试提取 Markdown 代码块(```json ... ```)中的内容
75
+ 2. 通过大括号平衡计数找到最外层的 JSON 对象
76
+ 3. 使用 json.loads 解析
77
+
78
+ 参数:
79
+ content: LLM 返回的原始文本
80
+
81
+ 返回:
82
+ 解析出的 JSON 对象字典
83
+
84
+ 异常:
85
+ GenerationParseError: 内容无法解析为 JSON 对象(携带原始内容前 2000 字符)
86
+ """
87
+ original = content
88
+
89
+ # 尝试找到 Markdown JSON 代码块
90
+ match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL)
91
+ if match:
92
+ content = match.group(1).strip()
93
+
94
+ # 通过大括号平衡计数找到最外层的 JSON 对象(字符串感知,字符串内的 '}' 不参与配平)
95
+ start = content.find("{")
96
+ if start != -1:
97
+ end = find_json_object_end(content, start)
98
+ if end is not None:
99
+ content = content[start : end + 1]
100
+
101
+ try:
102
+ parsed = json.loads(content)
103
+ if not isinstance(parsed, dict):
104
+ raise GenerationParseError(
105
+ f"LLM 响应解析结果不是 JSON 对象,实际类型: {type(parsed).__name__}",
106
+ raw_content=original[:2000],
107
+ )
108
+ return parsed
109
+ except json.JSONDecodeError as e:
110
+ raise GenerationParseError(
111
+ f"AI 返回的内容无法解析为有效配置,请重试或调整描述后重试({e})",
112
+ raw_content=original[:2000],
113
+ )
114
+
115
+
116
+ def try_extract_json_object(content: str | None) -> dict | None:
117
+ """尽力从文本中提取一个 JSON 对象,失败返回 None(不抛异常)。
118
+
119
+ 与 parse_llm_response 的差别:本函数用于"兜底解析"场景
120
+ (如 Agent 最后一轮 content),解析失败不视为错误。
121
+
122
+ 参数:
123
+ content: 待提取的文本,可为 None
124
+
125
+ 返回:
126
+ 提取到的 JSON 对象字典;无法提取时返回 None
127
+ """
128
+ if not content:
129
+ return None
130
+ match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL)
131
+ text = match.group(1).strip() if match else content
132
+ start = text.find("{")
133
+ if start == -1:
134
+ return None
135
+ end = find_json_object_end(text, start)
136
+ if end is None:
137
+ return None
138
+ try:
139
+ parsed = json.loads(text[start : end + 1])
140
+ if isinstance(parsed, dict):
141
+ return parsed
142
+ except json.JSONDecodeError:
143
+ return None
144
+ return None