Skip to content

Commit 26ec9dd

Browse files
committed
feat: add ImageLoader for Azure blob image processing and refactor poster validation logic
1 parent ff38378 commit 26ec9dd

File tree

5 files changed

+134
-78
lines changed

5 files changed

+134
-78
lines changed

infra/main.bicep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,4 @@ output APIM_SERVICE_NAME string = apiManagement.name
651651
output TMDB_ENDPOINT string = 'https://${apiManagement.outputs.apiManagementProxyHostName}'
652652
output STORAGE_ACCOUNT_BLOB_URL string = storageAccountAzureRambi.outputs.primaryBlobEndpoint
653653
output AZURE_AI_PROJECT_ENDPOINT string = aiFoundryProject.outputs.projectEndpoint
654+
output AZURE_CLIENT_ID string = azrStorageContributor.properties.clientId
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from io import BytesIO
2+
from agent_framework import AgentRunResponse, ChatAgent, ChatMessage, ai_function
3+
from agent_framework.openai import OpenAIResponsesClient
4+
5+
import asyncio
6+
import os
7+
import httpx
8+
import logging
9+
import base64
10+
from urllib.parse import urlparse
11+
from random import randrange
12+
from typing import TYPE_CHECKING, Annotated, Any
13+
from azure.storage.blob import BlobServiceClient
14+
from PIL import Image
15+
from azure.identity import ManagedIdentityCredential, DefaultAzureCredential
16+
17+
logger = logging.getLogger(__name__)
18+
logger.setLevel(logging.INFO)
19+
20+
@ai_function
21+
def get_image_content(url: Annotated[str, "url to image stored in Azure Blob Storage"]) -> str:
22+
"""Get the current weather for a given location."""
23+
# Simulate weather data
24+
client_id = os.getenv("AZURE_CLIENT_ID", None)
25+
return ImageLoader(client_id).encode_image_from_url(url)
26+
27+
28+
class ImageLoader:
29+
def __init__(self, client_id: str = None):
30+
# Use managed identity for blob storage access
31+
if client_id:
32+
logger.info(f"Using managed identity {client_id} for blob storage access")
33+
self._credential = ManagedIdentityCredential(client_id=client_id)
34+
else:
35+
logger.info("Using default Azure credential for blob storage access")
36+
self._credential = DefaultAzureCredential()
37+
logger.info(f"Credential initialized: {self._credential}")
38+
39+
40+
def _blob_service_client(self, storage_account_url: str) -> str:
41+
return BlobServiceClient(
42+
account_url=storage_account_url,
43+
credential=self._credential
44+
)
45+
46+
def encode_image_from_url(self, image_url: str) -> str:
47+
logger.info(f"Encoding image from URL: {image_url}")
48+
"""Encode image from URL to base64, using managed identity for Azure blob storage URLs."""
49+
try:
50+
# Check if this is an Azure blob storage URL
51+
if self._is_azure_blob_url(image_url):
52+
logger.info("Detected Azure blob storage URL, using authenticated access")
53+
return self._encode_image_from_blob_url(image_url)
54+
else:
55+
# Regular HTTP URL - use direct access
56+
logger.info("Using direct HTTP access for non-blob URL")
57+
with httpx.Client() as client:
58+
response = client.get(image_url)
59+
response.raise_for_status()
60+
61+
logger.info(f"Image fetched successfully: {len(response.content)} bytes")
62+
image = Image.open(BytesIO(response.content))
63+
64+
# Convert to base64
65+
logger.info("Encoding image to base64")
66+
return base64.b64encode(response.content).decode('utf-8')
67+
except Exception as e:
68+
logger.error(f"Error encoding image from URL {image_url}: {str(e)}")
69+
raise RuntimeError(f"Failed to process image from URL: {str(e)}") from e
70+
71+
def _is_azure_blob_url(self, url: str) -> bool:
72+
"""Check if URL is an Azure blob storage URL."""
73+
try:
74+
parsed = urlparse(url)
75+
logger.info(f"Parsed URL netloc: {parsed.netloc}")
76+
return 'blob.core.windows.net' in parsed.netloc
77+
except Exception:
78+
return False
79+
80+
def _encode_image_from_blob_url(self, blob_url: str) -> str:
81+
"""Encode image from Azure blob storage URL using managed identity."""
82+
try:
83+
# Parse the blob URL to extract container and blob name
84+
parsed = urlparse(blob_url)
85+
path_parts = parsed.path.lstrip('/').split('/')
86+
logger.info(f"Parsed blob URL path parts: {path_parts}")
87+
88+
if len(path_parts) < 2:
89+
raise RuntimeError("Invalid blob URL format: {blob_url}")
90+
91+
container_name = path_parts[0]
92+
blob_name = '/'.join(path_parts[1:])
93+
94+
logger.info(f"Accessing blob: container={container_name}, blob={blob_name}")
95+
96+
# Get blob client and download content
97+
blob_client = self._blob_service_client(parsed.netloc).get_blob_client(
98+
container=container_name,
99+
blob=blob_name
100+
)
101+
102+
# Download blob content
103+
with blob_client:
104+
blob_data = blob_client.download_blob()
105+
content = blob_data.readall()
106+
107+
logger.info(f"Blob downloaded successfully: {len(content)} bytes")
108+
109+
# Validate it's an image
110+
image = Image.open(BytesIO(content))
111+
112+
# Convert to base64
113+
logger.info("Encoding blob image to base64")
114+
return base64.b64encode(content).decode('utf-8')
115+
116+
except Exception as e:
117+
logger.error(f"Error accessing blob {blob_url}: {str(e)}")
118+
raise RuntimeError(
119+
f"Failed to access blob with managed identity: {str(e)}"
120+
) from e
121+

