databricks-tellr-app 0.3.13.dev1__tar.gz → 0.3.13.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/PKG-INFO +1 -1
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/SOURCES.txt +14 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/pyproject.toml +1 -1
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/chat_service.py +9 -70
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/export_job_queue.py +10 -23
- databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/__init__.py +10 -0
- databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/ast_guard.py +58 -0
- databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/codeprep.py +220 -0
- databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/gslides_runner.py +78 -0
- databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/jail.py +230 -0
- databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/pptx_runner.py +124 -0
- databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/protocol.py +41 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/google_slides_prompts_defaults.py +28 -18
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/html_to_google_slides.py +246 -185
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/html_to_pptx.py +73 -453
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/README.md +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app/run.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/requires.txt +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/top_level.txt +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/setup.cfg +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/setup.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/_otel_bootstrap.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/fixtures/tour_demo_deck.json +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/main.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/mcp_auth.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/mcp_server.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/middleware/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/middleware/request_logging.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/admin.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/admin_usage.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/chat.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/deck_contributors.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/export.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/google_slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/images.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/local_version.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/profiles.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/sessions.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/contributors.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/deck_prompts.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/identities.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/slide_styles.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/setup.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/tools.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/tour.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/verification.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/version.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/requests.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/responses.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/settings/requests.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/settings/responses.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/streaming.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/feedback_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/job_queue.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/session_manager.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/session_naming.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/usage_events.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/usage_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/utils/validation.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/config_loader.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/context_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/database.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/databricks_client.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/encryption.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/init_default_profile.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/lakebase.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/migrate_profiles_to_agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/mlflow_agent_spans.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/mlflow_tracing.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/otel_logging.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/permission_context.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/prompt_modules.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/settings_db.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/user_context.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/deck_contributor.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/genie_space.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/google_global_credentials.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/google_oauth_token.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/identity.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/image.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/profile.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/profile_contributor.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/prompts.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/request_log.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/session.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/slide_deck_prompt.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/slide_style_library.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/usage_event.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/user_preference.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/domain/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/domain/slide.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/domain/slide_deck.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/agent.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/agent_factory.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/config_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/config_validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/drive_uploader.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/evaluation/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/evaluation/llm_judge.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/genie_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/google_slides_auth.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/account_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/local_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/workspace_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/image_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/image_tools.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/permission_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/pptx_from_html_huashu.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/pptx_from_records.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/pptx_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/profile_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/streaming_callback.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/agent_bricks_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/genie_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/mcp_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/model_endpoint_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/vector_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/css_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/error_handling.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/html_safety.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/html_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/image_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/js_validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/logging_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/pi_filter.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/slide_hash.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/spotlight.py +0 -0
- {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/text_caps.py +0 -0
|
@@ -115,6 +115,13 @@ setup.py
|
|
|
115
115
|
./src/services/profile_service.py
|
|
116
116
|
./src/services/streaming_callback.py
|
|
117
117
|
./src/services/validator.py
|
|
118
|
+
./src/services/converter_jail/__init__.py
|
|
119
|
+
./src/services/converter_jail/ast_guard.py
|
|
120
|
+
./src/services/converter_jail/codeprep.py
|
|
121
|
+
./src/services/converter_jail/gslides_runner.py
|
|
122
|
+
./src/services/converter_jail/jail.py
|
|
123
|
+
./src/services/converter_jail/pptx_runner.py
|
|
124
|
+
./src/services/converter_jail/protocol.py
|
|
118
125
|
./src/services/evaluation/__init__.py
|
|
119
126
|
./src/services/evaluation/llm_judge.py
|
|
120
127
|
./src/services/identity_providers/__init__.py
|
|
@@ -258,6 +265,13 @@ src/services/pptx_prompts_defaults.py
|
|
|
258
265
|
src/services/profile_service.py
|
|
259
266
|
src/services/streaming_callback.py
|
|
260
267
|
src/services/validator.py
|
|
268
|
+
src/services/converter_jail/__init__.py
|
|
269
|
+
src/services/converter_jail/ast_guard.py
|
|
270
|
+
src/services/converter_jail/codeprep.py
|
|
271
|
+
src/services/converter_jail/gslides_runner.py
|
|
272
|
+
src/services/converter_jail/jail.py
|
|
273
|
+
src/services/converter_jail/pptx_runner.py
|
|
274
|
+
src/services/converter_jail/protocol.py
|
|
261
275
|
src/services/evaluation/__init__.py
|
|
262
276
|
src/services/evaluation/llm_judge.py
|
|
263
277
|
src/services/identity_providers/__init__.py
|
|
@@ -80,11 +80,6 @@ class ChatService:
|
|
|
80
80
|
# This avoids re-parsing HTML on every request
|
|
81
81
|
self._deck_cache: Dict[str, SlideDeck] = {}
|
|
82
82
|
|
|
83
|
-
# DB version each cached deck corresponds to. The cache is per-process
|
|
84
|
-
# while prod runs multiple uvicorn workers sharing one database, so a
|
|
85
|
-
# cache hit is only valid if the DB version hasn't moved on.
|
|
86
|
-
self._deck_cache_versions: Dict[str, int] = {}
|
|
87
|
-
|
|
88
83
|
logger.info("ChatService initialized successfully")
|
|
89
84
|
|
|
90
85
|
def _substitute_images_for_response(self, deck_dict, raw_html=None):
|
|
@@ -605,7 +600,7 @@ class ChatService:
|
|
|
605
600
|
slide_deck_dict = current_deck.to_dict()
|
|
606
601
|
|
|
607
602
|
try:
|
|
608
|
-
|
|
603
|
+
session_manager.save_slide_deck(
|
|
609
604
|
session_id=session_id,
|
|
610
605
|
title=current_deck.title,
|
|
611
606
|
html_content=current_deck.knit(),
|
|
@@ -615,7 +610,6 @@ class ChatService:
|
|
|
615
610
|
modified_by=_user,
|
|
616
611
|
expected_version=_deck_version_before_llm,
|
|
617
612
|
)
|
|
618
|
-
self._record_deck_version(session_id, save_result)
|
|
619
613
|
except VersionConflictError:
|
|
620
614
|
logger.warning(
|
|
621
615
|
"Chat save rejected: deck was edited during LLM call, reloading",
|
|
@@ -1391,7 +1385,7 @@ class ChatService:
|
|
|
1391
1385
|
slide_deck_dict = current_deck.to_dict()
|
|
1392
1386
|
|
|
1393
1387
|
try:
|
|
1394
|
-
|
|
1388
|
+
session_manager.save_slide_deck(
|
|
1395
1389
|
session_id=session_id,
|
|
1396
1390
|
title=current_deck.title,
|
|
1397
1391
|
html_content=current_deck.knit(),
|
|
@@ -1401,7 +1395,6 @@ class ChatService:
|
|
|
1401
1395
|
modified_by=_user,
|
|
1402
1396
|
expected_version=_deck_version_before_llm,
|
|
1403
1397
|
)
|
|
1404
|
-
self._record_deck_version(session_id, save_result)
|
|
1405
1398
|
except VersionConflictError:
|
|
1406
1399
|
logger.warning(
|
|
1407
1400
|
"Chat save rejected: deck was edited during LLM call, reloading",
|
|
@@ -1928,63 +1921,18 @@ class ChatService:
|
|
|
1928
1921
|
context = "\n\n[Attached images]\n" + "\n".join(image_descriptions)
|
|
1929
1922
|
return message + context
|
|
1930
1923
|
|
|
1931
|
-
def _deck_versions(self) -> Dict[str, int]:
|
|
1932
|
-
"""Return the cached-deck version map, creating it if needed.
|
|
1933
|
-
|
|
1934
|
-
Lazy creation keeps instances built without __init__ (test harnesses
|
|
1935
|
-
use ChatService.__new__) working. Callers must hold _cache_lock.
|
|
1936
|
-
"""
|
|
1937
|
-
try:
|
|
1938
|
-
return self._deck_cache_versions
|
|
1939
|
-
except AttributeError:
|
|
1940
|
-
self._deck_cache_versions = {}
|
|
1941
|
-
return self._deck_cache_versions
|
|
1942
|
-
|
|
1943
|
-
def _record_deck_version(self, session_id: str, save_result: Optional[Dict[str, Any]]) -> None:
|
|
1944
|
-
"""Remember which DB version the cached deck now corresponds to.
|
|
1945
|
-
|
|
1946
|
-
Call after save_slide_deck so the next _get_or_load_deck cache hit
|
|
1947
|
-
validates cleanly instead of reloading from the database.
|
|
1948
|
-
"""
|
|
1949
|
-
version = (save_result or {}).get("version")
|
|
1950
|
-
with self._cache_lock:
|
|
1951
|
-
if version is not None:
|
|
1952
|
-
self._deck_versions()[session_id] = version
|
|
1953
|
-
else:
|
|
1954
|
-
self._deck_versions().pop(session_id, None)
|
|
1955
|
-
|
|
1956
1924
|
def _get_or_load_deck(self, session_id: str) -> Optional[SlideDeck]:
|
|
1957
1925
|
"""Get deck from cache or load from database.
|
|
1958
1926
|
|
|
1959
1927
|
Thread-safe access to deck cache using _cache_lock.
|
|
1960
|
-
|
|
1961
|
-
The cache is per-process while prod runs multiple uvicorn workers
|
|
1962
|
-
sharing one database, so a cached deck is only served if its recorded
|
|
1963
|
-
version matches the current DB version; otherwise it is reloaded.
|
|
1964
|
-
|
|
1928
|
+
|
|
1965
1929
|
Uses deck_dict (slides array with individual scripts) when available,
|
|
1966
1930
|
falling back to from_html_string for legacy data.
|
|
1967
1931
|
"""
|
|
1968
1932
|
# Check cache first (with lock)
|
|
1969
1933
|
with self._cache_lock:
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
if cached_deck is not None:
|
|
1974
|
-
db_version = self._get_deck_version(session_id)
|
|
1975
|
-
if db_version is None or cached_version == db_version:
|
|
1976
|
-
return cached_deck
|
|
1977
|
-
logger.info(
|
|
1978
|
-
"Deck cache stale, reloading from database",
|
|
1979
|
-
extra={
|
|
1980
|
-
"session_id": session_id,
|
|
1981
|
-
"cached_version": cached_version,
|
|
1982
|
-
"db_version": db_version,
|
|
1983
|
-
},
|
|
1984
|
-
)
|
|
1985
|
-
with self._cache_lock:
|
|
1986
|
-
self._deck_cache.pop(session_id, None)
|
|
1987
|
-
self._deck_versions().pop(session_id, None)
|
|
1934
|
+
if session_id in self._deck_cache:
|
|
1935
|
+
return self._deck_cache[session_id]
|
|
1988
1936
|
|
|
1989
1937
|
# Try to load from database (outside lock to avoid blocking)
|
|
1990
1938
|
session_manager = get_session_manager()
|
|
@@ -2018,10 +1966,6 @@ class ChatService:
|
|
|
2018
1966
|
# Store in cache (with lock)
|
|
2019
1967
|
with self._cache_lock:
|
|
2020
1968
|
self._deck_cache[session_id] = deck
|
|
2021
|
-
if deck_data.get("version") is not None:
|
|
2022
|
-
self._deck_versions()[session_id] = deck_data["version"]
|
|
2023
|
-
else:
|
|
2024
|
-
self._deck_versions().pop(session_id, None)
|
|
2025
1969
|
return deck
|
|
2026
1970
|
except Exception as e:
|
|
2027
1971
|
logger.warning(f"Failed to load deck from database: {e}")
|
|
@@ -2064,7 +2008,6 @@ class ChatService:
|
|
|
2064
2008
|
"""Remove the cached deck for a session so the next read hits the DB."""
|
|
2065
2009
|
with self._cache_lock:
|
|
2066
2010
|
self._deck_cache.pop(session_id, None)
|
|
2067
|
-
self._deck_versions().pop(session_id, None)
|
|
2068
2011
|
|
|
2069
2012
|
def _replace_slide_htmls_from_cache(self, session_id: str, slide_context: Dict[str, Any]) -> Dict[str, Any]:
|
|
2070
2013
|
"""Replace frontend-supplied slide_htmls with backend cache versions.
|
|
@@ -2597,7 +2540,7 @@ class ChatService:
|
|
|
2597
2540
|
# Persist to database
|
|
2598
2541
|
deck_dict = current_deck.to_dict()
|
|
2599
2542
|
session_manager = get_session_manager()
|
|
2600
|
-
|
|
2543
|
+
session_manager.save_slide_deck(
|
|
2601
2544
|
session_id=session_id,
|
|
2602
2545
|
title=current_deck.title,
|
|
2603
2546
|
html_content=current_deck.knit(),
|
|
@@ -2606,7 +2549,6 @@ class ChatService:
|
|
|
2606
2549
|
deck_dict=deck_dict,
|
|
2607
2550
|
expected_version=expected_version,
|
|
2608
2551
|
)
|
|
2609
|
-
self._record_deck_version(session_id, save_result)
|
|
2610
2552
|
|
|
2611
2553
|
# Create save point
|
|
2612
2554
|
try:
|
|
@@ -2674,7 +2616,7 @@ class ChatService:
|
|
|
2674
2616
|
# Persist to database
|
|
2675
2617
|
deck_dict = current_deck.to_dict()
|
|
2676
2618
|
session_manager = get_session_manager()
|
|
2677
|
-
|
|
2619
|
+
session_manager.save_slide_deck(
|
|
2678
2620
|
session_id=session_id,
|
|
2679
2621
|
title=current_deck.title,
|
|
2680
2622
|
html_content=current_deck.knit(),
|
|
@@ -2683,7 +2625,6 @@ class ChatService:
|
|
|
2683
2625
|
deck_dict=deck_dict,
|
|
2684
2626
|
expected_version=expected_version,
|
|
2685
2627
|
)
|
|
2686
|
-
self._record_deck_version(session_id, save_result)
|
|
2687
2628
|
|
|
2688
2629
|
# Create save point immediately after persisting
|
|
2689
2630
|
try:
|
|
@@ -2740,7 +2681,7 @@ class ChatService:
|
|
|
2740
2681
|
# Persist to database
|
|
2741
2682
|
deck_dict = current_deck.to_dict()
|
|
2742
2683
|
session_manager = get_session_manager()
|
|
2743
|
-
|
|
2684
|
+
session_manager.save_slide_deck(
|
|
2744
2685
|
session_id=session_id,
|
|
2745
2686
|
title=current_deck.title,
|
|
2746
2687
|
html_content=current_deck.knit(),
|
|
@@ -2749,7 +2690,6 @@ class ChatService:
|
|
|
2749
2690
|
deck_dict=deck_dict,
|
|
2750
2691
|
expected_version=expected_version,
|
|
2751
2692
|
)
|
|
2752
|
-
self._record_deck_version(session_id, save_result)
|
|
2753
2693
|
|
|
2754
2694
|
# Create save point
|
|
2755
2695
|
try:
|
|
@@ -2804,7 +2744,7 @@ class ChatService:
|
|
|
2804
2744
|
# Persist to database
|
|
2805
2745
|
deck_dict = current_deck.to_dict()
|
|
2806
2746
|
session_manager = get_session_manager()
|
|
2807
|
-
|
|
2747
|
+
session_manager.save_slide_deck(
|
|
2808
2748
|
session_id=session_id,
|
|
2809
2749
|
title=current_deck.title,
|
|
2810
2750
|
html_content=current_deck.knit(),
|
|
@@ -2813,7 +2753,6 @@ class ChatService:
|
|
|
2813
2753
|
deck_dict=deck_dict,
|
|
2814
2754
|
expected_version=expected_version,
|
|
2815
2755
|
)
|
|
2816
|
-
self._record_deck_version(session_id, save_result)
|
|
2817
2756
|
|
|
2818
2757
|
# Create save point
|
|
2819
2758
|
try:
|
|
@@ -307,9 +307,6 @@ async def convert_slides_with_progress(
|
|
|
307
307
|
output_path: Path to save PPTX
|
|
308
308
|
chart_images_per_slide: Chart images per slide
|
|
309
309
|
"""
|
|
310
|
-
from pptx import Presentation
|
|
311
|
-
from pptx.util import Inches
|
|
312
|
-
|
|
313
310
|
total_slides = len(slides_html)
|
|
314
311
|
|
|
315
312
|
# -- Phase 1: Prepare assets + parallel LLM code generation ---------------
|
|
@@ -336,27 +333,17 @@ async def convert_slides_with_progress(
|
|
|
336
333
|
slide_inputs, on_codegen_progress=_on_codegen_progress,
|
|
337
334
|
)
|
|
338
335
|
|
|
339
|
-
# -- Phase 2: Sequential execution
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
prs.slide_height = Inches(7.5)
|
|
343
|
-
|
|
344
|
-
for i, (code, (html_str, _chart_files, _content_files, assets_dir)) in enumerate(
|
|
345
|
-
zip(codes, slide_inputs), 1,
|
|
346
|
-
):
|
|
347
|
-
update_export_progress(
|
|
348
|
-
job_id, i, total_slides,
|
|
349
|
-
status_message=f"Building slide {i}/{total_slides}…",
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
converter._execute_slide(code, prs, html_str, assets_dir, i)
|
|
353
|
-
|
|
354
|
-
logger.debug(
|
|
355
|
-
f"Processed slide {i}/{total_slides}",
|
|
356
|
-
extra={"job_id": job_id, "slide": i, "total": total_slides},
|
|
357
|
-
)
|
|
336
|
+
# -- Phase 2: Sequential execution INSIDE the subprocess jail -------------
|
|
337
|
+
def _relay(current: int, total: int, message: str) -> None:
|
|
338
|
+
update_export_progress(job_id, current, total, status_message=message)
|
|
358
339
|
|
|
359
|
-
|
|
340
|
+
# to_thread: keep the long blocking jail wait off the event loop (this
|
|
341
|
+
# coroutine runs on the loop; _relay is fired from the jail's stdout
|
|
342
|
+
# reader thread and update_export_progress opens its own session per call).
|
|
343
|
+
await asyncio.to_thread(
|
|
344
|
+
converter._run_pptx_conversion,
|
|
345
|
+
codes, slide_inputs, output_path, progress_cb=_relay,
|
|
346
|
+
)
|
|
360
347
|
|
|
361
348
|
|
|
362
349
|
async def export_worker() -> None:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# src/services/converter_jail/__init__.py
|
|
2
|
+
"""Subprocess jail for LLM-generated converter code (SDR-4437 HIGH-5)."""
|
|
3
|
+
|
|
4
|
+
from src.services.converter_jail.jail import ( # noqa: F401
|
|
5
|
+
JailError,
|
|
6
|
+
JailResult,
|
|
7
|
+
ResourceLimits,
|
|
8
|
+
run_gslides_jail,
|
|
9
|
+
run_pptx_jail,
|
|
10
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# src/services/converter_jail/ast_guard.py
|
|
2
|
+
"""Best-effort AST import allowlist for generated converter code.
|
|
3
|
+
|
|
4
|
+
Defense-in-depth ONLY — bypassable by construction (e.g. __import__,
|
|
5
|
+
importlib). The security boundary is the subprocess jail, not this check.
|
|
6
|
+
Runs on the trusted host before launch so obviously-hostile code never
|
|
7
|
+
reaches the child.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DisallowedImport(Exception):
|
|
14
|
+
"""Raised when generated code imports a module outside the allowlist."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Top-level module names generated converters legitimately need.
|
|
18
|
+
# Keep this GENEROUS for harmless stdlib: a snippet failing this check is
|
|
19
|
+
# downgraded to a blank placeholder slide (has_code=False in the job-dir
|
|
20
|
+
# builders, Tasks 4/6), so a too-narrow list silently blanks legitimate
|
|
21
|
+
# slides. The prompts only show example imports — they do not restrict the
|
|
22
|
+
# LLM to a fixed set — and the jail, not this list, is the security boundary.
|
|
23
|
+
DEFAULT_ALLOWED = frozenset({
|
|
24
|
+
# PPTX path
|
|
25
|
+
"pptx", "os", "re", "math", "json", "uuid", "base64", "io",
|
|
26
|
+
"datetime", "collections", "itertools", "string", "textwrap",
|
|
27
|
+
# harmless stdlib the LLM commonly emits unprompted
|
|
28
|
+
"sys", "time", "typing", "functools", "random", "copy",
|
|
29
|
+
"html", "unicodedata", "decimal",
|
|
30
|
+
# image handling
|
|
31
|
+
"PIL", "lxml",
|
|
32
|
+
# Google Slides path emits data only; googleapiclient is host-side now,
|
|
33
|
+
# but allow the http helper import in case a snippet still references it
|
|
34
|
+
# (host strips its effect; import alone is harmless).
|
|
35
|
+
"googleapiclient",
|
|
36
|
+
"xml",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _root(name: str) -> str:
|
|
41
|
+
return name.split(".", 1)[0]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_imports(code: str, allowed: frozenset = DEFAULT_ALLOWED) -> None:
|
|
45
|
+
"""Raise DisallowedImport if *code* imports a non-allowlisted top-level
|
|
46
|
+
module. Raises SyntaxError if *code* does not parse."""
|
|
47
|
+
tree = ast.parse(code)
|
|
48
|
+
for node in ast.walk(tree):
|
|
49
|
+
if isinstance(node, ast.Import):
|
|
50
|
+
for alias in node.names:
|
|
51
|
+
if _root(alias.name) not in allowed:
|
|
52
|
+
raise DisallowedImport(_root(alias.name))
|
|
53
|
+
elif isinstance(node, ast.ImportFrom):
|
|
54
|
+
# Relative imports (node.level > 0) have no module root to check;
|
|
55
|
+
# they cannot reach outside the (nonexistent) package, so allow.
|
|
56
|
+
if node.level == 0 and node.module is not None:
|
|
57
|
+
if _root(node.module) not in allowed:
|
|
58
|
+
raise DisallowedImport(_root(node.module))
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# src/services/converter_jail/codeprep.py
|
|
2
|
+
"""Dependency-light code-prep helpers shared by the jail runners (SDR-4437 PR-5).
|
|
3
|
+
|
|
4
|
+
STDLIB-ONLY BY DESIGN. This module is imported inside the sandboxed child
|
|
5
|
+
(`python -I`), so it must never pull in `databricks`, `google*`, `bs4`,
|
|
6
|
+
`pptx`, or any `src.services.*` converter module — those graphs drag the
|
|
7
|
+
credential-capable SDK into the jail just to call string transformers.
|
|
8
|
+
|
|
9
|
+
Contents:
|
|
10
|
+
- ``sanitize_code`` — moved verbatim from ``HtmlToPptxConverterV3._sanitize_code``.
|
|
11
|
+
- ``_fix_apostrophe_strings`` / ``_convert_single_to_double_quoted`` — moved
|
|
12
|
+
verbatim from ``html_to_google_slides``.
|
|
13
|
+
- ``prepare_requests_code`` — preps generated Google Slides data-out code
|
|
14
|
+
(wraps bare code into ``build_slide_requests``; request-shape fixes).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sanitize_code(code: str) -> str:
|
|
22
|
+
"""Fix common LLM-generated code issues before execution.
|
|
23
|
+
|
|
24
|
+
Handles smart quotes, em-dashes, and apostrophes inside string literals
|
|
25
|
+
that cause SyntaxError (e.g. ``Anthony's`` inside a single-quoted string).
|
|
26
|
+
"""
|
|
27
|
+
# Smart/curly quotes → straight quotes
|
|
28
|
+
for smart, straight in {
|
|
29
|
+
"‘": "'", "’": "'", # single curly quotes
|
|
30
|
+
"“": '"', "”": '"', # double curly quotes
|
|
31
|
+
}.items():
|
|
32
|
+
code = code.replace(smart, straight)
|
|
33
|
+
|
|
34
|
+
# Em-dash / en-dash → regular hyphen (prevents SyntaxError when
|
|
35
|
+
# they appear outside strings due to broken quoting)
|
|
36
|
+
code = code.replace("—", "-") # em-dash
|
|
37
|
+
code = code.replace("–", "-") # en-dash
|
|
38
|
+
|
|
39
|
+
# Fix apostrophes inside single-quoted strings (e.g. 'Anthony's workflow')
|
|
40
|
+
# by converting affected lines to use double-quoted strings.
|
|
41
|
+
for _ in range(20):
|
|
42
|
+
try:
|
|
43
|
+
ast.parse(code)
|
|
44
|
+
break
|
|
45
|
+
except SyntaxError as exc:
|
|
46
|
+
if exc.lineno is None:
|
|
47
|
+
break
|
|
48
|
+
lines = code.splitlines()
|
|
49
|
+
idx = exc.lineno - 1
|
|
50
|
+
if idx < 0 or idx >= len(lines):
|
|
51
|
+
break
|
|
52
|
+
original = lines[idx]
|
|
53
|
+
# Re-quote single-quoted strings that contain apostrophes
|
|
54
|
+
fixed = re.sub(
|
|
55
|
+
r"'([^']*\w'\w[^']*)'",
|
|
56
|
+
lambda m: '"' + m.group(1) + '"',
|
|
57
|
+
original,
|
|
58
|
+
)
|
|
59
|
+
if fixed == original:
|
|
60
|
+
break
|
|
61
|
+
lines[idx] = fixed
|
|
62
|
+
code = "\n".join(lines)
|
|
63
|
+
return code
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Apostrophe-in-string syntax fixer
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def _convert_single_to_double_quoted(line: str) -> str:
|
|
71
|
+
"""Re-quote single-quoted strings that contain apostrophes as double-quoted.
|
|
72
|
+
|
|
73
|
+
Scans each single-quote-delimited token on *line*. When an apostrophe is
|
|
74
|
+
surrounded by word characters (``\\w'\\w``) it is treated as part of the
|
|
75
|
+
content (not a closing delimiter), so the scanner continues until it finds a
|
|
76
|
+
genuine closing quote. Those strings are then re-emitted with double-quote
|
|
77
|
+
delimiters so Python can parse them correctly.
|
|
78
|
+
|
|
79
|
+
Example::
|
|
80
|
+
|
|
81
|
+
'We didn't do it' → "We didn't do it"
|
|
82
|
+
"""
|
|
83
|
+
result: list = []
|
|
84
|
+
i = 0
|
|
85
|
+
n = len(line)
|
|
86
|
+
|
|
87
|
+
while i < n:
|
|
88
|
+
if line[i] != "'":
|
|
89
|
+
result.append(line[i])
|
|
90
|
+
i += 1
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Scan for the "real" closing quote, treating word-apostrophe-word as content.
|
|
94
|
+
j = i + 1
|
|
95
|
+
while j < n:
|
|
96
|
+
if line[j] == "'":
|
|
97
|
+
before_word = j > 0 and line[j - 1].isalnum()
|
|
98
|
+
after_word = (j + 1) < n and line[j + 1].isalnum()
|
|
99
|
+
if before_word and after_word:
|
|
100
|
+
j += 1 # it's a contraction apostrophe — keep scanning
|
|
101
|
+
continue
|
|
102
|
+
break # genuine closing quote
|
|
103
|
+
j += 1
|
|
104
|
+
|
|
105
|
+
if j >= n:
|
|
106
|
+
# Never found a real closing quote — rest of line is broken content.
|
|
107
|
+
content = line[i + 1:]
|
|
108
|
+
result.append('"' + content.replace('"', '\\"') + '"')
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
content = line[i + 1: j]
|
|
112
|
+
if re.search(r"\w'\w", content):
|
|
113
|
+
# Content has an apostrophe → rewrite as double-quoted string.
|
|
114
|
+
result.append('"' + content.replace('"', '\\"') + '"')
|
|
115
|
+
else:
|
|
116
|
+
result.append(line[i: j + 1])
|
|
117
|
+
|
|
118
|
+
i = j + 1
|
|
119
|
+
|
|
120
|
+
return "".join(result)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _fix_apostrophe_strings(code: str) -> str:
|
|
124
|
+
"""Fix single-quoted literals whose content contains apostrophes (contractions).
|
|
125
|
+
|
|
126
|
+
The LLM often emits ``'text': 'We don't ...'``. The ``'`` in *don't* ends the
|
|
127
|
+
literal early; the parser may report ``unterminated string``, ``eol while
|
|
128
|
+
scanning``, or ``invalid character`` (e.g. em-dash) on the remainder.
|
|
129
|
+
|
|
130
|
+
Repeatedly ``ast.parse`` and rewrite the error line with
|
|
131
|
+
:func:`_convert_single_to_double_quoted` whenever that changes the line.
|
|
132
|
+
"""
|
|
133
|
+
for _ in range(20): # guard against infinite loops
|
|
134
|
+
try:
|
|
135
|
+
ast.parse(code)
|
|
136
|
+
return code
|
|
137
|
+
except SyntaxError as exc:
|
|
138
|
+
if exc.lineno is None:
|
|
139
|
+
return code
|
|
140
|
+
|
|
141
|
+
lines = code.splitlines()
|
|
142
|
+
line_idx = exc.lineno - 1
|
|
143
|
+
if line_idx >= len(lines):
|
|
144
|
+
return code
|
|
145
|
+
|
|
146
|
+
original = lines[line_idx]
|
|
147
|
+
fixed = _convert_single_to_double_quoted(original)
|
|
148
|
+
if fixed == original:
|
|
149
|
+
return code # heuristic cannot improve this error
|
|
150
|
+
|
|
151
|
+
lines[line_idx] = fixed
|
|
152
|
+
code = "\n".join(lines)
|
|
153
|
+
|
|
154
|
+
return code
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Google Slides data-out contract prep
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
_ALPHA_PAT = re.compile(r"""[,\s]*['"]alpha['"]\s*:\s*[\d.]+""")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def prepare_requests_code(code: str) -> str:
|
|
165
|
+
"""Prep generated code that defines build_slide_requests(...).
|
|
166
|
+
|
|
167
|
+
Reuses the request-shape fixers from the legacy ``_prepare_code`` but wraps
|
|
168
|
+
bare code into ``build_slide_requests`` (not ``add_slide_to_presentation``)
|
|
169
|
+
and never references services."""
|
|
170
|
+
if "def build_slide_requests" not in code:
|
|
171
|
+
lines = code.split("\n")
|
|
172
|
+
imports, body, helpers = [], [], []
|
|
173
|
+
in_helper = False
|
|
174
|
+
for line in lines:
|
|
175
|
+
stripped = line.strip()
|
|
176
|
+
if stripped.startswith(("import ", "from ")) and not body:
|
|
177
|
+
imports.append(line)
|
|
178
|
+
elif stripped.startswith(("def emu(", "def hex_to_rgb(")):
|
|
179
|
+
in_helper = True
|
|
180
|
+
helpers.append(line)
|
|
181
|
+
elif in_helper:
|
|
182
|
+
if stripped == "" or (line and line[0] == " "):
|
|
183
|
+
helpers.append(line)
|
|
184
|
+
if stripped == "":
|
|
185
|
+
in_helper = False
|
|
186
|
+
else:
|
|
187
|
+
in_helper = False
|
|
188
|
+
body.append(line)
|
|
189
|
+
else:
|
|
190
|
+
body.append(line)
|
|
191
|
+
indented = [(" " + l if l.strip() else "") for l in body]
|
|
192
|
+
parts = []
|
|
193
|
+
if imports:
|
|
194
|
+
parts.append("\n".join(imports) + "\n")
|
|
195
|
+
if helpers:
|
|
196
|
+
parts.append("\n".join(helpers) + "\n")
|
|
197
|
+
parts.append("def build_slide_requests(html_str, assets_dir, page_id):")
|
|
198
|
+
parts.extend(indented)
|
|
199
|
+
parts.append(" return requests")
|
|
200
|
+
code = "\n".join(parts)
|
|
201
|
+
|
|
202
|
+
if "import os" not in code:
|
|
203
|
+
code = "import os\nimport json\nimport uuid\n\n" + code
|
|
204
|
+
|
|
205
|
+
for smart, straight in {"‘": "'", "’": "'", "“": '"', "”": '"'}.items():
|
|
206
|
+
code = code.replace(smart, straight)
|
|
207
|
+
|
|
208
|
+
if "alpha" in code:
|
|
209
|
+
code = _ALPHA_PAT.sub("", code)
|
|
210
|
+
if "'paragraphStyle'" in code:
|
|
211
|
+
code = code.replace("'paragraphStyle'", "'style'")
|
|
212
|
+
if '"paragraphStyle"' in code:
|
|
213
|
+
code = code.replace('"paragraphStyle"', '"style"')
|
|
214
|
+
if "'textStyle'" in code:
|
|
215
|
+
code = code.replace("'textStyle'", "'style'")
|
|
216
|
+
if '"textStyle"' in code:
|
|
217
|
+
code = code.replace('"textStyle"', '"style"')
|
|
218
|
+
|
|
219
|
+
code = _fix_apostrophe_strings(code)
|
|
220
|
+
return code
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# src/services/converter_jail/gslides_runner.py
|
|
2
|
+
"""In-jail trusted runner for Google Slides conversion (SDR-4437 HIGH-5).
|
|
3
|
+
|
|
4
|
+
Runs each generated build_slide_requests(html_str, assets_dir, page_id)
|
|
5
|
+
snippet and collects the returned request lists into a JSON file. NO network,
|
|
6
|
+
NO credentials. The host validates, uploads assets, substitutes placeholder
|
|
7
|
+
URLs, and executes the requests.
|
|
8
|
+
|
|
9
|
+
CLI: python -I gslides_runner.py <src_parent> <job_dir> <requests_out_path>
|
|
10
|
+
(<src_parent> is part of the launcher's CLI contract; this runner does NOT
|
|
11
|
+
put it on sys.path — importing anything via `src.services` would execute that
|
|
12
|
+
package's heavy __init__ and drag the credential-capable SDK graph into the
|
|
13
|
+
jail child. Siblings are imported as top-level modules instead.)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import importlib.util
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
_HERE = Path(__file__).resolve().parent
|
|
23
|
+
|
|
24
|
+
if __package__ in (None, ""):
|
|
25
|
+
# Script mode (inside the jail child): import siblings as top-level
|
|
26
|
+
# modules so src/services/__init__.py (SDK/agent graph) never executes.
|
|
27
|
+
sys.path.insert(0, str(_HERE))
|
|
28
|
+
import protocol
|
|
29
|
+
from codeprep import prepare_requests_code
|
|
30
|
+
else:
|
|
31
|
+
# Imported on the trusted host (unit tests): normal package imports.
|
|
32
|
+
from src.services.converter_jail import protocol
|
|
33
|
+
from src.services.converter_jail.codeprep import prepare_requests_code
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _exec_snippet(code: str, html_str: str, assets_dir: str, page_id: str):
|
|
37
|
+
"""Run one snippet; return (requests_list_or_None, error_or_None)."""
|
|
38
|
+
code = prepare_requests_code(code)
|
|
39
|
+
tmp = Path(tempfile.mktemp(suffix=".py", prefix="jail_gslide_"))
|
|
40
|
+
tmp.write_text(code, encoding="utf-8")
|
|
41
|
+
try:
|
|
42
|
+
spec = importlib.util.spec_from_file_location("jail_gslide", str(tmp))
|
|
43
|
+
module = importlib.util.module_from_spec(spec)
|
|
44
|
+
spec.loader.exec_module(module)
|
|
45
|
+
requests = module.build_slide_requests(html_str, assets_dir, page_id)
|
|
46
|
+
if not isinstance(requests, list):
|
|
47
|
+
return None, f"build_slide_requests returned {type(requests).__name__}"
|
|
48
|
+
return requests, None
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
return None, repr(exc)
|
|
51
|
+
finally:
|
|
52
|
+
if tmp.exists():
|
|
53
|
+
tmp.unlink()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run(job_dir: str, requests_out_path: str) -> None:
|
|
57
|
+
manifest = json.loads((Path(job_dir) / protocol.MANIFEST_NAME).read_text())
|
|
58
|
+
out = []
|
|
59
|
+
for entry in manifest["slides"]:
|
|
60
|
+
i = entry["index"]
|
|
61
|
+
page_id = entry["page_id"]
|
|
62
|
+
sdir = Path(job_dir) / entry["dir"]
|
|
63
|
+
assets_dir = str(sdir / protocol.ASSETS_DIR)
|
|
64
|
+
if not entry.get("has_code"):
|
|
65
|
+
out.append({"index": i, "page_id": page_id, "requests": None,
|
|
66
|
+
"error": "no_code"})
|
|
67
|
+
continue
|
|
68
|
+
html_str = (sdir / protocol.HTML_NAME).read_text(encoding="utf-8")
|
|
69
|
+
code = (sdir / protocol.CODE_NAME).read_text(encoding="utf-8")
|
|
70
|
+
requests, error = _exec_snippet(code, html_str, assets_dir, page_id)
|
|
71
|
+
out.append({"index": i, "page_id": page_id,
|
|
72
|
+
"requests": requests, "error": error})
|
|
73
|
+
Path(requests_out_path).write_text(json.dumps(out))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
# argv: [runner, src_parent, job_dir, requests_out_path]
|
|
78
|
+
run(sys.argv[2], sys.argv[3])
|