databricks-tellr-app 0.4.2.dev13__tar.gz → 0.4.2.dev15__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.
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/PKG-INFO +2 -1
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/databricks_tellr_app.egg-info/PKG-INFO +2 -1
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/databricks_tellr_app.egg-info/requires.txt +1 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/pyproject.toml +14 -1
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/mcp_server.py +39 -2
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/export.py +71 -4
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/settings/design_systems.py +108 -14
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/agent_factory.py +53 -6
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/converter_jail/jail.py +14 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/converter_jail/pptx_runner.py +8 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/converter_jail/protocol.py +33 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/design_system_compiler.py +219 -15
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/design_system_service.py +73 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/html_to_pptx.py +182 -2
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/README.md +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/databricks_tellr_app/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/databricks_tellr_app/run.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/databricks_tellr_app.egg-info/top_level.txt +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/setup.cfg +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/setup.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/_otel_bootstrap.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/fixtures/tour_demo_deck.json +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/main.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/mcp_auth.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/middleware/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/middleware/csrf.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/middleware/request_logging.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/middleware/security_headers.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/_authz.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/admin.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/admin_usage.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/agent_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/chat.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/deck_contributors.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/feedback.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/google_slides.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/images.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/local_version.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/profiles.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/sessions.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/settings/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/settings/contributors.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/settings/deck_prompts.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/settings/identities.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/settings/slide_styles.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/setup.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/slides.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/tools.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/tour.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/verification.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/version.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/agent_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/feedback.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/requests.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/responses.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/settings/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/settings/requests.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/settings/responses.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/schemas/streaming.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/chat_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/export_job_queue.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/feedback_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/job_queue.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/session_manager.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/session_naming.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/usage_events.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/services/usage_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/utils/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/utils/validation.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/config_loader.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/context_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/database.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/databricks_client.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/defaults.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/encryption.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/init_default_profile.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/lakebase.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/migrate_profiles_to_agent_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/mlflow_agent_spans.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/mlflow_tracing.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/otel_logging.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/permission_context.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/prompt_modules.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/settings_db.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/core/user_context.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/deck_contributor.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/design_system.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/encryption_key.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/feedback.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/genie_space.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/google_global_credentials.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/google_oauth_token.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/identity.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/image.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/profile.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/profile_contributor.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/prompts.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/request_log.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/session.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/slide_deck_prompt.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/slide_style_library.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/usage_event.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/models/user_preference.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/database/types.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/domain/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/domain/slide.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/domain/slide_deck.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/agent.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/config_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/config_validator.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/converter_jail/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/converter_jail/ast_guard.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/converter_jail/codeprep.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/converter_jail/gslides_runner.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/design_system_templates.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/drive_uploader.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/evaluation/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/evaluation/llm_judge.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/genie_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/google_slides_auth.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/google_slides_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/html_to_google_slides.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/identity_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/identity_providers/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/identity_providers/account_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/identity_providers/local_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/identity_providers/workspace_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/image_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/image_tools.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/permission_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/pptx_from_html_huashu.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/pptx_from_records.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/pptx_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/profile_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/streaming_callback.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/tools/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/tools/agent_bricks_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/tools/ds_asset_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/tools/genie_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/tools/mcp_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/tools/model_endpoint_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/tools/vector_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/validator.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/css_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/ds_asset_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/error_handling.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/html_safety.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/html_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/image_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/js_validator.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/logging_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/pi_filter.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/slide_hash.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/spotlight.py +0 -0
- {databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/utils/text_caps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: databricks-tellr-app
|
|
3
|
-
Version: 0.4.2.
|
|
3
|
+
Version: 0.4.2.dev15
|
|
4
4
|
Summary: Tellr application package for Databricks Apps
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -33,6 +33,7 @@ Requires-Dist: pandas==2.3.3
|
|
|
33
33
|
Requires-Dist: python-pptx==0.6.23
|
|
34
34
|
Requires-Dist: playwright==1.48.0
|
|
35
35
|
Requires-Dist: Pillow==11.3.0
|
|
36
|
+
Requires-Dist: svgpathtools>=1.6.0
|
|
36
37
|
Requires-Dist: google-api-python-client==2.190.0
|
|
37
38
|
Requires-Dist: google-auth-oauthlib==1.2.4
|
|
38
39
|
Requires-Dist: google-auth-httplib2==0.3.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: databricks-tellr-app
|
|
3
|
-
Version: 0.4.2.
|
|
3
|
+
Version: 0.4.2.dev15
|
|
4
4
|
Summary: Tellr application package for Databricks Apps
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -33,6 +33,7 @@ Requires-Dist: pandas==2.3.3
|
|
|
33
33
|
Requires-Dist: python-pptx==0.6.23
|
|
34
34
|
Requires-Dist: playwright==1.48.0
|
|
35
35
|
Requires-Dist: Pillow==11.3.0
|
|
36
|
+
Requires-Dist: svgpathtools>=1.6.0
|
|
36
37
|
Requires-Dist: google-api-python-client==2.190.0
|
|
37
38
|
Requires-Dist: google-auth-oauthlib==1.2.4
|
|
38
39
|
Requires-Dist: google-auth-httplib2==0.3.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "databricks-tellr-app"
|
|
7
|
-
version = "0.4.2.
|
|
7
|
+
version = "0.4.2.dev15"
|
|
8
8
|
description = "Tellr application package for Databricks Apps"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -38,6 +38,19 @@ dependencies = [
|
|
|
38
38
|
"python-pptx==0.6.23",
|
|
39
39
|
"playwright==1.48.0",
|
|
40
40
|
"Pillow==11.3.0",
|
|
41
|
+
# WF-02: imported by BOTH Python emitters (html_to_pptx, html_to_google_slides)
|
|
42
|
+
# and declared in the root manifest, but never here — so on every deployment the
|
|
43
|
+
# lazy `from svgpathtools import parse_path` failed and image extraction became a
|
|
44
|
+
# silent no-op, letting base64 survive into the prompt.
|
|
45
|
+
#
|
|
46
|
+
# Ranged, matching the root declaration, rather than exact-pinned like the
|
|
47
|
+
# resolver-speedup block below. MEASURED before shipping, because a new
|
|
48
|
+
# dependency here is what has previously blown the Apps build budget: adding it
|
|
49
|
+
# takes the resolution from 196 to 198 packages in 15.7s vs 18.5s baseline — no
|
|
50
|
+
# backtracking, and FASTER, not slower. Its numpy and scipy requirements are
|
|
51
|
+
# already in the closure (via mlflow / scikit-learn / skops / pandas), so the only
|
|
52
|
+
# new distributions are svgpathtools itself and the tiny pure-Python svgwrite.
|
|
53
|
+
"svgpathtools>=1.6.0",
|
|
41
54
|
"google-api-python-client==2.190.0",
|
|
42
55
|
"google-auth-oauthlib==1.2.4",
|
|
43
56
|
"google-auth-httplib2==0.3.0",
|
|
@@ -649,7 +649,10 @@ def _render_deck_response(
|
|
|
649
649
|
"Poll the status of a deck generation or edit job. Returns "
|
|
650
650
|
"lightweight status while pending/running; when ready, returns the "
|
|
651
651
|
"complete deck as structured slide data, a standalone HTML "
|
|
652
|
-
"document, and URLs into tellr's full editor and view-only surfaces."
|
|
652
|
+
"document, and URLs into tellr's full editor and view-only surfaces. "
|
|
653
|
+
"A ready turn is not always an applied change: when "
|
|
654
|
+
"metadata.clarification_needed is true the deck was left UNCHANGED and "
|
|
655
|
+
"metadata.clarification carries the question to answer before retrying."
|
|
653
656
|
),
|
|
654
657
|
)
|
|
655
658
|
async def get_deck_status(
|
|
@@ -795,6 +798,34 @@ async def _get_deck_status_impl(
|
|
|
795
798
|
for m in db_messages
|
|
796
799
|
]
|
|
797
800
|
|
|
801
|
+
# WG2-01. An edit instruction that names no slide is DELIBERATELY not
|
|
802
|
+
# applied: chat_service asks which slide instead (its RC10 twins, plus the
|
|
803
|
+
# ambiguous add-vs-replace pair) and attaches ``clarification_needed`` to
|
|
804
|
+
# the COMPLETE event, which ``job_queue.process_chat_request`` copies
|
|
805
|
+
# verbatim into ``result["metadata"]``.
|
|
806
|
+
#
|
|
807
|
+
# Such a turn completes normally, so "ready" is the honest status and
|
|
808
|
+
# ``replacement_info`` is None — which is indistinguishable from an edit
|
|
809
|
+
# that applied but replaced nothing. Rebuilding ``metadata`` from
|
|
810
|
+
# hand-picked keys DROPPED the flag, so an automated caller reading
|
|
811
|
+
# ``status`` concluded the edit had landed when the deck was untouched; the
|
|
812
|
+
# only remaining trace was a ``message_type == "clarification"`` row in the
|
|
813
|
+
# transcript. Surfaced ADDITIVELY, and ``status`` keeps its existing meaning
|
|
814
|
+
# for clients already reading it.
|
|
815
|
+
clarification_needed = bool(result_metadata.get("clarification_needed"))
|
|
816
|
+
clarification = None
|
|
817
|
+
if clarification_needed:
|
|
818
|
+
# The question itself, so a caller can act on it without re-filtering
|
|
819
|
+
# the transcript. Last one wins: it is the turn's most recent ask.
|
|
820
|
+
clarification = next(
|
|
821
|
+
(
|
|
822
|
+
message.get("content")
|
|
823
|
+
for message in reversed(messages)
|
|
824
|
+
if message.get("message_type") == "clarification"
|
|
825
|
+
),
|
|
826
|
+
None,
|
|
827
|
+
)
|
|
828
|
+
|
|
798
829
|
return {
|
|
799
830
|
"session_id": session_id,
|
|
800
831
|
"request_id": request_id,
|
|
@@ -808,6 +839,8 @@ async def _get_deck_status_impl(
|
|
|
808
839
|
or result_metadata.get("latency_seconds"),
|
|
809
840
|
"experiment_url": result.get("experiment_url"),
|
|
810
841
|
"session_title": result.get("session_title"),
|
|
842
|
+
"clarification_needed": clarification_needed,
|
|
843
|
+
"clarification": clarification,
|
|
811
844
|
},
|
|
812
845
|
}
|
|
813
846
|
except MCPToolError:
|
|
@@ -851,7 +884,11 @@ def _check_contiguous(indices: list[int]) -> None:
|
|
|
851
884
|
"The edit is applied in-place; the session_id and deck_url stay "
|
|
852
885
|
"stable across edits. Returns a request_id; the caller polls "
|
|
853
886
|
"get_deck_status for completion and receives the updated deck "
|
|
854
|
-
"plus replacement_info summarizing what changed."
|
|
887
|
+
"plus replacement_info summarizing what changed. An instruction that "
|
|
888
|
+
"identifies no slide — neither via slide_indices nor by naming one in "
|
|
889
|
+
"the text — is answered with a clarifying question INSTEAD of an edit: "
|
|
890
|
+
"get_deck_status then reports metadata.clarification_needed with the "
|
|
891
|
+
"deck unchanged."
|
|
855
892
|
),
|
|
856
893
|
)
|
|
857
894
|
async def edit_deck(
|
{databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/api/routes/export.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Export endpoints for PDF and PPTX generation."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import re
|
|
4
5
|
import tempfile
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
@@ -41,6 +42,33 @@ class ExportPPTXRequest(BaseModel):
|
|
|
41
42
|
chart_images: Optional[list[list[ChartImage]]] = None # Chart images per slide (client-side captured)
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
# WF-03. `@import` is only valid before every other rule of ITS OWN STYLESHEET, and this
|
|
46
|
+
# document used to inject `*{}` and `html,body{}` ahead of the deck's CSS in a SINGLE
|
|
47
|
+
# `<style>` — so a deck whose CSS OPENS with a webfont `@import` had that rule silently
|
|
48
|
+
# discarded. Measured: 0 font faces in the export against 35 in the UI, which was the whole
|
|
49
|
+
# of the long-unexplained 38.25 px per-component residual on the DS-OFF deck.
|
|
50
|
+
#
|
|
51
|
+
# The fix is STRUCTURAL, and it is the reason there is no CSS parsing here. Two `<style>`
|
|
52
|
+
# elements are two stylesheets, so the deck gets its OWN `<style>`: its leading `@import` is
|
|
53
|
+
# then already first in its own sheet and valid with NO rewriting at all. Cascade order
|
|
54
|
+
# between separate `<style>` elements is document order, so resets-then-deck-then-resets
|
|
55
|
+
# keeps exactly the precedence the single combined sheet had.
|
|
56
|
+
#
|
|
57
|
+
# MEASURED IN CHROMIUM under the real SLIDE_CSP: combined -> 1 sheet, 0 font faces, import
|
|
58
|
+
# dead; split -> 2 sheets, font faces registered, import applied; and with a reset and the
|
|
59
|
+
# deck setting the same property at equal specificity, the DECK still wins on order.
|
|
60
|
+
#
|
|
61
|
+
# This REPLACES a hand-written leading-`@charset`/`@import` scanner that lived here. Three
|
|
62
|
+
# rounds of review found four tokenizer defects in it (`str.isspace()` accepting NBSP, an
|
|
63
|
+
# unescaped top-level `\)`, a lone `@charset` rewriting the document, an ASCII-only ident
|
|
64
|
+
# guard) and then a fifth (`@layer name;` may legally precede `@import`, which it missed,
|
|
65
|
+
# re-losing the font for ordinary modern CSS). Every one of those was a bug in DECIDING what
|
|
66
|
+
# the deck's CSS means. Emitting the deck's CSS VERBATIM in its own sheet removes the need to
|
|
67
|
+
# decide, so that entire class of defect is now unreachable rather than patched: NUL bytes,
|
|
68
|
+
# top-level `<!--`, a lone CR inside a string, escaped at-keywords and `@layer` are all
|
|
69
|
+
# simply the browser's business, exactly as they are in the deck's own stylesheet.
|
|
70
|
+
|
|
71
|
+
|
|
44
72
|
def build_slide_html(slide: dict, slide_deck: dict) -> str:
|
|
45
73
|
"""Build complete HTML for a single slide.
|
|
46
74
|
|
|
@@ -56,12 +84,11 @@ def build_slide_html(slide: dict, slide_deck: dict) -> str:
|
|
|
56
84
|
external_scripts = slide_deck.get("external_scripts", [])
|
|
57
85
|
deck_css = slide_deck.get("css", "")
|
|
58
86
|
deck_scripts = slide_deck.get("scripts", "")
|
|
59
|
-
|
|
87
|
+
|
|
60
88
|
# Clean up deck_scripts: remove any trailing extra IIFE closings
|
|
61
89
|
# deck_scripts should be a series of (function() { ... })(); blocks
|
|
62
90
|
# but sometimes there might be an extra })(); at the end
|
|
63
91
|
if deck_scripts:
|
|
64
|
-
import re
|
|
65
92
|
# Remove any trailing })(); that might be extra
|
|
66
93
|
deck_scripts = deck_scripts.rstrip()
|
|
67
94
|
# Count opening and closing IIFEs
|
|
@@ -102,8 +129,22 @@ def build_slide_html(slide: dict, slide_deck: dict) -> str:
|
|
|
102
129
|
}
|
|
103
130
|
)
|
|
104
131
|
|
|
132
|
+
# NO `crossorigin` — deliberately, and it must stay that way (WM-02).
|
|
133
|
+
#
|
|
134
|
+
# `crossorigin="anonymous"` turns these into CORS requests. cdn.tailwindcss.com
|
|
135
|
+
# answers with a 302 and no CORS headers, so the request failed with
|
|
136
|
+
# net::ERR_FAILED and Tailwind's Preflight never landed — measured consequence
|
|
137
|
+
# on a DS-OFF deck: h3 rendered 22px/600 in the exported file against
|
|
138
|
+
# 16px/400 on screen, up to 38.25 px of per-component drift and a heading
|
|
139
|
+
# losing a line to re-wrap. The frontend surfaces emit the same tag WITHOUT the
|
|
140
|
+
# attribute (see buildSlideDocument in frontend/src/services/slideDocument.ts)
|
|
141
|
+
# and it loads, so dropping it is parity with what the user actually saw.
|
|
142
|
+
#
|
|
143
|
+
# Safe to drop on its own because there is NO `integrity` attribute here: with
|
|
144
|
+
# SRI present, removing `crossorigin` would block the script outright and turn
|
|
145
|
+
# a 38 px drift into no styling at all.
|
|
105
146
|
scripts_html = "\n".join([
|
|
106
|
-
f' <script src="{src}"
|
|
147
|
+
f' <script src="{src}"></script>'
|
|
107
148
|
for src in external_scripts
|
|
108
149
|
])
|
|
109
150
|
|
|
@@ -147,7 +188,33 @@ def build_slide_html(slide: dict, slide_deck: dict) -> str:
|
|
|
147
188
|
background: #ffffff;
|
|
148
189
|
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
149
190
|
}}
|
|
150
|
-
|
|
191
|
+
</style>
|
|
192
|
+
<style>{deck_css}</style>
|
|
193
|
+
<style>
|
|
194
|
+
/* After deck CSS: the fixed-frame document owns its OWN box (WM-01).
|
|
195
|
+
The html/body rule above is emitted BEFORE the deck, so a deck-authored
|
|
196
|
+
`body {{ padding: 48px 0; gap: 48px }}` — real generator output — used to win
|
|
197
|
+
on order and translate the WHOLE slide down: `.slide` measured at y=48, and
|
|
198
|
+
the .pptx carried that offset on every positioned component (dy 46-48 px)
|
|
199
|
+
including the background rectangle, whose bottom then sat at 768 inside a
|
|
200
|
+
720 frame. So: a 48 px unpainted band at the top of every exported slide and
|
|
201
|
+
48 px of content pushed past the clip.
|
|
202
|
+
Restated HERE, after the deck, exactly as the frontend preview surfaces
|
|
203
|
+
already do it (SLIDE_PREVIEW_RESET_STYLE is appended after the deck CSS in
|
|
204
|
+
buildSlideDocument), so the export agrees with what the user saw on screen.
|
|
205
|
+
Deliberately NOT added to the shared SLIDE_ROOT_RESET_STYLE: that constant is
|
|
206
|
+
also injected into the standalone MULTI-slide export, whose scrolling layout
|
|
207
|
+
depends on `body {{ padding: 40px 20px }}`.
|
|
208
|
+
No `!important`, matching the frontend rule: it wins by ORDER at equal
|
|
209
|
+
specificity, so both surfaces treat a deck's `!important` body padding the
|
|
210
|
+
same way and cannot drift apart. That ORDER now spans separate `<style>`
|
|
211
|
+
elements — the deck has a stylesheet of its own so its leading `@import`
|
|
212
|
+
stays valid (WF-03) — and the cascade orders separate sheets by DOCUMENT
|
|
213
|
+
ORDER, so this later sheet still wins exactly as it did inside one block. */
|
|
214
|
+
html, body {{
|
|
215
|
+
margin: 0;
|
|
216
|
+
padding: 0;
|
|
217
|
+
}}
|
|
151
218
|
/* After deck CSS: flatten the slide root (outer margin / radius / shadow) —
|
|
152
219
|
inside this fixed 1280x720 overflow:hidden document a root margin
|
|
153
220
|
shifts content past the clip and truncates the export's bottom edge
|
|
@@ -25,6 +25,7 @@ import re
|
|
|
25
25
|
import unicodedata
|
|
26
26
|
from collections import OrderedDict
|
|
27
27
|
from typing import Any, List, Optional, cast
|
|
28
|
+
from urllib.parse import unquote
|
|
28
29
|
|
|
29
30
|
from fastapi import APIRouter, Depends, File, Form, HTTPException, Response, UploadFile, status
|
|
30
31
|
from pydantic import BaseModel, Field
|
|
@@ -833,6 +834,60 @@ def set_default_design_system(ds_id: int, db: Session = Depends(get_db)):
|
|
|
833
834
|
)
|
|
834
835
|
|
|
835
836
|
|
|
837
|
+
# SDR-4437 HIGH-3: workspace-global library writes are admin-only. REMOVING the
|
|
838
|
+
# org default is the same org-wide state change as setting it, so it carries the
|
|
839
|
+
# same gate — authorship must not buy it.
|
|
840
|
+
@router.post(
|
|
841
|
+
"/{ds_id}/clear-default", response_model=DesignSystemDetail,
|
|
842
|
+
dependencies=[Depends(require_admin)],
|
|
843
|
+
)
|
|
844
|
+
def clear_default_design_system(ds_id: int, db: Session = Depends(get_db)):
|
|
845
|
+
"""Remove the org-wide default, returning the library to "no default".
|
|
846
|
+
|
|
847
|
+
The lifecycle used to be ASYMMETRIC: ``set-default`` could promote a system and
|
|
848
|
+
switch between systems, but nothing could withdraw the flag — so returning to
|
|
849
|
+
no-default meant DELETING the design system or promoting a different one, and
|
|
850
|
+
the legacy slide-style fallback was unreachable in practice. Generation then
|
|
851
|
+
resolves no design system (``get_default_design_system_id`` returns None) and
|
|
852
|
+
new sessions take the legacy path.
|
|
853
|
+
|
|
854
|
+
Idempotent: clearing a system that is not the default succeeds and changes
|
|
855
|
+
nothing, so a double-click cannot produce an error to interpret.
|
|
856
|
+
|
|
857
|
+
Deliberately asymmetric with ``set-default``, which refuses an INACTIVE row:
|
|
858
|
+
promoting a tombstone into org-wide state is wrong, but REMOVING org-wide state
|
|
859
|
+
is always the safe direction and must never be blocked — refusing it would
|
|
860
|
+
strand the flag with no way to withdraw it.
|
|
861
|
+
|
|
862
|
+
Clearing correctly UNFREEZES rename/delete for the row's author: the freeze in
|
|
863
|
+
:func:`_require_creator_or_admin` is a function of the LOADED row's
|
|
864
|
+
``is_default``, so it lifts when the flag does. That grants a non-admin nothing
|
|
865
|
+
new, because this route is itself admin-only.
|
|
866
|
+
"""
|
|
867
|
+
try:
|
|
868
|
+
ds = db.query(DesignSystem).filter(DesignSystem.id == ds_id).first()
|
|
869
|
+
if not ds:
|
|
870
|
+
raise HTTPException(
|
|
871
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
872
|
+
detail=f"Design system {ds_id} not found",
|
|
873
|
+
)
|
|
874
|
+
if ds.is_default:
|
|
875
|
+
ds.is_default = False
|
|
876
|
+
db.commit()
|
|
877
|
+
db.refresh(ds)
|
|
878
|
+
logger.info(f"Cleared default design system: {ds.name} (id={ds.id})")
|
|
879
|
+
return _detail(ds)
|
|
880
|
+
except HTTPException:
|
|
881
|
+
raise
|
|
882
|
+
except Exception as e:
|
|
883
|
+
db.rollback()
|
|
884
|
+
logger.error(f"Error clearing default design system {ds_id}: {e}", exc_info=True)
|
|
885
|
+
raise HTTPException(
|
|
886
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
887
|
+
detail="Failed to clear default design system",
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
|
|
836
891
|
def _template_thumbnail_url(ds_id: int, template_id: int) -> str:
|
|
837
892
|
return f"/api/settings/design-systems/{ds_id}/templates/{template_id}/thumbnail"
|
|
838
893
|
|
|
@@ -1180,6 +1235,35 @@ _TEXT_SOURCE_MIMES = frozenset(
|
|
|
1180
1235
|
# pass — only ever seen in double-encoding smuggling attempts, so reject outright.
|
|
1181
1236
|
_ENCODED_TRAVERSAL_RE = re.compile(r"%(2e|2f|5c)", re.IGNORECASE)
|
|
1182
1237
|
|
|
1238
|
+
# How many decode passes a value may need before it stops changing. A value still
|
|
1239
|
+
# mutating after this many is pathological, never a real bundle path, and is
|
|
1240
|
+
# refused rather than normalized (see :func:`_validated_file_path`).
|
|
1241
|
+
_MAX_DECODE_PASSES = 4
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def _has_canonical_shape(value: str) -> bool:
|
|
1245
|
+
"""True when ``value`` has the shape of a canonical bundle-relative path.
|
|
1246
|
+
|
|
1247
|
+
The SHAPE half of :func:`_validated_file_path`, factored out so the exact same
|
|
1248
|
+
rules can be re-applied to each decoded form of a request path — one source of
|
|
1249
|
+
truth, rather than a second, weaker copy of the checks.
|
|
1250
|
+
"""
|
|
1251
|
+
if not value or "\\" in value:
|
|
1252
|
+
return False
|
|
1253
|
+
# NUL / C0 control characters never appear in a legitimate stored path, and
|
|
1254
|
+
# NUL in particular must be rejected BEFORE the DB lookup: psycopg2 refuses
|
|
1255
|
+
# NUL in a bound parameter (ValueError), which would surface as a 500
|
|
1256
|
+
# instead of the uniform opaque 404 (SQLite masks this as a no-match 404).
|
|
1257
|
+
if any(ord(ch) < 0x20 for ch in value):
|
|
1258
|
+
return False
|
|
1259
|
+
if _ENCODED_TRAVERSAL_RE.search(value):
|
|
1260
|
+
return False
|
|
1261
|
+
if value.startswith("/") or re.match(r"^[A-Za-z]:", value):
|
|
1262
|
+
return False
|
|
1263
|
+
if any(segment in ("", ".", "..") for segment in value.split("/")):
|
|
1264
|
+
return False
|
|
1265
|
+
return True
|
|
1266
|
+
|
|
1183
1267
|
|
|
1184
1268
|
def _validated_file_path(raw: str) -> Optional[str]:
|
|
1185
1269
|
"""Return ``raw`` when it is a canonical bundle-relative path, else ``None``.
|
|
@@ -1190,22 +1274,32 @@ def _validated_file_path(raw: str) -> Optional[str]:
|
|
|
1190
1274
|
backslashes, absolute/drive paths, empty/``.``/``..`` segments, and lingering
|
|
1191
1275
|
percent-encoded traversal bytes all fail. Lookups are DB-exact within one
|
|
1192
1276
|
design system (no filesystem involved), so this is defence-in-depth.
|
|
1277
|
+
|
|
1278
|
+
MULTIPLY-ENCODED forms are refused too. ``_ENCODED_TRAVERSAL_RE`` matches a
|
|
1279
|
+
lingering ``%2e``/``%2f``/``%5c``, but in ``%252e`` the traversal is hidden
|
|
1280
|
+
behind its own encoded ``%``, so ONE pass of that check cannot see it. The
|
|
1281
|
+
value is therefore decoded repeatedly and every intermediate form must satisfy
|
|
1282
|
+
the same shape rules; a value that keeps decoding into a traversal is refused
|
|
1283
|
+
at any depth.
|
|
1284
|
+
|
|
1285
|
+
The refusal keys on TRAVERSAL, never on the ``%`` character: a literal ``%``
|
|
1286
|
+
is legitimate in a brand's own filename, and an escape that decodes to
|
|
1287
|
+
something harmless (``%41`` -> ``A``) stays accepted. Rejecting ``%`` outright
|
|
1288
|
+
would turn a brand's real file into a 404 — the same class of brand-hostile
|
|
1289
|
+
limit the uncapped-text columns exist to avoid.
|
|
1193
1290
|
"""
|
|
1194
|
-
if not raw
|
|
1195
|
-
return None
|
|
1196
|
-
# NUL / C0 control characters never appear in a legitimate stored path, and
|
|
1197
|
-
# NUL in particular must be rejected BEFORE the DB lookup: psycopg2 refuses
|
|
1198
|
-
# NUL in a bound parameter (ValueError), which would surface as a 500
|
|
1199
|
-
# instead of the uniform opaque 404 (SQLite masks this as a no-match 404).
|
|
1200
|
-
if any(ord(ch) < 0x20 for ch in raw):
|
|
1201
|
-
return None
|
|
1202
|
-
if _ENCODED_TRAVERSAL_RE.search(raw):
|
|
1203
|
-
return None
|
|
1204
|
-
if raw.startswith("/") or re.match(r"^[A-Za-z]:", raw):
|
|
1205
|
-
return None
|
|
1206
|
-
if any(segment in ("", ".", "..") for segment in raw.split("/")):
|
|
1291
|
+
if not _has_canonical_shape(raw):
|
|
1207
1292
|
return None
|
|
1208
|
-
|
|
1293
|
+
candidate = raw
|
|
1294
|
+
for _ in range(_MAX_DECODE_PASSES):
|
|
1295
|
+
decoded = unquote(candidate)
|
|
1296
|
+
if decoded == candidate:
|
|
1297
|
+
return raw
|
|
1298
|
+
if not _has_canonical_shape(decoded):
|
|
1299
|
+
return None
|
|
1300
|
+
candidate = decoded
|
|
1301
|
+
# Still changing after _MAX_DECODE_PASSES: refuse rather than keep unwrapping.
|
|
1302
|
+
return None
|
|
1209
1303
|
|
|
1210
1304
|
|
|
1211
1305
|
def _is_text_source(path: str, mime: str) -> bool:
|
{databricks_tellr_app-0.4.2.dev13 → databricks_tellr_app-0.4.2.dev15}/src/services/agent_factory.py
RENAMED
|
@@ -312,6 +312,44 @@ def _get_prompt_content(
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
|
|
315
|
+
def _design_system_is_active(design_system_id: int) -> bool:
|
|
316
|
+
"""True when that design system exists and is ACTIVE.
|
|
317
|
+
|
|
318
|
+
The tool gate below needs the same verdict the prompt branch reaches at
|
|
319
|
+
:func:`_get_prompt_content` (``.filter_by(id=…, is_active=True)``, else
|
|
320
|
+
*"Design system not found, using default"*), so both halves of a generation
|
|
321
|
+
request agree about whether a design system is in play.
|
|
322
|
+
|
|
323
|
+
NOT the same question as ``chat_service.resolve_active_design_system_id``,
|
|
324
|
+
despite the name: that resolves the ``{{ds-asset:ID}}`` scope for a deck
|
|
325
|
+
RESPONSE and deliberately still resolves a soft-deleted system's bytes, so
|
|
326
|
+
already-generated decks keep their fonts and images (the D7 retention
|
|
327
|
+
contract). This is the AUTHORING question — may new content be made against
|
|
328
|
+
this brand — and a tombstone answers no.
|
|
329
|
+
|
|
330
|
+
Fails CLOSED. A lookup error degrades exactly the way the prompt branch does
|
|
331
|
+
(no brand material) rather than advertising a tool whose own lookup would fail
|
|
332
|
+
in the same breath.
|
|
333
|
+
"""
|
|
334
|
+
try:
|
|
335
|
+
from src.core.database import get_db_session
|
|
336
|
+
from src.database.models import DesignSystem
|
|
337
|
+
|
|
338
|
+
with get_db_session() as db:
|
|
339
|
+
return (
|
|
340
|
+
db.query(DesignSystem.id)
|
|
341
|
+
.filter_by(id=design_system_id, is_active=True)
|
|
342
|
+
.first()
|
|
343
|
+
is not None
|
|
344
|
+
)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error(
|
|
347
|
+
f"Failed to resolve design_system_id for tool registration: {e}",
|
|
348
|
+
extra={"design_system_id": design_system_id},
|
|
349
|
+
)
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
315
353
|
def _build_tools(
|
|
316
354
|
config: AgentConfig,
|
|
317
355
|
session_data: dict[str, Any],
|
|
@@ -351,12 +389,21 @@ def _build_tools(
|
|
|
351
389
|
)
|
|
352
390
|
tools.append(image_search_tool)
|
|
353
391
|
|
|
354
|
-
# Brand-asset search tool — ONLY when a design system is
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
|
|
392
|
+
# Brand-asset search tool — ONLY when a selected design system is ACTIVE.
|
|
393
|
+
# Bound to the design_system_id via closure so it surfaces only that system's
|
|
394
|
+
# assets; the compiled style's ASSET CONTRACT tells the model to call it. A
|
|
395
|
+
# pinned template_id (Phase 4) shapes prompt assembly only and never changes
|
|
396
|
+
# tool registration.
|
|
397
|
+
#
|
|
398
|
+
# The is_active half matters because a session keeps its pin after the design
|
|
399
|
+
# system is soft-deleted: on the id alone, generation got a fully working brand
|
|
400
|
+
# tool for a TOMBSTONE (measured: "Found 2 brand asset(s)" with embeddable
|
|
401
|
+
# handles) while the prompt branch, which does filter is_active, supplied no
|
|
402
|
+
# brand at all — so new content was authored against a deleted brand with none
|
|
403
|
+
# of its instructions. Both halves now answer the same question.
|
|
404
|
+
if config.design_system_id is not None and _design_system_is_active(
|
|
405
|
+
config.design_system_id
|
|
406
|
+
):
|
|
360
407
|
tools.append(build_ds_asset_tool(config.design_system_id))
|
|
361
408
|
|
|
362
409
|
genie_index = 0
|
|
@@ -43,6 +43,13 @@ class JailResult:
|
|
|
43
43
|
returncode: int
|
|
44
44
|
timed_out: bool
|
|
45
45
|
stderr_tail: str = ""
|
|
46
|
+
#: Per-slide outcomes reported by the trusted runner, in the order received:
|
|
47
|
+
#: True when the slide's generated code produced its slide, False when the
|
|
48
|
+
#: deterministic placeholder fallback was used. EMPTY when the runner reported
|
|
49
|
+
#: nothing (a runner with no result channel, or one that died before emitting) —
|
|
50
|
+
#: callers must treat empty as "unknown", never as "all failed", so an absent
|
|
51
|
+
#: channel cannot manufacture a failure.
|
|
52
|
+
slide_results: tuple = ()
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
@dataclass
|
|
@@ -177,8 +184,14 @@ def _spawn(
|
|
|
177
184
|
|
|
178
185
|
assert proc.stdout is not None
|
|
179
186
|
|
|
187
|
+
slide_results: List[bool] = []
|
|
188
|
+
|
|
180
189
|
def _drain_stdout():
|
|
181
190
|
for line in proc.stdout:
|
|
191
|
+
result = protocol.decode_slide_result(line)
|
|
192
|
+
if result is not None:
|
|
193
|
+
slide_results.append(result[1])
|
|
194
|
+
continue
|
|
182
195
|
decoded = protocol.decode_progress(line)
|
|
183
196
|
if decoded and progress_cb:
|
|
184
197
|
try:
|
|
@@ -207,6 +220,7 @@ def _spawn(
|
|
|
207
220
|
returncode=proc.returncode if proc.returncode is not None else -1,
|
|
208
221
|
timed_out=timed_out,
|
|
209
222
|
stderr_tail="".join(stderr_lines[-40:]),
|
|
223
|
+
slide_results=tuple(slide_results),
|
|
210
224
|
)
|
|
211
225
|
finally:
|
|
212
226
|
shutil.rmtree(env["HOME"], ignore_errors=True)
|
|
@@ -106,14 +106,22 @@ def run(job_dir: str, output_path: str, emit: Optional[Callable[[str], None]] =
|
|
|
106
106
|
sdir = Path(job_dir) / entry["dir"]
|
|
107
107
|
assets_dir = str(sdir / protocol.ASSETS_DIR)
|
|
108
108
|
|
|
109
|
+
# Report whether this slide got REAL content or the deterministic
|
|
110
|
+
# placeholder. Both fallback branches below are invisible in the saved
|
|
111
|
+
# file — it is a valid .pptx either way — so the host cannot otherwise
|
|
112
|
+
# tell an all-placeholder deck from a good one.
|
|
113
|
+
rendered = True
|
|
109
114
|
if not entry.get("has_code"):
|
|
110
115
|
_add_fallback(prs, i, f"Slide {i}")
|
|
116
|
+
rendered = False
|
|
111
117
|
else:
|
|
112
118
|
html_str = (sdir / protocol.HTML_NAME).read_text(encoding="utf-8")
|
|
113
119
|
code = (sdir / protocol.CODE_NAME).read_text(encoding="utf-8")
|
|
114
120
|
if not _exec_snippet(code, prs, html_str, assets_dir):
|
|
115
121
|
_add_fallback(prs, i, "Slide Content")
|
|
122
|
+
rendered = False
|
|
116
123
|
|
|
124
|
+
emit(protocol.encode_slide_result(entry["index"], rendered))
|
|
117
125
|
emit(protocol.encode_progress(i, total, f"Building slide {i}/{total}…"))
|
|
118
126
|
|
|
119
127
|
prs.save(output_path)
|
|
@@ -23,6 +23,39 @@ HTML_NAME = "html.txt"
|
|
|
23
23
|
ASSETS_DIR = "assets"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
# Sentinel prefix for the trusted per-slide RESULT channel. Same trust property as
|
|
27
|
+
# PROGRESS_PREFIX and for the same reason: only the trusted runner emits it, and
|
|
28
|
+
# generated code's stdout is redirected to stderr before any snippet executes, so a
|
|
29
|
+
# snippet cannot forge a result for itself. The host needs this because whether a
|
|
30
|
+
# slide produced REAL content is only known after validation and execution — a
|
|
31
|
+
# non-empty snippet can still be rejected by the import allowlist or raise when it
|
|
32
|
+
# runs, and both outcomes fall back to a placeholder slide. Without a report, an
|
|
33
|
+
# all-placeholder deck is indistinguishable from a good one.
|
|
34
|
+
RESULT_PREFIX = "@@TELLR_JAIL_SLIDE_RESULT@@ "
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def encode_slide_result(index: int, rendered: bool) -> str:
|
|
38
|
+
"""Encode one per-slide outcome as a single stdout line (with newline).
|
|
39
|
+
|
|
40
|
+
``rendered`` is True only when the slide's own generated code ran and produced
|
|
41
|
+
its slide; False whenever the deterministic placeholder fallback was used.
|
|
42
|
+
"""
|
|
43
|
+
payload = json.dumps({"index": index, "rendered": bool(rendered)})
|
|
44
|
+
return f"{RESULT_PREFIX}{payload}\n"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def decode_slide_result(line: str):
|
|
48
|
+
"""Decode a stdout line into ``(index, rendered)``, or None when the line is
|
|
49
|
+
not a slide-result line / is malformed."""
|
|
50
|
+
if not line.startswith(RESULT_PREFIX):
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(line[len(RESULT_PREFIX):].strip())
|
|
54
|
+
return int(data["index"]), bool(data["rendered"])
|
|
55
|
+
except (ValueError, KeyError, TypeError):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
26
59
|
def encode_progress(current: int, total: int, message: str) -> str:
|
|
27
60
|
"""Encode one progress event as a single stdout line (with newline)."""
|
|
28
61
|
payload = json.dumps({"current": current, "total": total, "message": message})
|