databricks-tellr-app 0.4.3.dev21__tar.gz → 0.4.3.dev23__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 (198) hide show
  1. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/pyproject.toml +1 -1
  4. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/chat_service.py +228 -46
  5. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/graph/builder.py +19 -2
  6. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/graph/event_emitter.py +40 -0
  7. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/graph/nodes.py +134 -21
  8. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/README.md +0 -0
  9. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/databricks_tellr_app/__init__.py +0 -0
  10. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/databricks_tellr_app/run.py +0 -0
  11. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
  12. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  13. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/databricks_tellr_app.egg-info/requires.txt +0 -0
  14. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  15. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/setup.cfg +0 -0
  16. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/setup.py +0 -0
  17. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/__init__.py +0 -0
  18. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/__init__.py +0 -0
  19. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/_otel_bootstrap.py +0 -0
  20. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/fixtures/tour_demo_deck.json +0 -0
  21. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/main.py +0 -0
  22. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/mcp_auth.py +0 -0
  23. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/mcp_server.py +0 -0
  24. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/middleware/__init__.py +0 -0
  25. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/middleware/csrf.py +0 -0
  26. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/middleware/request_logging.py +0 -0
  27. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/middleware/security_headers.py +0 -0
  28. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/__init__.py +0 -0
  29. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/_authz.py +0 -0
  30. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/admin.py +0 -0
  31. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/admin_usage.py +0 -0
  32. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/agent_config.py +0 -0
  33. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/chat.py +0 -0
  34. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/deck_contributors.py +0 -0
  35. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/export.py +0 -0
  36. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/feedback.py +0 -0
  37. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/google_slides.py +0 -0
  38. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/images.py +0 -0
  39. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/local_version.py +0 -0
  40. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/profiles.py +0 -0
  41. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/sessions.py +0 -0
  42. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/settings/__init__.py +0 -0
  43. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/settings/contributors.py +0 -0
  44. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/settings/deck_prompts.py +0 -0
  45. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/settings/design_systems.py +0 -0
  46. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/settings/identities.py +0 -0
  47. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/settings/slide_styles.py +0 -0
  48. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/setup.py +0 -0
  49. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/slides.py +0 -0
  50. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/tools.py +0 -0
  51. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/tour.py +0 -0
  52. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/verification.py +0 -0
  53. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/routes/version.py +0 -0
  54. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/__init__.py +0 -0
  55. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/agent_config.py +0 -0
  56. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/feedback.py +0 -0
  57. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/requests.py +0 -0
  58. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/responses.py +0 -0
  59. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/settings/__init__.py +0 -0
  60. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/settings/requests.py +0 -0
  61. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/settings/responses.py +0 -0
  62. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/schemas/streaming.py +0 -0
  63. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/__init__.py +0 -0
  64. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/deck_level_writer.py +0 -0
  65. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/export_job_queue.py +0 -0
  66. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/feedback_service.py +0 -0
  67. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/job_queue.py +0 -0
  68. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/session_manager.py +0 -0
  69. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/session_naming.py +0 -0
  70. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/slide_repository.py +0 -0
  71. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/usage_events.py +0 -0
  72. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/services/usage_service.py +0 -0
  73. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/utils/__init__.py +0 -0
  74. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/api/utils/validation.py +0 -0
  75. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/__init__.py +0 -0
  76. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/backfill_session_slides_startup.py +0 -0
  77. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/checkpointer.py +0 -0
  78. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/config_loader.py +0 -0
  79. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/context_utils.py +0 -0
  80. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/database.py +0 -0
  81. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/databricks_client.py +0 -0
  82. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/defaults.py +0 -0
  83. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/encryption.py +0 -0
  84. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/init_default_profile.py +0 -0
  85. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/lakebase.py +0 -0
  86. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/migrate_profiles_to_agent_config.py +0 -0
  87. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/mlflow_agent_spans.py +0 -0
  88. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/mlflow_tracing.py +0 -0
  89. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/otel_logging.py +0 -0
  90. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/permission_context.py +0 -0
  91. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/prompt_modules.py +0 -0
  92. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/settings_db.py +0 -0
  93. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/__init__.py +0 -0
  94. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/architect.py +0 -0
  95. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/build_reviewer.py +0 -0
  96. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/builder.py +0 -0
  97. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/data_analyst.py +0 -0
  98. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/deck_reviewer.py +0 -0
  99. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/fix_reviewer.py +0 -0
  100. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/skills/fixer.py +0 -0
  101. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/strip_retired_prompt_keys.py +0 -0
  102. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/core/user_context.py +0 -0
  103. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/__init__.py +0 -0
  104. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/__init__.py +0 -0
  105. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/deck_contributor.py +0 -0
  106. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/deck_review.py +0 -0
  107. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/design_system.py +0 -0
  108. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/encryption_key.py +0 -0
  109. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/feedback.py +0 -0
  110. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/genie_space.py +0 -0
  111. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/google_global_credentials.py +0 -0
  112. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/google_oauth_token.py +0 -0
  113. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/graph_checkpoint.py +0 -0
  114. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/identity.py +0 -0
  115. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/image.py +0 -0
  116. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/profile.py +0 -0
  117. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/profile_contributor.py +0 -0
  118. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/prompts.py +0 -0
  119. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/request_log.py +0 -0
  120. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/session.py +0 -0
  121. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/slide_deck_prompt.py +0 -0
  122. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/slide_style_library.py +0 -0
  123. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/usage_event.py +0 -0
  124. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/models/user_preference.py +0 -0
  125. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/database/types.py +0 -0
  126. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/domain/__init__.py +0 -0
  127. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/domain/deck_spec.py +0 -0
  128. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/domain/finding.py +0 -0
  129. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/domain/skill_io.py +0 -0
  130. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/domain/slide.py +0 -0
  131. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/domain/slide_deck.py +0 -0
  132. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/__init__.py +0 -0
  133. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/agent.py +0 -0
  134. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/agent_factory.py +0 -0
  135. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/agent_resolution.py +0 -0
  136. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/config_service.py +0 -0
  137. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/config_validator.py +0 -0
  138. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/converter_jail/__init__.py +0 -0
  139. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/converter_jail/ast_guard.py +0 -0
  140. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/converter_jail/codeprep.py +0 -0
  141. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/converter_jail/gslides_runner.py +0 -0
  142. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/converter_jail/jail.py +0 -0
  143. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/converter_jail/pptx_runner.py +0 -0
  144. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/converter_jail/protocol.py +0 -0
  145. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/deck_css_aggregator.py +0 -0
  146. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/deck_review_store.py +0 -0
  147. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/design_system_compiler.py +0 -0
  148. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/design_system_service.py +0 -0
  149. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/design_system_templates.py +0 -0
  150. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/drive_uploader.py +0 -0
  151. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/evaluation/__init__.py +0 -0
  152. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/evaluation/llm_judge.py +0 -0
  153. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/foreman_service.py +0 -0
  154. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/genie_service.py +0 -0
  155. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/google_slides_auth.py +0 -0
  156. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/google_slides_prompts_defaults.py +0 -0
  157. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/graph/__init__.py +0 -0
  158. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/graph/routers.py +0 -0
  159. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/graph/state.py +0 -0
  160. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/html_to_google_slides.py +0 -0
  161. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/html_to_pptx.py +0 -0
  162. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/identity_provider.py +0 -0
  163. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/identity_providers/__init__.py +0 -0
  164. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/identity_providers/account_provider.py +0 -0
  165. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/identity_providers/local_provider.py +0 -0
  166. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/identity_providers/workspace_provider.py +0 -0
  167. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/image_service.py +0 -0
  168. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/image_tools.py +0 -0
  169. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/permission_service.py +0 -0
  170. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/pptx_from_html_huashu.py +0 -0
  171. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/pptx_from_records.py +0 -0
  172. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/pptx_prompts_defaults.py +0 -0
  173. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/profile_service.py +0 -0
  174. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/spec_sync.py +0 -0
  175. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/streaming_callback.py +0 -0
  176. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/template_sections.py +0 -0
  177. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/tools/__init__.py +0 -0
  178. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/tools/agent_bricks_tool.py +0 -0
  179. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/tools/ds_asset_tool.py +0 -0
  180. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/tools/genie_tool.py +0 -0
  181. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/tools/mcp_tool.py +0 -0
  182. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/tools/model_endpoint_tool.py +0 -0
  183. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/tools/vector_tool.py +0 -0
  184. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/services/validator.py +0 -0
  185. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/__init__.py +0 -0
  186. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/css_utils.py +0 -0
  187. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/ds_asset_utils.py +0 -0
  188. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/error_handling.py +0 -0
  189. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/graph_safety.py +0 -0
  190. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/html_safety.py +0 -0
  191. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/html_utils.py +0 -0
  192. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/image_utils.py +0 -0
  193. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/js_validator.py +0 -0
  194. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/logging_config.py +0 -0
  195. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/pi_filter.py +0 -0
  196. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/slide_hash.py +0 -0
  197. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/src/utils/spotlight.py +0 -0
  198. {databricks_tellr_app-0.4.3.dev21 → databricks_tellr_app-0.4.3.dev23}/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.dev21
