databricks-tellr-app 0.4.3.dev5__tar.gz → 0.4.3.dev7__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.3.dev5 → databricks_tellr_app-0.4.3.dev7}/PKG-INFO +1 -1
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/pyproject.toml +1 -1
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/images.py +45 -27
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/requests.py +2 -2
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/chat_service.py +12 -9
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/database.py +158 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/image.py +15 -1
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/agent.py +4 -17
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/image_service.py +9 -9
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/image_tools.py +2 -2
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/image_utils.py +5 -4
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/README.md +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app/run.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/requires.txt +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/top_level.txt +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/setup.cfg +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/setup.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/_otel_bootstrap.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/fixtures/tour_demo_deck.json +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/main.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/mcp_auth.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/mcp_server.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/csrf.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/request_logging.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/security_headers.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/_authz.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/admin.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/admin_usage.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/agent_config.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/chat.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/deck_contributors.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/export.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/feedback.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/google_slides.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/local_version.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/profiles.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/sessions.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/contributors.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/deck_prompts.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/design_systems.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/identities.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/slide_styles.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/setup.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/slides.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/tools.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/tour.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/verification.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/version.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/agent_config.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/feedback.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/responses.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/settings/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/settings/requests.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/settings/responses.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/streaming.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/export_job_queue.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/feedback_service.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/job_queue.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/session_manager.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/session_naming.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/usage_events.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/usage_service.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/utils/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/utils/validation.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/config_loader.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/context_utils.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/databricks_client.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/defaults.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/encryption.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/init_default_profile.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/lakebase.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/migrate_profiles_to_agent_config.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/mlflow_agent_spans.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/mlflow_tracing.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/otel_logging.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/permission_context.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/prompt_modules.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/settings_db.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/user_context.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/deck_contributor.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/design_system.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/encryption_key.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/feedback.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/genie_space.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/google_global_credentials.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/google_oauth_token.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/identity.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/profile.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/profile_contributor.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/prompts.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/request_log.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/session.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/slide_deck_prompt.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/slide_style_library.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/usage_event.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/user_preference.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/types.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/domain/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/domain/slide.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/domain/slide_deck.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/agent_factory.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/config_service.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/config_validator.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/ast_guard.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/codeprep.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/gslides_runner.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/jail.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/pptx_runner.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/protocol.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/design_system_compiler.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/design_system_service.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/design_system_templates.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/drive_uploader.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/evaluation/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/evaluation/llm_judge.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/genie_service.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/google_slides_auth.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/google_slides_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/html_to_google_slides.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/html_to_pptx.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_provider.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/account_provider.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/local_provider.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/workspace_provider.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/permission_service.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/pptx_from_html_huashu.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/pptx_from_records.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/pptx_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/profile_service.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/streaming_callback.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/agent_bricks_tool.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/ds_asset_tool.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/genie_tool.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/mcp_tool.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/model_endpoint_tool.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/vector_tool.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/validator.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/__init__.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/css_utils.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/ds_asset_utils.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/error_handling.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/html_safety.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/html_utils.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/js_validator.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/logging_config.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/pi_filter.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/slide_hash.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/spotlight.py +0 -0
- {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/text_caps.py +0 -0
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/images.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Image upload and management API endpoints."""
|
|
2
|
+
import base64
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
from typing import List, Optional
|
|
@@ -20,7 +21,7 @@ router = APIRouter(prefix="/api/images", tags=["images"])
|
|
|
20
21
|
|
|
21
22
|
class ImageResponse(BaseModel):
|
|
22
23
|
"""Response schema for image metadata."""
|
|
23
|
-
id: int
|
|
24
|
+
id: str # Opaque token (SDR-4437 F-TM-7) — the internal int PK is never exposed.
|
|
24
25
|
filename: str
|
|
25
26
|
original_filename: str
|
|
26
27
|
mime_type: str
|
|
@@ -46,7 +47,7 @@ class ImageListResponse(BaseModel):
|
|
|
46
47
|
|
|
47
48
|
class ImageDataResponse(BaseModel):
|
|
48
49
|
"""Response containing image base64 data."""
|
|
49
|
-
id:
|
|
50
|
+
id: str # Opaque token (SDR-4437 F-TM-7).
|
|
50
51
|
mime_type: str
|
|
51
52
|
base64_data: str
|
|
52
53
|
data_uri: str # Ready-to-use: "data:image/png;base64,..."
|
|
@@ -80,7 +81,7 @@ def _get_current_user() -> str:
|
|
|
80
81
|
|
|
81
82
|
def _image_to_response(img: ImageAsset) -> ImageResponse:
|
|
82
83
|
return ImageResponse(
|
|
83
|
-
id=img.
|
|
84
|
+
id=img.token, # Expose the opaque token, never the enumerable int PK.
|
|
84
85
|
filename=img.filename,
|
|
85
86
|
original_filename=img.original_filename,
|
|
86
87
|
mime_type=img.mime_type,
|
|
@@ -167,58 +168,75 @@ def list_images(
|
|
|
167
168
|
)
|
|
168
169
|
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
def _deny_ephemeral_cross_user(image: ImageAsset) -> None:
|
|
172
|
+
"""Defense-in-depth (SDR-4437 F-TM-7): chat-pasted ("ephemeral") images are
|
|
173
|
+
private to their uploader. Library images stay open-read. Raise 404 (not 403)
|
|
174
|
+
so a wrong caller cannot even confirm the image exists.
|
|
175
|
+
"""
|
|
176
|
+
if image.category == "ephemeral" and image.uploaded_by != _get_current_user():
|
|
177
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.get("/{token}", response_model=ImageResponse)
|
|
181
|
+
def get_image(token: str, db: Session = Depends(get_db)):
|
|
182
|
+
"""Get image metadata by opaque token."""
|
|
173
183
|
try:
|
|
174
184
|
image = db.query(ImageAsset).filter(
|
|
175
|
-
ImageAsset.
|
|
185
|
+
ImageAsset.token == token,
|
|
176
186
|
ImageAsset.is_active == True,
|
|
177
187
|
).first()
|
|
178
188
|
if not image:
|
|
179
189
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
|
|
190
|
+
_deny_ephemeral_cross_user(image)
|
|
180
191
|
return _image_to_response(image)
|
|
181
192
|
except HTTPException:
|
|
182
193
|
raise
|
|
183
194
|
except Exception as e:
|
|
184
|
-
logger.error(f"Error getting image
|
|
195
|
+
logger.error(f"Error getting image: {e}", exc_info=True)
|
|
185
196
|
raise HTTPException(
|
|
186
197
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
187
198
|
detail="Failed to get image",
|
|
188
199
|
)
|
|
189
200
|
|
|
190
201
|
|
|
191
|
-
@router.get("/{
|
|
192
|
-
def get_image_data(
|
|
193
|
-
"""Get full image as base64 data (for embedding in slides)."""
|
|
202
|
+
@router.get("/{token}/data", response_model=ImageDataResponse)
|
|
203
|
+
def get_image_data(token: str, db: Session = Depends(get_db)):
|
|
204
|
+
"""Get full image as base64 data (for embedding in slides), keyed by token."""
|
|
194
205
|
try:
|
|
195
|
-
|
|
206
|
+
image = db.query(ImageAsset).filter(
|
|
207
|
+
ImageAsset.token == token,
|
|
208
|
+
ImageAsset.is_active == True,
|
|
209
|
+
).first()
|
|
210
|
+
if not image:
|
|
211
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
|
|
212
|
+
_deny_ephemeral_cross_user(image)
|
|
213
|
+
b64_data = base64.b64encode(image.image_data).decode("utf-8")
|
|
196
214
|
return ImageDataResponse(
|
|
197
|
-
id=
|
|
198
|
-
mime_type=mime_type,
|
|
215
|
+
id=image.token,
|
|
216
|
+
mime_type=image.mime_type,
|
|
199
217
|
base64_data=b64_data,
|
|
200
|
-
data_uri=f"data:{mime_type};base64,{b64_data}",
|
|
218
|
+
data_uri=f"data:{image.mime_type};base64,{b64_data}",
|
|
201
219
|
)
|
|
202
|
-
except
|
|
203
|
-
raise
|
|
220
|
+
except HTTPException:
|
|
221
|
+
raise
|
|
204
222
|
except Exception as e:
|
|
205
|
-
logger.error(f"Error getting image data
|
|
223
|
+
logger.error(f"Error getting image data: {e}", exc_info=True)
|
|
206
224
|
raise HTTPException(
|
|
207
225
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
208
226
|
detail="Failed to get image data",
|
|
209
227
|
)
|
|
210
228
|
|
|
211
229
|
|
|
212
|
-
@router.put("/{
|
|
230
|
+
@router.put("/{token}", response_model=ImageResponse)
|
|
213
231
|
def update_image(
|
|
214
|
-
|
|
232
|
+
token: str,
|
|
215
233
|
request: ImageUpdateRequest,
|
|
216
234
|
db: Session = Depends(get_db),
|
|
217
235
|
):
|
|
218
236
|
"""Update image metadata (tags, description, category)."""
|
|
219
237
|
try:
|
|
220
238
|
image = db.query(ImageAsset).filter(
|
|
221
|
-
ImageAsset.
|
|
239
|
+
ImageAsset.token == token,
|
|
222
240
|
ImageAsset.is_active == True,
|
|
223
241
|
).first()
|
|
224
242
|
if not image:
|
|
@@ -246,20 +264,20 @@ def update_image(
|
|
|
246
264
|
raise
|
|
247
265
|
except Exception as e:
|
|
248
266
|
db.rollback()
|
|
249
|
-
logger.error(f"Error updating image
|
|
267
|
+
logger.error(f"Error updating image: {e}", exc_info=True)
|
|
250
268
|
raise HTTPException(
|
|
251
269
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
252
270
|
detail="Failed to update image",
|
|
253
271
|
)
|
|
254
272
|
|
|
255
273
|
|
|
256
|
-
@router.delete("/{
|
|
257
|
-
def delete_image(
|
|
274
|
+
@router.delete("/{token}", status_code=status.HTTP_204_NO_CONTENT)
|
|
275
|
+
def delete_image(token: str, db: Session = Depends(get_db)):
|
|
258
276
|
"""Soft-delete an image."""
|
|
259
277
|
try:
|
|
260
278
|
user = _get_current_user()
|
|
261
279
|
image = db.query(ImageAsset).filter(
|
|
262
|
-
ImageAsset.
|
|
280
|
+
ImageAsset.token == token,
|
|
263
281
|
ImageAsset.is_active == True, # noqa: E712
|
|
264
282
|
).first()
|
|
265
283
|
if not image:
|
|
@@ -270,14 +288,14 @@ def delete_image(image_id: int, db: Session = Depends(get_db)):
|
|
|
270
288
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
271
289
|
detail="You don't have permission to modify this image",
|
|
272
290
|
)
|
|
273
|
-
image_service.delete_image(db,
|
|
291
|
+
image_service.delete_image(db, token, user)
|
|
274
292
|
except HTTPException:
|
|
275
293
|
raise
|
|
276
294
|
except ValueError as e:
|
|
277
295
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
278
296
|
except Exception as e:
|
|
279
297
|
db.rollback()
|
|
280
|
-
logger.error(f"Error deleting image
|
|
298
|
+
logger.error(f"Error deleting image: {e}", exc_info=True)
|
|
281
299
|
raise HTTPException(
|
|
282
300
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
283
301
|
detail="Failed to delete image",
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/requests.py
RENAMED
|
@@ -74,9 +74,9 @@ class ChatRequest(BaseModel):
|
|
|
74
74
|
default=None,
|
|
75
75
|
description="Optional context containing the slides to edit",
|
|
76
76
|
)
|
|
77
|
-
image_ids: Optional[list[
|
|
77
|
+
image_ids: Optional[list[str]] = Field(
|
|
78
78
|
default=None,
|
|
79
|
-
description="
|
|
79
|
+
description="Opaque tokens of images attached to this message (SDR-4437 F-TM-7)",
|
|
80
80
|
)
|
|
81
81
|
agent_config: Optional[dict] = Field(
|
|
82
82
|
default=None,
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/chat_service.py
RENAMED
|
@@ -345,7 +345,7 @@ class ChatService:
|
|
|
345
345
|
session_id: str,
|
|
346
346
|
message: str,
|
|
347
347
|
slide_context: Optional[Dict[str, Any]] = None,
|
|
348
|
-
image_ids: Optional[List[
|
|
348
|
+
image_ids: Optional[List[str]] = None,
|
|
349
349
|
) -> Dict[str, Any]:
|
|
350
350
|
"""Send a message to the agent and get response.
|
|
351
351
|
|
|
@@ -828,7 +828,7 @@ class ChatService:
|
|
|
828
828
|
message: str,
|
|
829
829
|
slide_context: Optional[Dict[str, Any]] = None,
|
|
830
830
|
request_id: Optional[str] = None,
|
|
831
|
-
image_ids: Optional[List[
|
|
831
|
+
image_ids: Optional[List[str]] = None,
|
|
832
832
|
is_first_message_override: Optional[bool] = None,
|
|
833
833
|
) -> Generator[StreamEvent, None, None]:
|
|
834
834
|
"""Send a message and yield streaming events.
|
|
@@ -1993,27 +1993,30 @@ class ChatService:
|
|
|
1993
1993
|
|
|
1994
1994
|
return ([], None)
|
|
1995
1995
|
|
|
1996
|
-
def _inject_image_context(self, message: str, image_ids: List[
|
|
1997
|
-
"""Append image metadata to user message so the agent knows about attached images.
|
|
1996
|
+
def _inject_image_context(self, message: str, image_ids: List[str]) -> str:
|
|
1997
|
+
"""Append image metadata to user message so the agent knows about attached images.
|
|
1998
|
+
|
|
1999
|
+
``image_ids`` are opaque image tokens (SDR-4437 F-TM-7), not int PKs.
|
|
2000
|
+
"""
|
|
1998
2001
|
from src.core.database import get_db_session
|
|
1999
2002
|
from src.services import image_service
|
|
2000
2003
|
|
|
2001
2004
|
image_descriptions = []
|
|
2002
2005
|
with get_db_session() as db:
|
|
2003
|
-
for
|
|
2006
|
+
for img_token in image_ids:
|
|
2004
2007
|
try:
|
|
2005
2008
|
img = db.query(image_service.ImageAsset).filter(
|
|
2006
|
-
image_service.ImageAsset.
|
|
2009
|
+
image_service.ImageAsset.token == img_token,
|
|
2007
2010
|
image_service.ImageAsset.is_active == True,
|
|
2008
2011
|
).first()
|
|
2009
2012
|
if img:
|
|
2010
2013
|
image_descriptions.append(
|
|
2011
|
-
f'- Image ID {img.
|
|
2014
|
+
f'- Image ID {img.token}: "{img.original_filename}" '
|
|
2012
2015
|
f'({img.description or "no description"}). '
|
|
2013
|
-
f'Use: <img src="{{{{image:{img.
|
|
2016
|
+
f'Use: <img src="{{{{image:{img.token}}}}}" alt="{img.description or img.original_filename}" />'
|
|
2014
2017
|
)
|
|
2015
2018
|
except Exception as e:
|
|
2016
|
-
logger.warning(f"Failed to load image {
|
|
2019
|
+
logger.warning(f"Failed to load image {img_token} for context injection: {e}")
|
|
2017
2020
|
|
|
2018
2021
|
if not image_descriptions:
|
|
2019
2022
|
return message
|
|
@@ -524,6 +524,14 @@ def _run_migrations(engine, schema: str | None = None):
|
|
|
524
524
|
# --- image_assets.tags: json → jsonb (PostgreSQL) for native @> queries ---
|
|
525
525
|
_migrate_image_assets_tags_json_to_jsonb(conn, schema, _qual, is_sqlite)
|
|
526
526
|
|
|
527
|
+
# --- image_assets.token: unguessable external id + backfill (SDR-4437 F-TM-7) ---
|
|
528
|
+
_migrate_image_assets_add_token(conn, inspector, schema, _qual, is_sqlite)
|
|
529
|
+
|
|
530
|
+
# --- stored decks: rewrite {{image:<int-id>}} -> {{image:<token>}} placeholders
|
|
531
|
+
# --- (SDR-4437 F-TM-7). Runs AFTER the token backfill so every image has one. ---
|
|
532
|
+
_migrate_rewrite_deck_image_placeholders(
|
|
533
|
+
conn, inspector, schema, _qual, is_sqlite
|
|
534
|
+
)
|
|
527
535
|
# --- agent_config: style precedence in the DB, for the raw-SQL writers no
|
|
528
536
|
# --- Python-side bind hook can see (PostgreSQL only; no-op on SQLite) ---
|
|
529
537
|
_migrate_agent_config_precedence_trigger(
|
|
@@ -1058,6 +1066,156 @@ def _ensure_llm_judge_backend_default(
|
|
|
1058
1066
|
)
|
|
1059
1067
|
|
|
1060
1068
|
|
|
1069
|
+
def _migrate_image_assets_add_token(conn, inspector, schema, _qual, is_sqlite) -> None:
|
|
1070
|
+
"""Add the unguessable ``token`` column to image_assets and backfill (SDR-4437 F-TM-7).
|
|
1071
|
+
|
|
1072
|
+
The int PK is enumerable, so image reads now key on an opaque token. Fresh
|
|
1073
|
+
installs get the column via create_all(); existing tables need this ALTER +
|
|
1074
|
+
per-row backfill. Idempotent: gated on the column's absence.
|
|
1075
|
+
"""
|
|
1076
|
+
import secrets
|
|
1077
|
+
|
|
1078
|
+
from sqlalchemy import text
|
|
1079
|
+
|
|
1080
|
+
table = "image_assets"
|
|
1081
|
+
try:
|
|
1082
|
+
cols = {c["name"] for c in inspector.get_columns(table, schema=schema)}
|
|
1083
|
+
except Exception:
|
|
1084
|
+
return
|
|
1085
|
+
if not cols or "token" in cols:
|
|
1086
|
+
return
|
|
1087
|
+
|
|
1088
|
+
q = _qual(table)
|
|
1089
|
+
logger.info(f"Migration: adding token column to {table} (SDR-4437 F-TM-7)")
|
|
1090
|
+
# Add nullable first so the ALTER succeeds on a populated table.
|
|
1091
|
+
conn.execute(text(f"ALTER TABLE {q} ADD COLUMN token VARCHAR(64)"))
|
|
1092
|
+
|
|
1093
|
+
# Backfill every existing row with a distinct unguessable token.
|
|
1094
|
+
row_ids = [r[0] for r in conn.execute(text(f"SELECT id FROM {q} WHERE token IS NULL")).fetchall()]
|
|
1095
|
+
for row_id in row_ids:
|
|
1096
|
+
conn.execute(
|
|
1097
|
+
text(f"UPDATE {q} SET token = :tok WHERE id = :id"),
|
|
1098
|
+
{"tok": secrets.token_urlsafe(32), "id": row_id},
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
# Enforce uniqueness (and non-null on PostgreSQL, now that every row has a token).
|
|
1102
|
+
conn.execute(text(
|
|
1103
|
+
f"CREATE UNIQUE INDEX IF NOT EXISTS ix_image_assets_token ON {q} (token)"
|
|
1104
|
+
))
|
|
1105
|
+
if not is_sqlite:
|
|
1106
|
+
conn.execute(text(f"ALTER TABLE {q} ALTER COLUMN token SET NOT NULL"))
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
#: Persistent (table, column) pairs whose stored text can carry an
|
|
1110
|
+
#: ``{{image:<id>}}`` placeholder — the same field set ``substitute_deck_dict_images``
|
|
1111
|
+
#: resolves at render time, but at their storage sites:
|
|
1112
|
+
#: * ``session_slide_decks.deck_json`` — the serialized deck dict (slides[].html,
|
|
1113
|
+
#: top-level css, in-dict html_content),
|
|
1114
|
+
#: * ``session_slide_decks.html_content`` — the separate full knitted HTML column,
|
|
1115
|
+
#: * ``slide_deck_versions.deck_json`` — savepoint snapshots, so a restored older
|
|
1116
|
+
#: version renders too.
|
|
1117
|
+
#: All three are declared ``Column(Text)`` and no json/jsonb migration touches them,
|
|
1118
|
+
#: so they are plain ``TEXT`` on BOTH SQLite and PostgreSQL/Lakebase — ``replace()``
|
|
1119
|
+
#: applies directly with no jsonb cast. (Transient scratch such as
|
|
1120
|
+
#: ``chat_requests.result_json`` is deliberately excluded — it is not persisted deck
|
|
1121
|
+
#: state.)
|
|
1122
|
+
_DECK_PLACEHOLDER_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
1123
|
+
("session_slide_decks", "deck_json"),
|
|
1124
|
+
("session_slide_decks", "html_content"),
|
|
1125
|
+
("slide_deck_versions", "deck_json"),
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _migrate_rewrite_deck_image_placeholders(conn, inspector, schema, _qual, is_sqlite) -> None:
|
|
1130
|
+
"""Rewrite stored deck ``{{image:<int-id>}}`` placeholders to ``{{image:<token>}}`` (SDR-4437 F-TM-7).
|
|
1131
|
+
|
|
1132
|
+
PR #237 switched the external image identifier from the enumerable int PK to
|
|
1133
|
+
an unguessable token, and ``substitute_image_placeholders`` now resolves
|
|
1134
|
+
``{{image:<token>}}``. Decks saved BEFORE that change have ``{{image:<int-id>}}``
|
|
1135
|
+
baked into their stored HTML/JSON, so their embedded images would stop
|
|
1136
|
+
rendering. This one-time, idempotent migration rewrites those placeholders in
|
|
1137
|
+
place, using the ``id -> token`` map that ``_migrate_image_assets_add_token``
|
|
1138
|
+
(which runs just before this) has guaranteed exists on every image row.
|
|
1139
|
+
|
|
1140
|
+
The string work stays IN THE DATABASE — Python only drives a per-``(id, token)``
|
|
1141
|
+
loop of set-based ``replace()`` UPDATEs, exactly like the token backfill drives
|
|
1142
|
+
its loop. Deck blobs are never pulled into the app to be regexed.
|
|
1143
|
+
|
|
1144
|
+
Correctness / performance:
|
|
1145
|
+
|
|
1146
|
+
- **Per-image sequential loop, not a join.** A single
|
|
1147
|
+
``UPDATE ... FROM image_assets`` applies only ONE arbitrary matched row per
|
|
1148
|
+
target row, so a deck referencing two images would be half-rewritten. The
|
|
1149
|
+
sequential per-image ``replace()`` accumulates correctly across multi-image
|
|
1150
|
+
decks. ``regexp_replace`` can't express it either — the replacement is a
|
|
1151
|
+
per-match lookup into ``image_assets``, not a fixed template.
|
|
1152
|
+
- **Prefix-safe.** The literal ``{{image:5}}`` (closing braces included) never
|
|
1153
|
+
matches inside ``{{image:51}}``, so there is no id-prefix collision.
|
|
1154
|
+
- **Idempotent + cheap.** The ``LIKE`` guard means once the int placeholders are
|
|
1155
|
+
gone (first run) re-runs match nothing; unreferenced or hard-deleted ids are
|
|
1156
|
+
simply never touched. Cost is O(images) set-based UPDATEs, fine at startup.
|
|
1157
|
+
- **No jsonb cast.** Every target column (:data:`_DECK_PLACEHOLDER_COLUMNS`) is
|
|
1158
|
+
``TEXT`` on both backends, so ``replace()`` applies directly. Tokens are
|
|
1159
|
+
``[A-Za-z0-9_-]`` (JSON-safe), so text-level replacement inside a JSON blob
|
|
1160
|
+
cannot corrupt its structure.
|
|
1161
|
+
- **Soft-deleted images included** (no ``is_active`` filter) — a deck may still
|
|
1162
|
+
reference an image that was later soft-deleted, and it must keep resolving.
|
|
1163
|
+
|
|
1164
|
+
A no-op when ``image_assets`` (or its ``token`` column) or every target table is
|
|
1165
|
+
absent on a given deploy.
|
|
1166
|
+
"""
|
|
1167
|
+
from sqlalchemy import text
|
|
1168
|
+
|
|
1169
|
+
# image_assets + its token column must exist (guaranteed by the prior migration
|
|
1170
|
+
# on any live deploy; guarded so this can be exercised standalone).
|
|
1171
|
+
try:
|
|
1172
|
+
image_cols = {c["name"] for c in inspector.get_columns("image_assets", schema=schema)}
|
|
1173
|
+
except Exception:
|
|
1174
|
+
return
|
|
1175
|
+
if "token" not in image_cols:
|
|
1176
|
+
return
|
|
1177
|
+
|
|
1178
|
+
# Only rewrite target tables/columns that actually exist on this deploy.
|
|
1179
|
+
live_targets: list[tuple[str, str]] = []
|
|
1180
|
+
for table_name, column_name in _DECK_PLACEHOLDER_COLUMNS:
|
|
1181
|
+
try:
|
|
1182
|
+
cols = {c["name"] for c in inspector.get_columns(table_name, schema=schema)}
|
|
1183
|
+
except Exception:
|
|
1184
|
+
continue
|
|
1185
|
+
if column_name in cols:
|
|
1186
|
+
live_targets.append((table_name, column_name))
|
|
1187
|
+
if not live_targets:
|
|
1188
|
+
return
|
|
1189
|
+
|
|
1190
|
+
rows = conn.execute(
|
|
1191
|
+
text(f"SELECT id, token FROM {_qual('image_assets')} WHERE token IS NOT NULL")
|
|
1192
|
+
).fetchall()
|
|
1193
|
+
if not rows:
|
|
1194
|
+
return
|
|
1195
|
+
|
|
1196
|
+
logger.info(
|
|
1197
|
+
"Migration: rewriting {{image:<id>}} deck placeholders to tokens for "
|
|
1198
|
+
"%d images across %d columns (SDR-4437 F-TM-7)",
|
|
1199
|
+
len(rows),
|
|
1200
|
+
len(live_targets),
|
|
1201
|
+
)
|
|
1202
|
+
for image_id, token in rows:
|
|
1203
|
+
old = f"{{{{image:{image_id}}}}}"
|
|
1204
|
+
new = f"{{{{image:{token}}}}}"
|
|
1205
|
+
for table_name, column_name in live_targets:
|
|
1206
|
+
q = _qual(table_name)
|
|
1207
|
+
# Identifiers come from the hardcoded _DECK_PLACEHOLDER_COLUMNS; the
|
|
1208
|
+
# placeholder literals are bound parameters. The LIKE guard makes rows
|
|
1209
|
+
# without this id (and every row on a re-run) a no-op.
|
|
1210
|
+
conn.execute(
|
|
1211
|
+
text(
|
|
1212
|
+
f"UPDATE {q} SET {column_name} = replace({column_name}, :old, :new) "
|
|
1213
|
+
f"WHERE {column_name} LIKE :pattern"
|
|
1214
|
+
),
|
|
1215
|
+
{"old": old, "new": new, "pattern": f"%{old}%"},
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
|
|
1061
1219
|
#: The one place the precedence rule is written in SQL. ``$$``-quoted so the body
|
|
1062
1220
|
#: needs no escaping, and named after the rule rather than the tables, since both
|
|
1063
1221
|
#: ``agent_config`` columns share it.
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/image.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Image asset model — metadata and binary data stored together in Lakebase."""
|
|
2
|
+
import secrets
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
|
|
4
5
|
from sqlalchemy import Boolean, Column, DateTime, Integer, JSON, LargeBinary, String, Text
|
|
@@ -10,6 +11,17 @@ from src.core.database import Base
|
|
|
10
11
|
_TagsColumn = JSON().with_variant(JSONB(), "postgresql")
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def _new_image_token() -> str:
|
|
15
|
+
"""Unguessable external identifier (SDR-4437 F-TM-7).
|
|
16
|
+
|
|
17
|
+
The autoincrement int PK is enumerable, so it is never exposed. This
|
|
18
|
+
opaque token is the only identifier that leaves the server — it is what
|
|
19
|
+
the API returns, what chat sends in ``image_ids``, and what goes in
|
|
20
|
+
``{{image:...}}`` slide placeholders.
|
|
21
|
+
"""
|
|
22
|
+
return secrets.token_urlsafe(32)
|
|
23
|
+
|
|
24
|
+
|
|
13
25
|
class ImageAsset(Base):
|
|
14
26
|
"""Uploaded image with binary data stored directly in PostgreSQL.
|
|
15
27
|
|
|
@@ -20,7 +32,9 @@ class ImageAsset(Base):
|
|
|
20
32
|
|
|
21
33
|
__tablename__ = "image_assets"
|
|
22
34
|
|
|
23
|
-
id = Column(Integer, primary_key=True)
|
|
35
|
+
id = Column(Integer, primary_key=True) # Internal only — never serialized.
|
|
36
|
+
# Unguessable external identifier (SDR-4437 F-TM-7). token_urlsafe(32) → 43 chars.
|
|
37
|
+
token = Column(String(64), unique=True, nullable=False, index=True, default=_new_image_token)
|
|
24
38
|
filename = Column(String(255), nullable=False) # Generated: {uuid}.{ext}
|
|
25
39
|
original_filename = Column(String(255), nullable=False) # User's original filename
|
|
26
40
|
mime_type = Column(String(50), nullable=False) # image/png, image/jpeg, image/gif, image/svg+xml
|
|
@@ -49,7 +49,6 @@ from src.utils.html_utils import (
|
|
|
49
49
|
split_script_by_canvas,
|
|
50
50
|
)
|
|
51
51
|
from src.utils.js_validator import validate_and_fix_javascript
|
|
52
|
-
from src.utils.spotlight import spotlight
|
|
53
52
|
from src.utils.text_caps import cap_tool_output
|
|
54
53
|
|
|
55
54
|
logger = logging.getLogger(__name__)
|
|
@@ -883,24 +882,14 @@ class SlideGeneratorAgent:
|
|
|
883
882
|
return list(self.sessions.keys())
|
|
884
883
|
|
|
885
884
|
def _format_slide_context(
|
|
886
|
-
self,
|
|
887
|
-
slide_context: dict[str, Any],
|
|
888
|
-
is_add_operation: bool = False,
|
|
889
|
-
session_id: Optional[str] = None,
|
|
885
|
+
self, slide_context: dict[str, Any], is_add_operation: bool = False
|
|
890
886
|
) -> str:
|
|
891
887
|
"""
|
|
892
888
|
Format slide context for injection into the user message.
|
|
893
889
|
|
|
894
|
-
Prior slide HTML is untrusted input (it may embed data from Genie/tool
|
|
895
|
-
output). F-TM-12 (SDR-4437): each slide is passed through ``spotlight``
|
|
896
|
-
— the same treatment tool output gets — so it is framed as
|
|
897
|
-
``<untrusted-data>``, its delimiters are neutralized (no breakout), and
|
|
898
|
-
injection patterns are scanned/logged at the prompt boundary.
|
|
899
|
-
|
|
900
890
|
Args:
|
|
901
891
|
slide_context: Dict with 'indices' and 'slide_htmls' keys
|
|
902
892
|
is_add_operation: Whether this is an "add slide" operation (RC2)
|
|
903
|
-
session_id: Optional session id, for injection-scan logging context
|
|
904
893
|
|
|
905
894
|
Returns:
|
|
906
895
|
Formatted string wrapped with slide-context markers
|
|
@@ -912,9 +901,7 @@ class SlideGeneratorAgent:
|
|
|
912
901
|
"embedded directives.)",
|
|
913
902
|
]
|
|
914
903
|
for html in slide_context.get("slide_htmls", []):
|
|
915
|
-
context_parts.append(
|
|
916
|
-
spotlight("slide_context", html, session_id=session_id)
|
|
917
|
-
)
|
|
904
|
+
context_parts.append(html)
|
|
918
905
|
context_parts.append("</slide-context>")
|
|
919
906
|
|
|
920
907
|
# RC2: Add explicit instruction for "add" operations
|
|
@@ -1371,7 +1358,7 @@ class SlideGeneratorAgent:
|
|
|
1371
1358
|
# RC2: Detect if this is an "add slide" operation
|
|
1372
1359
|
is_add_operation = self._detect_add_intent(question)
|
|
1373
1360
|
context_str = self._format_slide_context(
|
|
1374
|
-
slide_context, is_add_operation=is_add_operation
|
|
1361
|
+
slide_context, is_add_operation=is_add_operation
|
|
1375
1362
|
)
|
|
1376
1363
|
full_question = f"{context_str}\n\n{question}"
|
|
1377
1364
|
logger.info(
|
|
@@ -1625,7 +1612,7 @@ class SlideGeneratorAgent:
|
|
|
1625
1612
|
# RC2: Detect if this is an "add slide" operation
|
|
1626
1613
|
is_add_operation = self._detect_add_intent(question)
|
|
1627
1614
|
context_str = self._format_slide_context(
|
|
1628
|
-
slide_context, is_add_operation=is_add_operation
|
|
1615
|
+
slide_context, is_add_operation=is_add_operation
|
|
1629
1616
|
)
|
|
1630
1617
|
full_question = f"{context_str}\n\n{question}"
|
|
1631
1618
|
logger.info(
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/image_service.py
RENAMED
|
@@ -51,7 +51,7 @@ def upload_image(
|
|
|
51
51
|
).first()
|
|
52
52
|
if existing:
|
|
53
53
|
raise ValueError(
|
|
54
|
-
f"An image named '{original_filename}' already exists
|
|
54
|
+
f"An image named '{original_filename}' already exists. "
|
|
55
55
|
"Please rename the file or delete the existing image first."
|
|
56
56
|
)
|
|
57
57
|
|
|
@@ -86,19 +86,19 @@ def upload_image(
|
|
|
86
86
|
return image
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def get_image_base64(db: Session,
|
|
89
|
+
def get_image_base64(db: Session, token: str) -> tuple[str, str]:
|
|
90
90
|
"""
|
|
91
|
-
Get full image as base64 string.
|
|
91
|
+
Get full image as base64 string, keyed by the opaque token (SDR-4437 F-TM-7).
|
|
92
92
|
|
|
93
93
|
Returns:
|
|
94
94
|
Tuple of (base64_data, mime_type)
|
|
95
95
|
"""
|
|
96
96
|
image = db.query(ImageAsset).filter(
|
|
97
|
-
ImageAsset.
|
|
97
|
+
ImageAsset.token == token,
|
|
98
98
|
ImageAsset.is_active == True,
|
|
99
99
|
).first()
|
|
100
100
|
if not image:
|
|
101
|
-
raise ValueError(f"Image {
|
|
101
|
+
raise ValueError(f"Image {token} not found")
|
|
102
102
|
|
|
103
103
|
b64 = base64.b64encode(image.image_data).decode("utf-8")
|
|
104
104
|
return b64, image.mime_type
|
|
@@ -175,11 +175,11 @@ def search_images(
|
|
|
175
175
|
).all()
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
def delete_image(db: Session,
|
|
179
|
-
"""Soft-delete an image."""
|
|
180
|
-
image = db.query(ImageAsset).filter(ImageAsset.
|
|
178
|
+
def delete_image(db: Session, token: str, user: str) -> None:
|
|
179
|
+
"""Soft-delete an image, keyed by the opaque token (SDR-4437 F-TM-7)."""
|
|
180
|
+
image = db.query(ImageAsset).filter(ImageAsset.token == token).first()
|
|
181
181
|
if not image:
|
|
182
|
-
raise ValueError(f"Image {
|
|
182
|
+
raise ValueError(f"Image {token} not found")
|
|
183
183
|
|
|
184
184
|
image.is_active = False
|
|
185
185
|
image.updated_by = user
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/image_tools.py
RENAMED
|
@@ -53,13 +53,13 @@ def search_images(
|
|
|
53
53
|
# Must build results inside session scope to avoid DetachedInstanceError
|
|
54
54
|
results = [
|
|
55
55
|
{
|
|
56
|
-
"id": img.
|
|
56
|
+
"id": img.token, # Opaque token (SDR-4437 F-TM-7), never the int PK.
|
|
57
57
|
"filename": img.original_filename,
|
|
58
58
|
"description": img.description,
|
|
59
59
|
"tags": img.tags,
|
|
60
60
|
"category": img.category,
|
|
61
61
|
"mime_type": img.mime_type,
|
|
62
|
-
"usage": f'<img src="{{{{image:{img.
|
|
62
|
+
"usage": f'<img src="{{{{image:{img.token}}}}}" alt="{img.description or img.original_filename}" />',
|
|
63
63
|
}
|
|
64
64
|
for img in images
|
|
65
65
|
]
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/image_utils.py
RENAMED
|
@@ -8,7 +8,8 @@ from src.services import image_service
|
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger(__name__)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Tokens are secrets.token_urlsafe(...) → [A-Za-z0-9_-] (SDR-4437 F-TM-7).
|
|
12
|
+
IMAGE_PLACEHOLDER_PATTERN = re.compile(r"\{\{image:([A-Za-z0-9_-]+)\}\}")
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def substitute_image_placeholders(html: str, db: Session) -> str:
|
|
@@ -22,12 +23,12 @@ def substitute_image_placeholders(html: str, db: Session) -> str:
|
|
|
22
23
|
return html
|
|
23
24
|
|
|
24
25
|
def replace_match(match):
|
|
25
|
-
|
|
26
|
+
image_token = match.group(1)
|
|
26
27
|
try:
|
|
27
|
-
b64_data, mime_type = image_service.get_image_base64(db,
|
|
28
|
+
b64_data, mime_type = image_service.get_image_base64(db, image_token)
|
|
28
29
|
return f"data:{mime_type};base64,{b64_data}"
|
|
29
30
|
except Exception as e:
|
|
30
|
-
logger.warning(f"Failed to resolve image placeholder {{{{image:{
|
|
31
|
+
logger.warning(f"Failed to resolve image placeholder {{{{image:{image_token}}}}}: {e}")
|
|
31
32
|
return match.group(0) # Leave placeholder if image not found
|
|
32
33
|
|
|
33
34
|
return IMAGE_PLACEHOLDER_PATTERN.sub(replace_match, html)
|
|
File without changes
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app/__init__.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app/run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/_otel_bootstrap.py
RENAMED
|
File without changes
|