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.301"
VERSION = "0.261.302"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# functions_collaboration_generated_documents.py
"""Documents that agents generated in a shared conversation, and who may download them.
"""Documents that agents generated in a conversation, and who may download them.

Check warning on line 2 in application/single_app/functions_collaboration_generated_documents.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.

An agent creates a document with the SimpleChat action's upload functions. Each stores the file
in the requester's personal workspace or in a group workspace and returns its id, which stays on
the message in the agent citation. That is what lets a shared conversation list what was produced
in it. Listing reveals only file names; downloading follows the workspace's own download rules,
whichever group the reader currently has active.
the message in the agent citation. That is what lets a conversation list what was produced in it,

Check warning on line 6 in application/single_app/functions_collaboration_generated_documents.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.
whether it is a shared conversation or a personal one. Listing reveals only file names;

Check warning on line 7 in application/single_app/functions_collaboration_generated_documents.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.
downloading follows the workspace's own download rules, whichever group the reader currently has
active.
"""

import json
Expand Down
153 changes: 153 additions & 0 deletions application/single_app/route_backend_conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@
from flask import Response, current_app, request, stream_with_context
from werkzeug.utils import secure_filename
from functions_debug import debug_print
from functions_collaboration_generated_documents import (
authorize_generated_document_download,
can_download_generated_document,
collect_generated_documents,
)
from functions_documents import (
build_document_download_response,
delete_chat_upload_workspace_documents_for_conversation,
serialize_chat_upload_workspace_documents_for_conversation,
)
Expand Down Expand Up @@ -969,6 +975,54 @@
)


def _list_personal_conversation_generated_documents(conversation_item):

Check warning on line 978 in application/single_app/route_backend_conversations.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.
"""Return the documents SimpleChat upload actions created in a personal conversation.

Check warning on line 979 in application/single_app/route_backend_conversations.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.

``conversation_item`` must already be authorized with ``_authorize_personal_conversation_read``.

Check warning on line 981 in application/single_app/route_backend_conversations.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.
Only messages the thread shows are read, filtered the way ``/api/get_messages`` filters them:
deleted messages, the assistant artifact store, hidden generated chat files and replaced

Check warning on line 983 in application/single_app/route_backend_conversations.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
thread attempts are left out. A citation stored in compact form is rebuilt from its artifact

Check warning on line 984 in application/single_app/route_backend_conversations.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.
record first, because the created document's id is in the full tool result.

Check warning on line 985 in application/single_app/route_backend_conversations.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.
"""
conversation_id = conversation_item['id']

Check warning on line 987 in application/single_app/route_backend_conversations.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.
raw_messages = list(cosmos_messages_container.query_items(
query='SELECT * FROM c WHERE c.conversation_id = @conversation_id',
parameters=[{'name': '@conversation_id', 'value': conversation_id}],
partition_key=conversation_id,
))
raw_messages.sort(key=lambda item: (
str(item.get('timestamp') or ''),
int(item.get('fork_sequence')) if str(item.get('fork_sequence') or '').isdigit() else 0,
str(item.get('id') or ''),
))
raw_messages = exclude_soft_deleted_messages(raw_messages)
artifact_payload_map = build_message_artifact_payload_map(raw_messages)

visible_messages = []
for item in filter_assistant_artifact_items(raw_messages):
metadata = item.get('metadata') if isinstance(item.get('metadata'), dict) else {}
if metadata.get('is_generated_chat_artifact', False):
continue
thread_info = metadata.get('thread_info') if isinstance(metadata.get('thread_info'), dict) else {}
active_thread = thread_info.get('active_thread')
if active_thread is True or active_thread is None:
visible_messages.append(item)

documents = collect_generated_documents(
hydrate_agent_citations_from_artifacts(visible_messages, artifact_payload_map),
)
log_event(
'[CONVERSATION_GENERATED_DOCUMENTS] Listed generated documents for a personal conversation.',
extra={
'conversation_id': conversation_id,
'message_count': len(visible_messages),
'document_count': len(documents),
},
debug_only=True,
)
return documents


def _rebuild_authorized_personal_conversation_used_documents(
user_id,
conversation_id,
Expand Down Expand Up @@ -2278,6 +2332,105 @@
print(f"Error retrieving conversation metadata: {e}")
return jsonify({'error': 'Failed to retrieve conversation metadata'}), 500

@bp.route('/api/conversations/<conversation_id>/generated-documents', methods=['GET'])
@swagger_route(security=get_auth_security())
@login_required
@user_required
def list_personal_conversation_generated_documents_api(conversation_id):
"""List the documents agents created in this conversation, and which the reader may download.

The personal counterpart of the shared conversation list: the same documents, read from the
SimpleChat upload actions' results, under the same workspace download rules.
"""
user_id = get_current_user_id()
if not user_id:
return jsonify({'error': 'User not authenticated'}), 401

try:
conversation_item = _authorize_personal_conversation_read(user_id, conversation_id)
settings = get_settings()
documents = [
{
'document_id': document['document_id'],
'file_name': document['file_name'],
'workspace_scope': document['workspace_scope'],
'preview': document['preview'],
'message_id': document['message_id'],
'created_at': document['created_at'],
'can_download': can_download_generated_document(user_id, document, settings=settings),
}
for document in _list_personal_conversation_generated_documents(conversation_item)
]
return jsonify({'documents': documents}), 200
except LookupError:
return jsonify({'error': 'Conversation not found'}), 404
except PermissionError:
return jsonify({'error': 'Forbidden'}), 403
except Exception as exc:
log_event(
'[CONVERSATION_GENERATED_DOCUMENTS] Failed to list generated documents.',
extra={'conversation_id': conversation_id, 'exception_type': type(exc).__name__},
level=logging.ERROR,
exceptionTraceback=True,
)
return jsonify({'error': 'Failed to list generated documents'}), 500

@bp.route(
'/api/conversations/<conversation_id>/generated-documents/<document_id>/download',
methods=['GET'],
)
@swagger_route(security=get_auth_security())
@login_required
@user_required
def download_personal_conversation_generated_document_api(conversation_id, document_id):
"""Download a document an agent created in this conversation, under its workspace's rules."""
user_id = get_current_user_id()
if not user_id:
return jsonify({'error': 'User not authenticated'}), 401

