Skip to content

Commit b09c202

Browse files
authored
Merge pull request #1205 from microsoft/fix/1204-prior-grounded-source-continuity
Merge prior grounded sources into follow-up tasks
2 parents 96a452a + 0bd2158 commit b09c202

5 files changed

Lines changed: 393 additions & 9 deletions

File tree

‎application/single_app/config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
EXECUTOR_TYPE = 'thread'
9797
EXECUTOR_MAX_WORKERS = 30
9898
SESSION_TYPE = 'filesystem'
99-
VERSION = "0.250.156"
99+
VERSION = "0.250.157"
100100
IS_DEVELOPMENT = is_development_env_enabled()
101101

102102
SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')

‎application/single_app/route_backend_chats.py‎

Lines changed: 218 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15728,6 +15728,7 @@ def result_requires_message_reload(result: Any) -> bool:
1572815728
history_grounded_search_used = False
1572915729
history_only_answerability = None
1573015730
prior_grounded_document_refs = []
15731+
prior_grounded_source_merge = None
1573115732
continuity_decision = None
1573215733
effective_document_scope = document_scope
1573315734
effective_selected_document_ids = list(selected_document_ids or [])
@@ -16067,6 +16068,67 @@ def result_requires_message_reload(result: Any) -> bool:
1606716068
selected_document_id = effective_selected_document_id
1606816069
document_scope = effective_document_scope
1606916070

16071+
prior_grounded_document_refs = _normalize_prior_grounded_document_refs(conversation_item)
16072+
current_document_context_available = bool(
16073+
effective_selected_document_ids
16074+
or effective_selected_document_id
16075+
or hybrid_search_enabled
16076+
or document_context_requested
16077+
)
16078+
if (
16079+
current_document_context_available
16080+
and prior_grounded_document_refs
16081+
and _prior_grounded_source_reference_requested(user_message)
16082+
):
16083+
prior_search_parameters = build_prior_grounded_document_search_parameters(
16084+
prior_grounded_document_refs
16085+
)
16086+
prior_search_parameters = revalidate_prior_grounded_document_search_parameters(
16087+
user_id,
16088+
prior_search_parameters,
16089+
)
16090+
prior_grounded_source_merge = _merge_prior_grounded_sources_with_current_context(
16091+
effective_selected_document_ids,
16092+
effective_document_scope,
16093+
effective_active_group_ids,
16094+
effective_active_public_workspace_ids,
16095+
prior_search_parameters,
16096+
)
16097+
if prior_grounded_source_merge.get('used'):
16098+
effective_selected_document_ids = list(
16099+
prior_grounded_source_merge.get('document_ids') or []
16100+
)
16101+
effective_selected_document_id = (
16102+
effective_selected_document_ids[0]
16103+
if len(effective_selected_document_ids) == 1
16104+
else None
16105+
)
16106+
effective_document_scope = prior_grounded_source_merge.get('doc_scope') or 'all'
16107+
effective_active_group_ids = list(
16108+
prior_grounded_source_merge.get('active_group_ids') or []
16109+
)
16110+
effective_active_group_id = (
16111+
effective_active_group_ids[0]
16112+
if effective_active_group_ids
16113+
else None
16114+
)
16115+
effective_active_public_workspace_ids = list(
16116+
prior_grounded_source_merge.get('active_public_workspace_ids') or []
16117+
)
16118+
effective_active_public_workspace_id = (
16119+
effective_active_public_workspace_ids[0]
16120+
if effective_active_public_workspace_ids
16121+
else None
16122+
)
16123+
selected_document_ids = list(effective_selected_document_ids)
16124+
selected_document_id = effective_selected_document_id
16125+
document_scope = effective_document_scope
16126+
active_group_ids = list(effective_active_group_ids)
16127+
active_group_id = effective_active_group_id
16128+
active_public_workspace_ids = list(effective_active_public_workspace_ids)
16129+
active_public_workspace_id = effective_active_public_workspace_id
16130+
hybrid_search_enabled = True
16131+
1607016132
mixed_source_manifest = []
1607116133
mixed_source_partitions = {}
1607216134
mixed_source_narrative_document_ids = []
@@ -16274,6 +16336,13 @@ def result_requires_message_reload(result: Any) -> bool:
1627416336
'tags': tags_filter,
1627516337
'classification': classifications_to_send
1627616338
}
16339+
if prior_grounded_source_merge:
16340+
user_metadata['workspace_search']['prior_grounded_source_merge'] = {
16341+
'used': bool(prior_grounded_source_merge.get('used')),
16342+
'prior_document_count': len(prior_grounded_source_merge.get('prior_document_ids') or []),
16343+
'added_document_count': len(prior_grounded_source_merge.get('prior_added_document_ids') or []),
16344+
'selection_origin': 'selected+history',
16345+
}
1627716346
if assigned_knowledge_filters:
1627816347
assigned_knowledge = assigned_knowledge_filters.get('assigned_knowledge') or {}
1627916348
user_metadata['workspace_search']['assigned_knowledge'] = {
@@ -16599,8 +16668,8 @@ def result_requires_message_reload(result: Any) -> bool:
1659916668
not original_hybrid_search_enabled
1660016669
and not explicit_external_retrieval_requested
1660116670
and not mixed_source_explicit_selection
16671+
and not prior_grounded_source_merge
1660216672
):
16603-
prior_grounded_document_refs = _normalize_prior_grounded_document_refs(conversation_item)
1660416673
if prior_grounded_document_refs:
1660516674
continuity_decision = _resolve_reauthorized_continuity_decision(
1660616675
settings,
@@ -18283,6 +18352,7 @@ def record_tabular_post_processing_thought(thought_payload):
1828318352
current_document_context_requested=(
1828418353
is_mixed_source_chat_search_enabled(settings)
1828518354
and document_context_requested
18355+
or bool(prior_grounded_source_merge)
1828618356
),
1828718357
):
1828818358
history_grounding_message = build_history_grounding_system_message()
@@ -19971,6 +20041,7 @@ def stream_cancel_requested():
1997120041
history_grounded_search_used = False
1997220042
history_only_answerability = None
1997320043
prior_grounded_document_refs = []
20044+
prior_grounded_source_merge = None
1997420045
continuity_decision = None
1997520046
effective_document_scope = document_scope
1997620047
effective_selected_document_ids = list(selected_document_ids or [])
@@ -20330,6 +20401,67 @@ def build_streaming_capability_usage():
2033020401
selected_document_id = effective_selected_document_id
2033120402
document_scope = effective_document_scope
2033220403