3
+ Version: 0.4.3.dev23
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.dev21
3
+ Version: 0.4.3.dev23
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.dev21"
7
+ version = "0.4.3.dev23"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -14,7 +14,7 @@ import re
14
14
  import threading
15
15
  import uuid
16
16
  from datetime import datetime
17
- from typing import Any, Dict, Generator, List, Optional
17
+ from typing import Any, Callable, Dict, Generator, List, Optional
18
18
 
19
19
  from langchain_community.chat_message_histories import ChatMessageHistory
20
20
  from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
@@ -51,6 +51,18 @@ logger = logging.getLogger(__name__)
51
51
  #: own CSS styles it, and any content here would be content nobody asked for.
52
52
  BLANK_SLIDE_HTML = '<div class="slide"></div>'
53
53
 
54
+ # The deck-spec entry a newly inserted slide gets: contiguous with its neighbours
55
+ # (`DeckSpec.slide_at` depends on that) and deliberately undescribed. The route
56
+ # that inserts fires the spec-dirty marker and the arc-review sweeper writes the
57
+ # prose; inventing a purpose here would put words in the architect's mouth.
58
+ _BLANK_DECK_SPEC_ENTRY: Dict[str, Any] = {
59
+ "purpose": "",
60
+ "content_brief": "",
61
+ "assumes": "",
62
+ "hands_off": "",
63
+ "data_references": [],
64
+ }
65
+
54
66
 
