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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
EXECUTOR_MAX_WORKERS = 30
CHAT_STREAM_EXECUTOR_MAX_WORKERS = max(8, int(os.getenv('SIMPLECHAT_CHAT_STREAM_WORKERS', '32') or '32'))
SESSION_TYPE = 'filesystem'
VERSION = "0.261.423"
VERSION = "0.261.424"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
55 changes: 48 additions & 7 deletions application/single_app/functions_orchestration_result_runtime.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# functions_orchestration_result_runtime.py
"""Typed runtime handoffs; no clients, model calls, publication, or new storage.

Version: 0.261.365
Version: 0.261.424
"""

from copy import deepcopy
Expand Down Expand Up @@ -274,13 +274,39 @@
return None


def _native_inventory_evidence_state(prepared, document_id):
"""An inspected empty inventory is evidence of absence, not missing excerpts."""

Check warning on line 278 in application/single_app/functions_orchestration_result_runtime.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains dynamic execution, persistence, or system access marker. Recommendation%3A Do not execute changed lifecycle scripts or installers while this finding is unresolved.
coverage = prepared.get('coverage')
inventories = coverage.get('native_inspection') if type(coverage) is dict else None
if type(inventories) is not list:
return 'unknown'
matches = [
item for item in inventories
if type(item) is dict and item.get('document_id') == document_id
]
if len(matches) != 1:
return 'unknown'
inventory = matches[0]
if inventory.get('execution_mode') != 'native_inspection' or inventory.get('section') != 'comments':
return 'unknown'
total, returned = inventory.get('total_records'), inventory.get('returned_records')
if type(total) is not int or type(returned) is not int or not 0 <= returned <= total:
return 'unknown'
flags = ('partial', 'has_more', 'limit_reached')
if returned < total or any(inventory.get(flag) is True for flag in flags):
return 'partial'
if inventory.get('inventory_complete') is True and all(inventory.get(flag) is False for flag in flags):
return 'complete'
return 'unknown'


def _read_result_document_citations(service, reference, *, evidence_status_out=None):
root = service.open_result(reference, allow_partial=True, require_current_sources=True)
if root.metadata()['source_count'] == 0:
return []
sources, citations, visited, expanded = {}, {}, set(), set()
coverage_values = {}
search_counts, source_names = {}, {}
search_counts, source_names, source_outcomes = {}, {}, {}

