Skip to content

Commit e68c7e5

Browse files
taylorn-aiTaylorpamelafox
authored
feat: Add extra search index fields to Knowledge Agent response (#2696)
* Update coverage report generation command - Fix command to generate HTML report for coverage using `diff-cover` 🛠️ * Add support for agentic reference hydration - Introduce ENABLE_AGENTIC_REF_HYDRATION environment variable to control reference hydration behaviour 🌱 - Update Approach classes to accept hydrate_references parameter for managing reference hydration logic 🔧 - Modify document retrieval logic to hydrate references when enabled, improving data completeness 📄 * Enhance agentic retrieval with optional field hydration - ✨ Add support for enabling extra field hydration in agentic retrieval - 🔧 Update infrastructure to include new parameter for hydration - 📝 Modify documentation to reflect changes in usage instructions * Add tests for hydration support for agentic retrieval - 🎉 Introduce ENABLE_AGENTIC_REF_HYDRATION environment variable for configuration - 🧪 Implement mock search results for hydration testing in agentic retrieval - 🔍 Create tests for agentic retrieval with and without hydration enabled - 📜 Ensure hydrated results include additional fields from search results * Ran ruff and black on new tests * Ran black on changed files * Update test snapshots * Working on tests * Refactor mock retrieval functions for improved flexibility - 🎨 Introduce `create_mock_retrieve` to parameterise mock retrieval responses. - 🔄 Remove redundant mock search functions to streamline code. - 🧪 Update tests to use the new mock retrieval function for various scenarios. - 🧹 Clean up unused mock functions to enhance maintainability. * Rename env var to match API parameter * Revert CONTRIBUTING.md TOC change as unneeded --------- Co-authored-by: Taylor <[email protected]> Co-authored-by: Pamela Fox <[email protected]>
1 parent f2007b2 commit e68c7e5

File tree

15 files changed

+805
-161
lines changed

15 files changed

+805
-161
lines changed

app/backend/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ async def setup_clients():
471471
USE_CHAT_HISTORY_BROWSER = os.getenv("USE_CHAT_HISTORY_BROWSER", "").lower() == "true"
472472
USE_CHAT_HISTORY_COSMOS = os.getenv("USE_CHAT_HISTORY_COSMOS", "").lower() == "true"
473473
USE_AGENTIC_RETRIEVAL = os.getenv("USE_AGENTIC_RETRIEVAL", "").lower() == "true"
474+
ENABLE_AGENTIC_RETRIEVAL_SOURCE_DATA = os.getenv("ENABLE_AGENTIC_RETRIEVAL_SOURCE_DATA", "").lower() == "true"
474475

475476
# WEBSITE_HOSTNAME is always set by App Service, RUNNING_IN_PRODUCTION is set in main.bicep
476477
RUNNING_ON_AZURE = os.getenv("WEBSITE_HOSTNAME") is not None or os.getenv("RUNNING_IN_PRODUCTION") is not None
@@ -689,6 +690,7 @@ async def setup_clients():
689690
query_speller=AZURE_SEARCH_QUERY_SPELLER,
690691
prompt_manager=prompt_manager,
691692
reasoning_effort=OPENAI_REASONING_EFFORT,
693+
hydrate_references=ENABLE_AGENTIC_RETRIEVAL_SOURCE_DATA,
692694
multimodal_enabled=USE_MULTIMODAL,
693695
image_embeddings_client=image_embeddings_client,
694696
global_blob_manager=global_blob_manager,
@@ -716,6 +718,7 @@ async def setup_clients():
716718
query_speller=AZURE_SEARCH_QUERY_SPELLER,
717719
prompt_manager=prompt_manager,
718720
reasoning_effort=OPENAI_REASONING_EFFORT,
721+
hydrate_references=ENABLE_AGENTIC_RETRIEVAL_SOURCE_DATA,
719722
multimodal_enabled=USE_MULTIMODAL,
720723
image_embeddings_client=image_embeddings_client,
721724
global_blob_manager=global_blob_manager,

app/backend/approaches/approach.py

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def __init__(
162162
openai_host: str,
163163
prompt_manager: PromptManager,
164164
reasoning_effort: Optional[str] = None,
165+
hydrate_references: bool = False,
165166
multimodal_enabled: bool = False,
166167
image_embeddings_client: Optional[ImageEmbeddings] = None,
167168
global_blob_manager: Optional[BlobManager] = None,
@@ -179,6 +180,7 @@ def __init__(
179180
self.openai_host = openai_host
180181
self.prompt_manager = prompt_manager
181182
self.reasoning_effort = reasoning_effort
183+
self.hydrate_references = hydrate_references
182184
self.include_token_usage = True
183185
self.multimodal_enabled = multimodal_enabled
184186
self.image_embeddings_client = image_embeddings_client
@@ -236,7 +238,7 @@ async def search(
236238
vector_queries=search_vectors,
237239
)
238240

239-
documents = []
241+
documents: list[Document] = []
240242
async for page in results.by_page():
241243
async for document in page:
242244
documents.append(
@@ -299,40 +301,112 @@ async def run_agentic_retrieval(
299301
)
300302
)
301303

302-
# STEP 2: Generate a contextual and content specific answer using the search results and chat history
304+
# Map activity id -> agent's internal search query
303305
activities = response.activity
304-
activity_mapping = (
306+
activity_mapping: dict[int, str] = (
305307
{
306-
activity.id: activity.query.search if activity.query else ""
308+
activity.id: activity.query.search
307309
for activity in activities
308-
if isinstance(activity, KnowledgeAgentSearchActivityRecord)
310+
if (
311+
isinstance(activity, KnowledgeAgentSearchActivityRecord)
312+
and activity.query
313+
and activity.query.search is not None
314+
)
309315
}
310316
if activities
311317
else {}
312318
)
313319

314-
results = []
315-
if response and response.references:
316-
if results_merge_strategy == "interleaved":
317-
# Use interleaved reference order
318-
references = sorted(response.references, key=lambda reference: int(reference.id))
319-
else:
320-
# Default to descending strategy
321-
references = response.references
322-
for reference in references:
323-
if isinstance(reference, KnowledgeAgentAzureSearchDocReference) and reference.source_data:
324-
results.append(
320+
# No refs? we're done
321+
if not (response and response.references):
322+
return response, []
323+
324+
# Extract references
325+
refs = [r for r in response.references if isinstance(r, KnowledgeAgentAzureSearchDocReference)]
326+
327+
documents: list[Document] = []
328+
329+
if self.hydrate_references:
330+
# Hydrate references to get full documents
331+
documents = await self.hydrate_agent_references(
332+
references=refs,
333+
top=top,
334+
)
335+
else:
336+
# Create documents from reference source data
337+
for ref in refs:
338+
if ref.source_data:
339+
documents.append(
325340
Document(
326-
id=reference.doc_key,
327-
content=reference.source_data["content"],
328-
sourcepage=reference.source_data["sourcepage"],
329-
search_agent_query=activity_mapping[reference.activity_source],
341+
id=ref.doc_key,
342+
content=ref.source_data.get("content"),
343+
sourcepage=ref.source_data.get("sourcepage"),
330344
)
331345
)
332-
if top and len(results) == top:
333-
break
346+
if top and len(documents) >= top:
347+
break
348+
349+
# Build mappings for agent queries and sorting
350+
ref_to_activity: dict[str, int] = {}
351+
doc_to_ref_id: dict[str, str] = {}
352+
for ref in refs:
353+
if ref.doc_key:
354+
ref_to_activity[ref.doc_key] = ref.activity_source
355+
doc_to_ref_id[ref.doc_key] = ref.id
356+
357+
# Inject agent search queries into all documents
358+
for doc in documents:
359+
if doc.id and doc.id in ref_to_activity:
360+
activity_id = ref_to_activity[doc.id]
361+
doc.search_agent_query = activity_mapping.get(activity_id, "")
362+
363+
# Apply sorting strategy to the documents
364+
if results_merge_strategy == "interleaved": # Use interleaved reference order
365+
documents = sorted(
366+
documents,
367+
key=lambda d: int(doc_to_ref_id.get(d.id, 0)) if d.id and doc_to_ref_id.get(d.id) else 0,
368+
)
369+
# else: Default - preserve original order
370+
371+
return response, documents
372+
373+
async def hydrate_agent_references(
374+
self,
375+
references: list[KnowledgeAgentAzureSearchDocReference],
376+
top: Optional[int],
377+
) -> list[Document]:
378+
doc_keys: set[str] = set()
379+
380+
for ref in references:
381+
if not ref.doc_key:
382+
continue
383+
doc_keys.add(ref.doc_key)
384+
if top and len(doc_keys) >= top:
385+
break
386+
387+
if not doc_keys:
388+
return []
389+
390+
# Build search filter only on unique doc IDs
391+
id_csv = ",".join(doc_keys)
392+
id_filter = f"search.in(id, '{id_csv}', ',')"
393+
394+
# Fetch full documents
395+
hydrated_docs: list[Document] = await self.search(
396+
top=len(doc_keys),
397+
query_text=None,
398+
filter=id_filter,
399+
vectors=[],
400+
use_text_search=False,
401+
use_vector_search=False,
402+
use_semantic_ranker=False,
403+
use_semantic_captions=False,
404+
minimum_search_score=None,
405+
minimum_reranker_score=None,
406+
use_query_rewriting=False,
407+
)
334408

335-
return response, results
409+
return hydrated_docs
336410

337411
async def get_sources_content(
338412
self,

app/backend/approaches/chatreadretrieveread.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(
5757
query_speller: str,
5858
prompt_manager: PromptManager,
5959
reasoning_effort: Optional[str] = None,
60+
hydrate_references: bool = False,
6061
multimodal_enabled: bool = False,
6162
image_embeddings_client: Optional[ImageEmbeddings] = None,
6263
global_blob_manager: Optional[BlobManager] = None,
@@ -84,6 +85,7 @@ def __init__(
8485
self.query_rewrite_tools = self.prompt_manager.load_tools("chat_query_rewrite_tools.json")
8586
self.answer_prompt = self.prompt_manager.load_prompt("chat_answer_question.prompty")
8687
self.reasoning_effort = reasoning_effort
88+
self.hydrate_references = hydrate_references
8789
self.include_token_usage = True
8890
self.multimodal_enabled = multimodal_enabled
8991
self.image_embeddings_client = image_embeddings_client

app/backend/approaches/retrievethenread.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(
4646
query_speller: str,
4747
prompt_manager: PromptManager,
4848
reasoning_effort: Optional[str] = None,
49+
hydrate_references: bool = False,
4950
multimodal_enabled: bool = False,
5051
image_embeddings_client: Optional[ImageEmbeddings] = None,
5152
global_blob_manager: Optional[BlobManager] = None,
@@ -73,6 +74,7 @@ def __init__(
7374
self.answer_prompt = self.prompt_manager.load_prompt("ask_answer_question.prompty")
7475
self.reasoning_effort = reasoning_effort
7576
self.include_token_usage = True
77+
self.hydrate_references = hydrate_references
7678
self.multimodal_enabled = multimodal_enabled
7779
self.image_embeddings_client = image_embeddings_client
7880
self.global_blob_manager = global_blob_manager

docs/agentic_retrieval.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,33 @@ See the agentic retrieval documentation.
3434
azd env set AZURE_OPENAI_SEARCHAGENT_MODEL_VERSION 2025-04-14
3535
```
3636

37-
3. **Update the infrastructure and application:**
37+
3. **(Optional) Enable extra field hydration**
38+
39+
By default, agentic retrieval only returns fields included in the semantic configuration.
40+
41+
You can enable this optional feature below, to include all fields from the search index in the result.
42+
⚠️ This feature is currently only compatible with indexes set up with integrated vectorization,
43+
or indexes that otherwise have an "id" field marked as filterable.
44+
45+
```shell
46+
azd env set ENABLE_AGENTIC_RETRIEVAL_SOURCE_DATA true
47+
```
48+
49+
4. **Update the infrastructure and application:**
3850

3951
Execute `azd up` to provision the infrastructure changes (only the new model, if you ran `up` previously) and deploy the application code with the updated environment variables.
4052

41-
4. **Try out the feature:**
53+
5. **Try out the feature:**
4254

4355
Open the web app and start a new chat. Agentic retrieval will be used to find all sources.
4456

45-
5. **Experiment with max subqueries:**
57+
6. **Experiment with max subqueries:**
4658

4759
Select the developer options in the web app and change max subqueries to any value between 1 and 20. This controls the maximum amount of subqueries that can be created in the query plan.
4860

4961
![Max subqueries screenshot](./images/max-subqueries.png)
5062

51-
6. **Review the query plan**
63+
7. **Review the query plan**
5264

5365
Agentic retrieval use additional billed tokens behind the scenes for the planning process.
5466
To see the token usage, select the lightbulb icon on a chat answer. This will open the "Thought process" tab, which shows the amount of tokens used by and the queries produced by the planning process

infra/main.bicep

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ param storageSkuName string // Set in main.parameters.json
4141

4242
param defaultReasoningEffort string // Set in main.parameters.json
4343
param useAgenticRetrieval bool // Set in main.parameters.json
44+
param enableAgenticRetrievalSourceData bool // Set in main.parameters.json
4445

4546
param userStorageAccountName string = ''
4647
param userStorageContainerName string = 'user-content'
@@ -423,6 +424,7 @@ var appEnvVariables = {
423424
USE_SPEECH_OUTPUT_BROWSER: useSpeechOutputBrowser
424425
USE_SPEECH_OUTPUT_AZURE: useSpeechOutputAzure
425426
USE_AGENTIC_RETRIEVAL: useAgenticRetrieval
427+
ENABLE_AGENTIC_RETRIEVAL_SOURCE_DATA: enableAgenticRetrievalSourceData
426428
// Chat history settings
427429
USE_CHAT_HISTORY_BROWSER: useChatHistoryBrowser
428430
USE_CHAT_HISTORY_COSMOS: useChatHistoryCosmos

infra/main.parameters.json

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -104,79 +104,79 @@
104104
"backendServiceName": {
105105
"value": "${AZURE_APP_SERVICE}"
106106
},
107-
"chatGptModelName":{
107+
"chatGptModelName": {
108108
"value": "${AZURE_OPENAI_CHATGPT_MODEL}"
109109
},
110110
"chatGptDeploymentName": {
111111
"value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT}"
112112
},
113-
"chatGptDeploymentVersion":{
113+
"chatGptDeploymentVersion": {
114114
"value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION}"
115115
},
116-
"chatGptDeploymentSkuName":{
116+
"chatGptDeploymentSkuName": {
117117
"value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT_SKU}"
118118
},
119-
"chatGptDeploymentCapacity":{
119+
"chatGptDeploymentCapacity": {
120120
"value": "${AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY}"
121121
},
122-
"embeddingModelName":{
122+
"embeddingModelName": {
123123
"value": "${AZURE_OPENAI_EMB_MODEL_NAME}"
124124
},
125125
"embeddingDeploymentName": {
126126
"value": "${AZURE_OPENAI_EMB_DEPLOYMENT}"
127127
},
128-
"embeddingDeploymentVersion":{
128+
"embeddingDeploymentVersion": {
129129
"value": "${AZURE_OPENAI_EMB_DEPLOYMENT_VERSION}"
130130
},
131-
"embeddingDeploymentSkuName":{
131+
"embeddingDeploymentSkuName": {
132132
"value": "${AZURE_OPENAI_EMB_DEPLOYMENT_SKU}"
133133
},
134-
"embeddingDeploymentCapacity":{
134+
"embeddingDeploymentCapacity": {
135135
"value": "${AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY}"
136136
},
137137
"embeddingDimensions": {
138138
"value": "${AZURE_OPENAI_EMB_DIMENSIONS}"
139139
},
140-
"evalModelName":{
140+
"evalModelName": {
141141
"value": "${AZURE_OPENAI_EVAL_MODEL}"
142142
},
143-
"evalModelVersion":{
143+
"evalModelVersion": {
144144
"value": "${AZURE_OPENAI_EVAL_MODEL_VERSION}"
145145
},
146146
"evalDeploymentName": {
147147
"value": "${AZURE_OPENAI_EVAL_DEPLOYMENT}"
148148
},
149-
"evalDeploymentSkuName":{
149+
"evalDeploymentSkuName": {
150150
"value": "${AZURE_OPENAI_EVAL_DEPLOYMENT_SKU}"
151151
},
152-
"evalDeploymentCapacity":{
152+
"evalDeploymentCapacity": {
153153
"value": "${AZURE_OPENAI_EVAL_DEPLOYMENT_CAPACITY}"
154154
},
155-
"searchAgentModelName":{
155+
"searchAgentModelName": {
156156
"value": "${AZURE_OPENAI_SEARCHAGENT_MODEL}"
157157
},
158-
"searchAgentModelVersion":{
158+
"searchAgentModelVersion": {
159159
"value": "${AZURE_OPENAI_SEARCHAGENT_MODEL_VERSION}"
160160
},
161161
"searchAgentDeploymentName": {
162162
"value": "${AZURE_OPENAI_SEARCHAGENT_DEPLOYMENT}"
163163
},
164-
"searchAgentDeploymentSkuName":{
164+
"searchAgentDeploymentSkuName": {
165165
"value": "${AZURE_OPENAI_SEARCHAGENT_DEPLOYMENT_SKU}"
166166
},
167-
"searchAgentDeploymentCapacity":{
167+
"searchAgentDeploymentCapacity": {
168168
"value": "${AZURE_OPENAI_SEARCHAGENT_DEPLOYMENT_CAPACITY}"
169169
},
170170
"openAiHost": {
171171
"value": "${OPENAI_HOST=azure}"
172172
},
173-
"azureOpenAiCustomUrl":{
173+
"azureOpenAiCustomUrl": {
174174
"value": "${AZURE_OPENAI_CUSTOM_URL}"
175175
},
176-
"azureOpenAiApiVersion":{
176+
"azureOpenAiApiVersion": {
177177
"value": "${AZURE_OPENAI_API_VERSION}"
178178
},
179-
"azureOpenAiApiKey":{
179+
"azureOpenAiApiKey": {
180180
"value": "${AZURE_OPENAI_API_KEY_OVERRIDE}"
181181
},
182182
"azureOpenAiDisableKeys": {
@@ -324,7 +324,7 @@
324324
"value": "${DEPLOYMENT_TARGET=containerapps}"
325325
},
326326
"webAppExists": {
327-
"value": "${SERVICE_WEB_RESOURCE_EXISTS=false}"
327+
"value": "${SERVICE_WEB_RESOURCE_EXISTS=false}"
328328
},
329329
"azureContainerAppsWorkloadProfile": {
330330
"value": "${AZURE_CONTAINER_APPS_WORKLOAD_PROFILE=Consumption}"
@@ -338,6 +338,9 @@
338338
"useAgenticRetrieval": {
339339
"value": "${USE_AGENTIC_RETRIEVAL=false}"
340340
},
341+
"enableAgenticRetrievalSourceData": {
342+
"value": "${ENABLE_AGENTIC_RETRIEVAL_SOURCE_DATA=false}"
343+
},
341344
"ragSearchTextEmbeddings": {
342345
"value": "${RAG_SEARCH_TEXT_EMBEDDINGS=true}"
343346
},

0 commit comments

Comments
 (0)