abstractcore 2.20.0__tar.gz → 2.20.2__tar.gz
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.
- {abstractcore-2.20.0 → abstractcore-2.20.2}/PKG-INFO +1 -1
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/__init__.py +16 -2
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/client.py +38 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/context.py +44 -3
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/models.py +38 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/capability_defaults.py +36 -2
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/email_cli.py +71 -2
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/manager.py +33 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/model_catalog.py +52 -2
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/route_engines.py +37 -1
- abstractcore-2.20.2/abstractcore/providers/capability_host.py +72 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/app.py +7 -1
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/host_profile.py +18 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/version.py +1 -1
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/PKG-INFO +1 -1
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/SOURCES.txt +1 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/LICENSE +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/README.md +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/__main__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/app_config_utils.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/deepsearch.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/extractor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/intent.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/judge.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/apps/summarizer.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/detection.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/enums.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/response_postprocessing.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/architectures/thinking_controls.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/OCRA.ttf +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/OCRB.ttf +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/OCRBL.ttf +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/README.md +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/abstractframework_identity.json +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/architecture_formats.json +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/bloc-schema.jsonld +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_capabilities.json +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_capabilities.schema.json +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_downloads_catalog.json +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/model_downloads_catalog.schema.json +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/semantic_models.md +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/assets/session_schema.json +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/errors.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/host.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/music_selectors.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/registry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/scene3d_selectors.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/types.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/capabilities/vision_catalog.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/bodystructure.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/errors.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/imap_codec.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/legacy.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/limits.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/oauth.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/policy.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/search.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/store.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/comms/email/vault.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/analytics.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/cache.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/config.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/exceptions.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/glyph_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/optimizer.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/orchestrator.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/pil_text_renderer.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/quality.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/text_formatter.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/compression/vision_compressor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/engines.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/host_jobs.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/main.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/model_materializer.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/model_verify.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/models_engines_cli.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/provider_profiles.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/recommendations.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/config/vision_config.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/theme_sync.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/themes.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/console/web.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/bloc_kv.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/bloc_metadata.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/cached_session.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/degeneration.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/endpoint_damping.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/enums.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/factory.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/file_blocs.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/file_boxes.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/generate_contract.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/interface.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/multimodal_generation.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/output_specs.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/retry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/session.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/core/types.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/download.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/embeddings/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/embeddings/manager.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/embeddings/models.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/endpoint/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/endpoint/app.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/events/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/exceptions/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/client.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/factory.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/naming.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/stdio_client.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/mcp/tool_source.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/auto_handler.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/base.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/capabilities.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/delivery.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/enrichment.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/anthropic_handler.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/local_handler.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/handlers/openai_handler.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/openai_parts.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/pdf_routing.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/audio_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/direct_pdf_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/glyph_pdf_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/image_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/office_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/pdf_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/text_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/processors/video_processor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/types.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/utils/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/utils/image_scaler.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/utils/video_frames.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/media/vision_fallback.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_deepsearch.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_extractor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_intent.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_judge.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/processing/basic_summarizer.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/_http.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/anthropic_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/base.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/endpoint_context.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/generation_cancel.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/generation_progress.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/hf_residency.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/huggingface_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/lmstudio_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_model_rules.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_native_cache.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_native_session.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_prefill_observer.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_qwen4.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_residency.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_runtime.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/mlx_vision_addon.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/model_capabilities.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/model_config_fingerprint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/ollama_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/openai_compatible_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/openai_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/openrouter_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/portkey_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/process_residency.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/registry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/speculation.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/streaming.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/tokenizer_fingerprint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/vllm_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/providers/weights_fingerprint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/py.typed +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/__main__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/audio_endpoints.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/camera_endpoints.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/capability_generation.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/console_routes.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/credentials.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/email_routes.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/first_run.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/host_routes.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/scene3d_endpoints.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/session_routes.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/server/vision_endpoints.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/handler.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/retry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/structured/schema_compat.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/testing/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/testing/mailserver.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/abstractignore.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/arg_canonicalizer.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/arg_coercion.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/browser_tools.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/code_analysis.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/common_tools.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/comms_tools.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/core.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/email_digests.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/fetch_url_ssrf.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/handler.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/inventory.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/parser.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/process_tree.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/registry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/risk_facts.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/shell_session.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/shell_tools.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/syntax_rewriter.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/tag_rewriter.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/telegram_tdlib.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/telegram_tools.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/tools/wire_naming.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/__init__.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/async_stream.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/cli.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/context_calibration.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/context_estimate.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/data_registry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/display.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/file_filters.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/hostinfo.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/identity.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/install_settings.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/jsonish.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/memory.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/message_preprocessor.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/model_cache.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/model_fit.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/residency.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/self_fixes.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/structured_logging.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/token_utils.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/trace_export.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/truncation.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/vlm_token_calculator.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/windows_dll.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore/utils/workspace_paths.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/dependency_links.txt +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/entry_points.txt +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/requires.txt +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/abstractcore.egg-info/top_level.txt +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/pyproject.toml +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/setup.cfg +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_abstractendpoint_singleton_provider.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_agentic_cli_compatibility.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_basic_session.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_cache_dtype.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_endpoint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_engine_fingerprint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_model_config_fingerprint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_tokenizer_fingerprint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_kv_weights_fingerprint.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_bloc_metadata_json_extract.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_cached_session_kv_mode.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_cached_session_kv_thinking_prefix.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_capabilities_registry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_capabilities_registry_preferred_backends_from_config.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_complete_integration.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_comprehensive_events.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_console_web.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_core_components.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_display_rule.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_email_digests.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_embedding_model_token_warnings.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_enhanced_prompt.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_environment_variable_tool_call_tags.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_factory.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_file_bloc_store_unit.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_file_boxes_cached_session.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_final_accuracy.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_final_comprehensive.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_final_graceful_errors.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_generate_contract.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_generate_response_reasoning_property_unit.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_generate_with_outputs.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_graceful_fallback.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_import_debug.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_import_safety.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_incremental_thinking_tag_stripper.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_install_hints_no_bare_packages.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_install_settings.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_integrated_functionality.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_llms_sources.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_mcp_integration.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_mcp_stdio_client.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_multimodal_generate_output.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_openai_prompt_cache_key_passthrough.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_output_specs.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_packaging_assets.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_packaging_extras.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_api.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_bloc_composition.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_bloc_e2e_gate.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_control_plane.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_prompt_cache_prepare_modules_thinking_http.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_retry_observability.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_retry_strategy.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_seed_determinism.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_seed_temperature_basic.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_sensory_prompting.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_server_prompt_cache_control_plane_proxy.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_server_prompt_cache_key_passthrough.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_session_reasoning_persistence_unit.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_structured_output_truncation_retry.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_text_only_model_experience.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_unload_memory.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_user_scenario_validation.py +0 -0
- {abstractcore-2.20.0 → abstractcore-2.20.2}/tests/test_voice_host_credentials.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: abstractcore
|
|
3
|
-
Version: 2.20.
|
|
3
|
+
Version: 2.20.2
|
|
4
4
|
Summary: Unified interface to all LLM providers with essential infrastructure for tool calling, streaming, and model management
|
|
5
5
|
Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
6
6
|
Maintainer-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
@@ -23,8 +23,8 @@ credentials), `oauth` (OAuth2 / XOAUTH2), `store` (settings + secret + status),
|
|
|
23
23
|
(the guarded send), `errors` (typed errors), `legacy` (one-time import of pre-2.20 settings).
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
from .client import DEFAULT_MAX_MESSAGE_BYTES, MAX_LIST_LIMIT, EmailClient, tls_context
|
|
27
|
-
from .context import EmailContext, guarded_send
|
|
26
|
+
from .client import DEFAULT_MAX_MESSAGE_BYTES, MAX_LIST_LIMIT, EmailClient, auto_submitted_value, tls_context
|
|
27
|
+
from .context import EmailContext, guarded_send, mark_automatic
|
|
28
28
|
from .errors import (
|
|
29
29
|
EmailAgentToolsOff,
|
|
30
30
|
EmailAttachmentNotFound,
|
|
@@ -54,6 +54,11 @@ from .errors import (
|
|
|
54
54
|
)
|
|
55
55
|
from .limits import SendRateLimiter
|
|
56
56
|
from .models import (
|
|
57
|
+
AUTO_SUBMITTED_GENERATED,
|
|
58
|
+
AUTO_SUBMITTED_HEADER,
|
|
59
|
+
AUTO_SUBMITTED_REPLIED,
|
|
60
|
+
AUTO_SUBMITTED_VALUES,
|
|
61
|
+
AUTOMATION_MARKER_HEADER,
|
|
57
62
|
Attachment,
|
|
58
63
|
AttachmentInfo,
|
|
59
64
|
EmailAccount,
|
|
@@ -68,6 +73,7 @@ from .models import (
|
|
|
68
73
|
SendLimits,
|
|
69
74
|
SendResult,
|
|
70
75
|
SmtpSettings,
|
|
76
|
+
automation_marker_value,
|
|
71
77
|
)
|
|
72
78
|
from .oauth import (
|
|
73
79
|
BUILTIN_CLIENTS,
|
|
@@ -87,6 +93,14 @@ from .store import EmailAccountStore, EmailSettings
|
|
|
87
93
|
from .vault import SecretVault
|
|
88
94
|
|
|
89
95
|
__all__ = [
|
|
96
|
+
"AUTO_SUBMITTED_GENERATED",
|
|
97
|
+
"AUTO_SUBMITTED_HEADER",
|
|
98
|
+
"AUTO_SUBMITTED_REPLIED",
|
|
99
|
+
"AUTO_SUBMITTED_VALUES",
|
|
100
|
+
"AUTOMATION_MARKER_HEADER",
|
|
101
|
+
"auto_submitted_value",
|
|
102
|
+
"automation_marker_value",
|
|
103
|
+
"mark_automatic",
|
|
90
104
|
"DEFAULT_MAX_MESSAGE_BYTES",
|
|
91
105
|
"MAX_LIST_LIMIT",
|
|
92
106
|
"BUILTIN_CLIENTS",
|
|
@@ -61,6 +61,9 @@ from .errors import (
|
|
|
61
61
|
classify_smtp_code,
|
|
62
62
|
)
|
|
63
63
|
from .models import (
|
|
64
|
+
AUTO_SUBMITTED_HEADER,
|
|
65
|
+
AUTO_SUBMITTED_VALUES,
|
|
66
|
+
AUTOMATION_MARKER_HEADER,
|
|
64
67
|
Attachment,
|
|
65
68
|
AttachmentInfo,
|
|
66
69
|
EmailAccount,
|
|
@@ -72,6 +75,7 @@ from .models import (
|
|
|
72
75
|
OutgoingMessage,
|
|
73
76
|
SendResult,
|
|
74
77
|
ServerSettings,
|
|
78
|
+
automation_marker_value,
|
|
75
79
|
)
|
|
76
80
|
from .oauth import OAuthTokenProvider, xoauth2_string
|
|
77
81
|
from .policy import normalize_address, parse_recipients
|
|
@@ -87,6 +91,7 @@ MAX_LIST_LIMIT = 100
|
|
|
87
91
|
|
|
88
92
|
_SUMMARY_HEADERS = (
|
|
89
93
|
"FROM TO CC SUBJECT DATE MESSAGE-ID REPLY-TO IN-REPLY-TO IMPORTANCE X-PRIORITY PRIORITY LIST-UNSUBSCRIBE"
|
|
94
|
+
" AUTO-SUBMITTED X-ABSTRACTFRAMEWORK-AUTOMATION"
|
|
90
95
|
)
|
|
91
96
|
_SUMMARY_ITEMS = f"(UID FLAGS RFC822.SIZE INTERNALDATE BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS ({_SUMMARY_HEADERS})])"
|
|
92
97
|
_DETAIL_ITEMS = "(UID FLAGS RFC822.SIZE INTERNALDATE BODYSTRUCTURE BODY.PEEK[HEADER])"
|
|
@@ -348,6 +353,22 @@ def x_priority_value(header_value: str) -> Optional[int]:
|
|
|
348
353
|
return None
|
|
349
354
|
|
|
350
355
|
|
|
356
|
+
def auto_submitted_value(header_value: str) -> Optional[str]:
|
|
357
|
+
"""`Auto-Submitted:` (RFC 3834) as its lower-cased keyword without parameters.
|
|
358
|
+
|
|
359
|
+
`auto-replied; owner-email="a@b"` -> "auto-replied"; absent or empty -> None. The keyword
|
|
360
|
+
is returned as sent (an extension keyword stays itself); a value that is not a keyword
|
|
361
|
+
(spaces, punctuation) is returned as "unknown" (present and not "no").
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
v = str(header_value or "").split(";", 1)[0].strip().lower()
|
|
365
|
+
if not v:
|
|
366
|
+
return None
|
|
367
|
+
if all(c.isascii() and (c.isalnum() or c in "-_.") for c in v):
|
|
368
|
+
return v
|
|
369
|
+
return "unknown"
|
|
370
|
+
|
|
371
|
+
|
|
351
372
|
def _structure(attrs: dict) -> Optional[MimePart]:
|
|
352
373
|
tok = attrs.get("BODYSTRUCTURE")
|
|
353
374
|
if not isinstance(tok, list):
|
|
@@ -733,6 +754,8 @@ class EmailClient:
|
|
|
733
754
|
x_priority=x_priority_value(_header(msg, "X-Priority")),
|
|
734
755
|
priority=priority_value(_header(msg, "Priority")),
|
|
735
756
|
list_unsubscribe=bool(_header(msg, "List-Unsubscribe")),
|
|
757
|
+
auto_submitted=auto_submitted_value(_header(msg, AUTO_SUBMITTED_HEADER)),
|
|
758
|
+
framework_marker=automation_marker_value(_header(msg, AUTOMATION_MARKER_HEADER)),
|
|
736
759
|
)
|
|
737
760
|
|
|
738
761
|
def _cap(self, max_message_bytes: Optional[int]) -> int:
|
|
@@ -1194,6 +1217,21 @@ class EmailClient:
|
|
|
1194
1217
|
mime["In-Reply-To"] = message.in_reply_to
|
|
1195
1218
|
if message.references:
|
|
1196
1219
|
mime["References"] = " ".join(message.references)
|
|
1220
|
+
if message.auto_submitted:
|
|
1221
|
+
if message.auto_submitted not in AUTO_SUBMITTED_VALUES:
|
|
1222
|
+
raise EmailInvalidMessage(
|
|
1223
|
+
f"Auto-Submitted must be one of {', '.join(AUTO_SUBMITTED_VALUES)}, got {message.auto_submitted!r}.",
|
|
1224
|
+
"Use auto-generated for automatic mail, auto-replied for an automatic answer to one message.",
|
|
1225
|
+
)
|
|
1226
|
+
mime[AUTO_SUBMITTED_HEADER] = message.auto_submitted
|
|
1227
|
+
if message.automation_marker:
|
|
1228
|
+
marker = automation_marker_value(message.automation_marker)
|
|
1229
|
+
if not marker:
|
|
1230
|
+
raise EmailInvalidMessage(
|
|
1231
|
+
"The automation marker must be one line of printable ASCII (at most 200 characters).",
|
|
1232
|
+
"Give an identifier such as occurrence:<run id>.",
|
|
1233
|
+
)
|
|
1234
|
+
mime[AUTOMATION_MARKER_HEADER] = marker
|
|
1197
1235
|
except (ValueError, TypeError) as exc:
|
|
1198
1236
|
raise EmailInvalidMessage(
|
|
1199
1237
|
"A message header is not valid (line breaks are not allowed in headers).",
|
|
@@ -18,13 +18,22 @@ policy and the limits run here, always, for every caller.
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import ssl
|
|
21
|
-
from dataclasses import dataclass, field
|
|
21
|
+
from dataclasses import dataclass, field, replace
|
|
22
22
|
from typing import Any, Callable, Dict, Optional
|
|
23
23
|
|
|
24
24
|
from .client import DEFAULT_MAX_MESSAGE_BYTES, EmailClient
|
|
25
25
|
from .errors import EmailDisabled, EmailInvalidMessage, EmailNotConfigured
|
|
26
26
|
from .limits import SendRateLimiter
|
|
27
|
-
from .models import
|
|
27
|
+
from .models import (
|
|
28
|
+
AUTO_SUBMITTED_GENERATED,
|
|
29
|
+
AUTO_SUBMITTED_REPLIED,
|
|
30
|
+
EmailAccount,
|
|
31
|
+
EmailSecret,
|
|
32
|
+
OutgoingMessage,
|
|
33
|
+
SendLimits,
|
|
34
|
+
SendResult,
|
|
35
|
+
automation_marker_value,
|
|
36
|
+
)
|
|
28
37
|
from .oauth import OAuthTokenProvider
|
|
29
38
|
from .policy import PolicyDecision, RecipientPolicy, evaluate, parse_recipients
|
|
30
39
|
|
|
@@ -46,6 +55,11 @@ class EmailContext:
|
|
|
46
55
|
# are not fetched; the read returns a typed skip record instead (never a cut body).
|
|
47
56
|
max_message_bytes: int = DEFAULT_MAX_MESSAGE_BYTES
|
|
48
57
|
on_sent: Optional[Callable[[Dict[str, Any]], None]] = field(default=None, repr=False)
|
|
58
|
+
# Set by a host for a context used by something that sends automatically (an automation
|
|
59
|
+
# occurrence, a notification): every message sent through it carries RFC 3834
|
|
60
|
+
# `Auto-Submitted` and the framework marker (`X-AbstractFramework-Automation: <value>`),
|
|
61
|
+
# so this account's own mail watcher never admits it (no self-triggering loop).
|
|
62
|
+
automation_marker: str = ""
|
|
49
63
|
|
|
50
64
|
def __repr__(self) -> str:
|
|
51
65
|
return f"EmailContext(address={self.account.address!r}, enabled={self.enabled}, source={self.source!r})"
|
|
@@ -81,8 +95,27 @@ class EmailContext:
|
|
|
81
95
|
return guarded_send(self, message)
|
|
82
96
|
|
|
83
97
|
|
|
98
|
+
def mark_automatic(message: OutgoingMessage, marker: str) -> OutgoingMessage:
|
|
99
|
+
"""`message` with the framework marker and RFC 3834 `Auto-Submitted` (kept when set).
|
|
100
|
+
|
|
101
|
+
`Auto-Submitted` is `auto-replied` for an answer to one message (In-Reply-To set), else
|
|
102
|
+
`auto-generated`.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
marker_value = automation_marker_value(marker)
|
|
106
|
+
if not marker_value:
|
|
107
|
+
raise EmailInvalidMessage(
|
|
108
|
+
"The automation marker must be one line of printable ASCII (at most 200 characters).",
|
|
109
|
+
"Give an identifier such as occurrence:<run id>.",
|
|
110
|
+
)
|
|
111
|
+
auto = message.auto_submitted or (AUTO_SUBMITTED_REPLIED if message.in_reply_to else AUTO_SUBMITTED_GENERATED)
|
|
112
|
+
return replace(message, auto_submitted=auto, automation_marker=message.automation_marker or marker_value)
|
|
113
|
+
|
|
114
|
+
|
|
84
115
|
def guarded_send(ctx: EmailContext, message: OutgoingMessage) -> SendResult:
|
|
85
116
|
ctx.require_enabled()
|
|
117
|
+
if ctx.automation_marker:
|
|
118
|
+
message = mark_automatic(message, ctx.automation_marker)
|
|
86
119
|
if not ctx.account.can_send:
|
|
87
120
|
raise EmailNotConfigured(
|
|
88
121
|
"This account has no SMTP (send) settings.",
|
|
@@ -100,7 +133,15 @@ def guarded_send(ctx: EmailContext, message: OutgoingMessage) -> SendResult:
|
|
|
100
133
|
raise
|
|
101
134
|
if ctx.on_sent is not None:
|
|
102
135
|
try:
|
|
103
|
-
ctx.on_sent(
|
|
136
|
+
ctx.on_sent(
|
|
137
|
+
{
|
|
138
|
+
"message_id": result.message_id,
|
|
139
|
+
"accepted": list(result.accepted),
|
|
140
|
+
"refused": dict(result.refused),
|
|
141
|
+
"auto_submitted": message.auto_submitted,
|
|
142
|
+
"automation_marker": message.automation_marker,
|
|
143
|
+
}
|
|
144
|
+
)
|
|
104
145
|
except Exception:
|
|
105
146
|
pass
|
|
106
147
|
return result
|
|
@@ -486,6 +486,32 @@ class Attachment:
|
|
|
486
486
|
return b""
|
|
487
487
|
|
|
488
488
|
|
|
489
|
+
# RFC 3834 section 5: a message sent automatically carries `Auto-Submitted: auto-generated`
|
|
490
|
+
# (not a response to one message) or `auto-replied` (a response to one message); mail
|
|
491
|
+
# written by a person carries none (or `no`). Responders must not answer such messages.
|
|
492
|
+
AUTO_SUBMITTED_HEADER = "Auto-Submitted"
|
|
493
|
+
AUTO_SUBMITTED_GENERATED = "auto-generated"
|
|
494
|
+
AUTO_SUBMITTED_REPLIED = "auto-replied"
|
|
495
|
+
AUTO_SUBMITTED_VALUES = (AUTO_SUBMITTED_GENERATED, AUTO_SUBMITTED_REPLIED)
|
|
496
|
+
# The framework's own marker on every message it sends automatically (notifications, digests,
|
|
497
|
+
# send-email actions, agent sends from automations): the value names what sent it (e.g.
|
|
498
|
+
# `occurrence:<run id>`, `notification:<key>`). A mail watcher never admits a message that
|
|
499
|
+
# carries it and comes from its own account, so an automation can never trigger itself.
|
|
500
|
+
AUTOMATION_MARKER_HEADER = "X-AbstractFramework-Automation"
|
|
501
|
+
MAX_AUTOMATION_MARKER = 200
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def automation_marker_value(value: Any) -> str:
|
|
505
|
+
"""A marker value as sent: one line of printable ASCII, at most 200 characters, else ""."""
|
|
506
|
+
|
|
507
|
+
text = str(value or "").strip()
|
|
508
|
+
if not text or len(text) > MAX_AUTOMATION_MARKER:
|
|
509
|
+
return ""
|
|
510
|
+
if any(not (32 <= ord(c) < 127) for c in text):
|
|
511
|
+
return ""
|
|
512
|
+
return text
|
|
513
|
+
|
|
514
|
+
|
|
489
515
|
@dataclass(frozen=True)
|
|
490
516
|
class OutgoingMessage:
|
|
491
517
|
to: Tuple[str, ...] = ()
|
|
@@ -497,6 +523,11 @@ class OutgoingMessage:
|
|
|
497
523
|
attachments: Tuple[Attachment, ...] = ()
|
|
498
524
|
in_reply_to: str = ""
|
|
499
525
|
references: Tuple[str, ...] = ()
|
|
526
|
+
# RFC 3834 `Auto-Submitted` ("auto-generated" | "auto-replied"; "" = written by a person)
|
|
527
|
+
# and the framework marker (`X-AbstractFramework-Automation`, "" = none). Set by the
|
|
528
|
+
# sender for automatic mail, or stamped by `guarded_send` from `EmailContext.automation_marker`.
|
|
529
|
+
auto_submitted: str = ""
|
|
530
|
+
automation_marker: str = ""
|
|
500
531
|
|
|
501
532
|
@property
|
|
502
533
|
def recipients(self) -> List[str]:
|
|
@@ -564,6 +595,11 @@ class MessageSummary:
|
|
|
564
595
|
x_priority: Optional[int] = None
|
|
565
596
|
priority: Optional[str] = None
|
|
566
597
|
list_unsubscribe: bool = False
|
|
598
|
+
# RFC 3834 `Auto-Submitted`, lower-cased keyword without parameters ("no", "auto-generated",
|
|
599
|
+
# "auto-replied", or an extension keyword); None when the header is absent or empty.
|
|
600
|
+
auto_submitted: Optional[str] = None
|
|
601
|
+
# The framework marker header's value (`X-AbstractFramework-Automation`); "" when absent.
|
|
602
|
+
framework_marker: str = ""
|
|
567
603
|
|
|
568
604
|
@property
|
|
569
605
|
def seen(self) -> bool:
|
|
@@ -592,6 +628,8 @@ class MessageSummary:
|
|
|
592
628
|
"x_priority": self.x_priority,
|
|
593
629
|
"priority": self.priority,
|
|
594
630
|
"list_unsubscribe": self.list_unsubscribe,
|
|
631
|
+
"auto_submitted": self.auto_submitted,
|
|
632
|
+
"framework_marker": self.framework_marker,
|
|
595
633
|
}
|
|
596
634
|
|
|
597
635
|
|
|
@@ -625,6 +625,32 @@ def _host_platform(host: Mapping[str, Any]) -> Tuple[str, str, Optional[str]]:
|
|
|
625
625
|
return os_id or "unknown", arch or "unknown", accelerator if isinstance(accelerator, str) else None
|
|
626
626
|
|
|
627
627
|
|
|
628
|
+
def recommended_engine_missing_reason(provider: Any, host: Mapping[str, Any]) -> Optional[str]:
|
|
629
|
+
"""Why a recommended provider's IN-PROCESS engine is not installed in this install, or None.
|
|
630
|
+
|
|
631
|
+
Read from the host profile's `engines_installed` (utils/host_profile.py, from
|
|
632
|
+
`route_engines.provider_engine_installed`): the light install profile has no MLX, MLX-Gen
|
|
633
|
+
or PyTorch, so a recommendation naming one would fail at first use (0.7.0 end-to-end: a
|
|
634
|
+
128 GB Mac on the light profile got mlx/Qwen3.8-Flash-Next-4bit as its text default and
|
|
635
|
+
every call, voice included, failed with "MLX dependencies not installed"). A host mapping
|
|
636
|
+
without that key (a synthetic host, an older profile) is not judged. The sentence is
|
|
637
|
+
`route_engines.route_engine_missing`'s own (it names the install setting to add).
|
|
638
|
+
Configured routes are judged by `route_engines.routes_engine_missing` (`engine_missing`),
|
|
639
|
+
never folded into `route_unavailable`.
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
installed = host.get("engines_installed")
|
|
643
|
+
pid = str(provider or "").strip().lower()
|
|
644
|
+
if not isinstance(installed, Mapping) or installed.get(pid) is not False:
|
|
645
|
+
return None
|
|
646
|
+
from .route_engines import route_engine_missing
|
|
647
|
+
|
|
648
|
+
flag = route_engine_missing(pid)
|
|
649
|
+
if flag is not None and flag.get("reason"):
|
|
650
|
+
return str(flag["reason"])
|
|
651
|
+
return f"the {pid} engine is not installed in this Python environment (the light install profile has no local engines)"
|
|
652
|
+
|
|
653
|
+
|
|
628
654
|
def recommended_route_unavailable_reason(
|
|
629
655
|
provider: Any, host: Mapping[str, Any], key: Optional[str] = None
|
|
630
656
|
) -> Optional[str]:
|
|
@@ -632,7 +658,8 @@ def recommended_route_unavailable_reason(
|
|
|
632
658
|
|
|
633
659
|
`key` (the route) only words the sentence ("MLX-Gen video generation").
|
|
634
660
|
Engine support only: the memory gate of fit-gated routes is
|
|
635
|
-
`recommended_unavailable_routes
|
|
661
|
+
`recommended_unavailable_routes`; whether THIS install has the engine is
|
|
662
|
+
`recommended_engine_missing_reason`.
|
|
636
663
|
"""
|
|
637
664
|
|
|
638
665
|
from .engines import _support
|
|
@@ -746,7 +773,8 @@ def _unavailable_reasons(
|
|
|
746
773
|
routes: Mapping[str, CapabilityRouteDefault], downloads: Mapping[str, Mapping[str, str]], host: Mapping[str, Any]
|
|
747
774
|
) -> Dict[str, str]:
|
|
748
775
|
"""`{key: reason}` for every recommended row this host cannot run: its
|
|
749
|
-
engine has no build here,
|
|
776
|
+
engine has no build here, this install does not have its in-process engine
|
|
777
|
+
(`recommended_engine_missing_reason`), or (fit-gated rows) its model does not fit."""
|
|
750
778
|
|
|
751
779
|
out: Dict[str, str] = {}
|
|
752
780
|
for key, route in routes.items():
|
|
@@ -757,6 +785,12 @@ def _unavailable_reasons(
|
|
|
757
785
|
reason = f"{reason}; {next_step}"
|
|
758
786
|
out[key] = reason
|
|
759
787
|
continue
|
|
788
|
+
# The host runs the engine, but this install does not have it (the light profile):
|
|
789
|
+
# never recommend (seed, apply-recommended) a route that fails at first use.
|
|
790
|
+
reason = recommended_engine_missing_reason(route.provider, host)
|
|
791
|
+
if reason:
|
|
792
|
+
out[key] = reason
|
|
793
|
+
continue
|
|
760
794
|
if key in _FIT_GATED_ROUTES:
|
|
761
795
|
reason = _fit_gate_reason(key, downloads[key], host)
|
|
762
796
|
if reason:
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
abstractcore email registered-address me@example.com
|
|
14
14
|
|
|
15
15
|
Credentials are direct parameters (`--password <value>`, `--client-secret <value>`) and are
|
|
16
|
-
stored encrypted; they are never printed.
|
|
16
|
+
stored encrypted; they are never printed. A script or program passes them on stdin instead, so
|
|
17
|
+
the secret never appears on a command line (where other local users can read it while the
|
|
18
|
+
command runs): `--password-stdin` / `--client-secret-stdin` read exactly one line from stdin
|
|
19
|
+
(the trailing newline is removed, nothing else), e.g.
|
|
20
|
+
`printf '%s\n' "$PW" | abstractcore email connect --address me@example.com ... --password-stdin`. Every verb takes `--json` (the `email_settings_v1`
|
|
17
21
|
document of `EmailAccountStore.public()`, or `{ok: false, error: {code, cause, fix}}`).
|
|
18
22
|
Exit codes: 0 ok, 1 error, 2 refused (a failed connection test, or `disconnect` without
|
|
19
23
|
`--yes`).
|
|
@@ -187,6 +191,61 @@ def _obtain_oauth_tokens(args: argparse.Namespace, oauth: Any, client_secret: st
|
|
|
187
191
|
return loop.finish(timeout_s=float(args.oauth_timeout))
|
|
188
192
|
|
|
189
193
|
|
|
194
|
+
def _read_stdin_secret(flag: str, what: str) -> str:
|
|
195
|
+
"""One secret from stdin: exactly one line, the trailing newline removed (nothing else)."""
|
|
196
|
+
from abstractcore.comms.email import EmailInvalidSettings
|
|
197
|
+
|
|
198
|
+
stream = sys.stdin
|
|
199
|
+
if stream is None:
|
|
200
|
+
raise EmailInvalidSettings(
|
|
201
|
+
f"{flag} was given but this process has no stdin.",
|
|
202
|
+
f"Pipe the {what} into the command (printf '%s\\n' \"$SECRET\" | abstractcore email connect ... {flag}).",
|
|
203
|
+
)
|
|
204
|
+
line = stream.readline()
|
|
205
|
+
if line.endswith("\n"):
|
|
206
|
+
line = line[:-1]
|
|
207
|
+
if line.endswith("\r"):
|
|
208
|
+
line = line[:-1]
|
|
209
|
+
if not line:
|
|
210
|
+
raise EmailInvalidSettings(
|
|
211
|
+
f"{flag} read an empty {what} from stdin.",
|
|
212
|
+
f"Write the {what} as the first line of stdin (printf '%s\\n' \"$SECRET\" | abstractcore email connect ... {flag}).",
|
|
213
|
+
)
|
|
214
|
+
return line
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _stdin_secrets(args: argparse.Namespace) -> None:
|
|
218
|
+
"""Resolve `--password-stdin` / `--client-secret-stdin` into args.password / args.client_secret."""
|
|
219
|
+
from abstractcore.comms.email import EmailInvalidSettings
|
|
220
|
+
|
|
221
|
+
if args.password_stdin and args.client_secret_stdin:
|
|
222
|
+
raise EmailInvalidSettings(
|
|
223
|
+
"--password-stdin and --client-secret-stdin cannot be combined (stdin carries one secret).",
|
|
224
|
+
"Use --password-stdin for a password account, or --client-secret-stdin with --oauth.",
|
|
225
|
+
)
|
|
226
|
+
if args.password_stdin:
|
|
227
|
+
if args.password:
|
|
228
|
+
raise EmailInvalidSettings(
|
|
229
|
+
"--password and --password-stdin were both given.",
|
|
230
|
+
"Give the password once: --password <value>, or --password-stdin with the password on stdin.",
|
|
231
|
+
)
|
|
232
|
+
if args.oauth:
|
|
233
|
+
raise EmailInvalidSettings("--password-stdin is not used with --oauth.", "Remove --password-stdin; OAuth2 signs in through the browser.")
|
|
234
|
+
args.password = _read_stdin_secret("--password-stdin", "password")
|
|
235
|
+
if args.client_secret_stdin:
|
|
236
|
+
if args.client_secret:
|
|
237
|
+
raise EmailInvalidSettings(
|
|
238
|
+
"--client-secret and --client-secret-stdin were both given.",
|
|
239
|
+
"Give the client secret once: --client-secret <value>, or --client-secret-stdin with the secret on stdin.",
|
|
240
|
+
)
|
|
241
|
+
if not args.oauth:
|
|
242
|
+
raise EmailInvalidSettings(
|
|
243
|
+
"--client-secret-stdin is only used with --oauth.",
|
|
244
|
+
"Add --oauth google|microsoft|custom, or remove --client-secret-stdin.",
|
|
245
|
+
)
|
|
246
|
+
args.client_secret = _read_stdin_secret("--client-secret-stdin", "client secret")
|
|
247
|
+
|
|
248
|
+
|
|
190
249
|
def cmd_connect(args: argparse.Namespace) -> int:
|
|
191
250
|
from abstractcore.comms.email import (
|
|
192
251
|
EmailAccount,
|
|
@@ -203,6 +262,7 @@ def cmd_connect(args: argparse.Namespace) -> int:
|
|
|
203
262
|
|
|
204
263
|
as_json = bool(args.json)
|
|
205
264
|
try:
|
|
265
|
+
_stdin_secrets(args)
|
|
206
266
|
preset = provider_preset(args.oauth, tenant=args.tenant or "") if args.oauth and args.oauth != "custom" else {}
|
|
207
267
|
address = (args.address or "").strip() or (args.username or "").strip()
|
|
208
268
|
if not address:
|
|
@@ -254,7 +314,8 @@ def cmd_connect(args: argparse.Namespace) -> int:
|
|
|
254
314
|
if not args.password:
|
|
255
315
|
raise EmailInvalidSettings(
|
|
256
316
|
"The password is missing.",
|
|
257
|
-
"Give --password <value> (an app password for providers with two-step verification
|
|
317
|
+
"Give --password <value> (an app password for providers with two-step verification; "
|
|
318
|
+
"--password-stdin reads it from stdin), or --oauth <provider>.",
|
|
258
319
|
)
|
|
259
320
|
account = EmailAccount.build(
|
|
260
321
|
address=address, username=args.username or address, imap=imap, smtp=smtp,
|
|
@@ -475,6 +536,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
475
536
|
c.add_argument("--display-name", help="Name shown to recipients")
|
|
476
537
|
c.add_argument("--username", help="Sign-in user name (default: the address)")
|
|
477
538
|
c.add_argument("--password", help="Password or app password (stored encrypted)")
|
|
539
|
+
c.add_argument(
|
|
540
|
+
"--password-stdin", action="store_true",
|
|
541
|
+
help="Read the password from stdin (one line; for scripts: the password never appears on a command line)",
|
|
542
|
+
)
|
|
478
543
|
c.add_argument("--imap-host")
|
|
479
544
|
c.add_argument("--imap-port", type=int)
|
|
480
545
|
c.add_argument("--imap-security", choices=("ssl", "starttls"))
|
|
@@ -490,6 +555,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
490
555
|
c.add_argument("--oauth", choices=("google", "microsoft", "custom"), help="Sign in with OAuth2 instead of a password")
|
|
491
556
|
c.add_argument("--client-id", help="Your own OAuth client id (default: the built-in AbstractFramework client, when one is registered for the provider)")
|
|
492
557
|
c.add_argument("--client-secret", help="Your own OAuth client secret (stored encrypted)")
|
|
558
|
+
c.add_argument(
|
|
559
|
+
"--client-secret-stdin", action="store_true",
|
|
560
|
+
help="Read the OAuth client secret from stdin (one line; the secret never appears on a command line)",
|
|
561
|
+
)
|
|
493
562
|
c.add_argument("--tenant", help="Microsoft tenant (default common)")
|
|
494
563
|
c.add_argument("--oauth-flow", choices=("device", "loopback"), help="device code (Microsoft default) or browser on this machine (Google default)")
|
|
495
564
|
c.add_argument("--token-endpoint")
|
|
@@ -117,6 +117,34 @@ _PROVIDER_MODEL_PREFIXES = {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def route_provider_changed(stored: Any, requested: Any) -> bool:
|
|
121
|
+
"""True when a route write names a provider other than the stored one (case-insensitive).
|
|
122
|
+
|
|
123
|
+
Such a write that names no options drops the old engine's speculation request
|
|
124
|
+
(`options_after_provider_change`; AbstractGateway's route save applies the same rule).
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if requested is None:
|
|
128
|
+
return False
|
|
129
|
+
old = str(stored or "").strip().lower()
|
|
130
|
+
new = str(requested or "").strip().lower()
|
|
131
|
+
return bool(old) and old != new
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def options_after_provider_change(options: Any) -> Dict[str, Any]:
|
|
135
|
+
"""The stored route options a write that moves the route to ANOTHER provider keeps.
|
|
136
|
+
|
|
137
|
+
A speculation REQUEST (`speculation: {mode: native_mtp, ...}` or `true`) names how the old
|
|
138
|
+
engine loads the model (an MLX MTP head, a drafter), so it is dropped; `speculation: false`
|
|
139
|
+
(off) is valid for every engine and is kept, like every other option.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
out = dict(options) if isinstance(options, dict) else {}
|
|
143
|
+
if "speculation" in out and out["speculation"] is not False:
|
|
144
|
+
out.pop("speculation")
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
120
148
|
def _split_provider_model(value: str, *, default_provider: str) -> Tuple[str, str]:
|
|
121
149
|
raw = str(value or "").strip()
|
|
122
150
|
if not raw:
|
|
@@ -2032,6 +2060,11 @@ class ConfigurationManager:
|
|
|
2032
2060
|
task=task,
|
|
2033
2061
|
)
|
|
2034
2062
|
merged_options = options if isinstance(options, dict) else stored.get("options")
|
|
2063
|
+
if not isinstance(options, dict) and route_provider_changed(stored.get("provider"), provider):
|
|
2064
|
+
# A write that moves the route to another provider and names no options drops the
|
|
2065
|
+
# old engine's speculation request (0.7.0 end-to-end: an LM Studio text route kept
|
|
2066
|
+
# the MLX tier's `speculation: native_mtp`); every other option is kept.
|
|
2067
|
+
merged_options = options_after_provider_change(merged_options)
|
|
2035
2068
|
return self.set_capability_default(
|
|
2036
2069
|
kind,
|
|
2037
2070
|
modality,
|
|
@@ -141,6 +141,10 @@ APPLE_TEXT_TIERS: Tuple[Dict[str, Any], ...] = (
|
|
|
141
141
|
"plain": "mlx-community/Qwen3.8-Flash-Next-4bit", "mtp": "Jundot/Qwen3.8-Flash-Next-oQ4e-mtp"},
|
|
142
142
|
)
|
|
143
143
|
_TIER_PROVIDER = "mlx"
|
|
144
|
+
# The tier's engine when this install has no MLX (the light profile): LM Studio serves the same
|
|
145
|
+
# rows (`qwen/qwen3.5-9b@q4_k_m`, `qwen/qwen3.8-27b@q4_k_m`) from its own app, nothing to
|
|
146
|
+
# install in this Python (`recommended_text_model`, basis `apple_silicon_engine_fallback`).
|
|
147
|
+
_SERVER_TIER_PROVIDER = "lmstudio"
|
|
144
148
|
|
|
145
149
|
# The portable text model on an engine that runs where LM Studio does not
|
|
146
150
|
# (Intel Macs). Same catalog row as the portable default; Ollama's bare tag
|
|
@@ -292,7 +296,9 @@ def recommended_text_model(
|
|
|
292
296
|
model the id the route stores (the served id; for MLX the repo id)
|
|
293
297
|
options route options: the portable route's MTP policy
|
|
294
298
|
(`speculation`), overlaid with the artifact's own options
|
|
295
|
-
basis `apple_silicon_tiers`, `
|
|
299
|
+
basis `apple_silicon_tiers`, `apple_silicon_engine_fallback` (Apple
|
|
300
|
+
silicon without MLX in this install: the tier's LM Studio build,
|
|
301
|
+
else the nearest smaller tier's), `portable_default`,
|
|
296
302
|
`portable_engine_fallback` (the portable engine has no build
|
|
297
303
|
on this host, e.g. LM Studio on an Intel Mac), or
|
|
298
304
|
`no_supported_engine` (neither has one, e.g. FreeBSD: the
|
|
@@ -319,7 +325,51 @@ def recommended_text_model(
|
|
|
319
325
|
accelerator = str(profile.get("accelerator") or "none")
|
|
320
326
|
memory = _memory_gib(profile)
|
|
321
327
|
use_mtp = MTP_RECOMMENDED if mtp is None else bool(mtp)
|
|
328
|
+
from .model_materializer import split_artifact as _split_artifact
|
|
329
|
+
|
|
330
|
+
mlx_missing = None
|
|
322
331
|
if accelerator == "metal":
|
|
332
|
+
from .capability_defaults import recommended_engine_missing_reason
|
|
333
|
+
|
|
334
|
+
mlx_missing = recommended_engine_missing_reason(_TIER_PROVIDER, profile)
|
|
335
|
+
if accelerator == "metal" and mlx_missing:
|
|
336
|
+
# The tiers are MLX builds, and this install has no MLX (the light profile): the
|
|
337
|
+
# tier's model on LM Studio (a server route, nothing to install in this Python), else
|
|
338
|
+
# the nearest SMALLER tier that has an LM Studio build (Flash-Next has none) -- never
|
|
339
|
+
# an engine that fails at first use (0.7.0 end-to-end, light install on a 128 GB Mac).
|
|
340
|
+
tier, rule = _apple_tier(memory)
|
|
341
|
+
order = list(APPLE_TEXT_TIERS)
|
|
342
|
+
start = order.index(tier)
|
|
343
|
+
chosen = None
|
|
344
|
+
for candidate in reversed(order[: start + 1]):
|
|
345
|
+
for seed_row in _load_seed_cached().get("rows") or []:
|
|
346
|
+
if seed_row.get("id") != candidate["row"]:
|
|
347
|
+
continue
|
|
348
|
+
art0 = next((a for a in seed_row.get("artifacts") or [] if a.get("provider") == _SERVER_TIER_PROVIDER), None)
|
|
349
|
+
if art0 is not None:
|
|
350
|
+
chosen = (candidate, seed_row, art0)
|
|
351
|
+
break
|
|
352
|
+
if chosen:
|
|
353
|
+
break
|
|
354
|
+
if chosen is None:
|
|
355
|
+
raise LookupError("no Apple silicon tier row has an LM Studio build")
|
|
356
|
+
cand, row, art = chosen
|
|
357
|
+
which = (
|
|
358
|
+
"the tier's LM Studio build" if cand is tier
|
|
359
|
+
else f"the LM Studio build of the nearest smaller tier ({cand['row']}; {tier['row']} has no LM Studio build)"
|
|
360
|
+
)
|
|
361
|
+
out = {
|
|
362
|
+
"provider": _SERVER_TIER_PROVIDER,
|
|
363
|
+
"artifact": str(art["artifact"]),
|
|
364
|
+
"model": _split_artifact(str(art["artifact"]))[0], # LM Studio serves the id without `@quant`
|
|
365
|
+
# The host-wide MTP policy, as on the portable LM Studio route of every other host.
|
|
366
|
+
"options": dict(_portable_text_default()["options"], **json.loads(json.dumps(art.get("options") or {}))),
|
|
367
|
+
"catalog_id": row["id"],
|
|
368
|
+
"basis": "apple_silicon_engine_fallback",
|
|
369
|
+
"tier": f"{rule}: {which}, because the tier's MLX build cannot run in this install ({mlx_missing.rstrip('.')})",
|
|
370
|
+
"mtp": False,
|
|
371
|
+
}
|
|
372
|
+
elif accelerator == "metal":
|
|
323
373
|
tier, rule = _apple_tier(memory)
|
|
324
374
|
provider, artifact = _TIER_PROVIDER, _tier_artifact(tier, use_mtp)
|
|
325
375
|
row, art = _seed_row_and_artifact(str(tier["row"]), provider, artifact)
|
|
@@ -1664,7 +1714,7 @@ def catalog(
|
|
|
1664
1714
|
if arts:
|
|
1665
1715
|
tier = tier_by_row.get(seed_row["id"]) if accelerator == "metal" else None
|
|
1666
1716
|
forced = (_TIER_PROVIDER, _tier_artifact(tier)) if tier is not None else None
|
|
1667
|
-
if text_pick["basis"]
|
|
1717
|
+
if text_pick["basis"] in ("portable_engine_fallback", "apple_silicon_engine_fallback") and seed_row["id"] == text_pick["catalog_id"]:
|
|
1668
1718
|
forced = (text_pick["provider"], text_pick["artifact"])
|
|
1669
1719
|
if forced is not None and not any((a["provider"], a["artifact"]) == forced for a in arts):
|
|
1670
1720
|
forced = None # an engine filter (`engine=ollama`) removed the tier artifact
|
|
@@ -49,7 +49,13 @@ import shlex
|
|
|
49
49
|
from importlib import metadata
|
|
50
50
|
from typing import Any, Dict, Mapping, Optional, Tuple
|
|
51
51
|
|
|
52
|
-
__all__ = [
|
|
52
|
+
__all__ = [
|
|
53
|
+
"ABSTRACTVOICE_ENGINE_RUNTIME_FLOOR",
|
|
54
|
+
"provider_engine_installed",
|
|
55
|
+
"provider_engines_installed",
|
|
56
|
+
"route_engine_missing",
|
|
57
|
+
"routes_engine_missing",
|
|
58
|
+
]
|
|
53
59
|
|
|
54
60
|
# The first AbstractVoice with `abstractvoice.engine_runtime` (the public
|
|
55
61
|
# runtime probe). Mirrors the floor of the `voice` extra in pyproject.toml;
|
|
@@ -219,6 +225,36 @@ def _dist_version(dist: str) -> str:
|
|
|
219
225
|
return "(unknown version)"
|
|
220
226
|
|
|
221
227
|
|
|
228
|
+
# In-process providers a RECOMMENDATION may name, and what makes each usable in this Python
|
|
229
|
+
# environment (the host profile's `engines_installed`, read by the recommendation so the light
|
|
230
|
+
# install profile is never handed an engine it does not have). Voice engines are AbstractVoice's
|
|
231
|
+
# own answer (`route_engine_missing(..., key=)`), not judged here: they ship with the light
|
|
232
|
+
# profile and asking needs AbstractVoice imported.
|
|
233
|
+
def provider_engine_installed(provider: Any) -> Optional[bool]:
|
|
234
|
+
"""True / False for an in-process provider a recommendation can name (mlx, mlx-gen,
|
|
235
|
+
diffusers, acestep), None for any other provider (not judged). Lookups only."""
|
|
236
|
+
|
|
237
|
+
pid = str(provider or "").strip().lower()
|
|
238
|
+
if pid == "mlx":
|
|
239
|
+
return _importable("mlx_lm")
|
|
240
|
+
if pid == "mlx-gen":
|
|
241
|
+
return _distributed("abstractvision") and _distributed("mlx-gen")
|
|
242
|
+
if pid in ("diffusers", "acestep"):
|
|
243
|
+
return _importable("torch")
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def provider_engines_installed() -> Dict[str, bool]:
|
|
248
|
+
"""`{provider: installed}` for every provider `provider_engine_installed` judges."""
|
|
249
|
+
|
|
250
|
+
out: Dict[str, bool] = {}
|
|
251
|
+
for pid in ("mlx", "mlx-gen", "diffusers", "acestep"):
|
|
252
|
+
value = provider_engine_installed(pid)
|
|
253
|
+
if value is not None:
|
|
254
|
+
out[pid] = bool(value)
|
|
255
|
+
return out
|
|
256
|
+
|
|
257
|
+
|
|
222
258
|
def route_engine_missing(provider: Any, model: Any = None, key: Any = None) -> Optional[Dict[str, Any]]:
|
|
223
259
|
"""`{engine, name, reason, install[, engine_row]}` when the in-process
|
|
224
260
|
engine behind this route is not installed here, else None.
|