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 @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.326"
VERSION = "0.261.327"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
2 changes: 2 additions & 0 deletions application/single_app/functions_citation_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@
for field_name in (
"title",
"file_name",
"file_type",
"classification",
"scope",
):
Expand Down Expand Up @@ -583,6 +584,7 @@
incoming_metadata = {
"title": citation.get("title") or citation.get("file_name"),
"file_name": citation.get("file_name") or citation.get("title"),
"file_type": citation.get("file_type"),

Check warning on line 587 in application/single_app/functions_citation_tracking.py

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.
"classification": (
citation.get("classification")
or citation.get("document_classification")
Expand Down
27 changes: 24 additions & 3 deletions application/single_app/functions_orchestration_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,13 +1283,13 @@
_log_failure("Microsoft 365 citations could not be read for the answer.", self.record, exc)
return []

def _validate_citations(self, citations):
def _validate_citations(self, citations, *, enrich_display=False):

Check warning on line 1286 in application/single_app/functions_orchestration_execution.py

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.

Check warning on line 1286 in application/single_app/functions_orchestration_execution.py

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.
document_ids = sorted({
citation["document_id"] for citation in citations
if isinstance(citation, dict) and citation.get("document_id")
})
if not document_ids:
return
return citations

Check warning on line 1292 in application/single_app/functions_orchestration_execution.py

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.
manifest = resolve_context_source_manifest(
self.context, document_ids, settings=self._bootstrap.get_settings(),
user_id=self.record["user_id"],
Expand All @@ -1299,6 +1299,27 @@
if source.get("authorization_status") == "authorized"
}:
raise HarnessExecutionError("context_unavailable")
if not enrich_display:
return citations

Check warning on line 1303 in application/single_app/functions_orchestration_execution.py

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.
sources = {
source["document_id"]: source for source in manifest
if source.get("authorization_status") == "authorized"

Check warning on line 1306 in application/single_app/functions_orchestration_execution.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.
}
enriched = deepcopy(citations)

Check warning on line 1308 in application/single_app/functions_orchestration_execution.py

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.
for citation in enriched:

Check warning on line 1309 in application/single_app/functions_orchestration_execution.py

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 not isinstance(citation, dict) or not citation.get("document_id"):

Check warning on line 1310 in application/single_app/functions_orchestration_execution.py

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.
continue
source = sources[citation["document_id"]]

Check warning on line 1312 in application/single_app/functions_orchestration_execution.py

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.
# Lineage-only Analyze/Compare references have identity, but no display metadata.
# Keep strict retained-result descriptors unchanged; name only the published copy.
for field, source_field in (("title", "display_name"), ("file_name", "file_name")):
value = source.get(source_field)
if isinstance(value, str) and value.strip():
citation[field] = value.strip()
extension = source.get("extension")
if isinstance(extension, str) and extension.strip():
citation["file_type"] = extension.strip().lstrip(".").lower()
return enriched

def _touch_conversation(self, documents, message=None):
# Cache/citation integrations belong after application initialization and
Expand Down Expand Up @@ -1425,7 +1446,7 @@
citations = read_result_document_citations(self.context, reference)
if not self._delivery_only:
citations = result.get("citations") or []
self._validate_citations(citations)
citations = self._validate_citations(citations, enrich_display=True)
if prepared:
# The answer is published only while every result it read is still the user's.
workflow_result_contexts = self._workflow_result_lineage(current)
Expand Down
77 changes: 26 additions & 51 deletions application/v2_ui/src/components/chat/ConversationDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useEffect, useMemo } from 'react';
import { clsx } from 'clsx';
import {
CalendarDays,
FileText,
Files,
ListOrdered,
ListTree,
Expand All @@ -28,6 +27,12 @@ import {
import { collectConversationMedia } from '../../lib/conversationMedia';
import { mergeGeneratedEntries } from '../../lib/conversationGeneratedFiles';
import { m365SecondaryLine, readUsedM365Items } from '../../lib/m365Citations';
import {
conversationDocuments,
conversationDocumentName,
conversationDocumentSource,
} from '../../lib/conversationDocuments';
import { DocumentIcon } from '../documents/documentPresentation';
import { EmptyState, Skeleton } from '../ui/primitives';
import {
GeneratedSection,
Expand Down Expand Up @@ -93,26 +98,6 @@ function ContentsMode() {
);
}

function scopeLabel(document: UsedDocument): string {
const scope = document.scope;
if (!scope?.type) {
return '';
}
if (scope.name) {
return scope.name;
}
if (scope.type === 'personal') {
return 'Personal workspace';
}
if (scope.type === 'group') {
return 'Group workspace';
}
if (scope.type === 'public') {
return 'Public workspace';
}
return scope.type;
}

/** Summarize where in a document the citations landed. */
function locationLabel(document: UsedDocument): string {
const pages = (document.page_numbers ?? []).filter(
Expand All @@ -129,22 +114,34 @@ function locationLabel(document: UsedDocument): string {
}

function DocumentRow({ document }: { document: UsedDocument }) {
const name =
document.title || document.file_name || document.document_id || 'Untitled document';
const { title, filename } = conversationDocumentName(document);
const workspaces = useBootstrapStore((state) => state.data?.scope);
// A document can be attached to the conversation without having been cited yet, so
// the badge distinguishes "used as grounding" from "actually referenced".
const isCited = (document.citation_ids ?? []).length > 0;
const scope = scopeLabel(document);
const scope = conversationDocumentSource(
document, workspaces?.groups ?? [], workspaces?.public_workspaces ?? [],
);
const location = locationLabel(document);

return (
<li className="glass-flat rounded-xl p-3">
<li className="glass-flat rounded-xl p-3" data-used-document={document.document_id}>
<div className="flex items-start gap-2.5">
<FileText size={16} className="mt-0.5 shrink-0 text-text-3" />
<DocumentIcon document={document} size={16} className="mt-0.5" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm text-text-1" title={name}>
{name}
<p className="truncate text-sm text-text-1" title={title}>
{title}
</p>
{filename && (
<p className="mt-0.5 truncate text-xs text-text-3" title={filename}>
{filename}
</p>
)}
{scope && (
<p className="mt-0.5 truncate text-xs text-text-3" title={scope}>
{scope}
</p>
)}
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-text-3">
{isCited && (
<span className="inline-flex items-center gap-1 rounded-full bg-accent-soft px-1.5 py-0.5 text-accent">
Expand All @@ -153,7 +150,6 @@ function DocumentRow({ document }: { document: UsedDocument }) {
</span>
)}
{location && <span>{location}</span>}
{scope && <span>{scope}</span>}
{document.classification && <span>{document.classification}</span>}
</div>
</div>
Expand Down Expand Up @@ -253,28 +249,7 @@ function DocumentsMode() {
}
}, [activeConversationId, metadata, metadataLoading, metadataError, loadMetadata]);

const documents = useMemo(() => {
if (!metadata) {
return [] as UsedDocument[];
}
// Three sources, all document-shaped: current aggregates, the pre-v2 tracking
// list, and files uploaded straight into the conversation. De-duplicated by id so
// a document present in more than one does not appear twice.
const merged = new Map<string, UsedDocument>();
for (const list of [
metadata.used_documents,
metadata.legacy_used_documents,
metadata.linked_workspace_documents,
]) {
for (const document of list ?? []) {
const id = String(document?.document_id ?? '').trim();
if (id && !merged.has(id)) {
merged.set(id, document);
}
}
}
return [...merged.values()];
}, [metadata]);
const documents = useMemo(() => conversationDocuments(metadata), [metadata]);
// Microsoft 365 items are listed only to their owner; the server sends no others.
const m365 = useMemo(() => readUsedM365Items(metadata), [metadata]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const EXTENSION_TONES: Record<string, string> = {
pptx: 'text-warn',
};

export function documentExtension(document: WorkspaceDocument): string {
export function documentExtension(document: Pick<WorkspaceDocument, 'file_type' | 'file_name'>): string {
const explicit = String(document.file_type ?? '').replace('.', '').toLowerCase();
if (explicit) {
return explicit;
Expand All @@ -107,7 +107,7 @@ export function DocumentIcon({
size = 18,
className,
}: {
document: WorkspaceDocument;
document: Pick<WorkspaceDocument, 'file_type' | 'file_name'>;
size?: number;
className?: string;
}) {
Expand Down
5 changes: 4 additions & 1 deletion application/v2_ui/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,10 @@ export function Sidebar({ mobile = false }: { mobile?: boolean }) {
aria-label={mobile ? 'Close navigation' : 'Collapse navigation'}
aria-expanded="true"
aria-controls="primary-navigation"
className="ml-auto rounded-lg p-1.5 text-text-3 transition-colors hover:bg-surface-2 hover:text-text-1"
className={clsx(
'rounded-lg p-1.5 text-text-3 transition-colors hover:bg-surface-2 hover:text-text-1',
mobile && 'ml-auto',
)}
>
<ChevronLeft size={17} />
</button>
Expand Down
64 changes: 64 additions & 0 deletions application/v2_ui/src/lib/conversationDocuments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// conversationDocuments.ts

import type { ConversationMetadata, UsedDocument, WorkspaceRef } from './types';

function displayText(value: unknown, documentId = ''): string {
if (typeof value !== 'string') return '';
const text = value.trim();
return text === documentId || ['unknown document', 'unknown', 'untitled document'].includes(text.toLowerCase())
? '' : text;
}

/** Preserve the used-document set while filling gaps from other records of the same document. */
export function conversationDocuments(metadata: ConversationMetadata | null): UsedDocument[] {
const merged = new Map<string, UsedDocument>();
for (const list of [
metadata?.used_documents, metadata?.legacy_used_documents, metadata?.linked_workspace_documents,
]) {
for (const document of list ?? []) {
const id = String(document?.document_id ?? '').trim();
if (!id) continue;
const previous = merged.get(id);
if (!previous) {
merged.set(id, { ...document });
continue;
}
const combined = { ...previous };
for (const field of ['title', 'file_name', 'file_type'] as const) {
if (!displayText(previous[field], id) && displayText(document[field], id)) {
combined[field] = document[field];
}
}
if (!previous.scope?.type) {
combined.scope = document.scope;
} else if (previous.scope.type === document.scope?.type && previous.scope.id === document.scope.id) {
combined.scope = { ...document.scope, ...previous.scope };
if (!displayText(previous.scope.name, previous.scope.id)) {
combined.scope.name = document.scope.name;
}
}
merged.set(id, combined);
}
}
return [...merged.values()];
}

export function conversationDocumentName(document: UsedDocument): { title: string; filename: string } {
const filename = displayText(document.file_name, document.document_id);
const title = displayText(document.title, document.document_id) || filename || 'Untitled document';
return { title, filename: filename === title ? '' : filename };
}

export function conversationDocumentSource(
document: UsedDocument,
groups: readonly WorkspaceRef[],
publicWorkspaces: readonly WorkspaceRef[],
): string {
const scope = document.scope;
if (scope?.type === 'personal') return 'Personal workspace';
if (scope?.type !== 'group' && scope?.type !== 'public') return '';
const catalog = scope.type === 'group' ? groups : publicWorkspaces;
const name = displayText(scope.name, scope.id)
|| displayText(catalog.find((workspace) => workspace.id === scope.id)?.name, scope.id);
return name || (scope.type === 'group' ? 'Group workspace' : 'Public workspace');
}
1 change: 1 addition & 0 deletions application/v2_ui/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ export interface UsedDocument {
document_id: string;
title?: string;
file_name?: string;
file_type?: string;
classification?: string;
scope?: UsedDocumentScope;
chunk_ids?: string[];
Expand Down
Loading
Loading