55
67
  def resolve_active_design_system_id(session_id: Optional[str]) -> Optional[int]:
56
68
  """The session's pinned/active design-system id, or None.
@@ -1117,6 +1129,7 @@ class ChatService:
1117
1129
  session_id,
1118
1130
  message,
1119
1131
  is_first_message=is_first_message,
1132
+ request_id=request_id,
1120
1133
  )
1121
1134
  return
1122
1135
 
@@ -1788,6 +1801,7 @@ class ChatService:
1788
1801
  message: str,
1789
1802
  *,
1790
1803
  is_first_message: bool = False,
1804
+ request_id: Optional[str] = None,
1791
1805
  ) -> Generator[StreamEvent, None, None]:
1792
1806
  """Run one turn on the LangGraph engine, yielding events as they arrive.
1793
1807
 
@@ -1835,6 +1849,14 @@ class ChatService:
1835
1849
  ``architect_node`` is the sole resolver of the design contract
1836
1850
  and the template bytes.
1837
1851
  is_first_message: Whether to generate a session title this turn.
1852
+ request_id: The async transport's ``ChatRequest.request_id``, handed
1853
+ to ``invoke_graph`` so a node persisting a chat message tags the
1854
+ row with it. ``GET /chat/poll`` reads assistant text through
1855
+ ``get_messages_for_request``, which filters on that column, so
1856
+ without it the architect's reply is written but no polling client
1857
+ can see it — and polling is the transport the deployed app uses.
1858
+ ``None`` on the SSE path, which persists no request row: that
1859
+ client is reading the yielded events instead.
1838
1860
 
1839
1861
  Yields:
1840
1862
  Every ``StreamEvent`` the graph emits, then ``COMPLETE`` carrying
