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.
Files changed (167) hide show
  1. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/pyproject.toml +1 -1
  4. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/images.py +45 -27
  5. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/requests.py +2 -2
  6. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/chat_service.py +12 -9
  7. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/database.py +158 -0
  8. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/image.py +15 -1
  9. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/agent.py +4 -17
  10. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/image_service.py +9 -9
  11. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/image_tools.py +2 -2
  12. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/image_utils.py +5 -4
  13. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/README.md +0 -0
  14. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app/__init__.py +0 -0
  15. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app/run.py +0 -0
  16. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
  17. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  18. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/requires.txt +0 -0
  19. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  20. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/setup.cfg +0 -0
  21. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/setup.py +0 -0
  22. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/__init__.py +0 -0
  23. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/__init__.py +0 -0
  24. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/_otel_bootstrap.py +0 -0
  25. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/fixtures/tour_demo_deck.json +0 -0
  26. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/main.py +0 -0
  27. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/mcp_auth.py +0 -0
  28. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/mcp_server.py +0 -0
  29. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/__init__.py +0 -0
  30. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/csrf.py +0 -0
  31. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/request_logging.py +0 -0
  32. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/middleware/security_headers.py +0 -0
  33. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/__init__.py +0 -0
  34. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/_authz.py +0 -0
  35. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/admin.py +0 -0
  36. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/admin_usage.py +0 -0
  37. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/agent_config.py +0 -0
  38. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/chat.py +0 -0
  39. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/deck_contributors.py +0 -0
  40. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/export.py +0 -0
  41. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/feedback.py +0 -0
  42. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/google_slides.py +0 -0
  43. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/local_version.py +0 -0
  44. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/profiles.py +0 -0
  45. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/sessions.py +0 -0
  46. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/__init__.py +0 -0
  47. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/contributors.py +0 -0
  48. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/deck_prompts.py +0 -0
  49. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/design_systems.py +0 -0
  50. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/identities.py +0 -0
  51. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/settings/slide_styles.py +0 -0
  52. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/setup.py +0 -0
  53. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/slides.py +0 -0
  54. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/tools.py +0 -0
  55. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/tour.py +0 -0
  56. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/verification.py +0 -0
  57. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/routes/version.py +0 -0
  58. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/__init__.py +0 -0
  59. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/agent_config.py +0 -0
  60. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/feedback.py +0 -0
  61. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/responses.py +0 -0
  62. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/settings/__init__.py +0 -0
  63. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/settings/requests.py +0 -0
  64. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/settings/responses.py +0 -0
  65. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/schemas/streaming.py +0 -0
  66. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/__init__.py +0 -0
  67. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/export_job_queue.py +0 -0
  68. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/feedback_service.py +0 -0
  69. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/job_queue.py +0 -0
  70. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/session_manager.py +0 -0
  71. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/session_naming.py +0 -0
  72. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/usage_events.py +0 -0
  73. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/services/usage_service.py +0 -0
  74. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/utils/__init__.py +0 -0
  75. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/api/utils/validation.py +0 -0
  76. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/__init__.py +0 -0
  77. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/config_loader.py +0 -0
  78. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/context_utils.py +0 -0
  79. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/databricks_client.py +0 -0
  80. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/defaults.py +0 -0
  81. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/encryption.py +0 -0
  82. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/init_default_profile.py +0 -0
  83. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/lakebase.py +0 -0
  84. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/migrate_profiles_to_agent_config.py +0 -0
  85. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/mlflow_agent_spans.py +0 -0
  86. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/mlflow_tracing.py +0 -0
  87. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/otel_logging.py +0 -0
  88. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/permission_context.py +0 -0
  89. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/prompt_modules.py +0 -0
  90. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/settings_db.py +0 -0
  91. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/core/user_context.py +0 -0
  92. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/__init__.py +0 -0
  93. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/__init__.py +0 -0
  94. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/deck_contributor.py +0 -0
  95. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/design_system.py +0 -0
  96. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/encryption_key.py +0 -0
  97. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/feedback.py +0 -0
  98. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/genie_space.py +0 -0
  99. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/google_global_credentials.py +0 -0
  100. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/google_oauth_token.py +0 -0
  101. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/identity.py +0 -0
  102. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/profile.py +0 -0
  103. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/profile_contributor.py +0 -0
  104. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/prompts.py +0 -0
  105. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/request_log.py +0 -0
  106. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/session.py +0 -0
  107. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/slide_deck_prompt.py +0 -0
  108. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/slide_style_library.py +0 -0
  109. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/usage_event.py +0 -0
  110. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/models/user_preference.py +0 -0
  111. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/database/types.py +0 -0
  112. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/domain/__init__.py +0 -0
  113. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/domain/slide.py +0 -0
  114. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/domain/slide_deck.py +0 -0
  115. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/__init__.py +0 -0
  116. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/agent_factory.py +0 -0
  117. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/config_service.py +0 -0
  118. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/config_validator.py +0 -0
  119. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/__init__.py +0 -0
  120. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/ast_guard.py +0 -0
  121. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/codeprep.py +0 -0
  122. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/gslides_runner.py +0 -0
  123. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/jail.py +0 -0
  124. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/pptx_runner.py +0 -0
  125. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/converter_jail/protocol.py +0 -0
  126. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/design_system_compiler.py +0 -0
  127. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/design_system_service.py +0 -0
  128. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/design_system_templates.py +0 -0
  129. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/drive_uploader.py +0 -0
  130. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/evaluation/__init__.py +0 -0
  131. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/evaluation/llm_judge.py +0 -0
  132. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/genie_service.py +0 -0
  133. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/google_slides_auth.py +0 -0
  134. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/google_slides_prompts_defaults.py +0 -0
  135. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/html_to_google_slides.py +0 -0
  136. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/html_to_pptx.py +0 -0
  137. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_provider.py +0 -0
  138. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/__init__.py +0 -0
  139. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/account_provider.py +0 -0
  140. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/local_provider.py +0 -0
  141. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/identity_providers/workspace_provider.py +0 -0
  142. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/permission_service.py +0 -0
  143. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/pptx_from_html_huashu.py +0 -0
  144. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/pptx_from_records.py +0 -0
  145. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/pptx_prompts_defaults.py +0 -0
  146. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/profile_service.py +0 -0
  147. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/streaming_callback.py +0 -0
  148. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/__init__.py +0 -0
  149. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/agent_bricks_tool.py +0 -0
  150. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/ds_asset_tool.py +0 -0
  151. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/genie_tool.py +0 -0
  152. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/mcp_tool.py +0 -0
  153. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/model_endpoint_tool.py +0 -0
  154. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/tools/vector_tool.py +0 -0
  155. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/services/validator.py +0 -0
  156. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/__init__.py +0 -0
  157. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/css_utils.py +0 -0
  158. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/ds_asset_utils.py +0 -0
  159. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/error_handling.py +0 -0
  160. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/html_safety.py +0 -0
  161. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/html_utils.py +0 -0
  162. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/js_validator.py +0 -0
  163. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/logging_config.py +0 -0
  164. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/pi_filter.py +0 -0
  165. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/slide_hash.py +0 -0
  166. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/src/utils/spotlight.py +0 -0
  167. {databricks_tellr_app-0.4.3.dev5 → databricks_tellr_app-0.4.3.dev7}/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.3.dev5
