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,281 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """@fileoverview AI 流式 SSE 端点
17
+
18
+ 把后端流式内核(StreamingOrchestrator + EventJournal + sse_event_stream)
19
+ 暴露为 HTTP SSE 端点。
20
+
21
+ 端点:
22
+ - POST /ai/chat/stream: 聊天流式,包装 ChatAgentRunner
23
+ - POST /ai/jobs/{job_id}/cancel: 取消正在运行的 job(软取消)
24
+
25
+ 设计要点:
26
+ - 每个流式请求: 创建 job_id → 启动 orchestrator 后台 task → 返回 SSE StreamingResponse
27
+ - 取消信号: 内存中 _cancel_events 字典(job_id → asyncio.Event)
28
+ - Last-Event-ID: 仅作为本连接内 journal 回放的起始游标(§2.8);不支持跨连接断线续传——
29
+ 每次请求都新建 job_id 与 journal,重连等价于重新发起会话(真续传需 job 持久化+按 job_id 重连端点,属未来 feature)
30
+ - 资源清理: orchestrator 结束后注销 cancel_event,防止内存泄漏
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import asyncio
36
+ import logging
37
+ import threading
38
+ import uuid
39
+ from collections.abc import AsyncIterator
40
+ from typing import Any
41
+
42
+ from fastapi import Header, HTTPException
43
+ from fastapi.responses import StreamingResponse
44
+
45
+ from app.shared.services.ai.streaming.event_journal import EventJournal
46
+ from app.shared.services.ai.streaming.orchestrator import StreamingOrchestrator
47
+ from app.shared.services.ai.streaming.pending_interaction_store import (
48
+ ConfirmController,
49
+ InteractionController,
50
+ get_global_pending_interaction_store,
51
+ )
52
+ from app.shared.services.ai.streaming.sse_response import sse_event_stream
53
+ from app.shared.services.llm.config import loader
54
+ from app.shared.services.llm.providers.registry import create
55
+
56
+ from .models import AiChatConfirmRequest, AiChatRequest, AiChatRespondRequest
57
+ from .router import router
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+ # 内存中保存运行中 job 的取消信号: job_id → asyncio.Event
62
+ # orchestrator 结束后通过 _unregister_cancel_event 移除,防止内存泄漏
63
+ _cancel_events: dict[str, asyncio.Event] = {}
64
+ _cancel_events_lock = threading.Lock()
65
+
66
+ # 后台 orchestrator task 的强引用集合:防止 task 被 GC 回收(Python asyncio 已知行为)
67
+ # task 完成后通过 add_done_callback 自动从集合移除
68
+ _background_tasks: set[asyncio.Task[None]] = set()
69
+
70
+ # journal 存储目录基础(按项目 config_path 分片),复用 AgentJobStorage 的 .precis 约定
71
+ # 实际路径在端点中按 project_path 拼接
72
+
73
+
74
+ def _unregister_cancel_event(job_id: str) -> None:
75
+ """@methoddesc 从 _cancel_events 移除已完成 job 的取消信号(防内存泄漏)。"""
76
+ with _cancel_events_lock:
77
+ _cancel_events.pop(job_id, None)
78
+
79
+
80
+ def _journal_dir_for(project_path: str | None) -> str:
81
+ """@methoddesc 根据项目路径返回 journal 目录(复用项目本地 .precis 约定)。
82
+
83
+ 无 project_path 时回退到用户级 ~/.precis。
84
+ """
85
+ import os
86
+
87
+ if project_path:
88
+ return os.path.join(project_path, ".precis", "stream_jobs")
89
+ return os.path.join(os.path.expanduser("~"), ".precis", "stream_jobs")
90
+
91
+
92
+ @router.post("/chat/stream", summary="AI 聊天流式接口(SSE)")
93
+ async def chat_stream(
94
+ request: AiChatRequest,
95
+ x_project_config_path: str | None = Header(None),
96
+ last_event_id: int = Header(default=0, alias="Last-Event-ID"),
97
+ ) -> StreamingResponse:
98
+ """@methoddesc 聊天流式端点
99
+
100
+ 流程:
101
+ 1. 加载 Provider 配置并实例化
102
+ 2. 创建 job_id、EventJournal、cancel_event、event_queue
103
+ 3. 启动后台 orchestrator task(并发执行 run_chat)
104
+ 4. 返回 SSE StreamingResponse,从 journal 回放(本连接内) + 实时推送队列
105
+
106
+ 参数:
107
+ request: AiChatRequest(复用现有聊天请求模型)
108
+ x_project_config_path: 项目配置路径 header
109
+ last_event_id: 本连接内 journal 回放的起始事件号(非断线续传,见 §2.8 声明)
110
+
111
+ 返回:
112
+ StreamingResponse(media_type=text/event-stream)
113
+ """
114
+ config = loader.load()
115
+
116
+ # 校验项目路径(修复 #10:防止 X-Project-Config-Path 任意目录读写)
117
+ from .utils import validate_project_path
118
+
119
+ safe_project_path = validate_project_path(x_project_config_path)
120
+
121
+ # 获取默认 provider(与 /chat 端点逻辑一致)
122
+ provider_id = config.defaults.get("chat")
123
+ if not provider_id:
124
+ raise HTTPException(400, detail="尚未设置默认的 AI 模型,请先在设置中选择一个 AI 模型")
125
+
126
+ provider_cfg = next((p for p in config.providers if p.id == provider_id), None)
127
+ if not provider_cfg:
128
+ raise HTTPException(404, detail=f"未找到对应的 AI 模型配置,可能已被删除(ID: {provider_id})")
129
+
130
+ # 实例化 provider
131
+ try:
132
+ provider = create(provider_cfg)
133
+ except ImportError as e:
134
+ raise HTTPException(502, detail=f"AI 服务初始化失败,请检查模型配置是否正确({e})") from e
135
+
136
+ # 创建 job 资源
137
+ job_id = f"stream_{uuid.uuid4().hex[:12]}"
138
+ journal_dir = _journal_dir_for(safe_project_path)
139
+ journal = EventJournal(job_id=job_id, journal_dir=journal_dir)
140
+
141
+ cancel_event = asyncio.Event()
142
+ with _cancel_events_lock:
143
+ _cancel_events[job_id] = cancel_event
144
+
145
+ # 上下文节点与历史转换(与 /chat 端点一致)
146
+ context_nodes = [node.model_dump() for node in request.context.selectedNodes]
147
+ # 画布节点快照(供 read_canvas 工具查询画布真实状态)
148
+ canvas_nodes = [node.model_dump() for node in request.context.canvasNodes]
149
+ history = [{"role": h.role, "content": h.content} for h in (request.history or [])]
150
+
151
+ # 实时事件队列: orchestrator emit 时推入, sse_event_stream 消费
152
+ event_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
153
+
154
+ orchestrator = StreamingOrchestrator(
155
+ job_id=job_id,
156
+ journal=journal,
157
+ cancel_event=cancel_event,
158
+ event_queue=event_queue,
159
+ )
160
+
161
+ async def _run_orchestrator() -> None:
162
+ """后台执行 run_chat,结束后注销 cancel_event。"""
163
+ try:
164
+ await orchestrator.run_chat(
165
+ message=request.message,
166
+ history=history,
167
+ provider=provider,
168
+ project_path=safe_project_path,
169
+ context_nodes=context_nodes,
170
+ canvas_nodes=canvas_nodes,
171
+ )
172
+ finally:
173
+ _unregister_cancel_event(job_id)
174
+
175
+ # 启动后台 task:持有强引用防 GC,完成回调中移除
176
+ task = asyncio.create_task(_run_orchestrator())
177
+ _background_tasks.add(task)
178
+ task.add_done_callback(_background_tasks.discard)
179
+
180
+ # 返回 SSE 流: 先回放 journal(本连接内,自 last_event_id 起), 再实时推送队列
181
+ async def _sse_generator() -> AsyncIterator[str]:
182
+ try:
183
+ async for frame in sse_event_stream(
184
+ journal=journal,
185
+ last_event_id=last_event_id,
186
+ event_queue=event_queue,
187
+ ):
188
+ yield frame
189
+ finally:
190
+ # 客户端断连时(StreamingResponse 被取消),通知后台 task 停止
191
+ # 这让 executor 在下一个取消检查点中断,避免后台 task 泄漏
192
+ cancel_event.set()
193
+
194
+ return StreamingResponse(_sse_generator(), media_type="text/event-stream")
195
+
196
+
197
+ @router.post("/jobs/{job_id}/cancel", summary="取消正在运行的 AI job(软取消)")
198
+ async def cancel_job(job_id: str) -> dict[str, str]:
199
+ """@methoddesc 取消端点(软取消)
200
+
201
+ 设置 cancel_event 后,orchestrator/executor 在下一个检查点(turn 开始/chunk 间)中断。
202
+ 已落盘的 apply_actions 改动保留,前端轨迹如实显示已执行步数。
203
+ 同步 resolve 挂起的 apply_actions 确认门为 reject。
204
+
205
+ 参数:
206
+ job_id: 任务 ID
207
+
208
+ 返回:
209
+ {"status": "cancelling"} 或 {"status": "not_found"}
210
+ """
211
+ with _cancel_events_lock:
212
+ ev = _cancel_events.get(job_id)
213
+ if ev is not None:
214
+ ev.set()
215
+ # 同步 resolve 该 job 下所有挂起的交互门(apply→reject, ask→skipped)。
216
+ # store 同时持有两类 controller,resolve 签名不同,按 isinstance 分别处理。
217
+ pending_store = get_global_pending_interaction_store()
218
+ for controller in pending_store.pop_by_job_prefix(job_id):
219
+ if not controller.is_resolved:
220
+ if isinstance(controller, ConfirmController):
221
+ await controller.resolve("reject")
222
+ elif isinstance(controller, InteractionController):
223
+ await controller.resolve({"skipped": True, "reason": "cancelled"})
224
+ return {"status": "cancelling"}
225
+ return {"status": "not_found"}
226
+
227
+
228
+ @router.post("/chat/{job_id}/confirm", summary="确认/拒绝挂起的 apply_actions 改动")
229
+ async def confirm_apply(job_id: str, request: AiChatConfirmRequest) -> dict[str, str]:
230
+ """@methoddesc 确认端点
231
+
232
+ 用户在前端点击"确认"或"拒绝"后调用。
233
+ decide 后 resolve ConfirmController 唤醒挂起的 apply_actions 协程。
234
+
235
+ 参数:
236
+ job_id: 任务 ID
237
+ request: 含 decision 字段("confirm" 或 "reject")
238
+
239
+ 返回:
240
+ {"status": "resolved", "decision": "confirm"} 或 404
241
+ """
242
+ pending_store = get_global_pending_interaction_store()
243
+ # 优先用 apply_id 精确查找;为空时回退到 job 维度(兼容单 apply 场景)
244
+ lookup_key = request.apply_id if request.apply_id else job_id
245
+ controller = pending_store.get(lookup_key)
246
+ if controller is None:
247
+ raise HTTPException(404, detail="无挂起的改动(可能已决策或任务已结束)")
248
+ # /confirm 只处理 apply 确认(ConfirmController);若拿到 InteractionController 说明 key 冲突,拒绝
249
+ if not isinstance(controller, ConfirmController):
250
+ raise HTTPException(409, detail="该交互不是 apply 确认类型,无法用 /confirm 处理")
251
+ if controller.is_resolved:
252
+ return {"status": "already_resolved", "decision": controller.decision or "unknown"}
253
+ await controller.resolve(request.decision)
254
+ return {"status": "resolved", "decision": request.decision}
255
+
256
+
257
+ @router.post("/chat/{job_id}/respond", summary="回答 agent 的 ask_user 提问")
258
+ async def respond_to_ask(job_id: str, request: AiChatRespondRequest) -> dict[str, Any]:
259
+ """@methoddesc 回答端点
260
+
261
+ 用户在前端 AskUserCard 提交回答后调用。
262
+ resolve InteractionController 唤醒挂起的 ask_user 协程。
263
+
264
+ 参数:
265
+ job_id: 任务 ID
266
+ request: 含 ask_id 和 response 字段
267
+
268
+ 返回:
269
+ {"ok": true, "already_resolved": false} 或 404(ask_id 不存在)/ 409(类型不符)
270
+ """
271
+ pending_store = get_global_pending_interaction_store()
272
+ controller = pending_store.get(request.ask_id)
273
+ if controller is None:
274
+ raise HTTPException(404, detail="提问已过期或不存在(可能已回答或任务已结束)")
275
+ # /respond 只处理 ask 交互(InteractionController);若拿到 ConfirmController 说明 key 冲突,拒绝
276
+ if not isinstance(controller, InteractionController):
277
+ raise HTTPException(409, detail="该交互不是 ask 提问类型,无法用 /respond 处理")
278
+ if controller.is_resolved:
279
+ return {"ok": True, "already_resolved": True}
280
+ await controller.resolve(request.response)
281
+ return {"ok": True, "already_resolved": False}
@@ -0,0 +1,182 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview AI 路由通用工具端点模块
18
+
19
+ 功能概述:
20
+ - 提供路径展开工具,支持目录递归、通配符、相对路径解析
21
+ - 用于 AI 配置生成前批量收集数据文件路径
22
+ - 支持基于 X-Project-Config-Path 解析相对路径
23
+
24
+ 架构设计:
25
+ - 纯工具函数,不依赖具体 AI 业务逻辑
26
+ - 自动过滤隐藏目录和常见非数据目录
27
+ - 支持的数据文件类型:.xlsx, .xls, .csv, .json, .jsonl
28
+
29
+ 输入示例:
30
+ POST /ai/utils/expand-paths
31
+ ["data/", "*.csv", "./users.xlsx"]
32
+
33
+ 输出示例:
34
+ [
35
+ "D:/project/data/users.xlsx",
36
+ "D:/project/data/orders.csv"
37
+ ]
38
+ """
39
+
40
+ import os
41
+
42
+ from fastapi import Header
43
+
44
+ from .router import router
45
+
46
+ # 支持的数据文件扩展名
47
+ _DATA_EXTENSIONS = {".xlsx", ".xls", ".csv", ".json", ".jsonl"}
48
+
49
+ # 递归遍历时跳过的目录
50
+ _SKIP_DIRS = {"node_modules", "__pycache__", ".git", ".venv", "venv", ".idea"}
51
+
52
+
53
+ def validate_project_path(raw_path: str | None) -> str:
54
+ """校验 X-Project-Config-Path Header 值是否为合法 Precis 项目根。
55
+
56
+ B-sec3: 现委托 app.api.dependencies._validate_project_root(单一事实源),
57
+ 与通用依赖 get_project_config_path 共用同一套校验,消除双套不一致。
58
+
59
+ 参数:
60
+ raw_path: Header 原始值
61
+
62
+ 返回:
63
+ 规范化后的绝对路径
64
+
65
+ 抛出:
66
+ HTTPException(400): 校验失败
67
+ """
68
+ from app.api.dependencies import _validate_project_root
69
+
70
+ return _validate_project_root(raw_path)
71
+
72
+
73
+ _MAX_DEPTH = 5
74
+ _MAX_FILES = 1000
75
+
76
+
77
+ def _expand_directory(dir_path: str, max_depth: int = _MAX_DEPTH) -> list[str]:
78
+ """递归展开目录,收集所有数据文件路径
79
+
80
+ :param dir_path: 目录绝对路径
81
+ :param max_depth: 最大递归深度
82
+ :return: 数据文件路径列表(去重、排序)
83
+ """
84
+ result = []
85
+ for root, dirs, files in os.walk(dir_path):
86
+ depth = root[len(dir_path) :].count(os.sep)
87
+ if depth >= max_depth:
88
+ dirs[:] = []
89
+ continue
90
+ dirs[:] = [d for d in dirs if not d.startswith(".") and d not in _SKIP_DIRS]
91
+ for fname in files:
92
+ ext = os.path.splitext(fname)[1].lower()
93
+ if ext in _DATA_EXTENSIONS:
94
+ result.append(os.path.join(root, fname))
95
+ if len(result) >= _MAX_FILES:
96
+ return result
97
+ return sorted(result)
98
+
99
+
100
+ def _resolve_path(path: str, config_path: str | None) -> str:
101
+ """解析路径:如果为相对路径,则基于项目配置目录拼接
102
+
103
+ :param path: 原始路径(可能是相对或绝对)
104
+ :param config_path: 项目配置目录(绝对路径),用于解析相对路径
105
+ :return: 绝对路径
106
+ """
107
+ # 如果已经是绝对路径,直接返回
108
+ if os.path.isabs(path):
109
+ return path
110
+
111
+ # 如果有配置路径,基于配置路径解析相对路径
112
+ if config_path:
113
+ base = os.path.dirname(config_path) if os.path.isfile(config_path) else config_path
114
+ return os.path.normpath(os.path.join(base, path))
115
+
116
+ # 兜底:返回原路径
117
+ return path
118
+
119
+
120
+ @router.post(
121
+ "/utils/expand-paths",
122
+ response_model=list[str],
123
+ summary="展开路径列表",
124
+ responses={
125
+ 500: {"description": "服务器内部错误"},
126
+ },
127
+ )
128
+ def expand_paths(
129
+ paths: list[str],
130
+ x_project_config_path: str | None = Header(None, alias="X-Project-Config-Path"),
131
+ ) -> list[str]:
132
+ """展开路径列表
133
+
134
+ 支持以下展开方式:
135
+ - 目录递归展开:自动遍历目录及其子目录,收集所有数据文件
136
+ - 通配符展开:支持 * 和 ? 通配符
137
+ - 相对路径解析:基于 X-Project-Config-Path 解析相对路径
138
+
139
+ 数据文件类型:.xlsx, .xls, .csv, .json, .jsonl
140
+ """
141
+ result = []
142
+ seen = set()
143
+
144
+ for raw_path in paths:
145
+ # 1. 解析为绝对路径
146
+ path = _resolve_path(raw_path, x_project_config_path)
147
+
148
+ # 2. 如果是目录,递归展开
149
+ if os.path.isdir(path):
150
+ for file_path in _expand_directory(path):
151
+ norm = os.path.normpath(file_path)
152
+ if norm not in seen:
153
+ seen.add(norm)
154
+ result.append(norm)
155
+ continue
156
+
157
+ # 3. 如果是具体文件,直接保留
158
+ if os.path.isfile(path):
159
+ norm = os.path.normpath(path)
160
+ if norm not in seen:
161
+ seen.add(norm)
162
+ result.append(norm)
163
+ continue
164
+
165
+ # 4. 通配符展开
166
+ if "*" in raw_path or "?" in raw_path:
167
+ import glob
168
+
169
+ for matched in glob.glob(path):
170
+ if os.path.isfile(matched):
171
+ norm = os.path.normpath(matched)
172
+ if norm not in seen:
173
+ seen.add(norm)
174
+ result.append(norm)
175
+ elif os.path.isdir(matched):
176
+ for file_path in _expand_directory(matched):
177
+ norm = os.path.normpath(file_path)
178
+ if norm not in seen:
179
+ seen.add(norm)
180
+ result.append(norm)
181
+
182
+ return result
@@ -0,0 +1,34 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview 核心路由模块包入口
18
+
19
+ 功能概述:
20
+ - 标记 backend/app/api/routers/core 目录为 Python 包
21
+ - 聚合正则工具、报告通知、白名单管理、工作区配置等核心路由
22
+
23
+ 架构设计:
24
+ - 各子模块独立定义 APIRouter 实例
25
+ - 通过 api/routers/__init__.py 统一导入并注册到主应用
26
+ - 核心路由提供不依赖具体项目配置的基础服务能力
27
+
28
+ 输入示例:
29
+ from app.api.routers.core.regex import router as regex_router
30
+ from app.api.routers.core.reporting import router as reporting_router
31
+
32
+ 输出示例:
33
+ 无直接输出,仅作为包标记
34
+ """
@@ -0,0 +1,188 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # Copyright 2026 Precis Team
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """
17
+ @fileoverview 连接规则 API 路由模块
18
+
19
+ 功能概述:
20
+ - 提供节点连接规则的读取、保存和重置接口
21
+ - 定义节点类型和 handles 之间的允许连接关系
22
+ - 支持 strict(严格)和 loose(宽松)两种验证模式
23
+
24
+ 架构设计:
25
+ - 规则持久化在项目根目录的 connection-rules.precis.yaml
26
+ - 使用 Pydantic 模型验证规则结构和字段
27
+ - 不存在的规则文件返回空规则列表
28
+
29
+ 输入示例:
30
+ PUT /connection-rules
31
+ {
32
+ "version": "1.0",
33
+ "rules": [
34
+ {
35
+ "id": "schema_to_constraint",
36
+ "name": "Schema -> Constraint",
37
+ "source": {"node_types": ["SchemaNode"]},
38
+ "target": {"node_types": ["ConstraintNode"]}
39
+ }
40
+ ]
41
+ }
42
+
43
+ 输出示例:
44
+ {"message": "连接规则已成功保存!"}
45
+ """
46
+
47
+ import logging
48
+ import os
49
+ from pathlib import Path
50
+
51
+ from fastapi import APIRouter, Depends, HTTPException
52
+
53
+ from app.api.dependencies import get_project_config_path
54
+ from app.api.models import StandardResponse
55
+ from app.api.models.connection_rules import ConnectionRulesModel
56
+ from app.shared.core.io.yaml import read_yaml, write_yaml_atomic
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+ router = APIRouter(
61
+ prefix="/api/latest/connection-rules",
62
+ tags=["Connection Rules"],
63
+ )
64
+
65
+ CONNECTION_RULES_FILENAME = "connection-rules.precis.yaml"
66
+
67
+
68
+ def _rules_path(config_path: str) -> str:
69
+ return os.path.join(config_path, CONNECTION_RULES_FILENAME)
70
+
71
+
72
+ @router.get(
73
+ "",
74
+ response_model=ConnectionRulesModel,
75
+ summary="获取当前项目的连接规则",
76
+ responses={
77
+ 500: {"description": "加载连接规则失败"},
78
+ },
79
+ )
80
+ def get_connection_rules(config_path: str = Depends(get_project_config_path)) -> ConnectionRulesModel:
81
+ """
82
+ 获取当前项目的连接规则。
83
+
84
+ 使用场景:
85
+ - 前端初始化画布时加载连接规则,用于限制节点间允许的连接关系
86
+ - 用户自定义连接规则后重新加载
87
+
88
+ 副作用:
89
+ - 如果规则文件不存在,返回默认空规则列表(不报错),保证前端始终能拿到有效数据
90
+ - 如果解析失败,返回 500 错误并附带详细原因
91
+
92
+ 参数:
93
+ config_path: 项目配置根目录(通过 Depends 注入)
94
+
95
+ 返回:
96
+ ConnectionRulesModel: 连接规则集合,包含版本号和规则列表
97
+ """
98
+ rules_file_path = _rules_path(config_path)
99
+
100
+ if not os.path.isfile(rules_file_path):
101
+ return ConnectionRulesModel(version="1.0", rules=[])
102
+
103
+ try:
104
+ data = read_yaml(Path(rules_file_path))
105
+ if not data:
106
+ return ConnectionRulesModel(version="1.0", rules=[])
107
+
108
+ return ConnectionRulesModel(**data)
109
+ except Exception as e:
110
+ logger.error(f"加载连接规则失败: {e}")
111
+ raise HTTPException(status_code=500, detail=f"加载连接规则失败: {str(e)}")
112
+
113
+
114
+ @router.put(
115
+ "",
116
+ response_model=StandardResponse,
117
+ summary="保存当前项目的连接规则",
118
+ responses={
119
+ 500: {"description": "保存连接规则失败"},
120
+ },
121
+ )
122
+ def save_connection_rules(
123
+ rules: ConnectionRulesModel, config_path: str = Depends(get_project_config_path)
124
+ ) -> dict[str, str]:
125
+ """
126
+ 保存当前项目的连接规则。
127
+
128
+ 使用场景:
129
+ - 用户在设置面板中修改连接规则后保存
130
+ - 前端通过可视化编辑器调整节点连接限制后持久化
131
+
132
+ 副作用:
133
+ - 直接覆盖写入 connection-rules.precis.yaml 文件
134
+ - 使用 exclude_unset=True 避免将未设置的字段写入文件,保持 YAML 整洁
135
+
136
+ 参数:
137
+ rules: 新的连接规则集合
138
+ config_path: 项目配置根目录(通过 Depends 注入)
139
+
140
+ 返回:
141
+ StandardResponse: 操作结果消息
142
+ """
143
+ rules_file_path = _rules_path(config_path)
144
+
145
+ try:
146
+ data = rules.model_dump(exclude_unset=True)
147
+ write_yaml_atomic(Path(rules_file_path), data)
148
+ return {"message": "连接规则已成功保存!"}
149
+ except Exception as e:
150
+ logger.error(f"保存连接规则失败: {e}")
151
+ raise HTTPException(status_code=500, detail=f"保存连接规则失败: {str(e)}")
152
+
153
+
154
+ @router.post(
155
+ "/reset",
156
+ response_model=StandardResponse,
157
+ summary="重置连接规则为默认值",
158
+ responses={
159
+ 500: {"description": "重置连接规则失败"},
160
+ },
161
+ )
162
+ def reset_connection_rules(config_path: str = Depends(get_project_config_path)) -> dict[str, str]:
163
+ """
164
+ 重置连接规则为默认值。
165
+
166
+ 使用场景:
167
+ - 用户自定义规则后出现问题,需要恢复系统默认的连接规则
168
+ - 清理项目中的连接规则配置
169
+
170
+ 副作用:
171
+ - 删除项目根目录下的 connection-rules.precis.yaml 文件
172
+ - 删除后,前端应回退到内置的默认连接规则
173
+
174
+ 参数:
175
+ config_path: 项目配置根目录(通过 Depends 注入)
176
+
177
+ 返回:
178
+ StandardResponse: 操作结果消息
179
+ """
180
+ rules_file_path = _rules_path(config_path)
181
+
182
+ try:
183
+ if os.path.isfile(rules_file_path):
184
+ os.remove(rules_file_path)
185
+ return {"message": "连接规则已重置为默认值"}
186
+ except Exception as e:
187
+ logger.error(f"重置连接规则失败: {e}")
188
+ raise HTTPException(status_code=500, detail=f"重置连接规则失败: {str(e)}")