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.
- app/__init__.py +16 -0
- app/api/__init__.py +32 -0
- app/api/dependencies.py +185 -0
- app/api/main.py +301 -0
- app/api/middleware/__init__.py +34 -0
- app/api/middleware/exception_handler.py +126 -0
- app/api/middleware/request_logging.py +97 -0
- app/api/middleware/token_auth.py +215 -0
- app/api/models/__init__.py +151 -0
- app/api/models/connection_rules.py +98 -0
- app/api/models/files.py +99 -0
- app/api/models/full_validation.py +554 -0
- app/api/models/project.py +117 -0
- app/api/models/projects.py +80 -0
- app/api/models/schema.py +121 -0
- app/api/models/v2_responses.py +46 -0
- app/api/models/validation.py +303 -0
- app/api/models/workspace.py +118 -0
- app/api/routers/__init__.py +56 -0
- app/api/routers/ai/__init__.py +36 -0
- app/api/routers/ai/chat.py +243 -0
- app/api/routers/ai/generate.py +156 -0
- app/api/routers/ai/hardware.py +199 -0
- app/api/routers/ai/jobs.py +544 -0
- app/api/routers/ai/migrate.py +471 -0
- app/api/routers/ai/models.py +352 -0
- app/api/routers/ai/ollama.py +108 -0
- app/api/routers/ai/providers.py +491 -0
- app/api/routers/ai/router.py +44 -0
- app/api/routers/ai/stream.py +281 -0
- app/api/routers/ai/utils.py +182 -0
- app/api/routers/core/__init__.py +34 -0
- app/api/routers/core/connection_rules.py +188 -0
- app/api/routers/core/data_sources.py +374 -0
- app/api/routers/core/regex.py +333 -0
- app/api/routers/core/reporting.py +263 -0
- app/api/routers/files/__init__.py +24 -0
- app/api/routers/files/ops.py +179 -0
- app/api/routers/files/transfer.py +101 -0
- app/api/routers/preview/__init__.py +35 -0
- app/api/routers/preview/content_mode.py +264 -0
- app/api/routers/preview/header_row.py +154 -0
- app/api/routers/preview/models.py +81 -0
- app/api/routers/preview/path_mode.py +440 -0
- app/api/routers/preview/router.py +30 -0
- app/api/routers/project/__init__.py +61 -0
- app/api/routers/project/base.py +111 -0
- app/api/routers/project/constraint.py +345 -0
- app/api/routers/project/full_config.py +411 -0
- app/api/routers/project/full_config_writer.py +327 -0
- app/api/routers/project/helpers.py +167 -0
- app/api/routers/project/inspection_fix.py +330 -0
- app/api/routers/project/manifest.py +605 -0
- app/api/routers/project/models.py +177 -0
- app/api/routers/project/pattern.py +268 -0
- app/api/routers/project/regex.py +358 -0
- app/api/routers/project/scanner.py +157 -0
- app/api/routers/project/schema.py +567 -0
- app/api/routers/project/schema_helpers.py +149 -0
- app/api/routers/project/settings.py +399 -0
- app/api/routers/project/template.py +325 -0
- app/api/routers/project/validation.py +227 -0
- app/api/routers/project/view.py +177 -0
- app/api/routers/project/workspaces.py +167 -0
- app/api/routers/projects/__init__.py +25 -0
- app/api/routers/projects/create.py +121 -0
- app/api/routers/projects/open.py +103 -0
- app/api/routers/projects/scan.py +100 -0
- app/api/routers/validation/__init__.py +39 -0
- app/api/routers/validation/common.py +263 -0
- app/api/routers/validation/content_mode.py +401 -0
- app/api/routers/validation/history.py +235 -0
- app/api/routers/validation/inline_mode.py +196 -0
- app/api/routers/validation/path_mode.py +314 -0
- app/api/routers/validation/router.py +51 -0
- app/api/services/full_validation_response_builder.py +412 -0
- app/api/services/io_error_messages.py +41 -0
- app/api/services/preview_service.py +248 -0
- app/cli/__init__.py +18 -0
- app/cli/__main__.py +30 -0
- app/cli/shared_services/__init__.py +42 -0
- app/cli/shared_services/config_ops.py +581 -0
- app/cli/shared_services/generation_ops.py +160 -0
- app/cli/shared_services/project_ops.py +288 -0
- app/cli/shell/__init__.py +48 -0
- app/cli/shell/commands/__init__.py +63 -0
- app/cli/shell/commands/ai/__init__.py +260 -0
- app/cli/shell/commands/ai/chat.py +331 -0
- app/cli/shell/commands/ai/delete.py +165 -0
- app/cli/shell/commands/ai/diff.py +70 -0
- app/cli/shell/commands/ai/display.py +428 -0
- app/cli/shell/commands/ai/executor.py +225 -0
- app/cli/shell/commands/ai/executor_utils.py +271 -0
- app/cli/shell/commands/ai/generate.py +285 -0
- app/cli/shell/commands/ai/interaction.py +262 -0
- app/cli/shell/commands/ai/migrate.py +281 -0
- app/cli/shell/commands/ai/resolver.py +224 -0
- app/cli/shell/commands/ai/status.py +101 -0
- app/cli/shell/commands/ai/switch.py +158 -0
- app/cli/shell/commands/ai/utils.py +52 -0
- app/cli/shell/commands/base.py +343 -0
- app/cli/shell/commands/config/__init__.py +131 -0
- app/cli/shell/commands/config/base.py +80 -0
- app/cli/shell/commands/config/check.py +246 -0
- app/cli/shell/commands/config/edit.py +148 -0
- app/cli/shell/commands/config/get.py +111 -0
- app/cli/shell/commands/config/init.py +123 -0
- app/cli/shell/commands/config/inspect.py +164 -0
- app/cli/shell/commands/config/list.py +104 -0
- app/cli/shell/commands/config/set.py +119 -0
- app/cli/shell/commands/config/show.py +150 -0
- app/cli/shell/commands/exit.py +67 -0
- app/cli/shell/commands/help.py +104 -0
- app/cli/shell/commands/infer_schema.py +156 -0
- app/cli/shell/commands/open.py +201 -0
- app/cli/shell/commands/project.py +215 -0
- app/cli/shell/commands/provider.py +619 -0
- app/cli/shell/commands/system.py +170 -0
- app/cli/shell/commands/validate.py +455 -0
- app/cli/shell/completer.py +231 -0
- app/cli/shell/config_storage.py +191 -0
- app/cli/shell/exceptions.py +90 -0
- app/cli/shell/formatter.py +371 -0
- app/cli/shell/interactive_menu.py +348 -0
- app/cli/shell/main.py +286 -0
- app/cli/shell/parser.py +266 -0
- app/cli/start.py +180 -0
- app/cli_main.py +52 -0
- app/mcp_server.py +311 -0
- app/shared/core/__init__.py +18 -0
- app/shared/core/app_version.py +59 -0
- app/shared/core/config/__init__.py +119 -0
- app/shared/core/config/server.py +193 -0
- app/shared/core/data_source/__init__.py +106 -0
- app/shared/core/data_source/loader.py +348 -0
- app/shared/core/data_source/loaders/__init__.py +213 -0
- app/shared/core/data_source/loaders/base.py +215 -0
- app/shared/core/data_source/loaders/converter.py +337 -0
- app/shared/core/data_source/loaders/csv_loader.py +252 -0
- app/shared/core/data_source/loaders/excel_loader.py +666 -0
- app/shared/core/data_source/loaders/extractor.py +268 -0
- app/shared/core/data_source/loaders/json_loader.py +354 -0
- app/shared/core/data_source/loaders/registry.py +88 -0
- app/shared/core/data_source/loaders/sql_loader.py +313 -0
- app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
- app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
- app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
- app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
- app/shared/core/data_source/schema_info.py +50 -0
- app/shared/core/data_source/specs/__init__.py +128 -0
- app/shared/core/data_source/specs/base.py +258 -0
- app/shared/core/data_source/specs/csv_source.py +137 -0
- app/shared/core/data_source/specs/excel_source.py +141 -0
- app/shared/core/data_source/specs/file_base.py +252 -0
- app/shared/core/data_source/specs/json_source.py +226 -0
- app/shared/core/data_source/specs/sql_source.py +161 -0
- app/shared/core/encoding.py +36 -0
- app/shared/core/io/__init__.py +24 -0
- app/shared/core/io/yaml.py +379 -0
- app/shared/core/manifest_schema/__init__.py +52 -0
- app/shared/core/manifest_schema/types.py +99 -0
- app/shared/core/manifest_schema/version.py +175 -0
- app/shared/core/patterns/__init__.py +24 -0
- app/shared/core/patterns/loader.py +134 -0
- app/shared/core/patterns/writer.py +244 -0
- app/shared/core/project/__init__.py +47 -0
- app/shared/core/project/constraint/builders/__init__.py +50 -0
- app/shared/core/project/constraint/builders/base.py +100 -0
- app/shared/core/project/constraint/builders/composite.py +77 -0
- app/shared/core/project/constraint/builders/conditional.py +67 -0
- app/shared/core/project/constraint/builders/foreign_key.py +53 -0
- app/shared/core/project/constraint/builders/registry.py +64 -0
- app/shared/core/project/constraint/builders/scripted.py +51 -0
- app/shared/core/project/constraint/builders/single_column.py +86 -0
- app/shared/core/project/constraint/builders/unique.py +53 -0
- app/shared/core/project/constraint/factory.py +170 -0
- app/shared/core/project/constraint/reader.py +214 -0
- app/shared/core/project/constraint/registry.py +233 -0
- app/shared/core/project/constraint/types/__init__.py +63 -0
- app/shared/core/project/constraint/types/constraint_file.py +261 -0
- app/shared/core/project/constraint/types/refs.py +460 -0
- app/shared/core/project/constraint/types.py +28 -0
- app/shared/core/project/constraint/writer.py +181 -0
- app/shared/core/project/loader/__init__.py +56 -0
- app/shared/core/project/loader/loader.py +30 -0
- app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
- app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
- app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
- app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
- app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
- app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
- app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
- app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
- app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
- app/shared/core/project/loader/loader_parts/main.py +432 -0
- app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
- app/shared/core/project/loader/loader_parts/runtime.py +125 -0
- app/shared/core/project/loader/types.py +246 -0
- app/shared/core/project/manifest/coverage.py +391 -0
- app/shared/core/project/manifest/reader.py +260 -0
- app/shared/core/project/manifest/types.py +91 -0
- app/shared/core/project/manifest/types_parts/__init__.py +60 -0
- app/shared/core/project/manifest/types_parts/constants.py +40 -0
- app/shared/core/project/manifest/types_parts/data_source.py +68 -0
- app/shared/core/project/manifest/types_parts/info.py +59 -0
- app/shared/core/project/manifest/types_parts/manifest.py +294 -0
- app/shared/core/project/manifest/types_parts/refs.py +153 -0
- app/shared/core/project/manifest/types_parts/settings.py +92 -0
- app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
- app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
- app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
- app/shared/core/project/manifest/types_parts/template.py +66 -0
- app/shared/core/project/manifest/writer.py +262 -0
- app/shared/core/project/manual_data/__init__.py +27 -0
- app/shared/core/project/manual_data/types.py +75 -0
- app/shared/core/project/regex/reader.py +197 -0
- app/shared/core/project/regex/types.py +405 -0
- app/shared/core/project/regex/writer.py +123 -0
- app/shared/core/project/schema/reader.py +170 -0
- app/shared/core/project/schema/types.py +47 -0
- app/shared/core/project/schema/types_parts/__init__.py +36 -0
- app/shared/core/project/schema/types_parts/column.py +174 -0
- app/shared/core/project/schema/types_parts/column_utils.py +72 -0
- app/shared/core/project/schema/types_parts/constraint.py +165 -0
- app/shared/core/project/schema/types_parts/schema_id.py +66 -0
- app/shared/core/project/schema/types_parts/source.py +255 -0
- app/shared/core/project/schema/types_parts/source_options.py +347 -0
- app/shared/core/project/schema/types_parts/table.py +230 -0
- app/shared/core/project/schema/writer.py +139 -0
- app/shared/core/project/schema_ref_check.py +95 -0
- app/shared/core/project/template/__init__.py +27 -0
- app/shared/core/project/template/expander.py +263 -0
- app/shared/core/project/template/reader.py +120 -0
- app/shared/core/project/template/types.py +114 -0
- app/shared/core/project/transform/reader.py +76 -0
- app/shared/core/project/transform/types.py +116 -0
- app/shared/core/project/transform/writer.py +84 -0
- app/shared/core/pydantic_messages.py +59 -0
- app/shared/core/reporter/__init__.py +46 -0
- app/shared/core/reporter/reporter.py +220 -0
- app/shared/core/reporter/reporters/__init__.py +65 -0
- app/shared/core/reporter/reporters/base.py +188 -0
- app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
- app/shared/core/reporter/reporters/email_reporter.py +271 -0
- app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
- app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
- app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
- app/shared/core/utils/__init__.py +18 -0
- app/shared/core/utils/path_utils.py +60 -0
- app/shared/core/utils/regex_extract.py +113 -0
- app/shared/domain/__init__.py +114 -0
- app/shared/domain/constraints/__init__.py +76 -0
- app/shared/domain/constraints/allowed_values.py +211 -0
- app/shared/domain/constraints/base.py +141 -0
- app/shared/domain/constraints/charset.py +337 -0
- app/shared/domain/constraints/composite.py +174 -0
- app/shared/domain/constraints/condition_registry.py +130 -0
- app/shared/domain/constraints/conditional.py +629 -0
- app/shared/domain/constraints/date_logic.py +731 -0
- app/shared/domain/constraints/foreign_key.py +261 -0
- app/shared/domain/constraints/key_normalization.py +56 -0
- app/shared/domain/constraints/not_null.py +185 -0
- app/shared/domain/constraints/range.py +360 -0
- app/shared/domain/constraints/regex.py +218 -0
- app/shared/domain/constraints/scripted.py +276 -0
- app/shared/domain/constraints/unique.py +233 -0
- app/shared/domain/data_engine.py +384 -0
- app/shared/domain/data_types.py +113 -0
- app/shared/domain/data_types_parts/__init__.py +67 -0
- app/shared/domain/data_types_parts/base.py +204 -0
- app/shared/domain/data_types_parts/composite.py +250 -0
- app/shared/domain/data_types_parts/expression.py +202 -0
- app/shared/domain/data_types_parts/extracted.py +106 -0
- app/shared/domain/data_types_parts/json_types.py +234 -0
- app/shared/domain/data_types_parts/scalars.py +719 -0
- app/shared/domain/data_types_parts/sequence.py +129 -0
- app/shared/domain/dataset_schema.py +48 -0
- app/shared/domain/eval_sandbox.py +63 -0
- app/shared/domain/expression_system.py +366 -0
- app/shared/domain/regex_flags.py +53 -0
- app/shared/domain/schema/builder.py +223 -0
- app/shared/domain/schema/models.py +339 -0
- app/shared/domain/transforms/__init__.py +28 -0
- app/shared/domain/transforms/aggregate.py +141 -0
- app/shared/domain/transforms/base.py +171 -0
- app/shared/domain/transforms/cast_type.py +120 -0
- app/shared/domain/transforms/concat.py +103 -0
- app/shared/domain/transforms/conditional_assign.py +114 -0
- app/shared/domain/transforms/date_format.py +81 -0
- app/shared/domain/transforms/digits.py +77 -0
- app/shared/domain/transforms/drop_duplicates.py +94 -0
- app/shared/domain/transforms/fill_na.py +94 -0
- app/shared/domain/transforms/filter_rows.py +97 -0
- app/shared/domain/transforms/lookup.py +78 -0
- app/shared/domain/transforms/lower_case.py +70 -0
- app/shared/domain/transforms/map_value.py +90 -0
- app/shared/domain/transforms/math_expr.py +112 -0
- app/shared/domain/transforms/modulo.py +82 -0
- app/shared/domain/transforms/regex_extract.py +103 -0
- app/shared/domain/transforms/registry.py +95 -0
- app/shared/domain/transforms/replace.py +88 -0
- app/shared/domain/transforms/sort_rows.py +94 -0
- app/shared/domain/transforms/string_split.py +84 -0
- app/shared/domain/transforms/strip.py +73 -0
- app/shared/domain/transforms/substring.py +92 -0
- app/shared/domain/transforms/upper_case.py +70 -0
- app/shared/domain/transforms/weighted_sum.py +104 -0
- app/shared/domain/validation_constraints.py +69 -0
- app/shared/services/__init__.py +62 -0
- app/shared/services/ai/__init__.py +48 -0
- app/shared/services/ai/agent/__init__.py +40 -0
- app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
- app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
- app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
- app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
- app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
- app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
- app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
- app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
- app/shared/services/ai/agent/executor.py +554 -0
- app/shared/services/ai/agent/memory.py +256 -0
- app/shared/services/ai/agent/planner.py +282 -0
- app/shared/services/ai/agent/tool_registry.py +296 -0
- app/shared/services/ai/agent/tools/__init__.py +32 -0
- app/shared/services/ai/agent/tools/config_generate.py +133 -0
- app/shared/services/ai/agent/tools/config_refine.py +109 -0
- app/shared/services/ai/agent/tools/config_validate.py +403 -0
- app/shared/services/ai/agent/tools/merge_results.py +136 -0
- app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
- app/shared/services/ai/agent/tools/script_parse.py +359 -0
- app/shared/services/ai/agent/types.py +138 -0
- app/shared/services/ai/chat_agent_runner.py +596 -0
- app/shared/services/ai/chat_orchestrator.py +725 -0
- app/shared/services/ai/failure_messages.py +65 -0
- app/shared/services/ai/job_storage.py +274 -0
- app/shared/services/ai/migrate_service.py +550 -0
- app/shared/services/ai/streaming/__init__.py +65 -0
- app/shared/services/ai/streaming/event_journal.py +197 -0
- app/shared/services/ai/streaming/orchestrator.py +262 -0
- app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
- app/shared/services/ai/streaming/sse_response.py +148 -0
- app/shared/services/ai/streaming/types.py +73 -0
- app/shared/services/ai/types.py +213 -0
- app/shared/services/ai/utils.py +322 -0
- app/shared/services/diff/config_diff.py +320 -0
- app/shared/services/hardware.py +237 -0
- app/shared/services/llm/__init__.py +62 -0
- app/shared/services/llm/actions/__init__.py +42 -0
- app/shared/services/llm/actions/_canvas_validator.py +147 -0
- app/shared/services/llm/actions/_constraint_validator.py +401 -0
- app/shared/services/llm/actions/_regex_validator.py +85 -0
- app/shared/services/llm/actions/_schema_validator.py +82 -0
- app/shared/services/llm/actions/_settings_validator.py +76 -0
- app/shared/services/llm/actions/_transform_validator.py +78 -0
- app/shared/services/llm/actions/action_handlers.py +400 -0
- app/shared/services/llm/actions/action_parser.py +63 -0
- app/shared/services/llm/actions/action_processor.py +468 -0
- app/shared/services/llm/actions/action_validator.py +311 -0
- app/shared/services/llm/actions/diff_compute.py +195 -0
- app/shared/services/llm/actions/regex_handlers.py +284 -0
- app/shared/services/llm/actions/registry.py +336 -0
- app/shared/services/llm/actions/schema_handlers.py +335 -0
- app/shared/services/llm/actions/settings_handlers.py +180 -0
- app/shared/services/llm/actions/specs.py +259 -0
- app/shared/services/llm/actions/transform_handlers.py +279 -0
- app/shared/services/llm/actions/validation_types.py +147 -0
- app/shared/services/llm/cache/__init__.py +18 -0
- app/shared/services/llm/cache/response_cache.py +80 -0
- app/shared/services/llm/chat/__init__.py +35 -0
- app/shared/services/llm/chat/chat_system_prompt.py +561 -0
- app/shared/services/llm/chat/response_parser.py +296 -0
- app/shared/services/llm/config/__init__.py +45 -0
- app/shared/services/llm/config/crypto.py +135 -0
- app/shared/services/llm/config/loader.py +258 -0
- app/shared/services/llm/config/models.py +202 -0
- app/shared/services/llm/config/presets.py +128 -0
- app/shared/services/llm/config_generator.py +163 -0
- app/shared/services/llm/constraints/__init__.py +41 -0
- app/shared/services/llm/constraints/constraint_builder.py +177 -0
- app/shared/services/llm/constraints/constraint_deletion.py +84 -0
- app/shared/services/llm/constraints/constraint_id.py +174 -0
- app/shared/services/llm/constraints/frontend_instructions.py +333 -0
- app/shared/services/llm/constraints/inline_batch.py +279 -0
- app/shared/services/llm/discovery/__init__.py +35 -0
- app/shared/services/llm/discovery/scanner.py +181 -0
- app/shared/services/llm/generation/__init__.py +42 -0
- app/shared/services/llm/generation/agent_wiring.py +156 -0
- app/shared/services/llm/generation/config_builder.py +386 -0
- app/shared/services/llm/generation/errors.py +36 -0
- app/shared/services/llm/generation/existing_config.py +99 -0
- app/shared/services/llm/generation/profiler.py +279 -0
- app/shared/services/llm/generation/prompt_builder.py +199 -0
- app/shared/services/llm/generation/response_parser.py +144 -0
- app/shared/services/llm/generation/service.py +622 -0
- app/shared/services/llm/models.py +104 -0
- app/shared/services/llm/providers/__init__.py +48 -0
- app/shared/services/llm/providers/base.py +310 -0
- app/shared/services/llm/providers/cached_provider.py +90 -0
- app/shared/services/llm/providers/ollama.py +498 -0
- app/shared/services/llm/providers/openai.py +334 -0
- app/shared/services/llm/providers/registry.py +108 -0
- app/shared/services/llm/schema_resolver.py +141 -0
- app/shared/services/llm/suggestion_utils.py +219 -0
- app/shared/services/llm/validate_executor.py +163 -0
- app/shared/services/llm/yaml_io.py +406 -0
- app/shared/services/preview/__init__.py +18 -0
- app/shared/services/preview/loader.py +145 -0
- app/shared/services/preview/path_validation.py +138 -0
- app/shared/services/project_loader.py +57 -0
- app/shared/services/schema_inference.py +260 -0
- app/shared/services/schema_runtime_builder.py +120 -0
- app/shared/services/validation/__init__.py +52 -0
- app/shared/services/validation/chunked_loader.py +509 -0
- app/shared/services/validation/dag/__init__.py +35 -0
- app/shared/services/validation/dag/builder.py +141 -0
- app/shared/services/validation/dag/executor.py +225 -0
- app/shared/services/validation/dag/sorter.py +77 -0
- app/shared/services/validation/data_loader.py +231 -0
- app/shared/services/validation/engine.py +446 -0
- app/shared/services/validation/executor.py +1030 -0
- app/shared/services/validation/extractors.py +266 -0
- app/shared/services/validation/history.py +209 -0
- app/shared/services/validation/json_payload.py +136 -0
- app/shared/services/validation/loader.py +152 -0
- app/shared/services/validation/memory_monitor.py +179 -0
- app/shared/services/validation/postprocess.py +208 -0
- app/shared/services/validation/progress.py +64 -0
- app/shared/services/validation/report_export.py +222 -0
- app/shared/services/validation/resolver.py +180 -0
- app/shared/services/validation/service.py +418 -0
- app/shared/services/validation/types.py +238 -0
- app/shared/services/validation/validators/__init__.py +36 -0
- app/shared/services/validation/validators/adapter.py +182 -0
- app/shared/services/validation/validators/base.py +332 -0
- app/shared/services/validation/validators/composite.py +233 -0
- app/shared/services/validation/validators/date_logic.py +276 -0
- app/start_server.py +133 -0
- precis_cli-0.1.3.dist-info/METADATA +180 -0
- precis_cli-0.1.3.dist-info/RECORD +442 -0
- precis_cli-0.1.3.dist-info/WHEEL +5 -0
- precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
- 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)}")
|