3
+ Version: 0.4.3.dev7
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.4.3.dev5
3
+ Version: 0.4.3.dev7
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databricks-tellr-app"
7
- version = "0.4.3.dev5"
7
+ version = "0.4.3.dev7"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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: int
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.id,
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
- @router.get("/{image_id}", response_model=ImageResponse)
171
- def get_image(image_id: int, db: Session = Depends(get_db)):
172
- """Get image metadata by ID."""
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.id == image_id,
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 {image_id}: {e}", exc_info=True)
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("/{image_id}/data", response_model=ImageDataResponse)
192
- def get_image_data(image_id: int, db: Session = Depends(get_db)):
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
- b64_data, mime_type = image_service.get_image_base64(db, image_id)
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=image_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 ValueError as e:
203
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
220
+ except HTTPException:
221
+ raise
204
222
  except Exception as e:
205
- logger.error(f"Error getting image data {image_id}: {e}", exc_info=True)
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("/{image_id}", response_model=ImageResponse)
230
+ @router.put("/{token}", response_model=ImageResponse)
213
231
  def update_image(
214
- image_id: int,
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.id == image_id,
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 {image_id}: {e}", exc_info=True)
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("/{image_id}", status_code=status.HTTP_204_NO_CONTENT)
257
- def delete_image(image_id: int, db: Session = Depends(get_db)):
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.id == image_id,
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, image_id, user)
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 {image_id}: {e}", exc_info=True)
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",
@@ -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[int]] = Field(
77
+ image_ids: Optional[list[str]] = Field(
78
78
  default=None,
79
- description="IDs of images attached to this message (from upload or paste)",
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,
@@ -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[int]] = None,
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[int]] = None,
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[int]) -> str:
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 img_id in image_ids:
2006
+ for img_token in image_ids:
2004
2007
  try:
2005
2008
  img = db.query(image_service.ImageAsset).filter(
2006
- image_service.ImageAsset.id == img_id,
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.id}: "{img.original_filename}" '
2014
+ f'- Image ID {img.token}: "{img.original_filename}" '
2012
2015
  f'({img.description or "no description"}). '
2013
- f'Use: <img src="{{{{image:{img.id}}}}}" alt="{img.description or img.original_filename}" />'
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 {img_id} for context injection: {e}")
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.
@@ -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, session_id=session_id
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, session_id=session_id
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(
@@ -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 (id={existing.id}). "
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, image_id: int) -> tuple[str, str]:
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.id == image_id,
97
+ ImageAsset.token == token,
98
98
  ImageAsset.is_active == True,
99
99
  ).first()
100
100
  if not image:
101
- raise ValueError(f"Image {image_id} not found")
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, image_id: int, user: str) -> None:
179
- """Soft-delete an image."""
180
- image = db.query(ImageAsset).filter(ImageAsset.id == image_id).first()
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 {image_id} not found")
182
+ raise ValueError(f"Image {token} not found")
183
183
 
184
184
  image.is_active = False
185
185
  image.updated_by = user
@@ -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.id,
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.id}}}}}" alt="{img.description or img.original_filename}" />',
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
  ]
@@ -8,7 +8,8 @@ from src.services import image_service
8
8
 
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
- IMAGE_PLACEHOLDER_PATTERN = re.compile(r"\{\{image:(\d+)\}\}")
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
- image_id = int(match.group(1))
26
+ image_token = match.group(1)
26
27
  try:
27
- b64_data, mime_type = image_service.get_image_base64(db, image_id)
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:{image_id}}}}}: {e}")
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)