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

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
130 changes: 121 additions & 9 deletions application/single_app/functions_generated_file_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@
)
# Below this a stored action reads as a lookup, not the dataset a request is asking for.
PRIOR_TURN_SUBSTANTIVE_ROW_COUNT = 10
# Actions advertise partial results in different ways, so accept the common spellings.
FUNCTION_RESULT_TRUNCATION_KEYS = {
'truncated',
'istruncated',
'wastruncated',
'resultstruncated',
'rowstruncated',
}
FUNCTION_RESULT_TRUNCATION_MAX_DEPTH = 3
FUNCTION_RESULT_TRUNCATION_MAX_ITEMS = 20
FUNCTION_RESULT_CONTROL_KEYS = {
'count',
'detail',
Expand Down Expand Up @@ -224,6 +234,15 @@
'do not tell the user to copy or save rows manually, and do not paste a sample of the rows '
'in place of the artifact.'
)
# Agents commonly re-request an action from the same start time, which re-reads rows already held.

Check warning on line 237 in application/single_app/functions_generated_file_exports.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.
GENERATED_FILE_PAGING_GUIDANCE = (
'When an action reports that its results were truncated, request the remaining data with a '
'window that starts after the last row you already have instead of repeating the original '
'range, and say plainly that the data is partial if you cannot retrieve the rest.'
)
GENERATED_FILE_TRUNCATED_ROWS_NOTE = (
'The source action reported truncated results, so this file covers only the rows it returned.'
)


def _normalize_question_for_artifact_detection(user_question: str) -> str:
Expand Down Expand Up @@ -473,7 +492,11 @@
clarification_guidance = build_csv_output_clarification_guidance(user_question)
return ' '.join(
guidance_part
for guidance_part in (clarification_guidance, CSV_PUBLICATION_GUIDANCE)
for guidance_part in (
clarification_guidance,
CSV_PUBLICATION_GUIDANCE,
GENERATED_FILE_PAGING_GUIDANCE,
)
if guidance_part
)
if output_format == 'json':
Expand All @@ -497,7 +520,7 @@
'evidence. Structured function results from this turn may be included as labeled tables in the '
'generated file. Do not claim that you cannot create or attach files, do not tell the user to '
'copy or save the content manually, do not invent rows, and do not claim an attachment exists '
'before the file-output finalizer publishes it.'
f'before the file-output finalizer publishes it. {GENERATED_FILE_PAGING_GUIDANCE}'
)
return ''

Expand Down Expand Up @@ -539,11 +562,14 @@
return None
function_rows = extract_authorized_function_result_rows(function_results)
row_provenance = ROW_SOURCE_CURRENT_ACTION
rows_truncated = function_results_report_truncated_rows(function_results) if function_rows else False
if not assistant_rows and not function_rows and prior_function_results_loader is not None:
# This turn answered from evidence gathered earlier, so reuse those rows instead of none.
function_rows = extract_authorized_function_result_rows(prior_function_results_loader())
prior_function_results = prior_function_results_loader()
function_rows = extract_authorized_function_result_rows(prior_function_results)
if function_rows:
row_provenance = ROW_SOURCE_EARLIER_ACTION
rows_truncated = function_results_report_truncated_rows(prior_function_results)
function_passthrough = evaluate_generated_file_passthrough_eligibility(
user_question,
rows=function_rows,
Expand All @@ -566,6 +592,7 @@
row_source=row_source,
assistant_content=assistant_text,
passthrough_reason_code=(function_passthrough.get('reason_code') if use_function_rows else None),
rows_truncated=rows_truncated if use_function_rows else False,
)

if not assistant_text and not assistant_rows and not function_rows:
Expand All @@ -589,6 +616,7 @@
passthrough_reason_code=(
function_passthrough.get('reason_code') if row_source in FUNCTION_RESULT_ROW_SOURCES else None
),
rows_truncated=rows_truncated if row_source in FUNCTION_RESULT_ROW_SOURCES else False,
)


Expand All @@ -605,10 +633,13 @@