Check warning on line 309 in application/single_app/functions_orchestration_result_runtime.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
citation_fields = (
'citation_id', 'chunk_id', 'file_name', 'title', 'page_number', 'chunk_sequence',
'sheet_name', 'location_label', 'location_value', 'classification', 'document_classification', 'score',
Expand Down Expand Up @@ -327,6 +353,7 @@
visit(ResultRef.from_dict(parent))
retained_citations = []
native_reference = None
producer_counts = {}
if (
producer.capability_id == 'document_search'
and (current.output_name, current.kind) in (
Expand Down Expand Up @@ -360,7 +387,9 @@
if prepared['content_scope'] in ('native_excel_annotations', 'native_document_annotations'):
native_reference = prepared_reader.reference.to_dict()
for source in lineage['sources']:
search_counts.setdefault(source_key(source), set())
identity = source_key(source)
search_counts.setdefault(identity, set())

Check warning on line 391 in application/single_app/functions_orchestration_result_runtime.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
producer_counts[identity] = set()
elif producer.capability_id == CAPABILITY_ACTION_INVOKE and lineage['sources']:
prepared = read_complete_input(reader)
if (
Expand Down Expand Up @@ -419,14 +448,21 @@
producer.capability_id == 'document_search' and identity_key in search_counts
and citation.get('evidence_kind') != 'native_annotation_coverage'
):
search_counts[identity_key].add((
evidence_key = (
producer, current.manifest_sha256,
canonical_digest({name: value for name, value in projected.items() if name != 'evidence_id'}),
))
)
search_counts[identity_key].add(evidence_key)

Check warning on line 455 in application/single_app/functions_orchestration_result_runtime.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
producer_counts[identity_key].add(evidence_key)
identity = {key: value for key, value in projected.items() if key != 'evidence_id'}
citations.setdefault(source_key(matches[0]), {}).setdefault(
canonical_digest(identity), projected,
)
for identity, counts in producer_counts.items():
state = _native_inventory_evidence_state(prepared, identity[2]) if native_reference is not None else (
'complete' if counts else 'missing'
)
source_outcomes.setdefault(identity, {})[key] = state
reader.recheck()

visit(reference)
Expand All @@ -440,15 +476,20 @@
rows = []
for index, (identity, source) in enumerate(sources.items(), start=1):
count = len(search_counts[identity]) if identity in search_counts else None
outcome = evidence_status_from_sources([
{'state': state} for state in source_outcomes.get(identity, {}).values()
])
row = {
"name": str(source_names.get(identity) or source.get("file_name") or f"Selected source {index}")[:160],
"document_id": source["document_id"],
"state": "missing" if count == 0 else "complete" if count is not None else "unknown",
"state": outcome["state"] if outcome is not None else "unknown",
}
if count is not None:
row["evidence_count"] = count
rows.append(row)
evidence_status_out["value"] = evidence_status_from_sources(rows)
evidence_status_out["value"] = evidence_status_from_sources(
rows, partial=root.completeness.status == 'partial',
)
return projected


Expand Down
84 changes: 75 additions & 9 deletions application/single_app/static/js/chat/chat-analysis-results.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// chat-analysis-results.js
// Saved Analyze findings supplement the answer; only an authorized complete-record page is read.
// Saved Analyze findings live in Sources; only an authorized complete-record page is read.

const pageSize = 25;
const validationNotices = {
Expand Down Expand Up @@ -96,6 +96,56 @@
return results;
}

function savedAnalysisLimitation(descriptors, unavailable) {
if (unavailable || descriptors.some(descriptor => !descriptor || descriptor.available === false)) {
return 'Saved analysis unavailable';
}
const labels = {
invalid: 'Analysis validation failed',
partial: 'Partial analysis',
pending: 'Analysis validation pending',
not_validated: 'Analysis not validated',

Check warning on line 107 in application/single_app/static/js/chat/chat-analysis-results.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
};
for (const status of Object.keys(labels)) {
if (descriptors.some(descriptor => descriptor?.validation_status === status)) {
return labels[status];
}
}
return null;
}

function renderAnalysisWarning(message) {
const label = savedAnalysisLimitation(message._savedAnalysisDescriptors,
message.dataset.savedAnalysisUnavailable === 'true');
let warning = message._savedAnalysisWarning;
if (!label) {
warning?.remove();
message._savedAnalysisWarning = null;
return;
}
if (!warning) {
warning = node('div', 'saved-analysis-warning mt-2');
warning.setAttribute('role', 'status');

Check warning on line 128 in application/single_app/static/js/chat/chat-analysis-results.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
const button = node('button', 'btn btn-sm alert alert-warning py-1 px-2 mb-0 text-start');
button.type = 'button';
button.addEventListener('click', () => {
const toggle = message._savedAnalysisSources.parentElement.querySelector(
':scope > .message-footer .citation-toggle-btn',

Check warning on line 133 in application/single_app/static/js/chat/chat-analysis-results.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
);
if (toggle?.getAttribute('aria-expanded') !== 'true') {
toggle?.click();
}
});
warning.append(button);
message._savedAnalysisSources.parentElement.querySelector(':scope > .message-text')?.after(warning);
message._savedAnalysisWarning = warning;
}
const button = warning.querySelector('button');
button.textContent = `${label}. View sources`;
button.setAttribute('aria-controls', message._savedAnalysisSources.id);
warning.classList.toggle('d-none', message.dataset.savedAnalysisMasked === 'true');

Check warning on line 146 in application/single_app/static/js/chat/chat-analysis-results.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
}

export function syncSavedAnalysisConversation(conversationId = activeConversationId()) {
const next = String(conversationId || '').trim() || null;
if (contextConversationId !== next) {
Expand Down Expand Up @@ -304,8 +354,9 @@

function hideUnavailableActions(message) {
message.dataset.savedAnalysisUnavailable = 'true';
message.querySelectorAll('.saved-analysis-downloads, .generated-tabular-outputs-container, .assistant-inline-export-actions, .message-footer')
message.querySelectorAll('.saved-analysis-downloads, .generated-tabular-outputs-container, .assistant-inline-export-actions, .message-footer .message-actions')
.forEach(element => element.classList.add('d-none'));
renderAnalysisWarning(message);
}

export function hydrateSavedAnalysisResult(message, fullMessage) {
Expand All @@ -315,18 +366,32 @@
}
message.dataset.savedAnalysis = 'true';
message._savedAnalysisDescriptors = descriptors;
let anchor = message.querySelector('.message-text');
const sources = message.querySelector(':scope > .message-content > .message-bubble > .message-sources');
message._savedAnalysisSources = sources;
for (const descriptor of descriptors) {
anchor = hydrateAnalysisResult(message, descriptor, anchor);
hydrateAnalysisResult(message, descriptor, sources);
}
renderAnalysisWarning(message);
message.addEventListener('chat:sources-toggled', event => {
if (!event.detail.open) {
views.forEach(view => {
if (message.contains(view.root)) {
view.dispose();
}
});
sources.querySelectorAll('.saved-analysis-result details[open]').forEach(details => {
details.open = false;
});
}
});
}

function hydrateAnalysisResult(message, descriptor, anchor) {
const root = node('section', 'saved-analysis-result border rounded p-3 mt-3');
function hydrateAnalysisResult(message, descriptor, sources) {
const root = node('section', 'saved-analysis-result border-top pt-3 mt-3');
root.setAttribute('aria-label', descriptor?.label ? `Saved analysis: ${descriptor.label}` : 'Saved analysis');
const heading = node('h3', 'h6 mb-1 text-break', descriptor?.label || 'Saved analysis');
root.append(heading);
anchor?.after(root);
sources.append(root);
const controllers = new Set();
const view = { root, dispose: () => {
controllers.forEach(controller => controller.abort());
Expand Down Expand Up @@ -380,7 +445,7 @@
}
};
const status = node('p', 'small text-muted mb-1',
`0 of ${descriptor.record_count} records displayed · ${descriptor.source_count} sources in the saved analysis.`);
`${descriptor.record_count} saved findings · ${descriptor.source_count} sources.`);
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
const validationNotice = status => descriptor.semantics?.mode === 'source_observations'
Expand Down Expand Up @@ -544,7 +609,7 @@
} else {
view.dispose();
content.replaceChildren();
status.textContent = `0 of ${descriptor.record_count} records displayed · ${descriptor.source_count} sources in the saved analysis.`;
status.textContent = `${descriptor.record_count} saved findings · ${descriptor.source_count} sources.`;
}
});
retry.addEventListener('click', () => void loadPage());
Expand Down Expand Up @@ -582,6 +647,7 @@
element.classList.toggle('d-none', masked || (element.classList.contains('saved-analysis-downloads') &&
message.dataset.savedAnalysisUnavailable === 'true'));
});
message._savedAnalysisWarning?.classList.toggle('d-none', masked);

