databricks-tellr-app 0.4.2.dev13__tar.gz → 0.4.2.dev15__tar.gz

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