function_rows = extract_authorized_function_result_rows(function_results)
row_provenance = ROW_SOURCE_CURRENT_ACTION
rows_truncated = function_results_report_truncated_rows(function_results) if function_rows else False
if not function_rows and prior_function_results_loader is not None:
function_rows = extract_authorized_function_result_rows(prior_function_results_loader())
prior_function_results = prior_function_results_loader()
function_rows = extract_authorized_function_result_rows(prior_function_results)
if function_rows:
row_provenance = ROW_SOURCE_EARLIER_ACTION
rows_truncated = function_results_report_truncated_rows(prior_function_results)
if not function_rows:
return None

Expand All @@ -629,6 +660,7 @@
'row_count': len(function_rows),
'row_source': row_provenance,
'passthrough_reason_code': function_passthrough.get('reason_code'),
'rows_truncated': rows_truncated,
}


Expand Down Expand Up @@ -659,6 +691,8 @@
) -> List[Tuple[str, List[Dict[str, Any]]]]:
"""Group authorized rows by action, so paged calls to one action stay one dataset."""
grouped_rows: Dict[str, List[Dict[str, Any]]] = {}
# Agents often re-page an action from the same start, so pages overlap rather than extend.

Check warning on line 694 in application/single_app/functions_generated_file_exports.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.
seen_group_rows: Dict[str, set] = {}
for function_result in function_results or []:
if not isinstance(function_result, dict):
continue
Expand All @@ -670,13 +704,81 @@
)
if not structured_rows:
continue
grouped_rows.setdefault(
_get_function_result_label(function_result),
[],
).extend(structured_rows)
function_label = _get_function_result_label(function_result)
group_rows = grouped_rows.setdefault(function_label, [])
seen_rows = seen_group_rows.setdefault(function_label, set())
page_signatures = set()
for structured_row in structured_rows:
row_signature = _build_function_result_row_signature(structured_row)
# Repeats inside one response are records the action counted, so only drop rows an
# earlier page of the same action already contributed.
if row_signature is not None:
if row_signature in seen_rows:
continue
page_signatures.add(row_signature)
group_rows.append(structured_row)
seen_rows.update(page_signatures)
return list(grouped_rows.items())


def _build_function_result_row_signature(row: Any) -> Optional[Tuple[Tuple[str, str], ...]]:
if not isinstance(row, dict):
return None
try:

Check warning on line 727 in application/single_app/functions_generated_file_exports.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.
return tuple(sorted(
(_normalize_function_result_key(key), _serialize_row_signature_value(value))
for key, value in row.items()
))
except TypeError:
return None


def _serialize_row_signature_value(value: Any) -> str:
if isinstance(value, (dict, list, tuple, set)):
return json.dumps(value, default=str, sort_keys=True)
return '' if value is None else str(value)


def function_results_report_truncated_rows(
function_results: Optional[List[Dict[str, Any]]],
) -> bool:
"""Report whether a row-contributing action said it returned only part of the matching data."""
for function_result in function_results or []:
if not isinstance(function_result, dict):
continue
if function_result.get('success') is False or _is_tabular_function_result(function_result):
continue
payload = _parse_function_result_payload(function_result.get('function_result'))
if not _extract_function_result_rows(payload):
continue
if _payload_reports_truncation(payload):
return True
return False


def _payload_reports_truncation(payload: Any, depth: int = 0) -> bool:
if depth > FUNCTION_RESULT_TRUNCATION_MAX_DEPTH:
return False
if isinstance(payload, dict):
for key, value in payload.items():
if _normalize_function_result_key(key) not in FUNCTION_RESULT_TRUNCATION_KEYS:
continue
if value is True or str(value).strip().casefold() == 'true':
return True
return any(
_payload_reports_truncation(value, depth + 1)
for value in payload.values()
if isinstance(value, (dict, list))
)
if isinstance(payload, list):
return any(
_payload_reports_truncation(item, depth + 1)
for item in payload[:FUNCTION_RESULT_TRUNCATION_MAX_ITEMS]
if isinstance(item, (dict, list))
)
return False


def _select_dominant_function_row_group(
function_row_groups: Sequence[Tuple[str, List[Dict[str, Any]]]],
) -> Optional[List[Dict[str, Any]]]:
Expand Down Expand Up @@ -917,6 +1019,8 @@
row_source = str(export_payload.get('row_source') or '').strip()
if row_source:
artifact_metadata['row_source'] = row_source
if export_payload.get('rows_truncated'):
artifact_metadata['rows_truncated'] = True