20404+
prior_grounded_document_refs = _normalize_prior_grounded_document_refs(conversation_item)
20405+
current_document_context_available = bool(
20406+
effective_selected_document_ids
20407+
or effective_selected_document_id
20408+
or hybrid_search_enabled
20409+
or document_context_requested
20410+
)
20411+
if (
20412+
current_document_context_available
20413+
and prior_grounded_document_refs
20414+
and _prior_grounded_source_reference_requested(user_message)
20415+
):
20416+
prior_search_parameters = build_prior_grounded_document_search_parameters(
20417+
prior_grounded_document_refs
20418+
)
20419+
prior_search_parameters = revalidate_prior_grounded_document_search_parameters(
20420+
user_id,
20421+
prior_search_parameters,
20422+
)
20423+
prior_grounded_source_merge = _merge_prior_grounded_sources_with_current_context(
20424+
effective_selected_document_ids,
20425+
effective_document_scope,
20426+
effective_active_group_ids,
20427+
effective_active_public_workspace_ids,
20428+
prior_search_parameters,
20429+
)
20430+
if prior_grounded_source_merge.get('used'):
20431+
effective_selected_document_ids = list(
20432+
prior_grounded_source_merge.get('document_ids') or []
20433+
)
20434+
effective_selected_document_id = (
20435+
effective_selected_document_ids[0]
20436+
if len(effective_selected_document_ids) == 1
20437+
else None
20438+
)
20439+
effective_document_scope = prior_grounded_source_merge.get('doc_scope') or 'all'
20440+
effective_active_group_ids = list(
20441+
prior_grounded_source_merge.get('active_group_ids') or []
20442+
)
20443+
effective_active_group_id = (
20444+
effective_active_group_ids[0]
20445+
if effective_active_group_ids
20446+
else None
20447+
)
20448+
effective_active_public_workspace_ids = list(
20449+
prior_grounded_source_merge.get('active_public_workspace_ids') or []
20450+
)
20451+
effective_active_public_workspace_id = (
20452+
effective_active_public_workspace_ids[0]
20453+
if effective_active_public_workspace_ids
20454+
else None
20455+
)
20456+
selected_document_ids = list(effective_selected_document_ids)
20457+
selected_document_id = effective_selected_document_id
20458+
document_scope = effective_document_scope
20459+
active_group_ids = list(effective_active_group_ids)
20460+
active_group_id = effective_active_group_id
20461+
active_public_workspace_ids = list(effective_active_public_workspace_ids)
20462+
active_public_workspace_id = effective_active_public_workspace_id
20463+
hybrid_search_enabled = True
20464+
2033320465
mixed_source_manifest = []
2033420466
mixed_source_partitions = {}
2033520467
mixed_source_narrative_document_ids = []
@@ -20527,6 +20659,13 @@ def build_streaming_capability_usage():
2052720659
'active_public_workspace_ids': effective_active_public_workspace_ids,
2052820660
'classification': classifications_to_send
2052920661
}
20662+
if prior_grounded_source_merge:
20663+
user_metadata['workspace_search']['prior_grounded_source_merge'] = {
20664+
'used': bool(prior_grounded_source_merge.get('used')),
20665+
'prior_document_count': len(prior_grounded_source_merge.get('prior_document_ids') or []),
20666+
'added_document_count': len(prior_grounded_source_merge.get('prior_added_document_ids') or []),
20667+
'selection_origin': 'selected+history',
20668+
}
2053020669
if assigned_knowledge_filters:
2053120670
assigned_knowledge = assigned_knowledge_filters.get('assigned_knowledge') or {}
2053220671
user_metadata['workspace_search']['assigned_knowledge'] = {
@@ -20872,8 +21011,8 @@ def record_and_publish_streaming_thought(thought_payload):
2087221011
not original_hybrid_search_enabled
2087321012
and not explicit_external_retrieval_requested
2087421013
and not mixed_source_explicit_selection
21014+
and not prior_grounded_source_merge
2087521015
):
20876-
prior_grounded_document_refs = _normalize_prior_grounded_document_refs(conversation_item)
2087721016
if prior_grounded_document_refs:
2087821017
continuity_decision = _resolve_reauthorized_continuity_decision(
2087921018
settings,
@@ -22170,6 +22309,7 @@ def record_and_publish_streaming_thought(thought_payload):
2217022309
current_document_context_requested=(
2217122310
is_mixed_source_chat_search_enabled(settings)
2217222311
and document_context_requested
22312+
or bool(prior_grounded_source_merge)
2217322313
),
2217422314
):
2217522315
history_grounding_message = build_history_grounding_system_message()
@@ -24246,6 +24386,82 @@ def revalidate_prior_grounded_document_search_parameters(user_id, search_paramet
2424624386
return normalized_parameters
2424724387

2424824388

24389+
def _prior_grounded_source_reference_requested(user_message):
24390+
"""Return whether the user is asking to reuse a prior grounded source."""
24391+
normalized_message = re.sub(r'\s+', ' ', str(user_message or '').strip().casefold())
24392+
if not normalized_message:
24393+
return False
24394+
24395+
source_noun_pattern = (
24396+
r'(?:source|document|file|template|spreadsheet|workbook|table|xml|json|pdf|csv|xlsx|xls)'
24397+
)
24398+
reference_patterns = (
24399+
rf'\b(?:that|those|same|previous|prior|earlier|last)\s+{source_noun_pattern}\b',
24400+
rf'\b(?:that|the|same|previous|prior|earlier|last)\s+'
24401+
rf'(?:xml|json|pdf|csv|xlsx|xls)\s+(?:source|document|file|template|workbook)\b',
24402+
rf'\b(?:into|in|with|using|from|against|compare\s+to)\s+'
24403+
rf'(?:that|the|same|previous|prior|earlier|last)\s+{source_noun_pattern}\b',
24404+
)
24405+
return any(re.search(pattern, normalized_message) for pattern in reference_patterns)
24406+
24407+
24408+
def _merge_unique_strings(*collections):
24409+
merged = []
24410+
seen_values = set()
24411+
for collection in collections:
24412+
for value in list(collection or []):
24413+
normalized_value = str(value or '').strip()
24414+
if not normalized_value or normalized_value in seen_values:
24415+
continue
24416+
seen_values.add(normalized_value)
24417+
merged.append(normalized_value)
24418+
return merged
24419+
24420+
24421+
def _merge_prior_grounded_sources_with_current_context(
24422+
current_document_ids,
24423+
current_document_scope,
24424+
current_active_group_ids,
24425+
current_active_public_workspace_ids,
24426+
prior_search_parameters,
24427+
):
24428+
"""Merge reauthorized prior grounded source parameters into current source context."""
24429+
prior_document_ids = _normalize_conversation_task_document_ids(
24430+
(prior_search_parameters or {}).get('document_ids')
24431+
)
24432+
current_document_ids = _normalize_conversation_task_document_ids(current_document_ids)
24433+
current_document_id_set = set(current_document_ids)
24434+
prior_only_document_ids = [
24435+
document_id
24436+
for document_id in prior_document_ids
24437+
if document_id not in current_document_id_set
24438+
]
24439+
merged_document_ids = _merge_unique_strings(current_document_ids, prior_document_ids)
24440+
24441+
current_scope = str(current_document_scope or '').strip().lower()
24442+
prior_scope = str((prior_search_parameters or {}).get('doc_scope') or '').strip().lower()
24443+
scope_candidates = [scope for scope in (current_scope, prior_scope) if scope]
24444+
merged_scope = scope_candidates[0] if len(set(scope_candidates)) == 1 else 'all'
24445+
if not scope_candidates:
24446+
merged_scope = None
24447+
24448+
return {
24449+
'used': bool(prior_only_document_ids),
24450+
'document_ids': merged_document_ids,
24451+
'prior_document_ids': prior_document_ids,
24452+
'prior_added_document_ids': prior_only_document_ids,
24453+
'doc_scope': merged_scope,
24454+
'active_group_ids': _merge_unique_strings(
24455+
current_active_group_ids,
24456+
(prior_search_parameters or {}).get('active_group_ids'),
24457+
),
24458+
'active_public_workspace_ids': _merge_unique_strings(
24459+
current_active_public_workspace_ids,
24460+
(prior_search_parameters or {}).get('active_public_workspace_ids'),
24461+
),
24462+
}
24463+
24464+
2424924465
def build_history_only_assessment_messages(history_segments, default_system_prompt=''):
2425024466
"""Construct the prompt context used to decide whether history alone is sufficient."""
2425124467
assessment_messages = []
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Prior Grounded Source Continuity Fix
2+
3+
Fixed/Implemented in version: **0.250.157**
4+
5+
Related work: Fixes #1204
6+
7+
## Issue Description
8+
9+
Follow-up mixed-source tasks did not automatically combine previously grounded sources with the current turn's selected or retrieved sources. For example, after summarizing an XML template, a user could ask to add a selected PDF's content into "that XML file," but the current turn would only use the PDF context and not rehydrate the prior XML template as an active source.
10+
11+
## Root Cause Analysis
12+
13+
SimpleChat already persisted compact prior grounded source references in `last_grounded_document_refs`, and it had a history-grounded fallback path for turns with workspace search disabled. However, that fallback only ran when the current turn was not already performing retrieval or explicit mixed-source selection. A turn with a current PDF/source selection therefore bypassed the fallback and never merged in prior cited sources.
14+
15+
## Technical Details
16+
17+
Files modified:
18+
19+
- `application/single_app/route_backend_chats.py`
20+
- `application/single_app/config.py`
21+
- `functional_tests/test_chat_history_grounded_follow_up_fix.py`
22+
- `docs/explanation/release_notes.md`
23+
24+
Code changes:
25+
26+
- Added detection for explicit follow-up references to prior sources, including phrases such as "that XML file," "same template," and "previous spreadsheet."
27+
- Added a shared merge helper that combines current selected document IDs with revalidated prior grounded document IDs while preserving group and public workspace scope context.
28+
- Wired the merge helper into both standard and streaming chat paths before mixed-source manifest/search resolution.
29+
- Stored a compact `prior_grounded_source_merge` metadata marker on the current user message when prior sources are merged.
30+
- Prevented the no-search history grounding prompt from being inserted when prior sources have been merged into the current retrieval context.
31+
- Updated `config.py` from `0.250.156` to `0.250.157`.
32+
33+
## Testing Approach
34+
35+
Updated `functional_tests/test_chat_history_grounded_follow_up_fix.py` to verify:
36+
37+
- Prior-source reference phrases are detected for XML templates, same-source prompts, and previous spreadsheets.
38+
- Generic current-source prompts do not trigger prior-source reuse.
39+
- Current document IDs and prior grounded document IDs merge without duplicates.
40+
- Mixed personal/group scopes collapse to `all` with authorized group context preserved.
41+
- Standard and streaming chat paths both include prior-source merge wiring.
42+
43+
## Impact Analysis
44+
45+
Users can chain document workflows more naturally when a follow-up explicitly references a prior grounded source and also supplies or selects a current source. The prior source is still treated only as a reauthorization hint; source content and storage locators are not trusted from conversation history.
46+
47+
## Validation
48+
49+
- `python functional_tests\test_chat_history_grounded_follow_up_fix.py`
50+
51+
## Before and After
52+
53+
Before, a follow-up such as "add this PDF content into that XML file" could ground only on the selected PDF. After this fix, the route detects the reference to the prior XML source, revalidates it, and includes it alongside the current PDF before mixed-source execution.

0 commit comments

Comments
 (0)