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,333 @@
|
|
|
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
|
+
- 根据动作类型生成前端渲染指令
|
|
21
|
+
- 支持约束、Schema、Regex、Transform、Settings 全部操作
|
|
22
|
+
- 统一指令格式,便于前端 dispatcher 分发处理
|
|
23
|
+
- 约束指令的 targetNodeId 走与写 YAML 相同的名称→ID fallback 解析,
|
|
24
|
+
保证"写文件"和"生成前端指令"两条出口的 ID 解析能力对等
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
import yaml
|
|
34
|
+
|
|
35
|
+
from app.shared.services.llm.constraints.constraint_id import _generate_constraint_id
|
|
36
|
+
from app.shared.services.llm.schema_resolver import _resolve_id_from_name
|
|
37
|
+
from app.shared.services.llm.suggestion_utils import normalize_constraint_type
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _read_yaml_file(path: Path) -> dict[str, Any] | None:
|
|
43
|
+
"""安全读取 YAML 文件,失败返回 None(仅用于 UPDATE 指令重读磁盘真实结果)。"""
|
|
44
|
+
try:
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return None
|
|
47
|
+
with open(path, encoding="utf-8") as f:
|
|
48
|
+
return yaml.safe_load(f) or {}
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning(f"[frontend_instructions] 重读文件失败 {path}: {e}")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# 动作分类集合从注册表派生(单一事实源),避免本地硬编码与注册表不同步
|
|
55
|
+
from app.shared.services.llm.actions.registry import (
|
|
56
|
+
CONSTRAINT_ACTION_TYPES,
|
|
57
|
+
REGEX_ACTION_TYPES,
|
|
58
|
+
SCHEMA_ACTION_TYPES,
|
|
59
|
+
TRANSFORM_ACTION_TYPES,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def generate_frontend_instructions(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
@methoddesc 生成前端渲染指令(通用入口)
|
|
66
|
+
|
|
67
|
+
根据 actionType 分发到对应的指令生成器。
|
|
68
|
+
workspace_path 用于约束指令的名称→ID fallback 解析,
|
|
69
|
+
保证 targetNodeId 与写 YAML 路径使用同一套解析逻辑。
|
|
70
|
+
|
|
71
|
+
参数:
|
|
72
|
+
action: 动作字典,包含 actionType 和对应的 spec
|
|
73
|
+
workspace_path: 项目工作区路径,启用约束指令的 ID 解析(默认空=原样透传)
|
|
74
|
+
|
|
75
|
+
返回:
|
|
76
|
+
前端渲染指令字典
|
|
77
|
+
"""
|
|
78
|
+
action_type = action.get("actionType", "")
|
|
79
|
+
|
|
80
|
+
if action_type in CONSTRAINT_ACTION_TYPES:
|
|
81
|
+
return _generate_constraint_instruction(action, workspace_path)
|
|
82
|
+
elif action_type in SCHEMA_ACTION_TYPES:
|
|
83
|
+
return _generate_schema_instruction(action, workspace_path)
|
|
84
|
+
elif action_type in REGEX_ACTION_TYPES:
|
|
85
|
+
return _generate_regex_instruction(action, workspace_path)
|
|
86
|
+
elif action_type in TRANSFORM_ACTION_TYPES:
|
|
87
|
+
return _generate_transform_instruction(action, workspace_path)
|
|
88
|
+
elif action_type == "UPDATE_SETTINGS":
|
|
89
|
+
return _generate_settings_instruction(action)
|
|
90
|
+
elif action_type == "ADD_TO_CANVAS":
|
|
91
|
+
return _generate_canvas_instruction(action, workspace_path)
|
|
92
|
+
else:
|
|
93
|
+
return {"actionType": action_type}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _generate_constraint_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
生成约束类前端指令
|
|
99
|
+
|
|
100
|
+
与 _build_constraint_refs 保持对等的 ID 解析能力:
|
|
101
|
+
当 targetNodeId 为空或可疑时,用 _resolve_id_from_name 从 schema 文件
|
|
102
|
+
解析出确定性的 table_id / column_id,避免前端拿到 AI 原话找不到节点。
|
|
103
|
+
"""
|
|
104
|
+
action_type = action.get("actionType")
|
|
105
|
+
constraint_spec = action.get("constraintSpec", {})
|
|
106
|
+
|
|
107
|
+
# 标准化为 PascalCase 正名再下发:提示词教 AI 输出 PascalCase(NotNull),
|
|
108
|
+
# 但 LLM 也可能回大写下划线(NOT_NULL)。写盘路径已做同样的标准化,
|
|
109
|
+
# 此处对齐可保证前端指令与已写入的约束文件类型一致,避免前端拿到
|
|
110
|
+
# 自己无法识别的类型字符串而静默不建节点(幽灵约束)。
|
|
111
|
+
constraint_type = normalize_constraint_type(constraint_spec.get("type", ""))
|
|
112
|
+
target_node_id = constraint_spec.get("targetNodeId", "")
|
|
113
|
+
target_column_id = constraint_spec.get("targetColumnId", "")
|
|
114
|
+
table_name = constraint_spec.get("tableName", "")
|
|
115
|
+
target_column = constraint_spec.get("targetColumn", "")
|
|
116
|
+
is_inline = constraint_spec.get("isInline", False)
|
|
117
|
+
# params 原样透传(min/max/allowedValues/expression/toTableId 等):
|
|
118
|
+
# 前端建独立约束节点时须据此填充节点 data,否则保存链路会用空 params
|
|
119
|
+
# 覆盖后端已写入的参数(静默数据丢失)。
|
|
120
|
+
params = constraint_spec.get("params", {})
|
|
121
|
+
|
|
122
|
+
# 关键:与写 YAML 路径(_build_constraint_refs)相同的 fallback 解析
|
|
123
|
+
# 当 workspace_path 可用时,把 tableName/targetColumn 解析为确定性的 schema/column ID
|
|
124
|
+
if workspace_path and table_name:
|
|
125
|
+
fallback_table_id, fallback_column_id = _resolve_id_from_name(
|
|
126
|
+
workspace_path, table_name, target_column or target_column_id
|
|
127
|
+
)
|
|
128
|
+
# 仅在 AI 未给出明确 ID、或给出空值时,用 fallback 覆盖
|
|
129
|
+
# (AI 明确给出的非空 ID 优先,尊重其意图)
|
|
130
|
+
if fallback_table_id and not target_node_id:
|
|
131
|
+
target_node_id = fallback_table_id
|
|
132
|
+
if fallback_column_id and not target_column_id:
|
|
133
|
+
target_column_id = fallback_column_id
|
|
134
|
+
|
|
135
|
+
filename_table = table_name or target_node_id or "unknown"
|
|
136
|
+
filename_column = target_column or target_column_id or "unknown"
|
|
137
|
+
|
|
138
|
+
constraint_id = _generate_constraint_id(constraint_type, filename_table or "inline", filename_column)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"actionType": action_type,
|
|
142
|
+
"constraintSpec": {
|
|
143
|
+
"type": constraint_type,
|
|
144
|
+
"targetNodeId": target_node_id,
|
|
145
|
+
"targetColumnId": target_column_id,
|
|
146
|
+
"tableName": table_name,
|
|
147
|
+
"targetColumn": target_column,
|
|
148
|
+
"constraintId": constraint_id,
|
|
149
|
+
"isInline": is_inline,
|
|
150
|
+
"params": params,
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _generate_schema_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
|
|
156
|
+
"""生成 Schema 类前端指令。
|
|
157
|
+
|
|
158
|
+
ADD:回声输入(输入即落盘结果)。
|
|
159
|
+
UPDATE:重读磁盘,返回合并后的真实完整列/源(避免回声仅含 LLM 增量导致画布丢列)。
|
|
160
|
+
DELETE:只需 id/name 用于前端定位,不重读(文件已删)。
|
|
161
|
+
"""
|
|
162
|
+
action_type = action.get("actionType")
|
|
163
|
+
spec = action.get("schemaSpec", {})
|
|
164
|
+
schema_id = spec.get("schemaId") or spec.get("id", "")
|
|
165
|
+
name = spec.get("name", "")
|
|
166
|
+
|
|
167
|
+
columns = spec.get("columns", [])
|
|
168
|
+
source = spec.get("source")
|
|
169
|
+
|
|
170
|
+
# UPDATE 时重读磁盘真实结果(_update_schema 会保留未列出的列,回声会丢列)
|
|
171
|
+
if action_type == "UPDATE_SCHEMA" and workspace_path:
|
|
172
|
+
schemas_dir = Path(workspace_path) / "schemas"
|
|
173
|
+
# 按与 handler 一致的方式定位文件:先按 id,再按 name
|
|
174
|
+
candidates = list(schemas_dir.glob("*.yaml")) if schemas_dir.exists() else []
|
|
175
|
+
for sf in candidates:
|
|
176
|
+
data = _read_yaml_file(sf)
|
|
177
|
+
if data and (data.get("id") == schema_id or (name and data.get("name") == name)):
|
|
178
|
+
columns = data.get("columns", columns)
|
|
179
|
+
source = data.get("source", source)
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"actionType": action_type,
|
|
184
|
+
"schemaSpec": {
|
|
185
|
+
"name": name,
|
|
186
|
+
"schemaId": schema_id,
|
|
187
|
+
"columns": columns,
|
|
188
|
+
"source": source,
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _generate_regex_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
|
|
194
|
+
"""生成 Regex 类前端指令。
|
|
195
|
+
|
|
196
|
+
ADD:回声输入。UPDATE:重读磁盘真实 pattern/matchMode 等(整段替换,回声≈结果,重读为一致性)。
|
|
197
|
+
DELETE:只需 id/name 定位,不重读。
|
|
198
|
+
"""
|
|
199
|
+
action_type = action.get("actionType")
|
|
200
|
+
spec = action.get("regexSpec", {})
|
|
201
|
+
regex_id = spec.get("regexId") or spec.get("id", "")
|
|
202
|
+
name = spec.get("name", "")
|
|
203
|
+
|
|
204
|
+
pattern = spec.get("pattern", "")
|
|
205
|
+
match_mode = spec.get("matchMode", "full")
|
|
206
|
+
case_sensitive = spec.get("caseSensitive", False)
|
|
207
|
+
description = spec.get("description")
|
|
208
|
+
|
|
209
|
+
if action_type == "UPDATE_REGEX" and workspace_path and regex_id:
|
|
210
|
+
data = _read_yaml_file(Path(workspace_path) / "regex" / f"{regex_id}.regex.yaml")
|
|
211
|
+
if data:
|
|
212
|
+
pattern = data.get("pattern", pattern)
|
|
213
|
+
match_mode = data.get("matchMode", match_mode)
|
|
214
|
+
case_sensitive = data.get("caseSensitive", case_sensitive)
|
|
215
|
+
description = data.get("description", description)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"actionType": action_type,
|
|
219
|
+
"regexSpec": {
|
|
220
|
+
"name": name,
|
|
221
|
+
"regexId": regex_id,
|
|
222
|
+
"pattern": pattern,
|
|
223
|
+
"matchMode": match_mode,
|
|
224
|
+
"caseSensitive": case_sensitive,
|
|
225
|
+
"targetNodeId": spec.get("targetNodeId"),
|
|
226
|
+
"targetColumn": spec.get("targetColumn"),
|
|
227
|
+
"description": description,
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _generate_transform_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
|
|
233
|
+
"""生成 Transform 类前端指令。
|
|
234
|
+
|
|
235
|
+
ADD:回声输入。UPDATE:重读磁盘真实 params 等(整段替换,回声≈结果,重读为一致性)。
|
|
236
|
+
DELETE:只需 id 定位,不重读。
|
|
237
|
+
"""
|
|
238
|
+
action_type = action.get("actionType")
|
|
239
|
+
spec = action.get("transformSpec", {})
|
|
240
|
+
transform_id = spec.get("transformId") or spec.get("id", "")
|
|
241
|
+
|
|
242
|
+
params = spec.get("params", {})
|
|
243
|
+
output_columns = spec.get("outputColumns", [])
|
|
244
|
+
description = spec.get("description")
|
|
245
|
+
|
|
246
|
+
if action_type == "UPDATE_TRANSFORM" and workspace_path and transform_id:
|
|
247
|
+
data = _read_yaml_file(Path(workspace_path) / "transforms" / f"{transform_id}.transform.yaml")
|
|
248
|
+
if data:
|
|
249
|
+
params = data.get("params", params)
|
|
250
|
+
output_columns = data.get("outputColumns", output_columns)
|
|
251
|
+
description = data.get("description", description)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"actionType": action_type,
|
|
255
|
+
"transformSpec": {
|
|
256
|
+
"transformId": transform_id,
|
|
257
|
+
"type": spec.get("type", ""),
|
|
258
|
+
"description": description,
|
|
259
|
+
"inputFromNode": spec.get("inputFromNode"),
|
|
260
|
+
"inputColumn": spec.get("inputColumn"),
|
|
261
|
+
"params": params,
|
|
262
|
+
"outputColumns": output_columns,
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _generate_settings_instruction(action: dict[str, Any]) -> dict[str, Any]:
|
|
268
|
+
"""生成 Settings 类前端指令"""
|
|
269
|
+
action_type = action.get("actionType")
|
|
270
|
+
spec = action.get("settingsSpec", {})
|
|
271
|
+
return {
|
|
272
|
+
"actionType": action_type,
|
|
273
|
+
"settingsSpec": {
|
|
274
|
+
"category": spec.get("category", ""),
|
|
275
|
+
"settings": spec.get("settings", {}),
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _generate_canvas_instruction(action: dict[str, Any], workspace_path: str = "") -> dict[str, Any]:
|
|
281
|
+
"""生成 ADD_TO_CANVAS 前端指令(重读磁盘真实配置)
|
|
282
|
+
|
|
283
|
+
与 ADD_*(回声输入)不同:ADD_TO_CANVAS 的语义是"显示已存在的配置到画布",
|
|
284
|
+
故重读磁盘拿到真实数据(columns/source/pattern 等),保证画布显示与配置一致,
|
|
285
|
+
而非依赖 LLM 输入(可能不完整或与磁盘不同步)。
|
|
286
|
+
|
|
287
|
+
前端收到后委托 graphStore.importV2ResourceToCanvas 创建节点(幂等)。
|
|
288
|
+
"""
|
|
289
|
+
spec = action.get("canvasSpec", {})
|
|
290
|
+
resource_kind = spec.get("resourceKind", "")
|
|
291
|
+
resource_id = spec.get("resourceId", "")
|
|
292
|
+
resource_name = spec.get("resourceName") or spec.get("name", "")
|
|
293
|
+
|
|
294
|
+
# 定位磁盘配置文件并重读真实数据
|
|
295
|
+
# dir_map 与 _canvas_validator._list_existing_resource_ids 保持一致
|
|
296
|
+
dir_map = {
|
|
297
|
+
"schema": ("schemas", "*.schema.yaml"),
|
|
298
|
+
"regex": ("regex", "*.regex.yaml"),
|
|
299
|
+
"constraint": ("constraints", "*.constraint.yaml"),
|
|
300
|
+
"transform": ("transforms", "*.transform.yaml"),
|
|
301
|
+
}
|
|
302
|
+
real_config: dict[str, Any] = {}
|
|
303
|
+
if workspace_path and resource_kind in dir_map:
|
|
304
|
+
subdir, pattern = dir_map[resource_kind]
|
|
305
|
+
target_dir = Path(workspace_path) / subdir
|
|
306
|
+
if target_dir.exists():
|
|
307
|
+
for f in target_dir.glob(pattern):
|
|
308
|
+
data = _read_yaml_file(f)
|
|
309
|
+
if not data:
|
|
310
|
+
continue
|
|
311
|
+
# 按 id 或 name 匹配目标资源
|
|
312
|
+
fid = data.get("id", "")
|
|
313
|
+
fname = data.get("name", "")
|
|
314
|
+
if (resource_id and str(fid) == str(resource_id)) or (
|
|
315
|
+
resource_name and str(fname) == str(resource_name)
|
|
316
|
+
):
|
|
317
|
+
real_config = data
|
|
318
|
+
# 同步补全 resourceId(若 LLM 只给了 name)
|
|
319
|
+
if not resource_id and fid:
|
|
320
|
+
resource_id = str(fid)
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
"actionType": "ADD_TO_CANVAS",
|
|
325
|
+
"canvasSpec": {
|
|
326
|
+
"resourceKind": resource_kind,
|
|
327
|
+
"resourceId": resource_id,
|
|
328
|
+
"name": resource_name,
|
|
329
|
+
# 透传重读的真实配置,前端可据此直接构建节点(importV2ResourceToCanvas 也会独立重读,
|
|
330
|
+
# 但携带 config 可让前端在不依赖 API 往返时也能渲染)
|
|
331
|
+
"config": real_config,
|
|
332
|
+
},
|
|
333
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""
|
|
17
|
+
@fileoverview 内联约束批量处理模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 批量处理同一 schema 的多个内联约束
|
|
21
|
+
- 核心优化:只读取和写入 schema 文件一次,避免多次 IO
|
|
22
|
+
- 所有约束先在内存中处理,最后统一原子性写入
|
|
23
|
+
|
|
24
|
+
输入示例:
|
|
25
|
+
results = process_inline_batch(actions, "/workspace")
|
|
26
|
+
|
|
27
|
+
输出示例:
|
|
28
|
+
[
|
|
29
|
+
{"action": action, "success": True, "message": "inline:notnull_users_email", "frontendInstructions": {...}},
|
|
30
|
+
...
|
|
31
|
+
]
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
import yaml
|
|
41
|
+
|
|
42
|
+
from app.shared.services.llm.constraints.constraint_builder import (
|
|
43
|
+
CONSTRAINT_TYPE_MAP,
|
|
44
|
+
_build_constraint_params,
|
|
45
|
+
)
|
|
46
|
+
from app.shared.services.llm.constraints.constraint_id import _generate_constraint_id
|
|
47
|
+
from app.shared.services.llm.constraints.frontend_instructions import generate_frontend_instructions
|
|
48
|
+
from app.shared.services.llm.yaml_io import FileLock, atomic_write_yaml
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _collect_target_schema_id(action: dict[str, Any]) -> str | None:
|
|
54
|
+
"""
|
|
55
|
+
@methoddesc 从 action 中提取目标 schema ID(用于批量处理时分组)
|
|
56
|
+
|
|
57
|
+
参数:
|
|
58
|
+
action: 动作字典
|
|
59
|
+
|
|
60
|
+
返回:
|
|
61
|
+
schema ID 字符串,如果无法识别则返回 None
|
|
62
|
+
"""
|
|
63
|
+
spec = action.get("constraintSpec", {})
|
|
64
|
+
# 优先使用 tableName,其次是 targetNodeId
|
|
65
|
+
value = spec.get("tableName") or spec.get("targetNodeId")
|
|
66
|
+
return value if isinstance(value, str) else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_inline_action(action: dict[str, Any]) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
@methoddesc 判断动作是否为内联约束操作
|
|
72
|
+
|
|
73
|
+
内联约束直接存储在 schema 文件中,而非独立的 .constraint.yaml 文件。
|
|
74
|
+
|
|
75
|
+
参数:
|
|
76
|
+
action: 动作字典
|
|
77
|
+
|
|
78
|
+
返回:
|
|
79
|
+
True 表示内联约束,False 表示独立约束
|
|
80
|
+
"""
|
|
81
|
+
spec = action.get("constraintSpec", {})
|
|
82
|
+
return bool(spec.get("isInline", False))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def process_inline_batch(actions: list[dict[str, Any]], workspace_path: str) -> list[dict[str, Any]]:
|
|
86
|
+
"""
|
|
87
|
+
@methoddesc 批量处理同一 schema 的多个内联约束
|
|
88
|
+
|
|
89
|
+
核心优化:只读取和写入 schema 文件一次,避免多个独立约束操作导致多次 IO。
|
|
90
|
+
所有约束先在内存中处理,最后统一原子性写入。
|
|
91
|
+
|
|
92
|
+
参数:
|
|
93
|
+
actions: 同一 schema 的内联约束操作列表
|
|
94
|
+
workspace_path: 项目工作区路径
|
|
95
|
+
|
|
96
|
+
返回:
|
|
97
|
+
每个 action 的处理结果列表
|
|
98
|
+
|
|
99
|
+
示例:
|
|
100
|
+
>>> results = process_inline_batch([
|
|
101
|
+
... {"actionType": "ADD_CONSTRAINT_NODE", "constraintSpec": {"isInline": True, ...}},
|
|
102
|
+
... {"actionType": "ADD_CONSTRAINT_NODE", "constraintSpec": {"isInline": True, ...}},
|
|
103
|
+
... ], "/workspace")
|
|
104
|
+
"""
|
|
105
|
+
if not actions:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
# 从第一个 action 中提取 schema 标识(假设同一批的 schema 相同)
|
|
109
|
+
first_action = actions[0]
|
|
110
|
+
spec = first_action.get("constraintSpec", {})
|
|
111
|
+
target_node_id = spec.get("targetNodeId", "")
|
|
112
|
+
table_name = spec.get("tableName", "")
|
|
113
|
+
target_table_id = target_node_id or table_name
|
|
114
|
+
|
|
115
|
+
results = []
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
schemas_dir = Path(workspace_path) / "schemas"
|
|
119
|
+
schema_file = None
|
|
120
|
+
|
|
121
|
+
# 在 schemas 目录中查找匹配的 schema 文件
|
|
122
|
+
for sf in schemas_dir.glob("*.yaml"):
|
|
123
|
+
with open(sf, encoding="utf-8") as f:
|
|
124
|
+
sd = yaml.safe_load(f) or {}
|
|
125
|
+
if sd.get("id") == target_table_id or sd.get("name") == table_name:
|
|
126
|
+
schema_file = sf
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
if not schema_file:
|
|
130
|
+
# schema 文件不存在,所有操作标记为失败
|
|
131
|
+
for action in actions:
|
|
132
|
+
results.append(
|
|
133
|
+
{
|
|
134
|
+
"action": action,
|
|
135
|
+
"success": False,
|
|
136
|
+
"message": f"未找到 schema 文件: {target_table_id}",
|
|
137
|
+
"frontendInstructions": None,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
return results
|
|
141
|
+
|
|
142
|
+
# 使用文件锁保护,一次性处理所有约束
|
|
143
|
+
with FileLock(str(schema_file)):
|
|
144
|
+
# 重新读取文件(确保获取最新内容)
|
|
145
|
+
with open(schema_file, encoding="utf-8") as f:
|
|
146
|
+
schema_data = yaml.safe_load(f) or {}
|
|
147
|
+
|
|
148
|
+
columns = schema_data.get("columns", [])
|
|
149
|
+
if "constraints" not in schema_data:
|
|
150
|
+
schema_data["constraints"] = []
|
|
151
|
+
|
|
152
|
+
# 逐个处理约束(全部在内存中修改,不立即写入)
|
|
153
|
+
has_delete = False
|
|
154
|
+
for action in actions:
|
|
155
|
+
try:
|
|
156
|
+
spec = action.get("constraintSpec", {})
|
|
157
|
+
action_type = action.get("actionType", "")
|
|
158
|
+
constraint_type = spec.get("type", "")
|
|
159
|
+
target_column = spec.get("targetColumn", "")
|
|
160
|
+
target_column_id = spec.get("targetColumnId", "")
|
|
161
|
+
|
|
162
|
+
# 将约束类型别名标准化
|
|
163
|
+
std_type = CONSTRAINT_TYPE_MAP.get(constraint_type, constraint_type)
|
|
164
|
+
|
|
165
|
+
# 查找目标列的 ID(优先使用传入的 ID,否则按名称查找)
|
|
166
|
+
column_id = target_column_id
|
|
167
|
+
if not column_id:
|
|
168
|
+
for col in columns:
|
|
169
|
+
if col.get("name") == target_column:
|
|
170
|
+
column_id = col.get("id")
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
if not column_id:
|
|
174
|
+
results.append(
|
|
175
|
+
{
|
|
176
|
+
"action": action,
|
|
177
|
+
"success": False,
|
|
178
|
+
"message": f"未找到列 ID: {target_column}",
|
|
179
|
+
"frontendInstructions": None,
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# 生成约束 ID
|
|
185
|
+
filename_table = table_name or target_node_id or "unknown"
|
|
186
|
+
constraint_id = _generate_constraint_id(std_type, filename_table, target_column)
|
|
187
|
+
|
|
188
|
+
# DELETE 动作:从内存中的 constraints 移除「同列 + 同类型」项,绝不新增。
|
|
189
|
+
# 旧实现不区分 actionType,删除动作会被当作添加处理(只增不删还报成功)。
|
|
190
|
+
if action_type == "DELETE_CONSTRAINT_NODE":
|
|
191
|
+
has_delete = True
|
|
192
|
+
remaining = [
|
|
193
|
+
c
|
|
194
|
+
for c in schema_data["constraints"]
|
|
195
|
+
if not (c.get("column") == column_id and c.get("type") == std_type)
|
|
196
|
+
]
|
|
197
|
+
if len(remaining) == len(schema_data["constraints"]):
|
|
198
|
+
results.append(
|
|
199
|
+
{
|
|
200
|
+
"action": action,
|
|
201
|
+
"success": False,
|
|
202
|
+
"message": f"未找到内联约束: {std_type} on {filename_table}.{target_column}",
|
|
203
|
+
"frontendInstructions": None,
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
continue
|
|
207
|
+
schema_data["constraints"] = remaining
|
|
208
|
+
logger.info(f"[批量处理] 删除内联约束: {constraint_id}")
|
|
209
|
+
results.append(
|
|
210
|
+
{
|
|
211
|
+
"action": action,
|
|
212
|
+
"success": True,
|
|
213
|
+
"message": f"inline:{constraint_id}",
|
|
214
|
+
"frontendInstructions": generate_frontend_instructions(action, workspace_path),
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
# 构建内联约束结构(字段与其他约束保持一致)
|
|
220
|
+
constraint_description = spec.get("description") or f"{constraint_id}"
|
|
221
|
+
inline_constraint = {
|
|
222
|
+
"id": constraint_id,
|
|
223
|
+
"column": column_id,
|
|
224
|
+
"type": std_type,
|
|
225
|
+
"enabled": True,
|
|
226
|
+
"description": constraint_description,
|
|
227
|
+
}
|
|
228
|
+
params = _build_constraint_params(std_type, spec)
|
|
229
|
+
if params:
|
|
230
|
+
inline_constraint["params"] = params
|
|
231
|
+
|
|
232
|
+
# 检查是否已存在相同列和类型的约束
|
|
233
|
+
existing_idx = None
|
|
234
|
+
for idx, existing in enumerate(schema_data["constraints"]):
|
|
235
|
+
if existing.get("column") == column_id and existing.get("type") == std_type:
|
|
236
|
+
existing_idx = idx
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
if existing_idx is not None:
|
|
240
|
+
# 更新现有约束
|
|
241
|
+
schema_data["constraints"][existing_idx] = inline_constraint
|
|
242
|
+
logger.info(f"[批量处理] 更新内联约束: {constraint_id}")
|
|
243
|
+
else:
|
|
244
|
+
# 添加新约束
|
|
245
|
+
schema_data["constraints"].append(inline_constraint)
|
|
246
|
+
logger.info(f"[批量处理] 添加内联约束: {constraint_id}")
|
|
247
|
+
|
|
248
|
+
results.append(
|
|
249
|
+
{
|
|
250
|
+
"action": action,
|
|
251
|
+
"success": True,
|
|
252
|
+
"message": f"inline:{constraint_id}",
|
|
253
|
+
"frontendInstructions": generate_frontend_instructions(action, workspace_path),
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.error(f"[批量处理] 处理约束失败: {e}")
|
|
259
|
+
results.append(
|
|
260
|
+
{"action": action, "success": False, "message": str(e), "frontendInstructions": None}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# 只要有成功的操作,就一次性写入所有修改
|
|
264
|
+
if any(r["success"] for r in results):
|
|
265
|
+
# 含删除动作时必须全量替换写入:atomic_write_yaml 的 preserve_format
|
|
266
|
+
# 路径按 id 合并列表(只更新/追加、不删除),会把已删除的约束"复活"
|
|
267
|
+
atomic_write_yaml(schema_file, schema_data, preserve_format=not has_delete)
|
|
268
|
+
logger.info(f"[批量处理] 成功保存 {len([r for r in results if r['success']])} 个约束到 schema")
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"[批量处理] 批量处理失败: {e}")
|
|
272
|
+
# 为尚未记录结果的操作标记为失败
|
|
273
|
+
for i, action in enumerate(actions):
|
|
274
|
+
if len(results) < i + 1:
|
|
275
|
+
results.append(
|
|
276
|
+
{"action": action, "success": False, "message": f"批量处理失败: {e}", "frontendInstructions": None}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return results
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""@fileoverview LLM 服务发现模块入口
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 导出本地 LLM 服务扫描相关类(ServiceScanner、DiscoveredService)
|
|
20
|
+
- 供 API 层调用以自动发现本地运行的 AI 服务
|
|
21
|
+
|
|
22
|
+
架构设计:
|
|
23
|
+
- 聚合导出 scanner.py 中的公共类和全局实例
|
|
24
|
+
- 通过 __all__ 控制对外暴露的接口
|
|
25
|
+
|
|
26
|
+
输入示例:
|
|
27
|
+
from app.shared.services.llm.discovery import scanner, DiscoveredService
|
|
28
|
+
|
|
29
|
+
输出示例:
|
|
30
|
+
services = await scanner.scan()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from .scanner import DiscoveredService, ServiceScanner, scanner
|
|
34
|
+
|
|
35
|
+
__all__ = ["ServiceScanner", "DiscoveredService", "scanner"]
|