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,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
|
+
"""@fileoverview 数据文件画像
|
|
17
|
+
|
|
18
|
+
读取数据文件的前 N 行,提取列名、数据类型、空值数和样本值,生成供
|
|
19
|
+
Prompt 构建与 Agent 任务消息使用的画像数据。纯数据加工逻辑,无 Provider
|
|
20
|
+
依赖;取消检查通过 is_cancelled 回调注入,保持模块无状态。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from .errors import CancelledError
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ProfilingOptions:
|
|
39
|
+
"""数据画像选项"""
|
|
40
|
+
|
|
41
|
+
sample_rows: int = 100
|
|
42
|
+
sample_values_per_column: int = 100
|
|
43
|
+
max_files: int = 50
|
|
44
|
+
max_cell_chars: int = 500
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def profile_files(
|
|
48
|
+
file_paths: list[str], options: ProfilingOptions, is_cancelled: Callable[[], bool]
|
|
49
|
+
) -> list[dict]:
|
|
50
|
+
"""分析数据文件获取画像。
|
|
51
|
+
|
|
52
|
+
读取各类数据文件的前 N 行,提取列名、数据类型、空值数和样本值。
|
|
53
|
+
Excel 多 sheet 时每个 sheet 产出独立的 table 画像。
|
|
54
|
+
|
|
55
|
+
参数:
|
|
56
|
+
file_paths: 数据文件路径列表
|
|
57
|
+
options: 画像选项(采样行数/每列样本数/文件数上限/单元格截断长度)
|
|
58
|
+
is_cancelled: 取消状态探测回调,返回 True 时抛出 CancelledError
|
|
59
|
+
|
|
60
|
+
返回:
|
|
61
|
+
画像结果列表,每项含 path/table_name/sheet_name/columns
|
|
62
|
+
"""
|
|
63
|
+
import pandas as pd
|
|
64
|
+
|
|
65
|
+
results = []
|
|
66
|
+
|
|
67
|
+
# 限制最多处理的文件数量
|
|
68
|
+
for path in file_paths[: options.max_files]:
|
|
69
|
+
if is_cancelled():
|
|
70
|
+
raise CancelledError("Generation cancelled")
|
|
71
|
+
if not os.path.exists(path):
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
ext = os.path.splitext(path)[1].lower()
|
|
75
|
+
# 使用文件名(不含扩展名)作为默认表名
|
|
76
|
+
table_name = os.path.splitext(os.path.basename(path))[0]
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# 收集该文件的所有 sheets:(sheet_name, df) 列表。
|
|
80
|
+
# Excel 可能多 sheet,每个 sheet 作为独立 table 画像;其他类型单 sheet(sheet_name=None)
|
|
81
|
+
sheets: list[tuple[str | None, pd.DataFrame]] = []
|
|
82
|
+
if ext in [".xlsx", ".xls"]:
|
|
83
|
+
xl = pd.ExcelFile(path)
|
|
84
|
+
if not xl.sheet_names:
|
|
85
|
+
continue
|
|
86
|
+
# 读取所有 sheet(修复:原仅读 sheet_names[0],多 sheet Excel 会丢数据)
|
|
87
|
+
for sn in xl.sheet_names:
|
|
88
|
+
sheet_df = pd.read_excel(path, sheet_name=sn, nrows=options.sample_rows)
|
|
89
|
+
sheets.append((sn, sheet_df))
|
|
90
|
+
elif ext == ".csv":
|
|
91
|
+
df = pd.read_csv(path, nrows=options.sample_rows, encoding="utf-8")
|
|
92
|
+
sheets.append((None, df))
|
|
93
|
+
elif ext == ".json":
|
|
94
|
+
# JSON 文件处理
|
|
95
|
+
with open(path, encoding="utf-8") as f:
|
|
96
|
+
data = json.load(f)
|
|
97
|
+
# 处理嵌套结构
|
|
98
|
+
if isinstance(data, dict):
|
|
99
|
+
# 查找数据记录数组
|
|
100
|
+
records = find_json_records(data)
|
|
101
|
+
if records:
|
|
102
|
+
df = pd.json_normalize(records)
|
|
103
|
+
else:
|
|
104
|
+
df = pd.json_normalize([data])
|
|
105
|
+
elif isinstance(data, list):
|
|
106
|
+
df = pd.json_normalize(data)
|
|
107
|
+
else:
|
|
108
|
+
continue
|
|
109
|
+
# 限制行数
|
|
110
|
+
sheets.append((None, df.head(options.sample_rows)))
|
|
111
|
+
elif ext == ".jsonl":
|
|
112
|
+
# JSON Lines 文件处理
|
|
113
|
+
records = []
|
|
114
|
+
with open(path, encoding="utf-8") as f:
|
|
115
|
+
for i, line in enumerate(f):
|
|
116
|
+
if i >= options.sample_rows:
|
|
117
|
+
break
|
|
118
|
+
line = line.strip()
|
|
119
|
+
if line:
|
|
120
|
+
records.append(json.loads(line))
|
|
121
|
+
sheets.append((None, pd.json_normalize(records)))
|
|
122
|
+
else:
|
|
123
|
+
# 不支持的文件类型
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# 每个 sheet 独立构建画像结果(Excel 多 sheet 会产出多个 table 画像)
|
|
127
|
+
multi_sheet = len(sheets) > 1
|
|
128
|
+
for sheet_name, df in sheets:
|
|
129
|
+
if df is None or df.empty:
|
|
130
|
+
continue
|
|
131
|
+
# 分析列信息
|
|
132
|
+
columns = []
|
|
133
|
+
for col_name in df.columns:
|
|
134
|
+
if is_cancelled():
|
|
135
|
+
raise CancelledError("Generation cancelled")
|
|
136
|
+
col_data = df[col_name]
|
|
137
|
+
|
|
138
|
+
# 获取样本值(处理复杂类型如列表、字典)
|
|
139
|
+
# 使用 dict.fromkeys 保序去重,避免 list 的 O(n) in 查找导致的 O(n²) 开销
|
|
140
|
+
seen: dict[str, None] = {}
|
|
141
|
+
max_samples = options.sample_values_per_column
|
|
142
|
+
for v in col_data.dropna():
|
|
143
|
+
if len(seen) >= max_samples:
|
|
144
|
+
break
|
|
145
|
+
# 处理复杂类型并截断
|
|
146
|
+
v_str = str(v)[: options.max_cell_chars]
|
|
147
|
+
if v_str not in seen:
|
|
148
|
+
seen[v_str] = None
|
|
149
|
+
sample_values: list[str] = list(seen.keys())
|
|
150
|
+
|
|
151
|
+
columns.append(
|
|
152
|
+
{
|
|
153
|
+
"name": str(col_name),
|
|
154
|
+
"dtype": str(col_data.dtype),
|
|
155
|
+
"null_count": int(col_data.isna().sum()),
|
|
156
|
+
"sample_values": sample_values,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# 多 sheet Excel 时用 "文件名_sheet名" 作为 table_name 区分,避免多表同名
|
|
161
|
+
effective_table_name = f"{table_name}_{sheet_name}" if (multi_sheet and sheet_name) else table_name
|
|
162
|
+
result = {
|
|
163
|
+
"path": path,
|
|
164
|
+
"table_name": effective_table_name,
|
|
165
|
+
"sheet_name": sheet_name,
|
|
166
|
+
"columns": columns,
|
|
167
|
+
}
|
|
168
|
+
results.append(result)
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
# 读取失败,记录警告并跳过,不影响其他文件的处理。
|
|
172
|
+
# 注意:与原实现一致,列循环内取消抛出的 CancelledError 也会走到这里被记为
|
|
173
|
+
# 警告跳过——取消最终由调用方的阶段边界检查(_check_cancelled)兜底。
|
|
174
|
+
logger.warning(f"无法分析文件 {path}: {e}")
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
return results
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def find_json_records(data: dict) -> list[dict] | None:
|
|
181
|
+
"""在嵌套 JSON 对象中查找数据记录数组。
|
|
182
|
+
|
|
183
|
+
策略:递归遍历 JSON 的所有节点,找到最长的、元素为字典的数组。
|
|
184
|
+
|
|
185
|
+
参数:
|
|
186
|
+
data: 已解析的 JSON 对象
|
|
187
|
+
|
|
188
|
+
返回:
|
|
189
|
+
最长的记录数组;不存在时返回 None
|
|
190
|
+
"""
|
|
191
|
+
best_array = None
|
|
192
|
+
best_length = 0
|
|
193
|
+
|
|
194
|
+
def search(obj: Any) -> None:
|
|
195
|
+
nonlocal best_array, best_length
|
|
196
|
+
|
|
197
|
+
if isinstance(obj, dict):
|
|
198
|
+
for key, value in obj.items():
|
|
199
|
+
if isinstance(value, list):
|
|
200
|
+
# 检查是否是数据记录数组(元素是字典)
|
|
201
|
+
if value and isinstance(value[0], dict):
|
|
202
|
+
if len(value) > best_length:
|
|
203
|
+
best_length = len(value)
|
|
204
|
+
best_array = value
|
|
205
|
+
# 继续搜索数组内部(处理嵌套列表)
|
|
206
|
+
for item in value:
|
|
207
|
+
if isinstance(item, (dict, list)):
|
|
208
|
+
search(item)
|
|
209
|
+
elif isinstance(value, (dict, list)):
|
|
210
|
+
search(value)
|
|
211
|
+
elif isinstance(obj, list):
|
|
212
|
+
for item in obj:
|
|
213
|
+
if isinstance(item, (dict, list)):
|
|
214
|
+
search(item)
|
|
215
|
+
|
|
216
|
+
search(data)
|
|
217
|
+
return best_array
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def filter_profiling(
|
|
221
|
+
profiling_data: list[dict],
|
|
222
|
+
table_names: list[str] | None,
|
|
223
|
+
columns_filter: list[str] | None,
|
|
224
|
+
) -> list[dict]:
|
|
225
|
+
"""根据 scope 过滤画像数据。
|
|
226
|
+
|
|
227
|
+
参数:
|
|
228
|
+
profiling_data: 完整画像数据
|
|
229
|
+
table_names: 表名白名单,None 表示不过滤表
|
|
230
|
+
columns_filter: 列过滤,支持 "table.column" 或裸列名,None 表示不过滤列
|
|
231
|
+
|
|
232
|
+
返回:
|
|
233
|
+
过滤后的画像数据(浅拷贝,columns 为新列表)
|
|
234
|
+
"""
|
|
235
|
+
result = []
|
|
236
|
+
table_col_map: dict[str, list[str]] = {}
|
|
237
|
+
if columns_filter:
|
|
238
|
+
for tc in columns_filter:
|
|
239
|
+
if "." in tc:
|
|
240
|
+
t, c = tc.split(".", 1)
|
|
241
|
+
table_col_map.setdefault(t, []).append(c)
|
|
242
|
+
else:
|
|
243
|
+
table_col_map.setdefault("", []).append(tc)
|
|
244
|
+
|
|
245
|
+
for item in profiling_data:
|
|
246
|
+
table_name = item.get("table_name", "")
|
|
247
|
+
if table_names and table_name not in table_names:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
columns = item.get("columns", [])
|
|
251
|
+
if columns_filter:
|
|
252
|
+
allowed = set(table_col_map.get(table_name, []) + table_col_map.get("", []))
|
|
253
|
+
if allowed:
|
|
254
|
+
columns = [c for c in columns if c.get("name", "") in allowed]
|
|
255
|
+
|
|
256
|
+
if columns:
|
|
257
|
+
filtered = dict(item)
|
|
258
|
+
filtered["columns"] = columns
|
|
259
|
+
result.append(filtered)
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def format_profiling_for_agent(profiling_data: list[dict]) -> str:
|
|
264
|
+
"""将画像数据格式化为 Agent 任务消息中的文本段落。"""
|
|
265
|
+
lines = []
|
|
266
|
+
for item in profiling_data:
|
|
267
|
+
table_name = item.get("table_name", "")
|
|
268
|
+
lines.append(f"### {table_name}")
|
|
269
|
+
lines.append(f"文件: {item.get('path', '')}")
|
|
270
|
+
if item.get("sheet_name"):
|
|
271
|
+
lines.append(f"Sheet: {item['sheet_name']}")
|
|
272
|
+
for col in item.get("columns", []):
|
|
273
|
+
samples = col.get("sample_values", [])[:3]
|
|
274
|
+
samples_str = ", ".join(str(s)[:30] for s in samples)
|
|
275
|
+
line = f"- {col['name']}: {col['dtype']}, 空值{col['null_count']}"
|
|
276
|
+
if samples_str:
|
|
277
|
+
line += f", 例: {samples_str}"
|
|
278
|
+
lines.append(line)
|
|
279
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,199 @@
|
|
|
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 Prompt 构建器
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 根据数据画像结果构建 LLM 提示词
|
|
20
|
+
- 包含 Schema、Constraint、Regex Node 的格式说明
|
|
21
|
+
- 支持 prompt 长度预算:超限后自动降级(减少列数 → 减少文件数)
|
|
22
|
+
|
|
23
|
+
输入示例:
|
|
24
|
+
profiling_data = [{"table_name": "users", "columns": [...]}]
|
|
25
|
+
prompt, warnings = build_prompt(profiling_data, "电商数据校验")
|
|
26
|
+
|
|
27
|
+
输出示例:
|
|
28
|
+
(prompt_string, ["部分文件因 prompt 过长被省略: ..."])
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# 默认 prompt 长度预算(字符数)。DeepSeek v4-flash 支持 1M 上下文,
|
|
38
|
+
# 但给 system prompt、输出和 overhead 留余量,预算设为 80K 字符。
|
|
39
|
+
DEFAULT_MAX_PROMPT_CHARS = 80_000
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _estimate_prompt_len(
|
|
43
|
+
profiling_data: list[dict],
|
|
44
|
+
project_name: str,
|
|
45
|
+
max_columns_per_table: int,
|
|
46
|
+
max_sample_values: int,
|
|
47
|
+
max_sample_length: int,
|
|
48
|
+
) -> int:
|
|
49
|
+
"""估算 prompt 长度(不实际构建完整字符串)"""
|
|
50
|
+
length = len(project_name) + 200 # 固定开销(header、格式说明等)
|
|
51
|
+
for item in profiling_data:
|
|
52
|
+
length += 100 # 表头开销
|
|
53
|
+
length += len(item.get("table_name", ""))
|
|
54
|
+
length += len(item.get("path", ""))
|
|
55
|
+
if item.get("sheet_name"):
|
|
56
|
+
length += 50
|
|
57
|
+
columns = item.get("columns", [])[:max_columns_per_table]
|
|
58
|
+
for col in columns:
|
|
59
|
+
length += 80 + len(col["name"]) + len(str(col["dtype"]))
|
|
60
|
+
samples = col.get("sample_values", [])[:max_sample_values]
|
|
61
|
+
length += sum(min(len(str(s)), max_sample_length) for s in samples)
|
|
62
|
+
remaining = len(item.get("columns", [])) - max_columns_per_table
|
|
63
|
+
if remaining > 0:
|
|
64
|
+
length += 40
|
|
65
|
+
return length
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_prompt(
|
|
69
|
+
profiling_data: list[dict],
|
|
70
|
+
project_name: str,
|
|
71
|
+
max_prompt_chars: int = DEFAULT_MAX_PROMPT_CHARS,
|
|
72
|
+
extra_context: list[str] | None = None,
|
|
73
|
+
) -> tuple[str, list[str]]:
|
|
74
|
+
"""
|
|
75
|
+
@methoddesc 构建生成提示词(带长度预算和自动降级)
|
|
76
|
+
|
|
77
|
+
策略(渐进式降级):
|
|
78
|
+
1. 先用较宽松的列数限制(20 列/表,2 样本值)估算长度
|
|
79
|
+
2. 若超限,先降低列数限制(20 → 10 → 5)
|
|
80
|
+
3. 若仍超限,减少文件数(保留前 N 个)
|
|
81
|
+
4. 最终返回 truncation 警告列表
|
|
82
|
+
|
|
83
|
+
参数:
|
|
84
|
+
profiling_data: 数据画像结果列表
|
|
85
|
+
project_name: 项目名称
|
|
86
|
+
max_prompt_chars: prompt 最大字符数预算
|
|
87
|
+
extra_context: 额外上下文文本列表(如上一轮结果、校验问题等)
|
|
88
|
+
|
|
89
|
+
返回:
|
|
90
|
+
(prompt 字符串, truncation 警告列表)
|
|
91
|
+
"""
|
|
92
|
+
max_columns_per_table = 20
|
|
93
|
+
max_sample_values = 2
|
|
94
|
+
max_sample_length = 30
|
|
95
|
+
warnings: list[str] = []
|
|
96
|
+
|
|
97
|
+
# 阶段 1: 估算长度,尝试逐步降级
|
|
98
|
+
# 降级策略:列数限制 [20, 10, 5, 3],然后截断文件数
|
|
99
|
+
for columns_limit in (20, 10, 5, 3):
|
|
100
|
+
max_columns_per_table = columns_limit
|
|
101
|
+
estimated = _estimate_prompt_len(
|
|
102
|
+
profiling_data, project_name, max_columns_per_table, max_sample_values, max_sample_length
|
|
103
|
+
)
|
|
104
|
+
if estimated <= max_prompt_chars:
|
|
105
|
+
if columns_limit < 20:
|
|
106
|
+
warnings.append(f"数据文件较多,prompt 长度受限,每表仅展示前 {columns_limit} 列")
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
# 即使 3 列也超限,需要截断文件数
|
|
110
|
+
# 二分查找最大可处理的文件数
|
|
111
|
+
lo, hi = 1, len(profiling_data)
|
|
112
|
+
best_files = 1
|
|
113
|
+
while lo <= hi:
|
|
114
|
+
mid = (lo + hi) // 2
|
|
115
|
+
estimated = _estimate_prompt_len(
|
|
116
|
+
profiling_data[:mid], project_name, 3, max_sample_values, max_sample_length
|
|
117
|
+
)
|
|
118
|
+
if estimated <= max_prompt_chars:
|
|
119
|
+
best_files = mid
|
|
120
|
+
lo = mid + 1
|
|
121
|
+
else:
|
|
122
|
+
hi = mid - 1
|
|
123
|
+
|
|
124
|
+
# 先记录总数再切片:旧实现在切片后取 len,omitted 恒为 0,
|
|
125
|
+
# 超预算文件被静默丢弃且无任何警告
|
|
126
|
+
total_files = len(profiling_data)
|
|
127
|
+
profiling_data = profiling_data[:best_files]
|
|
128
|
+
omitted = total_files - best_files
|
|
129
|
+
if omitted > 0:
|
|
130
|
+
warnings.append(f"数据文件过多(共 {total_files} 个),仅分析前 {best_files} 个文件")
|
|
131
|
+
max_columns_per_table = 3
|
|
132
|
+
|
|
133
|
+
# 阶段 2: 实际构建 prompt
|
|
134
|
+
prompt = f"""为项目 "{project_name}" 生成数据验证配置。
|
|
135
|
+
|
|
136
|
+
## 数据画像
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
for item in profiling_data:
|
|
140
|
+
table_name = item.get("table_name", "")
|
|
141
|
+
prompt += f"### {table_name}\n"
|
|
142
|
+
prompt += f"文件: {item.get('path', '')}\n"
|
|
143
|
+
if item.get("sheet_name"):
|
|
144
|
+
prompt += f"Sheet: {item['sheet_name']}\n"
|
|
145
|
+
|
|
146
|
+
columns = item.get("columns", [])[:max_columns_per_table]
|
|
147
|
+
for col in columns:
|
|
148
|
+
samples = col.get("sample_values", [])[:max_sample_values]
|
|
149
|
+
samples_str = ", ".join(str(s)[:max_sample_length] for s in samples)
|
|
150
|
+
prompt += f"- {col['name']}: {col['dtype']}, 空值{col['null_count']}"
|
|
151
|
+
if samples_str:
|
|
152
|
+
prompt += f", 例: {samples_str}"
|
|
153
|
+
prompt += "\n"
|
|
154
|
+
|
|
155
|
+
remaining = len(item.get("columns", [])) - max_columns_per_table
|
|
156
|
+
if remaining > 0:
|
|
157
|
+
prompt += f"- ... 还有 {remaining} 列未显示\n"
|
|
158
|
+
|
|
159
|
+
# 追加额外上下文
|
|
160
|
+
if extra_context:
|
|
161
|
+
prompt += "\n## 额外上下文\n\n"
|
|
162
|
+
for ctx in extra_context:
|
|
163
|
+
prompt += f"{ctx}\n\n"
|
|
164
|
+
|
|
165
|
+
prompt += """
|
|
166
|
+
## 输出要求
|
|
167
|
+
|
|
168
|
+
返回 JSON 配置,包含 schemas、constraints、regex_nodes 三个字段。
|
|
169
|
+
|
|
170
|
+
Schema 格式(请为每个 schema 提供语义化、唯一的 id,使用小写英文+下划线,如 "users"、"order_items"。该 id 会作为约束和正则节点引用的主键):
|
|
171
|
+
{"id": "users", "name": "用户表", "source": {"mode": "relative_file", "path": "data/users.xlsx", "header_row": 0}, "columns": [{"id": "email", "name": "邮箱", "type": "string", "primary_key": true, "nullable": false}]}
|
|
172
|
+
|
|
173
|
+
Constraint 格式(V2,refs/params 分离,table_id 必须与 schema 的 id 一致):
|
|
174
|
+
{"id": "users_email_notnull", "type": "NotNull", "enabled": true, "refs": {"table_id": "users", "column_id": "email"}, "params": {}}
|
|
175
|
+
|
|
176
|
+
支持类型:NotNull, Unique, AllowedValues(refs+params{allowed_values}), Range(refs+params{min,max}), ForeignKey, Conditional。
|
|
177
|
+
|
|
178
|
+
Regex Node 格式:
|
|
179
|
+
{"id": "email_regex", "name": "邮箱格式", "pattern": "^[^@]+@[^@]+$", "match_mode": "full", "source_ref": {"table_id": "users", "column_id": "email"}}
|
|
180
|
+
|
|
181
|
+
建议:
|
|
182
|
+
- email/手机号/身份证 → regex_nodes
|
|
183
|
+
- status/gender → AllowedValues
|
|
184
|
+
- 主键 → Unique
|
|
185
|
+
- 非空列 → NotNull
|
|
186
|
+
- 数值列 → Range
|
|
187
|
+
|
|
188
|
+
直接返回 JSON,不要解释。"""
|
|
189
|
+
|
|
190
|
+
actual_len = len(prompt)
|
|
191
|
+
logger.info(
|
|
192
|
+
"[build_prompt] files=%d, columns_limit=%d, estimated=%d, actual=%d",
|
|
193
|
+
len(profiling_data),
|
|
194
|
+
max_columns_per_table,
|
|
195
|
+
_estimate_prompt_len(profiling_data, project_name, max_columns_per_table, max_sample_values, max_sample_length),
|
|
196
|
+
actual_len,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return prompt, warnings
|
|
@@ -0,0 +1,144 @@
|
|
|
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 响应 JSON 提取与解析
|
|
17
|
+
|
|
18
|
+
从 LLM 返回的自由文本中可靠地提取 JSON 配置对象。核心是字符串感知的
|
|
19
|
+
大括号配平扫描:JSON 字符串值内的 '{' / '}' 不参与配平,避免字符串值
|
|
20
|
+
包含 '}' 时把合法 JSON 截断(如 {"a":"x}y"} 被截成 {"a":"x})。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
from .errors import GenerationParseError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_json_object_end(text: str, start: int) -> int | None:
|
|
32
|
+
"""从 start(应指向 '{')开始做字符串感知的大括号配平扫描。
|
|
33
|
+
|
|
34
|
+
跟踪扫描位置是否处于双引号字符串内部(处理反斜杠转义):
|
|
35
|
+
字符串内的 '{' / '}' 不参与配平,避免 JSON 字符串值包含 '}' 时
|
|
36
|
+
把合法 JSON 截断(如 {"a":"x}y"} 被截成 {"a":"x})。
|
|
37
|
+
|
|
38
|
+
参数:
|
|
39
|
+
text: 待扫描文本
|
|
40
|
+
start: 起始索引(text[start] 应为 '{')
|
|
41
|
+
|
|
42
|
+
返回:
|
|
43
|
+
匹配闭括号的索引(含);扫描不到配平闭括号时返回 None
|
|
44
|
+
"""
|
|
45
|
+
balance = 0
|
|
46
|
+
in_string = False
|
|
47
|
+
escaped = False
|
|
48
|
+
for i in range(start, len(text)):
|
|
49
|
+
char = text[i]
|
|
50
|
+
if in_string:
|
|
51
|
+
if escaped:
|
|
52
|
+
# 转义序列的第二个字符被消耗,不参与边界判断
|
|
53
|
+
escaped = False
|
|
54
|
+
elif char == "\\":
|
|
55
|
+
escaped = True
|
|
56
|
+
elif char == '"':
|
|
57
|
+
in_string = False
|
|
58
|
+
continue
|
|
59
|
+
if char == '"':
|
|
60
|
+
in_string = True
|
|
61
|
+
elif char == "{":
|
|
62
|
+
balance += 1
|
|
63
|
+
elif char == "}":
|
|
64
|
+
balance -= 1
|
|
65
|
+
if balance == 0:
|
|
66
|
+
return i
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_llm_response(content: str) -> dict:
|
|
71
|
+
"""解析 LLM 返回的文本,提取其中的 JSON 配置。
|
|
72
|
+
|
|
73
|
+
处理步骤:
|
|
74
|
+
1. 尝试提取 Markdown 代码块(```json ... ```)中的内容
|
|
75
|
+
2. 通过大括号平衡计数找到最外层的 JSON 对象
|
|
76
|
+
3. 使用 json.loads 解析
|
|
77
|
+
|
|
78
|
+
参数:
|
|
79
|
+
content: LLM 返回的原始文本
|
|
80
|
+
|
|
81
|
+
返回:
|
|
82
|
+
解析出的 JSON 对象字典
|
|
83
|
+
|
|
84
|
+
异常:
|
|
85
|
+
GenerationParseError: 内容无法解析为 JSON 对象(携带原始内容前 2000 字符)
|
|
86
|
+
"""
|
|
87
|
+
original = content
|
|
88
|
+
|
|
89
|
+
# 尝试找到 Markdown JSON 代码块
|
|
90
|
+
match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL)
|
|
91
|
+
if match:
|
|
92
|
+
content = match.group(1).strip()
|
|
93
|
+
|
|
94
|
+
# 通过大括号平衡计数找到最外层的 JSON 对象(字符串感知,字符串内的 '}' 不参与配平)
|
|
95
|
+
start = content.find("{")
|
|
96
|
+
if start != -1:
|
|
97
|
+
end = find_json_object_end(content, start)
|
|
98
|
+
if end is not None:
|
|
99
|
+
content = content[start : end + 1]
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
parsed = json.loads(content)
|
|
103
|
+
if not isinstance(parsed, dict):
|
|
104
|
+
raise GenerationParseError(
|
|
105
|
+
f"LLM 响应解析结果不是 JSON 对象,实际类型: {type(parsed).__name__}",
|
|
106
|
+
raw_content=original[:2000],
|
|
107
|
+
)
|
|
108
|
+
return parsed
|
|
109
|
+
except json.JSONDecodeError as e:
|
|
110
|
+
raise GenerationParseError(
|
|
111
|
+
f"AI 返回的内容无法解析为有效配置,请重试或调整描述后重试({e})",
|
|
112
|
+
raw_content=original[:2000],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def try_extract_json_object(content: str | None) -> dict | None:
|
|
117
|
+
"""尽力从文本中提取一个 JSON 对象,失败返回 None(不抛异常)。
|
|
118
|
+
|
|
119
|
+
与 parse_llm_response 的差别:本函数用于"兜底解析"场景
|
|
120
|
+
(如 Agent 最后一轮 content),解析失败不视为错误。
|
|
121
|
+
|
|
122
|
+
参数:
|
|
123
|
+
content: 待提取的文本,可为 None
|
|
124
|
+
|
|
125
|
+
返回:
|
|
126
|
+
提取到的 JSON 对象字典;无法提取时返回 None
|
|
127
|
+
"""
|
|
128
|
+
if not content:
|
|
129
|
+
return None
|
|
130
|
+
match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL)
|
|
131
|
+
text = match.group(1).strip() if match else content
|
|
132
|
+
start = text.find("{")
|
|
133
|
+
if start == -1:
|
|
134
|
+
return None
|
|
135
|
+
end = find_json_object_end(text, start)
|
|
136
|
+
if end is None:
|
|
137
|
+
return None
|
|
138
|
+
try:
|
|
139
|
+
parsed = json.loads(text[start : end + 1])
|
|
140
|
+
if isinstance(parsed, dict):
|
|
141
|
+
return parsed
|
|
142
|
+
except json.JSONDecodeError:
|
|
143
|
+
return None
|
|
144
|
+
return None
|