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,498 @@
|
|
|
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 Ollama 原生 Provider
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 使用 Ollama 原生 API(/api/chat、/api/tags)进行对话
|
|
21
|
+
- 实现非流式对话、流式对话、模型列表、健康检查
|
|
22
|
+
- 自动重试机制(指数退避,最多 3 次)
|
|
23
|
+
- Session 复用:通过 aiohttp.ClientSession 保持连接池
|
|
24
|
+
|
|
25
|
+
架构设计:
|
|
26
|
+
- 继承 BaseProvider,实现统一的 Provider 接口
|
|
27
|
+
- 使用 aiohttp 进行原生异步 HTTP 通信
|
|
28
|
+
- 与 OpenAIProvider 并行存在,供用户根据部署环境选择
|
|
29
|
+
|
|
30
|
+
输入示例:
|
|
31
|
+
req = ChatRequest(
|
|
32
|
+
messages=[ChatMessage(role="user", content="你好")],
|
|
33
|
+
temperature=0.7
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
输出示例:
|
|
37
|
+
resp = await provider.chat(req)
|
|
38
|
+
# ChatResponse(content="你好!", model="llama3.2")
|
|
39
|
+
|
|
40
|
+
async for chunk in provider.chat_stream(req):
|
|
41
|
+
print(chunk, end="")
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import asyncio
|
|
47
|
+
import json
|
|
48
|
+
import logging
|
|
49
|
+
from collections.abc import AsyncIterator
|
|
50
|
+
from typing import TYPE_CHECKING, Any
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
import aiohttp as _aiohttp
|
|
54
|
+
|
|
55
|
+
from app.shared.services.llm.config.models import AIProvider
|
|
56
|
+
else:
|
|
57
|
+
try:
|
|
58
|
+
import aiohttp as _aiohttp
|
|
59
|
+
except ImportError:
|
|
60
|
+
_aiohttp = None
|
|
61
|
+
|
|
62
|
+
from .base import BaseProvider, ChatRequest, ChatResponse, StreamChunk
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
_MAX_RETRIES = 3
|
|
67
|
+
_RETRY_BASE_DELAY = 1.0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OllamaProvider(BaseProvider):
|
|
71
|
+
"""
|
|
72
|
+
@classdesc Ollama 原生 Provider
|
|
73
|
+
|
|
74
|
+
使用 Ollama 原生 API(/api/chat、/api/tags)进行对话。
|
|
75
|
+
支持非流式对话、流式对话、模型列表、健康检查。
|
|
76
|
+
具备自动重试机制(指数退避,最多 3 次)。
|
|
77
|
+
|
|
78
|
+
使用场景:
|
|
79
|
+
- 本地部署的 Ollama 服务
|
|
80
|
+
- 需要原生 Ollama 特性的场景
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def name(self) -> str:
|
|
85
|
+
return "Ollama"
|
|
86
|
+
|
|
87
|
+
def __init__(self, config: AIProvider) -> None:
|
|
88
|
+
super().__init__(config)
|
|
89
|
+
self.timeout_seconds = config.network.timeout if config.network else 60
|
|
90
|
+
self._session: _aiohttp.ClientSession | None = None
|
|
91
|
+
# 按模型名缓存探测到的上下文窗口,避免每次都发起 /api/show 请求
|
|
92
|
+
self._context_window_cache: dict[str, int] = {}
|
|
93
|
+
|
|
94
|
+
async def _get_session(self) -> _aiohttp.ClientSession:
|
|
95
|
+
"""
|
|
96
|
+
@methoddesc 获取或创建 aiohttp 会话(懒加载 + 自动重建)
|
|
97
|
+
|
|
98
|
+
返回:
|
|
99
|
+
aiohttp.ClientSession 实例
|
|
100
|
+
"""
|
|
101
|
+
if _aiohttp is None:
|
|
102
|
+
raise ImportError("aiohttp 未安装,请运行 pip install aiohttp")
|
|
103
|
+
if self._session is None or self._session.closed:
|
|
104
|
+
timeout = _aiohttp.ClientTimeout(total=self.timeout_seconds)
|
|
105
|
+
self._session = _aiohttp.ClientSession(timeout=timeout)
|
|
106
|
+
return self._session
|
|
107
|
+
|
|
108
|
+
def _build_stream_timeout(self) -> Any:
|
|
109
|
+
"""构造流式请求专用超时
|
|
110
|
+
|
|
111
|
+
会话默认 ClientTimeout(total=60s) 会覆盖整个流式生命周期:本地模型生成长回复时
|
|
112
|
+
总时长轻松超过 60s,导致流被中途掐断。流式路径改为:
|
|
113
|
+
- total=None:不限流式总时长
|
|
114
|
+
- connect:保留建连超时(服务不可达仍能快速失败)
|
|
115
|
+
- sock_read:chunk 间隔空闲超时(服务端挂死仍可退出,正常逐 token 输出不会触发)
|
|
116
|
+
"""
|
|
117
|
+
if _aiohttp is None:
|
|
118
|
+
raise ImportError("aiohttp 未安装,请运行 pip install aiohttp")
|
|
119
|
+
return _aiohttp.ClientTimeout(total=None, connect=self.timeout_seconds, sock_read=self.timeout_seconds)
|
|
120
|
+
|
|
121
|
+
async def close(self) -> None:
|
|
122
|
+
"""
|
|
123
|
+
@methoddesc 显式关闭 HTTP 会话,释放连接池资源
|
|
124
|
+
|
|
125
|
+
建议在不再需要 Provider 时调用,避免资源泄漏。
|
|
126
|
+
"""
|
|
127
|
+
if self._session and not self._session.closed:
|
|
128
|
+
await self._session.close()
|
|
129
|
+
self._session = None
|
|
130
|
+
|
|
131
|
+
def __del__(self) -> None:
|
|
132
|
+
"""
|
|
133
|
+
@methoddesc 析构函数
|
|
134
|
+
|
|
135
|
+
如果 session 未关闭则发出资源警告。
|
|
136
|
+
"""
|
|
137
|
+
if self._session and not self._session.closed:
|
|
138
|
+
import warnings
|
|
139
|
+
|
|
140
|
+
warnings.warn("OllamaProvider 未显式关闭 session,请在使用完毕后调用 close()", ResourceWarning)
|
|
141
|
+
|
|
142
|
+
async def _post(self, endpoint: str, data: dict) -> dict:
|
|
143
|
+
if _aiohttp is None:
|
|
144
|
+
raise ImportError("aiohttp 未安装,请运行 pip install aiohttp")
|
|
145
|
+
|
|
146
|
+
url = f"{self.cfg.base_url}/api/{endpoint}"
|
|
147
|
+
for attempt in range(_MAX_RETRIES):
|
|
148
|
+
try:
|
|
149
|
+
session = await self._get_session()
|
|
150
|
+
async with session.post(url, json=data) as resp:
|
|
151
|
+
if resp.status >= 500:
|
|
152
|
+
raise _aiohttp.ClientResponseError(
|
|
153
|
+
request_info=resp.request_info,
|
|
154
|
+
history=resp.history,
|
|
155
|
+
status=resp.status,
|
|
156
|
+
message=f"服务端错误: {resp.status}",
|
|
157
|
+
)
|
|
158
|
+
if resp.status == 429:
|
|
159
|
+
raise _aiohttp.ClientResponseError(
|
|
160
|
+
request_info=resp.request_info,
|
|
161
|
+
history=resp.history,
|
|
162
|
+
status=resp.status,
|
|
163
|
+
message="请求被限流",
|
|
164
|
+
)
|
|
165
|
+
if resp.status >= 400:
|
|
166
|
+
text = await resp.text()
|
|
167
|
+
raise _aiohttp.ClientResponseError(
|
|
168
|
+
request_info=resp.request_info,
|
|
169
|
+
history=resp.history,
|
|
170
|
+
status=resp.status,
|
|
171
|
+
message=f"请求失败({resp.status}): {text[:200]}",
|
|
172
|
+
)
|
|
173
|
+
data = await resp.json()
|
|
174
|
+
if not isinstance(data, dict):
|
|
175
|
+
raise ValueError(f"Ollama 返回非 JSON 对象: {data!r}")
|
|
176
|
+
return data
|
|
177
|
+
except (TimeoutError, _aiohttp.ClientConnectionError, _aiohttp.ClientResponseError) as e:
|
|
178
|
+
# 非流式 chat 是"生成"类昂贵且非幂等的操作:
|
|
179
|
+
# - 仅连接错误(请求未送达服务端)可安全重试
|
|
180
|
+
# - 超时/429/5xx 说明服务端可能已开始生成,自动重试会重复消耗生成成本,
|
|
181
|
+
# 故不再重试,直接抛出由上层决定是否重发
|
|
182
|
+
should_retry = isinstance(e, _aiohttp.ClientConnectionError)
|
|
183
|
+
if should_retry and attempt < _MAX_RETRIES - 1:
|
|
184
|
+
delay = _RETRY_BASE_DELAY * (2**attempt)
|
|
185
|
+
logger.warning(f"[Ollama] 请求失败(第 {attempt + 1} 次),{delay:.1f}s 后重试: {e}")
|
|
186
|
+
await asyncio.sleep(delay)
|
|
187
|
+
else:
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
# 所有重试已耗尽且未正常返回,理论上不会到达此处
|
|
191
|
+
raise RuntimeError("Ollama 请求在重试后仍未完成")
|
|
192
|
+
|
|
193
|
+
def _build_messages_payload(self, req: ChatRequest) -> list[dict[str, Any]]:
|
|
194
|
+
"""构造 Ollama 对话消息 payload,支持 tool_calls 与 tool_call_id。"""
|
|
195
|
+
messages_payload: list[dict[str, Any]] = []
|
|
196
|
+
for m in req.messages:
|
|
197
|
+
msg: dict[str, Any] = {"role": m.role}
|
|
198
|
+
if m.content is not None:
|
|
199
|
+
msg["content"] = m.content
|
|
200
|
+
if m.tool_calls is not None:
|
|
201
|
+
msg["tool_calls"] = m.tool_calls
|
|
202
|
+
if m.tool_call_id is not None:
|
|
203
|
+
msg["tool_call_id"] = m.tool_call_id
|
|
204
|
+
messages_payload.append(msg)
|
|
205
|
+
return messages_payload
|
|
206
|
+
|
|
207
|
+
def _build_chat_options(self, req: ChatRequest) -> dict[str, Any]:
|
|
208
|
+
"""构造 Ollama /api/chat 请求体,按需附加 tools/tool_choice。"""
|
|
209
|
+
data: dict[str, Any] = {
|
|
210
|
+
"model": self._get_model(req.model),
|
|
211
|
+
"messages": self._build_messages_payload(req),
|
|
212
|
+
"options": {"temperature": req.temperature},
|
|
213
|
+
}
|
|
214
|
+
if req.tools:
|
|
215
|
+
data["tools"] = req.tools
|
|
216
|
+
if req.tool_choice is not None:
|
|
217
|
+
data["tool_choice"] = req.tool_choice
|
|
218
|
+
return data
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _parse_ollama_tool_calls(message: dict[str, Any]) -> list[dict[str, Any]] | None:
|
|
222
|
+
"""从 Ollama 响应 message 中解析 tool_calls。"""
|
|
223
|
+
raw_tool_calls = message.get("tool_calls")
|
|
224
|
+
if not raw_tool_calls:
|
|
225
|
+
return None
|
|
226
|
+
tool_calls: list[dict[str, Any]] = []
|
|
227
|
+
for tc in raw_tool_calls:
|
|
228
|
+
function_info = tc.get("function", {})
|
|
229
|
+
tool_calls.append(
|
|
230
|
+
{
|
|
231
|
+
"id": tc.get("id", ""),
|
|
232
|
+
"type": tc.get("type", "function"),
|
|
233
|
+
"function": {
|
|
234
|
+
"name": function_info.get("name", ""),
|
|
235
|
+
"arguments": function_info.get("arguments", ""),
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
return tool_calls
|
|
240
|
+
|
|
241
|
+
async def chat(self, req: ChatRequest) -> ChatResponse:
|
|
242
|
+
"""
|
|
243
|
+
@methoddesc 发送非流式对话请求
|
|
244
|
+
|
|
245
|
+
参数:
|
|
246
|
+
req: 对话请求对象
|
|
247
|
+
|
|
248
|
+
返回:
|
|
249
|
+
ChatResponse: AI 回复内容
|
|
250
|
+
|
|
251
|
+
异常:
|
|
252
|
+
ValueError: 响应解析失败
|
|
253
|
+
"""
|
|
254
|
+
data = self._build_chat_options(req)
|
|
255
|
+
data["stream"] = False
|
|
256
|
+
resp = await self._post("chat", data)
|
|
257
|
+
try:
|
|
258
|
+
message = resp["message"]
|
|
259
|
+
content = message.get("content", "")
|
|
260
|
+
model = resp.get("model", self._get_model(req.model))
|
|
261
|
+
tool_calls = self._parse_ollama_tool_calls(message)
|
|
262
|
+
except (KeyError, TypeError) as e:
|
|
263
|
+
raise ValueError(f"解析 Ollama 响应失败: {e}, 响应: {resp}") from e
|
|
264
|
+
return ChatResponse(content=content or "", tool_calls=tool_calls, model=model)
|
|
265
|
+
|
|
266
|
+
async def chat_stream(self, req: ChatRequest) -> AsyncIterator[StreamChunk]:
|
|
267
|
+
"""
|
|
268
|
+
@methoddesc 发送流式对话请求(支持 tool_calls),逐块返回统一 StreamChunk
|
|
269
|
+
|
|
270
|
+
统一输出契约:
|
|
271
|
+
- delta 文本 → StreamChunk(type="delta", text=...)
|
|
272
|
+
- tool_calls(单 chunk 完整体到达时) → StreamChunk(type="tool_calls", tool_calls=[...OpenAI 格式])
|
|
273
|
+
|
|
274
|
+
重试策略(与 chat/_post 对齐):
|
|
275
|
+
- 连接错误(ClientConnectionError/TimeoutError)或限流/服务端错误(429/500/502/503)触发指数退避重试,最多 _MAX_RETRIES 次。
|
|
276
|
+
- **关键安全门**:只在"尚未 yield 任何 chunk 前"重试。一旦已经向下游输出过 token,
|
|
277
|
+
即使后续流中断也不重试(避免重复输出导致内容错乱)。
|
|
278
|
+
- 已 yield 后的异常直接抛出,由上层 executor 处理。
|
|
279
|
+
|
|
280
|
+
参数:
|
|
281
|
+
req: 对话请求对象
|
|
282
|
+
|
|
283
|
+
返回:
|
|
284
|
+
AsyncIterator[StreamChunk]: 统一流式输出单元
|
|
285
|
+
"""
|
|
286
|
+
if _aiohttp is None:
|
|
287
|
+
raise ImportError("aiohttp 未安装,请运行 pip install aiohttp")
|
|
288
|
+
|
|
289
|
+
data = self._build_chat_options(req)
|
|
290
|
+
data["stream"] = True
|
|
291
|
+
url = f"{self.cfg.base_url}/api/chat"
|
|
292
|
+
|
|
293
|
+
last_exc: Exception | None = None
|
|
294
|
+
for attempt in range(_MAX_RETRIES):
|
|
295
|
+
# 本轮标志:是否已向下游 yield 过任何 chunk。决定异常时能否重试。
|
|
296
|
+
yielded_any = False
|
|
297
|
+
try:
|
|
298
|
+
session = await self._get_session()
|
|
299
|
+
# 流式请求使用专用超时(total=None),避免会话默认 total=60s 掐断长回复
|
|
300
|
+
async with session.post(url, json=data, timeout=self._build_stream_timeout()) as resp:
|
|
301
|
+
if resp.status >= 400:
|
|
302
|
+
text = await resp.text()
|
|
303
|
+
raise _aiohttp.ClientResponseError(
|
|
304
|
+
request_info=resp.request_info,
|
|
305
|
+
history=resp.history,
|
|
306
|
+
status=resp.status,
|
|
307
|
+
message=f"流式请求失败({resp.status}): {text[:200]}",
|
|
308
|
+
)
|
|
309
|
+
async for line in resp.content:
|
|
310
|
+
if not line:
|
|
311
|
+
continue
|
|
312
|
+
try:
|
|
313
|
+
chunk = json.loads(line)
|
|
314
|
+
except json.JSONDecodeError:
|
|
315
|
+
continue
|
|
316
|
+
if "error" in chunk:
|
|
317
|
+
raise ValueError(f"Ollama 流式错误: {chunk['error']}")
|
|
318
|
+
if "message" not in chunk:
|
|
319
|
+
continue
|
|
320
|
+
message = chunk["message"]
|
|
321
|
+
# 1. 文本增量 → yield delta
|
|
322
|
+
content = message.get("content", "")
|
|
323
|
+
if content:
|
|
324
|
+
yielded_any = True
|
|
325
|
+
yield StreamChunk(type="delta", text=content)
|
|
326
|
+
# 2. tool_calls 完整体 → yield tool_calls(复用已有解析逻辑,转 OpenAI 格式)
|
|
327
|
+
parsed_tcs = self._parse_ollama_tool_calls(message)
|
|
328
|
+
if parsed_tcs:
|
|
329
|
+
yielded_any = True
|
|
330
|
+
yield StreamChunk(type="tool_calls", tool_calls=parsed_tcs)
|
|
331
|
+
# 流正常耗尽,返回
|
|
332
|
+
return
|
|
333
|
+
except (TimeoutError, _aiohttp.ClientConnectionError, _aiohttp.ClientResponseError) as e:
|
|
334
|
+
last_exc = e
|
|
335
|
+
# 判断是否可重试:连接错误/超时,或状态码在 429/500/502/503
|
|
336
|
+
should_retry = isinstance(e, (_aiohttp.ClientConnectionError, asyncio.TimeoutError)) or (
|
|
337
|
+
isinstance(e, _aiohttp.ClientResponseError) and e.status in (429, 500, 502, 503)
|
|
338
|
+
)
|
|
339
|
+
if not should_retry:
|
|
340
|
+
raise
|
|
341
|
+
# 已 yield 出 chunk 则不再重试(无法回收已发出的内容)
|
|
342
|
+
if yielded_any:
|
|
343
|
+
raise
|
|
344
|
+
# 未达最大重试次数 → 指数退避后重试
|
|
345
|
+
if attempt < _MAX_RETRIES - 1:
|
|
346
|
+
delay = _RETRY_BASE_DELAY * (2**attempt)
|
|
347
|
+
logger.warning(f"[Ollama] 流式请求失败(第 {attempt + 1} 次),{delay:.1f}s 后重试: {e}")
|
|
348
|
+
await asyncio.sleep(delay)
|
|
349
|
+
else:
|
|
350
|
+
raise
|
|
351
|
+
except Exception as e:
|
|
352
|
+
# 已 yield 出 chunk 后的非预期异常:不可重试
|
|
353
|
+
if yielded_any:
|
|
354
|
+
raise ValueError(f"Ollama 流式响应异常(已输出部分内容): {e}") from e
|
|
355
|
+
# 未 yield 前的解析类异常:不重试
|
|
356
|
+
raise ValueError(f"Ollama 流式响应异常: {e}") from e
|
|
357
|
+
|
|
358
|
+
# 理论上不会到达(重试耗尽会在循环内 raise)
|
|
359
|
+
if last_exc is not None:
|
|
360
|
+
raise last_exc
|
|
361
|
+
raise RuntimeError("Ollama 流式请求在重试后仍未完成")
|
|
362
|
+
|
|
363
|
+
def _resolve_context_window(self, model: str | None) -> int | None:
|
|
364
|
+
"""
|
|
365
|
+
@methoddesc 通过 Ollama /api/show 同步探测模型的上下文窗口
|
|
366
|
+
|
|
367
|
+
查询本地 Ollama 服务获取模型真实 context_length,按模型名缓存到实例。
|
|
368
|
+
探测失败(服务不可达、响应格式异常、字段缺失)时静默返回 None,
|
|
369
|
+
由基类 get_context_window 回退到全局默认值。
|
|
370
|
+
|
|
371
|
+
使用标准库 urllib 发起同步请求(Ollama 通常运行在 localhost,延迟极低),
|
|
372
|
+
避免引入 requests 等额外依赖,也避免在同步调用上下文中处理 async。
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
model: 模型名称,None 则使用配置中的默认模型
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
探测到的上下文窗口大小(tokens),探测不到返回 None
|
|
379
|
+
"""
|
|
380
|
+
model_name = (model or self.cfg.model).strip()
|
|
381
|
+
if not model_name:
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
# 命中缓存直接返回
|
|
385
|
+
if model_name in self._context_window_cache:
|
|
386
|
+
return self._context_window_cache[model_name]
|
|
387
|
+
|
|
388
|
+
url = f"{self.cfg.base_url}/api/show"
|
|
389
|
+
payload = json.dumps({"name": model_name}).encode("utf-8")
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
import urllib.request
|
|
393
|
+
|
|
394
|
+
req = urllib.request.Request(
|
|
395
|
+
url,
|
|
396
|
+
data=payload,
|
|
397
|
+
headers={"Content-Type": "application/json"},
|
|
398
|
+
method="POST",
|
|
399
|
+
)
|
|
400
|
+
with urllib.request.urlopen(req, timeout=self.timeout_seconds) as resp: # noqa: S310
|
|
401
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
402
|
+
|
|
403
|
+
# Ollama /api/show 返回的 context_length 位置优先级:
|
|
404
|
+
# 1. model_info."llama.context_length"(GPTForCausalLM 等架构)
|
|
405
|
+
# 2. model_info."<arch>.context_length"(通用键名后缀匹配)
|
|
406
|
+
# 3. 顶层 parameters 里的 num_ctx(部分版本)
|
|
407
|
+
model_info = data.get("model_info", {}) or {}
|
|
408
|
+
context_length = self._extract_context_length(model_info)
|
|
409
|
+
if context_length is None:
|
|
410
|
+
# 部分旧版 Ollama 在 parameters 中暴露 num_ctx
|
|
411
|
+
params = data.get("parameters", "")
|
|
412
|
+
context_length = self._parse_num_ctx(params)
|
|
413
|
+
|
|
414
|
+
if context_length and context_length > 0:
|
|
415
|
+
self._context_window_cache[model_name] = context_length
|
|
416
|
+
return context_length
|
|
417
|
+
|
|
418
|
+
logger.debug("Ollama /api/show 未返回有效的 context_length: model=%s", model_name)
|
|
419
|
+
return None
|
|
420
|
+
except Exception as e:
|
|
421
|
+
# 探测失败是预期内的降级路径(服务未启动/模型未拉取),仅记 debug 日志
|
|
422
|
+
logger.debug("Ollama 上下文窗口探测失败,将回退到默认值: model=%s, error=%s", model_name, e)
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def _extract_context_length(model_info: dict) -> int | None:
|
|
427
|
+
"""
|
|
428
|
+
@methoddesc 从 Ollama model_info 中提取 context_length
|
|
429
|
+
|
|
430
|
+
Ollama 的键名因模型架构而异(如 llama.context_length、qwen2.context_length),
|
|
431
|
+
统一按后缀 ".context_length" 匹配。
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
model_info: /api/show 返回的 model_info 字典
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
context_length 值,未找到返回 None
|
|
438
|
+
"""
|
|
439
|
+
for key, value in model_info.items():
|
|
440
|
+
if key.endswith(".context_length") and isinstance(value, (int, float)):
|
|
441
|
+
return int(value)
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
@staticmethod
|
|
445
|
+
def _parse_num_ctx(parameters: str | None) -> int | None:
|
|
446
|
+
"""
|
|
447
|
+
@methoddesc 从 Ollama parameters 字符串中解析 num_ctx
|
|
448
|
+
|
|
449
|
+
parameters 形如 "num_ctx\t8192\\nstop\t<...>",解析 num_ctx 行。
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
parameters: /api/show 返回的 parameters 字符串
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
num_ctx 值,未找到或格式异常返回 None
|
|
456
|
+
"""
|
|
457
|
+
if not parameters:
|
|
458
|
+
return None
|
|
459
|
+
for line in parameters.splitlines():
|
|
460
|
+
parts = line.split()
|
|
461
|
+
if len(parts) >= 2 and parts[0] == "num_ctx":
|
|
462
|
+
try:
|
|
463
|
+
return int(parts[1])
|
|
464
|
+
except ValueError:
|
|
465
|
+
return None
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
async def list_models(self) -> list[str]:
|
|
469
|
+
"""
|
|
470
|
+
@methoddesc 获取 Ollama 本地安装的模型列表
|
|
471
|
+
|
|
472
|
+
返回:
|
|
473
|
+
模型名称字符串列表
|
|
474
|
+
"""
|
|
475
|
+
url = f"{self.cfg.base_url}/api/tags"
|
|
476
|
+
session = await self._get_session()
|
|
477
|
+
async with session.get(url) as resp:
|
|
478
|
+
data = await resp.json()
|
|
479
|
+
return [m["name"] for m in data.get("models", [])]
|
|
480
|
+
|
|
481
|
+
async def health(self) -> dict[str, Any]:
|
|
482
|
+
"""
|
|
483
|
+
@methoddesc 健康检查
|
|
484
|
+
|
|
485
|
+
尝试获取模型列表并计算延迟。
|
|
486
|
+
|
|
487
|
+
返回:
|
|
488
|
+
{"status": "ok", "latency_ms": 延迟毫秒数} 或
|
|
489
|
+
{"status": "error", "error": 错误信息}
|
|
490
|
+
"""
|
|
491
|
+
import time
|
|
492
|
+
|
|
493
|
+
start = time.time()
|
|
494
|
+
try:
|
|
495
|
+
await self.list_models()
|
|
496
|
+
return {"status": "ok", "latency_ms": int((time.time() - start) * 1000)}
|
|
497
|
+
except Exception as e:
|
|
498
|
+
return {"status": "error", "error": str(e)}
|