diff --git a/app/backend/approaches/approach.py b/app/backend/approaches/approach.py index bc2f918860..7dc81814bd 100644 --- a/app/backend/approaches/approach.py +++ b/app/backend/approaches/approach.py @@ -261,6 +261,7 @@ async def run_agentic_retrieval( filter_add_on: Optional[str] = None, minimum_reranker_score: Optional[float] = None, max_docs_for_reranker: Optional[int] = None, + results_merge_strategy: Optional[str] = None, ) -> tuple[KnowledgeAgentRetrievalResponse, list[Document]]: # STEP 1: Invoke agentic retrieval response = await agent_client.retrieve( @@ -298,7 +299,13 @@ async def run_agentic_retrieval( results = [] if response and response.references: - for reference in response.references: + if results_merge_strategy == "interleaved": + # Use interleaved reference order + references = sorted(response.references, key=lambda reference: int(reference.id)) + else: + # Default to descending strategy + references = response.references + for reference in references: if isinstance(reference, KnowledgeAgentAzureSearchDocReference) and reference.source_data: results.append( Document( diff --git a/app/backend/approaches/chatreadretrieveread.py b/app/backend/approaches/chatreadretrieveread.py index 772529ec7d..ed87976e3b 100644 --- a/app/backend/approaches/chatreadretrieveread.py +++ b/app/backend/approaches/chatreadretrieveread.py @@ -235,6 +235,7 @@ async def run_agentic_retrieval_approach( search_index_filter = self.build_filter(overrides, auth_claims) top = overrides.get("top", 3) max_subqueries = overrides.get("max_subqueries", 10) + results_merge_strategy = overrides.get("results_merge_strategy", "interleaved") # 50 is the amount of documents that the reranker can process per query max_docs_for_reranker = max_subqueries * 50 @@ -246,6 +247,7 @@ async def run_agentic_retrieval_approach( filter_add_on=search_index_filter, minimum_reranker_score=minimum_reranker_score, max_docs_for_reranker=max_docs_for_reranker, + results_merge_strategy=results_merge_strategy, ) text_sources = self.get_sources_content(results, use_semantic_captions=False, use_image_citation=False) @@ -259,6 +261,7 @@ async def run_agentic_retrieval_approach( { "reranker_threshold": minimum_reranker_score, "max_docs_for_reranker": max_docs_for_reranker, + "results_merge_strategy": results_merge_strategy, "filter": search_index_filter, }, ), diff --git a/app/backend/approaches/retrievethenread.py b/app/backend/approaches/retrievethenread.py index 032fa1a90c..d59f903b0e 100644 --- a/app/backend/approaches/retrievethenread.py +++ b/app/backend/approaches/retrievethenread.py @@ -186,6 +186,7 @@ async def run_agentic_retrieval_approach( search_index_filter = self.build_filter(overrides, auth_claims) top = overrides.get("top", 3) max_subqueries = overrides.get("max_subqueries", 10) + results_merge_strategy = overrides.get("results_merge_strategy", "interleaved") # 50 is the amount of documents that the reranker can process per query max_docs_for_reranker = max_subqueries * 50 @@ -197,6 +198,7 @@ async def run_agentic_retrieval_approach( filter_add_on=search_index_filter, minimum_reranker_score=minimum_reranker_score, max_docs_for_reranker=max_docs_for_reranker, + results_merge_strategy=results_merge_strategy, ) text_sources = self.get_sources_content(results, use_semantic_captions=False, use_image_citation=False) @@ -210,6 +212,7 @@ async def run_agentic_retrieval_approach( { "reranker_threshold": minimum_reranker_score, "max_docs_for_reranker": max_docs_for_reranker, + "results_merge_strategy": results_merge_strategy, "filter": search_index_filter, }, ), diff --git a/app/frontend/src/api/models.ts b/app/frontend/src/api/models.ts index d6014ba25f..63ff5c31f4 100644 --- a/app/frontend/src/api/models.ts +++ b/app/frontend/src/api/models.ts @@ -27,6 +27,7 @@ export type ChatAppRequestOverrides = { seed?: number; top?: number; max_subqueries?: number; + results_merge_strategy?: string; temperature?: number; minimum_search_score?: number; minimum_reranker_score?: number; diff --git a/app/frontend/src/components/Settings/Settings.tsx b/app/frontend/src/components/Settings/Settings.tsx index 7d101abf36..a1c4f46632 100644 --- a/app/frontend/src/components/Settings/Settings.tsx +++ b/app/frontend/src/components/Settings/Settings.tsx @@ -15,6 +15,7 @@ export interface SettingsProps { temperature: number; retrieveCount: number; maxSubqueryCount: number; + resultsMergeStrategy: string; seed: number | null; minimumSearchScore: number; minimumRerankerScore: number; @@ -55,6 +56,7 @@ export const Settings = ({ temperature, retrieveCount, maxSubqueryCount, + resultsMergeStrategy, seed, minimumSearchScore, minimumRerankerScore, @@ -108,6 +110,7 @@ export const Settings = ({ const retrieveCountFieldId = useId("retrieveCountField"); const maxSubqueryCountId = useId("maxSubqueryCount"); const maxSubqueryCountFieldId = useId("maxSubqueryCountField"); + const resultsMergeStrategyFieldId = useId("resultsMergeStrategy"); const includeCategoryId = useId("includeCategory"); const includeCategoryFieldId = useId("includeCategoryField"); const excludeCategoryId = useId("excludeCategory"); @@ -218,8 +221,8 @@ export const Settings = ({ className={styles.settingsSeparator} label={t("labels.maxSubqueryCount")} type="number" - min={1} - max={20} + min={2} + max={40} defaultValue={maxSubqueryCount.toString()} onChange={(_ev, val) => onChange("maxSubqueryCount", parseInt(val || "10"))} aria-labelledby={maxSubqueryCountId} @@ -227,6 +230,24 @@ export const Settings = ({ /> )} + {showAgenticRetrievalOption && useAgenticRetrieval && ( + , option?: IDropdownOption) => + onChange("resultsMergeStrategy", option?.key) + } + aria-labelledby={includeCategoryId} + options={[ + { key: "interleaved", text: t("labels.resultsMergeStrategyOptions.interleaved") }, + { key: "descending", text: t("labels.resultsMergeStrategyOptions.descending") } + ]} + onRenderLabel={props => renderLabel(props, includeCategoryId, includeCategoryFieldId, t("helpTexts.resultsMergeStrategy"))} + /> + )} + (RetrievalMode.Hybrid); const [retrieveCount, setRetrieveCount] = useState(3); const [maxSubqueryCount, setMaxSubqueryCount] = useState(10); + const [resultsMergeStrategy, setResultsMergeStrategy] = useState("interleaved"); const [useSemanticRanker, setUseSemanticRanker] = useState(true); const [useSemanticCaptions, setUseSemanticCaptions] = useState(false); const [useQueryRewriting, setUseQueryRewriting] = useState(false); @@ -102,6 +103,9 @@ export function Component(): JSX.Element { setShowSpeechOutputAzure(config.showSpeechOutputAzure); setShowAgenticRetrievalOption(config.showAgenticRetrievalOption); setUseAgenticRetrieval(config.showAgenticRetrievalOption); + if (config.showAgenticRetrievalOption) { + setRetrieveCount(10); + } }); }; @@ -136,6 +140,7 @@ export function Component(): JSX.Element { exclude_category: excludeCategory.length === 0 ? undefined : excludeCategory, top: retrieveCount, max_subqueries: maxSubqueryCount, + results_merge_strategy: resultsMergeStrategy, temperature: temperature, minimum_reranker_score: minimumRerankerScore, minimum_search_score: minimumSearchScore, @@ -196,6 +201,9 @@ export function Component(): JSX.Element { case "maxSubqueryCount": setMaxSubqueryCount(value); break; + case "resultsMergeStrategy": + setResultsMergeStrategy(value); + break; case "useSemanticRanker": setUseSemanticRanker(value); break; @@ -347,6 +355,7 @@ export function Component(): JSX.Element { temperature={temperature} retrieveCount={retrieveCount} maxSubqueryCount={maxSubqueryCount} + resultsMergeStrategy={resultsMergeStrategy} seed={seed} minimumSearchScore={minimumSearchScore} minimumRerankerScore={minimumRerankerScore} diff --git a/app/frontend/src/pages/chat/Chat.tsx b/app/frontend/src/pages/chat/Chat.tsx index 8813d5c4a7..579ad05fea 100644 --- a/app/frontend/src/pages/chat/Chat.tsx +++ b/app/frontend/src/pages/chat/Chat.tsx @@ -47,6 +47,7 @@ const Chat = () => { const [minimumSearchScore, setMinimumSearchScore] = useState(0); const [retrieveCount, setRetrieveCount] = useState(3); const [maxSubqueryCount, setMaxSubqueryCount] = useState(10); + const [resultsMergeStrategy, setResultsMergeStrategy] = useState("interleaved"); const [retrievalMode, setRetrievalMode] = useState(RetrievalMode.Hybrid); const [useSemanticRanker, setUseSemanticRanker] = useState(true); const [useQueryRewriting, setUseQueryRewriting] = useState(false); @@ -135,6 +136,9 @@ const Chat = () => { setShowChatHistoryCosmos(config.showChatHistoryCosmos); setShowAgenticRetrievalOption(config.showAgenticRetrievalOption); setUseAgenticRetrieval(config.showAgenticRetrievalOption); + if (config.showAgenticRetrievalOption) { + setRetrieveCount(10); + } }); }; @@ -215,6 +219,8 @@ const Chat = () => { include_category: includeCategory.length === 0 ? undefined : includeCategory, exclude_category: excludeCategory.length === 0 ? undefined : excludeCategory, top: retrieveCount, + max_subqueries: maxSubqueryCount, + results_merge_strategy: resultsMergeStrategy, temperature: temperature, minimum_reranker_score: minimumRerankerScore, minimum_search_score: minimumSearchScore, @@ -312,6 +318,9 @@ const Chat = () => { case "maxSubqueryCount": setMaxSubqueryCount(value); break; + case "resultsMergeStrategy": + setResultsMergeStrategy(value); + break; case "useSemanticRanker": setUseSemanticRanker(value); break; @@ -534,6 +543,7 @@ const Chat = () => { temperature={temperature} retrieveCount={retrieveCount} maxSubqueryCount={maxSubqueryCount} + resultsMergeStrategy={resultsMergeStrategy} seed={seed} minimumSearchScore={minimumSearchScore} minimumRerankerScore={minimumRerankerScore} diff --git a/tests/snapshots/test_app/test_ask_rtr_text_agent/agent_client0/result.json b/tests/snapshots/test_app/test_ask_rtr_text_agent/agent_client0/result.json index 827d9ace6d..ec9bbf3360 100644 --- a/tests/snapshots/test_app/test_ask_rtr_text_agent/agent_client0/result.json +++ b/tests/snapshots/test_app/test_ask_rtr_text_agent/agent_client0/result.json @@ -18,7 +18,8 @@ "props": { "filter": null, "max_docs_for_reranker": 500, - "reranker_threshold": 0 + "reranker_threshold": 0, + "results_merge_strategy": "interleaved" }, "title": "Use agentic retrieval" }, diff --git a/tests/snapshots/test_app/test_ask_rtr_text_agent_filter/agent_auth_client0/result.json b/tests/snapshots/test_app/test_ask_rtr_text_agent_filter/agent_auth_client0/result.json index 909fbe1fda..029348cb23 100644 --- a/tests/snapshots/test_app/test_ask_rtr_text_agent_filter/agent_auth_client0/result.json +++ b/tests/snapshots/test_app/test_ask_rtr_text_agent_filter/agent_auth_client0/result.json @@ -18,7 +18,8 @@ "props": { "filter": "category ne 'excluded' and (oids/any(g:search.in(g, 'OID_X')) or groups/any(g:search.in(g, 'GROUP_Y, GROUP_Z')))", "max_docs_for_reranker": 500, - "reranker_threshold": 0 + "reranker_threshold": 0, + "results_merge_strategy": "interleaved" }, "title": "Use agentic retrieval" }, diff --git a/tests/snapshots/test_app/test_chat_text_agent/agent_client0/result.json b/tests/snapshots/test_app/test_chat_text_agent/agent_client0/result.json index 9e618d61b1..5f813bd45d 100644 --- a/tests/snapshots/test_app/test_chat_text_agent/agent_client0/result.json +++ b/tests/snapshots/test_app/test_chat_text_agent/agent_client0/result.json @@ -18,7 +18,8 @@ "props": { "filter": null, "max_docs_for_reranker": 500, - "reranker_threshold": 0 + "reranker_threshold": 0, + "results_merge_strategy": "interleaved" }, "title": "Use agentic retrieval" }, diff --git a/tests/snapshots/test_app/test_chat_text_filter_agent/agent_auth_client0/result.json b/tests/snapshots/test_app/test_chat_text_filter_agent/agent_auth_client0/result.json index 40e7000667..1069d6870e 100644 --- a/tests/snapshots/test_app/test_chat_text_filter_agent/agent_auth_client0/result.json +++ b/tests/snapshots/test_app/test_chat_text_filter_agent/agent_auth_client0/result.json @@ -18,7 +18,8 @@ "props": { "filter": "category ne 'excluded' and (oids/any(g:search.in(g, 'OID_X')) or groups/any(g:search.in(g, 'GROUP_Y, GROUP_Z')))", "max_docs_for_reranker": 500, - "reranker_threshold": 0 + "reranker_threshold": 0, + "results_merge_strategy": "interleaved" }, "title": "Use agentic retrieval" },