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,554 @@
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 主循环:LLM 调用 → 工具执行 → 结果回传 → 下一轮
20
+ - 支持最大迭代轮数、取消检查、checkpoint 回调
21
+ - 约定:名为 generate_config 的工具是最终输出工具,调用成功并返回 config 时任务结束
22
+
23
+ 架构设计:
24
+ - AgentExecutor 组合 AIProvider、ToolRegistry、AgentMemory
25
+ - 每轮调用 LLM,若返回 tool_calls 则执行工具
26
+ - 若工具调用中包含 generate_config 且成功返回 config,则以该 config 作为最终结果结束
27
+ - 若 LLM 直接返回可解析为配置 JSON 的文本,也接受为最终结果
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import asyncio
33
+ import logging
34
+ from collections.abc import Callable
35
+ from typing import Any
36
+
37
+ from app.shared.services.llm.providers.base import BaseProvider, ChatMessage, ChatRequest
38
+
39
+ from .memory import AgentMemory
40
+ from .tool_registry import ToolRegistry
41
+ from .types import AgentMetrics, AgentResult, AgentTurn, ToolResult
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ def _tool_call_signature(tc: Any) -> str:
47
+ """提取 tool_call 的去重签名。
48
+
49
+ 用于防止同一动作被重复执行(Ollama 多行重复场景)。
50
+ - id 非空:用 id(OpenAI 等提供稳定 id 的 provider 走快路径)
51
+ - id 为空:用 function.name + arguments 的组合签名(Ollama 的 id 通常为空)
52
+ """
53
+ if not isinstance(tc, dict):
54
+ return ""
55
+ tc_id: str = str(tc.get("id", ""))
56
+ if tc_id:
57
+ return tc_id
58
+ func = tc.get("function", {})
59
+ if not isinstance(func, dict):
60
+ return ""
61
+ name: str = str(func.get("name", ""))
62
+ args: str = str(func.get("arguments", ""))
63
+ return f"{name}:{args}"
64
+
65
+
66
+ def _cross_turn_signature(tc: Any) -> str:
67
+ """跨轮重复检测专用签名:只看 name + arguments,不看 id。
68
+
69
+ 与 _tool_call_signature 的区别:跨轮场景下 id 必然不同(LLM 每次新生成),
70
+ 若用 id 会导致"同工具同参数"的重复调用无法被识别。本函数专注于"做了同样的事"。
71
+
72
+ 用于 P1-2 收敛引导的跨轮重复检测。
73
+ """
74
+ if not isinstance(tc, dict):
75
+ return ""
76
+ func = tc.get("function", {})
77
+ if not isinstance(func, dict):
78
+ return ""
79
+ name: str = str(func.get("name", ""))
80
+ args: str = str(func.get("arguments", ""))
81
+ return f"{name}:{args}"
82
+
83
+
84
+ # 默认系统提示词
85
+ DEFAULT_SYSTEM_PROMPT = """你是一个数据治理专家 Agent,擅长分析数据文件并生成数据验证配置。
86
+ 你可以调用工具来完成任务。每一步请根据观察结果决定下一步行动。
87
+ 当需要输出最终结果时,请调用 generate_config 工具,其返回的 config 会被作为最终结果。"""
88
+
89
+ # 最终输出工具名
90
+ FINAL_OUTPUT_TOOL = "generate_config"
91
+
92
+
93
+ # 防僵死总时长下限(秒):即使单次 IO 超时很小,整体守卫也不低于 5 分钟
94
+ STREAM_GUARD_MIN_TIMEOUT = 300.0
95
+ # 单次 IO 超时 → 整体防僵死上限的放大倍数:多轮工具调用的长回复需要远大于单次 IO 的预算
96
+ STREAM_GUARD_MULTIPLIER = 5
97
+
98
+
99
+ def stream_guard_timeout(provider_timeout: Any) -> float:
100
+ """由 provider 单次 IO 超时推导 Agent 流式调用的整体防僵死上限。
101
+
102
+ 两层超时的分工:
103
+ - provider 内部的 aiohttp connect/read 超时(timeout_seconds,Ollama 默认 60s)
104
+ 管"单次 IO"——一次读不到数据就快速失败,避免单个请求挂死;
105
+ - 本层 wait_for 只防"整体僵死"(流永不结束、取消信号失效),不限制生成时长。
106
+
107
+ 若直接复用 timeout_seconds 作总上限,本地模型多轮长回复会被 60s 掐断;
108
+ 故取 timeout_seconds * 5 且不低于 300s。非实数(mock/未实现 provider)回退 300s。
109
+
110
+ 返回:
111
+ 整体防僵死上限(秒)
112
+ """
113
+ # 必须校验为实数,否则非数值(如 MagicMock)参与运算会产生无意义/非法的 timeout
114
+ if isinstance(provider_timeout, (int, float)):
115
+ return max(provider_timeout * STREAM_GUARD_MULTIPLIER, STREAM_GUARD_MIN_TIMEOUT)
116
+ return STREAM_GUARD_MIN_TIMEOUT
117
+
118
+
119
+ # 进度回调签名:stage, progress(0-1), extra dict
120
+ def default_progress_callback(stage: str, progress: float, extra: dict[str, Any] | None = None) -> None:
121
+ pass
122
+
123
+
124
+ # 默认取消回调
125
+ def default_cancelled_callback() -> bool:
126
+ return False
127
+
128
+
129
+ # 默认 tool result 回调
130
+ def default_on_tool_result(tr: ToolResult) -> None:
131
+ pass
132
+
133
+
134
+ class AgentExecutor:
135
+ """
136
+ @classdesc Agent 执行器
137
+
138
+ 实现规划-执行-观察的循环。
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ provider: BaseProvider,
144
+ registry: ToolRegistry,
145
+ system_prompt: str | None = None,
146
+ max_iterations: int = 5,
147
+ max_tokens: int = 120000,
148
+ progress_callback: Callable[[str, float, dict[str, Any] | None], None] | None = None,
149
+ checkpoint_callback: Callable[[dict[str, Any]], None] | None = None,
150
+ cancelled_callback: Callable[[], bool] | None = None,
151
+ on_tool_result: Callable[[ToolResult], None] | None = None,
152
+ on_chunk: Callable[[str], None] | None = None,
153
+ on_turn: Callable[[int], None] | None = None,
154
+ on_tool_call: Callable[[str, str, int], None] | None = None,
155
+ final_output_tool: str | None = FINAL_OUTPUT_TOOL,
156
+ ):
157
+ """
158
+ @methoddesc 初始化 Agent 执行器
159
+
160
+ 参数:
161
+ provider: AIProvider 配置
162
+ registry: 工具注册表
163
+ system_prompt: 系统提示词,None 使用默认
164
+ max_iterations: 最大迭代轮数
165
+ max_tokens: 记忆最大 token 预算
166
+ progress_callback: 进度回调
167
+ checkpoint_callback: checkpoint 保存回调
168
+ cancelled_callback: 取消检查回调
169
+ on_tool_result: 工具结果回调,用于提取 metrics
170
+ on_chunk: 流式文本增量回调(逐字触发),用于 SSE 流式输出
171
+ on_turn: 新轮次开始回调,参数为轮次序号
172
+ on_tool_call: 工具调用开始回调,参数为 (tool_name, call_id, turn)
173
+ """
174
+ self.provider = provider
175
+ self.registry = registry
176
+ self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
177
+ self.max_iterations = max_iterations
178
+ self.max_tokens = max_tokens
179
+ self.progress_callback = progress_callback or default_progress_callback
180
+ self.checkpoint_callback = checkpoint_callback or (lambda x: None)
181
+ self.cancelled_callback = cancelled_callback or default_cancelled_callback
182
+ self.on_tool_result = on_tool_result or default_on_tool_result
183
+ self.on_chunk = on_chunk or (lambda text: None)
184
+ self.on_turn = on_turn or (lambda turn: None)
185
+ self.on_tool_call = on_tool_call or (lambda name, call_id, turn: None)
186
+ # 最终输出工具名:generation agent 用 "generate_config",chat agent 传 None 跳过最终配置提取
187
+ self.final_output_tool = final_output_tool
188
+ self._chat_service: Any | None = None
189
+ self._memory: AgentMemory | None = None
190
+ # P1-2 跨轮 tool_call 重复检测:记录上一轮的工具调用签名,本轮命中时注入提醒
191
+ self._prev_tool_signatures: set[str] = set()
192
+
193
+ async def run(
194
+ self,
195
+ task_message: str,
196
+ initial_checkpoint: dict[str, Any] | None = None,
197
+ ) -> AgentResult:
198
+ """
199
+ @methoddesc 运行 Agent
200
+
201
+ 参数:
202
+ task_message: 用户任务描述
203
+ initial_checkpoint: 可选的初始 checkpoint 数据,含 messages 时从断点续跑
204
+
205
+ 返回:
206
+ AgentResult 执行结果
207
+ """
208
+ # 续跑:从 checkpoint 恢复 memory,而非新建
209
+ if initial_checkpoint and initial_checkpoint.get("messages"):
210
+ self._memory = AgentMemory.restore_from_checkpoint(initial_checkpoint)
211
+ start_turn = initial_checkpoint.get("turn", 0) + 1
212
+ else:
213
+ self._memory = AgentMemory(self.system_prompt, max_tokens=self.max_tokens)
214
+ self._memory.set_task(task_message)
215
+ start_turn = 1
216
+
217
+ # P1-2:每次 run 重置跨轮签名集合,避免跨任务污染
218
+ self._prev_tool_signatures = set()
219
+
220
+ tools = self.registry.get_definitions()
221
+
222
+ result = AgentResult(success=True, iterations=0)
223
+
224
+ for turn_idx in range(start_turn, self.max_iterations + 1):
225
+ # 取消检查点 1: turn 开始
226
+ if self.cancelled_callback():
227
+ result.success = False
228
+ result.cancelled = True
229
+ result.error = "任务已取消"
230
+ return result
231
+
232
+ self.on_turn(turn_idx)
233
+ self.progress_callback(
234
+ f"agent_turn_{turn_idx}",
235
+ turn_idx / self.max_iterations,
236
+ {"iterations": turn_idx, "max_iterations": self.max_iterations},
237
+ )
238
+
239
+ # P1-2 收敛引导:在最后一轮(达到 max_iterations)提醒 LLM 直接回复,
240
+ # 避免它继续调工具导致循环用尽后直接 fail。reminder 在 get_messages 前注入。
241
+ if turn_idx >= self.max_iterations:
242
+ self._memory.add_system_reminder(
243
+ f"注意:你已用尽全部 {self.max_iterations} 轮工具调用预算。"
244
+ "请立即基于已获取的信息给出最终回复,不要再调用任何工具。"
245
+ )
246
+
247
+ messages = self._memory.get_messages()
248
+ logger.debug(f"Agent turn {turn_idx}, messages={len(messages)}")
249
+
250
+ chat_messages = []
251
+ for m in messages:
252
+ chat_messages.append(
253
+ ChatMessage(
254
+ role=m["role"],
255
+ content=m.get("content"),
256
+ tool_calls=m.get("tool_calls"),
257
+ tool_call_id=m.get("tool_call_id"),
258
+ )
259
+ )
260
+ req = ChatRequest(
261
+ messages=chat_messages,
262
+ model=self.provider.model if hasattr(self.provider, "model") else None,
263
+ temperature=0.3,
264
+ tools=tools,
265
+ tool_choice="auto",
266
+ )
267
+
268
+ # 流式调用: 累积文本 + tool_calls,逐字触发 on_chunk
269
+ # chat_stream 是 async generator function(async def + yield),直接 async for 即可
270
+ content = ""
271
+ raw_tool_calls: list[dict[str, Any]] = []
272
+ cancelled_in_stream = False
273
+ received_any_chunk = False # 追踪是否收到过任何 chunk(区分"空流"与"返回但内容空")
274
+ try:
275
+ # 用 wait_for 包裹整体流式读取,防止 provider 阻塞时取消信号永不触发。
276
+ # 两层超时分工:provider 内部的 connect/read 超时管单次 IO;
277
+ # 本层只防整体僵死,取单次超时的 5 倍且不低于 300s(见 stream_guard_timeout),
278
+ # 不限制生成长度——本地模型多轮长回复不会被 60s 单次超时连坐掐断。
279
+ stream_timeout = stream_guard_timeout(getattr(self.provider, "timeout_seconds", None))
280
+
281
+ async def _consume_stream() -> None:
282
+ nonlocal content, raw_tool_calls, cancelled_in_stream, received_any_chunk
283
+ async for chunk in self.provider.chat_stream(req):
284
+ received_any_chunk = True
285
+ # 取消检查点 2: 流内 chunk 间
286
+ if self.cancelled_callback():
287
+ cancelled_in_stream = True
288
+ return
289
+ if chunk.type == "delta":
290
+ content += chunk.text or ""
291
+ self.on_chunk(chunk.text or "")
292
+ elif chunk.type == "tool_calls" and chunk.tool_calls:
293
+ # 去重:防止 Ollama 多行重复导致同一动作多次执行。
294
+ # 关键:Ollama 的 tool_call id 通常为空,旧守卫 `if tc_id and ...`
295
+ # 对空 id 短路失效,恰在它要防的 Ollama 场景上漏过。
296
+ # 修复:id 非空用 id(快路径);否则用 name+arguments 签名兜底。
297
+ for tc in chunk.tool_calls:
298
+ sig = _tool_call_signature(tc)
299
+ if sig and any(_tool_call_signature(existing) == sig for existing in raw_tool_calls):
300
+ continue
301
+ raw_tool_calls.append(tc)
302
+
303
+ await asyncio.wait_for(_consume_stream(), timeout=stream_timeout)
304
+ except TimeoutError:
305
+ logger.error(f"Agent LLM 流式调用超时({stream_timeout}s)")
306
+ result.success = False
307
+ result.error = f"AI 服务响应超时({stream_timeout}s)"
308
+ return result
309
+ except Exception as e:
310
+ logger.error(f"Agent LLM 流式调用失败: {e}")
311
+ result.success = False
312
+ result.error = f"AI 服务调用失败: {e}"
313
+ return result
314
+
315
+ # 流内取消:保留已累积的 content 到 result(让 cancelled 事件携带 partial reply)
316
+ if cancelled_in_stream:
317
+ result.success = False
318
+ result.cancelled = True
319
+ result.error = "任务已取消"
320
+ result.content = content # 保留部分内容
321
+ return result
322
+
323
+ # 空流检测:provider 没有返回任何 chunk(连空的 delta/tool_calls 都没有)
324
+ # 这才是真正的异常空流,避免注入非法空 assistant 消息导致后续轮次 provider 400 错误
325
+ # 注意:content 为空但有 tool_calls(或收到过空 delta)是合法的,不算空流
326
+ if not received_any_chunk:
327
+ logger.warning(f"Agent 第 {turn_idx} 轮 LLM 返回空响应(无任何 chunk)")
328
+ result.success = False
329
+ result.error = "LLM 返回空响应,请检查模型配置或重试"
330
+ return result
331
+
332
+ tool_calls = [self.registry.parse_tool_call(tc) for tc in raw_tool_calls]
333
+
334
+ # P1-2 跨轮重复检测:若本轮存在与上一轮完全相同的 tool_call(同工具同参数),
335
+ # 为下一轮注入提醒,避免 LLM 反复调同一工具同参数空转。
336
+ # 注意:reminder 追加到 memory 末尾,下一轮 get_messages 时 LLM 可见。
337
+ self._check_repeated_tool_calls(raw_tool_calls)
338
+
339
+ self._memory.add_assistant_message(content=content or None, tool_calls=raw_tool_calls or None)
340
+
341
+ turn = AgentTurn(
342
+ turn=turn_idx,
343
+ content=content,
344
+ tool_calls=tool_calls,
345
+ )
346
+
347
+ if tool_calls:
348
+ # 触发工具调用开始回调
349
+ for tc in tool_calls:
350
+ self.on_tool_call(tc.name, tc.id, turn_idx)
351
+ # 执行工具
352
+ self.progress_callback("agent_executing_tools", turn_idx / self.max_iterations, None)
353
+ tool_results = await self.registry.execute_many(tool_calls)
354
+ turn.tool_results = tool_results
355
+
356
+ final_config = self._extract_final_config(tool_results)
357
+ if final_config is not None:
358
+ # 最终输出工具已生成配置,任务结束
359
+ result.config = final_config
360
+ result.success = True
361
+ result.iterations = turn_idx
362
+ result.content = content
363
+ self._memory.add_turn(turn)
364
+ result.turns.append(turn)
365
+ final_checkpoint = self._make_checkpoint(turn_idx, turn, result.metrics.to_dict())
366
+ try:
367
+ self.checkpoint_callback(final_checkpoint)
368
+ except Exception:
369
+ logger.warning("checkpoint_callback 异常,已忽略", exc_info=True)
370
+ return result
371
+
372
+ for tr in tool_results:
373
+ self.on_tool_result(tr)
374
+ observation = tr.observation
375
+ if isinstance(observation, dict):
376
+ observation_text = self._dict_to_text(observation)
377
+ else:
378
+ observation_text = str(observation)
379
+ # 失败时把 error 回灌给 LLM,否则 tool 消息 content 会是空串,
380
+ # LLM 无法得知失败原因(apply_actions 的 did-you-mean 建议等也会丢失)。
381
+ if not tr.success and tr.error:
382
+ observation_text = (observation_text + "\n" if observation_text else "") + f"[ERROR] {tr.error}"
383
+ self._memory.add_tool_result(tr.call_id, tr.name, observation_text)
384
+
385
+ self._memory.add_turn(turn)
386
+ result.turns.append(turn)
387
+ result.iterations = turn_idx
388
+
389
+ # 更新 checkpoint
390
+ checkpoint = self._make_checkpoint(turn_idx, turn, None)
391
+ try:
392
+ self.checkpoint_callback(checkpoint)
393
+ except Exception:
394
+ logger.warning("checkpoint_callback 异常,已忽略", exc_info=True)
395
+ continue
396
+
397
+ # 没有 tool_calls 且 content 非空,视为最终结果
398
+ if content:
399
+ self._memory.add_turn(turn)
400
+ result.turns.append(turn)
401
+ result.iterations = turn_idx
402
+ result.content = content
403
+
404
+ # 尝试从 content 解析配置
405
+ config = self._try_parse_config(content)
406
+ if config:
407
+ result.config = config
408
+
409
+ final_checkpoint = self._make_checkpoint(turn_idx, turn, result.metrics.to_dict())
410
+ try:
411
+ self.checkpoint_callback(final_checkpoint)
412
+ except Exception:
413
+ logger.warning("checkpoint_callback 异常,已忽略", exc_info=True)
414
+ break
415
+
416
+ # content 为空且无 tool_calls,记录 turn 后继续
417
+ self._memory.add_turn(turn)
418
+ result.turns.append(turn)
419
+ result.iterations = turn_idx
420
+
421
+ # 如果达到最大轮数仍未结束。
422
+ # 注意:最后一轮以纯文本收敛(上方 break 的正常路径)时 content 非空且 config 为 None,
423
+ # 这是 chat agent 的合法结局,不能误判为失败——只有"既无 config 也无文本产出"才算用尽预算。
424
+ if result.iterations >= self.max_iterations and result.config is None and not result.content:
425
+ result.success = False
426
+ result.error = f"达到最大迭代轮数 {self.max_iterations},仍未获得最终结果"
427
+
428
+ return result
429
+
430
+ def _check_repeated_tool_calls(self, raw_tool_calls: list[dict[str, Any]]) -> None:
431
+ """P1-2 跨轮重复调用检测。
432
+
433
+ 比对本轮 tool_calls 与上一轮的签名集合:若存在完全相同的调用(同工具同参数),
434
+ 为下一轮注入提醒,避免 LLM 反复调用同一工具同一参数空转浪费预算。
435
+ 无论是否命中,都把本轮签名更新到集合,供下一轮比对。
436
+
437
+ 参数:
438
+ raw_tool_calls: 本轮 LLM 返回的原始 tool_calls(OpenAI dict 格式)
439
+ """
440
+ # _memory 类型为 AgentMemory | None,但在 run() 中已保证调用本方法前完成初始化;
441
+ # 此处防御性判断同时为 mypy 提供 None 收敛,与 _create_checkpoint 中的写法保持一致。
442
+ if not self._memory or not raw_tool_calls:
443
+ return
444
+ current_signatures = {_cross_turn_signature(tc) for tc in raw_tool_calls}
445
+ # 去掉空签名(无法识别的 tool_call 不参与检测)
446
+ current_signatures.discard("")
447
+ if not current_signatures:
448
+ return
449
+
450
+ repeated = current_signatures & self._prev_tool_signatures
451
+ if repeated:
452
+ # 构造友好提示,列出重复的工具名(从签名解析,格式 "name:args")
453
+ names = sorted({sig.split(":", 1)[0] for sig in repeated if ":" in sig})
454
+ tool_list = "、".join(names) if names else "同一工具"
455
+ self._memory.add_system_reminder(
456
+ f"提示:你刚才已经调用过 {tool_list}(相同参数),请勿重复调用同一工具同一参数。"
457
+ "请直接使用已有的工具结果继续推理,或在必要时更换参数/工具。"
458
+ )
459
+
460
+ # 更新签名集合供下一轮比对
461
+ self._prev_tool_signatures = current_signatures
462
+
463
+ def _extract_final_config(self, tool_results: list[ToolResult]) -> dict[str, Any] | None:
464
+ """从工具结果中提取最终配置(final_output_tool 指定工具的返回值)。
465
+
466
+ 若 final_output_tool 为 None(如 chat agent),直接返回 None,跳过最终配置提取。
467
+ """
468
+ if not self.final_output_tool:
469
+ return None
470
+ for tr in tool_results:
471
+ if tr.name != self.final_output_tool:
472
+ continue
473
+ if not tr.success:
474
+ continue
475
+ observation = tr.observation
476
+ if isinstance(observation, dict) and observation.get("success"):
477
+ config = observation.get("config")
478
+ if isinstance(config, dict):
479
+ return config
480
+ return None
481
+
482
+ def _make_checkpoint(
483
+ self,
484
+ turn: int,
485
+ agent_turn: AgentTurn,
486
+ metrics: dict[str, Any] | None,
487
+ ) -> dict[str, Any]:
488
+ """构造 checkpoint(含完整 messages,支持续跑)。"""
489
+ memory_checkpoint = self._memory.create_checkpoint() if self._memory else {}
490
+ return {
491
+ **memory_checkpoint,
492
+ "turn": turn,
493
+ "content": agent_turn.content,
494
+ "tool_calls": [{"id": c.id, "name": c.name, "arguments": c.arguments} for c in agent_turn.tool_calls],
495
+ "tool_results": [
496
+ {
497
+ "call_id": r.call_id,
498
+ "name": r.name,
499
+ "success": r.success,
500
+ "observation": r.observation if isinstance(r.observation, dict) else str(r.observation),
501
+ "error": r.error,
502
+ }
503
+ for r in agent_turn.tool_results
504
+ ],
505
+ "metrics": metrics,
506
+ }
507
+
508
+ @staticmethod
509
+ def _dict_to_text(data: dict[str, Any]) -> str:
510
+ """将字典转为文本。"""
511
+ import json
512
+
513
+ try:
514
+ return json.dumps(data, ensure_ascii=False)
515
+ except (TypeError, ValueError):
516
+ return str(data)
517
+
518
+ @staticmethod
519
+ def _try_parse_config(content: str | None) -> dict[str, Any] | None:
520
+ """尝试从文本中提取 JSON 配置。"""
521
+ if not content:
522
+ return None
523
+ import json
524
+ import re
525
+
526
+ # 尝试找到 Markdown JSON 代码块
527
+ match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL)
528
+ text = match.group(1).strip() if match else content
529
+
530
+ start = text.find("{")
531
+ if start == -1:
532
+ return None
533
+ balance = 0
534
+ for i, char in enumerate(text[start:], start=start):
535
+ if char == "{":
536
+ balance += 1
537
+ elif char == "}":
538
+ balance -= 1
539
+ if balance == 0:
540
+ try:
541
+ parsed: Any = json.loads(text[start : i + 1])
542
+ if isinstance(parsed, dict):
543
+ return parsed
544
+ except json.JSONDecodeError:
545
+ return None
546
+ return None
547
+
548
+ @staticmethod
549
+ def build_metrics_from_issues(issues: list[dict[str, Any]]) -> AgentMetrics:
550
+ """根据校验问题构造 metrics。"""
551
+ metrics = AgentMetrics()
552
+ metrics.issues = issues
553
+ metrics.failed_rules = len(issues)
554
+ return metrics