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

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
173 changes: 169 additions & 4 deletions application/single_app/functions_tabular_generated_exports.py

Large diffs are not rendered by default.

131 changes: 125 additions & 6 deletions application/single_app/functions_tabular_orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
import hashlib
import json
import os
import re
from typing import Mapping

from functions_analysis_deliverables import (
ANALYSIS_ARTIFACT_ROLE_PRIMARY_ANALYSIS,
ANALYSIS_ARTIFACT_ROLE_REQUESTED_OUTPUT,
ANALYSIS_DELIVERABLE_EVENT_FINALIZED,
ANALYSIS_DELIVERABLE_EVENT_PLANNED,
ANALYSIS_ORDERING_NOT_APPLICABLE,
Expand All @@ -18,6 +21,7 @@
ANALYSIS_VALIDATION_PROFILE_ARTIFACT_SET,
ANALYSIS_VALIDATION_PROFILE_EXACT_ROWS_SCHEMA,
ANALYSIS_VALIDATION_PROFILE_EXACT_ROWS_SCHEMA_AND_RULES,
build_analysis_deliverable_artifact,
build_analysis_deliverable_contract,
emit_analysis_deliverable_contract_event,
)
Expand Down Expand Up @@ -323,11 +327,76 @@ def question_requests_tabular_hierarchical_analysis(user_question):
)


def question_requests_tabular_exhaustive_row_output(user_question):
"""Return True when the user requires one narrative result per source row."""
normalized_question = str(user_question or "").strip().lower()
if not normalized_question:
return False

row_output_markers = (
"for each row",
"for every row",
"for each line",
"for every line",
"line by line",
"row by row",
"each line item",
"each row individually",
"each line individually",
"individually for each row",
"individually for each line",
"one answer per row",
"one answer per line",
"one result per row",
"one result per line",
"one output per row",
"one output per line",
"one markdown section per row",
"one markdown section per line",
)
return any(marker in normalized_question for marker in row_output_markers)


def extract_tabular_row_analysis_questions(user_question, max_questions=20):
"""Extract an ordered, bounded question list for exact-row narrative output."""
question_text = str(user_question or "").strip()
if not question_text:
return []

marker_match = re.search(
r"(?is)\b(?:questions?\s+(?:are|is)\s+as\s+follows|answer\s+(?:the\s+)?following\s+questions?)\s*:?",
question_text,
)
candidate_text = question_text[marker_match.end():] if marker_match else question_text
line_candidates = []
for raw_line in candidate_text.splitlines():
normalized_line = re.sub(r"^\s*(?:[-*•]|\d+[.)])\s*", "", raw_line).strip()
if normalized_line:
line_candidates.append(normalized_line)
if len(line_candidates) >= 2:
return line_candidates[:max_questions]

split_candidates = re.split(
r"(?i)(?=\b(?:what|why|how|when|where|who|which|does|do|is|are|can|could|should|would|concerns?)\b)",
candidate_text,
)
questions = []
for candidate in split_candidates:
normalized_candidate = re.sub(r"\s+", " ", candidate).strip(" :-\t\r\n")
if not normalized_candidate:
continue
questions.append(normalized_candidate[:500])
if len(questions) >= max_questions:
break
return questions


