Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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 .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Azure Search OpenAI Demo",
"image": "mcr.microsoft.com/devcontainers/python:3.11-bookworm",
"image": "mcr.microsoft.com/devcontainers/python:3.13-bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": {
// This should match the version of Node.js in Github Actions workflows
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/evaluate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ jobs:
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
version: "0.4.20"
version: "0.9.5"
cache-dependency-glob: "requirements**.txt"
python-version: "3.11"

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest", "windows-latest"]
python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
node_version: ["20.14", "22"]
steps:
- uses: actions/checkout@v5
Expand All @@ -36,7 +36,7 @@ jobs:
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
version: "0.4.20"
version: "0.9.5"
cache-dependency-glob: "requirements**.txt"
python-version: ${{ matrix.python_version }}
activate-environment: true
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.3
rev: v0.14.2
hooks:
- id: ruff
- repo: https://github.com/psf/black
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ When sending pull requests, make sure to follow the PULL_REQUEST_TEMPLATE.md for
To upgrade a particular package in the backend, use the following command, replacing `<package-name>` with the name of the package you want to upgrade:

```shell
cd app/backend && uv pip compile requirements.in -o requirements.txt --python-version 3.9 --upgrade-package package-name
cd app/backend && uv pip compile requirements.in -o requirements.txt --python-version 3.10 --upgrade-package package-name
```

## Checking Python type hints
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ A related option is VS Code Dev Containers, which will open the project in your
1. Install the required tools:

- [Azure Developer CLI](https://aka.ms/azure-dev/install)
- [Python 3.9, 3.10, or 3.11](https://www.python.org/downloads/)
- [Python 3.10, 3.11, 3.12, 3.13, or 3.14](https://www.python.org/downloads/)
- **Important**: Python and the pip package manager must be in the path in Windows for the setup scripts to work.
- **Important**: Ensure you can run `python --version` from console. On Ubuntu, you might need to run `sudo apt install python-is-python3` to link `python` to `python3`.
- [Node.js 20+](https://nodejs.org/download/)
Expand Down
2 changes: 1 addition & 1 deletion app/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-bullseye
FROM python:3.13-bookworm

WORKDIR /app

Expand Down
6 changes: 3 additions & 3 deletions app/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import mimetypes
import os
import time
from collections.abc import AsyncGenerator, Awaitable
from collections.abc import AsyncGenerator, Awaitable, Callable
from pathlib import Path
from typing import Any, Callable, Union, cast
from typing import Any, cast

from azure.cognitiveservices.speech import (
ResultReason,
Expand Down Expand Up @@ -477,7 +477,7 @@ async def setup_clients():
# Use the current user identity for keyless authentication to Azure services.
# This assumes you use 'azd auth login' locally, and managed identity when deployed on Azure.
# The managed identity is setup in the infra/ folder.
azure_credential: Union[AzureDeveloperCliCredential, ManagedIdentityCredential]
azure_credential: AzureDeveloperCliCredential | ManagedIdentityCredential
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, we can use | syntax!

azure_ai_token_provider: Callable[[], Awaitable[str]]
if RUNNING_ON_AZURE:
current_app.logger.info("Setting up Azure credential using ManagedIdentityCredential")
Expand Down
6 changes: 3 additions & 3 deletions app/backend/approaches/approach.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from abc import ABC
from collections.abc import AsyncGenerator, Awaitable
from dataclasses import dataclass, field
from typing import Any, Optional, TypedDict, Union, cast
from typing import Any, Optional, TypedDict, cast

from azure.search.documents.agent.aio import KnowledgeAgentRetrievalClient
from azure.search.documents.agent.models import (
Expand Down Expand Up @@ -190,7 +190,7 @@ def build_filter(self, overrides: dict[str, Any]) -> Optional[str]:
filters.append("category eq '{}'".format(include_category.replace("'", "''")))
if exclude_category:
filters.append("category ne '{}'".format(exclude_category.replace("'", "''")))
return None if len(filters) == 0 else " and ".join(filters)
return None if not filters else " and ".join(filters)
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The logic change from len(filters) == 0 to not filters is correct but unrelated to the type hint modernization that is the focus of this PR. This type of refactoring should be in a separate commit to keep changes focused and easier to review.

Suggested change
return None if not filters else " and ".join(filters)
return None if len(filters) == 0 else " and ".join(filters)

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that ruff did it, so it must be part of ruff check.


async def search(
self,
Expand Down Expand Up @@ -520,7 +520,7 @@ def create_chat_completion(
temperature: Optional[float] = None,
n: Optional[int] = None,
reasoning_effort: Optional[ChatCompletionReasoningEffort] = None,
) -> Union[Awaitable[ChatCompletion], Awaitable[AsyncStream[ChatCompletionChunk]]]:
) -> Awaitable[ChatCompletion] | Awaitable[AsyncStream[ChatCompletionChunk]]:
if chatgpt_model in self.GPT_REASONING_MODELS:
params: dict[str, Any] = {
# max_tokens is not supported
Expand Down
6 changes: 3 additions & 3 deletions app/backend/approaches/chatreadretrieveread.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import re
from collections.abc import AsyncGenerator, Awaitable
from typing import Any, Optional, Union, cast
from typing import Any, Optional, cast

from azure.search.documents.agent.aio import KnowledgeAgentRetrievalClient
from azure.search.documents.aio import SearchClient
Expand Down Expand Up @@ -215,7 +215,7 @@ async def run_until_final_call(
overrides: dict[str, Any],
auth_claims: dict[str, Any],
should_stream: bool = False,
) -> tuple[ExtraInfo, Union[Awaitable[ChatCompletion], Awaitable[AsyncStream[ChatCompletionChunk]]]]:
) -> tuple[ExtraInfo, Awaitable[ChatCompletion] | Awaitable[AsyncStream[ChatCompletionChunk]]]:
use_agentic_retrieval = True if overrides.get("use_agentic_retrieval") else False
original_user_query = messages[-1]["content"]

Expand Down Expand Up @@ -243,7 +243,7 @@ async def run_until_final_call(
)

chat_coroutine = cast(
Union[Awaitable[ChatCompletion], Awaitable[AsyncStream[ChatCompletionChunk]]],
Awaitable[ChatCompletion] | Awaitable[AsyncStream[ChatCompletionChunk]],
self.create_chat_completion(
self.chatgpt_deployment,
self.chatgpt_model,
Expand Down
6 changes: 2 additions & 4 deletions app/backend/chat_history/cosmosdb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import time
from typing import Any, Union
from typing import Any

from azure.cosmos.aio import ContainerProxy, CosmosClient
from azure.identity.aio import AzureDeveloperCliCredential, ManagedIdentityCredential
Expand Down Expand Up @@ -209,9 +209,7 @@ async def setup_clients():
AZURE_CHAT_HISTORY_DATABASE = os.getenv("AZURE_CHAT_HISTORY_DATABASE")
AZURE_CHAT_HISTORY_CONTAINER = os.getenv("AZURE_CHAT_HISTORY_CONTAINER")

azure_credential: Union[AzureDeveloperCliCredential, ManagedIdentityCredential] = current_app.config[
CONFIG_CREDENTIAL
]
azure_credential: AzureDeveloperCliCredential | ManagedIdentityCredential = current_app.config[CONFIG_CREDENTIAL]

if USE_CHAT_HISTORY_COSMOS:
current_app.logger.info("USE_CHAT_HISTORY_COSMOS is true, setting up CosmosDB client")
Expand Down
4 changes: 2 additions & 2 deletions app/backend/core/sessionhelper.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import uuid
from typing import Union
from typing import Optional


def create_session_id(
config_chat_history_cosmos_enabled: bool, config_chat_history_browser_enabled: bool
) -> Union[str, None]:
) -> Optional[str]:
if config_chat_history_cosmos_enabled:
return str(uuid.uuid4())
if config_chat_history_browser_enabled:
Expand Down
3 changes: 2 additions & 1 deletion app/backend/decorators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from collections.abc import Callable
from functools import wraps
from typing import Any, Callable, TypeVar, cast
from typing import Any, TypeVar, cast

from quart import abort, current_app, request

Expand Down
84 changes: 42 additions & 42 deletions app/backend/prepdocs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import os
from enum import Enum
from typing import Optional, Union
from typing import Optional

import aiohttp
from azure.core.credentials import AzureKeyCredential
Expand Down Expand Up @@ -45,7 +45,7 @@
logger = logging.getLogger("scripts")


def clean_key_if_exists(key: Union[str, None]) -> Union[str, None]:
def clean_key_if_exists(key: Optional[str]) -> Optional[str]:
"""Remove leading and trailing whitespace from a key if it exists. If the key is empty, return None."""
if key is not None and key.strip() != "":
return key.strip()
Expand All @@ -69,16 +69,16 @@ async def setup_search_info(
search_service: str,
index_name: str,
azure_credential: AsyncTokenCredential,
use_agentic_retrieval: Union[bool, None] = None,
azure_openai_endpoint: Union[str, None] = None,
agent_name: Union[str, None] = None,
agent_max_output_tokens: Union[int, None] = None,
azure_openai_searchagent_deployment: Union[str, None] = None,
azure_openai_searchagent_model: Union[str, None] = None,
search_key: Union[str, None] = None,
azure_vision_endpoint: Union[str, None] = None,
use_agentic_retrieval: Optional[bool] = None,
azure_openai_endpoint: Optional[str] = None,
agent_name: Optional[str] = None,
agent_max_output_tokens: Optional[int] = None,
azure_openai_searchagent_deployment: Optional[str] = None,
azure_openai_searchagent_model: Optional[str] = None,
search_key: Optional[str] = None,
azure_vision_endpoint: Optional[str] = None,
) -> SearchInfo:
search_creds: Union[AsyncTokenCredential, AzureKeyCredential] = (
search_creds: AsyncTokenCredential | AzureKeyCredential = (
azure_credential if search_key is None else AzureKeyCredential(search_key)
)
if use_agentic_retrieval and azure_openai_searchagent_model is None:
Expand All @@ -104,10 +104,10 @@ def setup_blob_manager(
storage_container: str,
storage_resource_group: str,
subscription_id: str,
storage_key: Union[str, None] = None,
image_storage_container: Union[str, None] = None, # Added this parameter
storage_key: Optional[str] = None,
image_storage_container: Optional[str] = None, # Added this parameter
):
storage_creds: Union[AsyncTokenCredential, str] = azure_credential if storage_key is None else storage_key
storage_creds: AsyncTokenCredential | str = azure_credential if storage_key is None else storage_key

return BlobManager(
endpoint=f"https://{storage_account}.blob.core.windows.net",
Expand All @@ -122,18 +122,18 @@ def setup_blob_manager(

def setup_list_file_strategy(
azure_credential: AsyncTokenCredential,
local_files: Union[str, None],
datalake_storage_account: Union[str, None],
datalake_filesystem: Union[str, None],
datalake_path: Union[str, None],
datalake_key: Union[str, None],
local_files: Optional[str],
datalake_storage_account: Optional[str],
datalake_filesystem: Optional[str],
datalake_path: Optional[str],
datalake_key: Optional[str],
enable_global_documents: bool = False,
):
list_file_strategy: ListFileStrategy
if datalake_storage_account:
if datalake_filesystem is None or datalake_path is None:
raise ValueError("DataLake file system and path are required when using Azure Data Lake Gen2")
adls_gen2_creds: Union[AsyncTokenCredential, str] = azure_credential if datalake_key is None else datalake_key
adls_gen2_creds: AsyncTokenCredential | str = azure_credential if datalake_key is None else datalake_key
logger.info("Using Data Lake Gen2 Storage Account: %s", datalake_storage_account)
list_file_strategy = ADLSGen2ListFileStrategy(
data_lake_storage_account=datalake_storage_account,
Expand Down Expand Up @@ -164,13 +164,13 @@ def setup_embeddings_service(
openai_host: OpenAIHost,
emb_model_name: str,
emb_model_dimensions: int,
azure_openai_service: Union[str, None],
azure_openai_custom_url: Union[str, None],
azure_openai_deployment: Union[str, None],
azure_openai_key: Union[str, None],
azure_openai_service: Optional[str],
azure_openai_custom_url: Optional[str],
azure_openai_deployment: Optional[str],
azure_openai_key: Optional[str],
azure_openai_api_version: str,
openai_key: Union[str, None],
openai_org: Union[str, None],
openai_key: Optional[str],
openai_org: Optional[str],
disable_vectors: bool = False,
disable_batch_vectors: bool = False,
):
Expand All @@ -179,7 +179,7 @@ def setup_embeddings_service(
return None

if openai_host in [OpenAIHost.AZURE, OpenAIHost.AZURE_CUSTOM]:
azure_open_ai_credential: Union[AsyncTokenCredential, AzureKeyCredential] = (
azure_open_ai_credential: AsyncTokenCredential | AzureKeyCredential = (
azure_credential if azure_openai_key is None else AzureKeyCredential(azure_openai_key)
)
return AzureOpenAIEmbeddingService(
Expand Down Expand Up @@ -207,12 +207,12 @@ def setup_embeddings_service(
def setup_openai_client(
openai_host: OpenAIHost,
azure_credential: AsyncTokenCredential,
azure_openai_api_key: Union[str, None] = None,
azure_openai_api_version: Union[str, None] = None,
azure_openai_service: Union[str, None] = None,
azure_openai_custom_url: Union[str, None] = None,
openai_api_key: Union[str, None] = None,
openai_organization: Union[str, None] = None,
azure_openai_api_key: Optional[str] = None,
azure_openai_api_version: Optional[str] = None,
azure_openai_service: Optional[str] = None,
azure_openai_custom_url: Optional[str] = None,
openai_api_key: Optional[str] = None,
openai_organization: Optional[str] = None,
):
if openai_host not in OpenAIHost:
raise ValueError(f"Invalid OPENAI_HOST value: {openai_host}. Must be one of {[h.value for h in OpenAIHost]}.")
Expand Down Expand Up @@ -264,23 +264,23 @@ def setup_openai_client(

def setup_file_processors(
azure_credential: AsyncTokenCredential,
document_intelligence_service: Union[str, None],
document_intelligence_key: Union[str, None] = None,
document_intelligence_service: Optional[str],
document_intelligence_key: Optional[str] = None,
local_pdf_parser: bool = False,
local_html_parser: bool = False,
use_content_understanding: bool = False,
use_multimodal: bool = False,
openai_client: Union[AsyncOpenAI, None] = None,
openai_model: Union[str, None] = None,
openai_deployment: Union[str, None] = None,
content_understanding_endpoint: Union[str, None] = None,
openai_client: Optional[AsyncOpenAI] = None,
openai_model: Optional[str] = None,
openai_deployment: Optional[str] = None,
content_understanding_endpoint: Optional[str] = None,
):
sentence_text_splitter = SentenceTextSplitter()

doc_int_parser: Optional[DocumentAnalysisParser] = None
# check if Azure Document Intelligence credentials are provided
if document_intelligence_service is not None:
documentintelligence_creds: Union[AsyncTokenCredential, AzureKeyCredential] = (
documentintelligence_creds: AsyncTokenCredential | AzureKeyCredential = (
azure_credential if document_intelligence_key is None else AzureKeyCredential(document_intelligence_key)
)
doc_int_parser = DocumentAnalysisParser(
Expand Down Expand Up @@ -348,8 +348,8 @@ def setup_file_processors(


def setup_image_embeddings_service(
azure_credential: AsyncTokenCredential, vision_endpoint: Union[str, None], use_multimodal: bool
) -> Union[ImageEmbeddings, None]:
azure_credential: AsyncTokenCredential, vision_endpoint: Optional[str], use_multimodal: bool
) -> Optional[ImageEmbeddings]:
image_embeddings_service: Optional[ImageEmbeddings] = None
if use_multimodal:
if vision_endpoint is None:
Expand Down
6 changes: 3 additions & 3 deletions app/backend/prepdocslib/blobmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import re
from pathlib import Path
from typing import IO, Any, Optional, TypedDict, Union
from typing import IO, Any, Optional, TypedDict
from urllib.parse import unquote

from azure.core.credentials_async import AsyncTokenCredential
Expand Down Expand Up @@ -169,7 +169,7 @@ async def _ensure_directory(self, directory_path: str, user_oid: str) -> DataLak
await directory_client.set_access_control(owner=user_oid)
return directory_client

async def upload_blob(self, file: Union[File, IO], filename: str, user_oid: str) -> str:
async def upload_blob(self, file: File | IO, filename: str, user_oid: str) -> str:
"""
Uploads a file directly to the user's directory in ADLS (no subdirectory).

Expand Down Expand Up @@ -393,7 +393,7 @@ def __init__(
self,
endpoint: str,
container: str,
credential: Union[AsyncTokenCredential, str],
credential: AsyncTokenCredential | str,
image_container: Optional[str] = None,
account: Optional[str] = None,
resource_group: Optional[str] = None,
Expand Down
Loading
Loading