Check warning on line 650 in application/single_app/static/js/chat/chat-analysis-results.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
if (masked && message._savedAnalysisDescriptors.some(descriptor => sameAnalysis(selectedContext, descriptor))) {
clearSavedAnalysisContext();
}
Expand Down
31 changes: 20 additions & 11 deletions application/single_app/static/js/chat/chat-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -6144,13 +6144,11 @@
`Condition check (Boolean(citationsButtonsHtml) && hasAnyCitations): ${shouldShowCitations}`
);

if (shouldShowCitations) {
const hasSavedAnalysis = readSavedAnalyses(fullMessageObject?.metadata).length > 0;
if (shouldShowCitations || hasSavedAnalysis) {

Check warning on line 6148 in application/single_app/static/js/chat/chat-messages.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
console.log(">>> Will generate and include citation elements.");
const citationsContainerId = `citations-${messageId || Date.now()}`;
citationToggleHtml = `<button class="btn btn-sm btn-link text-muted citation-toggle-btn" title="Show sources" aria-expanded="false" aria-controls="${citationsContainerId}"><i class="bi bi-journal-text"></i></button>`;
// citationsButtonsHtml already contains a <div class="citations-container"> wrapper
// Just add ID and display style by wrapping minimally
citationContentContainerHtml = `<div id="${citationsContainerId}" style="display: none;">${citationsButtonsHtml}</div>`;
citationToggleHtml = '<button class="btn btn-sm btn-link text-muted citation-toggle-btn" title="Show sources" aria-label="Show sources" aria-expanded="false"><i class="bi bi-journal-text"></i></button>';
citationContentContainerHtml = '<div class="message-sources d-none"></div>';
} else {
console.log(">>> Will NOT generate citation elements.");
}
Expand Down Expand Up @@ -6185,6 +6183,16 @@
</div>
</div>`;

const sourcesContainer = messageDiv.querySelector(':scope > .message-content > .message-bubble > .message-sources');
const sourcesToggle = messageDiv.querySelector(
':scope > .message-content > .message-bubble > .message-footer .citation-toggle-btn',
);
if (sourcesContainer) {
sourcesContainer.id = `citations-${messageId || Date.now()}`;
sourcesContainer.innerHTML = sanitizeHtmlSafely(citationsButtonsHtml);
sourcesToggle.setAttribute('aria-controls', sourcesContainer.id);
}

messageDiv.dataset.replySenderName = stripHtmlTags(senderLabel).replace(/\s+/g, " ").trim() || "AI";
messageDiv.dataset.replyPreviewText = buildPlainTextPreview(renderedAiContent.previewMarkdown);

Expand Down Expand Up @@ -6381,24 +6389,25 @@
showToast("Failed to copy text.", "warning");
});
});
const toggleBtn = messageDiv.querySelector(".citation-toggle-btn");
const toggleBtn = sourcesToggle;
if (toggleBtn) {
toggleBtn.addEventListener("click", () => {
/* ... toggle logic ... */
const targetId = toggleBtn.getAttribute("aria-controls");
const citationsContainer = messageDiv.querySelector(`#${targetId}`);
const citationsContainer = sourcesContainer;
if (!citationsContainer) return;

// Store current scroll position to maintain user's view
const currentScrollTop = document.getElementById('chat-messages-container')?.scrollTop || window.pageYOffset;

const isExpanded = citationsContainer.style.display !== "none";
citationsContainer.style.display = isExpanded ? "none" : "block";
const isExpanded = !citationsContainer.classList.contains('d-none');
citationsContainer.classList.toggle('d-none', isExpanded);
toggleBtn.setAttribute("aria-expanded", !isExpanded);
toggleBtn.title = isExpanded ? "Show sources" : "Hide sources";
toggleBtn.setAttribute("aria-label", toggleBtn.title);
toggleBtn.innerHTML = isExpanded
? '<i class="bi bi-journal-text"></i>'
: '<i class="bi bi-chevron-up"></i>';
messageDiv.dispatchEvent(new CustomEvent('chat:sources-toggled', { detail: { open: !isExpanded } }));
// Note: Removed scrollChatToBottom() to prevent jumping when expanding citations

// Restore scroll position after DOM changes
Expand Down
16 changes: 9 additions & 7 deletions application/v2_ui/src/components/chat/AnalysisResult.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// AnalysisResult.tsx
// Lazy, complete-record pages supporting the readable answer above them.
// Lazy, complete-record pages in a message's Sources disclosure.

import { useCallback, useEffect, useState } from 'react';
import { fetchAnalysisEvidence, fetchAnalysisRecords } from '../../lib/endpoints';
Expand Down Expand Up @@ -99,9 +99,11 @@ function Evidence({
export function AnalysisResult({
descriptor,
onUnavailable,
knownUnavailable,
}: {
descriptor: SavedAnalysisDescriptor;
onUnavailable: (descriptor: SavedAnalysisDescriptor) => void;
onUnavailable?: (descriptor: SavedAnalysisDescriptor, message: string) => void;
knownUnavailable?: string;
}) {
const [open, setOpen] = useState(false);
const [offset, setOffset] = useState(0);
Expand All @@ -113,12 +115,12 @@ export function AnalysisResult({
const activeConversationId = useChatStore((state) => state.activeConversationId);
const selectAnalysisResult = useChatStore((state) => state.selectAnalysisResult);
const belongsHere = descriptor.conversation_id === activeConversationId;
const blocked = descriptor.available === false || !belongsHere || Boolean(unavailable);
const blocked = descriptor.available === false || !belongsHere || Boolean(unavailable || knownUnavailable);

const reportUnavailable = useCallback((message: string) => {
setUnavailable(message);
setPage(null);
onUnavailable(descriptor);
onUnavailable?.(descriptor, message);
const store = useChatStore.getState();
if (sameAnalysis(store.analysisResultContext, descriptor)) {
store.clearAnalysisResultContext();
Expand Down Expand Up @@ -158,11 +160,11 @@ export function AnalysisResult({

return (
<section aria-label={descriptor.label ? `Saved analysis: ${descriptor.label}` : 'Saved analysis'}
className="mt-3 min-w-0 rounded-xl border border-edge p-3">
<p className="break-words text-sm font-medium text-text-1">{descriptor.label || 'Saved analysis'}</p>
className="min-w-0 border-b border-edge pb-4">
<h4 className="break-words text-sm font-medium text-text-1">{descriptor.label || 'Saved analysis'}</h4>
{blocked ? (
<p role="status" aria-live="polite" className="mt-1 text-sm text-text-2">
{unavailable || analysisUnavailableMessage()}
{unavailable || knownUnavailable || analysisUnavailableMessage()}
</p>
) : (
<>
Expand Down
Loading
Loading