@@ -1858,6 +1880,7 @@ class ChatService:
1858
1880
  session_id,
1859
1881
  {"architect_message": message},
1860
1882
  emitter=event_queue,
1883
+ request_id=request_id,
1861
1884
  )
1862
1885
  except Exception as e:
1863
1886
  logger.error(
@@ -3343,6 +3366,13 @@ class ChatService:
3343
3366
  )
3344
3367
  self._record_deck_version(session_id, save_result)
3345
3368
 
3369
+ # The deck spec's slide list has to move with the deck's. AFTER
3370
+ # save_slide_deck, never before: the spec write bumps deck.version, which
3371
+ # would make the caller's expected_version stale and 409 a legitimate save.
3372
+ spec_result = self._reorder_deck_spec_slides(session_id, new_order)
3373
+ if spec_result:
3374
+ self._record_deck_version(session_id, spec_result)
3375
+
3346
3376
  # Create save point
3347
3377
  try:
3348
3378
  self.create_save_point(
@@ -3518,6 +3548,13 @@ class ChatService:
3518
3548
  )
3519
3549
  self._record_deck_version(session_id, save_result)
3520
3550
 
3551
+ # The deck spec's slide list has to move with the deck's. AFTER
3552
+ # save_slide_deck, never before: the spec write bumps deck.version, which
3553
+ # would make the caller's expected_version stale and 409 a legitimate save.
3554
+ spec_result = self._duplicate_deck_spec_slide(session_id, index)
3555
+ if spec_result:
3556
+ self._record_deck_version(session_id, spec_result)
3557
+
3521
3558
  # Create save point
3522
3559
  try:
