Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 101 additions & 6 deletions application/single_app/route_backend_conversation_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from collections import Counter, defaultdict
from datetime import datetime
from html import escape as _escape_html
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple

from bs4 import BeautifulSoup, NavigableString, Tag
from config import *
Expand Down Expand Up @@ -56,6 +56,9 @@ def api_export_conversations():
packaging (str): Output packaging — "single" or "zip".
include_summary_intro (bool): Whether to generate a per-conversation intro.
summary_model_deployment (str): Optional model deployment for summary generation.
summary_model_id (str): Optional multi-endpoint model id for summary generation.
summary_model_endpoint_id (str): Optional multi-endpoint endpoint id for summary generation.
summary_model_provider (str): Optional provider (aoai/aifoundry) for summary generation.
"""
user_id = get_current_user_id()
if not user_id:
Expand All @@ -70,6 +73,9 @@ def api_export_conversations():
packaging = str(data.get('packaging', 'single')).lower()
include_summary_intro = bool(data.get('include_summary_intro', False))
summary_model_deployment = str(data.get('summary_model_deployment', '') or '').strip()
summary_model_id = str(data.get('summary_model_id', '') or '').strip()
summary_model_endpoint_id = str(data.get('summary_model_endpoint_id', '') or '').strip()
summary_model_provider = str(data.get('summary_model_provider', '') or '').strip()

if not conversation_ids or not isinstance(conversation_ids, list):
return jsonify({'error': 'At least one conversation_id is required'}), 400
Expand Down Expand Up @@ -115,7 +121,10 @@ def api_export_conversations():
user_id=user_id,
settings=settings,
include_summary_intro=include_summary_intro,
summary_model_deployment=summary_model_deployment
summary_model_deployment=summary_model_deployment,
summary_model_id=summary_model_id,
summary_model_endpoint_id=summary_model_endpoint_id,
summary_model_provider=summary_model_provider
)
)

Expand Down Expand Up @@ -247,7 +256,10 @@ def _build_export_entry(
user_id: str,
settings: Dict[str, Any],
include_summary_intro: bool = False,
summary_model_deployment: str = ''
summary_model_deployment: str = '',
summary_model_id: str = '',
summary_model_endpoint_id: str = '',
summary_model_provider: str = '',
) -> Dict[str, Any]:
artifact_payload_map = build_message_artifact_payload_map(raw_messages)
filtered_messages = _filter_messages_for_export(raw_messages)
Expand Down Expand Up @@ -309,6 +321,9 @@ def _build_export_entry(
settings=settings,
enabled=include_summary_intro,
summary_model_deployment=summary_model_deployment,
summary_model_id=summary_model_id,
summary_model_endpoint_id=summary_model_endpoint_id,
summary_model_provider=summary_model_provider,
message_time_start=message_time_start,
message_time_end=message_time_end
)
Expand Down Expand Up @@ -660,7 +675,12 @@ def generate_conversation_summary(
model_deployment: str,
message_time_start: str = None,
message_time_end: str = None,
conversation_id: str = None
conversation_id: str = None,
model_id: Optional[str] = None,
model_endpoint_id: Optional[str] = None,
model_provider: Optional[str] = None,
user_id: Optional[str] = None,
active_group_ids: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Generate a conversation summary using the LLM and optionally persist it.

Expand All @@ -686,7 +706,25 @@ def generate_conversation_summary(

transcript_text = _truncate_for_summary(transcript_text)

gpt_client, gpt_model = _initialize_gpt_client(settings, model_deployment)
# Prefer multi-endpoint model resolution so summaries use the same
# AOAI endpoint/deployment as chat for a given model selection.
gpt_client: AzureOpenAI
gpt_model: str

multi_endpoint_client = _initialize_summary_multi_endpoint_client(
settings,
model_deployment=model_deployment,
model_id=model_id,
model_endpoint_id=model_endpoint_id,
model_provider=model_provider,
user_id=user_id,
active_group_ids=active_group_ids or [],
)

if multi_endpoint_client is not None:
gpt_client, gpt_model = multi_endpoint_client
else:
gpt_client, gpt_model = _initialize_gpt_client(settings, model_deployment)
summary_prompt = (
"You are summarizing a conversation for an export document. "
"Read the full conversation below and write a concise summary. "
Expand Down Expand Up @@ -761,6 +799,9 @@ def _build_summary_intro(
settings: Dict[str, Any],
enabled: bool,
summary_model_deployment: str,
summary_model_id: str = '',
summary_model_endpoint_id: str = '',
summary_model_provider: str = '',
message_time_start: str = None,
message_time_end: str = None
) -> Dict[str, Any]:
Expand Down Expand Up @@ -809,7 +850,12 @@ def _build_summary_intro(
model_deployment=summary_model_deployment,
message_time_start=message_time_start,
message_time_end=message_time_end,
conversation_id=conversation_id
conversation_id=conversation_id,
model_id=summary_model_id or None,
model_endpoint_id=summary_model_endpoint_id or None,
model_provider=summary_model_provider or None,
user_id=conversation.get('user_id'),
active_group_ids=[],
)

summary_intro.update({
Expand Down Expand Up @@ -848,6 +894,55 @@ def _truncate_for_summary(transcript_text: str) -> str:
)


def _initialize_summary_multi_endpoint_client(
settings: Dict[str, Any],
model_deployment: str,
model_id: Optional[str] = None,
model_endpoint_id: Optional[str] = None,
model_provider: Optional[str] = None,
user_id: Optional[str] = None,
active_group_ids: Optional[List[str]] = None,
) -> Optional[Tuple[AzureOpenAI, str]]:
"""Initialize a GPT client for summaries using the multi-endpoint model config.

