Skip to content
Open
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: 2 additions & 0 deletions Foundry-IQ-Multi-Source/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
.outputs/
163 changes: 163 additions & 0 deletions Foundry-IQ-Multi-Source/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Foundry IQ with a Search index and external MCP source

This sample creates one Foundry IQ knowledge base that references both a
`SearchIndexKnowledgeSource` and an `McpServerKnowledgeSource`. A single
complex query combines fictional public release facts from an Azure AI Search
index with current product guidance from the public Microsoft Learn MCP server.
The run fails unless the returned `activity` and `references` prove that both
sources contributed.

## What the sample proves

```text
Complex query
|
v
Foundry IQ knowledge base
|-- SearchIndexKnowledgeSource --> bundled Contoso release brief
`-- McpServerKnowledgeSource ----> https://learn.microsoft.com/api/mcp
microsoft_docs_search
|
v
Synthesized answer + activity + references
```

The bundled query asks for a Wave B date and response-time SLO that exist only
in `data/release-brief.json`, plus current MCP retrieval guidance available
through Microsoft Learn. This makes a one-source answer incomplete by design.

## Prerequisites

- Python 3.11 or later.
- Azure AI Search in a region and API deployment that supports MCP Server
knowledge sources (`2026-05-01-preview`).
- Semantic ranker enabled on the Search service.
- An Azure OpenAI chat deployment supported by Foundry IQ.
- Permission to create indexes, knowledge sources, and knowledge bases.

The sample uses `DefaultAzureCredential` for Azure AI Search by default. Assign
your user the Search Service Contributor and Search Index Data Contributor
roles. For keyless model access, enable a managed identity on the Search
service and grant it Cognitive Services OpenAI User on the Azure OpenAI
resource. Optional key environment variables are supported for environments
where RBAC isn't configured.

## Run from scratch

```powershell
cd Foundry-IQ-Multi-Source
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -e ".[dev]"
Copy-Item sample.env .env
```

Edit `.env`, sign in, and run:

```powershell
az login
foundry-iq-multi-source run
```

The `run` action creates the index, uploads the bundled public sample data,
creates both knowledge sources and the knowledge base, executes the
cross-source query, verifies source participation, writes the full response to
`.outputs/latest-result.json`, and deletes the created resources.

Setup refuses to overwrite any existing resource with the configured names.
Each successful creation is recorded in the ignored
`.outputs/resources.json` ownership manifest. Cleanup deletes only resources
listed in that manifest and uses the creation ETag with a conditional delete.
A failed or interrupted run therefore can't delete unrelated, modified, or
replacement resources with similar names.

To inspect resources before cleanup:

```powershell
foundry-iq-multi-source run --keep-resources
foundry-iq-multi-source cleanup
```

The actions are also independently runnable:

```powershell
foundry-iq-multi-source setup
foundry-iq-multi-source query
foundry-iq-multi-source cleanup
```

## How both sources are made reliable

| Control | Setting | Why |
|---|---|---|
| Search request routing | `always_query_source=True` | The indexed release brief is always queried. |
| MCP tool inclusion | `inclusion_mode="always"` | Parsed MCP output isn't dropped during final reranking. |
| Initial reranker floor | `reranker_threshold=0.0` on both sources | Validation starts permissively so evidence isn't filtered before tuning. |
| Failure behavior | `fail_on_error=True` on both sources | A missing source is visible instead of producing a plausible partial answer. |
| Per-source document cap | `max_output_documents=50` | Uses the preview API's minimum accepted value (50–200). |
| Runtime budget | `max_runtime_in_seconds=120` | Remote MCP calls can take longer than index retrieval. |
| Trace visibility | `include_activity=True`, `include_references=True`, and `include_reference_source_data=True` | The response contains auditable routing and grounding evidence. |

`always_query_source` is intentionally omitted from
`McpServerKnowledgeSourceParams`: MCP Server knowledge sources don't support
that request-time control. The cross-source query and retrieval instructions
make the tool relevant, while tool-level `inclusion_mode="always"` keeps its
parsed output in the candidate set.

After the flow is stable, change MCP inclusion to `"reranked"` and increase
`reranker_threshold` gradually while checking the same trace verifier. Don't
optimize those settings from answer text alone.

## Expected evidence

The CLI prints a compact result similar to:

```json
{
"search": {
"knowledge_source_name": "foundry-iq-search-source",
"activity_count": 1,
"reference_count": 2
},
"mcp": {
"knowledge_source_name": "foundry-iq-learn-mcp-source",
"activity_count": 1,
"reference_count": 3
},
"both_participated": true
}
```

The verifier doesn't trust names in prose. Each reference must either name the
knowledge source or link through `activitySource` to an activity record for
that source.

## Troubleshooting

- **`mcpServer` is rejected during knowledge-source creation:** The Search
service deployment or region doesn't expose the preview feature.
- **Only the Search source appears:** Keep the query's explicit request for
current Microsoft Learn guidance, retain MCP `inclusion_mode="always"`, and
inspect MCP activity errors.
- **MCP activity exists but no MCP reference survives:** Keep the initial
`reranker_threshold=0.0`; confirm `include_references` and
`include_reference_source_data` are enabled.
- **The request times out:** Increase `max_runtime_in_seconds`; don't lower it
below the latency budget required by the remote tool.
- **Model authorization fails:** Grant the Search service managed identity
Cognitive Services OpenAI User, or set `AZURE_OPENAI_API_KEY` locally.
- **A configured resource name already exists:** Choose different names in
`.env`. The sample never updates or deletes a resource it didn't create.

## External service boundary

The Microsoft Learn MCP endpoint is public and unauthenticated, but content
still leaves the Search service boundary for the external tool call. Review
the MCP server's terms, data-handling behavior, network reachability, and
compliance fit before replacing the public sample with enterprise data.

References:

- [Create an MCP Server knowledge source](https://learn.microsoft.com/azure/search/agentic-knowledge-source-how-to-mcp-server)
- [Create a knowledge base](https://learn.microsoft.com/azure/search/agentic-retrieval-how-to-create-knowledge-base)
- [Retrieve from a knowledge base](https://learn.microsoft.com/azure/search/agentic-retrieval-how-to-retrieve)
20 changes: 20 additions & 0 deletions Foundry-IQ-Multi-Source/data/release-brief.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"id": "trail-release-wave-b",
"title": "Contoso Trail Guide public release brief",
"content": "Wave B of the Contoso Trail Guide public demo is scheduled for August 18, 2026. The end-to-end response-time service objective is 75 seconds. The go-live reviewer must confirm that the final recommendation combines the release brief with current platform guidance rather than answering from either source alone.",
"source_url": "data/release-brief.json#trail-release-wave-b"
},
{
"id": "trail-release-evidence",
"title": "Contoso Trail Guide evidence checklist",
"content": "The release gate requires machine-readable evidence that both the indexed release brief and the external documentation source contributed to the answer. A prose claim that both sources ran is not sufficient.",
"source_url": "data/release-brief.json#trail-release-evidence"
},
{
"id": "trail-release-data-policy",
"title": "Contoso Trail Guide sample data policy",
"content": "This Contoso demonstration uses only fictional release facts and public Microsoft documentation. It must not store credentials, tenant content, or customer data in the index or repository.",
"source_url": "data/release-brief.json#trail-release-data-policy"
}
]
27 changes: 27 additions & 0 deletions Foundry-IQ-Multi-Source/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "foundry-iq-multi-source"
version = "0.1.0"
description = "Foundry IQ sample combining a Search index and an external MCP server."
requires-python = ">=3.11"
dependencies = [
"azure-identity>=1.23.0",
"azure-search-documents==12.1.0b1",
"python-dotenv>=1.1.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.4.0"]

[project.scripts]
foundry-iq-multi-source = "foundry_iq_multi_source.cli:main"

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]

[tool.setuptools.packages.find]
where = ["src"]
19 changes: 19 additions & 0 deletions Foundry-IQ-Multi-Source/sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Required Azure resources. Copy this file to .env and replace placeholders.
SEARCH_ENDPOINT=https://your-search-service.search.windows.net
AZURE_OPENAI_ENDPOINT=https://your-openai-resource.openai.azure.com
AZURE_OPENAI_DEPLOYMENT=gpt-5-mini
AZURE_OPENAI_MODEL=gpt-5-mini

# Optional deterministic resource names.
INDEX_NAME=foundry-iq-multi-source-index
SEARCH_KNOWLEDGE_SOURCE_NAME=foundry-iq-search-source
MCP_KNOWLEDGE_SOURCE_NAME=foundry-iq-learn-mcp-source
KNOWLEDGE_BASE_NAME=foundry-iq-multi-source-kb

# The public Microsoft Learn MCP server requires no secret.
MCP_SERVER_URL=https://learn.microsoft.com/api/mcp
MCP_TOOL_NAME=microsoft_docs_search

# Optional key-based fallback. Prefer Azure RBAC and managed identity.
# SEARCH_API_KEY=
# AZURE_OPENAI_API_KEY=
14 changes: 14 additions & 0 deletions Foundry-IQ-Multi-Source/src/foundry_iq_multi_source/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Foundry IQ multi-source sample."""

from .sample import build_knowledge_base, build_mcp_knowledge_source
from .sample import build_retrieval_request, build_search_knowledge_source
from .trace import DualSourceEvidence, verify_dual_source_evidence

__all__ = [
"DualSourceEvidence",
"build_knowledge_base",
"build_mcp_knowledge_source",
"build_retrieval_request",
"build_search_knowledge_source",
"verify_dual_source_evidence",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .cli import main

main()
68 changes: 68 additions & 0 deletions Foundry-IQ-Multi-Source/src/foundry_iq_multi_source/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from __future__ import annotations

import argparse
import json
import sys

from .config import DEFAULT_QUERY, Settings
from .sample import MultiSourceSample


def main() -> None:
parser = argparse.ArgumentParser(
description=(
"Run a Foundry IQ query across a Search index and an external MCP "
"server."
)
)
parser.add_argument(
"action",
choices=["run", "setup", "query", "cleanup"],
nargs="?",
default="run",
)
parser.add_argument("--query", default=DEFAULT_QUERY)
parser.add_argument(
"--keep-resources",
action="store_true",
help="Keep resources after the run action for inspection.",
)
args = parser.parse_args()

settings = Settings.from_env()
sample = MultiSourceSample(settings, settings.search_credential())
operation_failed = False
try:
if args.action in {"run", "setup"}:
sample.setup()
print("Created the index, both knowledge sources, and knowledge base.")
if args.action in {"run", "query"}:
answer, evidence = sample.query(args.query)
print("\nSynthesized answer\n------------------")
print(answer)
print("\nDual-source evidence\n--------------------")
print(json.dumps(evidence.as_dict(), indent=2))
print(f"\nFull response: {settings.output_path}")
if args.action == "cleanup":
sample.cleanup()
print("Deleted all sample resources.")
except Exception:
operation_failed = True
raise
finally:
if args.action == "run" and not args.keep_resources:
try:
sample.cleanup()
print("Deleted all sample resources.")
except Exception as cleanup_error:
if operation_failed:
print(
f"Cleanup also failed: {cleanup_error}",
file=sys.stderr,
)
else:
raise
finally:
sample.close()
else:
sample.close()
Loading