def get_tabular_generated_output_task_type(
generated_output_requested,
hierarchical_analysis_requested,
settings,
action_mode=None,
exhaustive_row_output_requested=False,
):
"""Map request intent to the existing durable generated-output task type."""
hierarchical_analysis_enabled = settings_flag_enabled(
Expand All @@ -336,6 +405,10 @@ def get_tabular_generated_output_task_type(
False,
)
analysis_required = str(action_mode or "").strip().lower() == "analyze"
if exhaustive_row_output_requested and hierarchical_analysis_enabled:
return TABULAR_RUN_TASK_COMBINED if analysis_required else TABULAR_RUN_TASK_STRUCTURED_EXPORT
if exhaustive_row_output_requested:
return None
if generated_output_requested and analysis_required:
return TABULAR_RUN_TASK_COMBINED
if generated_output_requested and hierarchical_analysis_requested and hierarchical_analysis_enabled:
Expand Down Expand Up @@ -575,13 +648,29 @@ def plan_tabular_request(
structured_output_formats = get_tabular_generated_output_formats(user_question)
generated_output_requested = question_requests_tabular_generated_output(user_question)
hierarchical_analysis_requested = question_requests_tabular_hierarchical_analysis(user_question)
exhaustive_row_output_requested = question_requests_tabular_exhaustive_row_output(user_question)
exhaustive_narrative_row_output_requested = bool(
exhaustive_row_output_requested and not structured_output_formats
)
row_analysis_questions = (
extract_tabular_row_analysis_questions(user_question)
if exhaustive_narrative_row_output_requested
else []
)
durable_task_type = get_tabular_generated_output_task_type(
generated_output_requested,
hierarchical_analysis_requested,
settings,
action_mode=normalized_action_mode,
exhaustive_row_output_requested=exhaustive_narrative_row_output_requested,
)
output_format = (
structured_output_formats[0]
if structured_output_formats
else "md"
if exhaustive_narrative_row_output_requested
else None
)
output_format = structured_output_formats[0] if structured_output_formats else None
execution_contract = durable_task_type or TABULAR_EXECUTION_CONTRACT_FOREGROUND_AGGREGATE
source_coverage = _build_source_coverage(normalized_contexts)
execution_group_id = _build_execution_group_id(
Expand Down Expand Up @@ -642,16 +731,42 @@ def plan_tabular_request(
if transformation_spec
else ANALYSIS_TRANSFORMATION_MODE_SEMANTIC
)
exact_row_output_requested = generated_output_requested or exhaustive_narrative_row_output_requested
validation_profile = (
ANALYSIS_VALIDATION_PROFILE_EXACT_ROWS_SCHEMA_AND_RULES
if generated_output_requested and transformation_spec
if exact_row_output_requested and transformation_spec
else ANALYSIS_VALIDATION_PROFILE_EXACT_ROWS_SCHEMA
if generated_output_requested
if exact_row_output_requested
else ANALYSIS_VALIDATION_PROFILE_ARTIFACT_SET
)
row_analysis_output_schema = [
f"answer_{question_index}"
for question_index in range(1, len(row_analysis_questions) + 1)
] or (["row_analysis"] if exhaustive_narrative_row_output_requested else [])
requested_artifacts = None
if exhaustive_narrative_row_output_requested:
requested_artifacts = []
request_order = 0
if normalized_action_mode == "analyze":
requested_artifacts.append(build_analysis_deliverable_artifact(
"analysis-summary",
ANALYSIS_ARTIFACT_ROLE_PRIMARY_ANALYSIS,
"md",
required=True,
request_order=request_order,
))
request_order += 1
requested_artifacts.append(build_analysis_deliverable_artifact(
"row-analysis-md",
ANALYSIS_ARTIFACT_ROLE_REQUESTED_OUTPUT,
"md",
required=True,
request_order=request_order,
))
deliverable_contract = build_analysis_deliverable_contract(
action_mode=action_mode,
requested_output_formats=requested_output_formats,
requested_artifacts=requested_artifacts,
analysis_required=(
normalized_action_mode == "analyze"
or durable_task_type in {
Expand All @@ -660,18 +775,19 @@ def plan_tabular_request(
}
),
public_output_schema=(
output_hints.get("public_output_schema")
row_analysis_output_schema
or output_hints.get("public_output_schema")
or output_hints.get("output_schema")
or []
),
row_cardinality=(
ANALYSIS_ROW_CARDINALITY_ONE_PER_SOURCE_ROW
if generated_output_requested
if exact_row_output_requested
else ANALYSIS_ROW_CARDINALITY_NOT_APPLICABLE
),
ordering=(
ANALYSIS_ORDERING_SOURCE_ORDER
if generated_output_requested
if exact_row_output_requested
else ANALYSIS_ORDERING_NOT_APPLICABLE
),
transformation_mode=transformation_mode,
Expand All @@ -689,6 +805,9 @@ def plan_tabular_request(
"durable_task_type": durable_task_type,
"generated_output_requested": generated_output_requested,
"hierarchical_analysis_requested": hierarchical_analysis_requested,
"exhaustive_row_output_requested": exhaustive_narrative_row_output_requested,
"row_analysis_mode": "exhaustive" if exhaustive_narrative_row_output_requested else "summary",
"row_analysis_questions": row_analysis_questions,
"requested_output_formats": requested_output_formats,
"output_format": output_format,
"action_mode": normalized_action_mode,
Expand Down
56 changes: 47 additions & 9 deletions application/single_app/route_backend_chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
build_tabular_legacy_post_tool_fallback_decision as _shared_build_tabular_legacy_post_tool_fallback_decision,
get_tabular_generated_output_format as _shared_get_tabular_generated_output_format,
get_tabular_generated_output_task_type as _shared_get_tabular_generated_output_task_type,
question_requests_tabular_exhaustive_row_output as _shared_question_requests_tabular_exhaustive_row_output,
question_requests_tabular_generated_output as _shared_question_requests_tabular_generated_output,
question_requests_tabular_hierarchical_analysis as _shared_question_requests_tabular_hierarchical_analysis,
settings_flag_enabled as _shared_settings_flag_enabled,
Expand Down Expand Up @@ -5016,16 +5017,28 @@ def question_requests_tabular_hierarchical_analysis(user_question):
return _shared_question_requests_tabular_hierarchical_analysis(user_question)


def question_requests_tabular_exhaustive_row_output(user_question):
"""Return True when the prompt requires one narrative result per source row."""
return _shared_question_requests_tabular_exhaustive_row_output(user_question)


def _settings_flag_enabled(settings, key, default=False):
return _shared_settings_flag_enabled(settings, key, default=default)


def _get_tabular_generated_output_task_type(generated_output_requested, hierarchical_analysis_requested, settings, action_mode=None):
def _get_tabular_generated_output_task_type(
generated_output_requested,
hierarchical_analysis_requested,
settings,
action_mode=None,
exhaustive_row_output_requested=False,
):
return _shared_get_tabular_generated_output_task_type(
generated_output_requested,
hierarchical_analysis_requested,
settings,
action_mode=action_mode,
exhaustive_row_output_requested=exhaustive_row_output_requested,
)


Expand Down Expand Up @@ -5760,23 +5773,38 @@ def _build_tabular_generated_output_query_descriptor(
return descriptor


def _build_direct_tabular_generated_output_source(user_question, file_contexts, user_id, conversation_id, settings, action_mode=None):
def _build_direct_tabular_generated_output_source(
user_question,
file_contexts,
user_id,
conversation_id,
settings,
action_mode=None,
planner_metadata=None,
):
"""Build a replayable full-tabular source descriptor without requiring a prior tool page."""
generated_output_requested = question_requests_tabular_generated_output(user_question)
hierarchical_analysis_requested = question_requests_tabular_hierarchical_analysis(user_question)
exhaustive_row_output_requested = question_requests_tabular_exhaustive_row_output(user_question)
durable_task_type = _get_tabular_generated_output_task_type(
generated_output_requested,
hierarchical_analysis_requested,
settings,
action_mode=action_mode,
exhaustive_row_output_requested=exhaustive_row_output_requested,
)
analysis_only_requested = durable_task_type == TABULAR_RUN_TASK_HIERARCHICAL_ANALYSIS
combined_requested = durable_task_type == TABULAR_RUN_TASK_COMBINED
if generated_output_requested and str(action_mode or '').strip().lower() == 'analyze' and not durable_task_type:
return None
if not generated_output_requested and not analysis_only_requested:
if not generated_output_requested and not analysis_only_requested and not exhaustive_row_output_requested:
return None
if hierarchical_analysis_requested and not generated_output_requested and not analysis_only_requested:
if (
hierarchical_analysis_requested
and not generated_output_requested
and not analysis_only_requested
and not exhaustive_row_output_requested
):
return None

normalized_contexts = dedupe_tabular_file_contexts(file_contexts)
Expand Down Expand Up @@ -5920,7 +5948,11 @@ def _build_direct_tabular_generated_output_source(user_question, file_contexts,
)
),
})
output_format = get_tabular_generated_output_format(user_question) or 'md'
output_format = (
str((planner_metadata or {}).get('output_format') or '').strip().lower()
or get_tabular_generated_output_format(user_question)
or 'md'
)
queued_output_format = 'md' if analysis_only_requested else output_format
return {
'file_context': file_context,
Expand Down Expand Up @@ -5958,6 +5990,7 @@ def _build_direct_tabular_generated_output_source(user_question, file_contexts,
'batch_count_estimate': max(1, math.ceil(row_count / max(batch_budget['max_rows'], 1))),
'analysis_only_requested': analysis_only_requested,
'combined_requested': combined_requested,
'exhaustive_row_output_requested': exhaustive_row_output_requested,
}


Expand Down Expand Up @@ -6014,6 +6047,7 @@ def emit_direct_parity_event(event_name, planner_result=None, metrics=None, dime
conversation_id,
settings,
action_mode=planner_action_mode,
planner_metadata=planner_metadata,
)
if not direct_source:
emit_direct_parity_event(
Expand Down Expand Up @@ -6044,6 +6078,10 @@ def emit_direct_parity_event(event_name, planner_result=None, metrics=None, dime
planner_metadata=planner_metadata,
)
background_metadata = build_background_tabular_generated_output_metadata(background_run)
actual_batch_count = (
_safe_int(background_metadata.get('batch_count'))
or direct_source['batch_count_estimate']
)
accepted_parity_result = parity_result
if callable(parity_result_builder):
accepted_parity_result = parity_result_builder(
Expand All @@ -6061,7 +6099,7 @@ def emit_direct_parity_event(event_name, planner_result=None, metrics=None, dime
metrics={
'source_count': len(file_contexts or []),
'row_count': direct_source.get('row_count'),
'batch_count_estimate': direct_source.get('batch_count_estimate'),
'batch_count_estimate': actual_batch_count,
},
)
emit_direct_parity_event(
Expand All @@ -6082,7 +6120,7 @@ def emit_direct_parity_event(event_name, planner_result=None, metrics=None, dime
'content': title,
'detail': (
f"run_id={background_metadata.get('export_run_id')}; "
f"rows={direct_source['row_count']}; batches~={direct_source['batch_count_estimate']}; checkpointed=true"
f"rows={direct_source['row_count']}; batches={actual_batch_count}; checkpointed=true"
),
'activity': build_tabular_post_processing_activity_payload(
'tabular.generated_output',
Expand All @@ -6092,7 +6130,7 @@ def emit_direct_parity_event(event_name, planner_result=None, metrics=None, dime
output_format=direct_source['output_format'],
file_name=direct_source['source_candidate'].get('filename'),
batch_index=0,
batch_count=direct_source['batch_count_estimate'],
batch_count=actual_batch_count,
),
}
maybe_callback_result = thought_callback(thought_payload)
Expand All @@ -6105,7 +6143,7 @@ def emit_direct_parity_event(event_name, planner_result=None, metrics=None, dime
'conversation_id': conversation_id,
'source_file_name': direct_source['source_candidate'].get('filename'),
'row_count': direct_source['row_count'],
'batch_count_estimate': direct_source['batch_count_estimate'],
'batch_count_estimate': actual_batch_count,
'task_type': direct_source.get('task_type') or 'structured_export',
'output_format': direct_source['output_format'],
'export_run_id': background_metadata.get('export_run_id'),
Expand Down
7 changes: 7 additions & 0 deletions application/single_app/static/js/chat/chat-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4311,6 +4311,13 @@ function renderReplyQuoteHtml(fullMessageObject = null) {
}

function getGeneratedAnalysisArtifactTitle(outputMetadata, outputFormat) {
const artifactId = String(outputMetadata?.artifact_id || outputMetadata?.member_id || '').trim().toLowerCase();
if (artifactId === 'analysis-summary') {
return 'Analyze Markdown summary';
}
if (artifactId === 'row-analysis-md') {
return 'Row-by-row Markdown output';
}
const capability = String(outputMetadata?.capability || '').trim().toLowerCase();
if (capability === 'analyze') {
return `Analyze ${outputFormat.toUpperCase()} artifact`;
Expand Down
Loading
Loading