This mirrors the streaming model resolution path so that summaries use the
exact same AOAI endpoint/deployment as chat for a given model selection.
Returns (client, deployment) when a multi-endpoint config can be resolved,
otherwise None to allow fallback to the legacy single-endpoint settings.
"""
try:
from route_backend_chats import resolve_streaming_multi_endpoint_gpt_config
except Exception:
return None

if not settings.get('enable_multi_model_endpoints', False):
return None

request_payload = {
'model_deployment': model_deployment or '',
'model_id': model_id or '',
'model_endpoint_id': model_endpoint_id or '',
'model_provider': model_provider or '',
}

try:
resolved = resolve_streaming_multi_endpoint_gpt_config(
settings,
request_payload,
user_id or '',
active_group_ids=active_group_ids or [],
allow_default_selection=False,
)
except Exception:
return None

if not resolved:
return None

gpt_client, deployment, _provider, _endpoint, _auth_settings, _api_version = resolved
return gpt_client, deployment


def _initialize_gpt_client(settings: Dict[str, Any], requested_model: str = ''):
enable_gpt_apim = settings.get('enable_gpt_apim', False)

Expand Down
15 changes: 14 additions & 1 deletion application/single_app/route_backend_conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,9 @@ def generate_conversation_summary_api(conversation_id):

body = request.get_json(silent=True) or {}
model_deployment = body.get('model_deployment', '')
model_id = body.get('model_id') or None
model_endpoint_id = body.get('model_endpoint_id') or None
model_provider = body.get('model_provider') or None

# Query messages for this conversation
try:
Expand Down Expand Up @@ -1045,7 +1048,17 @@ def generate_conversation_summary_api(conversation_id):
model_deployment=model_deployment,
message_time_start=message_time_start,
message_time_end=message_time_end,
conversation_id=conversation_id
conversation_id=conversation_id,
model_id=model_id,
model_endpoint_id=model_endpoint_id,
model_provider=model_provider,
user_id=user_id,
# For now, summaries use the same group context that
# was already captured in conversation metadata. The
# generate_conversation_summary helper accepts
# active_group_ids but does not require it, so we pass
# None here to avoid referencing undefined variables.
active_group_ids=None,
)
return jsonify({'success': True, 'summary': summary_data}), 200

Expand Down
63 changes: 53 additions & 10 deletions application/single_app/static/js/chat/chat-conversation-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,19 +673,43 @@ function getAvailableModelOptions() {
if (!globalSelect) {
return '<option value="">Default</option>';
}
let options = '';

const tempSelect = document.createElement('select');

for (const opt of globalSelect.options) {
options += `<option value="${escapeHtml(opt.value)}"${opt.selected ? ' selected' : ''}>${escapeHtml(opt.text)}</option>`;
const cloned = document.createElement('option');
cloned.value = opt.value;
cloned.textContent = opt.text;

if (opt.dataset?.deploymentName) {
cloned.dataset.deploymentName = opt.dataset.deploymentName;
}
if (opt.dataset?.modelId) {
cloned.dataset.modelId = opt.dataset.modelId;
}
if (opt.dataset?.endpointId) {
cloned.dataset.endpointId = opt.dataset.endpointId;
}
if (opt.dataset?.provider) {
cloned.dataset.provider = opt.dataset.provider;
}
if (opt.selected) {
cloned.selected = true;
}

tempSelect.appendChild(cloned);
}
return options || '<option value="">Default</option>';

const html = tempSelect.innerHTML;
return html || '<option value="">Default</option>';
}

/**
* Handle summary generation (generate or regenerate)
* @param {string} conversationId - The conversation ID
* @param {string} modelDeployment - Selected model deployment
* @param {Object} modelPayload - Selected model metadata
*/
async function handleGenerateSummary(conversationId, modelDeployment) {
async function handleGenerateSummary(conversationId, modelPayload) {
const cardBody = document.getElementById('summary-card-body');
if (!cardBody) {
return;
Expand All @@ -704,7 +728,7 @@ async function handleGenerateSummary(conversationId, modelDeployment) {
const response = await fetch(`/api/conversations/${conversationId}/summary`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_deployment: modelDeployment })
body: JSON.stringify(modelPayload || {})
});

if (!response.ok) {
Expand Down Expand Up @@ -766,8 +790,18 @@ document.addEventListener('click', function(e) {
const cid = btn.getAttribute('data-conversation-id');
const modelSelect = document.getElementById('summary-model-select');
const selectedOption = modelSelect ? modelSelect.options[modelSelect.selectedIndex] : null;
const model = selectedOption?.dataset?.deploymentName || (modelSelect ? modelSelect.value : '');
handleGenerateSummary(cid, model);

const modelDeployment = selectedOption?.dataset?.deploymentName || (modelSelect ? modelSelect.value : '');
const modelId = selectedOption?.dataset?.modelId || '';
const modelEndpointId = selectedOption?.dataset?.endpointId || '';
const modelProvider = selectedOption?.dataset?.provider || '';

handleGenerateSummary(cid, {
model_deployment: modelDeployment,
model_id: modelId,
model_endpoint_id: modelEndpointId,
model_provider: modelProvider
});
return;
}

Expand All @@ -779,8 +813,17 @@ document.addEventListener('click', function(e) {
// Use the currently selected global model for regeneration
const globalSelect = document.getElementById('model-select');
const selectedOption = globalSelect ? globalSelect.options[globalSelect.selectedIndex] : null;
const model = selectedOption?.dataset?.deploymentName || (globalSelect ? globalSelect.value : '');
handleGenerateSummary(cid, model);
const modelDeployment = selectedOption?.dataset?.deploymentName || (globalSelect ? globalSelect.value : '');
const modelId = selectedOption?.dataset?.modelId || '';
const modelEndpointId = selectedOption?.dataset?.endpointId || '';
const modelProvider = selectedOption?.dataset?.provider || '';

handleGenerateSummary(cid, {
model_deployment: modelDeployment,
model_id: modelId,
model_endpoint_id: modelEndpointId,
model_provider: modelProvider
});
return;
}

Expand Down
44 changes: 40 additions & 4 deletions application/single_app/static/js/chat/chat-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ let exportFormat = 'json';
let exportPackaging = 'single';
let includeSummaryIntro = false;
let summaryModelDeployment = '';
let summaryModelId = '';
let summaryModelEndpointId = '';
let summaryModelProvider = '';
let currentStep = 1;
let totalSteps = 3;
let skipSelectionStep = false;
Expand Down Expand Up @@ -57,6 +60,10 @@ function openExportWizard(conversationIds, skipSelection) {
exportPackaging = conversationIds.length > 1 ? 'zip' : 'single';
includeSummaryIntro = false;
summaryModelDeployment = _getDefaultSummaryModel();
const defaultModelMeta = _getDefaultSummaryModelMetadata();
summaryModelId = defaultModelMeta.modelId;
summaryModelEndpointId = defaultModelMeta.endpointId;
summaryModelProvider = defaultModelMeta.provider;
skipSelectionStep = !!skipSelection;

// Determine step configuration
Expand Down Expand Up @@ -345,9 +352,17 @@ function _renderSummaryStep(container) {

if (summaryModelSelect && hasModelOptions) {
summaryModelSelect.value = defaultSummaryModel || summaryModelSelect.value;
summaryModelDeployment = summaryModelSelect.options[summaryModelSelect.selectedIndex]?.dataset?.deploymentName || summaryModelSelect.value;
const selected = summaryModelSelect.options[summaryModelSelect.selectedIndex];
summaryModelDeployment = selected?.dataset?.deploymentName || summaryModelSelect.value;
summaryModelId = selected?.dataset?.modelId || '';
summaryModelEndpointId = selected?.dataset?.endpointId || '';
summaryModelProvider = selected?.dataset?.provider || '';
summaryModelSelect.addEventListener('change', () => {
summaryModelDeployment = summaryModelSelect.options[summaryModelSelect.selectedIndex]?.dataset?.deploymentName || summaryModelSelect.value;
const opt = summaryModelSelect.options[summaryModelSelect.selectedIndex];
summaryModelDeployment = opt?.dataset?.deploymentName || summaryModelSelect.value;
summaryModelId = opt?.dataset?.modelId || '';
summaryModelEndpointId = opt?.dataset?.endpointId || '';
summaryModelProvider = opt?.dataset?.provider || '';
});
}

Expand All @@ -359,7 +374,11 @@ function _renderSummaryStep(container) {
}
if (includeSummaryIntro && summaryModelSelect && !summaryModelSelect.value) {
summaryModelSelect.value = _getDefaultSummaryModel();
summaryModelDeployment = summaryModelSelect.options[summaryModelSelect.selectedIndex]?.dataset?.deploymentName || summaryModelSelect.value;
const opt = summaryModelSelect.options[summaryModelSelect.selectedIndex];
summaryModelDeployment = opt?.dataset?.deploymentName || summaryModelSelect.value;
summaryModelId = opt?.dataset?.modelId || '';
summaryModelEndpointId = opt?.dataset?.endpointId || '';
summaryModelProvider = opt?.dataset?.provider || '';
}
});
}
Expand Down Expand Up @@ -532,7 +551,10 @@ async function _executeExport() {
format: exportFormat,
packaging: exportPackaging,
include_summary_intro: includeSummaryIntro,
summary_model_deployment: includeSummaryIntro ? summaryModelDeployment : null
summary_model_deployment: includeSummaryIntro ? summaryModelDeployment : null,
summary_model_id: includeSummaryIntro ? summaryModelId : null,
summary_model_endpoint_id: includeSummaryIntro ? summaryModelEndpointId : null,
summary_model_provider: includeSummaryIntro ? summaryModelProvider : null
})
});

Expand Down Expand Up @@ -605,6 +627,20 @@ function _getDefaultSummaryModel() {
return mainModelSelect.value || (mainModelSelect.options[0] ? mainModelSelect.options[0].value : '');
}

function _getDefaultSummaryModelMetadata() {
const mainModelSelect = getEl('model-select');
if (!mainModelSelect || !mainModelSelect.options.length) {
return { modelId: '', endpointId: '', provider: '' };
}

const selected = mainModelSelect.options[mainModelSelect.selectedIndex] || mainModelSelect.options[0];
return {
modelId: selected.dataset?.modelId || '',
endpointId: selected.dataset?.endpointId || '',
provider: selected.dataset?.provider || ''
};
}

// --- Expose Globally ---
window.chatExport = {
openExportWizard
Expand Down
Loading
Loading