@salesforce/afv-skills 1.14.0 → 1.16.0
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.
- package/package.json +1 -1
- package/skills/activating-datacloud/SKILL.md +0 -1
- package/skills/analyzing-omnistudio-dependencies/SKILL.md +0 -1
- package/skills/applying-slds/SKILL.md +322 -0
- package/skills/applying-slds/checklists.md +83 -0
- package/skills/applying-slds/examples.md +283 -0
- package/skills/applying-slds/guidance/README.md +83 -0
- package/skills/applying-slds/guidance/blueprints-index.md +213 -0
- package/skills/applying-slds/guidance/icons-guidance.md +186 -0
- package/skills/applying-slds/guidance/overviews/borders.md +236 -0
- package/skills/applying-slds/guidance/overviews/color.md +266 -0
- package/skills/applying-slds/guidance/overviews/display-density.md +366 -0
- package/skills/applying-slds/guidance/overviews/icons.md +240 -0
- package/skills/applying-slds/guidance/overviews/illustrations.md +235 -0
- package/skills/applying-slds/guidance/overviews/shadows.md +176 -0
- package/skills/applying-slds/guidance/overviews/spacing.md +216 -0
- package/skills/applying-slds/guidance/overviews/typography.md +323 -0
- package/skills/applying-slds/guidance/overviews/utilities.md +542 -0
- package/skills/applying-slds/guidance/slds-development-guide.md +288 -0
- package/skills/applying-slds/guidance/styling-hooks/borders.md +202 -0
- package/skills/applying-slds/guidance/styling-hooks/color/expressive-palette-hooks.md +153 -0
- package/skills/applying-slds/guidance/styling-hooks/color/index.md +171 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/accent-hooks.md +204 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/feedback-hooks.md +768 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/surface-hooks.md +337 -0
- package/skills/applying-slds/guidance/styling-hooks/color/system-hooks.md +132 -0
- package/skills/applying-slds/guidance/styling-hooks/index.md +327 -0
- package/skills/applying-slds/guidance/styling-hooks/shadows.md +238 -0
- package/skills/applying-slds/guidance/styling-hooks/spacing.md +254 -0
- package/skills/applying-slds/guidance/styling-hooks/typography.md +448 -0
- package/skills/applying-slds/guidance/utilities/alignment.md +119 -0
- package/skills/applying-slds/guidance/utilities/borders.md +131 -0
- package/skills/applying-slds/guidance/utilities/box.md +125 -0
- package/skills/applying-slds/guidance/utilities/color.md +165 -0
- package/skills/applying-slds/guidance/utilities/dark-mode.md +111 -0
- package/skills/applying-slds/guidance/utilities/description-list.md +168 -0
- package/skills/applying-slds/guidance/utilities/floats.md +117 -0
- package/skills/applying-slds/guidance/utilities/grid.md +264 -0
- package/skills/applying-slds/guidance/utilities/horizontal-list.md +110 -0
- package/skills/applying-slds/guidance/utilities/hyphenation.md +84 -0
- package/skills/applying-slds/guidance/utilities/index.md +205 -0
- package/skills/applying-slds/guidance/utilities/interactions.md +89 -0
- package/skills/applying-slds/guidance/utilities/layout.md +109 -0
- package/skills/applying-slds/guidance/utilities/line-clamp.md +131 -0
- package/skills/applying-slds/guidance/utilities/margin.md +155 -0
- package/skills/applying-slds/guidance/utilities/media-object.md +161 -0
- package/skills/applying-slds/guidance/utilities/name-value-list.md +152 -0
- package/skills/applying-slds/guidance/utilities/padding.md +155 -0
- package/skills/applying-slds/guidance/utilities/position.md +177 -0
- package/skills/applying-slds/guidance/utilities/print.md +114 -0
- package/skills/applying-slds/guidance/utilities/scrollable.md +126 -0
- package/skills/applying-slds/guidance/utilities/sizing.md +190 -0
- package/skills/applying-slds/guidance/utilities/themes.md +121 -0
- package/skills/applying-slds/guidance/utilities/truncate.md +127 -0
- package/skills/applying-slds/guidance/utilities/typography.md +166 -0
- package/skills/applying-slds/guidance/utilities/vertical-list.md +166 -0
- package/skills/applying-slds/guidance/utilities/visibility.md +228 -0
- package/skills/applying-slds/metadata/README.md +84 -0
- package/skills/applying-slds/metadata/blueprints/components/accordion.yaml +304 -0
- package/skills/applying-slds/metadata/blueprints/components/activity-timeline.yaml +92 -0
- package/skills/applying-slds/metadata/blueprints/components/alert.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/app-launcher.yaml +94 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar-group.yaml +81 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/badges.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/brand-band.yaml +198 -0
- package/skills/applying-slds/metadata/blueprints/components/breadcrumbs.yaml +95 -0
- package/skills/applying-slds/metadata/blueprints/components/builder-header.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/button-groups.yaml +82 -0
- package/skills/applying-slds/metadata/blueprints/components/button-icons.yaml +295 -0
- package/skills/applying-slds/metadata/blueprints/components/buttons.yaml +230 -0
- package/skills/applying-slds/metadata/blueprints/components/cards.yaml +124 -0
- package/skills/applying-slds/metadata/blueprints/components/carousel.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/chat.yaml +179 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button-group.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button.yaml +204 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-toggle.yaml +177 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/color-picker.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/combobox.yaml +136 -0
- package/skills/applying-slds/metadata/blueprints/components/counter.yaml +147 -0
- package/skills/applying-slds/metadata/blueprints/components/data-tables.yaml +157 -0
- package/skills/applying-slds/metadata/blueprints/components/datepickers.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/datetime-picker.yaml +155 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-composer.yaml +201 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-form-footer.yaml +161 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-utility-bar.yaml +175 -0
- package/skills/applying-slds/metadata/blueprints/components/drop-zone.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/dueling-picklist.yaml +196 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-icons.yaml +128 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-menu.yaml +141 -0
- package/skills/applying-slds/metadata/blueprints/components/expandable-section.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/expression.yaml +143 -0
- package/skills/applying-slds/metadata/blueprints/components/feeds.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/file-selector.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/files.yaml +119 -0
- package/skills/applying-slds/metadata/blueprints/components/form-element.yaml +145 -0
- package/skills/applying-slds/metadata/blueprints/components/global-header.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/global-navigation.yaml +100 -0
- package/skills/applying-slds/metadata/blueprints/components/icons.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/illustration.yaml +205 -0
- package/skills/applying-slds/metadata/blueprints/components/input.yaml +151 -0
- package/skills/applying-slds/metadata/blueprints/components/list-builder.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/lookups.yaml +132 -0
- package/skills/applying-slds/metadata/blueprints/components/map.yaml +118 -0
- package/skills/applying-slds/metadata/blueprints/components/menus.yaml +134 -0
- package/skills/applying-slds/metadata/blueprints/components/modals.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/notifications.yaml +88 -0
- package/skills/applying-slds/metadata/blueprints/components/page-headers.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/panels.yaml +149 -0
- package/skills/applying-slds/metadata/blueprints/components/path.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/picklist.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/pills.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/popovers.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-bar.yaml +110 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-indicator.yaml +133 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-ring.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/prompt.yaml +126 -0
- package/skills/applying-slds/metadata/blueprints/components/publishers.yaml +178 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-button-group.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-group.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/rich-text-editor.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-notifications.yaml +188 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-tabs.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/select.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/setup-assistant.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/slider.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/spinners.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/split-view.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/summary-detail.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/tabs.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/textarea.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/tiles.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/timepicker.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/toast.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/tooltips.yaml +107 -0
- package/skills/applying-slds/metadata/blueprints/components/tree-grid.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trees.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trial-bar.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-navigation.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-tabs.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/visual-picker.yaml +150 -0
- package/skills/applying-slds/metadata/blueprints/components/welcome-mat.yaml +136 -0
- package/skills/applying-slds/metadata/hooks-index.json +6272 -0
- package/skills/applying-slds/metadata/icon-metadata.json +38466 -0
- package/skills/applying-slds/metadata/utilities-index.json +21912 -0
- package/skills/applying-slds/references/component-selection.md +112 -0
- package/skills/applying-slds/references/icons-decision-guide.md +124 -0
- package/skills/applying-slds/references/styling-decision-guide.md +228 -0
- package/skills/applying-slds/references/utilities-quick-ref.md +125 -0
- package/skills/applying-slds/scripts/search-blueprints.cjs +117 -0
- package/skills/applying-slds/scripts/search-hooks.cjs +139 -0
- package/skills/applying-slds/scripts/search-icons.cjs +174 -0
- package/skills/applying-slds/scripts/search-utilities.cjs +161 -0
- package/skills/building-mobile-apps/SKILL.md +0 -1
- package/skills/building-omnistudio-callable-apex/SKILL.md +0 -1
- package/skills/building-omnistudio-datamapper/SKILL.md +0 -1
- package/skills/building-omnistudio-flexcard/SKILL.md +0 -1
- package/skills/building-omnistudio-integration-procedure/SKILL.md +0 -1
- package/skills/building-omnistudio-omniscript/SKILL.md +0 -1
- package/skills/building-sf-integrations/SKILL.md +0 -1
- package/skills/configuring-connected-apps/SKILL.md +0 -1
- package/skills/connecting-datacloud/SKILL.md +0 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +0 -1
- package/skills/debugging-apex-logs/SKILL.md +0 -1
- package/skills/deploying-metadata/SKILL.md +0 -1
- package/skills/deploying-omnistudio-datapacks/SKILL.md +0 -1
- package/skills/developing-agentforce/SKILL.md +0 -1
- package/skills/fetching-salesforce-docs/SKILL.md +0 -1
- package/skills/generating-custom-lightning-type/SKILL.md +17 -39
- package/skills/generating-custom-lightning-type/assets/primitive-types-and-constraints.md +41 -0
- package/skills/generating-custom-lightning-type/references/widget-rendition.md +124 -0
- package/skills/generating-lwc-components/SKILL.md +0 -1
- package/skills/generating-mermaid-diagrams/SKILL.md +0 -1
- package/skills/generating-visual-diagrams/SKILL.md +0 -1
- package/skills/handling-sf-data/SKILL.md +0 -1
- package/skills/harmonizing-datacloud/SKILL.md +0 -1
- package/skills/integrating-b2b-commerce-open-code-components/SKILL.md +0 -1
- package/skills/investigating-agentforce-architecture/README.md +156 -0
- package/skills/investigating-agentforce-architecture/SKILL.md +230 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_sobject.yaml +16 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_tooling_sobject.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/list_metadata_genaiprompttemplate.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/org_display.yaml +15 -0
- package/skills/investigating-agentforce-architecture/assets/cli/retrieve_genai_plugin.yaml +18 -0
- package/skills/investigating-agentforce-architecture/assets/cli/show_access_token.yaml +27 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/action_tree.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/data_flow.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/dependency_graph.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/invocation_sequence.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/planner_state.mmd +18 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_definition_details.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_version_lookup.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_ids_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_view_by_durable_ids.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_metadata_by_id.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/functions_by_plugins.soql +5 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_attrs_by_parent_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_bundle_functions.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_definition_by_agent_chain.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_functions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_instructions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugins_by_planner.soql +4 -0
- package/skills/investigating-agentforce-architecture/references/architecture_sections.md +243 -0
- package/skills/investigating-agentforce-architecture/references/contract.json +244 -0
- package/skills/investigating-agentforce-architecture/references/soql_fields.md +512 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/__init__.py +1 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/fs_guard.py +329 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/paths.py +110 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/sql.py +10 -0
- package/skills/investigating-agentforce-architecture/scripts/cache_check.py +234 -0
- package/skills/investigating-agentforce-architecture/scripts/config.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/fetch_soql.py +689 -0
- package/skills/investigating-agentforce-architecture/scripts/finalize.py +295 -0
- package/skills/investigating-agentforce-architecture/scripts/main.py +2835 -0
- package/skills/investigating-agentforce-architecture/scripts/metadata_listing.py +265 -0
- package/skills/investigating-agentforce-architecture/scripts/parallel_retrieve.py +69 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_bundle.py +215 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_wave.py +845 -0
- package/skills/investigating-agentforce-architecture/scripts/probe_channels.py +302 -0
- package/skills/investigating-agentforce-architecture/scripts/render_architecture.py +1043 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_bot.py +255 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_invocation_target.py +130 -0
- package/skills/investigating-agentforce-architecture/scripts/rest_client.py +763 -0
- package/skills/investigating-agentforce-architecture/scripts/retrieve_planner.py +13 -0
- package/skills/investigating-agentforce-architecture/scripts/sf_cli.py +242 -0
- package/skills/investigating-agentforce-architecture/scripts/soql_loader.py +253 -0
- package/skills/investigating-agentforce-architecture/scripts/summarize_tree.py +143 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/_bootstrap.py +23 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/genai_payloads.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check.py +307 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check_main.py +283 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_config.py +115 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_end_to_end_fixture.py +651 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py +278 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_flow_children_inflation.py +582 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_fs_guard.py +113 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_iterative_wave_b.py +478 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_main_pipeline.py +3359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parallel_retrieve.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_bundle.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave.py +644 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_classifiers.py +224 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_helpers.py +380 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py +397 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_per_branch_visited.py +244 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_channels.py +359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_cli_recipes.py +185 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_render_architecture.py +810 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_bot.py +203 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_creds.py +157 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_invocation_target.py +145 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_rest_client.py +1253 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_runtime_override.py +100 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_sf_cli.py +261 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_signature_stamping.py +466 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_soql_loader.py +501 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_summarize_tree.py +241 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_write_emit_ctx.py +480 -0
- package/skills/investigating-agentforce-architecture/tools/emit_env.py +157 -0
- package/skills/investigating-agentforce-architecture/tools/emit_result.py +262 -0
- package/skills/investigating-agentforce-architecture/tools/sanitize.py +33 -0
- package/skills/investigating-agentforce-architecture/tools/write_emit_ctx.py +332 -0
- package/skills/investigating-agentforce-d360/README.md +123 -0
- package/skills/investigating-agentforce-d360/SKILL.md +163 -0
- package/skills/investigating-agentforce-d360/assets/dc/app_generation.sql +51 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_category.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_quality.sql +41 -0
- package/skills/investigating-agentforce-d360/assets/dc/discover_sessions.sql +36 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback.sql +47 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback_details.sql +38 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_records.sql +45 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_llm.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_metadata.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_tags.sql +42 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_requests.sql +89 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_responses.sql +43 -0
- package/skills/investigating-agentforce-d360/assets/dc/generations.sql +52 -0
- package/skills/investigating-agentforce-d360/assets/dc/interactions.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messages.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messaging_session.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/moment_interactions.sql +34 -0
- package/skills/investigating-agentforce-d360/assets/dc/moments.sql +39 -0
- package/skills/investigating-agentforce-d360/assets/dc/participants.sql +48 -0
- package/skills/investigating-agentforce-d360/assets/dc/sessions.sql +78 -0
- package/skills/investigating-agentforce-d360/assets/dc/steps.sql +64 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_associations.sql +46 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definition_associations.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definitions.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/tags.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/telemetry_spans.sql +55 -0
- package/skills/investigating-agentforce-d360/references/artifacts.md +50 -0
- package/skills/investigating-agentforce-d360/references/dc_dmo_fields.md +823 -0
- package/skills/investigating-agentforce-d360/references/dc_pipeline_contract.md +608 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/__init__.py +2 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/cli_override.py +98 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/fs_guard.py +334 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/paths.py +155 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/sql.py +14 -0
- package/skills/investigating-agentforce-d360/scripts/assemble_dc.py +1624 -0
- package/skills/investigating-agentforce-d360/scripts/config.py +45 -0
- package/skills/investigating-agentforce-d360/scripts/dc.py +188 -0
- package/skills/investigating-agentforce-d360/scripts/discover_sessions.py +556 -0
- package/skills/investigating-agentforce-d360/scripts/fetch_dc.py +1045 -0
- package/skills/investigating-agentforce-d360/scripts/render_dc.py +1750 -0
- package/skills/investigating-agentforce-d360/scripts/resolve_session.py +264 -0
- package/skills/investigating-agentforce-d360/scripts/storage.py +92 -0
- package/skills/investigating-agentforce-d360/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/_bootstrap.py +15 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/synthetic_session.py +424 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_bootstrap_and_mode.py +115 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct_integration.py +158 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py +287 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_integration.py +247 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_dc_and_resolve_session.py +433 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions.py +458 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions_grep_ci.py +193 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_helpers.py +266 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_identity.py +528 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_main.py +251 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall.py +229 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall_full.py +283 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_identity_coherence.py +327 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_branches.py +256 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_gateway_direct.py +130 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_helpers.py +291 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_integration.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_planner_llm_calls.py +284 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_show_prompts_gating.py +215 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_from_disk.py +100 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_session_main.py +149 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_runtime_override.py +104 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape.py +95 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape_dropped_by_stdm.py +85 -0
- package/skills/managing-managed-event-subscription/SKILL.md +152 -0
- package/skills/managing-managed-event-subscription/assets/managed-event-subscription-template.xml +20 -0
- package/skills/managing-managed-event-subscription/references/delete-guide.md +57 -0
- package/skills/managing-managed-event-subscription/references/topic-name-formats.md +26 -0
- package/skills/managing-managed-event-subscription/references/update-constraints.md +30 -0
- package/skills/modeling-omnistudio-epc-catalog/SKILL.md +0 -1
- package/skills/observing-agentforce/SKILL.md +0 -1
- package/skills/orchestrating-datacloud/SKILL.md +0 -1
- package/skills/preparing-datacloud/SKILL.md +0 -1
- package/skills/querying-soql/SKILL.md +0 -1
- package/skills/retrieving-datacloud/SKILL.md +0 -1
- package/skills/running-apex-tests/SKILL.md +0 -1
- package/skills/running-code-analyzer/SKILL.md +0 -1
- package/skills/segmenting-datacloud/SKILL.md +0 -1
- package/skills/testing-agentforce/SKILL.md +0 -1
- package/skills/uplifting-components-to-slds2/SKILL.md +3 -2
- package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +30 -9
- package/skills/uplifting-components-to-slds2/references/examples.md +24 -6
- package/skills/validating-slds/SKILL.md +262 -0
- package/skills/validating-slds/references/quality-checks.md +308 -0
- package/skills/validating-slds/references/report-format.md +302 -0
- package/skills/validating-slds/scripts/analyze-quality.cjs +521 -0
|
@@ -0,0 +1,1624 @@
|
|
|
1
|
+
"""Assemble dc._session_tree.json from fetched DC artifacts.
|
|
2
|
+
|
|
3
|
+
Given `DATA_ROOT/<sid>/dc.*.json` + `dc._session_manifest.json` (produced by
|
|
4
|
+
`scripts/fetch_dc.py`), this module joins the rows in memory and emits:
|
|
5
|
+
|
|
6
|
+
- dc._session_tree.json — session-rooted hierarchical view
|
|
7
|
+
(Interaction → Step → Generation →
|
|
8
|
+
GatewayRequest, with audit rows nested)
|
|
9
|
+
|
|
10
|
+
The human-readable markdown summary is produced by a separate stage,
|
|
11
|
+
`scripts/render_dc.py`, which reads only the tree.
|
|
12
|
+
|
|
13
|
+
Design contract (see references/dc_pipeline_contract.md):
|
|
14
|
+
|
|
15
|
+
- No DMO fetches. Pure in-memory compute over already-fetched artifacts.
|
|
16
|
+
- Driven off `manifest["queries"][*]["name"]` — adding a 25th DMO to
|
|
17
|
+
fetch_dc.py doesn't require changes here (it just won't be placed in
|
|
18
|
+
the tree until the logic is extended).
|
|
19
|
+
- Declared binding chain nests GatewayRequest under LLM_STEP via
|
|
20
|
+
`Step.ssot__GenerationId__c → Generation → GatewayResponse → Request`.
|
|
21
|
+
- Chain-orphan GW calls fall through to a timestamp-window rule
|
|
22
|
+
(tier dominates: ACTION → TOPIC → TRUST_GUARDRAILS → any other;
|
|
23
|
+
innermost Step wins within a tier).
|
|
24
|
+
- PK collisions and parse warnings surface in `counts.*`, not stderr-only.
|
|
25
|
+
|
|
26
|
+
Invocation:
|
|
27
|
+
python3 scripts/assemble_dc.py --session <sid>
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import functools
|
|
33
|
+
import html
|
|
34
|
+
import json
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
from collections import defaultdict
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any, Dict, Iterable, List, Literal, Optional, Set, Tuple
|
|
42
|
+
|
|
43
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
44
|
+
|
|
45
|
+
from config import DATA_ROOT, paths
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---- sentinels + constants -------------------------------------------------
|
|
49
|
+
|
|
50
|
+
_NOT_SET = {"", "NOT_SET", None}
|
|
51
|
+
_INTERNAL_TRACE_RE = re.compile(r'"internalTraceId":"([a-f0-9]+)"') # @rule-suppress starter-sec-002 — re.compile, not eval/exec
|
|
52
|
+
|
|
53
|
+
# Real, non-placeholder agent version. Matches the canonical `^v[0-9]+$`
|
|
54
|
+
# shape that paths.session_dir requires AND excludes the `v0` placeholder
|
|
55
|
+
# stamped by fetch_dc's MyAgent fallback (fetch_dc.py:570-597).
|
|
56
|
+
# Used by `_promote_identity` to decide whether session_identity carries
|
|
57
|
+
# a richer agent_version that should win over a manifest placeholder.
|
|
58
|
+
_REAL_VERSION_RE = re.compile(r'^v[0-9]+$') # @rule-suppress starter-sec-002 — re.compile, not eval/exec
|
|
59
|
+
|
|
60
|
+
# Tier order for timestamp-window fallback. "any other" is an implicit last-resort
|
|
61
|
+
# catch-all covering LLM_STEP (without declared binding), SESSION_END, and any
|
|
62
|
+
# future step types not explicitly listed.
|
|
63
|
+
_TIER_ORDER = ("ACTION_STEP", "TOPIC_STEP", "TRUST_GUARDRAILS_STEP")
|
|
64
|
+
|
|
65
|
+
# Canonical identity-field name → ordered list of `gateway_request_tags.tag__c`
|
|
66
|
+
# values that carry it, tried in order. Agent versions emit different tag
|
|
67
|
+
# names: newer Atlas ReAct agents use `agent_developer_name` /
|
|
68
|
+
# `agent_version_api_name`; legacy MyAgent builds omit the developer
|
|
69
|
+
# name entirely and use the unprefixed `version_api_name`. First non-null
|
|
70
|
+
# value wins. A single-element list means "no fallback known."
|
|
71
|
+
_TAG_KEY_ALIASES: Dict[str, Tuple[str, ...]] = {
|
|
72
|
+
"agent_api_name": ("agent_developer_name",),
|
|
73
|
+
"agent_version": ("agent_version_api_name", "version_api_name"),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---- typed namespaces ------------------------------------------------------
|
|
78
|
+
#
|
|
79
|
+
# Four frozen dataclasses replace the former dict-bags. frozen=True guards
|
|
80
|
+
# attribute re-assignment, not mutation of the dict values themselves — the
|
|
81
|
+
# producers are responsible for handing in plain dicts (not defaultdicts) so
|
|
82
|
+
# downstream helpers don't rely on auto-creation the type doesn't promise.
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class Indexes:
|
|
86
|
+
interactions_by_id: Dict[str, dict]
|
|
87
|
+
participants_by_id: Dict[str, dict]
|
|
88
|
+
generations_by_id: Dict[str, dict]
|
|
89
|
+
gw_req_by_id: Dict[str, dict]
|
|
90
|
+
gw_resp_by_resp_id: Dict[str, dict]
|
|
91
|
+
feedback_by_id: Dict[str, dict]
|
|
92
|
+
gw_resp_by_req_id: Dict[str, List[dict]]
|
|
93
|
+
steps_by_interaction: Dict[str, List[dict]]
|
|
94
|
+
messages_by_interaction: Dict[str, List[dict]]
|
|
95
|
+
gw_tags_by_parent: Dict[str, List[dict]]
|
|
96
|
+
gw_md_by_parent: Dict[str, List[dict]]
|
|
97
|
+
gw_llm_by_parent: Dict[str, List[dict]]
|
|
98
|
+
quality_by_parent: Dict[str, List[dict]]
|
|
99
|
+
quality_by_id: Dict[str, dict]
|
|
100
|
+
feedback_by_gen: Dict[str, List[dict]]
|
|
101
|
+
feedback_details_by_parent: Dict[str, List[dict]]
|
|
102
|
+
participant_role_by_id: Dict[str, Optional[str]]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class PolymorphicSplits:
|
|
107
|
+
categories_by_generation: Dict[str, List[dict]]
|
|
108
|
+
categories_by_quality: Dict[str, List[dict]]
|
|
109
|
+
gw_records_by_gw_req: Dict[str, List[dict]]
|
|
110
|
+
gw_records_by_feedback: Dict[str, List[dict]]
|
|
111
|
+
tag_assoc_session: List[dict]
|
|
112
|
+
tag_assoc_by_interaction: Dict[str, List[dict]]
|
|
113
|
+
tag_assoc_by_moment: Dict[str, List[dict]]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class BindingResults:
|
|
118
|
+
declared_gw_ids: Set[str]
|
|
119
|
+
declared_steps_with_gw: frozenset
|
|
120
|
+
step_id_to_gw_id: Dict[str, Optional[str]]
|
|
121
|
+
declared_collisions: int
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class Catalog:
|
|
126
|
+
agents_observed: List[str]
|
|
127
|
+
tag_definitions: List[dict]
|
|
128
|
+
tag_definition_associations: List[dict]
|
|
129
|
+
tags: List[dict]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class BinderCtx:
|
|
134
|
+
"""Per-interaction scratch state used only by the timestamp-window pass.
|
|
135
|
+
|
|
136
|
+
Kept in a parallel `Dict[iid, BinderCtx]` instead of stashed on the
|
|
137
|
+
interaction view, so binder state can never leak into the emitted tree.
|
|
138
|
+
"""
|
|
139
|
+
start_ts: Optional[datetime]
|
|
140
|
+
end_ts: Optional[datetime]
|
|
141
|
+
steps_with_ts: List[Tuple[dict, Optional[datetime], Optional[datetime]]]
|
|
142
|
+
reserved_step_ids: frozenset
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---- session-dir resolution ----------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _find_session_dir(sid: str) -> Path:
|
|
148
|
+
"""Locate the session dir under the nested layout.
|
|
149
|
+
|
|
150
|
+
Given only a session id, we don't know ``(org, agent, version)`` upfront.
|
|
151
|
+
Strategy:
|
|
152
|
+
|
|
153
|
+
1. Validate ``sid`` against ``paths.SESSION_ID_RE`` at entry. ``sid`` comes
|
|
154
|
+
in via argv / resolve_session and flows directly into path composition
|
|
155
|
+
(``<org>/_sessions/<sid>.link``) and glob patterns; an unvalidated
|
|
156
|
+
value here would undo the traversal guard added in PR #657 BLOCKER-2.
|
|
157
|
+
2. Breadcrumb lookup: ``DATA_ROOT/<org>/_sessions/<sid>.link`` is a plain-
|
|
158
|
+
text relative-path pointer written by ``storage.save``. Iterate all
|
|
159
|
+
orgs, read each ``.link``, resolve against the breadcrumb's parent,
|
|
160
|
+
and **enforce containment** — a tampered or stale breadcrumb whose
|
|
161
|
+
target escapes ``DATA_ROOT`` is skipped (not raised) so a single
|
|
162
|
+
malicious breadcrumb can't DoS the whole resolver. Falls through to
|
|
163
|
+
the glob fallback on any breadcrumb miss.
|
|
164
|
+
3. Glob fallback: ``DATA_ROOT/*/*/<sid>/`` — ``sid`` is now validated at
|
|
165
|
+
entry so the pattern is fixed-depth and cannot glob outside its
|
|
166
|
+
intended 2-level subtree.
|
|
167
|
+
4. Raise ``SystemExit`` with a clear hint to run fetch_dc.py first.
|
|
168
|
+
|
|
169
|
+
Returns the absolute directory path.
|
|
170
|
+
"""
|
|
171
|
+
# Validate first — rejects "../etc", "a/b", "", None, and control chars.
|
|
172
|
+
# ``sid`` from here on is safe to use as a path segment and as the tail
|
|
173
|
+
# of a fixed-depth glob.
|
|
174
|
+
paths.validate_session_id(sid)
|
|
175
|
+
root = paths.DATA_ROOT
|
|
176
|
+
if root.is_dir():
|
|
177
|
+
# Resolve DATA_ROOT once so containment checks don't repeat the walk.
|
|
178
|
+
root_resolved = root.resolve()
|
|
179
|
+
for org_dir in root.iterdir():
|
|
180
|
+
if not org_dir.is_dir():
|
|
181
|
+
continue
|
|
182
|
+
link = org_dir / "_sessions" / f"{sid}.link"
|
|
183
|
+
if link.is_file():
|
|
184
|
+
try:
|
|
185
|
+
rel = link.read_text().strip()
|
|
186
|
+
except OSError:
|
|
187
|
+
continue
|
|
188
|
+
# Resolve relative to the breadcrumb's parent (_sessions/),
|
|
189
|
+
# then enforce that the resolved path stays inside
|
|
190
|
+
# DATA_ROOT. ``.link`` contents are user-writable — a planted
|
|
191
|
+
# breadcrumb with ``../../../../etc/passwd`` must NOT pivot
|
|
192
|
+
# the assembler outside the plugin's data tree.
|
|
193
|
+
target = (link.parent / rel).resolve()
|
|
194
|
+
if not target.is_relative_to(root_resolved):
|
|
195
|
+
# Stale or malicious breadcrumb. Skip — fall through to
|
|
196
|
+
# the glob fallback rather than raising, so one bad
|
|
197
|
+
# breadcrumb in one org doesn't block discovery in
|
|
198
|
+
# another.
|
|
199
|
+
continue
|
|
200
|
+
if target.is_dir():
|
|
201
|
+
return target
|
|
202
|
+
# Glob fallback. ``sid`` is validated; the pattern is fixed-depth.
|
|
203
|
+
matches = list(root.glob(f"*/*/{sid}"))
|
|
204
|
+
if len(matches) == 1:
|
|
205
|
+
return matches[0]
|
|
206
|
+
if len(matches) > 1:
|
|
207
|
+
# Placeholder agent dirs (leading-underscore name like
|
|
208
|
+
# ``<org>/_agent_<botid>__v0/``) mark provisional sessions
|
|
209
|
+
# whose identity wasn't fully resolved at first write. When a
|
|
210
|
+
# real (agent, version) dir also exists for the same session,
|
|
211
|
+
# it's the authoritative home; the placeholder is stale.
|
|
212
|
+
# Prefer the real dir.
|
|
213
|
+
real = [p for p in matches if not p.parent.name.startswith("_")]
|
|
214
|
+
if len(real) == 1:
|
|
215
|
+
return real[0]
|
|
216
|
+
raise SystemExit(
|
|
217
|
+
f"assemble_dc: session {sid} resolves to {len(matches)} dirs "
|
|
218
|
+
f"under {root} — ambiguous. Check for duplicate sessions "
|
|
219
|
+
f"across agents."
|
|
220
|
+
)
|
|
221
|
+
raise SystemExit(
|
|
222
|
+
f"assemble_dc: session dir for {sid} not found under {root}; "
|
|
223
|
+
f"run fetch_dc.py first"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---- loaders ---------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def _load(session_dir: Path, name: str, parse_warnings: List[str]) -> List[dict]:
|
|
230
|
+
"""Load dc.<name>.json from session_dir. Missing → []. Malformed → [] + warning."""
|
|
231
|
+
p = session_dir / f"dc.{name}.json"
|
|
232
|
+
if not p.is_file():
|
|
233
|
+
return []
|
|
234
|
+
try:
|
|
235
|
+
return json.loads(p.read_text())
|
|
236
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
237
|
+
print(f"assemble_dc: WARN dc.{name}.json unreadable: {str(e).splitlines()[0]}",
|
|
238
|
+
file=sys.stderr)
|
|
239
|
+
parse_warnings.append(name)
|
|
240
|
+
return []
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _load_manifest(session_dir: Path) -> dict:
|
|
244
|
+
p = session_dir / "dc._session_manifest.json"
|
|
245
|
+
if not p.is_file():
|
|
246
|
+
raise SystemExit(
|
|
247
|
+
f"assemble_dc: manifest not found at {p}; run fetch_dc.py first"
|
|
248
|
+
)
|
|
249
|
+
manifest = json.loads(p.read_text())
|
|
250
|
+
if "queries" not in manifest:
|
|
251
|
+
raise SystemExit("assemble_dc: manifest schema changed — no 'queries' key")
|
|
252
|
+
return manifest
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _load_all(sid: str) -> Tuple[dict, Dict[str, List[dict]], List[str], Path]:
|
|
256
|
+
"""Return (manifest, rows_by_name, parse_warnings, session_dir).
|
|
257
|
+
|
|
258
|
+
Iterates manifest["queries"][*]["name"] rather than a hard-coded list —
|
|
259
|
+
a new DMO added to fetch_dc.py is picked up automatically.
|
|
260
|
+
"""
|
|
261
|
+
session_dir = _find_session_dir(sid)
|
|
262
|
+
manifest = _load_manifest(session_dir)
|
|
263
|
+
parse_warnings: List[str] = []
|
|
264
|
+
rows = {
|
|
265
|
+
q["name"]: _load(session_dir, q["name"], parse_warnings)
|
|
266
|
+
for q in manifest["queries"]
|
|
267
|
+
}
|
|
268
|
+
return manifest, rows, parse_warnings, session_dir
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---- small helpers ---------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def _clean(value: Any) -> Any:
|
|
274
|
+
"""NOT_SET sentinel → None. Other values pass through."""
|
|
275
|
+
return None if value in _NOT_SET else value
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _harvest_str(value: Any) -> Optional[str]:
|
|
279
|
+
"""Harvest-layer string normalizer for the session-identity block.
|
|
280
|
+
|
|
281
|
+
Handles three quirks that `_clean` deliberately does not:
|
|
282
|
+
1. **html.unescape** — tag values arrive double-escaped
|
|
283
|
+
(`""0Xx…""`).
|
|
284
|
+
2. **Quote-strip** — after unescape most tag values are wrapped in
|
|
285
|
+
literal `"` characters; strip them.
|
|
286
|
+
3. **`UNSET_VALUE` sentinel** — Data Cloud emits this on a small set
|
|
287
|
+
of optional columns (e.g. `gateway_requests.promptTemplateVersionNo__c`,
|
|
288
|
+
certain `tag_first` values on cold-start sessions). Not observed
|
|
289
|
+
on the columns `_build_session_identity` currently reads, but
|
|
290
|
+
included defensively since the sentinel is part of the DC schema
|
|
291
|
+
contract and a harvest-layer reader should collapse it to None.
|
|
292
|
+
|
|
293
|
+
The binding / index layer uses `_clean()` / `_NOT_SET` which
|
|
294
|
+
intentionally omits these rules — they would be noise there.
|
|
295
|
+
"""
|
|
296
|
+
if value is None:
|
|
297
|
+
return None
|
|
298
|
+
s = html.unescape(str(value)).strip()
|
|
299
|
+
if len(s) >= 2 and s.startswith('"') and s.endswith('"'):
|
|
300
|
+
s = s[1:-1]
|
|
301
|
+
return None if s in ("", "NOT_SET", "UNSET_VALUE") else s
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _promote_identity(
|
|
305
|
+
manifest_value: Any,
|
|
306
|
+
session_identity_value: Any,
|
|
307
|
+
*,
|
|
308
|
+
kind: Literal["api_name", "version"],
|
|
309
|
+
) -> Any:
|
|
310
|
+
"""Pick the richer of (manifest, session_identity) for a top-level identity slot.
|
|
311
|
+
|
|
312
|
+
Background. ``fetch_dc._resolve_identity`` walks AGENT-role participant
|
|
313
|
+
rows for ``(api_name, version)``. On agent shapes like MyAgent the
|
|
314
|
+
AGENT rows can leave both fields NOT_SET, so the resolver falls back
|
|
315
|
+
(fetch_dc.py:570-597) to picking ``api_name`` from any participant row
|
|
316
|
+
and stamping ``version="v0"`` as a placeholder satisfying
|
|
317
|
+
``paths.session_dir``'s ``^v[0-9]+$`` shape. The placeholder is enough
|
|
318
|
+
to land the session in a directory, but it's wrong: by wave 5 the
|
|
319
|
+
fetch has materialized ``gateway_request_tags`` rows carrying the real
|
|
320
|
+
``agent_version_api_name`` (e.g. ``v24``), and ``_build_session_identity``
|
|
321
|
+
correctly harvests it onto ``session.identity``. The top-level
|
|
322
|
+
``identity`` block was previously copied verbatim from the manifest,
|
|
323
|
+
so the placeholder leaked downstream while the right value sat
|
|
324
|
+
visible in the same JSON.
|
|
325
|
+
|
|
326
|
+
Policy:
|
|
327
|
+
|
|
328
|
+
- ``kind="version"``: if manifest is the ``"v0"`` placeholder AND
|
|
329
|
+
session_identity carries a non-``v0`` value matching ``^v[0-9]+$``,
|
|
330
|
+
promote. Otherwise keep manifest.
|
|
331
|
+
- ``kind="api_name"``: if manifest is NOT_SET-ish (None / "" / "NOT_SET"
|
|
332
|
+
/ "NOT SET") AND session_identity has a real value, promote.
|
|
333
|
+
Otherwise keep manifest. (Crucially, when session_identity is None
|
|
334
|
+
and manifest has a value, we keep manifest — the strict AGENT-row
|
|
335
|
+
pick is intentional on healthy sessions.)
|
|
336
|
+
- When manifest and session_identity both carry real-but-disagreeing
|
|
337
|
+
values, the manifest wins. The strict AGENT-row pick is the
|
|
338
|
+
authoritative source on a normal session; we only promote in the
|
|
339
|
+
narrow case where the manifest carries a known placeholder /
|
|
340
|
+
NOT_SET sentinel and the harvest layer has something better.
|
|
341
|
+
"""
|
|
342
|
+
if kind == "version":
|
|
343
|
+
if manifest_value != "v0":
|
|
344
|
+
return manifest_value
|
|
345
|
+
if not isinstance(session_identity_value, str):
|
|
346
|
+
return manifest_value
|
|
347
|
+
if session_identity_value == "v0":
|
|
348
|
+
return manifest_value
|
|
349
|
+
if not _REAL_VERSION_RE.match(session_identity_value):
|
|
350
|
+
return manifest_value
|
|
351
|
+
return session_identity_value
|
|
352
|
+
# kind == "api_name"
|
|
353
|
+
if manifest_value not in (None, "", "NOT_SET", "NOT SET"):
|
|
354
|
+
return manifest_value
|
|
355
|
+
if isinstance(session_identity_value, str) and session_identity_value:
|
|
356
|
+
return session_identity_value
|
|
357
|
+
return manifest_value
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _reconcile_top_identity(
|
|
361
|
+
manifest: dict, session_identity: dict, org_id_15: Any,
|
|
362
|
+
) -> dict:
|
|
363
|
+
"""Build the top-level ``identity`` block, promoting placeholders.
|
|
364
|
+
|
|
365
|
+
Centralizes the policy in one place so the happy path
|
|
366
|
+
(`_assemble_session`) and the gateway-direct fallback don't drift
|
|
367
|
+
apart. Emits a stderr note when promotion fires so an investigator
|
|
368
|
+
sees the divergence at run time.
|
|
369
|
+
"""
|
|
370
|
+
manifest_api = manifest.get("agent_api_name")
|
|
371
|
+
manifest_ver = manifest.get("agent_version")
|
|
372
|
+
session_api = session_identity.get("agent_api_name")
|
|
373
|
+
session_ver = session_identity.get("agent_version")
|
|
374
|
+
|
|
375
|
+
promoted_api = _promote_identity(manifest_api, session_api, kind="api_name")
|
|
376
|
+
promoted_ver = _promote_identity(manifest_ver, session_ver, kind="version")
|
|
377
|
+
|
|
378
|
+
if promoted_api != manifest_api:
|
|
379
|
+
print(
|
|
380
|
+
f"assemble_dc: identity promoted: agent_api_name "
|
|
381
|
+
f"{manifest_api!r} -> {promoted_api!r} (from session.identity)",
|
|
382
|
+
file=sys.stderr,
|
|
383
|
+
)
|
|
384
|
+
if promoted_ver != manifest_ver:
|
|
385
|
+
print(
|
|
386
|
+
f"assemble_dc: identity promoted: agent_version "
|
|
387
|
+
f"{manifest_ver} -> {promoted_ver} (from session.identity)",
|
|
388
|
+
file=sys.stderr,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
"org_id_15": org_id_15,
|
|
393
|
+
"agent_api_name": promoted_api,
|
|
394
|
+
"agent_version": promoted_ver,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _resolve_end_type(session_row: dict, rows: dict) -> Optional[str]:
|
|
399
|
+
"""Resolve the session's terminal outcome with a Session→Step fallback.
|
|
400
|
+
|
|
401
|
+
Session DMO's ``ssot__AiAgentSessionEndType__c`` is authoritative when
|
|
402
|
+
populated, but on Messaging-channel and short E&O sessions it stays
|
|
403
|
+
``NOT_SET`` even after the SESSION_END interaction has materialized.
|
|
404
|
+
The runtime writes the actual outcome (``CLOSED_USER_REQUEST``,
|
|
405
|
+
``USER_ENDED``, ``ESCALATED``, ``TRANSFERRED``, ``TIMEOUT``) onto the
|
|
406
|
+
SESSION_END step's ``ssot__Name__c`` instead. Fall through to that
|
|
407
|
+
step when Session.EndType is missing, so the rendered summary stops
|
|
408
|
+
saying "session end not yet materialized in STDM" for sessions that
|
|
409
|
+
actually completed cleanly.
|
|
410
|
+
"""
|
|
411
|
+
primary = _clean(session_row.get("ssot__AiAgentSessionEndType__c"))
|
|
412
|
+
if primary:
|
|
413
|
+
return primary
|
|
414
|
+
for step in rows.get("steps", []) or ():
|
|
415
|
+
if step.get("ssot__AiAgentInteractionStepType__c") == "SESSION_END":
|
|
416
|
+
name = _clean(step.get("ssot__Name__c"))
|
|
417
|
+
if name:
|
|
418
|
+
return name
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _ts(value: Any) -> Optional[datetime]:
|
|
423
|
+
"""Parse an ISO-8601 timestamp. NOT_SET / non-string → None (unbounded)."""
|
|
424
|
+
if not isinstance(value, str) or value in _NOT_SET:
|
|
425
|
+
return None
|
|
426
|
+
try:
|
|
427
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
428
|
+
except ValueError:
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _index_unique(rows: Iterable[dict], key: str,
|
|
433
|
+
collisions: List[dict], dmo_label: str) -> Dict[str, dict]:
|
|
434
|
+
"""Build a {key_value: row} dict. On collision: first-write-wins + record."""
|
|
435
|
+
out: Dict[str, dict] = {}
|
|
436
|
+
for r in rows:
|
|
437
|
+
k = r.get(key)
|
|
438
|
+
if k in _NOT_SET:
|
|
439
|
+
continue
|
|
440
|
+
if k in out:
|
|
441
|
+
collisions.append({"dmo": dmo_label, "key": k})
|
|
442
|
+
else:
|
|
443
|
+
out[k] = r
|
|
444
|
+
return out
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _groupby(rows: Iterable[dict], key: str) -> Dict[str, List[dict]]:
|
|
448
|
+
out: Dict[str, List[dict]] = defaultdict(list)
|
|
449
|
+
for r in rows:
|
|
450
|
+
k = r.get(key)
|
|
451
|
+
if k not in _NOT_SET:
|
|
452
|
+
out[k].append(r)
|
|
453
|
+
return dict(out)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _extract_trace_id(interaction: dict) -> Optional[str]:
|
|
457
|
+
"""Prefer the primary column; fall back to AttributeText regex."""
|
|
458
|
+
tid = interaction.get("ssot__TelemetryTraceId__c")
|
|
459
|
+
if tid and tid not in _NOT_SET:
|
|
460
|
+
return tid
|
|
461
|
+
attr = interaction.get("ssot__AttributeText__c") or ""
|
|
462
|
+
if not attr:
|
|
463
|
+
return None
|
|
464
|
+
m = _INTERNAL_TRACE_RE.search(html.unescape(attr))
|
|
465
|
+
return m.group(1) if m else None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ---- declared binding chain ------------------------------------------------
|
|
469
|
+
|
|
470
|
+
def _declared_gw_for_step(step: dict,
|
|
471
|
+
generations_by_id: Dict[str, dict],
|
|
472
|
+
gw_resp_by_resp_id: Dict[str, dict],
|
|
473
|
+
gw_req_by_id: Dict[str, dict]) -> Optional[dict]:
|
|
474
|
+
"""Step → Generation → Response → Request. Returns the GatewayRequest row or None."""
|
|
475
|
+
gen_id = step.get("ssot__GenerationId__c")
|
|
476
|
+
if gen_id in _NOT_SET:
|
|
477
|
+
return None
|
|
478
|
+
gen = generations_by_id.get(gen_id)
|
|
479
|
+
if not gen:
|
|
480
|
+
return None
|
|
481
|
+
resp_id = gen.get("generationResponseId__c")
|
|
482
|
+
if resp_id in _NOT_SET:
|
|
483
|
+
return None
|
|
484
|
+
resp = gw_resp_by_resp_id.get(resp_id)
|
|
485
|
+
if not resp:
|
|
486
|
+
return None
|
|
487
|
+
req_id = resp.get("generationRequestId__c")
|
|
488
|
+
if req_id in _NOT_SET:
|
|
489
|
+
return None
|
|
490
|
+
return gw_req_by_id.get(req_id)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ---- timestamp-window fallback --------------------------------------------
|
|
494
|
+
|
|
495
|
+
def _tier(step_type: str) -> int:
|
|
496
|
+
"""Lower is better. 0 = ACTION, 1 = TOPIC, 2 = GUARDRAIL, 3 = any other."""
|
|
497
|
+
try:
|
|
498
|
+
return _TIER_ORDER.index(step_type)
|
|
499
|
+
except ValueError:
|
|
500
|
+
return len(_TIER_ORDER) # "any other"
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _window_contains(gw_ts: datetime, start: Optional[datetime],
|
|
504
|
+
end: Optional[datetime]) -> bool:
|
|
505
|
+
"""Closed-closed containment. None end_ts → +∞. Missing start → no match."""
|
|
506
|
+
if start is None:
|
|
507
|
+
return False
|
|
508
|
+
if gw_ts < start:
|
|
509
|
+
return False
|
|
510
|
+
if end is None:
|
|
511
|
+
return True # open-ended upward
|
|
512
|
+
return gw_ts <= end
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _bind_ts_window(gw_req: dict,
|
|
516
|
+
steps_with_ts: List[Tuple[dict, datetime, Optional[datetime]]],
|
|
517
|
+
interaction_window: Tuple[Optional[datetime], Optional[datetime]],
|
|
518
|
+
reserved_step_ids: "frozenset[str]") -> Tuple[str, Optional[str]]:
|
|
519
|
+
"""Return (placement, bound_step_id).
|
|
520
|
+
|
|
521
|
+
placement ∈ {"step", "interaction", "unbound"}.
|
|
522
|
+
bound_step_id is set only when placement == "step".
|
|
523
|
+
"""
|
|
524
|
+
gw_ts_raw = gw_req.get("timestamp__c")
|
|
525
|
+
gw_ts = _ts(gw_ts_raw)
|
|
526
|
+
if gw_ts is None:
|
|
527
|
+
return ("unbound", None)
|
|
528
|
+
|
|
529
|
+
# Step candidates: contains gw_ts AND not already declared-bound.
|
|
530
|
+
candidates = [
|
|
531
|
+
(step, start, end)
|
|
532
|
+
for step, start, end in steps_with_ts
|
|
533
|
+
if step["ssot__Id__c"] not in reserved_step_ids
|
|
534
|
+
and _window_contains(gw_ts, start, end)
|
|
535
|
+
]
|
|
536
|
+
if candidates:
|
|
537
|
+
# Best tier → innermost (shortest window) → latest start_ts.
|
|
538
|
+
def sort_key(c):
|
|
539
|
+
step, start, end = c
|
|
540
|
+
tier = _tier(step.get("ssot__AiAgentInteractionStepType__c", ""))
|
|
541
|
+
# Window size; treat None end_ts as "longest" (so nested closed wins).
|
|
542
|
+
if end is None:
|
|
543
|
+
width = float("inf")
|
|
544
|
+
else:
|
|
545
|
+
width = (end - start).total_seconds()
|
|
546
|
+
# Invert latest-start-wins by negating seconds since epoch.
|
|
547
|
+
return (tier, width, -start.timestamp())
|
|
548
|
+
candidates.sort(key=sort_key)
|
|
549
|
+
winner = candidates[0][0]
|
|
550
|
+
return ("step", winner["ssot__Id__c"])
|
|
551
|
+
|
|
552
|
+
# Interaction window.
|
|
553
|
+
i_start, i_end = interaction_window
|
|
554
|
+
if _window_contains(gw_ts, i_start, i_end):
|
|
555
|
+
return ("interaction", None)
|
|
556
|
+
return ("unbound", None)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# ---- gateway-request view builder -----------------------------------------
|
|
560
|
+
|
|
561
|
+
def _build_gw_view(gw_req: dict, binding_method: str, *,
|
|
562
|
+
idx: Indexes, dispatch: PolymorphicSplits,
|
|
563
|
+
bound_step_id: Optional[str] = None) -> dict:
|
|
564
|
+
"""Build one GatewayRequest view row.
|
|
565
|
+
|
|
566
|
+
`idx` and `dispatch` are kw-only so `functools.partial(_build_gw_view,
|
|
567
|
+
idx=..., dispatch=...)` bindings don't collide with positional args at
|
|
568
|
+
the 3 call sites (declared / timestamp_window / unbound).
|
|
569
|
+
"""
|
|
570
|
+
gw_id = gw_req["gatewayRequestId__c"]
|
|
571
|
+
responses = idx.gw_resp_by_req_id.get(gw_id, [])
|
|
572
|
+
view: Dict[str, Any] = {
|
|
573
|
+
"binding_method": binding_method,
|
|
574
|
+
"gateway_request_id": gw_id,
|
|
575
|
+
"feature": _clean(gw_req.get("feature__c")),
|
|
576
|
+
"model": _clean(gw_req.get("model__c")),
|
|
577
|
+
"provider": _clean(gw_req.get("provider__c")),
|
|
578
|
+
"prompt_template_dev_name": _clean(gw_req.get("promptTemplateDevName__c")),
|
|
579
|
+
"prompt_tokens": gw_req.get("promptTokens__c"),
|
|
580
|
+
"completion_tokens": gw_req.get("completionTokens__c"),
|
|
581
|
+
"total_tokens": gw_req.get("totalTokens__c"),
|
|
582
|
+
# Carry the raw input prompt through the hierarchical view so the
|
|
583
|
+
# renderer can surface it in the opt-in "Planner LLM calls" section.
|
|
584
|
+
# The 64 KB display cap lives in render_dc, not here — the tree
|
|
585
|
+
# stores the authoritative payload.
|
|
586
|
+
"prompt_text": gw_req.get("prompt__c"),
|
|
587
|
+
"response": responses[0] if responses else None,
|
|
588
|
+
"tags": idx.gw_tags_by_parent.get(gw_id, []),
|
|
589
|
+
"records": dispatch.gw_records_by_gw_req.get(gw_id, []),
|
|
590
|
+
"metadata": idx.gw_md_by_parent.get(gw_id, []),
|
|
591
|
+
"llm": idx.gw_llm_by_parent.get(gw_id, []),
|
|
592
|
+
}
|
|
593
|
+
if bound_step_id is not None:
|
|
594
|
+
view["bound_to_step_id"] = bound_step_id
|
|
595
|
+
return view
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# ---- main assembly ---------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
def _build_indexes(rows: Dict[str, List[dict]], collisions: List[dict]) -> Indexes:
|
|
601
|
+
"""Build all primary-key dicts and groupby tables. Returns a frozen Indexes."""
|
|
602
|
+
return Indexes(
|
|
603
|
+
interactions_by_id=_index_unique(
|
|
604
|
+
rows.get("interactions", []), "ssot__Id__c", collisions, "interactions_by_id"),
|
|
605
|
+
participants_by_id=_index_unique(
|
|
606
|
+
rows.get("participants", []), "ssot__Id__c", collisions, "participants_by_id"),
|
|
607
|
+
generations_by_id=_index_unique(
|
|
608
|
+
rows.get("generations", []), "generationId__c", collisions, "generations_by_id"),
|
|
609
|
+
gw_req_by_id=_index_unique(
|
|
610
|
+
rows.get("gateway_requests", []), "gatewayRequestId__c", collisions, "gw_req_by_id"),
|
|
611
|
+
gw_resp_by_resp_id=_index_unique(
|
|
612
|
+
rows.get("gateway_responses", []), "generationResponseId__c",
|
|
613
|
+
collisions, "gw_resp_by_resp_id"),
|
|
614
|
+
feedback_by_id=_index_unique(
|
|
615
|
+
rows.get("feedback", []), "feedbackId__c", collisions, "feedback_by_id"),
|
|
616
|
+
gw_resp_by_req_id=_groupby(rows.get("gateway_responses", []), "generationRequestId__c"),
|
|
617
|
+
steps_by_interaction=_groupby(rows.get("steps", []), "ssot__AiAgentInteractionId__c"),
|
|
618
|
+
messages_by_interaction=_groupby(rows.get("messages", []), "ssot__AiAgentInteractionId__c"),
|
|
619
|
+
gw_tags_by_parent=_groupby(rows.get("gateway_request_tags", []), "parent__c"),
|
|
620
|
+
gw_md_by_parent=_groupby(rows.get("gateway_request_metadata", []), "parent__c"),
|
|
621
|
+
gw_llm_by_parent=_groupby(rows.get("gateway_request_llm", []), "parent__c"),
|
|
622
|
+
quality_by_parent=_groupby(rows.get("content_quality", []), "parent__c"),
|
|
623
|
+
quality_by_id={q["id__c"]: q for q in rows.get("content_quality", []) if q.get("id__c")},
|
|
624
|
+
feedback_by_gen=_groupby(rows.get("feedback", []), "generationId__c"),
|
|
625
|
+
feedback_details_by_parent=_groupby(rows.get("feedback_details", []), "parent__c"),
|
|
626
|
+
participant_role_by_id={
|
|
627
|
+
p["ssot__Id__c"]: p.get("ssot__AiAgentSessionParticipantRole__c")
|
|
628
|
+
for p in rows.get("participants", []) if p.get("ssot__Id__c")
|
|
629
|
+
},
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _dispatch_polymorphic(rows: Dict[str, List[dict]], idx: Indexes) -> PolymorphicSplits:
|
|
634
|
+
"""Split ContentCategory, GtwyObjRecord, and TagAssociation by polymorphic parent.
|
|
635
|
+
|
|
636
|
+
Producers accumulate into defaultdicts for ergonomics, but the returned
|
|
637
|
+
dataclass stores plain dicts — frozen `Dict[...]` typing can't promise
|
|
638
|
+
auto-creation, so don't let it leak.
|
|
639
|
+
"""
|
|
640
|
+
# ContentCategory: parent is either a generation or a quality row.
|
|
641
|
+
cat_by_gen: Dict[str, List[dict]] = defaultdict(list)
|
|
642
|
+
cat_by_qual: Dict[str, List[dict]] = defaultdict(list)
|
|
643
|
+
for cat in rows.get("content_category", []):
|
|
644
|
+
parent = cat.get("parent__c")
|
|
645
|
+
if parent in _NOT_SET:
|
|
646
|
+
continue
|
|
647
|
+
if parent in idx.generations_by_id:
|
|
648
|
+
cat_by_gen[parent].append(cat)
|
|
649
|
+
elif parent in idx.quality_by_id:
|
|
650
|
+
cat_by_qual[parent].append(cat)
|
|
651
|
+
|
|
652
|
+
# GtwyObjRecord: parent is either a gateway_request or a feedback row.
|
|
653
|
+
rec_by_gw: Dict[str, List[dict]] = defaultdict(list)
|
|
654
|
+
rec_by_fb: Dict[str, List[dict]] = defaultdict(list)
|
|
655
|
+
for rec in rows.get("gateway_records", []):
|
|
656
|
+
parent = rec.get("parent__c")
|
|
657
|
+
if parent in _NOT_SET:
|
|
658
|
+
continue
|
|
659
|
+
if parent in idx.gw_req_by_id:
|
|
660
|
+
rec_by_gw[parent].append(rec)
|
|
661
|
+
elif parent in idx.feedback_by_id:
|
|
662
|
+
rec_by_fb[parent].append(rec)
|
|
663
|
+
|
|
664
|
+
# TagAssociation: exactly one of session/interaction/moment FK is populated.
|
|
665
|
+
ta_session: List[dict] = []
|
|
666
|
+
ta_by_int: Dict[str, List[dict]] = defaultdict(list)
|
|
667
|
+
ta_by_mom: Dict[str, List[dict]] = defaultdict(list)
|
|
668
|
+
for ta in rows.get("tag_associations", []):
|
|
669
|
+
if ta.get("ssot__AiAgentSessionId__c") not in _NOT_SET:
|
|
670
|
+
ta_session.append(ta)
|
|
671
|
+
elif ta.get("ssot__AiAgentInteractionId__c") not in _NOT_SET:
|
|
672
|
+
ta_by_int[ta["ssot__AiAgentInteractionId__c"]].append(ta)
|
|
673
|
+
elif ta.get("ssot__AiAgentMomentId__c") not in _NOT_SET:
|
|
674
|
+
ta_by_mom[ta["ssot__AiAgentMomentId__c"]].append(ta)
|
|
675
|
+
|
|
676
|
+
return PolymorphicSplits(
|
|
677
|
+
categories_by_generation=dict(cat_by_gen),
|
|
678
|
+
categories_by_quality=dict(cat_by_qual),
|
|
679
|
+
gw_records_by_gw_req=dict(rec_by_gw),
|
|
680
|
+
gw_records_by_feedback=dict(rec_by_fb),
|
|
681
|
+
tag_assoc_session=ta_session,
|
|
682
|
+
tag_assoc_by_interaction=dict(ta_by_int),
|
|
683
|
+
tag_assoc_by_moment=dict(ta_by_mom),
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _filter_catalog(rows: Dict[str, List[dict]]) -> Catalog:
|
|
688
|
+
"""Filter org-wide tag vocabulary to only what's reachable from session agents."""
|
|
689
|
+
agents = {
|
|
690
|
+
p.get("ssot__AiAgentApiName__c") for p in rows.get("participants", [])
|
|
691
|
+
if p.get("ssot__AiAgentSessionParticipantRole__c") == "AGENT"
|
|
692
|
+
and p.get("ssot__AiAgentApiName__c") not in _NOT_SET
|
|
693
|
+
} | {
|
|
694
|
+
m.get("ssot__AiAgentApiName__c") for m in rows.get("moments", [])
|
|
695
|
+
if m.get("ssot__AiAgentApiName__c") not in _NOT_SET
|
|
696
|
+
}
|
|
697
|
+
# Mirror fetch_dc._resolve_identity's USER-row fallback. On agent shapes
|
|
698
|
+
# like MyAgent, AGENT-role rows leave api_name=NOT_SET while USER
|
|
699
|
+
# rows correctly carry the agent's api_name. Without this fallback, the
|
|
700
|
+
# session lands in a `<api_name>__v0/` directory but `agents_observed`
|
|
701
|
+
# is empty — directory and rendered catalog disagree. Only fires when the
|
|
702
|
+
# primary (AGENT + moments) sources turned up nothing usable; in normal
|
|
703
|
+
# sessions the USER and AGENT rows agree and the union is idempotent.
|
|
704
|
+
if not agents:
|
|
705
|
+
agents = {
|
|
706
|
+
p.get("ssot__AiAgentApiName__c") for p in rows.get("participants", [])
|
|
707
|
+
if p.get("ssot__AiAgentApiName__c") not in _NOT_SET
|
|
708
|
+
}
|
|
709
|
+
agents_observed = sorted(a for a in agents if a)
|
|
710
|
+
relevant_assocs = [
|
|
711
|
+
a for a in rows.get("tag_definition_associations", [])
|
|
712
|
+
if a.get("ssot__AiAgentApiName__c") in agents_observed
|
|
713
|
+
]
|
|
714
|
+
relevant_def_ids = {
|
|
715
|
+
a["ssot__AiAgentTagDefinitionId__c"] for a in relevant_assocs
|
|
716
|
+
if a.get("ssot__AiAgentTagDefinitionId__c") not in _NOT_SET
|
|
717
|
+
}
|
|
718
|
+
return Catalog(
|
|
719
|
+
agents_observed=agents_observed,
|
|
720
|
+
tag_definitions=[d for d in rows.get("tag_definitions", [])
|
|
721
|
+
if d.get("ssot__Id__c") in relevant_def_ids],
|
|
722
|
+
tag_definition_associations=relevant_assocs,
|
|
723
|
+
tags=[t for t in rows.get("tags", [])
|
|
724
|
+
if t.get("ssot__AiAgentTagDefinitionId__c") in relevant_def_ids],
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _build_session_identity(rows: Dict[str, List[dict]], manifest: dict) -> dict:
|
|
729
|
+
"""Harvest 18 identity fields from 4 DMOs for the `session.identity` block.
|
|
730
|
+
|
|
731
|
+
All row iteration is preceded by a deterministic sort so repeated runs
|
|
732
|
+
produce byte-identical output regardless of fetch order. `_harvest_str()` is
|
|
733
|
+
the shared normalizer — html.unescape + quote-strip + NOT_SET /
|
|
734
|
+
UNSET_VALUE / empty coercion. See references/dc_pipeline_contract.md
|
|
735
|
+
§2.9a for the field-to-column mapping.
|
|
736
|
+
"""
|
|
737
|
+
# --- gateway_requests: sort by (timestamp__c, gatewayRequestId__c) ---
|
|
738
|
+
gwr_sorted = sorted(
|
|
739
|
+
rows.get("gateway_requests", []),
|
|
740
|
+
key=lambda r: (r.get("timestamp__c") or "",
|
|
741
|
+
r.get("gatewayRequestId__c") or ""),
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
def _first_gwr(key: str) -> Optional[str]:
|
|
745
|
+
for r in gwr_sorted:
|
|
746
|
+
v = _harvest_str(r.get(key))
|
|
747
|
+
if v is not None:
|
|
748
|
+
return v
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
org_id = _first_gwr("orgId__c")
|
|
752
|
+
platform_user_id = _first_gwr("userId__c")
|
|
753
|
+
planner_id = _first_gwr("plannerId__c")
|
|
754
|
+
bot_version_id = _first_gwr("botVersionId__c")
|
|
755
|
+
app_type = _first_gwr("appType__c")
|
|
756
|
+
|
|
757
|
+
# --- gateway_request_tags: sort by (parent__c, tag__c, tagValue__c) ---
|
|
758
|
+
tags_sorted = sorted(
|
|
759
|
+
rows.get("gateway_request_tags", []),
|
|
760
|
+
key=lambda r: (r.get("parent__c") or "",
|
|
761
|
+
r.get("tag__c") or "",
|
|
762
|
+
r.get("tagValue__c") or ""),
|
|
763
|
+
)
|
|
764
|
+
tag_first: Dict[str, Optional[str]] = {}
|
|
765
|
+
for t in tags_sorted:
|
|
766
|
+
k = t.get("tag__c")
|
|
767
|
+
if not k or k in tag_first:
|
|
768
|
+
continue
|
|
769
|
+
tag_first[k] = _harvest_str(t.get("tagValue__c"))
|
|
770
|
+
|
|
771
|
+
# --- sessions[0] — always exactly one row per session on this path ---
|
|
772
|
+
sessions = rows.get("sessions", [])
|
|
773
|
+
session_row = sessions[0] if sessions else {}
|
|
774
|
+
|
|
775
|
+
# --- participants: first USER-role row by ssot__Id__c ---
|
|
776
|
+
participants_sorted = sorted(
|
|
777
|
+
rows.get("participants", []),
|
|
778
|
+
key=lambda r: r.get("ssot__Id__c") or "",
|
|
779
|
+
)
|
|
780
|
+
messaging_end_user_id = None
|
|
781
|
+
for p in participants_sorted:
|
|
782
|
+
if p.get("ssot__AiAgentSessionParticipantRole__c") == "USER":
|
|
783
|
+
v = _harvest_str(p.get("ssot__ParticipantId__c"))
|
|
784
|
+
if v is not None:
|
|
785
|
+
messaging_end_user_id = v
|
|
786
|
+
break
|
|
787
|
+
|
|
788
|
+
def _aliased(identity_key: str) -> Optional[str]:
|
|
789
|
+
"""Resolve an identity field via its tag-name fallback chain.
|
|
790
|
+
|
|
791
|
+
Parameter name intentionally avoids shadowing `dataclasses.field`.
|
|
792
|
+
"""
|
|
793
|
+
for tag_key in _TAG_KEY_ALIASES[identity_key]:
|
|
794
|
+
v = tag_first.get(tag_key)
|
|
795
|
+
if v is not None:
|
|
796
|
+
return v
|
|
797
|
+
return None
|
|
798
|
+
|
|
799
|
+
# Expose VariableText__c bootstrap variables. Production messaging
|
|
800
|
+
# sessions leave this NOT_SET; Builder Previewer populates it with
|
|
801
|
+
# test-harness keys (__resolved_locale__, __supports_result_display__,
|
|
802
|
+
# etc.). Surfacing this is what makes channel-mode visible in the
|
|
803
|
+
# renderer.
|
|
804
|
+
messaging_session_id = _harvest_str(session_row.get("ssot__RelatedMessagingSessionId__c"))
|
|
805
|
+
voice_call_id = _harvest_str(session_row.get("ssot__RelatedVoiceCallId__c"))
|
|
806
|
+
bootstrap_variables = _parse_bootstrap_variables(
|
|
807
|
+
session_row.get("ssot__VariableText__c")
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Derive a `mode` field. ssot__AiAgentChannelType__c is identical for
|
|
811
|
+
# MIAW production and Builder Previewer (`SCRT2 - EmbeddedMessaging`);
|
|
812
|
+
# we have to look at related-id population and bootstrap_variables to
|
|
813
|
+
# tell them apart.
|
|
814
|
+
mode = _derive_mode(messaging_session_id, voice_call_id, bootstrap_variables)
|
|
815
|
+
|
|
816
|
+
# `voice_call_id` + `individual_id` are null on EmbeddedMessaging sessions.
|
|
817
|
+
# They populate on authenticated channels (voice, Experience Cloud with
|
|
818
|
+
# linked Individual). Kept for schema parallelism with messaging_session_id.
|
|
819
|
+
return {
|
|
820
|
+
"org_id": org_id,
|
|
821
|
+
"platform_user_id": platform_user_id,
|
|
822
|
+
"planner_id": planner_id,
|
|
823
|
+
"bot_version_id": bot_version_id,
|
|
824
|
+
"app_type": app_type,
|
|
825
|
+
"bot_id": tag_first.get("bot_id"),
|
|
826
|
+
"bot_name": tag_first.get("bot_name"),
|
|
827
|
+
"agent_api_name": _aliased("agent_api_name"),
|
|
828
|
+
"agent_label": tag_first.get("agent_label"),
|
|
829
|
+
"agent_version": _aliased("agent_version"),
|
|
830
|
+
"agent_type": tag_first.get("agent_type"),
|
|
831
|
+
"planner_name": tag_first.get("planner_name"),
|
|
832
|
+
"planner_type": tag_first.get("planner_type"),
|
|
833
|
+
"configured_model": tag_first.get("configured_model_name"),
|
|
834
|
+
"messaging_session_id": messaging_session_id,
|
|
835
|
+
"messaging_end_user_id": messaging_end_user_id,
|
|
836
|
+
"voice_call_id": voice_call_id,
|
|
837
|
+
"individual_id": _harvest_str(session_row.get("ssot__IndividualId__c")),
|
|
838
|
+
"bootstrap_variables": bootstrap_variables,
|
|
839
|
+
"mode": mode,
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
# Test-harness bootstrap keys that are observed in Builder Previewer sessions
|
|
844
|
+
# but NOT in MIAW production. The presence of any of these in
|
|
845
|
+
# `ssot__VariableText__c` is the strongest at-rest signal that a session was
|
|
846
|
+
# run through the Previewer rather than against a real customer messaging
|
|
847
|
+
# session. Listed here as a frozenset so it's read-only at module level.
|
|
848
|
+
# Builder Previewer adds these keys to ssot__VariableText__c at session
|
|
849
|
+
# bootstrap; MIAW production sessions don't seed them. Used by _derive_mode
|
|
850
|
+
# to distinguish previewer runs from real customer messaging sessions.
|
|
851
|
+
_BUILDER_PREVIEWER_INDICATOR_KEYS: frozenset[str] = frozenset({
|
|
852
|
+
"__resolved_locale__",
|
|
853
|
+
"__locale_instruction__",
|
|
854
|
+
"__supports_result_display__",
|
|
855
|
+
"__show_tool_results_invoked__",
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _parse_bootstrap_variables(raw: Any) -> Optional[dict]:
|
|
860
|
+
"""Parse `ssot__VariableText__c` defensively.
|
|
861
|
+
|
|
862
|
+
On real sessions this field can be:
|
|
863
|
+
- missing / None / NOT_SET / UNSET_VALUE → returns None
|
|
864
|
+
- well-formed JSON (Builder Previewer) → returns the dict
|
|
865
|
+
- HTML-entity-encoded JSON (some surfaces emit
|
|
866
|
+
the `"`-escaped form) → unescaped, returns the dict
|
|
867
|
+
- truncated or malformed JSON → returns
|
|
868
|
+
`{"_parse_error": True, "_raw": <first 200 chars>}` so the renderer
|
|
869
|
+
can still flag that a bootstrap exists, just not parseable.
|
|
870
|
+
|
|
871
|
+
Returns None for the empty cases so the caller can treat None as
|
|
872
|
+
"no bootstrap" without distinguishing missing from sentinel.
|
|
873
|
+
"""
|
|
874
|
+
if raw is None:
|
|
875
|
+
return None
|
|
876
|
+
s = html.unescape(str(raw)).strip()
|
|
877
|
+
if not s or s in _NOT_SET or s in ("NOT_SET", "UNSET_VALUE"):
|
|
878
|
+
return None
|
|
879
|
+
try:
|
|
880
|
+
parsed = json.loads(s)
|
|
881
|
+
except (json.JSONDecodeError, ValueError):
|
|
882
|
+
return {"_parse_error": True, "_raw": s[:200]}
|
|
883
|
+
# Defensive: VariableText__c is documented as a JSON object; if a future
|
|
884
|
+
# version emits a JSON array or scalar, surface it under `_raw` rather
|
|
885
|
+
# than letting downstream code crash on `.get()`.
|
|
886
|
+
if not isinstance(parsed, dict):
|
|
887
|
+
return {"_parse_error": True, "_raw": s[:200]}
|
|
888
|
+
return parsed
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _derive_mode(
|
|
892
|
+
messaging_session_id: Optional[str],
|
|
893
|
+
voice_call_id: Optional[str],
|
|
894
|
+
bootstrap_variables: Optional[dict],
|
|
895
|
+
) -> str:
|
|
896
|
+
"""Distinguish MIAW production from Builder Previewer from voice.
|
|
897
|
+
|
|
898
|
+
`ssot__AiAgentChannelType__c` is identical (`SCRT2 - EmbeddedMessaging`)
|
|
899
|
+
for MIAW and Builder Previewer — useless for distinguishing them. The
|
|
900
|
+
real signals, in priority order:
|
|
901
|
+
|
|
902
|
+
1. `ssot__RelatedVoiceCallId__c` set ↔ voice channel.
|
|
903
|
+
2. `ssot__RelatedMessagingSessionId__c` set ↔ MIAW production
|
|
904
|
+
(a real MessagingSession record exists).
|
|
905
|
+
3. `RelatedMessagingSessionId__c` NOT_SET AND bootstrap_variables
|
|
906
|
+
contains test-harness keys ↔ Builder Previewer.
|
|
907
|
+
4. None of the above ↔ unknown (e.g. headless API runs, agent script
|
|
908
|
+
previewer cases that don't seed VariableText__c).
|
|
909
|
+
|
|
910
|
+
Return a stable enum string the renderer can match on.
|
|
911
|
+
"""
|
|
912
|
+
if voice_call_id:
|
|
913
|
+
return "voice"
|
|
914
|
+
if messaging_session_id:
|
|
915
|
+
return "production_messaging"
|
|
916
|
+
if isinstance(bootstrap_variables, dict):
|
|
917
|
+
if set(bootstrap_variables) & _BUILDER_PREVIEWER_INDICATOR_KEYS:
|
|
918
|
+
return "builder_previewer"
|
|
919
|
+
return "unknown"
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _declared_binding_pass(rows: Dict[str, List[dict]], idx: Indexes) -> BindingResults:
|
|
923
|
+
"""Walk every step; claim GWs reachable via the declared chain.
|
|
924
|
+
|
|
925
|
+
Returns BindingResults(declared_gw_ids, declared_steps_with_gw,
|
|
926
|
+
step_id_to_gw_id, declared_collisions). `declared_steps_with_gw` is a
|
|
927
|
+
frozenset so downstream consumers can't accidentally mutate it.
|
|
928
|
+
`step_id_to_gw_id[step_id]` is the GW id when declared, or None when this
|
|
929
|
+
step's declared GW was already claimed by an earlier step (collision sentinel).
|
|
930
|
+
`declared_collisions` is the aggregate count of collision-sentinel entries.
|
|
931
|
+
"""
|
|
932
|
+
declared_gw_ids: set = set()
|
|
933
|
+
declared_steps_with_gw: set = set()
|
|
934
|
+
step_id_to_gw_id: Dict[str, Optional[str]] = {}
|
|
935
|
+
for step in rows.get("steps", []):
|
|
936
|
+
gw_req = _declared_gw_for_step(
|
|
937
|
+
step, idx.generations_by_id, idx.gw_resp_by_resp_id, idx.gw_req_by_id)
|
|
938
|
+
if gw_req is None:
|
|
939
|
+
continue
|
|
940
|
+
gw_id = gw_req["gatewayRequestId__c"]
|
|
941
|
+
if gw_id in declared_gw_ids:
|
|
942
|
+
# Collision: second+ step reaches a GW already claimed.
|
|
943
|
+
step_id_to_gw_id[step["ssot__Id__c"]] = None
|
|
944
|
+
continue
|
|
945
|
+
declared_gw_ids.add(gw_id)
|
|
946
|
+
declared_steps_with_gw.add(step["ssot__Id__c"])
|
|
947
|
+
step_id_to_gw_id[step["ssot__Id__c"]] = gw_id
|
|
948
|
+
return BindingResults(
|
|
949
|
+
declared_gw_ids=declared_gw_ids,
|
|
950
|
+
declared_steps_with_gw=frozenset(declared_steps_with_gw),
|
|
951
|
+
step_id_to_gw_id=step_id_to_gw_id,
|
|
952
|
+
declared_collisions=sum(1 for v in step_id_to_gw_id.values() if v is None),
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _build_step_view(step: dict, idx: Indexes, dispatch: PolymorphicSplits,
|
|
957
|
+
step_id_to_gw_id: Dict[str, Optional[str]],
|
|
958
|
+
build_gw) -> dict:
|
|
959
|
+
"""Emit one Step view, including its Generation and (if declared) GatewayRequest.
|
|
960
|
+
|
|
961
|
+
`build_gw` is a `functools.partial` pre-bound with `idx`/`dispatch`
|
|
962
|
+
(see `assemble()`); call sites only supply `gw_req`, `binding_method`,
|
|
963
|
+
and optionally `bound_step_id`.
|
|
964
|
+
"""
|
|
965
|
+
sid_step = step["ssot__Id__c"]
|
|
966
|
+
gen_id = step.get("ssot__GenerationId__c")
|
|
967
|
+
gen = idx.generations_by_id.get(gen_id) if gen_id not in _NOT_SET else None
|
|
968
|
+
generation_view = _build_generation_view(
|
|
969
|
+
gen, idx.quality_by_parent, dispatch.categories_by_generation,
|
|
970
|
+
dispatch.categories_by_quality, idx.feedback_by_gen,
|
|
971
|
+
idx.feedback_details_by_parent, dispatch.gw_records_by_feedback,
|
|
972
|
+
) if gen is not None else None
|
|
973
|
+
|
|
974
|
+
gw_id = step_id_to_gw_id.get(sid_step)
|
|
975
|
+
gw_view = None
|
|
976
|
+
collision_flag = gw_id is None and sid_step in step_id_to_gw_id
|
|
977
|
+
if gw_id is not None:
|
|
978
|
+
gw_view = build_gw(idx.gw_req_by_id[gw_id], "declared")
|
|
979
|
+
|
|
980
|
+
# Mirror the bound gateway_request's model identifier onto the step
|
|
981
|
+
# itself so renderers can show "LLM_STEP <name> · <model>" without
|
|
982
|
+
# dereferencing the nested gateway view. The mirror is None when no
|
|
983
|
+
# gateway_request is bound (declared chain didn't reach, or the STDM
|
|
984
|
+
# exporter dropped writes — see the `gateway_requests_dropped_by_stdm`
|
|
985
|
+
# session_shape).
|
|
986
|
+
step_model_name = gw_view.get("model") if gw_view else None
|
|
987
|
+
|
|
988
|
+
step_view: Dict[str, Any] = {
|
|
989
|
+
"id": sid_step,
|
|
990
|
+
"type": step.get("ssot__AiAgentInteractionStepType__c"),
|
|
991
|
+
"name": step.get("ssot__Name__c"),
|
|
992
|
+
"start_ts": step.get("ssot__StartTimestamp__c"),
|
|
993
|
+
"end_ts": step.get("ssot__EndTimestamp__c"),
|
|
994
|
+
"error_text": _clean(step.get("ssot__ErrorMessageText__c")),
|
|
995
|
+
"model_name": step_model_name,
|
|
996
|
+
"generation": generation_view,
|
|
997
|
+
"gateway_request": gw_view,
|
|
998
|
+
}
|
|
999
|
+
if collision_flag:
|
|
1000
|
+
step_view["gateway_request_collision"] = True
|
|
1001
|
+
return step_view
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _build_message_view(m: dict, participant_role_by_id: Dict[str, str]) -> dict:
|
|
1005
|
+
mtype = m.get("ssot__AiAgentInteractionMessageType__c")
|
|
1006
|
+
pid = m.get("ssot__AiAgentSessionParticipantId__c")
|
|
1007
|
+
role = participant_role_by_id.get(pid)
|
|
1008
|
+
if role is None:
|
|
1009
|
+
role = "USER" if mtype == "Input" else "AGENT" if mtype == "Output" else None
|
|
1010
|
+
return {
|
|
1011
|
+
"message_id": m.get("ssot__Id__c"),
|
|
1012
|
+
"type": mtype,
|
|
1013
|
+
"role": role,
|
|
1014
|
+
"participant_id": pid,
|
|
1015
|
+
"text": m.get("ssot__ContentText__c"),
|
|
1016
|
+
"content_type": m.get("ssot__AiAgentInteractionMsgContentType__c"),
|
|
1017
|
+
"modality": m.get("Modality__c"),
|
|
1018
|
+
"ts": m.get("ssot__MessageSentTimestamp__c"),
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _build_interaction_view(interaction: dict, rows: Dict[str, List[dict]],
|
|
1023
|
+
idx: Indexes, dispatch: PolymorphicSplits,
|
|
1024
|
+
binding: BindingResults,
|
|
1025
|
+
build_gw) -> Tuple[dict, BinderCtx]:
|
|
1026
|
+
"""Emit one Interaction view plus its BinderCtx.
|
|
1027
|
+
|
|
1028
|
+
Returns (view, binder_ctx). Binder scratch state lives in the `BinderCtx`
|
|
1029
|
+
and is keyed externally by `iid`; it never touches the emitted view, so
|
|
1030
|
+
it can't leak into `dc._session_tree.json`. `build_gw` is the
|
|
1031
|
+
`functools.partial` from `assemble()`.
|
|
1032
|
+
"""
|
|
1033
|
+
iid = interaction["ssot__Id__c"]
|
|
1034
|
+
trace_id = _extract_trace_id(interaction)
|
|
1035
|
+
|
|
1036
|
+
steps_sorted = sorted(
|
|
1037
|
+
idx.steps_by_interaction.get(iid, []),
|
|
1038
|
+
key=lambda s: (s.get("ssot__StartTimestamp__c") or "", s.get("ssot__Id__c") or ""))
|
|
1039
|
+
step_views = [_build_step_view(s, idx, dispatch, binding.step_id_to_gw_id, build_gw)
|
|
1040
|
+
for s in steps_sorted]
|
|
1041
|
+
|
|
1042
|
+
# Step windows consumed only by _ts_window_pass via the parallel BinderCtx.
|
|
1043
|
+
steps_with_ts: List[Tuple[dict, Optional[datetime], Optional[datetime]]] = [
|
|
1044
|
+
(s, _ts(s.get("ssot__StartTimestamp__c")), _ts(s.get("ssot__EndTimestamp__c")))
|
|
1045
|
+
for s in steps_sorted if _ts(s.get("ssot__StartTimestamp__c")) is not None
|
|
1046
|
+
]
|
|
1047
|
+
|
|
1048
|
+
messages_sorted = sorted(
|
|
1049
|
+
idx.messages_by_interaction.get(iid, []),
|
|
1050
|
+
key=lambda r: (r.get("ssot__MessageSentTimestamp__c") or "",
|
|
1051
|
+
r.get("ssot__Id__c") or ""))
|
|
1052
|
+
msg_views = [_build_message_view(m, idx.participant_role_by_id)
|
|
1053
|
+
for m in messages_sorted]
|
|
1054
|
+
|
|
1055
|
+
view = {
|
|
1056
|
+
"id": iid,
|
|
1057
|
+
"type": interaction.get("ssot__AiAgentInteractionType__c"),
|
|
1058
|
+
"topic": _clean(interaction.get("ssot__TopicApiName__c")),
|
|
1059
|
+
"trace_id": trace_id,
|
|
1060
|
+
"start_ts": interaction.get("ssot__StartTimestamp__c"),
|
|
1061
|
+
"end_ts": interaction.get("ssot__EndTimestamp__c"),
|
|
1062
|
+
"messages": msg_views,
|
|
1063
|
+
"telemetry_spans": [s for s in rows.get("telemetry_spans", [])
|
|
1064
|
+
if s.get("ssot__TelemetryTrace__c") == trace_id],
|
|
1065
|
+
"steps": step_views,
|
|
1066
|
+
"timestamp_bound_gateway_calls": [], # appended by _ts_window_pass
|
|
1067
|
+
"tag_associations": dispatch.tag_assoc_by_interaction.get(iid, []),
|
|
1068
|
+
}
|
|
1069
|
+
binder_ctx = BinderCtx(
|
|
1070
|
+
start_ts=_ts(interaction.get("ssot__StartTimestamp__c")),
|
|
1071
|
+
end_ts=_ts(interaction.get("ssot__EndTimestamp__c")),
|
|
1072
|
+
steps_with_ts=steps_with_ts,
|
|
1073
|
+
reserved_step_ids=binding.declared_steps_with_gw,
|
|
1074
|
+
)
|
|
1075
|
+
return view, binder_ctx
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _ts_window_pass(interactions_view: List[dict],
|
|
1079
|
+
binders: Dict[str, BinderCtx],
|
|
1080
|
+
idx: Indexes,
|
|
1081
|
+
binding: BindingResults,
|
|
1082
|
+
build_gw) -> Tuple[List[dict], dict]:
|
|
1083
|
+
"""Place every chain-orphan GW via timestamp-window, or into unbound[].
|
|
1084
|
+
|
|
1085
|
+
Reads per-interaction binder state from the parallel `binders` dict keyed
|
|
1086
|
+
by `iv["id"]` — never from the view itself. Mutates `interactions_view`
|
|
1087
|
+
in place only to append to `timestamp_bound_gateway_calls[]` (an emission
|
|
1088
|
+
field, pre-initialized to `[]` in `_build_interaction_view`).
|
|
1089
|
+
|
|
1090
|
+
Returns (unbound_gw_calls, gw_binding_counts).
|
|
1091
|
+
"""
|
|
1092
|
+
unbound: List[dict] = []
|
|
1093
|
+
counts = {
|
|
1094
|
+
"declared": len(binding.declared_gw_ids),
|
|
1095
|
+
"timestamp_window": 0,
|
|
1096
|
+
"unbound": 0,
|
|
1097
|
+
"declared_collisions": binding.declared_collisions,
|
|
1098
|
+
}
|
|
1099
|
+
for gw_id, gw_req in idx.gw_req_by_id.items():
|
|
1100
|
+
if gw_id in binding.declared_gw_ids:
|
|
1101
|
+
continue
|
|
1102
|
+
placed = False
|
|
1103
|
+
for iv in interactions_view:
|
|
1104
|
+
bctx = binders[iv["id"]]
|
|
1105
|
+
placement, bound_step_id = _bind_ts_window(
|
|
1106
|
+
gw_req,
|
|
1107
|
+
bctx.steps_with_ts,
|
|
1108
|
+
(bctx.start_ts, bctx.end_ts),
|
|
1109
|
+
bctx.reserved_step_ids,
|
|
1110
|
+
)
|
|
1111
|
+
if placement in ("step", "interaction"):
|
|
1112
|
+
iv["timestamp_bound_gateway_calls"].append(
|
|
1113
|
+
build_gw(gw_req, "timestamp_window", bound_step_id=bound_step_id))
|
|
1114
|
+
counts["timestamp_window"] += 1
|
|
1115
|
+
placed = True
|
|
1116
|
+
break
|
|
1117
|
+
if not placed:
|
|
1118
|
+
unbound.append(build_gw(gw_req, "unbound"))
|
|
1119
|
+
counts["unbound"] += 1
|
|
1120
|
+
|
|
1121
|
+
# Defense in depth: if anyone reintroduces the binder-cache-on-view
|
|
1122
|
+
# pattern in a future edit, this catches it before the tree ever writes.
|
|
1123
|
+
assert not any(k.startswith("_") for iv in interactions_view for k in iv), \
|
|
1124
|
+
"binder scratch state leaked into interaction view — do not stash on the view dict"
|
|
1125
|
+
|
|
1126
|
+
return unbound, counts
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _build_moments_view(rows: Dict[str, List[dict]], dispatch: PolymorphicSplits) -> List[dict]:
|
|
1130
|
+
"""session.moments[] with interaction_ids[] back-refs derived from MomentInteraction."""
|
|
1131
|
+
by_moment: Dict[str, List[str]] = defaultdict(list)
|
|
1132
|
+
for mi in rows.get("moment_interactions", []):
|
|
1133
|
+
mid = mi.get("ssot__AiAgentMomentId__c")
|
|
1134
|
+
iid = mi.get("ssot__AiAgentInteractionId__c")
|
|
1135
|
+
if mid not in _NOT_SET and iid not in _NOT_SET:
|
|
1136
|
+
by_moment[mid].append(iid)
|
|
1137
|
+
|
|
1138
|
+
moments_sorted = sorted(
|
|
1139
|
+
rows.get("moments", []),
|
|
1140
|
+
key=lambda r: (r.get("ssot__StartTimestamp__c") or "", r.get("ssot__Id__c") or ""))
|
|
1141
|
+
return [
|
|
1142
|
+
{
|
|
1143
|
+
"moment_id": m.get("ssot__Id__c"),
|
|
1144
|
+
"agent_api_name": _clean(m.get("ssot__AiAgentApiName__c")),
|
|
1145
|
+
"agent_version": _clean(m.get("ssot__AiAgentVersionApiName__c")),
|
|
1146
|
+
"request_summary_text": _clean(m.get("ssot__RequestSummaryText__c")),
|
|
1147
|
+
"response_summary_text": _clean(m.get("ssot__ResponseSummaryText__c")),
|
|
1148
|
+
"interaction_ids": sorted(by_moment.get(m.get("ssot__Id__c"), [])),
|
|
1149
|
+
"start_ts": m.get("ssot__StartTimestamp__c"),
|
|
1150
|
+
"end_ts": m.get("ssot__EndTimestamp__c"),
|
|
1151
|
+
"tag_associations": dispatch.tag_assoc_by_moment.get(m.get("ssot__Id__c"), []),
|
|
1152
|
+
}
|
|
1153
|
+
for m in moments_sorted
|
|
1154
|
+
]
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _build_participants_view(rows: Dict[str, List[dict]]) -> List[dict]:
|
|
1158
|
+
return [
|
|
1159
|
+
{
|
|
1160
|
+
"participant_id": p.get("ssot__Id__c"),
|
|
1161
|
+
"role": p.get("ssot__AiAgentSessionParticipantRole__c"),
|
|
1162
|
+
"agent_api_name": _clean(p.get("ssot__AiAgentApiName__c")),
|
|
1163
|
+
"agent_version": _clean(p.get("ssot__AiAgentVersionApiName__c")),
|
|
1164
|
+
"agent_type": _clean(p.get("ssot__AiAgentType__c")),
|
|
1165
|
+
}
|
|
1166
|
+
for p in sorted(
|
|
1167
|
+
rows.get("participants", []),
|
|
1168
|
+
key=lambda r: (r.get("ssot__StartTimestamp__c") or "", r.get("ssot__Id__c") or ""))
|
|
1169
|
+
]
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def _build_counts(rows: Dict[str, List[dict]], dispatch: PolymorphicSplits,
|
|
1173
|
+
binding_counts: dict,
|
|
1174
|
+
manifest: dict, collisions: List[dict],
|
|
1175
|
+
parse_warnings: List[str]) -> dict:
|
|
1176
|
+
int_by_type: Dict[str, int] = defaultdict(int)
|
|
1177
|
+
step_by_type: Dict[str, int] = defaultdict(int)
|
|
1178
|
+
for i in rows.get("interactions", []):
|
|
1179
|
+
int_by_type[i.get("ssot__AiAgentInteractionType__c")] += 1
|
|
1180
|
+
for s in rows.get("steps", []):
|
|
1181
|
+
step_by_type[s.get("ssot__AiAgentInteractionStepType__c")] += 1
|
|
1182
|
+
|
|
1183
|
+
return {
|
|
1184
|
+
"interactions_total": len(rows.get("interactions", [])),
|
|
1185
|
+
"interactions_turn": int_by_type.get("TURN", 0),
|
|
1186
|
+
"interactions_session_end": int_by_type.get("SESSION_END", 0),
|
|
1187
|
+
"steps_total": len(rows.get("steps", [])),
|
|
1188
|
+
"steps_by_type": {
|
|
1189
|
+
k: step_by_type.get(k, 0) for k in
|
|
1190
|
+
("LLM_STEP", "ACTION_STEP", "TOPIC_STEP", "TRUST_GUARDRAILS_STEP", "SESSION_END")
|
|
1191
|
+
},
|
|
1192
|
+
"generations": len(rows.get("generations", [])),
|
|
1193
|
+
"gateway_requests": len(rows.get("gateway_requests", [])),
|
|
1194
|
+
"gateway_responses": len(rows.get("gateway_responses", [])),
|
|
1195
|
+
"gateway_metadata": len(rows.get("gateway_request_metadata", [])),
|
|
1196
|
+
"gateway_llm": len(rows.get("gateway_request_llm", [])),
|
|
1197
|
+
"gateway_records_grounded": sum(len(v) for v in dispatch.gw_records_by_gw_req.values()),
|
|
1198
|
+
"gateway_records_feedback": sum(len(v) for v in dispatch.gw_records_by_feedback.values()),
|
|
1199
|
+
"feedback": len(rows.get("feedback", [])),
|
|
1200
|
+
"audit_chain_1to1_ok": (
|
|
1201
|
+
len(rows.get("gateway_requests", [])) == len(rows.get("gateway_responses", []))
|
|
1202
|
+
),
|
|
1203
|
+
"gw_binding": binding_counts,
|
|
1204
|
+
"session_shape": manifest.get("session_shape", "unknown"),
|
|
1205
|
+
"pk_collisions": collisions,
|
|
1206
|
+
"parse_warnings": parse_warnings,
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def assemble(sid: str) -> Tuple[dict, Path]:
|
|
1211
|
+
"""Orchestrate: load → index → dispatch → bind → build views → counts → tree.
|
|
1212
|
+
|
|
1213
|
+
Returns (tree, session_dir). The session_dir is resolved by
|
|
1214
|
+
``_find_session_dir`` (via breadcrumb or glob) and passed back so
|
|
1215
|
+
the caller can write ``dc._session_tree.json`` next to the inputs
|
|
1216
|
+
without re-scanning the disk.
|
|
1217
|
+
"""
|
|
1218
|
+
manifest, rows, parse_warnings, session_dir = _load_all(sid)
|
|
1219
|
+
|
|
1220
|
+
# Short-circuit: session row not found.
|
|
1221
|
+
sessions = rows.get("sessions", [])
|
|
1222
|
+
if not sessions:
|
|
1223
|
+
return _minimal_tree_session_not_found(sid, manifest, parse_warnings), session_dir
|
|
1224
|
+
|
|
1225
|
+
# Short-circuit: STDM Interaction/Step/Message DMOs haven't materialized yet
|
|
1226
|
+
# (gateway_requests present, interactions/steps empty). Render the gateway
|
|
1227
|
+
# chain directly instead of silently emitting an empty tree.
|
|
1228
|
+
if manifest.get("session_shape") == "interactions_not_materialized_yet":
|
|
1229
|
+
return _assemble_gateway_direct(sid, rows, manifest, parse_warnings), session_dir
|
|
1230
|
+
|
|
1231
|
+
collisions: List[dict] = []
|
|
1232
|
+
# Phase 1: independent bags.
|
|
1233
|
+
idx = _build_indexes(rows, collisions)
|
|
1234
|
+
dispatch = _dispatch_polymorphic(rows, idx)
|
|
1235
|
+
catalog = _filter_catalog(rows)
|
|
1236
|
+
# Phase 2: derived bag that depends on idx.
|
|
1237
|
+
binding = _declared_binding_pass(rows, idx)
|
|
1238
|
+
|
|
1239
|
+
# Bind the invariant gw-view args once; call sites supply only the varying ones.
|
|
1240
|
+
build_gw = functools.partial(_build_gw_view, idx=idx, dispatch=dispatch)
|
|
1241
|
+
|
|
1242
|
+
# Build per-Interaction views + parallel binder state (sorted by start_ts).
|
|
1243
|
+
interactions_view: List[dict] = []
|
|
1244
|
+
binders: Dict[str, BinderCtx] = {}
|
|
1245
|
+
for interaction in sorted(
|
|
1246
|
+
rows.get("interactions", []),
|
|
1247
|
+
key=lambda r: (r.get("ssot__StartTimestamp__c") or "",
|
|
1248
|
+
r.get("ssot__Id__c") or "")):
|
|
1249
|
+
view, bctx = _build_interaction_view(interaction, rows, idx, dispatch, binding, build_gw)
|
|
1250
|
+
interactions_view.append(view)
|
|
1251
|
+
binders[view["id"]] = bctx
|
|
1252
|
+
|
|
1253
|
+
# Timestamp-window fallback; mutates interactions_view only via
|
|
1254
|
+
# timestamp_bound_gateway_calls[].append.
|
|
1255
|
+
unbound_gw_calls, gw_binding_counts = _ts_window_pass(
|
|
1256
|
+
interactions_view, binders, idx, binding, build_gw)
|
|
1257
|
+
|
|
1258
|
+
session_row = sessions[0]
|
|
1259
|
+
session_identity = _build_session_identity(rows, manifest)
|
|
1260
|
+
# `org_id_15` is the canonical 15-char slice used by path helpers.
|
|
1261
|
+
# Prefer the manifest-stamped value (resolved in wave 1a of fetch_dc
|
|
1262
|
+
# from sessions[0].ssot__InternalOrganizationId__c); fall back to
|
|
1263
|
+
# slicing session_identity.org_id (the 18-char form from
|
|
1264
|
+
# gateway_requests) when the manifest is missing the field (older
|
|
1265
|
+
# artifacts predate the manifest stamp).
|
|
1266
|
+
org_id_15 = manifest.get("org_id_15")
|
|
1267
|
+
if not org_id_15 and session_identity.get("org_id"):
|
|
1268
|
+
org_id_15 = session_identity["org_id"][:15]
|
|
1269
|
+
session_identity["org_id_15"] = org_id_15
|
|
1270
|
+
|
|
1271
|
+
# Top-level identity block — canonical location for the 3 segments
|
|
1272
|
+
# needed to name the session dir. Richer identity fields live under
|
|
1273
|
+
# `session.identity` as before.
|
|
1274
|
+
# Promote richer values from `session_identity` when the manifest carries
|
|
1275
|
+
# placeholders (notably ``agent_version="v0"`` from the MyAgent
|
|
1276
|
+
# fallback in fetch_dc.py:570-597) — without this, the top-level block
|
|
1277
|
+
# diverges from `session.identity` in the same JSON file.
|
|
1278
|
+
top_identity = _reconcile_top_identity(manifest, session_identity, org_id_15)
|
|
1279
|
+
|
|
1280
|
+
return {
|
|
1281
|
+
"identity": top_identity,
|
|
1282
|
+
"session": {
|
|
1283
|
+
"id": sid,
|
|
1284
|
+
"_schema_version": 1,
|
|
1285
|
+
"org": {
|
|
1286
|
+
"alias": manifest.get("org_alias"),
|
|
1287
|
+
"instance_url": manifest.get("instance_url"),
|
|
1288
|
+
},
|
|
1289
|
+
"identity": session_identity,
|
|
1290
|
+
"start_ts": session_row.get("ssot__StartTimestamp__c"),
|
|
1291
|
+
"end_ts": session_row.get("ssot__EndTimestamp__c"),
|
|
1292
|
+
"end_type": _resolve_end_type(session_row, rows),
|
|
1293
|
+
"channel": _harvest_str(session_row.get("ssot__AiAgentChannelType__c")),
|
|
1294
|
+
"participants": _build_participants_view(rows),
|
|
1295
|
+
"moments": _build_moments_view(rows, dispatch),
|
|
1296
|
+
"interactions": interactions_view,
|
|
1297
|
+
"session_tag_associations": dispatch.tag_assoc_session,
|
|
1298
|
+
"unbound_gateway_calls": unbound_gw_calls,
|
|
1299
|
+
"counts": _build_counts(rows, dispatch, gw_binding_counts,
|
|
1300
|
+
manifest, collisions, parse_warnings),
|
|
1301
|
+
},
|
|
1302
|
+
"catalog": {
|
|
1303
|
+
"agents_observed": catalog.agents_observed,
|
|
1304
|
+
"tag_definitions": catalog.tag_definitions,
|
|
1305
|
+
"tag_definition_associations": catalog.tag_definition_associations,
|
|
1306
|
+
"tags": catalog.tags,
|
|
1307
|
+
},
|
|
1308
|
+
"_doc": (
|
|
1309
|
+
"Assembled from "
|
|
1310
|
+
"DATA_ROOT/<org>/<agent>__<ver>/<sid>/dc.*.json. "
|
|
1311
|
+
"See dc._session_manifest.json for per-query counts and empty reasons. "
|
|
1312
|
+
"Contract: references/dc_pipeline_contract.md."
|
|
1313
|
+
),
|
|
1314
|
+
}, session_dir
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def _build_generation_view(gen: dict,
|
|
1318
|
+
quality_by_parent: Dict[str, List[dict]],
|
|
1319
|
+
categories_by_generation: Dict[str, List[dict]],
|
|
1320
|
+
categories_by_quality: Dict[str, List[dict]],
|
|
1321
|
+
feedback_by_gen: Dict[str, List[dict]],
|
|
1322
|
+
feedback_details_by_parent: Dict[str, List[dict]],
|
|
1323
|
+
gw_records_by_feedback: Dict[str, List[dict]]) -> dict:
|
|
1324
|
+
gen_id = gen["generationId__c"]
|
|
1325
|
+
# Quality rows with their TOXICITY sub-categories nested.
|
|
1326
|
+
quality_rows = []
|
|
1327
|
+
for q in quality_by_parent.get(gen_id, []):
|
|
1328
|
+
q_view = dict(q)
|
|
1329
|
+
q_view["_toxicity_subcategories"] = categories_by_quality.get(q.get("id__c"), [])
|
|
1330
|
+
quality_rows.append(q_view)
|
|
1331
|
+
|
|
1332
|
+
# Feedback rows with details + feedback-attachment records nested.
|
|
1333
|
+
feedback_rows = []
|
|
1334
|
+
for fb in feedback_by_gen.get(gen_id, []):
|
|
1335
|
+
fid = fb.get("feedbackId__c")
|
|
1336
|
+
feedback_rows.append({
|
|
1337
|
+
"feedback_id": fid,
|
|
1338
|
+
"feedback": fb.get("feedback__c"),
|
|
1339
|
+
"action": fb.get("action__c"),
|
|
1340
|
+
"details": feedback_details_by_parent.get(fid, []),
|
|
1341
|
+
"records": gw_records_by_feedback.get(fid, []),
|
|
1342
|
+
})
|
|
1343
|
+
|
|
1344
|
+
return {
|
|
1345
|
+
"generation_id": gen_id,
|
|
1346
|
+
"response_id": _clean(gen.get("generationResponseId__c")),
|
|
1347
|
+
"response_text": gen.get("responseText__c"),
|
|
1348
|
+
"masked_response_text": gen.get("maskedResponseText__c"),
|
|
1349
|
+
"response_parameters": gen.get("responseParameters__c"),
|
|
1350
|
+
"feature": _clean(gen.get("feature__c")),
|
|
1351
|
+
"quality": quality_rows,
|
|
1352
|
+
"categories": categories_by_generation.get(gen_id, []),
|
|
1353
|
+
"feedback": feedback_rows,
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
_STDM_LAG_NOTE = (
|
|
1358
|
+
"Interaction/Step/Message DMOs materialize on a separate cadence from "
|
|
1359
|
+
"Gateway DMOs. For fresh sessions this view reflects the gateway chain "
|
|
1360
|
+
"directly. Re-run in 24–72 hours for the full hierarchical trace."
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def _assemble_gateway_direct(sid: str, rows: Dict[str, List[dict]],
|
|
1365
|
+
manifest: dict,
|
|
1366
|
+
parse_warnings: List[str]) -> dict:
|
|
1367
|
+
"""Build a gateway-chain-only tree for sessions whose STDM hierarchy hasn't materialized.
|
|
1368
|
+
|
|
1369
|
+
Mirrors ``_minimal_tree_session_not_found`` in shape, but populates a
|
|
1370
|
+
``session.gateway_chain[]`` harvested directly from ``gateway_requests``
|
|
1371
|
+
joined to ``gateway_request_tags`` / ``gateway_request_metadata`` /
|
|
1372
|
+
``gateway_responses``. ``session.interactions`` is an explicit empty list
|
|
1373
|
+
so consumers that walk it simply no-op.
|
|
1374
|
+
|
|
1375
|
+
The sentinel ``_source = "gateway_direct"`` is consumed by
|
|
1376
|
+
``render_dc._render_gateway_direct``.
|
|
1377
|
+
"""
|
|
1378
|
+
sessions = rows.get("sessions", [])
|
|
1379
|
+
session_row = sessions[0] if sessions else {}
|
|
1380
|
+
|
|
1381
|
+
# Reuse the canonical identity harvester — same tag-alias fallbacks,
|
|
1382
|
+
# same normalization — and apply the same org_id_15 fallback as the
|
|
1383
|
+
# happy path so the top-level identity block is consistent across shapes.
|
|
1384
|
+
session_identity = _build_session_identity(rows, manifest)
|
|
1385
|
+
org_id_15 = manifest.get("org_id_15")
|
|
1386
|
+
if not org_id_15 and session_identity.get("org_id"):
|
|
1387
|
+
org_id_15 = session_identity["org_id"][:15]
|
|
1388
|
+
session_identity["org_id_15"] = org_id_15
|
|
1389
|
+
|
|
1390
|
+
# Same placeholder-promotion policy as the happy path — see
|
|
1391
|
+
# `_reconcile_top_identity` for why we don't trust the manifest blindly.
|
|
1392
|
+
top_identity = _reconcile_top_identity(manifest, session_identity, org_id_15)
|
|
1393
|
+
|
|
1394
|
+
# Group the child DMOs by parent once so the per-request loop stays O(n).
|
|
1395
|
+
tags_by_req: Dict[str, List[dict]] = _groupby(
|
|
1396
|
+
rows.get("gateway_request_tags", []), "parent__c")
|
|
1397
|
+
md_by_req: Dict[str, List[dict]] = _groupby(
|
|
1398
|
+
rows.get("gateway_request_metadata", []), "parent__c")
|
|
1399
|
+
resp_by_req: Dict[str, List[dict]] = _groupby(
|
|
1400
|
+
rows.get("gateway_responses", []), "generationRequestId__c")
|
|
1401
|
+
|
|
1402
|
+
# Deterministic order: sort by (timestamp__c, gatewayRequestId__c) so
|
|
1403
|
+
# repeat runs on the same inputs produce byte-identical output.
|
|
1404
|
+
gw_sorted = sorted(
|
|
1405
|
+
rows.get("gateway_requests", []),
|
|
1406
|
+
key=lambda r: (r.get("timestamp__c") or "",
|
|
1407
|
+
r.get("gatewayRequestId__c") or ""),
|
|
1408
|
+
)
|
|
1409
|
+
|
|
1410
|
+
gateway_chain: List[dict] = []
|
|
1411
|
+
for gw in gw_sorted:
|
|
1412
|
+
gw_id = gw.get("gatewayRequestId__c")
|
|
1413
|
+
# sf__Id (platform row id) isn't harvested by fetch_dc; the logical
|
|
1414
|
+
# PK is gatewayRequestId__c. Keep both keys on the view so readers
|
|
1415
|
+
# can lift either without re-joining.
|
|
1416
|
+
sf_id = gw.get("sf__Id")
|
|
1417
|
+
|
|
1418
|
+
# timestamp: prefer the columns named in the change-request
|
|
1419
|
+
# (requestTimeStamp__c, createdAt__c), then fall back to the
|
|
1420
|
+
# documented column (timestamp__c in references/dc_dmo_fields.md).
|
|
1421
|
+
timestamp = (
|
|
1422
|
+
_clean(gw.get("requestTimeStamp__c"))
|
|
1423
|
+
or _clean(gw.get("createdAt__c"))
|
|
1424
|
+
or _clean(gw.get("timestamp__c"))
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
# Tag-driven fields. Use _harvest_str for html-unescape + quote-strip —
|
|
1428
|
+
# same normalizer _build_session_identity uses on tag values.
|
|
1429
|
+
tag_first: Dict[str, Optional[str]] = {}
|
|
1430
|
+
for t in sorted(
|
|
1431
|
+
tags_by_req.get(gw_id, []),
|
|
1432
|
+
key=lambda r: (r.get("tag__c") or "",
|
|
1433
|
+
r.get("tagValue__c") or "")):
|
|
1434
|
+
k = t.get("tag__c")
|
|
1435
|
+
if not k or k in tag_first:
|
|
1436
|
+
continue
|
|
1437
|
+
tag_first[k] = _harvest_str(t.get("tagValue__c"))
|
|
1438
|
+
|
|
1439
|
+
# Prompt-template name: prefer the direct column on the request;
|
|
1440
|
+
# fall back to the tag alias used by older agent builds.
|
|
1441
|
+
prompt_template_dev_name = (
|
|
1442
|
+
_clean(gw.get("promptTemplateDevName__c"))
|
|
1443
|
+
or tag_first.get("prompt_template_dev_name")
|
|
1444
|
+
)
|
|
1445
|
+
# `feature` likewise prefers the typed column, falls back to tag.
|
|
1446
|
+
feature = _clean(gw.get("feature__c")) or tag_first.get("feature")
|
|
1447
|
+
|
|
1448
|
+
# Response side — take the first by response timestamp. 1:1 invariant
|
|
1449
|
+
# holds on every live session we've observed, but defend against the
|
|
1450
|
+
# in-flight-call edge case by sorting deterministically.
|
|
1451
|
+
responses_sorted = sorted(
|
|
1452
|
+
resp_by_req.get(gw_id, []),
|
|
1453
|
+
key=lambda r: (r.get("timestamp__c") or "",
|
|
1454
|
+
r.get("generationResponseId__c") or ""),
|
|
1455
|
+
)
|
|
1456
|
+
response_view: Optional[dict] = None
|
|
1457
|
+
if responses_sorted:
|
|
1458
|
+
resp = responses_sorted[0]
|
|
1459
|
+
response_view = {
|
|
1460
|
+
"timestamp": _clean(resp.get("timestamp__c")),
|
|
1461
|
+
"finish_reason": _parse_finish_reason_params(resp.get("parameters__c")),
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
gateway_chain.append({
|
|
1465
|
+
"gateway_request_id": gw_id,
|
|
1466
|
+
"sf_id": sf_id,
|
|
1467
|
+
"timestamp": timestamp,
|
|
1468
|
+
"model": _clean(gw.get("model__c")),
|
|
1469
|
+
"provider": _clean(gw.get("provider__c")),
|
|
1470
|
+
"prompt_template_dev_name": prompt_template_dev_name,
|
|
1471
|
+
"feature": feature,
|
|
1472
|
+
"prompt_tokens": gw.get("promptTokens__c"),
|
|
1473
|
+
"completion_tokens": gw.get("completionTokens__c"),
|
|
1474
|
+
"total_tokens": gw.get("totalTokens__c"),
|
|
1475
|
+
# Raw prompt is authoritative on disk (dc.gateway_requests.json);
|
|
1476
|
+
# the 64 KB display cap lives in the render layer, not here.
|
|
1477
|
+
"prompt_text": gw.get("prompt__c"),
|
|
1478
|
+
"metadata": md_by_req.get(gw_id, []),
|
|
1479
|
+
"tags": tags_by_req.get(gw_id, []),
|
|
1480
|
+
"response": response_view,
|
|
1481
|
+
})
|
|
1482
|
+
|
|
1483
|
+
return {
|
|
1484
|
+
"identity": top_identity,
|
|
1485
|
+
"_source": "gateway_direct",
|
|
1486
|
+
"session": {
|
|
1487
|
+
"id": sid,
|
|
1488
|
+
"_schema_version": 1,
|
|
1489
|
+
"org": {
|
|
1490
|
+
"alias": manifest.get("org_alias"),
|
|
1491
|
+
"instance_url": manifest.get("instance_url"),
|
|
1492
|
+
},
|
|
1493
|
+
"identity": session_identity,
|
|
1494
|
+
"start_ts": session_row.get("ssot__StartTimestamp__c"),
|
|
1495
|
+
"end_ts": session_row.get("ssot__EndTimestamp__c"),
|
|
1496
|
+
"end_type": _resolve_end_type(session_row, rows),
|
|
1497
|
+
"channel": _harvest_str(session_row.get("ssot__AiAgentChannelType__c")),
|
|
1498
|
+
"participants": _build_participants_view(rows),
|
|
1499
|
+
# Explicit empty list — downstream consumers walk this and must
|
|
1500
|
+
# no-op safely when STDM hasn't materialized yet.
|
|
1501
|
+
"interactions": [],
|
|
1502
|
+
"gateway_chain": gateway_chain,
|
|
1503
|
+
"_stdm_lag_note": _STDM_LAG_NOTE,
|
|
1504
|
+
"counts": {
|
|
1505
|
+
"session_shape": manifest.get("session_shape",
|
|
1506
|
+
"interactions_not_materialized_yet"),
|
|
1507
|
+
"gateway_requests": len(rows.get("gateway_requests", [])),
|
|
1508
|
+
"gateway_responses": len(rows.get("gateway_responses", [])),
|
|
1509
|
+
"gateway_metadata": len(rows.get("gateway_request_metadata", [])),
|
|
1510
|
+
"gateway_tags": len(rows.get("gateway_request_tags", [])),
|
|
1511
|
+
"interactions_total": 0,
|
|
1512
|
+
"steps_total": 0,
|
|
1513
|
+
"parse_warnings": parse_warnings,
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
"_doc": (
|
|
1517
|
+
f"Session {sid}: STDM Interaction/Step/Message DMOs have not "
|
|
1518
|
+
"materialized yet (they lag Gateway DMOs by hours to days). "
|
|
1519
|
+
"Tree carries the gateway chain harvested directly from "
|
|
1520
|
+
"GenAIGatewayRequest + related audit DMOs. Re-run fetch_dc.py "
|
|
1521
|
+
"in 24–72h once the STDM hierarchy has caught up for the full "
|
|
1522
|
+
"hierarchical trace."
|
|
1523
|
+
),
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def _parse_finish_reason_params(parameters: Optional[str]) -> Optional[str]:
|
|
1528
|
+
"""Lift finish_reason out of GatewayResponse.parameters__c.
|
|
1529
|
+
|
|
1530
|
+
The field arrives HTML-escaped and the finish_reason value itself is
|
|
1531
|
+
often wrapped in escaped quotes (e.g. `\\"stop\\"`). Mirrors
|
|
1532
|
+
``render_dc._parse_finish_reason`` but against the gateway-response
|
|
1533
|
+
parameters column rather than Generation.responseParameters__c.
|
|
1534
|
+
"""
|
|
1535
|
+
if not parameters:
|
|
1536
|
+
return None
|
|
1537
|
+
try:
|
|
1538
|
+
decoded = html.unescape(parameters)
|
|
1539
|
+
parsed = json.loads(decoded)
|
|
1540
|
+
except (ValueError, TypeError):
|
|
1541
|
+
return None
|
|
1542
|
+
raw = parsed.get("finish_reason") if isinstance(parsed, dict) else None
|
|
1543
|
+
if not isinstance(raw, str):
|
|
1544
|
+
return None
|
|
1545
|
+
return raw.replace("\\", "").strip('"').strip() or None
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
def _minimal_tree_session_not_found(sid: str, manifest: dict,
|
|
1549
|
+
parse_warnings: List[str]) -> dict:
|
|
1550
|
+
# Note: deliberately does NOT include `session.identity` — harvest
|
|
1551
|
+
# sources (gateway_requests, gateway_request_tags, sessions[0],
|
|
1552
|
+
# participants) are all empty on this path. Renderer's minimal-tree
|
|
1553
|
+
# branch must handle absent identity. DOES include _schema_version so
|
|
1554
|
+
# the renderer's version check doesn't warn on every not-found session.
|
|
1555
|
+
#
|
|
1556
|
+
# Top-level `identity` still carries the manifest-stamped (org, agent,
|
|
1557
|
+
# version) when available — the session dir already exists under that
|
|
1558
|
+
# identity or the manifest couldn't have been read. Breadcrumb readers
|
|
1559
|
+
# depend on this block being present on EVERY tree, not just
|
|
1560
|
+
# full-populated ones.
|
|
1561
|
+
return {
|
|
1562
|
+
"identity": {
|
|
1563
|
+
"org_id_15": manifest.get("org_id_15"),
|
|
1564
|
+
"agent_api_name": manifest.get("agent_api_name"),
|
|
1565
|
+
"agent_version": manifest.get("agent_version"),
|
|
1566
|
+
},
|
|
1567
|
+
"session": {
|
|
1568
|
+
"id": sid,
|
|
1569
|
+
"_schema_version": 1,
|
|
1570
|
+
"org": {
|
|
1571
|
+
"alias": manifest.get("org_alias"),
|
|
1572
|
+
"instance_url": manifest.get("instance_url"),
|
|
1573
|
+
},
|
|
1574
|
+
"counts": {
|
|
1575
|
+
"session_shape": manifest.get("session_shape", "session_not_found"),
|
|
1576
|
+
"parse_warnings": parse_warnings,
|
|
1577
|
+
},
|
|
1578
|
+
},
|
|
1579
|
+
"_doc": (
|
|
1580
|
+
f"Session {sid} did not resolve in Data Cloud "
|
|
1581
|
+
"(sessions.json returned 0 rows). Check the session id, or wait for "
|
|
1582
|
+
"STDM materialization. No interactions, catalog, or audit rows available."
|
|
1583
|
+
),
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
# ---- public entry points --------------------------------------------------
|
|
1588
|
+
|
|
1589
|
+
def main_for_session(sid: str) -> int:
|
|
1590
|
+
"""Called by fetch_dc.py's auto-run hook and by `--session` CLI.
|
|
1591
|
+
|
|
1592
|
+
Writes ``dc._session_tree.json`` into the session dir located by
|
|
1593
|
+
``_find_session_dir`` (via breadcrumb / glob) — the caller does not
|
|
1594
|
+
need to know ``(org, agent, version)`` to invoke the assembler.
|
|
1595
|
+
"""
|
|
1596
|
+
tree, session_dir = assemble(sid)
|
|
1597
|
+
tree_path = session_dir / "dc._session_tree.json"
|
|
1598
|
+
tree_path.write_text(json.dumps(tree, indent=2, sort_keys=True, default=str) + "\n")
|
|
1599
|
+
print(f"assemble_dc: wrote {tree_path}", file=sys.stderr)
|
|
1600
|
+
return 0
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
def main() -> int:
|
|
1604
|
+
ap = argparse.ArgumentParser(
|
|
1605
|
+
prog="assemble_dc.py",
|
|
1606
|
+
description="Assemble dc._session_tree.json for one session.",
|
|
1607
|
+
)
|
|
1608
|
+
ap.add_argument("--session", required=True,
|
|
1609
|
+
help="AI-agent session UUID or MessagingSession id (0Mw...). "
|
|
1610
|
+
"Messaging ids are resolved from disk "
|
|
1611
|
+
"(DATA_ROOT/*/dc.sessions.json); run fetch_dc.py first "
|
|
1612
|
+
"if the session hasn't been fetched yet.")
|
|
1613
|
+
# Runtime-agnostic path overrides; default to ~/.vibe/...
|
|
1614
|
+
from _shared.cli_override import add_cli_flags, apply_overrides
|
|
1615
|
+
add_cli_flags(ap)
|
|
1616
|
+
args = ap.parse_args()
|
|
1617
|
+
apply_overrides(args, caller_globals=globals())
|
|
1618
|
+
from resolve_session import resolve_disk_or_live
|
|
1619
|
+
sid = resolve_disk_or_live(args.session)
|
|
1620
|
+
return main_for_session(sid)
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
if __name__ == "__main__":
|
|
1624
|
+
sys.exit(main())
|