src/movie_poster_agent_svc/main.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@
2424
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
2525
from agent_framework import ChatMessage, TextContent, UriContent, DataContent, Role
2626
from urllib.parse import urlparse
27-
27+
from ai_tools import ImageLoader
2828
load_dotenv()
2929

3030
# Configure logging
3131
logging.basicConfig(level=logging.INFO)
3232
logger = logging.getLogger(__name__)
33-
logger.setLevel(logging.INFO )
33+
logger.setLevel(logging.INFO)
3434

3535
# Configure telemetry
3636
if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"):
@@ -73,35 +73,17 @@ class PosterValidationAgent:
7373

7474
def __init__(self):
7575
"""Initialize the validation agent."""
76+
7677
self.project_endpoint = os.getenv("AZURE_AI_PROJECT_ENDPOINT")
7778
self.model_deployment = os.getenv("AZURE_AI_MODEL_DEPLOYMENT", "gpt-4o")
78-
7979
if not self.project_endpoint:
8080
raise ValueError("AZURE_AI_PROJECT_ENDPOINT environment variable is required")
81-
else:
82-
logger.info(f"Initializing agent with endpoint: {self.project_endpoint}")
83-
logger.info(f"Using model deployment: {self.model_deployment}")
8481

85-
# Initialize blob storage for image processing with managed identity
86-
self.blob_service_client = None
87-
self.storage_account_url = os.getenv("STORAGE_ACCOUNT_BLOB_URL")
88-
89-
if self.storage_account_url:
90-
# Use managed identity for blob storage access
91-
client_id = os.getenv("AZURE_CLIENT_ID")
92-
if client_id:
93-
logger.info(f"Using managed identity {client_id} for blob storage access")
94-
credential = ManagedIdentityCredential(client_id=client_id)
95-
else:
96-
logger.info("Using default Azure credential for blob storage access")
97-
credential = DefaultAzureCredential()
98-
99-
self.blob_service_client = BlobServiceClient(
100-
account_url=self.storage_account_url,
101-
credential=credential
102-
)
103-
logger.info(f"Blob service client initialized with URL: {self.storage_account_url}")
104-
82+
logger.info(f"Initializing agent with endpoint: {self.project_endpoint}")
83+
logger.info(f"Using model deployment: {self.model_deployment}")
84+
85+
self._image_loader = ImageLoader(os.getenv("AZURE_CLIENT_ID"))
86+
10587
async def create_agent(self) -> ChatAgent:
10688
"""Create and configure the chat agent."""
10789
agent_instructions = """
@@ -229,7 +211,7 @@ async def _encode_image_from_blob_url(self, blob_url: str) -> str:
229211
async def validate_poster(self, request: PosterValidationRequest) -> PosterValidationResponse:
230212
"""Validate a movie poster using the AI agent."""
231213
try:
232-
image_base64 = await self.encode_image_from_url(request.poster_url)
214+
image_base64 = self._image_loader.encode_image_from_url(request.poster_url) if request.poster_url else None
233215

234216
async with await self.create_agent() as agent:
235217
# Build the validation prompt

src/movie_poster_agent_svc/test_blob_access.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/movie_poster_agent_svc/test_validate_poster.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
from dotenv import load_dotenv
99
from main import PosterValidationAgent, PosterValidationRequest
10-
10+
from ai_tools import ImageLoader
1111
# Load environment variables
1212
load_dotenv()
1313

@@ -19,11 +19,11 @@ async def test_validate_poster():
1919
"""Test the updated validate_poster method."""
2020
try:
2121
# Initialize the agent
22-
agent = PosterValidationAgent()
22+
agent = ImageLoader()
2323

2424
# Create a test request
2525
test_request = PosterValidationRequest(
26-
poster_url="https://nazrambihxklazmdpap4s.blob.core.windows.net/movieposters/test_image.png",
26+
poster_url="https://nazrambihxklazmdpap4s.blob.core.windows.net/movieposters/525_3170_Romance_87490.png",
2727
poster_description="A dramatic movie poster featuring action scenes",
2828
movie_title="Test Movie",
2929
movie_genre="Action",

0 commit comments

Comments
 (0)