Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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 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 @@ -478,7 +478,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
102 changes: 51 additions & 51 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, TypedDict, cast

from azure.search.documents.agent.aio import KnowledgeAgentRetrievalClient
from azure.search.documents.agent.models import (
Expand Down Expand Up @@ -39,18 +39,18 @@

@dataclass
class Document:
id: Optional[str] = None
content: Optional[str] = None
category: Optional[str] = None
sourcepage: Optional[str] = None
sourcefile: Optional[str] = None
oids: Optional[list[str]] = None
groups: Optional[list[str]] = None
captions: Optional[list[QueryCaptionResult]] = None
score: Optional[float] = None
reranker_score: Optional[float] = None
search_agent_query: Optional[str] = None
images: Optional[list[dict[str, Any]]] = None
id: str | None = None
content: str | None = None
category: str | None = None
sourcepage: str | None = None
sourcefile: str | None = None
oids: list[str] | None = None
groups: list[str] | None = None
captions: list[QueryCaptionResult] | None = None
score: float | None = None
reranker_score: float | None = None
search_agent_query: str | None = None
images: list[dict[str, Any]] | None = None

def serialize_for_results(self) -> dict[str, Any]:
result_dict = {
Expand Down Expand Up @@ -84,8 +84,8 @@ def serialize_for_results(self) -> dict[str, Any]:
@dataclass
class ThoughtStep:
title: str
description: Optional[Any]
props: Optional[dict[str, Any]] = None
description: Any | None
props: dict[str, Any] | None = None

def update_token_usage(self, usage: CompletionUsage) -> None:
if self.props:
Expand All @@ -94,23 +94,23 @@ def update_token_usage(self, usage: CompletionUsage) -> None:

@dataclass
class DataPoints:
text: Optional[list[str]] = None
images: Optional[list] = None
citations: Optional[list[str]] = None
text: list[str] | None = None
images: list | None = None
citations: list[str] | None = None


@dataclass
class ExtraInfo:
data_points: DataPoints
thoughts: list[ThoughtStep] = field(default_factory=list)
followup_questions: Optional[list[Any]] = None
followup_questions: list[Any] | None = None


@dataclass
class TokenUsageProps:
prompt_tokens: int
completion_tokens: int
reasoning_tokens: Optional[int]
reasoning_tokens: int | None
total_tokens: int

@classmethod
Expand Down Expand Up @@ -153,19 +153,19 @@ def __init__(
search_client: SearchClient,
openai_client: AsyncOpenAI,
auth_helper: AuthenticationHelper,
query_language: Optional[str],
query_speller: Optional[str],
embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text"
query_language: str | None,
query_speller: str | None,
embedding_deployment: str | None, # Not needed for non-Azure OpenAI or for retrieval_mode="text"
embedding_model: str,
embedding_dimensions: int,
embedding_field: str,
openai_host: str,
prompt_manager: PromptManager,
reasoning_effort: Optional[str] = None,
reasoning_effort: str | None = None,
multimodal_enabled: bool = False,
image_embeddings_client: Optional[ImageEmbeddings] = None,
global_blob_manager: Optional[BlobManager] = None,
user_blob_manager: Optional[AdlsBlobManager] = None,
image_embeddings_client: ImageEmbeddings | None = None,
global_blob_manager: BlobManager | None = None,
user_blob_manager: AdlsBlobManager | None = None,
):
self.search_client = search_client
self.openai_client = openai_client
Expand All @@ -185,7 +185,7 @@ def __init__(
self.global_blob_manager = global_blob_manager
self.user_blob_manager = user_blob_manager

def build_filter(self, overrides: dict[str, Any], auth_claims: dict[str, Any]) -> Optional[str]:
def build_filter(self, overrides: dict[str, Any], auth_claims: dict[str, Any]) -> str | None:
include_category = overrides.get("include_category")
exclude_category = overrides.get("exclude_category")
security_filter = self.auth_helper.build_security_filters(overrides, auth_claims)
Expand All @@ -201,16 +201,16 @@ def build_filter(self, overrides: dict[str, Any], auth_claims: dict[str, Any]) -
async def search(
self,
top: int,
query_text: Optional[str],
filter: Optional[str],
query_text: str | None,
filter: str | None,
vectors: list[VectorQuery],
use_text_search: bool,
use_vector_search: bool,
use_semantic_ranker: bool,
use_semantic_captions: bool,
minimum_search_score: Optional[float] = None,
minimum_reranker_score: Optional[float] = None,
use_query_rewriting: Optional[bool] = None,
minimum_search_score: float | None = None,
minimum_reranker_score: float | None = None,
use_query_rewriting: bool | None = None,
) -> list[Document]:
search_text = query_text if use_text_search else ""
search_vectors = vectors if use_vector_search else []
Expand Down Expand Up @@ -271,10 +271,10 @@ async def run_agentic_retrieval(
messages: list[ChatCompletionMessageParam],
agent_client: KnowledgeAgentRetrievalClient,
search_index_name: str,
top: Optional[int] = None,
filter_add_on: Optional[str] = None,
minimum_reranker_score: Optional[float] = None,
results_merge_strategy: Optional[str] = None,
top: int | None = None,
filter_add_on: str | None = None,
minimum_reranker_score: float | None = None,
results_merge_strategy: str | None = None,
) -> tuple[KnowledgeAgentRetrievalResponse, list[Document]]:
# STEP 1: Invoke agentic retrieval
response = await agent_client.retrieve(
Expand Down Expand Up @@ -358,7 +358,7 @@ async def get_sources_content(
use_semantic_captions: bool,
include_text_sources: bool,
download_image_sources: bool,
user_oid: Optional[str] = None,
user_oid: str | None = None,
) -> DataPoints:
"""Extract text/image sources & citations from documents.

Expand Down Expand Up @@ -408,15 +408,15 @@ def clean_source(s: str) -> str:
citations.append(self.get_image_citation(doc.sourcepage or "", img["url"]))
return DataPoints(text=text_sources, images=image_sources, citations=citations)

def get_citation(self, sourcepage: Optional[str]):
def get_citation(self, sourcepage: str | None):
return sourcepage or ""

def get_image_citation(self, sourcepage: Optional[str], image_url: str):
def get_image_citation(self, sourcepage: str | None, image_url: str):
sourcepage_citation = self.get_citation(sourcepage)
image_filename = image_url.split("/")[-1]
return f"{sourcepage_citation}({image_filename})"

async def download_blob_as_base64(self, blob_url: str, user_oid: Optional[str] = None) -> Optional[str]:
async def download_blob_as_base64(self, blob_url: str, user_oid: str | None = None) -> str | None:
"""
Downloads a blob from either Azure Blob Storage or Azure Data Lake Storage and returns it as a base64 encoded string.

Expand Down Expand Up @@ -484,7 +484,7 @@ async def compute_multimodal_embedding(self, q: str):
multimodal_query_vector = await self.image_embeddings_client.create_embedding_for_text(q)
return VectorizedQuery(vector=multimodal_query_vector, k_nearest_neighbors=50, fields="images/embedding")

def get_system_prompt_variables(self, override_prompt: Optional[str]) -> dict[str, str]:
def get_system_prompt_variables(self, override_prompt: str | None) -> dict[str, str]:
# Allows client to replace the entire prompt, or to inject into the existing prompt using >>>
if override_prompt is None:
return {}
Expand All @@ -511,17 +511,17 @@ def get_lowest_reasoning_effort(self, model: str) -> ChatCompletionReasoningEffo

def create_chat_completion(
self,
chatgpt_deployment: Optional[str],
chatgpt_deployment: str | None,
chatgpt_model: str,
messages: list[ChatCompletionMessageParam],
overrides: dict[str, Any],
response_token_limit: int,
should_stream: bool = False,
tools: Optional[list[ChatCompletionToolParam]] = None,
temperature: Optional[float] = None,
n: Optional[int] = None,
reasoning_effort: Optional[ChatCompletionReasoningEffort] = None,
) -> Union[Awaitable[ChatCompletion], Awaitable[AsyncStream[ChatCompletionChunk]]]:
tools: list[ChatCompletionToolParam] | None = None,
temperature: float | None = None,
n: int | None = None,
reasoning_effort: ChatCompletionReasoningEffort | None = None,
) -> Awaitable[ChatCompletion] | Awaitable[AsyncStream[ChatCompletionChunk]]:
if chatgpt_model in self.GPT_REASONING_MODELS:
params: dict[str, Any] = {
# max_tokens is not supported
Expand Down Expand Up @@ -562,9 +562,9 @@ def format_thought_step_for_chatcompletion(
messages: list[ChatCompletionMessageParam],
overrides: dict[str, Any],
model: str,
deployment: Optional[str],
usage: Optional[CompletionUsage] = None,
reasoning_effort: Optional[ChatCompletionReasoningEffort] = None,
deployment: str | None,
usage: CompletionUsage | None = None,
reasoning_effort: ChatCompletionReasoningEffort | None = None,
) -> ThoughtStep:
properties: dict[str, Any] = {"model": model}
if deployment:
Expand Down
24 changes: 12 additions & 12 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, cast

from azure.search.documents.agent.aio import KnowledgeAgentRetrievalClient
from azure.search.documents.aio import SearchClient
Expand Down Expand Up @@ -39,14 +39,14 @@ def __init__(
*,
search_client: SearchClient,
search_index_name: str,
agent_model: Optional[str],
agent_deployment: Optional[str],
agent_model: str | None,
agent_deployment: str | None,
agent_client: KnowledgeAgentRetrievalClient,
auth_helper: AuthenticationHelper,
openai_client: AsyncOpenAI,
chatgpt_model: str,
chatgpt_deployment: Optional[str], # Not needed for non-Azure OpenAI
embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text"
chatgpt_deployment: str | None, # Not needed for non-Azure OpenAI
embedding_deployment: str | None, # Not needed for non-Azure OpenAI or for retrieval_mode="text"
embedding_model: str,
embedding_dimensions: int,
embedding_field: str,
Expand All @@ -55,11 +55,11 @@ def __init__(
query_language: str,
query_speller: str,
prompt_manager: PromptManager,
reasoning_effort: Optional[str] = None,
reasoning_effort: str | None = None,
multimodal_enabled: bool = False,
image_embeddings_client: Optional[ImageEmbeddings] = None,
global_blob_manager: Optional[BlobManager] = None,
user_blob_manager: Optional[AdlsBlobManager] = None,
image_embeddings_client: ImageEmbeddings | None = None,
global_blob_manager: BlobManager | None = None,
user_blob_manager: AdlsBlobManager | None = None,
):
self.search_client = search_client
self.search_index_name = search_index_name
Expand Down Expand Up @@ -107,7 +107,7 @@ def get_search_query(self, chat_completion: ChatCompletion, user_query: str):
return query_text
return user_query

def extract_followup_questions(self, content: Optional[str]):
def extract_followup_questions(self, content: str | None):
if content is None:
return content, []
return content.split("<<")[0], re.findall(r"<<([^>>]+)>>", content)
Expand Down Expand Up @@ -218,7 +218,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 @@ -246,7 +246,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
Loading