try:
conversation_item = _authorize_personal_conversation_read(user_id, conversation_id)
except LookupError:
return jsonify({'error': 'Conversation not found'}), 404
except PermissionError:
return jsonify({'error': 'Forbidden'}), 403

try:
# Only a document this conversation produced can be fetched through it.
document = next(
(
candidate
for candidate in _list_personal_conversation_generated_documents(conversation_item)
if candidate['document_id'] == document_id
),
None,
)
if not document:
return jsonify({'error': 'Document not found'}), 404
try:
document_record, group_id = authorize_generated_document_download(user_id, document)
except LookupError:
return jsonify({'error': 'Document not found or access denied'}), 404
return build_document_download_response(
document_record,
user_id=user_id,
group_id=group_id,
)
except FileNotFoundError:
return jsonify({'error': 'This document is not available yet.'}), 404
except PermissionError:
return jsonify({'error': 'You do not have permission to download this document'}), 403
except ScreeningError as error:
return jsonify({'error': error.public_message, 'error_code': error.code}), error.status_code
except Exception as exc:
log_event(
'[CONVERSATION_GENERATED_DOCUMENTS] Failed to download a generated document.',
extra={'conversation_id': conversation_id, 'exception_type': type(exc).__name__},
level=logging.ERROR,
exceptionTraceback=True,
)
return jsonify({'error': 'Unable to download document'}), 500

@bp.route('/api/conversations/<conversation_id>/kind', methods=['GET'])
@swagger_route(security=get_auth_security())
@login_required
Expand Down
36 changes: 24 additions & 12 deletions application/v2_ui/src/components/chat/ConversationDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// Right-hand drawer with three modes, mirroring the legacy offcanvas that hosts both a
// table of contents and the documents used in the conversation, plus the orchestration plan.
//
// Contents lists the user's turns so a long thread can be navigated; Documents lists the
// document-level citation aggregates the server records on the conversation; Plan hosts the
// orchestration plan surface when the feature is on.
// Contents lists the user's turns so a long thread can be navigated; Documents lists what the
// conversation produced (generated files and media) above the document-level citation
// aggregates the server records on the conversation; Plan hosts the orchestration plan surface
// when the feature is on.

import { useEffect, useMemo } from 'react';
import { clsx } from 'clsx';
Expand All @@ -19,13 +20,17 @@ import {
} from 'lucide-react';
import { useChatStore, type DrawerMode } from '../../stores/chatStore';
import { useBootstrapStore } from '../../stores/bootstrapStore';
import {
useConversationGeneratedDocuments,
useConversationGeneratedFiles,
} from '../../stores/generatedDocumentsStore';
import { collectConversationMedia } from '../../lib/conversationMedia';
import { mergeGeneratedEntries } from '../../lib/conversationGeneratedFiles';
import { EmptyState, Skeleton } from '../ui/primitives';
import {
GeneratedDocumentsSection,
GeneratedSection,
MediaSection,
SectionHeading,
useGeneratedDocuments,
} from './DrawerAssets';
import { OrchestrationPlanPanel } from './OrchestrationPlanPanel';
import type { UsedDocument } from '../../lib/types';
Expand Down Expand Up @@ -157,10 +162,15 @@ function DocumentRow({ document }: { document: UsedDocument }) {
function DocumentsMode() {
const { metadata, metadataLoading, metadataError, activeConversationId, loadMetadata } =
useChatStore();
const collaborative = useChatStore((state) => state.activeConversationKind === 'collaborative');
const kind = useChatStore((state) => state.activeConversationKind);
const messages = useChatStore((state) => state.messages);
const media = useMemo(() => collectConversationMedia(messages), [messages]);
const generated = useGeneratedDocuments(activeConversationId, collaborative);
const files = useConversationGeneratedFiles();
const generated = useConversationGeneratedDocuments();
const entries = useMemo(
() => mergeGeneratedEntries(files, generated.documents, messages),
[files, generated.documents, messages],
);

useEffect(() => {
if (activeConversationId && !metadata && !metadataLoading && !metadataError) {
Expand Down Expand Up @@ -191,13 +201,15 @@ function DocumentsMode() {
return [...merged.values()];
}, [metadata]);

const hasAssets = generated.documents.length > 0 || Boolean(generated.error) || media.length > 0;
const hasAssets = entries.length > 0 || Boolean(generated.error) || media.length > 0;
const assets = hasAssets && activeConversationId ? (
<>
<GeneratedDocumentsSection
<GeneratedSection
conversationId={activeConversationId}
documents={generated.documents}
kind={kind ?? 'personal'}
entries={entries}
error={generated.error}
onLocate={scrollToMessage}
/>
<MediaSection items={media} onLocate={scrollToMessage} />
</>
Expand Down Expand Up @@ -232,8 +244,8 @@ function DocumentsMode() {
return (
<EmptyState
icon={<Files size={24} />}
title="No documents used yet"
description="Documents referenced while answering will be listed here."
title="No documents yet"
description="Documents used or created while answering will be listed here."
/>
);
}
Expand Down
Loading
Loading