3523
3560
  self.create_save_point(
@@ -3660,33 +3697,66 @@ class ChatService:
3660
3697
  deck_dict, _ = self._substitute_images_for_response(deck_dict, session_id=session_id)
3661
3698
  return deck_dict
3662
3699
 
3663
- def _insert_deck_spec_slide(
3700
+ def _rewrite_deck_spec_slides(
3664
3701
  self,
3665
3702
  session_id: str,
3666
- position: int,
3703
+ reorder: Callable[[List[Dict[str, Any]]], Optional[List[Dict[str, Any]]]],
3704
+ *,
3705
+ label: str,
3667
3706
  ) -> Optional[Dict[str, Any]]:
3668
- """Give the deck spec an entry at *position* and shift the entries above it.
3707
+ """Re-order the deck spec's slide entries and re-stamp their positions.
3669
3708
 
3670
3709
  A deck spec describes the deck slide by slide, keyed on ``position``
3671
3710
  (``SlideSpec.position`` is the canonical identity, looked up by
3672
- ``DeckSpec.slide_at`` and never by list index). Inserting a slide without
3673
- renumbering would leave every entry above the insertion point describing
3674
- its neighbour.
3675
-
3676
- The new entry is a PLACEHOLDER — empty purpose and brief. The route that
3677
- reaches this fires the spec-dirty marker, and the arc-review sweeper is
3678
- what actually describes the new slide; inventing a purpose here would put
3679
- words in the architect's mouth, and leaving the entry out entirely would
3680
- break the contiguity ``slide_at`` depends on.
3711
+ ``DeckSpec.slide_at`` and never by list index). Any mutation that moves,
3712
+ adds or removes a slide must move its spec entry with it, or every entry
3713
+ past the change describes its neighbour.
3714
+
3715
+ Before this existed, ``deck_spec_json`` was renumbered on INSERT and on
3716
+ nothing else (final review C1), so after a delete, a duplicate or a reorder
3717
+ the deck-level spec and the per-row ``deck_spec_slide`` fragments — two
3718
+ representations of the same thing — disagreed, and three consumers read the
3719
+ deck-level one: the builder brief, §4.6's re-review and ``architect_node``'s
3720
+ edit-turn fallback. Measured: an edit turn dispatched a builder for a
3721
+ position with no slide and the deck gained one nobody asked for.
3722
+
3723
+ WHY ONE PRIMITIVE AND NOT FOUR WRITERS
3724
+ --------------------------------------
3725
+ Four hand-copied row-writers diverging is the defect class the previous
3726
+ workstream's final review found, and C1 is itself an instance of it — insert
3727
+ renumbered, its three siblings did not. So the four routes supply only
3728
+ *which entries end up in which order*; the position stamping, the raw-dict
3729
+ handling, the author stamp and the failure policy live here once.
3730
+
3731
+ *reorder* receives the positioned entries **in position order** and returns
3732
+ the new list in the new order — it does not touch ``position`` at all, which
3733
+ this method re-stamps contiguously from 0. Returning ``None`` leaves the
3734
+ spec untouched.
3735
+
3736
+ Operates on the RAW spec dict rather than parsing a ``DeckSpec``: a spec that
3737
+ no longer validates (an older shape, a hand-edited column) must not be
3738
+ destroyed by a renumber, and nothing here reads a field other than
3739
+ ``position``. Entries that are not dicts, or whose ``position`` is not an
3740
+ int, cannot be identified with a slide, so they are carried through
3741
+ **unchanged and last** rather than silently permuted.
3742
+
3743
+ A spec write that fails must never fail the mutation: the slide change is
3744
+ already committed by the time this runs, and the route's spec-dirty marker
3745
+ still fires, so the sweeper re-describes the deck either way.
3681
3746
 
3682
- Operates on the RAW spec dict rather than parsing a ``DeckSpec``: a spec
3683
- that no longer validates (an older shape, a hand-edited column) must not be
3684
- destroyed by a renumber, and this only touches ``slides[*].position``.
3747
+ Args:
3748
+ session_id: The mutating session. Resolves to the deck owner in the
3749
+ layers below, exactly as every other mutation does.
3750
+ reorder: Takes the positioned entries in position order, returns them in
3751
+ their new order, or ``None`` to leave the spec alone. Raises
3752
+ ``IndexError`` when the spec cannot answer the mutation.
3753
+ label: Route name, for the log lines only.
3685
3754
 
3686
3755
  Returns:
3687
3756
  The writer's result dict (carrying the new deck ``version``), or None
3688
- when there was no spec to shift — a deck built before the spec existed
3689
- is not an error.
3757
+ when there was no spec to renumber, the spec was unreadable, or
3758
+ *reorder* declined — none of which is an error. A deck built before the
3759
+ spec column existed has no spec.
3690
3760
  """
3691
3761
  # Imported here, not at module scope: src.api.services.__init__ imports
3692
3762
  # chat_service, so a module-level import of a sibling service is circular.
@@ -3698,7 +3768,7 @@ class ChatService:
3698
3768
  try:
3699
3769
  spec = read_deck_spec(session_id)
3700
3770
  except Exception as e:
3701
- logger.warning(f"Failed to read deck spec (insert_slide): {e}")
3771
+ logger.warning(f"Failed to read deck spec ({label}): {e}")
3702
3772
  return None
3703
3773
 
3704
3774
  if not spec:
@@ -3708,37 +3778,48 @@ class ChatService:
3708
3778
  if not isinstance(slides, list):
3709
3779
  logger.warning(
3710
3780
  "deck_spec_json has no slides list; leaving it untouched",
3711
- extra={"session_id": session_id},
3781
+ extra={"session_id": session_id, "route": label},
3712
3782
  )
3713
3783
  return None
3714
3784
 
3715
- shifted: List[Dict[str, Any]] = []
3785
+ # One pass, and identity is never inferred from equality: two entries with
3786
+ # the same content are still two entries.
3787
+ positioned: List[Dict[str, Any]] = []
3788
+ unpositioned: List[Any] = []
3716
3789
  for entry in slides:
3717
- if not isinstance(entry, dict):
3718
- shifted.append(entry)
3719
- continue
3790
+ if isinstance(entry, dict) and isinstance(entry.get("position"), int):
3791
+ positioned.append(entry)
3792
+ else:
3793
+ unpositioned.append(entry)
3794
+ positioned.sort(key=lambda e: e["position"])
3795
+
3796
+ try:
3797
+ rewritten = reorder(list(positioned))
3798
+ except IndexError:
3799
+ # The route validated its index against the DECK, so an index the spec
3800
+ # cannot answer means the two have already drifted. Leaving a drifted
3801
+ # spec alone is recoverable; renumbering it against the wrong slides is
3802
+ # not.
3803
+ logger.warning(
3804
+ "deck spec has %s entries and cannot answer this mutation; "
3805
+ "leaving it untouched",
3806
+ len(positioned),
3807
+ extra={"session_id": session_id, "route": label},
3808
+ )
3809
+ return None
3810
+
3811
+ if rewritten is None:
3812
+ return None
3813
+
3814
+ renumbered: List[Any] = []
3815
+ for index, entry in enumerate(rewritten):
3720
3816
  entry = dict(entry)
3721
- entry_position = entry.get("position")
3722
- if isinstance(entry_position, int) and entry_position >= position:
3723
- entry["position"] = entry_position + 1
3724
- shifted.append(entry)
3725
-
3726
- shifted.append(
3727
- {
3728
- "position": position,
3729
- "purpose": "",
3730
- "content_brief": "",
3731
- "assumes": "",
3732
- "hands_off": "",
3733
- "data_references": [],
3734
- }
3735
- )
3736
- shifted.sort(
3737
- key=lambda e: e.get("position", 0) if isinstance(e, dict) else 0
3738
- )
3817
+ entry["position"] = index
3818
+ renumbered.append(entry)
3819
+ renumbered.extend(unpositioned)
3739
3820
 
3740
3821
  spec = dict(spec)
3741
- spec["slides"] = shifted
3822
+ spec["slides"] = renumbered
3742
3823
 
3743
3824
  try:
3744
3825
  _user = get_current_username()
@@ -3752,11 +3833,105 @@ class ChatService:
3752
3833
  modified_by=_user,
3753
3834
  )
3754
3835
  except Exception as e:
3755
- # The slide is already saved and the marker will still fire, so a spec
3756
- # write that fails must not fail the insert.
3757
- logger.warning(f"Failed to shift deck spec (insert_slide): {e}")
3836
+ logger.warning(f"Failed to write deck spec ({label}): {e}")
3758
3837
  return None
3759
3838
 
3839
+ def _insert_deck_spec_slide(
3840
+ self,
3841
+ session_id: str,
3842
+ position: int,
3843
+ ) -> Optional[Dict[str, Any]]:
3844
+ """Give the deck spec a placeholder entry at *position*, shifting the rest up.
3845
+
3846
+ *position* is where the slide ACTUALLY landed, which the caller computes
3847
+ before the insert: ``SlideDeck.insert_slide`` delegates to ``list.insert``,
3848
+ which clamps a too-large position into an append, so the requested position
3849
+ and the real one diverge on an append. The clamp is re-applied here against
3850
+ the spec's own length rather than trusted, because a spec shorter than the
3851
+ deck would otherwise leave a hole.
3852
+ """
3853
+
3854
+ def _splice(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
3855
+ landed = min(position, len(entries))
3856
+ return (
3857
+ entries[:landed]
3858
+ + [dict(_BLANK_DECK_SPEC_ENTRY)]
3859
+ + entries[landed:]
3860
+ )
3861
+
3862
+ return self._rewrite_deck_spec_slides(
3863
+ session_id, _splice, label="insert_slide"
3864
+ )
3865
+
3866
+ def _delete_deck_spec_slide(
3867
+ self,
3868
+ session_id: str,
3869
+ index: int,
3870
+ ) -> Optional[Dict[str, Any]]:
3871
+ """Drop the deleted slide's spec entry and shift every higher entry down."""
3872
+
3873
+ def _drop(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
3874
+ if index >= len(entries):
3875
+ raise IndexError(index)
3876
+ return entries[:index] + entries[index + 1 :]
3877
+
3878
+ return self._rewrite_deck_spec_slides(
3879
+ session_id, _drop, label="delete_slide"
3880
+ )
3881
+
3882
+ def _duplicate_deck_spec_slide(
3883
+ self,
3884
+ session_id: str,
3885
+ index: int,
3886
+ ) -> Optional[Dict[str, Any]]:
3887
+ """Copy the duplicated slide's spec entry in beside it.
3888
+
3889
+ The clone gets its SOURCE's brief, not a blank one: a duplicate is a copy of
3890
+ a slide we already have a description for, so that description is known to be
3891
+ right, where insert's newcomer has none. The route's spec-dirty marker fires
3892
+ either way, so the sweeper re-describes both — this decides which starting
3893
+ point is true, not who writes the final prose.
3894
+ """
3895
+
3896
+ def _clone(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
3897
+ if index >= len(entries):
3898
+ raise IndexError(index)
3899
+ return (
3900
+ entries[: index + 1]
3901
+ + [dict(entries[index])]
3902
+ + entries[index + 1 :]
3903
+ )
3904
+
3905
+ return self._rewrite_deck_spec_slides(
3906
+ session_id, _clone, label="duplicate_slide"
3907
+ )
3908
+
3909
+ def _reorder_deck_spec_slides(
3910
+ self,
3911
+ session_id: str,
3912
+ new_order: List[int],
3913
+ ) -> Optional[Dict[str, Any]]:
3914
+ """Permute the spec entries by the same permutation the slides took.
3915
+
3916
+ ``new_order[j]`` is the OLD index of the slide that now sits at *j* — the
3917
+ route validates it is a true permutation of ``range(len(slides))`` before
3918
+ reaching here.
3919
+
3920
+ This is the case the consumer-side guard structurally cannot cover: a reorder
3921
+ preserves the position SET, so ``_persisted_spec_describes_these_rows`` reads
3922
+ a reordered deck as aligned while every entry describes a different slide.
3923
+ Renumbering in the route is the only thing that closes it.
3924
+ """
3925
+
3926
+ def _permute(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
3927
+ if len(new_order) != len(entries):
3928
+ raise IndexError(len(new_order))
3929
+ return [entries[old] for old in new_order]
3930
+
3931
+ return self._rewrite_deck_spec_slides(
3932
+ session_id, _permute, label="reorder_slides"
3933
+ )
3934
+
3760
3935
  def delete_slide(self, session_id: str, index: int, *, expected_version: Optional[int] = None) -> Dict[str, Any]:
3761
3936
  """Delete a slide.
3762
3937
 
@@ -3799,6 +3974,13 @@ class ChatService:
3799
3974
  )
3800
3975
  self._record_deck_version(session_id, save_result)
3801
3976
 
3977
+ # The deck spec's slide list has to move with the deck's. AFTER
3978
+ # save_slide_deck, never before: the spec write bumps deck.version, which
3979
+ # would make the caller's expected_version stale and 409 a legitimate save.
3980
+ spec_result = self._delete_deck_spec_slide(session_id, index)
3981
+ if spec_result:
3982
+ self._record_deck_version(session_id, spec_result)
3983
+
3802
3984
  # Create save point
3803
3985
  try:
3804
3986
  self.create_save_point(
@@ -73,7 +73,10 @@ from langgraph.graph import END, START, StateGraph
73
73
  from src.core.checkpointer import get_checkpointer
74
74
  from src.core.user_context import get_current_user
75
75
  from src.services.foreman_service import CAP
76
- from src.services.graph.event_emitter import set_event_emitter
76
+ from src.services.graph.event_emitter import (
77
+ set_chat_request_id,
78
+ set_event_emitter,
79
+ )
77
80
  from src.services.graph.nodes import (
78
81
  architect_node,
79
82
  build_reviewer_node,
@@ -188,6 +191,7 @@ def invoke_graph(
188
191
  emitter: Any = None,
189
192
  principal: Optional[str] = None,
190
193
  describe_only: bool = False,
194
+ request_id: Optional[str] = None,
191
195
  ) -> Dict[str, Any]:
192
196
  """Run one turn of the graph for *session_id*.
193
197
 
@@ -210,6 +214,14 @@ def invoke_graph(
210
214
  arc-review sweeper passes it: that turn exists to re-describe the
211
215
  deck a human hand-edited, and dispatching builders would overwrite
212
216
  the very edits that scheduled it.
217
+ request_id: The ``ChatRequest.request_id`` this turn is answering, or
218
+ ``None``. Bound in a ``ContextVar`` so a node persisting a chat
219
+ message can TAG it: ``GET /chat/poll`` reads a turn's assistant text
220
+ through ``get_messages_for_request``, which filters on that column,
221
+ so an untagged row is invisible to every polling client — the
222
+ transport the deployed app uses. ``None`` is correct on the SSE path
223
+ (no request row exists) and on the sweeper (no request at all); those
224
+ rows are still durable and still reach ``_conversation``.
213
225
 
214
226
  ``describe_only`` is turn-scoped, and it is wrapped HERE because this is the
215
227
  only place that knows ``turn_id``. It is written on **every** invocation,
@@ -231,12 +243,17 @@ def invoke_graph(
231
243
 
232
244
  The emitter is set on every invocation, ``None`` included: that is the reset
233
245
  a turn resumed in a new process needs, or events queue into the first
234
- process's queue forever.
246
+ process's queue forever. ``request_id`` is set unconditionally for the same
247
+ reason, and the failure it prevents is worse than a lost event: a stale id
248
+ left in the var would tag turn 2's chat rows with turn 1's request, so turn
249
+ 1's poll would receive turn 2's reply and turn 2's own poll would never see
250
+ it.
235
251
  """
236
252
  turn_id = uuid.uuid4().hex
237
253
  initiated_by = principal or get_current_user()
238
254
 
239
255
  set_event_emitter(emitter)
256
+ set_chat_request_id(request_id)
240
257
 
241
258
  state: Dict[str, Any] = dict(initial or {})
242
259
  state.update(
@@ -69,6 +69,46 @@ slide_cursor_var: ContextVar[Optional[list]] = ContextVar(
69
69
  "graph_slide_cursor", default=None
70
70
  )
71
71
 
72
+ #: The ``ChatRequest.request_id`` this turn is running under, or ``None``.
73
+ #:
74
+ #: **This is not an emission concern, and it is here because the LIFECYCLE is the
75
+ #: same one.** ``GET /chat/poll`` reads a turn's assistant text out of
76
+ #: ``get_messages_for_request``, which filters ``SessionMessage.request_id ==
77
+ #: request_id`` — so a chat row written by a node with no request id is invisible
78
+ #: to every polling client, which is the transport the deployed app uses. The id
79
+ #: cannot travel in ``GraphState`` (undeclared keys are silently dropped and
80
+ #: declaring it would put a transport detail in the graph's contract) and it
81
+ #: cannot be re-derived inside a node, so it rides the same ``ContextVar``
82
+ #: mechanism the emitter does, is set by ``invoke_graph`` on **every** invocation,
83
+ #: and is ``None`` on the SSE path (which has no request row) and on the sweeper.
84
+ chat_request_id_var: ContextVar[Optional[str]] = ContextVar(
85
+ "graph_chat_request_id", default=None
86
+ )
87
+
88
+
89
+ def set_chat_request_id(request_id: Optional[str]) -> None:
90
+ """Bind *request_id* for this turn's context; ``None`` clears it.
91
+
92
+ Called by ``invoke_graph`` unconditionally, for the reason
93
+ :func:`set_event_emitter` is: a value left over from an earlier turn in the
94
+ same process would tag turn 2's rows with turn 1's request, and turn 1's
95
+ poll — which a client may still be draining — would then receive turn 2's
96
+ reply while turn 2's own poll never saw it.
97
+ """
98
+ chat_request_id_var.set(request_id)
99
+
100
+
101
+ def get_chat_request_id() -> Optional[str]:
102
+ """The request id this turn runs under, or ``None`` when there is no request.
103
+
104
+ ``None`` is a normal, correct answer on two paths: the SSE transport (which
105
+ persists no ``ChatRequest`` row at all) and the arc-review sweeper (which has
106
+ no request). A row written with ``None`` is still a durable transcript row and
107
+ still reaches ``_conversation``; it is only the *polling* projection that
108
+ needs the id.
109
+ """
110
+ return chat_request_id_var.get()
111
+
72
112
 
73
113
  def set_event_emitter(emitter: Optional[queue.Queue]) -> None:
74
114
  """Install *emitter* as this context's event queue, and reset the slide cursor.