# Surfaced immediately so the participant who asked for the file sees the pending state in
# the same response instead of a download button that would be refused.
Expand All @@ -939,6 +1043,7 @@
assistant_content: str,
title: str = '',
passthrough_reason_code: Optional[str] = None,
rows_truncated: bool = False,
) -> Dict[str, Any]:
normalized_output_format = str(output_format or '').strip().lower()
row_count = len(rows or [])
Expand All @@ -952,12 +1057,14 @@
'preview_rows': list(rows or [])[:GENERATED_FILE_PREVIEW_ROWS],
'preview_lines': _build_preview_lines(assistant_content),
'row_source': row_source,
'rows_truncated': bool(rows_truncated),
'_structured_rows': list(rows or []),
'summary': _build_generated_file_summary(
normalized_output_format,
row_count,
row_source,
normalized_title,
bool(rows_truncated),
),
}
if passthrough_reason_code:
Expand All @@ -979,9 +1086,14 @@
row_count: int,
row_source: str,
title: str,
rows_truncated: bool = False,
) -> str:
row_detail = f' with {row_count} structured row(s)' if row_count else ''
return f'Prepared {title}{row_detail} from the {row_source}.'
summary = f'Prepared {title}{row_detail} from the {row_source}.'
if rows_truncated:
# The action capped its own response, so the file is a partial view of the matching data.
summary = f'{summary} {GENERATED_FILE_TRUNCATED_ROWS_NOTE}'
return summary


def _build_preview_lines(assistant_content: str) -> List[str]:
Expand Down
25 changes: 21 additions & 4 deletions application/single_app/route_backend_chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2371,6 +2371,17 @@
)


def _resolve_generated_file_guidance_format(user_question, conversation_id, user_id):

Check warning on line 2374 in application/single_app/route_backend_chats.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 artifact format this reply owes, including one carried from a clarification."""
requested_format = str(get_tabular_generated_output_format(user_question) or '').strip().lower()
if requested_format or not conversation_id:

Check warning on line 2377 in application/single_app/route_backend_chats.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 requested_format
# A reply that only answers "which rows and columns?" still owes the originally requested file,
# so the publication contract has to reach the model on this turn too.
pending_format = _resolve_pending_generated_file_format(user_question, conversation_id, user_id)

Check warning on line 2381 in application/single_app/route_backend_chats.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 str(pending_format or '').strip().lower()


def maybe_create_generated_file_output(
user_question,
assistant_content,
Expand Down Expand Up @@ -16278,7 +16289,11 @@
system_messages_for_augmentation = [] # Collect system messages from search
generated_file_output_guidance = build_generated_file_output_guidance(
user_message,
requested_format=get_tabular_generated_output_format(user_message),
requested_format=_resolve_generated_file_guidance_format(
user_message,
conversation_id,

Check warning on line 16294 in application/single_app/route_backend_chats.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.
user_id,
),
)
if generated_file_output_guidance:
system_messages_for_augmentation.append({
Expand Down Expand Up @@ -20631,9 +20646,11 @@
generated_tabular_outputs_list = []
generated_analysis_artifacts_list = []
system_messages_for_augmentation = []
requested_streamed_file_format = str(
get_tabular_generated_output_format(user_message) or ''
).strip().lower()
requested_streamed_file_format = _resolve_generated_file_guidance_format(
user_message,
conversation_id,

Check warning on line 20651 in application/single_app/route_backend_chats.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.
user_id,
)
suppress_streamed_file_payload = requested_streamed_file_format in {'json', 'xml'}
streamed_file_status_content = _build_streaming_assistant_file_status(
requested_streamed_file_format
Expand Down
9 changes: 9 additions & 0 deletions application/single_app/static/js/chat/chat-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -5347,6 +5347,15 @@ function renderReplyQuoteHtml(fullMessageObject = null) {
backgroundDetailElements.push(rowCountDetail);
}

if (outputMetadata?.rows_truncated) {
// The completed-artifact layout hides the summary, so partial coverage needs its own badge.
const truncatedBadge = document.createElement('span');
truncatedBadge.className = 'badge text-bg-warning';
truncatedBadge.textContent = 'Partial';
truncatedBadge.title = 'The source action reported truncated results, so this file covers only the rows it returned.';
header.appendChild(truncatedBadge);
}

card.appendChild(header);

if (!isCompletedTabularArtifact) {
Expand Down
Loading
Loading