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,256 @@
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 Agent 记忆模块
17
+
18
+ 功能概述:
19
+ - 管理 Agent 多轮对话的上下文
20
+ - 按 token 预算截断历史,保留 system prompt 和最近轮次
21
+ - 支持保存和恢复 checkpoint
22
+
23
+ 架构设计:
24
+ - 使用纯字典列表表示消息,兼容 OpenAI messages 格式
25
+ - token 估算委托给 utils.estimate_tokens 统一实现(中文字 1:1、英文单词/数字串/标点各计 1,另加固定开销)
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ from typing import Any
32
+
33
+ from app.shared.services.ai.agent.types import AgentTurn
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # 默认最大 token 预算
38
+ DEFAULT_MAX_TOKENS = 120000
39
+ # 单条消息保留的最大字符数
40
+ _MAX_MESSAGE_CHARS = 8000
41
+
42
+
43
+ class AgentMemory:
44
+ """
45
+ @classdesc Agent 短期记忆
46
+
47
+ 维护 system prompt + 用户初始任务 + 多轮对话历史。
48
+ """
49
+
50
+ def __init__(self, system_prompt: str, max_tokens: int = DEFAULT_MAX_TOKENS):
51
+ """
52
+ @methoddesc 初始化记忆
53
+
54
+ 参数:
55
+ system_prompt: 系统提示词
56
+ max_tokens: 最大 token 预算
57
+ """
58
+ self.system_prompt = system_prompt
59
+ self.max_tokens = max_tokens
60
+ self._messages: list[dict[str, Any]] = []
61
+ self._turns: list[AgentTurn] = []
62
+ self._turn_count: int = 0
63
+ if system_prompt:
64
+ self._messages.append({"role": "system", "content": system_prompt})
65
+
66
+ def set_task(self, task_message: str) -> None:
67
+ """设置用户初始任务消息。"""
68
+ # 确保只有一条用户初始任务,避免重复
69
+ self._messages = [m for m in self._messages if not (m.get("role") == "user" and m.get("_is_task"))]
70
+ self._messages.append({"role": "user", "content": task_message, "_is_task": True})
71
+
72
+ def add_assistant_message(
73
+ self,
74
+ content: str | None,
75
+ tool_calls: list[dict[str, Any]] | None = None,
76
+ ) -> None:
77
+ """添加 assistant 消息。"""
78
+ msg: dict[str, Any] = {"role": "assistant"}
79
+ if content:
80
+ msg["content"] = self._truncate(content)
81
+ if tool_calls:
82
+ msg["tool_calls"] = tool_calls
83
+ self._messages.append(msg)
84
+
85
+ def add_tool_result(self, tool_call_id: str, name: str, result: dict[str, Any] | str) -> None:
86
+ """添加工具结果消息。"""
87
+ observation = result if isinstance(result, str) else self._dict_to_observation(result)
88
+ self._messages.append(
89
+ {
90
+ "role": "tool",
91
+ "tool_call_id": tool_call_id,
92
+ "name": name,
93
+ "content": self._truncate(observation),
94
+ }
95
+ )
96
+
97
+ def add_system_reminder(self, content: str) -> None:
98
+ """注入一条系统级提醒消息(用 user role,带 _is_reminder 标记)。
99
+
100
+ 为什么不用 role="system":get_messages 会跳过所有 role=="system" 的非首条消息,
101
+ 导致 reminder 被丢弃。用 user role 可确保它进入 LLM 的上下文。
102
+
103
+ 用途(P1-2 收敛引导):
104
+ - 接近 max_iterations 时提醒 LLM 直接回复、停止调工具
105
+ - 检测到跨轮重复调用同一工具同参数时提醒 LLM 使用已有结果
106
+
107
+ 参数:
108
+ content: 提醒文本
109
+ """
110
+ if not content:
111
+ return
112
+ self._messages.append({"role": "user", "content": self._truncate(content), "_is_reminder": True})
113
+
114
+ def add_turn(self, turn: AgentTurn) -> None:
115
+ """记录完整 turn。"""
116
+ self._turns.append(turn)
117
+ self._turn_count += 1
118
+
119
+ def get_messages(self) -> list[dict[str, Any]]:
120
+ """
121
+ @methoddesc 获取截断后的消息列表
122
+
123
+ 保留 system prompt 和最近的用户/assistant/tool 消息,不超出 token 预算。
124
+ """
125
+ # 计算 system prompt 开销
126
+ system_tokens = self._estimate_tokens(self.system_prompt)
127
+ budget = self.max_tokens - system_tokens
128
+
129
+ # 从后往前累加,直到预算用完
130
+ selected: list[dict[str, Any]] = []
131
+ total = 0
132
+ # 跳过 system prompt(已在开头)
133
+ for msg in reversed(self._messages):
134
+ if msg.get("role") == "system":
135
+ continue
136
+ tokens = self._estimate_message_tokens(msg)
137
+ if total + tokens > budget and selected:
138
+ break
139
+ selected.append(msg)
140
+ total += tokens
141
+
142
+ selected.reverse()
143
+ result: list[dict[str, Any]] = []
144
+ if self.system_prompt:
145
+ result.append({"role": "system", "content": self.system_prompt})
146
+ result.extend(selected)
147
+ return result
148
+
149
+ def get_turns(self) -> list[AgentTurn]:
150
+ """获取所有 turn 记录。"""
151
+ return self._turns
152
+
153
+ def create_checkpoint(self) -> dict[str, Any]:
154
+ """创建当前记忆 checkpoint(含完整 messages,支持跨进程恢复)。"""
155
+ return {
156
+ "system_prompt": self.system_prompt,
157
+ "max_tokens": self.max_tokens,
158
+ "message_count": len(self._messages),
159
+ "turn_count": self._turn_count,
160
+ "messages": [dict(m) for m in self._messages],
161
+ }
162
+
163
+ @classmethod
164
+ def restore_from_checkpoint(cls, checkpoint: dict[str, Any]) -> AgentMemory:
165
+ """从 checkpoint 重建 memory 状态。
166
+
167
+ 参数:
168
+ checkpoint: create_checkpoint 返回的字典
169
+
170
+ 返回:
171
+ 重建的 AgentMemory 实例
172
+ """
173
+ mem = cls(
174
+ system_prompt=checkpoint.get("system_prompt", ""),
175
+ max_tokens=checkpoint.get("max_tokens", DEFAULT_MAX_TOKENS),
176
+ )
177
+ mem._messages = [dict(m) for m in checkpoint.get("messages", [])]
178
+ mem._turn_count = checkpoint.get("turn_count", 0)
179
+ return mem
180
+
181
+ @staticmethod
182
+ def _truncate(text: str, max_chars: int = _MAX_MESSAGE_CHARS) -> str:
183
+ """截断超长文本(最终兜底)。"""
184
+ if len(text) <= max_chars:
185
+ return text
186
+ return text[:max_chars] + f"\n... [truncated {len(text) - max_chars} chars]"
187
+
188
+ @staticmethod
189
+ def _summarize_dict(data: dict[str, Any], max_field_chars: int = 1500) -> dict[str, Any]:
190
+ """对工具结果的 dict 做智能裁剪。
191
+
192
+ 策略:小字段(< max_field_chars)完整保留;大字段(长列表/长字符串)截断,
193
+ 保留关键摘要信息(如 errors 列表的前几条、success 标志等)。
194
+ 相比直接对 JSON 文本硬截断,避免在字段中间被切断导致 agent 误解。
195
+ """
196
+ summarized: dict[str, Any] = {}
197
+ for key, value in data.items():
198
+ # 关键小字段始终完整保留
199
+ if key in {"success", "error", "message", "total_rules", "passed", "issues"}:
200
+ summarized[key] = value
201
+ continue
202
+ if isinstance(value, str):
203
+ summarized[key] = value if len(value) <= max_field_chars else value[:max_field_chars] + "...[truncated]"
204
+ elif isinstance(value, list):
205
+ # 列表:保留前 5 项 + 计数提示
206
+ if len(value) <= 5:
207
+ summarized[key] = value
208
+ else:
209
+ summarized[key] = value[:5] + [f"...[{len(value) - 5} more items]"]
210
+ elif isinstance(value, dict):
211
+ # 嵌套 dict 递归裁剪(降阈值避免嵌套膨胀)
212
+ summarized[key] = AgentMemory._summarize_dict(value, max_field_chars // 2)
213
+ else:
214
+ summarized[key] = value
215
+ return summarized
216
+
217
+ @staticmethod
218
+ def _dict_to_observation(data: dict[str, Any]) -> str:
219
+ """将字典转为 observation 文本(先智能裁剪大字段,避免 JSON 中途被硬截断)。"""
220
+ import json
221
+
222
+ summarized = AgentMemory._summarize_dict(data)
223
+ try:
224
+ return json.dumps(summarized, ensure_ascii=False, indent=2)
225
+ except (TypeError, ValueError):
226
+ return str(summarized)
227
+
228
+ @staticmethod
229
+ def _estimate_tokens(text: str) -> int:
230
+ """粗略估算 token 数。
231
+
232
+ 委托给 utils.estimate_tokens 统一实现(中文/英文词/数字/标点分类加权),
233
+ 避免项目内两套不一致的 token 估算系数。延迟导入防循环依赖。
234
+ """
235
+ if not text:
236
+ return 0
237
+ from app.shared.services.ai.utils import estimate_tokens
238
+
239
+ return estimate_tokens(text)
240
+
241
+ @classmethod
242
+ def _estimate_message_tokens(cls, msg: dict[str, Any]) -> int:
243
+ """估算单条消息 token 数。"""
244
+ content = msg.get("content") or ""
245
+ tokens = cls._estimate_tokens(content)
246
+ # tool_calls 也计入
247
+ tool_calls = msg.get("tool_calls") or []
248
+ for tc in tool_calls:
249
+ func = tc.get("function", {})
250
+ tokens += cls._estimate_tokens(func.get("name", ""))
251
+ args = func.get("arguments")
252
+ if isinstance(args, str):
253
+ tokens += cls._estimate_tokens(args)
254
+ elif isinstance(args, dict):
255
+ tokens += cls._estimate_tokens(str(args))
256
+ return tokens + 4 # 消息格式开销
@@ -0,0 +1,282 @@
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 Agent 分块规划器
17
+
18
+ 功能概述:
19
+ - 根据数据文件数量、列数自动决定分块策略
20
+ - 输出 chunk 列表,每个 chunk 包含要处理的文件/列范围
21
+
22
+ 架构设计:
23
+ - 简单启发式:文件数多按文件分,列数多按列分
24
+ - 每个 chunk 独立生成配置,最后合并
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass, field
30
+ from typing import Any
31
+
32
+
33
+ @dataclass
34
+ class Chunk:
35
+ """单个分块"""
36
+
37
+ chunk_id: str
38
+ file_paths: list[str] = field(default_factory=list)
39
+ table_names: list[str] = field(default_factory=list)
40
+ columns: list[str] = field(default_factory=list)
41
+ reason: str = ""
42
+
43
+
44
+ @dataclass
45
+ class ChunkPlan:
46
+ """分块计划"""
47
+
48
+ chunks: list[Chunk] = field(default_factory=list)
49
+ strategy: str = ""
50
+ reason: str = ""
51
+
52
+
53
+ def build_chunk_plan(
54
+ profiling_data: list[dict],
55
+ file_paths: list[str],
56
+ max_columns_per_chunk: int = 20,
57
+ max_files_per_chunk: int = 5,
58
+ ) -> ChunkPlan:
59
+ """
60
+ @methoddesc 构建分块计划
61
+
62
+ 参数:
63
+ profiling_data: 数据画像
64
+ file_paths: 所有文件路径
65
+ max_columns_per_chunk: 每个 chunk 最大列数
66
+ max_files_per_chunk: 每个 chunk 最大文件数
67
+
68
+ 返回:
69
+ ChunkPlan 分块计划
70
+ """
71
+ total_files = len(profiling_data)
72
+ total_columns = sum(len(item.get("columns", [])) for item in profiling_data)
73
+
74
+ # 决定策略
75
+ if total_files > max_files_per_chunk:
76
+ strategy = "by_file"
77
+ chunks = _chunk_by_file(profiling_data, max_files_per_chunk)
78
+ elif total_columns > max_columns_per_chunk:
79
+ strategy = "by_column"
80
+ chunks = _chunk_by_column(profiling_data, max_columns_per_chunk)
81
+ else:
82
+ strategy = "single"
83
+ chunks = [
84
+ Chunk(
85
+ chunk_id="single",
86
+ file_paths=file_paths,
87
+ table_names=[item.get("table_name", "") for item in profiling_data],
88
+ reason="数据量较小,一次性处理",
89
+ )
90
+ ]
91
+
92
+ reason = f"共 {total_files} 个文件、{total_columns} 列,采用 {strategy} 策略"
93
+ return ChunkPlan(chunks=chunks, strategy=strategy, reason=reason)
94
+
95
+
96
+ def _chunk_by_file(profiling_data: list[dict], max_files_per_chunk: int) -> list[Chunk]:
97
+ """按文件分块。"""
98
+ chunks: list[Chunk] = []
99
+ for i in range(0, len(profiling_data), max_files_per_chunk):
100
+ group = profiling_data[i : i + max_files_per_chunk]
101
+ chunk_id = f"files_{i // max_files_per_chunk + 1}"
102
+ chunks.append(
103
+ Chunk(
104
+ chunk_id=chunk_id,
105
+ file_paths=[item.get("path", "") for item in group],
106
+ table_names=[item.get("table_name", "") for item in group],
107
+ reason=f"处理第 {i + 1}-{min(i + max_files_per_chunk, len(profiling_data))} 个文件",
108
+ )
109
+ )
110
+ return chunks
111
+
112
+
113
+ def _chunk_by_column(profiling_data: list[dict], max_columns_per_chunk: int) -> list[Chunk]:
114
+ """按列分块(优先保持同文件列在一起,但单表列数超过限制时拆表)。"""
115
+ chunks: list[Chunk] = []
116
+ chunk_index = 1
117
+
118
+ for item in profiling_data:
119
+ table_name = item.get("table_name", "")
120
+ path = item.get("path", "")
121
+ columns = item.get("columns", [])
122
+ col_names = [f"{table_name}.{col.get('name', '')}" for col in columns]
123
+
124
+ # 如果单表列数超过限制,拆分该表
125
+ if len(col_names) > max_columns_per_chunk:
126
+ for i in range(0, len(col_names), max_columns_per_chunk):
127
+ chunk = Chunk(
128
+ chunk_id=f"cols_{chunk_index}",
129
+ file_paths=[path],
130
+ table_names=[table_name],
131
+ columns=col_names[i : i + max_columns_per_chunk],
132
+ reason=f"表 {table_name} 列数过多,拆分为第 {i // max_columns_per_chunk + 1} 组",
133
+ )
134
+ chunks.append(chunk)
135
+ chunk_index += 1
136
+ continue
137
+
138
+ # 否则尝试加入当前 chunk
139
+ if chunks and len(chunks[-1].columns) + len(col_names) <= max_columns_per_chunk:
140
+ current = chunks[-1]
141
+ if path and path not in current.file_paths:
142
+ current.file_paths.append(path)
143
+ if table_name and table_name not in current.table_names:
144
+ current.table_names.append(table_name)
145
+ current.columns.extend(col_names)
146
+ current.reason = f"按列分组,共 {len(current.columns)} 列"
147
+ else:
148
+ chunk = Chunk(
149
+ chunk_id=f"cols_{chunk_index}",
150
+ file_paths=[path],
151
+ table_names=[table_name],
152
+ columns=col_names,
153
+ reason=f"按列分组,共 {len(col_names)} 列",
154
+ )
155
+ chunks.append(chunk)
156
+ chunk_index += 1
157
+
158
+ return chunks
159
+
160
+
161
+ def plan_to_dict(plan: ChunkPlan) -> dict[str, Any]:
162
+ """将 ChunkPlan 转为可 JSON 序列化的字典。"""
163
+ return {
164
+ "strategy": plan.strategy,
165
+ "reason": plan.reason,
166
+ "chunks": [
167
+ {
168
+ "chunk_id": c.chunk_id,
169
+ "file_paths": c.file_paths,
170
+ "table_names": c.table_names,
171
+ "columns": c.columns,
172
+ "reason": c.reason,
173
+ }
174
+ for c in plan.chunks
175
+ ],
176
+ }
177
+
178
+
179
+ # ===== 按源(source)分块(迁移专用,不影响 generation 主路径)=====
180
+
181
+
182
+ @dataclass
183
+ class SourceChunk:
184
+ """单个来源分片"""
185
+
186
+ chunk_id: str
187
+ source_names: list[str] = field(default_factory=list)
188
+ source_indices: list[int] = field(default_factory=list) # 指向 parsed_intents 原始列表
189
+ reason: str = ""
190
+
191
+
192
+ @dataclass
193
+ class SourceChunkPlan:
194
+ """来源分块计划"""
195
+
196
+ chunks: list[SourceChunk] = field(default_factory=list)
197
+ strategy: str = "" # single | by_source
198
+ reason: str = ""
199
+
200
+
201
+ def _estimate_intent_tokens(intent_text: str) -> int:
202
+ """保守估算意图文本 token 数。"""
203
+ return max(1, len(intent_text or "") // 3)
204
+
205
+
206
+ def build_source_chunk_plan(
207
+ parsed_intents: list[dict],
208
+ max_sources_per_chunk: int = 5,
209
+ max_tokens_per_chunk: int = 8000,
210
+ ) -> SourceChunkPlan:
211
+ """
212
+ @methoddesc 构建按源分块计划
213
+
214
+ 参数:
215
+ parsed_intents: 解析后的来源意图列表,每个元素含 name/intent
216
+ max_sources_per_chunk: 每个分片最大来源数
217
+ max_tokens_per_chunk: 每个分片最大估算 token 数
218
+
219
+ 返回:
220
+ SourceChunkPlan 分块计划
221
+ """
222
+ n = len(parsed_intents)
223
+ if n == 0:
224
+ return SourceChunkPlan(chunks=[], strategy="single", reason="无来源,无需分片")
225
+
226
+ total_tokens = sum(_estimate_intent_tokens(it.get("intent", "")) for it in parsed_intents)
227
+ if n <= max_sources_per_chunk and total_tokens <= max_tokens_per_chunk:
228
+ return SourceChunkPlan(
229
+ chunks=[
230
+ SourceChunk(
231
+ chunk_id="source_single",
232
+ source_names=[it.get("name") or f"source_{idx + 1}" for idx, it in enumerate(parsed_intents)],
233
+ source_indices=list(range(n)),
234
+ reason=f"来源数 {n}、估算 {total_tokens} token,单次处理",
235
+ )
236
+ ],
237
+ strategy="single",
238
+ reason=f"共 {n} 个来源,单次处理",
239
+ )
240
+
241
+ chunks: list[SourceChunk] = []
242
+ current = SourceChunk(chunk_id="source_chunk_1")
243
+ current_tokens = 0
244
+ for idx, it in enumerate(parsed_intents):
245
+ it_tokens = _estimate_intent_tokens(it.get("intent", ""))
246
+ over_sources = len(current.source_indices) >= max_sources_per_chunk
247
+ over_tokens = current_tokens + it_tokens > max_tokens_per_chunk and current.source_indices
248
+ if over_sources or over_tokens:
249
+ current.reason = f"分片含 {len(current.source_indices)} 个来源,约 {current_tokens} token"
250
+ chunks.append(current)
251
+ current = SourceChunk(chunk_id=f"source_chunk_{len(chunks) + 1}")
252
+ current_tokens = 0
253
+ current.source_indices.append(idx)
254
+ current.source_names.append(it.get("name", f"source_{idx + 1}"))
255
+ current_tokens += it_tokens
256
+
257
+ if current.source_indices:
258
+ current.reason = f"分片含 {len(current.source_indices)} 个来源,约 {current_tokens} token"
259
+ chunks.append(current)
260
+
261
+ return SourceChunkPlan(
262
+ chunks=chunks,
263
+ strategy="by_source",
264
+ reason=f"共 {n} 个来源、约 {total_tokens} token,按源拆分为 {len(chunks)} 个分片",
265
+ )
266
+
267
+
268
+ def source_plan_to_dict(plan: SourceChunkPlan) -> dict[str, Any]:
269
+ """将 SourceChunkPlan 转为可 JSON 序列化的字典。"""
270
+ return {
271
+ "strategy": plan.strategy,
272
+ "reason": plan.reason,
273
+ "chunks": [
274
+ {
275
+ "chunk_id": c.chunk_id,
276
+ "source_names": c.source_names,
277
+ "source_indices": c.source_indices,
278
+ "reason": c.reason,
279
+ }
280
+ for c in plan.chunks
281
+ ],
282
+ }