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,256 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""@fileoverview Agent 记忆模块
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 管理 Agent 多轮对话的上下文
|
|
20
|
+
- 按 token 预算截断历史,保留 system prompt 和最近轮次
|
|
21
|
+
- 支持保存和恢复 checkpoint
|
|
22
|
+
|
|
23
|
+
架构设计:
|
|
24
|
+
- 使用纯字典列表表示消息,兼容 OpenAI messages 格式
|
|
25
|
+
- token 估算委托给 utils.estimate_tokens 统一实现(中文字 1:1、英文单词/数字串/标点各计 1,另加固定开销)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from app.shared.services.ai.agent.types import AgentTurn
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# 默认最大 token 预算
|
|
38
|
+
DEFAULT_MAX_TOKENS = 120000
|
|
39
|
+
# 单条消息保留的最大字符数
|
|
40
|
+
_MAX_MESSAGE_CHARS = 8000
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AgentMemory:
|
|
44
|
+
"""
|
|
45
|
+
@classdesc Agent 短期记忆
|
|
46
|
+
|
|
47
|
+
维护 system prompt + 用户初始任务 + 多轮对话历史。
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, system_prompt: str, max_tokens: int = DEFAULT_MAX_TOKENS):
|
|
51
|
+
"""
|
|
52
|
+
@methoddesc 初始化记忆
|
|
53
|
+
|
|
54
|
+
参数:
|
|
55
|
+
system_prompt: 系统提示词
|
|
56
|
+
max_tokens: 最大 token 预算
|
|
57
|
+
"""
|
|
58
|
+
self.system_prompt = system_prompt
|
|
59
|
+
self.max_tokens = max_tokens
|
|
60
|
+
self._messages: list[dict[str, Any]] = []
|
|
61
|
+
self._turns: list[AgentTurn] = []
|
|
62
|
+
self._turn_count: int = 0
|
|
63
|
+
if system_prompt:
|
|
64
|
+
self._messages.append({"role": "system", "content": system_prompt})
|
|
65
|
+
|
|
66
|
+
def set_task(self, task_message: str) -> None:
|
|
67
|
+
"""设置用户初始任务消息。"""
|
|
68
|
+
# 确保只有一条用户初始任务,避免重复
|
|
69
|
+
self._messages = [m for m in self._messages if not (m.get("role") == "user" and m.get("_is_task"))]
|
|
70
|
+
self._messages.append({"role": "user", "content": task_message, "_is_task": True})
|
|
71
|
+
|
|
72
|
+
def add_assistant_message(
|
|
73
|
+
self,
|
|
74
|
+
content: str | None,
|
|
75
|
+
tool_calls: list[dict[str, Any]] | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""添加 assistant 消息。"""
|
|
78
|
+
msg: dict[str, Any] = {"role": "assistant"}
|
|
79
|
+
if content:
|
|
80
|
+
msg["content"] = self._truncate(content)
|
|
81
|
+
if tool_calls:
|
|
82
|
+
msg["tool_calls"] = tool_calls
|
|
83
|
+
self._messages.append(msg)
|
|
84
|
+
|
|
85
|
+
def add_tool_result(self, tool_call_id: str, name: str, result: dict[str, Any] | str) -> None:
|
|
86
|
+
"""添加工具结果消息。"""
|
|
87
|
+
observation = result if isinstance(result, str) else self._dict_to_observation(result)
|
|
88
|
+
self._messages.append(
|
|
89
|
+
{
|
|
90
|
+
"role": "tool",
|
|
91
|
+
"tool_call_id": tool_call_id,
|
|
92
|
+
"name": name,
|
|
93
|
+
"content": self._truncate(observation),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def add_system_reminder(self, content: str) -> None:
|
|
98
|
+
"""注入一条系统级提醒消息(用 user role,带 _is_reminder 标记)。
|
|
99
|
+
|
|
100
|
+
为什么不用 role="system":get_messages 会跳过所有 role=="system" 的非首条消息,
|
|
101
|
+
导致 reminder 被丢弃。用 user role 可确保它进入 LLM 的上下文。
|
|
102
|
+
|
|
103
|
+
用途(P1-2 收敛引导):
|
|
104
|
+
- 接近 max_iterations 时提醒 LLM 直接回复、停止调工具
|
|
105
|
+
- 检测到跨轮重复调用同一工具同参数时提醒 LLM 使用已有结果
|
|
106
|
+
|
|
107
|
+
参数:
|
|
108
|
+
content: 提醒文本
|
|
109
|
+
"""
|
|
110
|
+
if not content:
|
|
111
|
+
return
|
|
112
|
+
self._messages.append({"role": "user", "content": self._truncate(content), "_is_reminder": True})
|
|
113
|
+
|
|
114
|
+
def add_turn(self, turn: AgentTurn) -> None:
|
|
115
|
+
"""记录完整 turn。"""
|
|
116
|
+
self._turns.append(turn)
|
|
117
|
+
self._turn_count += 1
|
|
118
|
+
|
|
119
|
+
def get_messages(self) -> list[dict[str, Any]]:
|
|
120
|
+
"""
|
|
121
|
+
@methoddesc 获取截断后的消息列表
|
|
122
|
+
|
|
123
|
+
保留 system prompt 和最近的用户/assistant/tool 消息,不超出 token 预算。
|
|
124
|
+
"""
|
|
125
|
+
# 计算 system prompt 开销
|
|
126
|
+
system_tokens = self._estimate_tokens(self.system_prompt)
|
|
127
|
+
budget = self.max_tokens - system_tokens
|
|
128
|
+
|
|
129
|
+
# 从后往前累加,直到预算用完
|
|
130
|
+
selected: list[dict[str, Any]] = []
|
|
131
|
+
total = 0
|
|
132
|
+
# 跳过 system prompt(已在开头)
|
|
133
|
+
for msg in reversed(self._messages):
|
|
134
|
+
if msg.get("role") == "system":
|
|
135
|
+
continue
|
|
136
|
+
tokens = self._estimate_message_tokens(msg)
|
|
137
|
+
if total + tokens > budget and selected:
|
|
138
|
+
break
|
|
139
|
+
selected.append(msg)
|
|
140
|
+
total += tokens
|
|
141
|
+
|
|
142
|
+
selected.reverse()
|
|
143
|
+
result: list[dict[str, Any]] = []
|
|
144
|
+
if self.system_prompt:
|
|
145
|
+
result.append({"role": "system", "content": self.system_prompt})
|
|
146
|
+
result.extend(selected)
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
def get_turns(self) -> list[AgentTurn]:
|
|
150
|
+
"""获取所有 turn 记录。"""
|
|
151
|
+
return self._turns
|
|
152
|
+
|
|
153
|
+
def create_checkpoint(self) -> dict[str, Any]:
|
|
154
|
+
"""创建当前记忆 checkpoint(含完整 messages,支持跨进程恢复)。"""
|
|
155
|
+
return {
|
|
156
|
+
"system_prompt": self.system_prompt,
|
|
157
|
+
"max_tokens": self.max_tokens,
|
|
158
|
+
"message_count": len(self._messages),
|
|
159
|
+
"turn_count": self._turn_count,
|
|
160
|
+
"messages": [dict(m) for m in self._messages],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def restore_from_checkpoint(cls, checkpoint: dict[str, Any]) -> AgentMemory:
|
|
165
|
+
"""从 checkpoint 重建 memory 状态。
|
|
166
|
+
|
|
167
|
+
参数:
|
|
168
|
+
checkpoint: create_checkpoint 返回的字典
|
|
169
|
+
|
|
170
|
+
返回:
|
|
171
|
+
重建的 AgentMemory 实例
|
|
172
|
+
"""
|
|
173
|
+
mem = cls(
|
|
174
|
+
system_prompt=checkpoint.get("system_prompt", ""),
|
|
175
|
+
max_tokens=checkpoint.get("max_tokens", DEFAULT_MAX_TOKENS),
|
|
176
|
+
)
|
|
177
|
+
mem._messages = [dict(m) for m in checkpoint.get("messages", [])]
|
|
178
|
+
mem._turn_count = checkpoint.get("turn_count", 0)
|
|
179
|
+
return mem
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _truncate(text: str, max_chars: int = _MAX_MESSAGE_CHARS) -> str:
|
|
183
|
+
"""截断超长文本(最终兜底)。"""
|
|
184
|
+
if len(text) <= max_chars:
|
|
185
|
+
return text
|
|
186
|
+
return text[:max_chars] + f"\n... [truncated {len(text) - max_chars} chars]"
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _summarize_dict(data: dict[str, Any], max_field_chars: int = 1500) -> dict[str, Any]:
|
|
190
|
+
"""对工具结果的 dict 做智能裁剪。
|
|
191
|
+
|
|
192
|
+
策略:小字段(< max_field_chars)完整保留;大字段(长列表/长字符串)截断,
|
|
193
|
+
保留关键摘要信息(如 errors 列表的前几条、success 标志等)。
|
|
194
|
+
相比直接对 JSON 文本硬截断,避免在字段中间被切断导致 agent 误解。
|
|
195
|
+
"""
|
|
196
|
+
summarized: dict[str, Any] = {}
|
|
197
|
+
for key, value in data.items():
|
|
198
|
+
# 关键小字段始终完整保留
|
|
199
|
+
if key in {"success", "error", "message", "total_rules", "passed", "issues"}:
|
|
200
|
+
summarized[key] = value
|
|
201
|
+
continue
|
|
202
|
+
if isinstance(value, str):
|
|
203
|
+
summarized[key] = value if len(value) <= max_field_chars else value[:max_field_chars] + "...[truncated]"
|
|
204
|
+
elif isinstance(value, list):
|
|
205
|
+
# 列表:保留前 5 项 + 计数提示
|
|
206
|
+
if len(value) <= 5:
|
|
207
|
+
summarized[key] = value
|
|
208
|
+
else:
|
|
209
|
+
summarized[key] = value[:5] + [f"...[{len(value) - 5} more items]"]
|
|
210
|
+
elif isinstance(value, dict):
|
|
211
|
+
# 嵌套 dict 递归裁剪(降阈值避免嵌套膨胀)
|
|
212
|
+
summarized[key] = AgentMemory._summarize_dict(value, max_field_chars // 2)
|
|
213
|
+
else:
|
|
214
|
+
summarized[key] = value
|
|
215
|
+
return summarized
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _dict_to_observation(data: dict[str, Any]) -> str:
|
|
219
|
+
"""将字典转为 observation 文本(先智能裁剪大字段,避免 JSON 中途被硬截断)。"""
|
|
220
|
+
import json
|
|
221
|
+
|
|
222
|
+
summarized = AgentMemory._summarize_dict(data)
|
|
223
|
+
try:
|
|
224
|
+
return json.dumps(summarized, ensure_ascii=False, indent=2)
|
|
225
|
+
except (TypeError, ValueError):
|
|
226
|
+
return str(summarized)
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _estimate_tokens(text: str) -> int:
|
|
230
|
+
"""粗略估算 token 数。
|
|
231
|
+
|
|
232
|
+
委托给 utils.estimate_tokens 统一实现(中文/英文词/数字/标点分类加权),
|
|
233
|
+
避免项目内两套不一致的 token 估算系数。延迟导入防循环依赖。
|
|
234
|
+
"""
|
|
235
|
+
if not text:
|
|
236
|
+
return 0
|
|
237
|
+
from app.shared.services.ai.utils import estimate_tokens
|
|
238
|
+
|
|
239
|
+
return estimate_tokens(text)
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def _estimate_message_tokens(cls, msg: dict[str, Any]) -> int:
|
|
243
|
+
"""估算单条消息 token 数。"""
|
|
244
|
+
content = msg.get("content") or ""
|
|
245
|
+
tokens = cls._estimate_tokens(content)
|
|
246
|
+
# tool_calls 也计入
|
|
247
|
+
tool_calls = msg.get("tool_calls") or []
|
|
248
|
+
for tc in tool_calls:
|
|
249
|
+
func = tc.get("function", {})
|
|
250
|
+
tokens += cls._estimate_tokens(func.get("name", ""))
|
|
251
|
+
args = func.get("arguments")
|
|
252
|
+
if isinstance(args, str):
|
|
253
|
+
tokens += cls._estimate_tokens(args)
|
|
254
|
+
elif isinstance(args, dict):
|
|
255
|
+
tokens += cls._estimate_tokens(str(args))
|
|
256
|
+
return tokens + 4 # 消息格式开销
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""@fileoverview Agent 分块规划器
|
|
17
|
+
|
|
18
|
+
功能概述:
|
|
19
|
+
- 根据数据文件数量、列数自动决定分块策略
|
|
20
|
+
- 输出 chunk 列表,每个 chunk 包含要处理的文件/列范围
|
|
21
|
+
|
|
22
|
+
架构设计:
|
|
23
|
+
- 简单启发式:文件数多按文件分,列数多按列分
|
|
24
|
+
- 每个 chunk 独立生成配置,最后合并
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Chunk:
|
|
35
|
+
"""单个分块"""
|
|
36
|
+
|
|
37
|
+
chunk_id: str
|
|
38
|
+
file_paths: list[str] = field(default_factory=list)
|
|
39
|
+
table_names: list[str] = field(default_factory=list)
|
|
40
|
+
columns: list[str] = field(default_factory=list)
|
|
41
|
+
reason: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ChunkPlan:
|
|
46
|
+
"""分块计划"""
|
|
47
|
+
|
|
48
|
+
chunks: list[Chunk] = field(default_factory=list)
|
|
49
|
+
strategy: str = ""
|
|
50
|
+
reason: str = ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_chunk_plan(
|
|
54
|
+
profiling_data: list[dict],
|
|
55
|
+
file_paths: list[str],
|
|
56
|
+
max_columns_per_chunk: int = 20,
|
|
57
|
+
max_files_per_chunk: int = 5,
|
|
58
|
+
) -> ChunkPlan:
|
|
59
|
+
"""
|
|
60
|
+
@methoddesc 构建分块计划
|
|
61
|
+
|
|
62
|
+
参数:
|
|
63
|
+
profiling_data: 数据画像
|
|
64
|
+
file_paths: 所有文件路径
|
|
65
|
+
max_columns_per_chunk: 每个 chunk 最大列数
|
|
66
|
+
max_files_per_chunk: 每个 chunk 最大文件数
|
|
67
|
+
|
|
68
|
+
返回:
|
|
69
|
+
ChunkPlan 分块计划
|
|
70
|
+
"""
|
|
71
|
+
total_files = len(profiling_data)
|
|
72
|
+
total_columns = sum(len(item.get("columns", [])) for item in profiling_data)
|
|
73
|
+
|
|
74
|
+
# 决定策略
|
|
75
|
+
if total_files > max_files_per_chunk:
|
|
76
|
+
strategy = "by_file"
|
|
77
|
+
chunks = _chunk_by_file(profiling_data, max_files_per_chunk)
|
|
78
|
+
elif total_columns > max_columns_per_chunk:
|
|
79
|
+
strategy = "by_column"
|
|
80
|
+
chunks = _chunk_by_column(profiling_data, max_columns_per_chunk)
|
|
81
|
+
else:
|
|
82
|
+
strategy = "single"
|
|
83
|
+
chunks = [
|
|
84
|
+
Chunk(
|
|
85
|
+
chunk_id="single",
|
|
86
|
+
file_paths=file_paths,
|
|
87
|
+
table_names=[item.get("table_name", "") for item in profiling_data],
|
|
88
|
+
reason="数据量较小,一次性处理",
|
|
89
|
+
)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
reason = f"共 {total_files} 个文件、{total_columns} 列,采用 {strategy} 策略"
|
|
93
|
+
return ChunkPlan(chunks=chunks, strategy=strategy, reason=reason)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _chunk_by_file(profiling_data: list[dict], max_files_per_chunk: int) -> list[Chunk]:
|
|
97
|
+
"""按文件分块。"""
|
|
98
|
+
chunks: list[Chunk] = []
|
|
99
|
+
for i in range(0, len(profiling_data), max_files_per_chunk):
|
|
100
|
+
group = profiling_data[i : i + max_files_per_chunk]
|
|
101
|
+
chunk_id = f"files_{i // max_files_per_chunk + 1}"
|
|
102
|
+
chunks.append(
|
|
103
|
+
Chunk(
|
|
104
|
+
chunk_id=chunk_id,
|
|
105
|
+
file_paths=[item.get("path", "") for item in group],
|
|
106
|
+
table_names=[item.get("table_name", "") for item in group],
|
|
107
|
+
reason=f"处理第 {i + 1}-{min(i + max_files_per_chunk, len(profiling_data))} 个文件",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
return chunks
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _chunk_by_column(profiling_data: list[dict], max_columns_per_chunk: int) -> list[Chunk]:
|
|
114
|
+
"""按列分块(优先保持同文件列在一起,但单表列数超过限制时拆表)。"""
|
|
115
|
+
chunks: list[Chunk] = []
|
|
116
|
+
chunk_index = 1
|
|
117
|
+
|
|
118
|
+
for item in profiling_data:
|
|
119
|
+
table_name = item.get("table_name", "")
|
|
120
|
+
path = item.get("path", "")
|
|
121
|
+
columns = item.get("columns", [])
|
|
122
|
+
col_names = [f"{table_name}.{col.get('name', '')}" for col in columns]
|
|
123
|
+
|
|
124
|
+
# 如果单表列数超过限制,拆分该表
|
|
125
|
+
if len(col_names) > max_columns_per_chunk:
|
|
126
|
+
for i in range(0, len(col_names), max_columns_per_chunk):
|
|
127
|
+
chunk = Chunk(
|
|
128
|
+
chunk_id=f"cols_{chunk_index}",
|
|
129
|
+
file_paths=[path],
|
|
130
|
+
table_names=[table_name],
|
|
131
|
+
columns=col_names[i : i + max_columns_per_chunk],
|
|
132
|
+
reason=f"表 {table_name} 列数过多,拆分为第 {i // max_columns_per_chunk + 1} 组",
|
|
133
|
+
)
|
|
134
|
+
chunks.append(chunk)
|
|
135
|
+
chunk_index += 1
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# 否则尝试加入当前 chunk
|
|
139
|
+
if chunks and len(chunks[-1].columns) + len(col_names) <= max_columns_per_chunk:
|
|
140
|
+
current = chunks[-1]
|
|
141
|
+
if path and path not in current.file_paths:
|
|
142
|
+
current.file_paths.append(path)
|
|
143
|
+
if table_name and table_name not in current.table_names:
|
|
144
|
+
current.table_names.append(table_name)
|
|
145
|
+
current.columns.extend(col_names)
|
|
146
|
+
current.reason = f"按列分组,共 {len(current.columns)} 列"
|
|
147
|
+
else:
|
|
148
|
+
chunk = Chunk(
|
|
149
|
+
chunk_id=f"cols_{chunk_index}",
|
|
150
|
+
file_paths=[path],
|
|
151
|
+
table_names=[table_name],
|
|
152
|
+
columns=col_names,
|
|
153
|
+
reason=f"按列分组,共 {len(col_names)} 列",
|
|
154
|
+
)
|
|
155
|
+
chunks.append(chunk)
|
|
156
|
+
chunk_index += 1
|
|
157
|
+
|
|
158
|
+
return chunks
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def plan_to_dict(plan: ChunkPlan) -> dict[str, Any]:
|
|
162
|
+
"""将 ChunkPlan 转为可 JSON 序列化的字典。"""
|
|
163
|
+
return {
|
|
164
|
+
"strategy": plan.strategy,
|
|
165
|
+
"reason": plan.reason,
|
|
166
|
+
"chunks": [
|
|
167
|
+
{
|
|
168
|
+
"chunk_id": c.chunk_id,
|
|
169
|
+
"file_paths": c.file_paths,
|
|
170
|
+
"table_names": c.table_names,
|
|
171
|
+
"columns": c.columns,
|
|
172
|
+
"reason": c.reason,
|
|
173
|
+
}
|
|
174
|
+
for c in plan.chunks
|
|
175
|
+
],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ===== 按源(source)分块(迁移专用,不影响 generation 主路径)=====
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class SourceChunk:
|
|
184
|
+
"""单个来源分片"""
|
|
185
|
+
|
|
186
|
+
chunk_id: str
|
|
187
|
+
source_names: list[str] = field(default_factory=list)
|
|
188
|
+
source_indices: list[int] = field(default_factory=list) # 指向 parsed_intents 原始列表
|
|
189
|
+
reason: str = ""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class SourceChunkPlan:
|
|
194
|
+
"""来源分块计划"""
|
|
195
|
+
|
|
196
|
+
chunks: list[SourceChunk] = field(default_factory=list)
|
|
197
|
+
strategy: str = "" # single | by_source
|
|
198
|
+
reason: str = ""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _estimate_intent_tokens(intent_text: str) -> int:
|
|
202
|
+
"""保守估算意图文本 token 数。"""
|
|
203
|
+
return max(1, len(intent_text or "") // 3)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def build_source_chunk_plan(
|
|
207
|
+
parsed_intents: list[dict],
|
|
208
|
+
max_sources_per_chunk: int = 5,
|
|
209
|
+
max_tokens_per_chunk: int = 8000,
|
|
210
|
+
) -> SourceChunkPlan:
|
|
211
|
+
"""
|
|
212
|
+
@methoddesc 构建按源分块计划
|
|
213
|
+
|
|
214
|
+
参数:
|
|
215
|
+
parsed_intents: 解析后的来源意图列表,每个元素含 name/intent
|
|
216
|
+
max_sources_per_chunk: 每个分片最大来源数
|
|
217
|
+
max_tokens_per_chunk: 每个分片最大估算 token 数
|
|
218
|
+
|
|
219
|
+
返回:
|
|
220
|
+
SourceChunkPlan 分块计划
|
|
221
|
+
"""
|
|
222
|
+
n = len(parsed_intents)
|
|
223
|
+
if n == 0:
|
|
224
|
+
return SourceChunkPlan(chunks=[], strategy="single", reason="无来源,无需分片")
|
|
225
|
+
|
|
226
|
+
total_tokens = sum(_estimate_intent_tokens(it.get("intent", "")) for it in parsed_intents)
|
|
227
|
+
if n <= max_sources_per_chunk and total_tokens <= max_tokens_per_chunk:
|
|
228
|
+
return SourceChunkPlan(
|
|
229
|
+
chunks=[
|
|
230
|
+
SourceChunk(
|
|
231
|
+
chunk_id="source_single",
|
|
232
|
+
source_names=[it.get("name") or f"source_{idx + 1}" for idx, it in enumerate(parsed_intents)],
|
|
233
|
+
source_indices=list(range(n)),
|
|
234
|
+
reason=f"来源数 {n}、估算 {total_tokens} token,单次处理",
|
|
235
|
+
)
|
|
236
|
+
],
|
|
237
|
+
strategy="single",
|
|
238
|
+
reason=f"共 {n} 个来源,单次处理",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
chunks: list[SourceChunk] = []
|
|
242
|
+
current = SourceChunk(chunk_id="source_chunk_1")
|
|
243
|
+
current_tokens = 0
|
|
244
|
+
for idx, it in enumerate(parsed_intents):
|
|
245
|
+
it_tokens = _estimate_intent_tokens(it.get("intent", ""))
|
|
246
|
+
over_sources = len(current.source_indices) >= max_sources_per_chunk
|
|
247
|
+
over_tokens = current_tokens + it_tokens > max_tokens_per_chunk and current.source_indices
|
|
248
|
+
if over_sources or over_tokens:
|
|
249
|
+
current.reason = f"分片含 {len(current.source_indices)} 个来源,约 {current_tokens} token"
|
|
250
|
+
chunks.append(current)
|
|
251
|
+
current = SourceChunk(chunk_id=f"source_chunk_{len(chunks) + 1}")
|
|
252
|
+
current_tokens = 0
|
|
253
|
+
current.source_indices.append(idx)
|
|
254
|
+
current.source_names.append(it.get("name", f"source_{idx + 1}"))
|
|
255
|
+
current_tokens += it_tokens
|
|
256
|
+
|
|
257
|
+
if current.source_indices:
|
|
258
|
+
current.reason = f"分片含 {len(current.source_indices)} 个来源,约 {current_tokens} token"
|
|
259
|
+
chunks.append(current)
|
|
260
|
+
|
|
261
|
+
return SourceChunkPlan(
|
|
262
|
+
chunks=chunks,
|
|
263
|
+
strategy="by_source",
|
|
264
|
+
reason=f"共 {n} 个来源、约 {total_tokens} token,按源拆分为 {len(chunks)} 个分片",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def source_plan_to_dict(plan: SourceChunkPlan) -> dict[str, Any]:
|
|
269
|
+
"""将 SourceChunkPlan 转为可 JSON 序列化的字典。"""
|
|
270
|
+
return {
|
|
271
|
+
"strategy": plan.strategy,
|
|
272
|
+
"reason": plan.reason,
|
|
273
|
+
"chunks": [
|
|
274
|
+
{
|
|
275
|
+
"chunk_id": c.chunk_id,
|
|
276
|
+
"source_names": c.source_names,
|
|
277
|
+
"source_indices": c.source_indices,
|
|
278
|
+
"reason": c.reason,
|
|
279
|
+
}
|
|
280
|
+
for c in plan.chunks
|
|
281
|
+
],
|
|
282
|
+
}
|