From 1ffd1097fd48c617cf9bdef3788547c190050f1e Mon Sep 17 00:00:00 2001 From: Kingshuk-Microsoft Date: Wed, 4 Feb 2026 14:00:40 +0530 Subject: [PATCH 01/44] fix: remove unused imports and improve error handling across multiple scripts --- archive-doc-gen/src/backend/settings.py | 5 +++ .../ChatHistory/ChatHistoryListItem.tsx | 1 - .../ChatHistory/chatHistoryListItem.test.tsx | 9 ++-- .../src/frontend/src/pages/chat/Chat.tsx | 3 -- .../tests/e2e-test/pages/draftPage.py | 1 - .../tests/e2e-test/tests/test_st_docgen_tc.py | 41 ++++++++++--------- .../scripts/create_image_search_index.py | 1 - content-gen/scripts/post_deploy.py | 4 +- .../scripts/sample_content_generation.py | 3 +- .../scripts/test_content_generation.py | 1 - content-gen/src/backend/app.py | 4 -- content-gen/src/backend/orchestrator.py | 21 +++++++--- .../src/backend/services/cosmos_service.py | 9 +++- content-gen/tests/rai_testing.py | 10 ++--- content-gen/tests/test_agents.py | 5 +-- docs/generate_architecture.py | 2 +- docs/generate_architecture_png.py | 9 ++-- 17 files changed, 64 insertions(+), 65 deletions(-) diff --git a/archive-doc-gen/src/backend/settings.py b/archive-doc-gen/src/backend/settings.py index 87c589c86..189f3b716 100644 --- a/archive-doc-gen/src/backend/settings.py +++ b/archive-doc-gen/src/backend/settings.py @@ -266,6 +266,11 @@ class _AzureSearchSettings(BaseSettings, DatasourcePayloadConstructor): extra="ignore", env_ignore_empty=True, ) + + def __init__(self, *args, settings: "_AppSettings", **data): + # Ensure DatasourcePayloadConstructor.__init__ runs so that _settings is initialized + super().__init__(*args, settings=settings, **data) + _type: Literal["azure_search"] = PrivateAttr(default="azure_search") top_k: int = Field(default=5, serialization_alias="top_n_documents") strictness: int = 3 diff --git a/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx index b14b50039..d62fdb2b3 100644 --- a/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx +++ b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx @@ -7,7 +7,6 @@ import { DialogType, IconButton, ITextField, - ITooltipHostStyles, List, PrimaryButton, Separator, diff --git a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx index a7eccb4c9..05dcb18f5 100644 --- a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx +++ b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx @@ -553,11 +553,10 @@ describe('ChatHistoryListItemCell', () => { }, 10000) test('shows error when trying to rename to an existing title', async () => { - const existingTitle = 'Existing Chat Title' - ; (historyRename as jest.Mock).mockResolvedValueOnce({ - ok: false, - json: async () => ({ message: 'Title already exists' }) - }) + (historyRename as jest.Mock).mockResolvedValueOnce({ + ok: false, + json: async () => ({ message: 'Title already exists' }) + }) renderWithContext(, mockAppState) diff --git a/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx index a13b5b569..3520e075e 100644 --- a/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx +++ b/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx @@ -6,9 +6,6 @@ import { Dialog, DialogType, Stack, - IStackTokens, - mergeStyleSets, - IModalStyles, Spinner, SpinnerSize } from '@fluentui/react' diff --git a/archive-doc-gen/tests/e2e-test/pages/draftPage.py b/archive-doc-gen/tests/e2e-test/pages/draftPage.py index 760a54e55..3ddebfd0b 100644 --- a/archive-doc-gen/tests/e2e-test/pages/draftPage.py +++ b/archive-doc-gen/tests/e2e-test/pages/draftPage.py @@ -1,5 +1,4 @@ import time -import os from base.base import BasePage from pytest_check import check from playwright.sync_api import expect diff --git a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py index de55c599a..172199607 100644 --- a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py +++ b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py @@ -6,7 +6,7 @@ import pytest from pytest_check import check from playwright.sync_api import expect -from config.constants import (URL, add_section, browse_question1, browse_question2, browse_question3, +from config.constants import (add_section, browse_question1, browse_question2, browse_question3, browse_question4, browse_question5, generate_question1, invalid_response, invalid_response1, remove_section) from pages.browsePage import BrowsePage @@ -538,7 +538,6 @@ def test_show_hide_chat_history(login_logout, request): page = login_logout home_page = HomePage(page) - browse_page = BrowsePage(page) generate_page = GeneratePage(page) log_capture = io.StringIO() @@ -2503,14 +2502,13 @@ def test_bug_7571_removed_sections_not_returning(request, login_logout): logger.info("Step 4: Enter a prompt to remove sections one by one 'Remove (section name)'") start = time.time() - # Select 3 sections to remove from the initial list - sections_to_remove = [] - if initial_count >= 3: - # Remove sections at positions 1, 2, and 3 (avoid removing first section for stability) - indices_to_remove = [1, 2, 3] if initial_count > 3 else list(range(1, initial_count)) - for idx in indices_to_remove: - if idx < len(initial_sections): - sections_to_remove.append(initial_sections[idx]) + # Select up to 3 sections to remove from the initial list (avoid removing first section for stability) + indices_to_remove = [1, 2, 3] if initial_count > 3 else list(range(1, initial_count)) + sections_to_remove = [ + initial_sections[idx] + for idx in indices_to_remove + if idx < len(initial_sections) + ] logger.info("Sections selected for removal: %s", sections_to_remove) @@ -2739,6 +2737,9 @@ def test_bug_9825_navigate_between_sections(request, login_logout): page.wait_for_timeout(1000) + duration = time.time() - start + logger.info("Execution Time for Step 6: %.2fs", duration) + logger.info("\n" + "="*80) logger.info("✅ TC 10157 Test Summary - Navigate between sections") logger.info("="*80) @@ -3398,7 +3399,7 @@ def test_bug_10177_edit_delete_icons_disabled_during_response(login_logout, requ try: threads.first.wait_for(state="visible", timeout=10000) logger.info("✅ Chat history created and displayed with %d thread(s)", threads.count()) - except: + except Exception: logger.error("❌ Chat history threads not visible after creation") # Try alternative locator threads_alt = page.locator('div[data-list-index]') @@ -3448,7 +3449,7 @@ def test_bug_10177_edit_delete_icons_disabled_during_response(login_logout, requ try: delete_icon.wait_for(state="visible", timeout=2000) is_delete_visible = True - except: + except Exception: is_delete_visible = False is_delete_enabled = delete_icon.is_enabled() if is_delete_visible else False @@ -3460,7 +3461,7 @@ def test_bug_10177_edit_delete_icons_disabled_during_response(login_logout, requ try: edit_icon.wait_for(state="visible", timeout=2000) is_edit_visible = True - except: + except Exception: is_edit_visible = False is_edit_enabled = edit_icon.is_enabled() if is_edit_visible else False @@ -3914,7 +3915,9 @@ def test_bug_16106_tooltip_on_chat_history_hover(login_logout, request): with check: assert thread_count > 0, "No chat history threads found to hover over" - if thread_count > 0: + if thread_count <= 0: + logger.warning("Skipping hover action: no chat history threads available.") + else: # Hover over the first chat thread to trigger tooltip first_thread = history_threads.nth(0) first_thread.hover() @@ -3989,8 +3992,6 @@ def test_bug_16106_tooltip_on_chat_history_hover(login_logout, request): if tooltip_found: logger.info("✅ Tooltip displayed successfully on chat history hover") logger.info("Tooltip text length: %d characters", len(tooltip_text)) - else: - logger.error("❌ BUG FOUND: No tooltip displayed when hovering over chat history") duration = time.time() - start logger.info("Execution Time for 'Verify tooltip': %.2fs", duration) @@ -4007,7 +4008,7 @@ def test_bug_16106_tooltip_on_chat_history_hover(login_logout, request): page.keyboard.press("Escape") page.wait_for_timeout(1000) logger.info("Closed chat history using Escape key") - except: + except Exception: logger.warning("Chat history panel may still be open") logger.info("\n%s", "="*80) @@ -4102,10 +4103,10 @@ def test_bug_26031_validate_empty_spaces_chat_input(login_logout, request): assert current_responses_empty == initial_responses, \ f"BUG: System accepted empty query. Response count changed from {initial_responses} to {current_responses_empty}" - if current_responses_empty == initial_responses: - logger.info("✅ System did not accept empty query - no response generated") - else: + if current_responses_empty != initial_responses: logger.error("❌ BUG: System accepted empty query and generated response") + else: + logger.info("✅ System did not accept empty query - no response generated") else: logger.info("✅ Send button is properly disabled for empty input") diff --git a/content-gen/scripts/create_image_search_index.py b/content-gen/scripts/create_image_search_index.py index c67a613e8..2218fecfb 100644 --- a/content-gen/scripts/create_image_search_index.py +++ b/content-gen/scripts/create_image_search_index.py @@ -10,7 +10,6 @@ """ import asyncio -import json import os import sys from pathlib import Path diff --git a/content-gen/scripts/post_deploy.py b/content-gen/scripts/post_deploy.py index 47ace7d6a..c5da80892 100644 --- a/content-gen/scripts/post_deploy.py +++ b/content-gen/scripts/post_deploy.py @@ -36,13 +36,11 @@ import argparse import asyncio import base64 -import json import os import sys -import time from dataclasses import dataclass from pathlib import Path -from typing import Optional, List, Dict, Any +from typing import List, Dict, Any try: import httpx diff --git a/content-gen/scripts/sample_content_generation.py b/content-gen/scripts/sample_content_generation.py index 05b02569c..c2a0e0d98 100644 --- a/content-gen/scripts/sample_content_generation.py +++ b/content-gen/scripts/sample_content_generation.py @@ -28,7 +28,6 @@ import asyncio import argparse import json -import os import sys from datetime import datetime from pathlib import Path @@ -288,7 +287,7 @@ async def main(): # Generate content try: - result = await generate_content_sample( + await generate_content_sample( brief=brief, products=products, generate_images=not args.no_images, diff --git a/content-gen/scripts/test_content_generation.py b/content-gen/scripts/test_content_generation.py index 3896a2465..acbb41a91 100644 --- a/content-gen/scripts/test_content_generation.py +++ b/content-gen/scripts/test_content_generation.py @@ -12,7 +12,6 @@ """ import asyncio -import json import sys import os from typing import Dict, Any diff --git a/content-gen/src/backend/app.py b/content-gen/src/backend/app.py index aecdf7f8d..5174fc34f 100644 --- a/content-gen/src/backend/app.py +++ b/content-gen/src/backend/app.py @@ -676,8 +676,6 @@ async def generate_content(): Returns streaming response with generated content. """ - import asyncio - data = await request.get_json() brief_data = data.get("brief", {}) @@ -876,8 +874,6 @@ async def regenerate_content(): Returns regenerated image with the modification applied. """ - import asyncio - data = await request.get_json() modification_request = data.get("modification_request", "") diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index 7677ca362..f097df086 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1511,8 +1511,10 @@ async def generate_content( try: prompt_data = json.loads(json_match.group(1)) prompt_text = prompt_data.get('prompt', prompt_data.get('image_prompt', prompt_text)) - except: - pass + except Exception as parse_error: + # Best-effort JSON extraction from markdown code block; on failure, + # fall back to the original prompt_text without interrupting image generation. + logger.debug(f"Failed to parse JSON from markdown code block for image prompt: {parse_error}") # Build product description for DALL-E context # Include detailed image descriptions if available for better color accuracy @@ -1576,7 +1578,13 @@ async def generate_content( for v in results["violations"] ) except (json.JSONDecodeError, KeyError): - pass + # If the compliance response is not valid JSON or missing expected keys, + # continue without structured violations data but log for observability. + logger.debug( + "Could not parse structured compliance violations from response; " + "proceeding without 'violations' / 'requires_modification'.", + exc_info=True, + ) except Exception as e: logger.exception(f"Error generating content: {e}") @@ -1744,8 +1752,11 @@ async def regenerate_image( prompt_data = json.loads(json_match.group(1)) prompt_text = prompt_data.get('prompt', prompt_text) change_summary = prompt_data.get('change_summary', modification_request) - except: - pass + except Exception as parse_error: + logger.debug( + "Failed to parse JSON from image modification response fallback: %s", + parse_error, + ) results["image_prompt"] = prompt_text results["message"] = f"Regenerating image: {change_summary}" diff --git a/content-gen/src/backend/services/cosmos_service.py b/content-gen/src/backend/services/cosmos_service.py index 11f55400d..caac4bc4d 100644 --- a/content-gen/src/backend/services/cosmos_service.py +++ b/content-gen/src/backend/services/cosmos_service.py @@ -308,8 +308,13 @@ async def get_conversation( max_item_count=1 ): return item - except Exception: - pass + except Exception as exc: + logger.warning( + "Cross-partition conversation lookup failed for id=%s, user_id=%s: %s", + conversation_id, + user_id, + exc, + ) return None diff --git a/content-gen/tests/rai_testing.py b/content-gen/tests/rai_testing.py index 78a8fcc30..f61346255 100644 --- a/content-gen/tests/rai_testing.py +++ b/content-gen/tests/rai_testing.py @@ -26,11 +26,9 @@ import argparse import asyncio import json -import os -import subprocess import sys import time -from dataclasses import dataclass, asdict +from dataclasses import dataclass from datetime import datetime from enum import Enum from pathlib import Path @@ -41,9 +39,9 @@ # Try to import Azure Identity for auth try: from azure.identity import AzureCliCredential, InteractiveBrowserCredential - AZURE_IDENTITY_AVAILABLE = True except ImportError: - AZURE_IDENTITY_AVAILABLE = False + # Azure Identity is optional; authentication features depending on it will be unavailable. + pass class TestCategory(Enum): @@ -966,7 +964,7 @@ async def main(): # Generate report report_path = runner.generate_report(args.output_dir) - print(f"\nReports saved to: {args.output_dir}") + print(f"\nReports saved to: {report_path}") return runner.print_summary() diff --git a/content-gen/tests/test_agents.py b/content-gen/tests/test_agents.py index feb7ae7e3..dccbdf667 100644 --- a/content-gen/tests/test_agents.py +++ b/content-gen/tests/test_agents.py @@ -2,10 +2,7 @@ Unit tests for the Content Generation agents. """ -import pytest -from unittest.mock import AsyncMock, MagicMock, patch - -from backend.models import CreativeBrief, Product, ComplianceSeverity +from backend.models import CreativeBrief, Product from backend.agents.text_content_agent import validate_text_compliance from backend.agents.compliance_agent import comprehensive_compliance_check diff --git a/docs/generate_architecture.py b/docs/generate_architecture.py index d6c680c12..13071e3d0 100644 --- a/docs/generate_architecture.py +++ b/docs/generate_architecture.py @@ -15,7 +15,7 @@ from diagrams.azure.compute import ContainerInstances, AppServices, ContainerRegistries from diagrams.azure.database import CosmosDb, BlobStorage from diagrams.azure.ml import CognitiveServices -from diagrams.azure.network import VirtualNetworks, PrivateEndpoint, DNSZones +from diagrams.azure.network import PrivateEndpoint, DNSZones from diagrams.azure.analytics import AnalysisServices from diagrams.onprem.client import User diff --git a/docs/generate_architecture_png.py b/docs/generate_architecture_png.py index 0f441e1b9..6a684cddd 100644 --- a/docs/generate_architecture_png.py +++ b/docs/generate_architecture_png.py @@ -19,7 +19,6 @@ def draw_rounded_rect(draw, xy, radius, fill=None, outline=None, width=1): """Draw a rounded rectangle""" - x1, y1, x2, y2 = xy draw.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width) def draw_service_box(draw, x, y, w, h, title, subtitle="", icon_type="default", highlight=False): @@ -69,7 +68,7 @@ def draw_service_box(draw, x, y, w, h, title, subtitle="", icon_type="default", try: font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 13) font_sub = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) - except: + except Exception: font_title = ImageFont.load_default() font_sub = ImageFont.load_default() @@ -113,7 +112,7 @@ def main(): try: font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 28) font_label = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 9) - except: + except OSError: font_title = ImageFont.load_default() font_label = ImageFont.load_default() @@ -134,9 +133,7 @@ def main(): COL1_X = 100 COL2_X = 340 - COL3_X = 580 COL4_X = 820 - COL5_X = 1100 # === ROW 1: Frontend Tier === # Container Registry @@ -211,7 +208,7 @@ def main(): # Copyright try: font_copy = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) - except: + except Exception: font_copy = ImageFont.load_default() draw.text((50, HEIGHT-30), "© 2024 Microsoft Corporation All rights reserved.", fill=TEXT_GRAY, font=font_copy) From f711994c4f1d78f33d91060924895ec72f11447f Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 4 Feb 2026 20:55:36 +0530 Subject: [PATCH 02/44] feat: Add main.waf.parameters.json with updated parameters and enable features --- content-gen/infra/main.parameters.json | 12 ---- content-gen/infra/main.waf.parameters.json | 69 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 content-gen/infra/main.waf.parameters.json diff --git a/content-gen/infra/main.parameters.json b/content-gen/infra/main.parameters.json index 051635a50..05c294453 100644 --- a/content-gen/infra/main.parameters.json +++ b/content-gen/infra/main.parameters.json @@ -49,18 +49,6 @@ }, "imageTag": { "value": "${imageTag=latest}" - }, - "enablePrivateNetworking": { - "value": "${enablePrivateNetworking}" - }, - "enableMonitoring": { - "value": "${enableMonitoring}" - }, - "enableScalability": { - "value": "${enableScalability}" - }, - "enableRedundancy": { - "value": "${enableRedundancy}" } } } diff --git a/content-gen/infra/main.waf.parameters.json b/content-gen/infra/main.waf.parameters.json new file mode 100644 index 000000000..fc60bdf89 --- /dev/null +++ b/content-gen/infra/main.waf.parameters.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "solutionName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "gptModelName": { + "value": "${gptModelName}" + }, + "gptModelVersion": { + "value": "${gptModelVersion}" + }, + "gptModelDeploymentType": { + "value": "${gptModelDeploymentType}" + }, + "gptModelCapacity": { + "value": "${gptModelCapacity}" + }, + "imageModelChoice": { + "value": "${imageModelChoice}" + }, + "dalleModelCapacity": { + "value": "${dalleModelCapacity}" + }, + "embeddingModel": { + "value": "${embeddingModel}" + }, + "embeddingDeploymentCapacity": { + "value": "${embeddingDeploymentCapacity}" + }, + "azureOpenaiAPIVersion": { + "value": "${azureOpenaiAPIVersion}" + }, + "azureAiServiceLocation": { + "value": "${azureAiServiceLocation}" + }, + "existingLogAnalyticsWorkspaceId": { + "value": "${existingLogAnalyticsWorkspaceId}" + }, + "azureExistingAIProjectResourceId": { + "value": "${azureExistingAIProjectResourceId}" + }, + "acrName": { + "value": "${acrName}" + }, + "imageTag": { + "value": "${imageTag=latest}" + }, + "enableMonitoring": { + "value": true + }, + "enablePrivateNetworking": { + "value": true + }, + "enableScalability": { + "value": true + }, + "virtualMachineAdminUsername": { + "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" + }, + "virtualMachineAdminPassword": { + "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" + } + } +} From caf4be81359d90b44edaaf24c2bfa2d309b16b6b Mon Sep 17 00:00:00 2001 From: Kanchan-Microsoft Date: Thu, 5 Feb 2026 15:52:10 +0530 Subject: [PATCH 03/44] updated deployment.md file with waf deployment steps. --- content-gen/docs/DEPLOYMENT.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/content-gen/docs/DEPLOYMENT.md b/content-gen/docs/DEPLOYMENT.md index 7100cf403..2c4f25618 100644 --- a/content-gen/docs/DEPLOYMENT.md +++ b/content-gen/docs/DEPLOYMENT.md @@ -161,9 +161,32 @@ Depending on your subscription quota and capacity, you can adjust quota settings +### Deployment Options + +The [`infra`](../infra) folder of the Content Generation Solution Accelerator contains the [`main.bicep`](../infra/main.bicep) Bicep script, which defines all Azure infrastructure components for this solution. + +By default, the `azd up` command uses the [`main.parameters.json`](../infra/main.parameters.json) file to deploy the solution. This file is pre-configured for a **sandbox environment**. + +For **production deployments**, the repository also provides [`main.waf.parameters.json`](../infra/main.waf.parameters.json), which applies a [Well-Architected Framework (WAF) aligned](https://learn.microsoft.com/en-us/azure/well-architected/) configuration. This can be used for Production scenarios. + +**How to choose your deployment configuration:** + +* **To use sandbox/dev environment** — Use the default `main.parameters.json` file. + +* **To use production configuration:** + +Before running `azd up`, copy the contents from the production configuration file to your main parameters file: + +1. Navigate to the `infra` folder in your project. +2. Open `main.waf.parameters.json` in a text editor (like Notepad, VS Code, etc.). +3. Select all content (Ctrl+A) and copy it (Ctrl+C). +4. Open `main.parameters.json` in the same text editor. +5. Select all existing content (Ctrl+A) and paste the copied content (Ctrl+V). +6. Save the file (Ctrl+S). + ### Deploying with AZD -Once you've opened the project in [Codespaces](#github-codespaces), [Dev Containers](#vs-code-dev-containers), or [locally](#local-environment), you can deploy it to Azure by following the steps in the [AZD Deployment Guide](AZD_DEPLOYMENT.md) +Once you've opened the project in [Codespaces](#github-codespaces), [Dev Containers](#vs-code-dev-containers), or [locally](#local-environment), you can deploy it to Azure by following the steps in the [AZD Deployment Guide](AZD_DEPLOYMENT.md). ## Post Deployment Steps From a0a1602a70e5da4b3c1b52a04deb9e633463e76f Mon Sep 17 00:00:00 2001 From: Shubhangi-Microsoft Date: Fri, 6 Feb 2026 16:25:09 +0530 Subject: [PATCH 04/44] Added fix for bug #33406 --- content-gen/infra/main.bicep | 17 +++- .../deploy_foundry_role_assignment.bicep | 84 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 content-gen/infra/modules/deploy_foundry_role_assignment.bicep diff --git a/content-gen/infra/main.bicep b/content-gen/infra/main.bicep index 092783a12..dc6f99946 100644 --- a/content-gen/infra/main.bicep +++ b/content-gen/infra/main.bicep @@ -240,9 +240,9 @@ var useExistingAiFoundryAiProject = !empty(azureExistingAIProjectResourceId) var aiFoundryAiServicesResourceGroupName = useExistingAiFoundryAiProject ? split(azureExistingAIProjectResourceId, '/')[4] : 'rg-${solutionSuffix}' -// var aiFoundryAiServicesSubscriptionId = useExistingAiFoundryAiProject -// ? split(azureExistingAIProjectResourceId, '/')[2] -// : subscription().id +var aiFoundryAiServicesSubscriptionId = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[2] + : subscription().subscriptionId var aiFoundryAiServicesResourceName = useExistingAiFoundryAiProject ? split(azureExistingAIProjectResourceId, '/')[8] : 'aif-${solutionSuffix}' @@ -572,6 +572,17 @@ var aiFoundryAiProjectEndpoint = useExistingAiFoundryAiProject ? 'https://${aiFoundryAiServicesResourceName}.services.ai.azure.com/api/projects/${aiFoundryAiProjectResourceName}' : aiFoundryAiServicesProject!.outputs.apiEndpoint +// ========== Role Assignments for Existing AI Services ========== // +module existingAiServicesRoleAssignments 'modules/deploy_foundry_role_assignment.bicep' = if (useExistingAiFoundryAiProject) { + name: take('module.foundry-role-assignment.${aiFoundryAiServicesResourceName}', 64) + scope: resourceGroup(aiFoundryAiServicesSubscriptionId, aiFoundryAiServicesResourceGroupName) + params: { + aiServicesName: aiFoundryAiServicesResourceName + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } +} + // ========== AI Search ========== // module aiSearch 'br/public:avm/res/search/search-service:0.11.1' = { name: take('avm.res.search.search-service.${aiSearchName}', 64) diff --git a/content-gen/infra/modules/deploy_foundry_role_assignment.bicep b/content-gen/infra/modules/deploy_foundry_role_assignment.bicep new file mode 100644 index 000000000..3121ca932 --- /dev/null +++ b/content-gen/infra/modules/deploy_foundry_role_assignment.bicep @@ -0,0 +1,84 @@ +// ========== existing-ai-services-roles.bicep ========== // +// Module to assign RBAC roles to managed identity on an existing AI Services account +// This is required when reusing an existing AI Foundry project from a different resource group + +@description('Required. The principal ID of the managed identity to grant access.') +param principalId string + +@description('Required. The name of the existing AI Services account.') +param aiServicesName string + +@description('Optional. The name of the existing AI Project.') +param aiProjectName string = '' + +@description('Optional. The principal type of the identity.') +@allowed([ + 'Device' + 'ForeignGroup' + 'Group' + 'ServicePrincipal' + 'User' +]) +param principalType string = 'ServicePrincipal' + +// ========== Role Definitions ========== // + +// Azure AI User role - for AI Foundry project access (used by AIProjectClient for image generation) +resource azureAiUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '53ca6127-db72-4b80-b1b0-d745d6d5456d' +} + +// Cognitive Services OpenAI User role - for chat completions (used by AzureOpenAIChatClient) +resource cognitiveServicesOpenAiUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' +} + +// ========== Existing Resources ========== // + +// Reference the existing AI Services account +resource existingAiServices 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiServicesName +} + +// Reference the existing AI Project (if provided) +resource existingAiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' existing = if (!empty(aiProjectName)) { + name: aiProjectName + parent: existingAiServices +} + +// ========== Role Assignments ========== // + +// Azure AI User role assignment - same as reference accelerator +// Required for AIProjectClient (used for image generation in Foundry mode) +resource assignAzureAiUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(existingAiServices.id, principalId, azureAiUserRole.id) + scope: existingAiServices + properties: { + roleDefinitionId: azureAiUserRole.id + principalId: principalId + principalType: principalType + } +} + +// Cognitive Services OpenAI User role assignment +// Required for AzureOpenAIChatClient (used for chat completions) +resource assignCognitiveServicesOpenAiUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(existingAiServices.id, principalId, cognitiveServicesOpenAiUserRole.id) + scope: existingAiServices + properties: { + roleDefinitionId: cognitiveServicesOpenAiUserRole.id + principalId: principalId + principalType: principalType + } +} + +// ========== Outputs ========== // + +@description('The resource ID of the existing AI Services account.') +output aiServicesResourceId string = existingAiServices.id + +@description('The endpoint of the existing AI Services account.') +output aiServicesEndpoint string = existingAiServices.properties.endpoint + +@description('The principal ID of the existing AI Project (if provided).') +output aiProjectPrincipalId string = !empty(aiProjectName) ? existingAiProject.identity.principalId : '' From 0f6e367ee5d80b88489cbf306aba667be197d196 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 11 Feb 2026 11:42:40 +0530 Subject: [PATCH 05/44] add flake8 configuration and PyLint workflow for code quality checks --- .flake8 | 5 +++++ .github/workflows/pylint.yml | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .flake8 create mode 100644 .github/workflows/pylint.yml diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..93f63e5d1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 88 +extend-ignore = E501 +exclude = .venv, frontend +ignore = E203, W503, G004, G200 \ No newline at end of file diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 000000000..282e5af1d --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,37 @@ +name: PyLint + +on: + push: + paths: + - 'content-gen/src/backend/**/*.py' + - 'content-gen/src/backend/requirements*.txt' + - '.flake8' + - '.github/workflows/pylint.yml' + +permissions: + contents: read + actions: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r content-gen/src/backend/requirements.txt + pip install flake8 # Ensure flake8 is installed explicitly + + - name: Run flake8 and pylint + run: | + flake8 --config=.flake8 content-gen/src/backend # Specify the directory to lint From bac0fff6713345ebf0cafa5a798b7576e06174b4 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 11 Feb 2026 13:03:37 +0530 Subject: [PATCH 06/44] Refactor services for improved readability and maintainability - Cleaned up whitespace and formatting in blob_service.py, cosmos_service.py, search_service.py, and settings.py. - Ensured consistent spacing and indentation throughout the codebase. - Enhanced code clarity by removing unnecessary blank lines and aligning comments. - No functional changes were made; this commit focuses solely on code style improvements. --- .../src/backend/agents/image_content_agent.py | 102 +++-- content-gen/src/backend/api/admin.py | 120 +++--- content-gen/src/backend/app.py | 330 +++++++------- content-gen/src/backend/models.py | 18 +- content-gen/src/backend/orchestrator.py | 404 +++++++++--------- .../src/backend/services/blob_service.py | 96 ++--- .../src/backend/services/cosmos_service.py | 206 ++++----- .../src/backend/services/search_service.py | 82 ++-- content-gen/src/backend/settings.py | 44 +- 9 files changed, 699 insertions(+), 703 deletions(-) diff --git a/content-gen/src/backend/agents/image_content_agent.py b/content-gen/src/backend/agents/image_content_agent.py index 24de18331..2340aa0a9 100644 --- a/content-gen/src/backend/agents/image_content_agent.py +++ b/content-gen/src/backend/agents/image_content_agent.py @@ -18,24 +18,24 @@ def _truncate_for_dalle(product_description: str, max_chars: int = 1500) -> str: """ Truncate product descriptions to fit DALL-E's 4000 character limit. Extracts the most visually relevant information (colors, hex codes, finishes). - + Args: product_description: The full product description(s) max_chars: Maximum characters to allow for product context - + Returns: Truncated description with essential visual details """ if not product_description or len(product_description) <= max_chars: return product_description - + import re - + # Extract essential visual info: product names, hex codes, color descriptions lines = product_description.split('\n') essential_parts = [] current_product = "" - + for line in lines: # Keep product name headers if line.startswith('### '): @@ -54,13 +54,13 @@ def _truncate_for_dalle(product_description: str, max_chars: int = 1500) -> str: # Keep finish descriptions elif 'finish' in line.lower() or 'matte' in line.lower() or 'eggshell' in line.lower(): essential_parts.append(line.strip()[:200]) - + result = '\n'.join(essential_parts) - + # If still too long, just truncate with ellipsis if len(result) > max_chars: - result = result[:max_chars-50] + '\n\n[Additional details truncated for DALL-E]' - + result = result[:max_chars - 50] + '\n\n[Additional details truncated for DALL-E]' + return result @@ -73,9 +73,9 @@ async def generate_dalle_image( ) -> dict: """ Generate a marketing image using DALL-E 3, gpt-image-1, or gpt-image-1.5. - + The model used is determined by AZURE_OPENAI_IMAGE_MODEL setting. - + Args: prompt: The main image generation prompt product_description: Auto-generated description of product image (for context) @@ -86,14 +86,14 @@ async def generate_dalle_image( quality: Image quality (model-specific, uses settings default if not provided) - dall-e-3: standard, hd - gpt-image-1/1.5: low, medium, high, auto - + Returns: Dictionary containing generated image data and metadata """ # Determine which model to use image_model = app_settings.azure_openai.effective_image_model logger.info(f"Using image generation model: {image_model}") - + # Use appropriate generator based on model if image_model in ["gpt-image-1", "gpt-image-1.5"]: return await _generate_gpt_image(prompt, product_description, scene_description, size, quality) @@ -110,31 +110,31 @@ async def _generate_dalle_image( ) -> dict: """ Generate a marketing image using DALL-E 3. - + Args: prompt: The main image generation prompt product_description: Auto-generated description of product image (for context) scene_description: Scene/setting description from creative brief size: Image size (1024x1024, 1024x1792, 1792x1024) quality: Image quality (standard, hd) - + Returns: Dictionary containing generated image data and metadata """ brand = app_settings.brand_guidelines - + # Use defaults from settings if not provided size = size or app_settings.azure_openai.image_size quality = quality or app_settings.azure_openai.image_quality - + # DALL-E 3 has a 4000 character limit for prompts # Truncate product descriptions to essential visual info truncated_product_desc = _truncate_for_dalle(product_description, max_chars=1500) - + # Also truncate the main prompt if it's too long main_prompt = prompt[:1000] if len(prompt) > 1000 else prompt scene_desc = scene_description[:500] if scene_description and len(scene_description) > 500 else scene_description - + # Build the full prompt with product context and brand guidelines full_prompt = f"""⚠️ ABSOLUTE RULE: THIS IMAGE MUST CONTAIN ZERO TEXT. NO WORDS. NO LETTERS. NO PRODUCT NAMES. NO LABELS. @@ -153,7 +153,7 @@ async def _generate_dalle_image( MANDATORY FINAL CHECKLIST: ✗ NO product names in the image -✗ NO color names in the image +✗ NO color names in the image ✗ NO text overlays or labels ✗ NO typography or lettering of any kind ✗ NO watermarks or logos @@ -162,7 +162,7 @@ async def _generate_dalle_image( ✓ Accurately reproduce product colors using exact hex codes ✓ Professional, polished marketing image """ - + # Final safety check - DALL-E 3 has 4000 char limit if len(full_prompt) > 3900: logger.warning(f"Prompt too long ({len(full_prompt)} chars), truncating...") @@ -190,20 +190,20 @@ async def _generate_dalle_image( credential = ManagedIdentityCredential(client_id=client_id) else: credential = DefaultAzureCredential() - + # Get token for Azure OpenAI token = await credential.get_token("https://cognitiveservices.azure.com/.default") - + # Use the dedicated DALL-E endpoint if configured, otherwise fall back to main endpoint dalle_endpoint = app_settings.azure_openai.dalle_endpoint or app_settings.azure_openai.endpoint logger.info(f"Using DALL-E endpoint: {dalle_endpoint}") - + client = AsyncAzureOpenAI( azure_endpoint=dalle_endpoint, azure_ad_token=token.token, api_version=app_settings.azure_openai.preview_api_version, ) - + try: response = await client.images.generate( model=app_settings.azure_openai.dalle_model, @@ -213,9 +213,9 @@ async def _generate_dalle_image( n=1, response_format="b64_json" ) - + image_data = response.data[0] - + return { "success": True, "image_base64": image_data.b64_json, @@ -226,7 +226,7 @@ async def _generate_dalle_image( finally: # Properly close the async client to avoid unclosed session warnings await client.close() - + except Exception as e: logger.exception(f"Error generating DALL-E image: {e}") return { @@ -246,50 +246,50 @@ async def _generate_gpt_image( ) -> dict: """ Generate a marketing image using gpt-image-1 or gpt-image-1.5. - + gpt-image models have different capabilities than DALL-E 3: - Supports larger prompt sizes - Different size options: 1024x1024, 1536x1024, 1024x1536, auto - Different quality options: low, medium, high, auto - May have better instruction following - + Args: prompt: The main image generation prompt product_description: Auto-generated description of product image (for context) scene_description: Scene/setting description from creative brief size: Image size (1024x1024, 1536x1024, 1024x1536, auto) quality: Image quality (low, medium, high, auto) - + Returns: Dictionary containing generated image data and metadata """ brand = app_settings.brand_guidelines - + # Use defaults from settings if not provided # Map DALL-E quality settings to gpt-image-1 or gpt-image-1.5 equivalents if needed size = size or app_settings.azure_openai.image_size quality = quality or app_settings.azure_openai.image_quality - + # Map DALL-E quality values to gpt-image-1 or gpt-image-1.5 equivalents quality_mapping = { "standard": "medium", "hd": "high", } quality = quality_mapping.get(quality, quality) - + # Map DALL-E sizes to gpt-image-1 or gpt-image-1.5 equivalents if needed size_mapping = { "1024x1792": "1024x1536", # Closest equivalent "1792x1024": "1536x1024", # Closest equivalent } size = size_mapping.get(size, size) - + # gpt-image-1 can handle larger prompts, so we can include more context truncated_product_desc = _truncate_for_dalle(product_description, max_chars=3000) - + main_prompt = prompt[:2000] if len(prompt) > 2000 else prompt scene_desc = scene_description[:1000] if scene_description and len(scene_description) > 1000 else scene_description - + # Build the full prompt with product context and brand guidelines full_prompt = f"""⚠️ ABSOLUTE RULE: THIS IMAGE MUST CONTAIN ZERO TEXT. NO WORDS. NO LETTERS. NO PRODUCT NAMES. NO COLOR NAMES. NO LABELS. @@ -326,25 +326,23 @@ async def _generate_gpt_image( credential = ManagedIdentityCredential(client_id=client_id) else: credential = DefaultAzureCredential() - + # Get token for Azure OpenAI token = await credential.get_token("https://cognitiveservices.azure.com/.default") - + # Use gpt-image-1 specific endpoint if configured, otherwise DALL-E endpoint, otherwise main endpoint - image_endpoint = ( - app_settings.azure_openai.gpt_image_endpoint or - app_settings.azure_openai.dalle_endpoint or - app_settings.azure_openai.endpoint - ) + image_endpoint = (app_settings.azure_openai.gpt_image_endpoint + or app_settings.azure_openai.dalle_endpoint + or app_settings.azure_openai.endpoint) logger.info(f"Using gpt-image-1 endpoint: {image_endpoint}") - + # Use the image-specific API version for gpt-image-1 (requires 2025-04-01-preview or newer) client = AsyncAzureOpenAI( azure_endpoint=image_endpoint, azure_ad_token=token.token, api_version=app_settings.azure_openai.image_api_version, ) - + try: # gpt-image-1/1.5 API call - note: gpt-image doesn't support response_format parameter # It returns base64 data directly in the response @@ -355,12 +353,12 @@ async def _generate_gpt_image( quality=quality, n=1, ) - + image_data = response.data[0] - + # gpt-image-1 returns b64_json directly without needing response_format parameter image_base64 = getattr(image_data, 'b64_json', None) - + # If no b64_json, try to get URL and fetch the image if not image_base64 and hasattr(image_data, 'url') and image_data.url: import aiohttp @@ -370,7 +368,7 @@ async def _generate_gpt_image( import base64 image_bytes = await resp.read() image_base64 = base64.b64encode(image_bytes).decode('utf-8') - + if not image_base64: return { "success": False, @@ -378,7 +376,7 @@ async def _generate_gpt_image( "prompt_used": full_prompt, "model": "gpt-image-1", } - + return { "success": True, "image_base64": image_base64, @@ -389,7 +387,7 @@ async def _generate_gpt_image( finally: # Properly close the async client to avoid unclosed session warnings await client.close() - + except Exception as e: logger.exception(f"Error generating gpt-image-1 image: {e}") return { diff --git a/content-gen/src/backend/api/admin.py b/content-gen/src/backend/api/admin.py index 9974be307..ab3948523 100644 --- a/content-gen/src/backend/api/admin.py +++ b/content-gen/src/backend/api/admin.py @@ -35,14 +35,14 @@ def verify_admin_api_key() -> bool: """ Verify the admin API key from request headers. - + If ADMIN_API_KEY is not set, all requests are allowed (development mode). If set, the request must include X-Admin-API-Key header with matching value. """ if not ADMIN_API_KEY: # No API key configured - allow all requests (development/initial setup) return True - + provided_key = request.headers.get("X-Admin-API-Key", "") return provided_key == ADMIN_API_KEY @@ -61,7 +61,7 @@ def unauthorized_response(): async def upload_images(): """ Upload product images to Blob Storage. - + Request body: { "images": [ @@ -73,7 +73,7 @@ async def upload_images(): ... ] } - + Returns: { "success": true, @@ -87,29 +87,29 @@ async def upload_images(): """ if not verify_admin_api_key(): return unauthorized_response() - + try: data = await request.get_json() images = data.get("images", []) - + if not images: return jsonify({ "error": "No images provided", "message": "Request body must contain 'images' array" }), 400 - + blob_service = await get_blob_service() await blob_service.initialize() - + results = [] uploaded_count = 0 failed_count = 0 - + for image_info in images: filename = image_info.get("filename", "") content_type = image_info.get("content_type", "image/png") image_data_b64 = image_info.get("data", "") - + if not filename or not image_data_b64: results.append({ "filename": filename or "unknown", @@ -118,11 +118,11 @@ async def upload_images(): }) failed_count += 1 continue - + try: # Decode base64 image data image_data = base64.b64decode(image_data_b64) - + # Upload to product-images container blob_client = blob_service._product_images_container.get_blob_client(filename) await blob_client.upload_blob( @@ -130,7 +130,7 @@ async def upload_images(): overwrite=True, content_settings=ContentSettings(content_type=content_type) ) - + results.append({ "filename": filename, "status": "uploaded", @@ -139,7 +139,7 @@ async def upload_images(): }) uploaded_count += 1 logger.info(f"Uploaded image: {filename} ({len(image_data):,} bytes)") - + except Exception as e: logger.error(f"Failed to upload image {filename}: {e}") results.append({ @@ -148,14 +148,14 @@ async def upload_images(): "error": str(e) }) failed_count += 1 - + return jsonify({ "success": failed_count == 0, "uploaded": uploaded_count, "failed": failed_count, "results": results }) - + except Exception as e: logger.exception(f"Error in upload_images: {e}") return jsonify({ @@ -170,7 +170,7 @@ async def upload_images(): async def load_sample_data(): """ Load sample product data to Cosmos DB. - + Request body: { "products": [ @@ -187,7 +187,7 @@ async def load_sample_data(): ], "clear_existing": true // Optional: delete existing products first } - + Returns: { "success": true, @@ -202,34 +202,34 @@ async def load_sample_data(): """ if not verify_admin_api_key(): return unauthorized_response() - + try: data = await request.get_json() products_data = data.get("products", []) clear_existing = data.get("clear_existing", False) - + if not products_data: return jsonify({ "error": "No products provided", "message": "Request body must contain 'products' array" }), 400 - + cosmos_service = await get_cosmos_service() - + deleted_count = 0 if clear_existing: logger.info("Deleting existing products...") deleted_count = await cosmos_service.delete_all_products() logger.info(f"Deleted {deleted_count} existing products") - + results = [] loaded_count = 0 failed_count = 0 - + for product_data in products_data: sku = product_data.get("sku", "") product_name = product_data.get("product_name", "") - + try: # Map incoming fields to Product model fields # Note: Product model requires 'description' field, map from incoming 'description' or 'marketing_description' @@ -248,10 +248,10 @@ async def load_sample_data(): "tags": product_data.get("tags", ""), "price": product_data.get("price", 0.0), } - + product = Product(**product_fields) await cosmos_service.upsert_product(product) - + results.append({ "sku": sku, "product_name": product_name, @@ -259,7 +259,7 @@ async def load_sample_data(): }) loaded_count += 1 logger.info(f"Loaded product: {product_name} ({sku})") - + except Exception as e: logger.error(f"Failed to load product {sku}: {e}") results.append({ @@ -269,19 +269,19 @@ async def load_sample_data(): "error": str(e) }) failed_count += 1 - + response = { "success": failed_count == 0, "loaded": loaded_count, "failed": failed_count, "results": results } - + if clear_existing: response["deleted"] = deleted_count - + return jsonify(response) - + except Exception as e: logger.exception(f"Error in load_sample_data: {e}") return jsonify({ @@ -296,13 +296,13 @@ async def load_sample_data(): async def create_search_index(): """ Create or update the Azure AI Search index with products from Cosmos DB. - + Request body (optional): { "index_name": "products", // Optional: defaults to "products" "reindex_all": true // Optional: re-index all products } - + Returns: { "success": true, @@ -317,7 +317,7 @@ async def create_search_index(): """ if not verify_admin_api_key(): return unauthorized_response() - + try: # Import search-related dependencies from azure.core.credentials import AzureKeyCredential @@ -338,17 +338,17 @@ async def create_search_index(): VectorSearch, VectorSearchProfile, ) - + data = await request.get_json() or {} index_name = data.get("index_name", app_settings.search.products_index if app_settings.search else "products") - + search_endpoint = app_settings.search.endpoint if app_settings.search else None if not search_endpoint: return jsonify({ "error": "Search service not configured", "message": "AZURE_AI_SEARCH_ENDPOINT environment variable not set" }), 500 - + # Get credential - try API key first, then RBAC admin_key = app_settings.search.admin_key if app_settings.search else None if admin_key: @@ -357,10 +357,10 @@ async def create_search_index(): else: credential = DefaultAzureCredential() logger.info("Using RBAC authentication for search") - + # Create index client index_client = SearchIndexClient(endpoint=search_endpoint, credential=credential) - + # Define index schema fields = [ SimpleField(name="id", type=SearchFieldDataType.String, key=True, filterable=True), @@ -381,12 +381,12 @@ async def create_search_index(): vector_search_profile_name="product-vector-profile" ) ] - + vector_search = VectorSearch( algorithms=[HnswAlgorithmConfiguration(name="hnsw-algorithm")], profiles=[VectorSearchProfile(name="product-vector-profile", algorithm_configuration_name="hnsw-algorithm")] ) - + semantic_config = SemanticConfiguration( name="product-semantic-config", prioritized_fields=SemanticPrioritizedFields( @@ -404,24 +404,24 @@ async def create_search_index(): ] ) ) - + index = SearchIndex( name=index_name, fields=fields, vector_search=vector_search, semantic_search=SemanticSearch(configurations=[semantic_config]) ) - + # Create or update index logger.info(f"Creating/updating search index: {index_name}") index_client.create_or_update_index(index) logger.info("Search index created/updated successfully") - + # Get products from Cosmos DB cosmos_service = await get_cosmos_service() products = await cosmos_service.get_all_products(limit=1000) logger.info(f"Found {len(products)} products to index") - + if not products: return jsonify({ "success": True, @@ -431,15 +431,15 @@ async def create_search_index(): "message": "No products found to index", "results": [] }) - + # Prepare documents for indexing documents = [] results = [] - + for product in products: p = product.model_dump() doc_id = p.get('sku', '').lower().replace("-", "_").replace(" ", "_") or p.get('id', 'unknown') - + combined_text = f""" {p.get('product_name', '')} Category: {p.get('category', '')} - {p.get('sub_category', '')} @@ -448,7 +448,7 @@ async def create_search_index(): Specifications: {p.get('detailed_spec_description', '')} Visual: {p.get('image_description', '')} """ - + documents.append({ "id": doc_id, "product_name": p.get("product_name", ""), @@ -462,22 +462,22 @@ async def create_search_index(): "combined_text": combined_text.strip(), "content_vector": [0.0] * 1536 # Placeholder vector }) - + results.append({ "sku": p.get("sku", ""), "product_name": p.get("product_name", ""), "status": "pending" }) - + # Upload documents to search index search_client = SearchClient(endpoint=search_endpoint, index_name=index_name, credential=credential) - + try: upload_result = search_client.upload_documents(documents) - + indexed_count = 0 failed_count = 0 - + for i, r in enumerate(upload_result): if r.succeeded: results[i]["status"] = "indexed" @@ -486,9 +486,9 @@ async def create_search_index(): results[i]["status"] = "failed" results[i]["error"] = str(r.error_message) if hasattr(r, 'error_message') else "Unknown error" failed_count += 1 - + logger.info(f"Indexed {indexed_count} products, {failed_count} failed") - + return jsonify({ "success": failed_count == 0, "indexed": indexed_count, @@ -496,14 +496,14 @@ async def create_search_index(): "index_name": index_name, "results": results }) - + except Exception as e: logger.exception(f"Failed to index documents: {e}") return jsonify({ "error": "Failed to index documents", "message": str(e) }), 500 - + except Exception as e: logger.exception(f"Error in create_search_index: {e}") return jsonify({ @@ -518,7 +518,7 @@ async def create_search_index(): async def admin_health(): """ Health check for admin API. - + Does not require authentication - used to verify the admin API is available. """ return jsonify({ diff --git a/content-gen/src/backend/app.py b/content-gen/src/backend/app.py index 3fe4ffc6c..fb28bde72 100644 --- a/content-gen/src/backend/app.py +++ b/content-gen/src/backend/app.py @@ -47,14 +47,14 @@ def get_authenticated_user(): """ Get the authenticated user from EasyAuth headers. - + In production (with App Service Auth), the X-Ms-Client-Principal-Id header contains the user's ID. In development mode, returns "anonymous". """ user_principal_id = request.headers.get("X-Ms-Client-Principal-Id", "") user_name = request.headers.get("X-Ms-Client-Principal-Name", "") auth_provider = request.headers.get("X-Ms-Client-Principal-Idp", "") - + return { "user_principal_id": user_principal_id or "anonymous", "user_name": user_name or "", @@ -82,27 +82,27 @@ async def health_check(): async def chat(): """ Process a chat message through the agent orchestration. - + Request body: { "message": "User's message", "conversation_id": "optional-uuid", "user_id": "user identifier" } - + Returns streaming response with agent responses. """ data = await request.get_json() - + message = data.get("message", "") conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") - + if not message: return jsonify({"error": "Message is required"}), 400 - + orchestrator = get_orchestrator() - + # Try to save to CosmosDB but don't fail if it's unavailable try: cosmos_service = await get_cosmos_service() @@ -117,7 +117,7 @@ async def chat(): ) except Exception as e: logger.warning(f"Failed to save message to CosmosDB: {e}") - + async def generate(): """Stream responses from the orchestrator.""" try: @@ -126,7 +126,7 @@ async def generate(): conversation_id=conversation_id ): yield f"data: {json.dumps(response)}\n\n" - + # Save assistant responses when final OR when requiring user input if response.get("is_final") or response.get("requires_user_input"): if response.get("content"): @@ -147,9 +147,9 @@ async def generate(): except Exception as e: logger.exception(f"Error in orchestrator: {e}") yield f"data: {json.dumps({'type': 'error', 'content': str(e), 'is_final': True})}\n\n" - + yield "data: [DONE]\n\n" - + return Response( generate(), mimetype="text/event-stream", @@ -167,14 +167,14 @@ async def parse_brief(): """ Parse a free-text creative brief into structured format. If critical information is missing, return clarifying questions. - + Request body: { "brief_text": "Free-form creative brief text", "conversation_id": "optional-uuid", "user_id": "user identifier" } - + Returns: Structured CreativeBrief JSON for user confirmation, or clarifying questions if info is missing. @@ -183,10 +183,10 @@ async def parse_brief(): brief_text = data.get("brief_text", "") conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") - + if not brief_text: return jsonify({"error": "Brief text is required"}), 400 - + # Save the user's brief text as a message to CosmosDB try: cosmos_service = await get_cosmos_service() @@ -201,10 +201,10 @@ async def parse_brief(): ) except Exception as e: logger.warning(f"Failed to save brief message to CosmosDB: {e}") - + orchestrator = get_orchestrator() parsed_brief, clarifying_questions, rai_blocked = await orchestrator.parse_brief(brief_text) - + # Check if request was blocked due to harmful content if rai_blocked: # Save the refusal as assistant response @@ -222,7 +222,7 @@ async def parse_brief(): ) except Exception as e: logger.warning(f"Failed to save RAI response to CosmosDB: {e}") - + return jsonify({ "rai_blocked": True, "requires_clarification": False, @@ -230,7 +230,7 @@ async def parse_brief(): "conversation_id": conversation_id, "message": clarifying_questions }) - + # Check if we need clarifying questions if clarifying_questions: # Save the clarifying questions as assistant response @@ -248,7 +248,7 @@ async def parse_brief(): ) except Exception as e: logger.warning(f"Failed to save clarifying questions to CosmosDB: {e}") - + return jsonify({ "brief": parsed_brief.model_dump(), "requires_clarification": True, @@ -257,7 +257,7 @@ async def parse_brief(): "conversation_id": conversation_id, "message": clarifying_questions }) - + # Save the assistant's parsing response try: cosmos_service = await get_cosmos_service() @@ -273,7 +273,7 @@ async def parse_brief(): ) except Exception as e: logger.warning(f"Failed to save parsing response to CosmosDB: {e}") - + return jsonify({ "brief": parsed_brief.model_dump(), "requires_clarification": False, @@ -287,14 +287,14 @@ async def parse_brief(): async def confirm_brief(): """ Confirm or modify a parsed creative brief. - + Request body: { "brief": { ... CreativeBrief fields ... }, "conversation_id": "optional-uuid", "user_id": "user identifier" } - + Returns: Confirmation status and next steps. """ @@ -302,20 +302,20 @@ async def confirm_brief(): brief_data = data.get("brief", {}) conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") - + try: brief = CreativeBrief(**brief_data) except Exception as e: return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400 - + # Try to save the confirmed brief to CosmosDB, preserving existing messages try: cosmos_service = await get_cosmos_service() - + # Get existing conversation to preserve messages existing = await cosmos_service.get_conversation(conversation_id, user_id) existing_messages = existing.get("messages", []) if existing else [] - + # Add confirmation message existing_messages.append({ "role": "assistant", @@ -323,7 +323,7 @@ async def confirm_brief(): "agent": "TriageAgent", "timestamp": datetime.now(timezone.utc).isoformat() }) - + await cosmos_service.save_conversation( conversation_id=conversation_id, user_id=user_id, @@ -333,7 +333,7 @@ async def confirm_brief(): ) except Exception as e: logger.warning(f"Failed to save brief to CosmosDB: {e}") - + return jsonify({ "status": "confirmed", "conversation_id": conversation_id, @@ -348,7 +348,7 @@ async def confirm_brief(): async def select_products(): """ Select or modify products via natural language. - + Request body: { "request": "User's natural language request", @@ -356,20 +356,20 @@ async def select_products(): "conversation_id": "optional-uuid", "user_id": "user identifier" } - + Returns: Selected products and assistant message. """ data = await request.get_json() - + request_text = data.get("request", "") current_products = data.get("current_products", []) conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") - + if not request_text: return jsonify({"error": "Request text is required"}), 400 - + # Save user message try: cosmos_service = await get_cosmos_service() @@ -384,14 +384,14 @@ async def select_products(): ) except Exception as e: logger.warning(f"Failed to save product selection request to CosmosDB: {e}") - + # Get available products from catalog try: cosmos_service = await get_cosmos_service() all_products = await cosmos_service.get_all_products(limit=50) # Use mode='json' to ensure datetime objects are serialized to strings available_products = [p.model_dump(mode='json') for p in all_products] - + # Convert blob URLs to proxy URLs for p in available_products: if p.get("image_url"): @@ -401,7 +401,7 @@ async def select_products(): except Exception as e: logger.warning(f"Failed to get products from CosmosDB: {e}") available_products = [] - + # Use orchestrator to process the selection request orchestrator = get_orchestrator() result = await orchestrator.select_products( @@ -409,7 +409,7 @@ async def select_products(): current_products=current_products, available_products=available_products ) - + # Save assistant response try: cosmos_service = await get_cosmos_service() @@ -425,7 +425,7 @@ async def select_products(): ) except Exception as e: logger.warning(f"Failed to save product selection response to CosmosDB: {e}") - + return jsonify({ "products": result.get("products", []), "action": result.get("action", "search"), @@ -436,23 +436,23 @@ async def select_products(): # ==================== Content Generation Endpoints ==================== -async def _run_generation_task(task_id: str, brief: CreativeBrief, products_data: list, - generate_images: bool, conversation_id: str, user_id: str): +async def _run_generation_task(task_id: str, brief: CreativeBrief, products_data: list, + generate_images: bool, conversation_id: str, user_id: str): """Background task to run content generation.""" try: logger.info(f"Starting background generation task {task_id}") _generation_tasks[task_id]["status"] = "running" _generation_tasks[task_id]["started_at"] = datetime.now(timezone.utc).isoformat() - + orchestrator = get_orchestrator() response = await orchestrator.generate_content( brief=brief, products=products_data, generate_images=generate_images ) - + logger.info(f"Generation task {task_id} completed. Response keys: {list(response.keys()) if response else 'None'}") - + # Handle image URL from orchestrator's blob save if response.get("image_blob_url"): blob_url = response["image_blob_url"] @@ -479,11 +479,11 @@ async def _run_generation_task(task_id: str, brief: CreativeBrief, products_data del response["image_base64"] except Exception as e: logger.warning(f"Failed to save image to blob: {e}") - + # Save to CosmosDB try: cosmos_service = await get_cosmos_service() - + await cosmos_service.add_message_to_conversation( conversation_id=conversation_id, user_id=user_id, @@ -494,7 +494,7 @@ async def _run_generation_task(task_id: str, brief: CreativeBrief, products_data "timestamp": datetime.now(timezone.utc).isoformat() } ) - + generated_content_to_save = { "text_content": response.get("text_content"), "image_url": response.get("image_url"), @@ -511,12 +511,12 @@ async def _run_generation_task(task_id: str, brief: CreativeBrief, products_data ) except Exception as e: logger.warning(f"Failed to save generated content to CosmosDB: {e}") - + _generation_tasks[task_id]["status"] = "completed" _generation_tasks[task_id]["result"] = response _generation_tasks[task_id]["completed_at"] = datetime.now(timezone.utc).isoformat() logger.info(f"Task {task_id} marked as completed") - + except Exception as e: logger.exception(f"Generation task {task_id} failed: {e}") _generation_tasks[task_id]["status"] = "failed" @@ -529,7 +529,7 @@ async def start_generation(): """ Start content generation and return immediately with a task ID. Client should poll /api/generate/status/ for results. - + Request body: { "brief": { ... CreativeBrief fields ... }, @@ -537,7 +537,7 @@ async def start_generation(): "generate_images": true/false, "conversation_id": "uuid" } - + Returns: { "task_id": "uuid", @@ -545,24 +545,23 @@ async def start_generation(): "message": "Generation started" } """ - global _generation_tasks - + data = await request.get_json() - + brief_data = data.get("brief", {}) products_data = data.get("products", []) generate_images = data.get("generate_images", True) conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") - + try: brief = CreativeBrief(**brief_data) except Exception as e: return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400 - + # Create task ID task_id = str(uuid.uuid4()) - + # Initialize task state _generation_tasks[task_id] = { "status": "pending", @@ -571,7 +570,7 @@ async def start_generation(): "result": None, "error": None } - + # Save user request try: cosmos_service = await get_cosmos_service() @@ -587,7 +586,7 @@ async def start_generation(): ) except Exception as e: logger.warning(f"Failed to save generation request to CosmosDB: {e}") - + # Start background task asyncio.create_task(_run_generation_task( task_id=task_id, @@ -597,9 +596,9 @@ async def start_generation(): conversation_id=conversation_id, user_id=user_id )) - + logger.info(f"Started generation task {task_id} for conversation {conversation_id}") - + return jsonify({ "task_id": task_id, "status": "pending", @@ -612,7 +611,7 @@ async def start_generation(): async def get_generation_status(task_id: str): """ Get the status of a generation task. - + Returns: { "task_id": "uuid", @@ -621,20 +620,19 @@ async def get_generation_status(task_id: str): "error": "error message" (if failed) } """ - global _generation_tasks - + if task_id not in _generation_tasks: return jsonify({"error": "Task not found"}), 404 - + task = _generation_tasks[task_id] - + response = { "task_id": task_id, "status": task["status"], "conversation_id": task.get("conversation_id"), "created_at": task.get("created_at"), } - + if task["status"] == "completed": response["result"] = task["result"] response["completed_at"] = task.get("completed_at") @@ -644,7 +642,7 @@ async def get_generation_status(task_id: str): elif task["status"] == "running": response["started_at"] = task.get("started_at") response["message"] = "Generation in progress..." - + return jsonify(response) @@ -652,7 +650,7 @@ async def get_generation_status(task_id: str): async def generate_content(): """ Generate content from a confirmed creative brief. - + Request body: { "brief": { ... CreativeBrief fields ... }, @@ -660,24 +658,24 @@ async def generate_content(): "generate_images": true/false, "conversation_id": "uuid" } - + Returns streaming response with generated content. """ import asyncio - + data = await request.get_json() - + brief_data = data.get("brief", {}) products_data = data.get("products", []) generate_images = data.get("generate_images", True) conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") - + try: brief = CreativeBrief(**brief_data) except Exception as e: return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400 - + # Save user request for content generation try: cosmos_service = await get_cosmos_service() @@ -693,14 +691,14 @@ async def generate_content(): ) except Exception as e: logger.warning(f"Failed to save generation request to CosmosDB: {e}") - + orchestrator = get_orchestrator() - + async def generate(): """Stream content generation responses with keepalive heartbeats.""" logger.info(f"Starting SSE generator for conversation {conversation_id}") generation_task = None - + try: # Create a task for the long-running generation generation_task = asyncio.create_task( @@ -711,10 +709,10 @@ async def generate(): ) ) logger.info("Generation task created") - + # Send keepalive heartbeats every 15 seconds while generation is running heartbeat_count = 0 - + while not generation_task.done(): # Check every 0.5 seconds (faster response to completion) for _ in range(30): # 30 * 0.5s = 15 seconds @@ -722,12 +720,12 @@ async def generate(): logger.info("Task completed during heartbeat wait (iteration)") break await asyncio.sleep(0.5) - + if not generation_task.done(): heartbeat_count += 1 logger.info(f"Sending heartbeat {heartbeat_count}") yield f"data: {json.dumps({'type': 'heartbeat', 'count': heartbeat_count, 'message': 'Generating content...'})}\n\n" - + logger.info(f"Generation task completed after {heartbeat_count} heartbeats") except asyncio.CancelledError: logger.warning(f"SSE generator cancelled for conversation {conversation_id}") @@ -744,7 +742,7 @@ async def generate(): if generation_task and not generation_task.done(): generation_task.cancel() raise - + # Get the result try: response = generation_task.result() @@ -753,7 +751,7 @@ async def generate(): has_image_blob = bool(response.get("image_blob_url")) if response else False image_size = len(response.get("image_base64", "")) if response else 0 logger.info(f"Has image_base64: {has_image_base64}, has_image_blob_url: {has_image_blob}, base64_size: {image_size} bytes") - + # Handle image URL from orchestrator's blob save if response.get("image_blob_url"): blob_url = response["image_blob_url"] @@ -787,11 +785,11 @@ async def generate(): # Keep image_base64 in response as fallback if blob storage fails else: logger.info("No image in response") - + # Save generated content to conversation try: cosmos_service = await get_cosmos_service() - + # Save the message await cosmos_service.add_message_to_conversation( conversation_id=conversation_id, @@ -803,7 +801,7 @@ async def generate(): "timestamp": datetime.now(timezone.utc).isoformat() } ) - + # Save the full generated content for restoration # Note: image_base64 is NOT saved to CosmosDB as it exceeds document size limits # Images will only persist if blob storage is working @@ -823,15 +821,15 @@ async def generate(): ) except Exception as e: logger.warning(f"Failed to save generated content to CosmosDB: {e}") - + # Format response to match what frontend expects yield f"data: {json.dumps({'type': 'agent_response', 'content': json.dumps(response), 'is_final': True})}\n\n" except Exception as e: logger.exception(f"Error generating content: {e}") yield f"data: {json.dumps({'type': 'error', 'content': str(e), 'is_final': True})}\n\n" - + yield "data: [DONE]\n\n" - + return Response( generate(), mimetype="text/event-stream", @@ -848,10 +846,10 @@ async def generate(): async def regenerate_content(): """ Regenerate image based on user modification request. - + This endpoint is called when the user wants to modify the generated image after initial content generation (e.g., "show a kitchen instead of dining room"). - + Request body: { "modification_request": "User's modification request", @@ -860,28 +858,28 @@ async def regenerate_content(): "previous_image_prompt": "Previous image prompt (optional)", "conversation_id": "uuid" } - + Returns regenerated image with the modification applied. """ import asyncio - + data = await request.get_json() - + modification_request = data.get("modification_request", "") brief_data = data.get("brief", {}) products_data = data.get("products", []) previous_image_prompt = data.get("previous_image_prompt") conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") - + if not modification_request: return jsonify({"error": "modification_request is required"}), 400 - + try: brief = CreativeBrief(**brief_data) except Exception as e: return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400 - + # Save user request for regeneration try: cosmos_service = await get_cosmos_service() @@ -896,14 +894,14 @@ async def regenerate_content(): ) except Exception as e: logger.warning(f"Failed to save regeneration request to CosmosDB: {e}") - + orchestrator = get_orchestrator() - + async def generate(): """Stream regeneration responses with keepalive heartbeats.""" logger.info(f"Starting image regeneration for conversation {conversation_id}") regeneration_task = None - + try: # Create a task for the regeneration regeneration_task = asyncio.create_task( @@ -914,7 +912,7 @@ async def generate(): previous_image_prompt=previous_image_prompt ) ) - + # Send keepalive heartbeats while regeneration is running heartbeat_count = 0 while not regeneration_task.done(): @@ -922,11 +920,11 @@ async def generate(): if regeneration_task.done(): break await asyncio.sleep(0.5) - + if not regeneration_task.done(): heartbeat_count += 1 yield f"data: {json.dumps({'type': 'heartbeat', 'count': heartbeat_count, 'message': 'Regenerating image...'})}\n\n" - + except asyncio.CancelledError: logger.warning(f"Regeneration cancelled for conversation {conversation_id}") if regeneration_task and not regeneration_task.done(): @@ -937,18 +935,18 @@ async def generate(): if regeneration_task and not regeneration_task.done(): regeneration_task.cancel() return - + # Get the result try: response = regeneration_task.result() logger.info(f"Regeneration complete. Response keys: {list(response.keys()) if response else 'None'}") - + # Check for RAI block if response.get("rai_blocked"): yield f"data: {json.dumps({'type': 'error', 'content': response.get('error', 'Request blocked by content safety'), 'rai_blocked': True, 'is_final': True})}\n\n" yield "data: [DONE]\n\n" return - + # Handle image URL from orchestrator's blob save if response.get("image_blob_url"): blob_url = response["image_blob_url"] @@ -972,7 +970,7 @@ async def generate(): del response["image_base64"] except Exception as e: logger.warning(f"Failed to save regenerated image to blob: {e}") - + # Save assistant response try: cosmos_service = await get_cosmos_service() @@ -988,14 +986,14 @@ async def generate(): ) except Exception as e: logger.warning(f"Failed to save regeneration response to CosmosDB: {e}") - + yield f"data: {json.dumps({'type': 'agent_response', 'content': json.dumps(response), 'is_final': True})}\n\n" except Exception as e: logger.exception(f"Error in regeneration: {e}") yield f"data: {json.dumps({'type': 'error', 'content': str(e), 'is_final': True})}\n\n" - + yield "data: [DONE]\n\n" - + return Response( generate(), mimetype="text/event-stream", @@ -1019,17 +1017,17 @@ async def proxy_generated_image(conversation_id: str, filename: str): try: blob_service = await get_blob_service() await blob_service.initialize() - + blob_name = f"{conversation_id}/{filename}" blob_client = blob_service._generated_images_container.get_blob_client(blob_name) - + # Download the blob download = await blob_client.download_blob() image_data = await download.readall() - + # Determine content type from filename content_type = "image/png" if filename.endswith(".png") else "image/jpeg" - + return Response( image_data, mimetype=content_type, @@ -1052,26 +1050,26 @@ async def proxy_product_image(filename: str): try: blob_service = await get_blob_service() await blob_service.initialize() - + blob_client = blob_service._product_images_container.get_blob_client(filename) - + # Get blob properties for ETag/Last-Modified properties = await blob_client.get_blob_properties() etag = properties.etag.strip('"') if properties.etag else None last_modified = properties.last_modified - + # Check If-None-Match header for cache validation if_none_match = request.headers.get("If-None-Match") if if_none_match and etag and if_none_match.strip('"') == etag: return Response(status=304) # Not Modified - + # Download the blob download = await blob_client.download_blob() image_data = await download.readall() - + # Determine content type from filename content_type = "image/png" if filename.endswith(".png") else "image/jpeg" - + headers = { "Cache-Control": "public, max-age=300, must-revalidate", # Cache 5 min, revalidate } @@ -1079,7 +1077,7 @@ async def proxy_product_image(filename: str): headers["ETag"] = f'"{etag}"' if last_modified: headers["Last-Modified"] = last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT") - + return Response( image_data, mimetype=content_type, @@ -1096,7 +1094,7 @@ async def proxy_product_image(filename: str): async def list_products(): """ List all products. - + Query params: category: Filter by category sub_category: Filter by sub-category @@ -1107,9 +1105,9 @@ async def list_products(): sub_category = request.args.get("sub_category") search = request.args.get("search") limit = int(request.args.get("limit", 20)) - + cosmos_service = await get_cosmos_service() - + if search: products = await cosmos_service.search_products(search, limit) elif category: @@ -1118,7 +1116,7 @@ async def list_products(): ) else: products = await cosmos_service.get_all_products(limit) - + # Convert blob URLs to proxy URLs for products with images product_list = [] for p in products: @@ -1130,7 +1128,7 @@ async def list_products(): filename = original_url.split("/")[-1] if "/" in original_url else original_url product_dict["image_url"] = f"/api/product-images/{filename}" product_list.append(product_dict) - + return jsonify({ "products": product_list, "count": len(product_list) @@ -1142,17 +1140,17 @@ async def get_product(sku: str): """Get a product by SKU.""" cosmos_service = await get_cosmos_service() product = await cosmos_service.get_product_by_sku(sku) - + if not product: return jsonify({"error": "Product not found"}), 404 - + product_dict = product.model_dump() # Convert direct blob URL to proxy URL if product_dict.get("image_url"): original_url = product_dict["image_url"] filename = original_url.split("/")[-1] if "/" in original_url else original_url product_dict["image_url"] = f"/api/product-images/{filename}" - + return jsonify(product_dict) @@ -1160,7 +1158,7 @@ async def get_product(sku: str): async def create_product(): """ Create or update a product. - + Request body: { "product_name": "...", @@ -1173,15 +1171,15 @@ async def create_product(): } """ data = await request.get_json() - + try: product = Product(**data) except Exception as e: return jsonify({"error": f"Invalid product format: {str(e)}"}), 400 - + cosmos_service = await get_cosmos_service() saved_product = await cosmos_service.upsert_product(product) - + return jsonify(saved_product.model_dump()), 201 @@ -1189,38 +1187,38 @@ async def create_product(): async def upload_product_image(sku: str): """ Upload an image for a product. - + The image will be stored and a description will be auto-generated using GPT-5 Vision. - + Request: multipart/form-data with 'image' file """ cosmos_service = await get_cosmos_service() product = await cosmos_service.get_product_by_sku(sku) - + if not product: return jsonify({"error": "Product not found"}), 404 - + files = await request.files if "image" not in files: return jsonify({"error": "No image file provided"}), 400 - + image_file = files["image"] image_data = image_file.read() content_type = image_file.content_type or "image/jpeg" - + blob_service = await get_blob_service() image_url, description = await blob_service.upload_product_image( sku=sku, image_data=image_data, content_type=content_type ) - + # Update product with image info product.image_url = image_url product.image_description = description await cosmos_service.upsert_product(product) - + return jsonify({ "image_url": image_url, "image_description": description, @@ -1234,21 +1232,21 @@ async def upload_product_image(sku: str): async def list_conversations(): """ List conversations for a user. - + Uses authenticated user from EasyAuth headers. In development mode (when not authenticated), uses "anonymous" as user_id. - + Query params: limit: Max number of results (default 20) """ auth_user = get_authenticated_user() user_id = auth_user["user_principal_id"] - + limit = int(request.args.get("limit", 20)) - + cosmos_service = await get_cosmos_service() conversations = await cosmos_service.get_user_conversations(user_id, limit) - + return jsonify({ "conversations": conversations, "count": len(conversations) @@ -1259,18 +1257,18 @@ async def list_conversations(): async def get_conversation(conversation_id: str): """ Get a specific conversation. - + Uses authenticated user from EasyAuth headers. """ auth_user = get_authenticated_user() user_id = auth_user["user_principal_id"] - + cosmos_service = await get_cosmos_service() conversation = await cosmos_service.get_conversation(conversation_id, user_id) - + if not conversation: return jsonify({"error": "Conversation not found"}), 404 - + return jsonify(conversation) @@ -1278,12 +1276,12 @@ async def get_conversation(conversation_id: str): async def delete_conversation(conversation_id: str): """ Delete a specific conversation. - + Uses authenticated user from EasyAuth headers. """ auth_user = get_authenticated_user() user_id = auth_user["user_principal_id"] - + try: cosmos_service = await get_cosmos_service() await cosmos_service.delete_conversation(conversation_id, user_id) @@ -1297,9 +1295,9 @@ async def delete_conversation(conversation_id: str): async def update_conversation(conversation_id: str): """ Update a conversation (rename). - + Uses authenticated user from EasyAuth headers. - + Request body: { "title": "New conversation title" @@ -1307,13 +1305,13 @@ async def update_conversation(conversation_id: str): """ auth_user = get_authenticated_user() user_id = auth_user["user_principal_id"] - + data = await request.get_json() new_title = data.get("title", "").strip() - + if not new_title: return jsonify({"error": "Title is required"}), 400 - + try: cosmos_service = await get_cosmos_service() result = await cosmos_service.rename_conversation(conversation_id, user_id, new_title) @@ -1364,24 +1362,24 @@ async def get_ui_config(): async def startup(): """Initialize services on application startup.""" logger.info("Starting Content Generation Solution Accelerator...") - + # Initialize orchestrator get_orchestrator() logger.info("Orchestrator initialized with Microsoft Agent Framework") - + # Try to initialize services - they may fail if CosmosDB/Blob storage is not accessible try: await get_cosmos_service() logger.info("CosmosDB service initialized") except Exception as e: logger.warning(f"CosmosDB service initialization failed (may be firewall): {e}") - + try: await get_blob_service() logger.info("Blob storage service initialized") except Exception as e: logger.warning(f"Blob storage service initialization failed: {e}") - + logger.info("Application startup complete") @@ -1389,13 +1387,13 @@ async def startup(): async def shutdown(): """Cleanup on application shutdown.""" logger.info("Shutting down Content Generation Solution Accelerator...") - + cosmos_service = await get_cosmos_service() await cosmos_service.close() - + blob_service = await get_blob_service() await blob_service.close() - + logger.info("Application shutdown complete") diff --git a/content-gen/src/backend/models.py b/content-gen/src/backend/models.py index cd357a226..5b9a5c79a 100644 --- a/content-gen/src/backend/models.py +++ b/content-gen/src/backend/models.py @@ -33,12 +33,12 @@ class ComplianceResult(BaseModel): """Result of compliance validation on generated content.""" is_valid: bool = Field(description="True if no error-level violations") violations: List[ComplianceViolation] = Field(default_factory=list) - + @property def has_errors(self) -> bool: """Check if there are any error-level violations.""" return any(v.severity == ComplianceSeverity.ERROR for v in self.violations) - + @property def has_warnings(self) -> bool: """Check if there are any warning-level violations.""" @@ -48,7 +48,7 @@ def has_warnings(self) -> bool: class CreativeBrief(BaseModel): """ Structured creative brief parsed from free-text input. - + The PlanningAgent extracts these fields from user's natural language creative brief description. """ @@ -61,7 +61,7 @@ class CreativeBrief(BaseModel): timelines: str = Field(description="Due dates and milestones") visual_guidelines: str = Field(description="Image requirements and visual direction") cta: str = Field(description="Call to action text and placement") - + # Metadata raw_input: Optional[str] = Field(default=None, description="Original free-text input") confidence_score: Optional[float] = Field(default=None, description="Extraction confidence 0-1") @@ -70,7 +70,7 @@ class CreativeBrief(BaseModel): class Product(BaseModel): """ Product information stored in CosmosDB. - + Designed for paint catalog products with name, description, tags, and price. Image URLs reference product images stored in Azure Blob Storage. """ @@ -81,7 +81,7 @@ class Product(BaseModel): price: float = Field(description="Price in USD") sku: str = Field(description="Stock keeping unit identifier (e.g., 'CP-0001')") image_url: Optional[str] = Field(default=None, description="URL to product image in Blob Storage") - + # Legacy fields for backward compatibility (optional) category: Optional[str] = Field(default="Paint", description="Product category") sub_category: Optional[str] = Field(default=None, description="Sub-category") @@ -89,7 +89,7 @@ class Product(BaseModel): detailed_spec_description: Optional[str] = Field(default=None, description="Detailed specs") model: Optional[str] = Field(default=None, description="Model number") image_description: Optional[str] = Field(default=None, description="Text description of image") - + # Metadata created_at: Optional[datetime] = None updated_at: Optional[datetime] = None @@ -121,7 +121,7 @@ class ContentGenerationResponse(BaseModel): products_used: List[str] = Field(default_factory=list, description="Product IDs used") generation_id: str = Field(description="Unique ID for this generation") created_at: datetime = Field(default_factory=datetime.utcnow) - + @property def requires_modification(self) -> bool: """Check if content has error-level violations requiring modification.""" @@ -137,7 +137,7 @@ class ConversationMessage(BaseModel): content: str created_at: datetime = Field(default_factory=datetime.utcnow) feedback: Optional[str] = None - + # For multimodal responses image_base64: Optional[str] = None compliance_warnings: Optional[List[ComplianceViolation]] = None diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index 7677ca362..8ea9538c1 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -21,9 +21,6 @@ import re from typing import AsyncIterator, Optional, cast -# Token endpoint for Azure Cognitive Services (used for Azure OpenAI) -TOKEN_ENDPOINT = "https://cognitiveservices.azure.com/.default" - from agent_framework import ( ChatMessage, HandoffBuilder, @@ -48,6 +45,9 @@ logger = logging.getLogger(__name__) +# Token endpoint for Azure Cognitive Services (used for Azure OpenAI) +TOKEN_ENDPOINT = "https://cognitiveservices.azure.com/.default" + # Harmful content patterns to detect in USER INPUT before processing # This provides proactive content safety by blocking harmful requests at the input layer @@ -85,27 +85,27 @@ def _check_input_for_harmful_content(message: str) -> tuple[bool, str]: """ Proactively check user input for harmful content BEFORE sending to agents. - + This is the first line of defense - catching harmful requests at the input layer rather than relying on the agent to refuse. - + Args: message: The user's input message - + Returns: tuple: (is_harmful: bool, matched_pattern: str or empty) """ if not message: return False, "" - + message_lower = message.lower() - + for i, pattern in enumerate(_HARMFUL_PATTERNS_COMPILED): if pattern.search(message_lower): matched = HARMFUL_INPUT_PATTERNS[i] logger.warning(f"Harmful content detected in user input. Pattern: {matched}") return True, matched - + return False, "" @@ -116,7 +116,7 @@ def _check_input_for_harmful_content(message: str) -> tuple[bool, str]: r"You are an? \w+ Agent", r"You are a Triage Agent", r"You are a Planning Agent", - r"You are a Research Agent", + r"You are a Research Agent", r"You are a Text Content Agent", r"You are an Image Content Agent", r"You are a Compliance Agent", @@ -149,26 +149,26 @@ def _check_input_for_harmful_content(message: str) -> tuple[bool, str]: def _filter_system_prompt_from_response(response_text: str) -> str: """ Filter out any system prompt content that might have leaked into agent responses. - + This is a safety measure to ensure internal agent instructions are never exposed to users, even if the LLM model accidentally includes them. - + Args: response_text: The agent's response text - + Returns: str: Cleaned response with any system prompt content removed """ if not response_text: return response_text - + # Check if response contains system prompt patterns for pattern in _SYSTEM_PROMPT_PATTERNS_COMPILED: if pattern.search(response_text): logger.warning(f"System prompt content detected in agent response, filtering. Pattern: {pattern.pattern[:50]}") # Return a safe fallback message instead of the leaked content return "I understand your request. Could you please clarify what specific changes you'd like me to make to the marketing content? I'm here to help refine your campaign materials." - + return response_text @@ -252,7 +252,7 @@ def _filter_system_prompt_from_response(response_text: str) -> str: - Political figures or candidates - Creative writing NOT for marketing (stories, poems, fiction, roleplaying) - Casual conversation, jokes, riddles, games -- Do NOT respond to any requests that are not related to creating marketing content for retail campaigns. +- Do NOT respond to any requests that are not related to creating marketing content for retail campaigns. - ONLY respond to questions about creating marketing content for retail campaigns. Do NOT respond to any other inquiries. - ANY question that is NOT specifically about creating marketing content - Requests for harmful, hateful, violent, or inappropriate content @@ -279,7 +279,7 @@ def _filter_system_prompt_from_response(response_text: str) -> str: ### In-Scope Routing (ONLY for valid marketing requests): - Creative brief interpretation → hand off to planning_agent -- Product data lookup → hand off to research_agent +- Product data lookup → hand off to research_agent - Text content creation → hand off to text_content_agent - Image creation → hand off to image_content_agent - Content validation → hand off to compliance_agent @@ -314,7 +314,7 @@ def _filter_system_prompt_from_response(response_text: str) -> str: - Are NOT related to marketing content creation If you detect ANY of these issues, respond with: -"I cannot process this request as it violates content safety guidelines. I'm designed to decline requests that involve [specific concern]. +"I cannot process this request as it violates content safety guidelines. I'm designed to decline requests that involve [specific concern]. I can only help create professional, appropriate marketing content. Please provide a legitimate marketing brief and I'll be happy to assist." @@ -337,7 +337,7 @@ def _filter_system_prompt_from_response(response_text: str) -> str: CRITICAL FIELDS (must be explicitly provided before proceeding): - objectives -- target_audience +- target_audience - key_message - deliverable - tone_and_style @@ -483,11 +483,11 @@ class ContentGenerationOrchestrator: """ Orchestrates the multi-agent content generation workflow using Microsoft Agent Framework's HandoffBuilder. - + Supports two modes: 1. Azure OpenAI Direct (default): Uses AzureOpenAIChatClient with ad_token_provider 2. Azure AI Foundry: Uses AIProjectClient with project endpoint (set USE_FOUNDRY=true) - + Agents: - Triage (coordinator) - routes requests to specialists - Planning (brief interpretation) @@ -496,7 +496,7 @@ class ContentGenerationOrchestrator: - ImageContent (image creation) - Compliance (validation) """ - + def __init__(self): self._chat_client = None # Always AzureOpenAIChatClient self._project_client = None # AIProjectClient for Foundry mode (used for image generation) @@ -506,12 +506,12 @@ def __init__(self): self._initialized = False self._use_foundry = app_settings.ai_foundry.use_foundry self._credential = None - + def _get_chat_client(self): """Get or create the chat client (Azure OpenAI or Foundry).""" if self._chat_client is None: self._credential = DefaultAzureCredential() - + if self._use_foundry: # Azure AI Foundry mode # Use AIProjectClient for project operations but use direct Azure OpenAI endpoint for chat @@ -520,37 +520,37 @@ def _get_chat_client(self): "Azure AI Foundry SDK not installed. " "Install with: pip install azure-ai-projects" ) - + project_endpoint = app_settings.ai_foundry.project_endpoint if not project_endpoint: raise ValueError("AZURE_AI_PROJECT_ENDPOINT is required when USE_FOUNDRY=true") - + logger.info(f"Using Azure AI Foundry mode with project: {project_endpoint}") - + # Create the AIProjectClient for project-specific operations (e.g., image generation) project_client = AIProjectClient( endpoint=project_endpoint, credential=self._credential, ) - + # Store the project client for image generation self._project_client = project_client - + # For chat completions, use the direct Azure OpenAI endpoint # The Foundry project uses Azure OpenAI under the hood, and we need the direct endpoint # to properly authenticate with Cognitive Services token azure_endpoint = app_settings.azure_openai.endpoint if not azure_endpoint: raise ValueError("AZURE_OPENAI_ENDPOINT is required for Foundry mode chat completions") - + def get_token() -> str: """Token provider callable - invoked for each request to ensure fresh tokens.""" token = self._credential.get_token(TOKEN_ENDPOINT) return token.token - + model_deployment = app_settings.ai_foundry.model_deployment or app_settings.azure_openai.gpt_model api_version = app_settings.azure_openai.api_version - + logger.info(f"Foundry mode using Azure OpenAI endpoint: {azure_endpoint}, deployment: {model_deployment}") self._chat_client = AzureOpenAIChatClient( endpoint=azure_endpoint, @@ -563,12 +563,12 @@ def get_token() -> str: endpoint = app_settings.azure_openai.endpoint if not endpoint: raise ValueError("AZURE_OPENAI_ENDPOINT is not configured") - + def get_token() -> str: """Token provider callable - invoked for each request to ensure fresh tokens.""" token = self._credential.get_token(TOKEN_ENDPOINT) return token.token - + logger.info("Using Azure OpenAI Direct mode with ad_token_provider") self._chat_client = AzureOpenAIChatClient( endpoint=endpoint, @@ -577,47 +577,47 @@ def get_token() -> str: ad_token_provider=get_token, ) return self._chat_client - + def initialize(self) -> None: """Initialize all agents and build the handoff workflow.""" if self._initialized: return - + mode_str = "Azure AI Foundry" if self._use_foundry else "Azure OpenAI Direct" logger.info(f"Initializing Content Generation Orchestrator ({mode_str} mode)...") - + # Get the chat client chat_client = self._get_chat_client() - + # Agent names - use underscores (AzureOpenAIChatClient works with both modes now) name_sep = "_" - + # Create all agents triage_agent = chat_client.create_agent( name=f"triage{name_sep}agent", instructions=TRIAGE_INSTRUCTIONS, ) - + planning_agent = chat_client.create_agent( name=f"planning{name_sep}agent", instructions=PLANNING_INSTRUCTIONS, ) - + research_agent = chat_client.create_agent( name=f"research{name_sep}agent", instructions=RESEARCH_INSTRUCTIONS, ) - + text_content_agent = chat_client.create_agent( name=f"text{name_sep}content{name_sep}agent", instructions=TEXT_CONTENT_INSTRUCTIONS, ) - + image_content_agent = chat_client.create_agent( name=f"image{name_sep}content{name_sep}agent", instructions=IMAGE_CONTENT_INSTRUCTIONS, ) - + compliance_agent = chat_client.create_agent( name=f"compliance{name_sep}agent", instructions=COMPLIANCE_INSTRUCTIONS, @@ -638,7 +638,7 @@ def initialize(self) -> None: # Workflow name - Foundry requires hyphens workflow_name = f"content{name_sep}generation{name_sep}workflow" - + # Build the handoff workflow # Triage can route to all specialists # Specialists hand back to triage after completing their task @@ -658,10 +658,10 @@ def initialize(self) -> None: .with_start_agent(triage_agent) # Triage can hand off to all specialists .add_handoff(triage_agent, [ - planning_agent, - research_agent, - text_content_agent, - image_content_agent, + planning_agent, + research_agent, + text_content_agent, + image_content_agent, compliance_agent ]) # All specialists can hand back to triage @@ -678,10 +678,10 @@ def initialize(self) -> None: ) .build() ) - + self._initialized = True logger.info(f"Content Generation Orchestrator initialized successfully ({mode_str} mode)") - + async def process_message( self, message: str, @@ -690,23 +690,23 @@ async def process_message( ) -> AsyncIterator[dict]: """ Process a user message through the orchestrated workflow. - + Uses the Agent Framework's HandoffBuilder workflow to coordinate between specialized agents. - + Args: message: The user's input message conversation_id: Unique identifier for the conversation context: Optional context (previous messages, user preferences) - + Yields: dict: Response chunks with agent responses and status updates """ if not self._initialized: self.initialize() - + logger.info(f"Processing message for conversation {conversation_id}") - + # PROACTIVE CONTENT SAFETY CHECK - Block harmful content at input layer # This is the first line of defense, before any agent processes the request is_harmful, matched_pattern = _check_input_for_harmful_content(message) @@ -723,18 +723,18 @@ async def process_message( "metadata": {"conversation_id": conversation_id} } return # Exit immediately - do not process through agents - + # Prepare the input with context full_input = message if context: full_input = f"Context:\n{json.dumps(context, indent=2)}\n\nUser Message:\n{message}" - + try: # Collect events from the workflow stream events = [] async for event in self._workflow.run_stream(full_input): events.append(event) - + # Handle different event types from the workflow if isinstance(event, WorkflowStatusEvent): yield { @@ -743,7 +743,7 @@ async def process_message( "is_final": False, "metadata": {"conversation_id": conversation_id} } - + elif isinstance(event, RequestInfoEvent): # Workflow is requesting user input if isinstance(event.data, HandoffAgentUserRequest): @@ -751,17 +751,17 @@ async def process_message( messages = event.data.agent_response.messages if hasattr(event.data, 'agent_response') and event.data.agent_response else [] if not isinstance(messages, list): messages = [messages] if messages else [] - + conversation_text = "\n".join([ f"{msg.author_name or msg.role.value}: {msg.text}" for msg in messages ]) - + # Get the last message content and filter any system prompt leakage last_msg_content = messages[-1].text if messages else (event.data.agent_response.text if hasattr(event.data, 'agent_response') and event.data.agent_response else "") last_msg_content = _filter_system_prompt_from_response(last_msg_content) last_msg_agent = messages[-1].author_name if messages and hasattr(messages[-1], 'author_name') else "unknown" - + yield { "type": "agent_response", "agent": last_msg_agent, @@ -772,14 +772,14 @@ async def process_message( "request_id": event.request_id, "metadata": {"conversation_id": conversation_id} } - + elif isinstance(event, WorkflowOutputEvent): # Final output from the workflow conversation = cast(list[ChatMessage], event.data) if isinstance(conversation, list) and conversation: # Get the last assistant message as the final response assistant_messages = [ - msg for msg in conversation + msg for msg in conversation if msg.role.value != "user" ] if assistant_messages: @@ -793,7 +793,7 @@ async def process_message( "is_final": True, "metadata": {"conversation_id": conversation_id} } - + except Exception as e: logger.exception(f"Error processing message: {e}") yield { @@ -802,7 +802,7 @@ async def process_message( "is_final": True, "metadata": {"conversation_id": conversation_id} } - + async def send_user_response( self, request_id: str, @@ -811,18 +811,18 @@ async def send_user_response( ) -> AsyncIterator[dict]: """ Send a user response to a pending workflow request. - + Args: request_id: The ID of the pending request user_response: The user's response conversation_id: Unique identifier for the conversation - + Yields: dict: Response chunks from continuing the workflow """ if not self._initialized: self.initialize() - + # PROACTIVE CONTENT SAFETY CHECK - Block harmful content in follow-up messages too is_harmful, matched_pattern = _check_input_for_harmful_content(user_response) if is_harmful: @@ -837,7 +837,7 @@ async def send_user_response( "metadata": {"conversation_id": conversation_id} } return # Exit immediately - do not continue workflow - + try: responses = {request_id: user_response} async for event in self._workflow.send_responses_streaming(responses): @@ -848,19 +848,19 @@ async def send_user_response( "is_final": False, "metadata": {"conversation_id": conversation_id} } - + elif isinstance(event, RequestInfoEvent): if isinstance(event.data, HandoffAgentUserRequest): # Get messages from agent_response (updated API) messages = event.data.agent_response.messages if hasattr(event.data, 'agent_response') and event.data.agent_response else [] if not isinstance(messages, list): messages = [messages] if messages else [] - + # Get the last message content and filter any system prompt leakage last_msg_content = messages[-1].text if messages else (event.data.agent_response.text if hasattr(event.data, 'agent_response') and event.data.agent_response else "") last_msg_content = _filter_system_prompt_from_response(last_msg_content) last_msg_agent = messages[-1].author_name if messages and hasattr(messages[-1], 'author_name') else "unknown" - + yield { "type": "agent_response", "agent": last_msg_agent, @@ -870,12 +870,12 @@ async def send_user_response( "request_id": event.request_id, "metadata": {"conversation_id": conversation_id} } - + elif isinstance(event, WorkflowOutputEvent): conversation = cast(list[ChatMessage], event.data) if isinstance(conversation, list) and conversation: assistant_messages = [ - msg for msg in conversation + msg for msg in conversation if msg.role.value != "user" ] if assistant_messages: @@ -889,7 +889,7 @@ async def send_user_response( "is_final": True, "metadata": {"conversation_id": conversation_id} } - + except Exception as e: logger.exception(f"Error sending user response: {e}") yield { @@ -898,7 +898,7 @@ async def send_user_response( "is_final": True, "metadata": {"conversation_id": conversation_id} } - + async def parse_brief( self, brief_text: str @@ -906,10 +906,10 @@ async def parse_brief( """ Parse a free-text creative brief into structured format. If critical information is missing, return clarifying questions. - + Args: brief_text: Free-text creative brief from user - + Returns: tuple: (CreativeBrief, clarifying_questions_or_none, is_blocked) - If all critical fields are provided: (brief, None, False) @@ -918,7 +918,7 @@ async def parse_brief( """ if not self._initialized: self.initialize() - + # PROACTIVE CONTENT SAFETY CHECK - Block harmful content at input layer is_harmful, matched_pattern = _check_input_for_harmful_content(brief_text) if is_harmful: @@ -936,13 +936,13 @@ async def parse_brief( cta="" ) return empty_brief, RAI_HARMFUL_CONTENT_RESPONSE, True - + # SECONDARY RAI CHECK - Use LLM-based classifier for comprehensive safety/scope validation try: rai_response = await self._rai_agent.run(brief_text) rai_result = str(rai_response).strip().upper() logger.info(f"RAI agent response for parse_brief: {rai_result}") - + if rai_result == "TRUE": logger.warning(f"RAI agent blocked content in parse_brief: {brief_text[:100]}...") empty_brief = CreativeBrief( @@ -961,7 +961,7 @@ async def parse_brief( # Log the error but continue - don't block legitimate requests due to RAI agent failures logger.warning(f"RAI agent check failed in parse_brief, continuing: {rai_error}") planning_agent = self._agents["planning"] - + # First, analyze the brief and check for missing critical fields analysis_prompt = f""" Analyze this creative brief request and determine if all critical information is provided. @@ -1006,10 +1006,10 @@ async def parse_brief( - Do NOT invent or assume information that wasn't explicitly stated - Make clarifying questions specific to the user's context (reference their product/campaign) """ - + # Use the agent's run method response = await planning_agent.run(analysis_prompt) - + # Parse the analysis response try: response_text = str(response) @@ -1021,10 +1021,10 @@ async def parse_brief( json_start = response_text.index("```") + 3 json_end = response_text.index("```", json_start) response_text = response_text[json_start:json_end].strip() - + analysis = json.loads(response_text) brief_data = analysis.get("extracted_fields", {}) - + # Ensure all fields are strings for key in brief_data: if isinstance(brief_data[key], dict): @@ -1035,26 +1035,26 @@ async def parse_brief( brief_data[key] = "" elif not isinstance(brief_data[key], str): brief_data[key] = str(brief_data[key]) - + # Ensure all required fields exist - for field in ['overview', 'objectives', 'target_audience', 'key_message', + for field in ['overview', 'objectives', 'target_audience', 'key_message', 'tone_and_style', 'deliverable', 'timelines', 'visual_guidelines', 'cta']: if field not in brief_data: brief_data[field] = "" - + brief = CreativeBrief(**brief_data) - + # Check if we need clarifying questions if analysis.get("status") == "incomplete" and analysis.get("clarifying_message"): return (brief, analysis["clarifying_message"], False) - + return (brief, None, False) - + except Exception as e: logger.error(f"Failed to parse brief analysis response: {e}") # Fallback to basic extraction return (self._extract_brief_from_text(brief_text), None, False) - + def _extract_brief_from_text(self, text: str) -> CreativeBrief: """Extract brief fields from labeled text like 'Overview: ...'""" fields = { @@ -1068,7 +1068,7 @@ def _extract_brief_from_text(self, text: str) -> CreativeBrief: 'visual_guidelines': '', 'cta': '' } - + # Common label variations label_map = { 'overview': ['overview'], @@ -1081,15 +1081,15 @@ def _extract_brief_from_text(self, text: str) -> CreativeBrief: 'visual_guidelines': ['visual guidelines', 'visual_guidelines', 'visuals'], 'cta': ['call to action', 'cta', 'call-to-action'] } - + lines = text.strip().split('\n') current_field = None - + for line in lines: line = line.strip() if not line: continue - + # Check if line starts with a label found_label = False for field, labels in label_map.items(): @@ -1103,17 +1103,17 @@ def _extract_brief_from_text(self, text: str) -> CreativeBrief: break if found_label: break - + # If no label found and we have a current field, append to it if not found_label and current_field: fields[current_field] += ' ' + line - + # If no fields were extracted, put everything in overview if not any(fields.values()): fields['overview'] = text - + return CreativeBrief(**fields) - + async def select_products( self, request_text: str, @@ -1122,20 +1122,20 @@ async def select_products( ) -> dict: """ Select or modify product selection via natural language. - + Args: request_text: User's natural language request for product selection current_products: Currently selected products (for modifications) available_products: List of available products to choose from - + Returns: dict: Selected products and assistant message """ if not self._initialized: self.initialize() - + research_agent = self._agents["research"] - + select_prompt = f""" You are helping a user select products for a marketing campaign. @@ -1167,11 +1167,11 @@ async def select_products( - For "search" action: include products matching the search criteria - Return complete product objects from the available catalog, not just names """ - + try: response = await research_agent.run(select_prompt) response_text = str(response) - + # Extract JSON from response if "```json" in response_text: json_start = response_text.index("```json") + 7 @@ -1181,7 +1181,7 @@ async def select_products( json_start = response_text.index("```") + 3 json_end = response_text.index("```", json_start) response_text = response_text[json_start:json_end].strip() - + result = json.loads(response_text) return { "products": result.get("selected_products", []), @@ -1199,11 +1199,11 @@ async def select_products( async def _generate_foundry_image(self, image_prompt: str, results: dict) -> None: """Generate image using direct REST API call to Azure OpenAI endpoint. - + Azure AI Foundry's agent-based image generation (Responses API) returns text descriptions instead of actual image data. This method uses a direct REST API call to the images/generations endpoint instead. - + Args: image_prompt: The prompt for image generation results: The results dict to update with image data @@ -1214,47 +1214,47 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non logger.error("httpx package not installed - required for Foundry image generation") results["image_error"] = "httpx package required for Foundry image generation" return - + try: if not self._credential: logger.error("Azure credential not available") results["image_error"] = "Azure credential not configured" return - + # Get token for Azure Cognitive Services token = self._credential.get_token(TOKEN_ENDPOINT) - + # Use the direct Azure OpenAI endpoint for image generation # This is different from the project endpoint - it goes directly to Azure OpenAI image_endpoint = app_settings.azure_openai.image_endpoint if not image_endpoint: # Fallback: try to derive from regular OpenAI endpoint image_endpoint = app_settings.azure_openai.endpoint - + if not image_endpoint: logger.error("No Azure OpenAI image endpoint configured") results["image_error"] = "Image endpoint not configured" return - + # Ensure endpoint doesn't end with / image_endpoint = image_endpoint.rstrip('/') - + image_deployment = app_settings.ai_foundry.image_deployment if not image_deployment: image_deployment = app_settings.azure_openai.image_model - + # The direct image API endpoint image_api_url = f"{image_endpoint}/openai/deployments/{image_deployment}/images/generations" api_version = app_settings.azure_openai.image_api_version or "2025-04-01-preview" - + logger.info(f"Calling Foundry direct image API: {image_api_url}") logger.info(f"Prompt: {image_prompt[:200]}...") - + headers = { "Authorization": f"Bearer {token.token}", "Content-Type": "application/json", } - + # gpt-image-1 parameters (no response_format parameter) payload = { "prompt": image_prompt, @@ -1262,34 +1262,34 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non "size": "1024x1024", "quality": "medium", # gpt-image-1 uses low/medium/high/auto } - + async with httpx.AsyncClient(timeout=120.0) as client: response = await client.post( f"{image_api_url}?api-version={api_version}", headers=headers, json=payload, ) - + if response.status_code != 200: error_text = response.text logger.error(f"Foundry image API error {response.status_code}: {error_text[:500]}") results["image_error"] = f"API error {response.status_code}: {error_text[:200]}" return - + response_data = response.json() - + # Extract image data from response data = response_data.get("data", []) if not data: logger.error("No image data in Foundry API response") results["image_error"] = "No image data in API response" return - + image_item = data[0] - + # Try to get base64 data (check both 'b64_json' and 'b64' fields) image_base64 = image_item.get("b64_json") or image_item.get("b64") - + if not image_base64: # If URL is provided instead, fetch the image image_url = image_item.get("url") @@ -1306,15 +1306,15 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non logger.error(f"No base64 or URL in response. Keys: {list(image_item.keys())}") results["image_error"] = f"No image data in response. Keys: {list(image_item.keys())}" return - + # Store revised prompt if available revised_prompt = image_item.get("revised_prompt") if revised_prompt: results["image_revised_prompt"] = revised_prompt logger.info(f"Revised prompt: {revised_prompt[:100]}...") - + logger.info(f"Received image data ({len(image_base64)} chars)") - + # Validate base64 data try: decoded = base64.b64decode(image_base64) @@ -1323,10 +1323,10 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non logger.error(f"Failed to decode image data: {e}") results["image_error"] = f"Failed to decode image: {e}" return - + # Save to blob storage await self._save_image_to_blob(image_base64, results) - + except httpx.TimeoutException: logger.error("Foundry image generation request timed out") results["image_error"] = "Image generation timed out after 120 seconds" @@ -1336,7 +1336,7 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non async def _save_image_to_blob(self, image_base64: str, results: dict) -> None: """Save generated image to blob storage. - + Args: image_base64: Base64-encoded image data results: The results dict to update with blob URL or base64 fallback @@ -1344,16 +1344,16 @@ async def _save_image_to_blob(self, image_base64: str, results: dict) -> None: try: from services.blob_service import BlobStorageService from datetime import datetime - + blob_service = BlobStorageService() gen_id = datetime.utcnow().strftime("%Y%m%d%H%M%S") logger.info(f"Saving image to blob storage (size: {len(image_base64)} bytes)...") - + blob_url = await blob_service.save_generated_image( conversation_id=f"gen_{gen_id}", image_base64=image_base64 ) - + if blob_url: results["image_blob_url"] = blob_url logger.info(f"Image saved to blob: {blob_url}") @@ -1372,18 +1372,18 @@ async def generate_content( ) -> dict: """ Generate complete content package from a confirmed creative brief. - + Args: brief: Confirmed creative brief products: List of products to feature generate_images: Whether to generate images - + Returns: dict: Generated content with compliance results """ if not self._initialized: self.initialize() - + results = { "text_content": None, "image_prompt": None, @@ -1391,7 +1391,7 @@ async def generate_content( "violations": [], "requires_modification": False } - + # Build the generation request for text content text_request = f""" Generate marketing content based on this creative brief: @@ -1406,12 +1406,12 @@ async def generate_content( Products to feature: {json.dumps(products or [])} """ - + try: # Generate text content text_response = await self._agents["text_content"].run(text_request) results["text_content"] = str(text_response) - + # Generate image prompt if requested if generate_images: # Build product context for image generation @@ -1426,16 +1426,16 @@ async def generate_content( desc = p.get('description', p.get('marketing_description', '')) tags = p.get('tags', '') product_details.append(f"- {name}: {desc} (Tags: {tags})") - + # Include detailed image description if available img_desc = p.get('image_description') if img_desc: image_descriptions.append(f"### {name} - Detailed Visual Description:\n{img_desc}") - + product_context = "\n".join(product_details) if image_descriptions: detailed_image_context = "\n\n".join(image_descriptions) - + image_request = f""" Create an image generation prompt for this marketing campaign: @@ -1455,34 +1455,34 @@ async def generate_content( For paint products, show the paint colors in context (on walls, swatches, or room settings). Use the detailed visual descriptions above to ensure accurate color reproduction in the generated image. """ - + # In Foundry mode, build the image prompt directly and use direct API # In Direct mode, use the image agent to create the prompt if self._use_foundry: # Build a direct image prompt for Foundry image_prompt_parts = ["Generate a professional marketing image:"] - + if brief.visual_guidelines: image_prompt_parts.append(f"Visual style: {brief.visual_guidelines}") - + if brief.tone_and_style: image_prompt_parts.append(f"Mood and tone: {brief.tone_and_style}") - + if product_context: image_prompt_parts.append(f"Products to feature: {product_context}") - + if detailed_image_context: image_prompt_parts.append(f"Product details: {detailed_image_context[:500]}") - + if brief.key_message: image_prompt_parts.append(f"Key message to convey: {brief.key_message}") - + image_prompt_parts.append("Style: High-quality, photorealistic marketing photography with professional lighting.") - + image_prompt = " ".join(image_prompt_parts) results["image_prompt"] = image_prompt logger.info(f"Created Foundry image prompt: {image_prompt[:200]}...") - + # Generate image using direct Foundry API logger.info("Generating image via Foundry direct API...") await self._generate_foundry_image(image_prompt, results) @@ -1490,14 +1490,14 @@ async def generate_content( # Direct mode: use image agent to create prompt, then generate via DALL-E image_response = await self._agents["image_content"].run(image_request) results["image_prompt"] = str(image_response) - + # Extract clean prompt from the response and generate actual image try: from agents.image_content_agent import generate_dalle_image - + # Try to extract a clean prompt from the agent response prompt_text = str(image_response) - + # If response is JSON, extract the prompt field if '{' in prompt_text: try: @@ -1511,13 +1511,13 @@ async def generate_content( try: prompt_data = json.loads(json_match.group(1)) prompt_text = prompt_data.get('prompt', prompt_data.get('image_prompt', prompt_text)) - except: + except Exception: pass - + # Build product description for DALL-E context # Include detailed image descriptions if available for better color accuracy product_description = detailed_image_context if detailed_image_context else product_context - + # Generate the actual image using DALL-E logger.info(f"Generating DALL-E image with prompt: {prompt_text[:200]}...") image_result = await generate_dalle_image( @@ -1525,22 +1525,22 @@ async def generate_content( product_description=product_description, scene_description=brief.visual_guidelines ) - + if image_result.get("success"): image_base64 = image_result.get("image_base64") results["image_revised_prompt"] = image_result.get("revised_prompt") logger.info("DALL-E image generated successfully") - + # Save to blob storage await self._save_image_to_blob(image_base64, results) else: logger.warning(f"DALL-E image generation failed: {image_result.get('error')}") results["image_error"] = image_result.get("error") - + except Exception as img_error: logger.exception(f"Error generating DALL-E image: {img_error}") results["image_error"] = str(img_error) - + # Run compliance check compliance_request = f""" Review this marketing content for compliance: @@ -1555,7 +1555,7 @@ async def generate_content( """ compliance_response = await self._agents["compliance"].run(compliance_request) results["compliance"] = str(compliance_response) - + # Try to parse compliance violations try: compliance_data = json.loads(str(compliance_response)) @@ -1577,17 +1577,17 @@ async def generate_content( ) except (json.JSONDecodeError, KeyError): pass - + except Exception as e: logger.exception(f"Error generating content: {e}") results["error"] = str(e) - + # Log results summary before returning logger.info(f"Orchestrator returning results with keys: {list(results.keys())}") has_image = bool(results.get("image_base64")) image_size = len(results.get("image_base64", "")) if has_image else 0 logger.info(f"Orchestrator results: has_image={has_image}, image_size={image_size}, has_error={bool(results.get('error'))}") - + return results async def regenerate_image( @@ -1599,24 +1599,24 @@ async def regenerate_image( ) -> dict: """ Regenerate just the image based on a user modification request. - + This method is called when the user wants to modify the generated image after initial content generation (e.g., "show a kitchen instead of dining room"). - + Args: modification_request: User's request for how to modify the image brief: The confirmed creative brief products: List of products to feature previous_image_prompt: The previous image prompt (if available) - + Returns: dict: Regenerated image with updated prompt """ if not self._initialized: self.initialize() - + logger.info(f"Regenerating image with modification: {modification_request[:100]}...") - + # PROACTIVE CONTENT SAFETY CHECK is_harmful, matched_pattern = _check_input_for_harmful_content(modification_request) if is_harmful: @@ -1626,7 +1626,7 @@ async def regenerate_image( "rai_blocked": True, "blocked_reason": "harmful_content_detected" } - + results = { "image_prompt": None, "image_base64": None, @@ -1634,7 +1634,7 @@ async def regenerate_image( "image_revised_prompt": None, "message": None } - + # Build product context product_context = "" detailed_image_context = "" @@ -1646,24 +1646,24 @@ async def regenerate_image( desc = p.get('description', p.get('marketing_description', '')) tags = p.get('tags', '') product_details.append(f"- {name}: {desc} (Tags: {tags})") - + img_desc = p.get('image_description') if img_desc: image_descriptions.append(f"### {name} - Detailed Visual Description:\n{img_desc}") - + product_context = "\n".join(product_details) if image_descriptions: detailed_image_context = "\n\n".join(image_descriptions) - + # Prepare optional sections for the prompt detailed_product_section = "" if detailed_image_context: detailed_product_section = f"DETAILED PRODUCT DESCRIPTIONS:\n{detailed_image_context}" - + previous_prompt_section = "" if previous_image_prompt: previous_prompt_section = f"PREVIOUS IMAGE PROMPT:\n{previous_image_prompt}" - + try: # Use the image content agent to create a modified prompt modification_prompt = f""" @@ -1694,41 +1694,41 @@ async def regenerate_image( - "style": Visual style description - "change_summary": Brief summary of what was changed """ - + if self._use_foundry: # Foundry mode: build prompt directly and call image API # Combine original brief context with modification new_prompt_parts = ["Generate a professional marketing image:"] - + # Apply the modification to visual guidelines if brief.visual_guidelines: new_prompt_parts.append(f"Visual style: {brief.visual_guidelines}") - + if brief.tone_and_style: new_prompt_parts.append(f"Mood and tone: {brief.tone_and_style}") - + if product_context: new_prompt_parts.append(f"Products to feature: {product_context}") - + # The key modification - incorporate user's change new_prompt_parts.append(f"IMPORTANT MODIFICATION: {modification_request}") - + if brief.key_message: new_prompt_parts.append(f"Key message to convey: {brief.key_message}") - + new_prompt_parts.append("Style: High-quality, photorealistic marketing photography with professional lighting.") - + image_prompt = " ".join(new_prompt_parts) results["image_prompt"] = image_prompt results["message"] = f"Regenerating image with your requested changes: {modification_request}" - + logger.info(f"Created modified Foundry image prompt: {image_prompt[:200]}...") await self._generate_foundry_image(image_prompt, results) else: # Direct mode: use image agent to interpret the modification image_response = await self._agents["image_content"].run(modification_prompt) prompt_text = str(image_response) - + # Extract the prompt from JSON response change_summary = modification_request if '{' in prompt_text: @@ -1744,25 +1744,25 @@ async def regenerate_image( prompt_data = json.loads(json_match.group(1)) prompt_text = prompt_data.get('prompt', prompt_text) change_summary = prompt_data.get('change_summary', modification_request) - except: + except Exception: pass - + results["image_prompt"] = prompt_text results["message"] = f"Regenerating image: {change_summary}" - + # Generate the actual image try: from agents.image_content_agent import generate_dalle_image - + product_description = detailed_image_context if detailed_image_context else product_context - + logger.info(f"Generating modified DALL-E image: {prompt_text[:200]}...") image_result = await generate_dalle_image( prompt=prompt_text, product_description=product_description, scene_description=brief.visual_guidelines ) - + if image_result.get("success"): image_base64 = image_result.get("image_base64") results["image_revised_prompt"] = image_result.get("revised_prompt") @@ -1771,17 +1771,17 @@ async def regenerate_image( else: logger.warning(f"Modified DALL-E image generation failed: {image_result.get('error')}") results["image_error"] = image_result.get("error") - + except Exception as img_error: logger.exception(f"Error generating modified DALL-E image: {img_error}") results["image_error"] = str(img_error) - + logger.info(f"Image regeneration complete. Has image: {bool(results.get('image_base64') or results.get('image_blob_url'))}") - + except Exception as e: logger.exception(f"Error regenerating image: {e}") results["error"] = str(e) - + return results diff --git a/content-gen/src/backend/services/blob_service.py b/content-gen/src/backend/services/blob_service.py index b175e90a7..ae91c53e9 100644 --- a/content-gen/src/backend/services/blob_service.py +++ b/content-gen/src/backend/services/blob_service.py @@ -23,49 +23,49 @@ class BlobStorageService: """Service for interacting with Azure Blob Storage.""" - + def __init__(self): self._client: Optional[BlobServiceClient] = None self._product_images_container: Optional[ContainerClient] = None self._generated_images_container: Optional[ContainerClient] = None - + async def _get_credential(self): """Get Azure credential for authentication.""" client_id = app_settings.base_settings.azure_client_id if client_id: return ManagedIdentityCredential(client_id=client_id) return DefaultAzureCredential() - + async def initialize(self) -> None: """Initialize Blob Storage client and containers.""" if self._client: return - + credential = await self._get_credential() - + self._client = BlobServiceClient( account_url=f"https://{app_settings.blob.account_name}.blob.core.windows.net", credential=credential ) - + self._product_images_container = self._client.get_container_client( app_settings.blob.product_images_container ) - + self._generated_images_container = self._client.get_container_client( app_settings.blob.generated_images_container ) - + logger.info("Blob Storage service initialized") - + async def close(self) -> None: """Close the Blob Storage client.""" if self._client: await self._client.close() self._client = None - + # ==================== Product Image Operations ==================== - + async def upload_product_image( self, sku: str, @@ -74,22 +74,22 @@ async def upload_product_image( ) -> Tuple[str, str]: """ Upload a product image and generate its description. - + Args: sku: Product SKU (used as blob name prefix) image_data: Raw image bytes content_type: MIME type of the image - + Returns: Tuple of (blob_url, generated_description) """ await self.initialize() - + # Generate unique blob name timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") extension = content_type.split("/")[-1] blob_name = f"{sku}/{timestamp}.{extension}" - + # Upload the image blob_client = self._product_images_container.get_blob_client(blob_name) await blob_client.upload_blob( @@ -97,45 +97,45 @@ async def upload_product_image( content_type=content_type, overwrite=True ) - + blob_url = blob_client.url - + # Generate description using GPT-5 Vision description = await self.generate_image_description(image_data) - + logger.info(f"Uploaded product image: {blob_name}") return blob_url, description - + async def get_product_image_url(self, sku: str) -> Optional[str]: """ Get the URL of the latest product image. - + Args: sku: Product SKU - + Returns: URL of the latest image, or None if not found """ await self.initialize() - + # List blobs with the SKU prefix blobs = [] async for blob in self._product_images_container.list_blobs( name_starts_with=f"{sku}/" ): blobs.append(blob) - + if not blobs: return None - + # Get the most recent blob latest_blob = sorted(blobs, key=lambda b: b.name, reverse=True)[0] blob_client = self._product_images_container.get_blob_client(latest_blob.name) - + return blob_client.url - + # ==================== Generated Image Operations ==================== - + async def save_generated_image( self, conversation_id: str, @@ -144,25 +144,25 @@ async def save_generated_image( ) -> str: """ Save a DALL-E generated image to blob storage. - + Args: conversation_id: ID of the conversation that generated the image image_base64: Base64-encoded image data content_type: MIME type of the image - + Returns: URL of the saved image """ await self.initialize() - + # Decode base64 image image_data = base64.b64decode(image_base64) - + # Generate unique blob name timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") extension = content_type.split("/")[-1] blob_name = f"{conversation_id}/{timestamp}.{extension}" - + # Upload the image blob_client = self._generated_images_container.get_blob_client(blob_name) await blob_client.upload_blob( @@ -170,63 +170,63 @@ async def save_generated_image( content_type=content_type, overwrite=True ) - + logger.info(f"Saved generated image: {blob_name}") return blob_client.url - + async def get_generated_images( self, conversation_id: str ) -> list[str]: """ Get all generated images for a conversation. - + Args: conversation_id: ID of the conversation - + Returns: List of image URLs """ await self.initialize() - + urls = [] async for blob in self._generated_images_container.list_blobs( name_starts_with=f"{conversation_id}/" ): blob_client = self._generated_images_container.get_blob_client(blob.name) urls.append(blob_client.url) - + return urls - + # ==================== Image Description Generation ==================== - + async def generate_image_description(self, image_data: bytes) -> str: """ Generate a detailed text description of an image using GPT-5 Vision. - + This is used to create descriptions of product images that can be used as context for DALL-E 3 image generation (since DALL-E 3 cannot accept image inputs directly). - + Args: image_data: Raw image bytes - + Returns: Detailed text description of the image """ # Encode image to base64 image_base64 = base64.b64encode(image_data).decode("utf-8") - + try: credential = await self._get_credential() token = await credential.get_token("https://cognitiveservices.azure.com/.default") - + client = AsyncAzureOpenAI( azure_endpoint=app_settings.azure_openai.endpoint, azure_ad_token=token.token, api_version=app_settings.azure_openai.api_version, ) - + response = await client.chat.completions.create( model=app_settings.azure_openai.gpt_model, messages=[ @@ -260,11 +260,11 @@ async def generate_image_description(self, image_data: bytes) -> str: ], max_tokens=500 ) - + description = response.choices[0].message.content logger.info(f"Generated image description: {description[:100]}...") return description - + except Exception as e: logger.exception(f"Error generating image description: {e}") return "Product image - description unavailable" diff --git a/content-gen/src/backend/services/cosmos_service.py b/content-gen/src/backend/services/cosmos_service.py index 11f55400d..6e452db2d 100644 --- a/content-gen/src/backend/services/cosmos_service.py +++ b/content-gen/src/backend/services/cosmos_service.py @@ -22,79 +22,79 @@ class CosmosDBService: """Service for interacting with Azure Cosmos DB.""" - + def __init__(self): self._client: Optional[CosmosClient] = None self._products_container: Optional[ContainerProxy] = None self._conversations_container: Optional[ContainerProxy] = None - + async def _get_credential(self): """Get Azure credential for authentication.""" client_id = app_settings.base_settings.azure_client_id if client_id: return ManagedIdentityCredential(client_id=client_id) return DefaultAzureCredential() - + async def initialize(self) -> None: """Initialize CosmosDB client and containers.""" if self._client: return - + credential = await self._get_credential() - + self._client = CosmosClient( url=app_settings.cosmos.endpoint, credential=credential ) - + database = self._client.get_database_client( app_settings.cosmos.database_name ) - + self._products_container = database.get_container_client( app_settings.cosmos.products_container ) - + self._conversations_container = database.get_container_client( app_settings.cosmos.conversations_container ) - + logger.info("CosmosDB service initialized") - + async def close(self) -> None: """Close the CosmosDB client.""" if self._client: await self._client.close() self._client = None - + # ==================== Product Operations ==================== - + async def get_product_by_sku(self, sku: str) -> Optional[Product]: """ Retrieve a product by its SKU. - + Args: sku: Product SKU identifier - + Returns: Product if found, None otherwise """ await self.initialize() - + query = "SELECT * FROM c WHERE c.sku = @sku" params = [{"name": "@sku", "value": sku}] - + items = [] async for item in self._products_container.query_items( query=query, parameters=params ): items.append(item) - + if items: return Product(**items[0]) return None - + async def get_products_by_category( self, category: str, @@ -103,20 +103,20 @@ async def get_products_by_category( ) -> List[Product]: """ Retrieve products by category. - + Args: category: Product category sub_category: Optional sub-category filter limit: Maximum number of products to return - + Returns: List of matching products """ await self.initialize() - + if sub_category: query = """ - SELECT TOP @limit * FROM c + SELECT TOP @limit * FROM c WHERE c.category = @category AND c.sub_category = @sub_category """ params = [ @@ -130,16 +130,16 @@ async def get_products_by_category( {"name": "@category", "value": category}, {"name": "@limit", "value": limit} ] - + products = [] async for item in self._products_container.query_items( query=query, parameters=params ): products.append(Product(**item)) - + return products - + async def search_products( self, search_term: str, @@ -147,20 +147,20 @@ async def search_products( ) -> List[Product]: """ Search products by name or description. - + Args: search_term: Text to search for limit: Maximum number of products to return - + Returns: List of matching products """ await self.initialize() - + search_lower = search_term.lower() query = """ - SELECT TOP @limit * FROM c - WHERE CONTAINS(LOWER(c.product_name), @search) + SELECT TOP @limit * FROM c + WHERE CONTAINS(LOWER(c.product_name), @search) OR CONTAINS(LOWER(c.marketing_description), @search) OR CONTAINS(LOWER(c.detailed_spec_description), @search) """ @@ -168,47 +168,47 @@ async def search_products( {"name": "@search", "value": search_lower}, {"name": "@limit", "value": limit} ] - + products = [] async for item in self._products_container.query_items( query=query, parameters=params ): products.append(Product(**item)) - + return products - + async def upsert_product(self, product: Product) -> Product: """ Create or update a product. - + Args: product: Product to upsert - + Returns: The upserted product """ await self.initialize() - + item = product.model_dump() item["id"] = product.sku # Use SKU as document ID item["updated_at"] = datetime.now(timezone.utc).isoformat() - + result = await self._products_container.upsert_item(item) return Product(**result) - + async def delete_product(self, sku: str) -> bool: """ Delete a product by SKU. - + Args: sku: Product SKU (also used as document ID) - + Returns: True if deleted successfully """ await self.initialize() - + try: await self._products_container.delete_item( item=sku, @@ -218,19 +218,19 @@ async def delete_product(self, sku: str) -> bool: except Exception as e: logger.warning(f"Failed to delete product {sku}: {e}") return False - + async def delete_all_products(self) -> int: """ Delete all products from the container. - + Returns: Number of products deleted """ await self.initialize() - + deleted_count = 0 query = "SELECT c.id FROM c" - + async for item in self._products_container.query_items(query=query): try: await self._products_container.delete_item( @@ -240,35 +240,35 @@ async def delete_all_products(self) -> int: deleted_count += 1 except Exception as e: logger.warning(f"Failed to delete product {item['id']}: {e}") - + return deleted_count - + async def get_all_products(self, limit: int = 100) -> List[Product]: """ Retrieve all products. - + Args: limit: Maximum number of products to return - + Returns: List of all products """ await self.initialize() - + query = "SELECT TOP @limit * FROM c" params = [{"name": "@limit", "value": limit}] - + products = [] async for item in self._products_container.query_items( query=query, parameters=params ): products.append(Product(**item)) - + return products - + # ==================== Conversation Operations ==================== - + async def get_conversation( self, conversation_id: str, @@ -276,16 +276,16 @@ async def get_conversation( ) -> Optional[dict]: """ Retrieve a conversation by ID. - + Args: conversation_id: Unique conversation identifier user_id: User ID for partition key (may not match if conversation was created by different user) - + Returns: Conversation data if found """ await self.initialize() - + try: # First try direct read with provided user_id (fast path) item = await self._conversations_container.read_item( @@ -295,13 +295,13 @@ async def get_conversation( return item except Exception: pass - + # Fallback: cross-partition query to find conversation by ID # This handles cases where the conversation was created with a different user_id try: query = "SELECT * FROM c WHERE c.id = @id" params = [{"name": "@id", "value": conversation_id}] - + async for item in self._conversations_container.query_items( query=query, parameters=params, @@ -310,9 +310,9 @@ async def get_conversation( return item except Exception: pass - + return None - + async def save_conversation( self, conversation_id: str, @@ -324,7 +324,7 @@ async def save_conversation( ) -> dict: """ Save or update a conversation. - + Args: conversation_id: Unique conversation identifier user_id: User ID for partition key @@ -332,12 +332,12 @@ async def save_conversation( brief: Associated creative brief metadata: Additional metadata generated_content: Generated marketing content - + Returns: The saved conversation document """ await self.initialize() - + item = { "id": conversation_id, "userId": user_id, # Partition key field (matches container definition /userId) @@ -348,10 +348,10 @@ async def save_conversation( "generated_content": generated_content, "updated_at": datetime.now(timezone.utc).isoformat() } - + result = await self._conversations_container.upsert_item(item) return result - + async def save_generated_content( self, conversation_id: str, @@ -360,19 +360,19 @@ async def save_generated_content( ) -> dict: """ Save generated content to an existing conversation. - + Args: conversation_id: Unique conversation identifier user_id: User ID for partition key generated_content: The generated content to save - + Returns: Updated conversation document """ await self.initialize() - + conversation = await self.get_conversation(conversation_id, user_id) - + if conversation: # Ensure userId is set (for partition key) - migrate old documents if not conversation.get("userId"): @@ -388,10 +388,10 @@ async def save_generated_content( "generated_content": generated_content, "updated_at": datetime.now(timezone.utc).isoformat() } - + result = await self._conversations_container.upsert_item(conversation) return result - + async def add_message_to_conversation( self, conversation_id: str, @@ -400,19 +400,19 @@ async def add_message_to_conversation( ) -> dict: """ Add a message to an existing conversation. - + Args: conversation_id: Unique conversation identifier user_id: User ID for partition key message: Message to add - + Returns: Updated conversation document """ await self.initialize() - + conversation = await self.get_conversation(conversation_id, user_id) - + if conversation: # Ensure userId is set (for partition key) - migrate old documents if not conversation.get("userId"): @@ -427,10 +427,10 @@ async def add_message_to_conversation( "messages": [message], "updated_at": datetime.now(timezone.utc).isoformat() } - + result = await self._conversations_container.upsert_item(conversation) return result - + async def get_user_conversations( self, user_id: str, @@ -438,26 +438,26 @@ async def get_user_conversations( ) -> List[dict]: """ Get all conversations for a user with summary data. - + Args: user_id: User ID ("anonymous" for unauthenticated users) limit: Maximum number of conversations - + Returns: List of conversation summaries """ await self.initialize() - + # For anonymous users, also include conversations with empty/null/undefined user_id # This handles legacy data before "anonymous" was used as the default if user_id == "anonymous": query = """ SELECT TOP @limit c.id, c.userId, c.user_id, c.updated_at, c.messages, c.brief, c.metadata - FROM c + FROM c WHERE c.userId = @user_id - OR c.user_id = @user_id - OR c.user_id = "" - OR c.user_id = null + OR c.user_id = @user_id + OR c.user_id = "" + OR c.user_id = null OR NOT IS_DEFINED(c.user_id) ORDER BY c.updated_at DESC """ @@ -468,7 +468,7 @@ async def get_user_conversations( else: query = """ SELECT TOP @limit c.id, c.userId, c.user_id, c.updated_at, c.messages, c.brief, c.metadata - FROM c + FROM c WHERE c.userId = @user_id OR c.user_id = @user_id ORDER BY c.updated_at DESC """ @@ -476,7 +476,7 @@ async def get_user_conversations( {"name": "@user_id", "value": user_id}, {"name": "@limit", "value": limit} ] - + conversations = [] async for item in self._conversations_container.query_items( query=query, @@ -485,7 +485,7 @@ async def get_user_conversations( messages = item.get("messages", []) brief = item.get("brief", {}) metadata = item.get("metadata", {}) - + custom_title = metadata.get("custom_title") if metadata else None if custom_title: title = custom_title @@ -499,13 +499,13 @@ async def get_user_conversations( break else: title = "Untitled Conversation" - + # Get last message preview last_message = "" if messages: last_msg = messages[-1] last_message = last_msg.get("content", "")[:100] - + conversations.append({ "id": item["id"], "title": title, @@ -513,9 +513,9 @@ async def get_user_conversations( "timestamp": item.get("updated_at", ""), "messageCount": len(messages) }) - + return conversations - + async def delete_conversation( self, conversation_id: str, @@ -523,25 +523,25 @@ async def delete_conversation( ) -> bool: """ Delete a conversation. - + Args: conversation_id: Unique conversation identifier user_id: User ID for partition key - + Returns: True if deleted successfully """ await self.initialize() - + # Get the conversation to find its partition key value conversation = await self.get_conversation(conversation_id, user_id) if not conversation: # Already doesn't exist, consider it deleted return True - + # Use userId (camelCase) as partition key, fallback to user_id for old documents partition_key = conversation.get("userId") or conversation.get("user_id") or user_id - + try: await self._conversations_container.delete_item( item=conversation_id, @@ -552,7 +552,7 @@ async def delete_conversation( except Exception as e: logger.warning(f"Failed to delete conversation {conversation_id}: {e}") raise - + async def rename_conversation( self, conversation_id: str, @@ -561,28 +561,28 @@ async def rename_conversation( ) -> Optional[dict]: """ Rename a conversation by setting a custom title in metadata. - + Args: conversation_id: Unique conversation identifier user_id: User ID for partition key new_title: New title for the conversation - + Returns: Updated conversation document or None if not found """ await self.initialize() - + conversation = await self.get_conversation(conversation_id, user_id) if not conversation: return None - + conversation["metadata"] = conversation.get("metadata", {}) conversation["metadata"]["custom_title"] = new_title # Ensure userId is set (for partition key) - migrate old documents if not conversation.get("userId"): conversation["userId"] = conversation.get("user_id") or user_id # Don't update updated_at - renaming shouldn't change sort order - + result = await self._conversations_container.upsert_item(conversation) return result diff --git a/content-gen/src/backend/services/search_service.py b/content-gen/src/backend/services/search_service.py index cd6729186..16c92f171 100644 --- a/content-gen/src/backend/services/search_service.py +++ b/content-gen/src/backend/services/search_service.py @@ -19,57 +19,57 @@ class SearchService: """Service for searching products and images in Azure AI Search.""" - + def __init__(self): self._products_client: Optional[SearchClient] = None self._images_client: Optional[SearchClient] = None self._credential = None - + def _get_credential(self): """Get search credential - prefer RBAC, fall back to API key.""" if self._credential: return self._credential - + # Try RBAC first try: self._credential = DefaultAzureCredential() return self._credential except Exception: pass - + # Fall back to API key if app_settings.search and app_settings.search.admin_key: self._credential = AzureKeyCredential(app_settings.search.admin_key) return self._credential - + raise ValueError("No valid search credentials available") - + def _get_products_client(self) -> SearchClient: """Get or create the products search client.""" if self._products_client is None: if not app_settings.search or not app_settings.search.endpoint: raise ValueError("Azure AI Search endpoint not configured") - + self._products_client = SearchClient( endpoint=app_settings.search.endpoint, index_name=app_settings.search.products_index, credential=self._get_credential() ) return self._products_client - + def _get_images_client(self) -> SearchClient: """Get or create the images search client.""" if self._images_client is None: if not app_settings.search or not app_settings.search.endpoint: raise ValueError("Azure AI Search endpoint not configured") - + self._images_client = SearchClient( endpoint=app_settings.search.endpoint, index_name=app_settings.search.images_index, credential=self._get_credential() ) return self._images_client - + async def search_products( self, query: str, @@ -79,37 +79,37 @@ async def search_products( ) -> List[Dict[str, Any]]: """ Search for products using Azure AI Search. - + Args: query: Search query text category: Optional category filter sub_category: Optional sub-category filter top: Maximum number of results - + Returns: List of matching products """ try: client = self._get_products_client() - + # Build filter filters = [] if category: filters.append(f"category eq '{category}'") if sub_category: filters.append(f"sub_category eq '{sub_category}'") - + filter_str = " and ".join(filters) if filters else None - + # Execute search results = client.search( search_text=query, filter=filter_str, top=top, select=["id", "product_name", "sku", "model", "category", "sub_category", - "marketing_description", "detailed_spec_description", "image_description"] + "marketing_description", "detailed_spec_description", "image_description"] ) - + products = [] for result in results: products.append({ @@ -124,14 +124,14 @@ async def search_products( "image_description": result.get("image_description"), "search_score": result.get("@search.score") }) - + logger.info(f"Product search for '{query}' returned {len(products)} results") return products - + except Exception as e: logger.error(f"Product search failed: {e}") return [] - + async def search_images( self, query: str, @@ -141,36 +141,36 @@ async def search_images( ) -> List[Dict[str, Any]]: """ Search for images/color palettes using Azure AI Search. - + Args: query: Search query (color, mood, style keywords) color_family: Optional color family filter (Cool, Warm, Neutral, etc.) mood: Optional mood filter top: Maximum number of results - + Returns: List of matching images with metadata """ try: client = self._get_images_client() - + # Build filter filters = [] if color_family: filters.append(f"color_family eq '{color_family}'") - + filter_str = " and ".join(filters) if filters else None - + # Execute search results = client.search( search_text=query, filter=filter_str, top=top, select=["id", "name", "filename", "primary_color", "secondary_color", - "color_family", "mood", "style", "description", "use_cases", - "blob_url", "keywords"] + "color_family", "mood", "style", "description", "use_cases", + "blob_url", "keywords"] ) - + images = [] for result in results: images.append({ @@ -188,14 +188,14 @@ async def search_images( "keywords": result.get("keywords"), "search_score": result.get("@search.score") }) - + logger.info(f"Image search for '{query}' returned {len(images)} results") return images - + except Exception as e: logger.error(f"Image search failed: {e}") return [] - + async def get_grounding_context( self, product_query: str, @@ -205,16 +205,16 @@ async def get_grounding_context( ) -> Dict[str, Any]: """ Get combined grounding context for content generation. - + Searches both products and images to provide comprehensive context for AI content generation. - + Args: product_query: Query for product search image_query: Optional query for image/style search category: Optional product category filter mood: Optional mood/style filter for images - + Returns: Combined grounding context with products and images """ @@ -224,7 +224,7 @@ async def get_grounding_context( category=category, top=5 ) - + # Search images if query provided images = [] if image_query: @@ -233,7 +233,7 @@ async def get_grounding_context( mood=mood, top=3 ) - + # Build grounding context context = { "products": products, @@ -242,9 +242,9 @@ async def get_grounding_context( "image_count": len(images), "grounding_summary": self._build_grounding_summary(products, images) } - + return context - + def _build_grounding_summary( self, products: List[Dict[str, Any]], @@ -252,7 +252,7 @@ def _build_grounding_summary( ) -> str: """Build a text summary of grounding context for agents.""" parts = [] - + if products: parts.append("## Available Products\n") for p in products[:5]: @@ -262,7 +262,7 @@ def _build_grounding_summary( if p.get('image_description'): parts.append(f" Visual: {p.get('image_description', '')[:100]}...") parts.append("") - + if images: parts.append("\n## Available Visual Styles\n") for img in images[:3]: @@ -272,7 +272,7 @@ def _build_grounding_summary( parts.append(f" Style: {img.get('style')}") parts.append(f" Best for: {img.get('use_cases', '')[:100]}") parts.append("") - + return "\n".join(parts) diff --git a/content-gen/src/backend/settings.py b/content-gen/src/backend/settings.py index d84ad99f1..08680ede8 100644 --- a/content-gen/src/backend/settings.py +++ b/content-gen/src/backend/settings.py @@ -62,16 +62,16 @@ class _AzureOpenAISettings(BaseSettings): gpt_model: str = Field(default="gpt-5", alias="AZURE_OPENAI_GPT_MODEL") model: str = "gpt-5" - + # Image generation model settings # Supported models: "dall-e-3" or "gpt-image-1" or "gpt-image-1.5" image_model: str = Field(default="dall-e-3", alias="AZURE_OPENAI_IMAGE_MODEL") dalle_model: str = Field(default="dall-e-3", alias="AZURE_OPENAI_DALLE_MODEL") # Legacy alias dalle_endpoint: Optional[str] = Field(default=None, alias="AZURE_OPENAI_DALLE_ENDPOINT") - + # gpt-image-1 or gpt-image-1.5 specific endpoint (if different from DALL-E endpoint) gpt_image_endpoint: Optional[str] = Field(default=None, alias="AZURE_OPENAI_GPT_IMAGE_ENDPOINT") - + resource: Optional[str] = None endpoint: Optional[str] = None temperature: float = 0.7 @@ -81,20 +81,20 @@ class _AzureOpenAISettings(BaseSettings): api_version: str = "2024-06-01" preview_api_version: str = "2024-02-01" image_api_version: str = Field(default="2025-04-01-preview", alias="AZURE_OPENAI_IMAGE_API_VERSION") - + # Image generation settings # For dall-e-3: 1024x1024, 1024x1792, 1792x1024 # For gpt-image-1: 1024x1024, 1536x1024, 1024x1536, auto image_size: str = "1024x1024" image_quality: str = "hd" # dall-e-3: standard/hd, gpt-image-1: low/medium/high/auto - + @property def effective_image_model(self) -> str: """Get the effective image model, preferring image_model over dalle_model.""" # If image_model is explicitly set and not the default, use it # Otherwise fall back to dalle_model for backwards compatibility return self.image_model if self.image_model else self.dalle_model - + @property def image_endpoint(self) -> Optional[str]: """Get the appropriate endpoint for the configured image model.""" @@ -105,22 +105,22 @@ def image_endpoint(self) -> Optional[str]: @property def image_generation_enabled(self) -> bool: """Check if image generation is available. - + Image generation requires either: - A DALL-E endpoint configured, OR - A gpt-image-1 or gpt-image-1.5 endpoint configured, OR - Using the main OpenAI endpoint with an image model configured - + Returns False if image_model is explicitly set to empty string or "none". """ # Check if image generation is explicitly disabled if not self.image_model or self.image_model.lower() in ("none", "disabled", ""): return False - + # Check if we have an endpoint that can handle image generation # Either a dedicated image endpoint or the main OpenAI endpoint has_image_endpoint = bool(self.dalle_endpoint or self.gpt_image_endpoint or self.endpoint) - + return has_image_endpoint @model_validator(mode="after") @@ -164,7 +164,7 @@ class _CosmosSettings(BaseSettings): class _AIFoundrySettings(BaseSettings): """Azure AI Foundry configuration for agent-based workflows. - + When USE_FOUNDRY=true, the orchestrator uses Azure AI Foundry's project endpoint instead of direct Azure OpenAI endpoints. """ @@ -177,7 +177,7 @@ class _AIFoundrySettings(BaseSettings): use_foundry: bool = Field(default=False, alias="USE_FOUNDRY") project_endpoint: Optional[str] = Field(default=None, alias="AZURE_AI_PROJECT_ENDPOINT") project_name: Optional[str] = Field(default=None, alias="AZURE_AI_PROJECT_NAME") - + # Model deployment names in Foundry model_deployment: Optional[str] = Field(default=None, alias="AZURE_AI_MODEL_DEPLOYMENT_NAME") image_deployment: str = Field(default="gpt-image-1", alias="AZURE_AI_IMAGE_MODEL_DEPLOYMENT") @@ -201,7 +201,7 @@ class _SearchSettings(BaseSettings): class _BrandGuidelinesSettings(BaseSettings): """ Brand guidelines stored as solution parameters. - + These are injected into all agent instructions for content strategy and compliance validation. """ @@ -215,32 +215,32 @@ class _BrandGuidelinesSettings(BaseSettings): # Voice and tone tone: str = "Professional yet approachable" voice: str = "Innovative, trustworthy, customer-focused" - + # Content restrictions (stored as comma-separated strings) prohibited_words_str: str = Field(default="", alias="BRAND_PROHIBITED_WORDS") required_disclosures_str: str = Field(default="", alias="BRAND_REQUIRED_DISCLOSURES") - + # Visual guidelines primary_color: str = "#0078D4" secondary_color: str = "#107C10" image_style: str = "Modern, clean, minimalist with bright lighting" typography: str = "Sans-serif, bold headlines, readable body text" - + # Compliance rules max_headline_length: int = 60 max_body_length: int = 500 require_cta: bool = True - + @property def prohibited_words(self) -> List[str]: """Parse prohibited words from comma-separated string.""" return parse_comma_separated(self.prohibited_words_str) - + @property def required_disclosures(self) -> List[str]: """Parse required disclosures from comma-separated string.""" return parse_comma_separated(self.required_disclosures_str) - + def get_compliance_prompt(self) -> str: """Generate compliance rules text for agent instructions.""" return f""" @@ -319,8 +319,8 @@ def get_compliance_prompt(self) -> str: - Avoid culturally insensitive or appropriative imagery **IMPORTANT - Photorealistic Product Images Are ACCEPTABLE:** -Photorealistic style for PRODUCT photography (e.g., paint cans, products, room scenes, textures) -is our standard marketing style and should NOT be flagged as a violation. Only flag photorealistic +Photorealistic style for PRODUCT photography (e.g., paint cans, products, room scenes, textures) +is our standard marketing style and should NOT be flagged as a violation. Only flag photorealistic content when it involves: - Fake/deepfake identifiable real people (SEVERITY: ERROR) - Misleading contexts designed to deceive consumers (SEVERITY: ERROR) @@ -444,7 +444,7 @@ class _AppSettings(BaseModel): ai_foundry: _AIFoundrySettings = _AIFoundrySettings() brand_guidelines: _BrandGuidelinesSettings = _BrandGuidelinesSettings() ui: Optional[_UiSettings] = _UiSettings() - + # Constructed properties chat_history: Optional[_ChatHistorySettings] = None blob: Optional[_StorageSettings] = None From 17aae47dd355d4ccd217113d3f2ded5a4729d795 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 11 Feb 2026 13:09:00 +0530 Subject: [PATCH 07/44] add workflow for checking broken links in Markdown files --- .github/workflows/broken-links-checker.yml | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/broken-links-checker.yml diff --git a/.github/workflows/broken-links-checker.yml b/.github/workflows/broken-links-checker.yml new file mode 100644 index 000000000..3b19db1df --- /dev/null +++ b/.github/workflows/broken-links-checker.yml @@ -0,0 +1,58 @@ +name: Broken Link Checker + +on: + pull_request: + paths: + - '**/*.md' + workflow_dispatch: + +permissions: + contents: read + actions: read + +jobs: + markdown-link-check: + name: Check Markdown Broken Links + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # For PR : Get only changed markdown files + - name: Get changed markdown files (PR only) + id: changed-markdown-files + if: github.event_name == 'pull_request' + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v46 + with: + files: | + **/*.md + + + # For PR: Check broken links only in changed files + - name: Check Broken Links in Changed Markdown Files + id: lychee-check-pr + if: github.event_name == 'pull_request' && steps.changed-markdown-files.outputs.any_changed == 'true' + uses: lycheeverse/lychee-action@v2.7.0 + with: + args: > + --verbose --no-progress --exclude ^https?:// + ${{ steps.changed-markdown-files.outputs.all_changed_files }} + failIfEmpty: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # For manual trigger: Check all markdown files in repo + - name: Check Broken Links in All Markdown Files in Entire Repo (Manual Trigger) + id: lychee-check-manual + if: github.event_name == 'workflow_dispatch' + uses: lycheeverse/lychee-action@v2.7.0 + with: + args: > + --verbose --no-progress --exclude ^https?:// + '**/*.md' + failIfEmpty: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 97c7c6d8efdc91e252a47c1db5e768463a18b641 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 11 Feb 2026 15:04:57 +0530 Subject: [PATCH 08/44] Add GitHub workflows for CodeQL analysis, stale issue management, telemetry template validation, and backend testing --- .github/workflows/codeql.yml | 44 +++++++++++ .github/workflows/stale-bot.yml | 74 +++++++++++++++++++ .../workflows/telemetry-template-check.yml | 34 +++++++++ .github/workflows/test.yml | 69 +++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/stale-bot.yml create mode 100644 .github/workflows/telemetry-template-check.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..ac9b1b756 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + paths: + - '**/*.py' + - '.github/workflows/codeql.yml' + pull_request: + branches: [ "main" ] + paths: + - '**/*.py' + - '.github/workflows/codeql.yml' + schedule: + - cron: '17 11 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml new file mode 100644 index 000000000..32d9d75df --- /dev/null +++ b/.github/workflows/stale-bot.yml @@ -0,0 +1,74 @@ +name: "Manage Stale Issues, PRs & Unmerged Branches" +on: + schedule: + - cron: '30 1 * * *' # Runs daily at 1:30 AM UTC + workflow_dispatch: # Allows manual triggering +permissions: + contents: write + issues: write + pull-requests: write +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: Mark Stale Issues and PRs + uses: actions/stale@v10 + with: + stale-issue-message: "This issue is stale because it has been open 180 days with no activity. Remove stale label or comment, or it will be closed in 30 days." + stale-pr-message: "This PR is stale because it has been open 180 days with no activity. Please update or it will be closed in 30 days." + days-before-stale: 180 + days-before-close: 30 + exempt-issue-labels: "keep" + exempt-pr-labels: "keep" + cleanup-branches: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # Fetch full history for accurate branch checks + - name: Fetch All Branches + run: git fetch --all --prune + - name: List Merged Branches With No Activity in Last 3 Months + run: | + echo "Branch Name,Last Commit Date,Committer,Committed In Branch,Action" > merged_branches_report.csv + for branch in $(git for-each-ref --format '%(refname:short) %(committerdate:unix)' refs/remotes/origin | awk -v date=$(date -d '3 months ago' +%s) '$2 < date {print $1}'); do + if [[ "$branch" != "origin/main" && "$branch" != "origin/dev" ]]; then + branch_name=${branch#origin/} + git fetch origin "$branch_name" || echo "Could not fetch branch: $branch_name" + last_commit_date=$(git log -1 --format=%ci "origin/$branch_name" || echo "Unknown") + committer_name=$(git log -1 --format=%cn "origin/$branch_name" || echo "Unknown") + committed_in_branch=$(git branch -r --contains "origin/$branch_name" | tr -d ' ' | paste -sd "," -) + echo "$branch_name,$last_commit_date,$committer_name,$committed_in_branch,Delete" >> merged_branches_report.csv + fi + done + - name: List PR Approved and Merged Branches Older Than 30 Days + run: | + for branch in $(gh api repos/${{ github.repository }}/pulls --jq '.[] | select(.merged_at != null and (.base.ref == "main" or .base.ref == "dev")) | select(.merged_at | fromdateiso8601 < (now - 2592000)) | .head.ref'); do + git fetch origin "$branch" || echo "Could not fetch branch: $branch" + last_commit_date=$(git log -1 --format=%ci origin/$branch || echo "Unknown") + committer_name=$(git log -1 --format=%cn origin/$branch || echo "Unknown") + committed_in_branch=$(git branch -r --contains "origin/$branch" | tr -d ' ' | paste -sd "," -) + echo "$branch,$last_commit_date,$committer_name,$committed_in_branch,Delete" >> merged_branches_report.csv + done + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: List Open PR Branches With No Activity in Last 3 Months + run: | + for branch in $(gh api repos/${{ github.repository }}/pulls --state open --jq '.[] | select(.base.ref == "main" or .base.ref == "dev") | .head.ref'); do + git fetch origin "$branch" || echo "Could not fetch branch: $branch" + last_commit_date=$(git log -1 --format=%ci origin/$branch || echo "Unknown") + committer_name=$(git log -1 --format=%cn origin/$branch || echo "Unknown") + if [[ $(date -d "$last_commit_date" +%s) -lt $(date -d '3 months ago' +%s) ]]; then + committed_in_branch=$(git branch -r --contains "origin/$branch" | tr -d ' ' | paste -sd "," -) + echo "$branch,$last_commit_date,$committer_name,$committed_in_branch,Delete" >> merged_branches_report.csv + fi + done + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload CSV Report of Inactive Branches + uses: actions/upload-artifact@v6 + with: + name: merged-branches-report + path: merged_branches_report.csv + retention-days: 30 diff --git a/.github/workflows/telemetry-template-check.yml b/.github/workflows/telemetry-template-check.yml new file mode 100644 index 000000000..f3688c5e0 --- /dev/null +++ b/.github/workflows/telemetry-template-check.yml @@ -0,0 +1,34 @@ +name: validate template property for telemetry + +on: + pull_request: + branches: + - main + paths: + - 'content-gen/azure.yaml' + +permissions: + contents: read + actions: read + +jobs: + validate-template-property: + name: validate-template-property + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Check for required metadata template line + run: | + if grep -E '^\s*#\s*template:\s*content-generation@' content-gen/azure.yaml; then + echo "ERROR: 'template' line is commented out in content-gen/azure.yaml! Please uncomment template line." + exit 1 + fi + + if ! grep -E '^\s*template:\s*content-generation@' content-gen/azure.yaml; then + echo "ERROR: Required 'template' line is missing in content-gen/azure.yaml! Please add template line for telemetry." + exit 1 + fi + echo "template line is present and not commented." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..e2c1e902b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +name: Test Workflow with Coverage + +on: + push: + branches: + - main + - dev + paths: + - '**/*.py' + - 'content-gen/src/backend/requirements*.txt' + - '.github/workflows/test.yml' + pull_request: + types: + - opened + - ready_for_review + - reopened + - synchronize + branches: + - main + - dev + paths: + - '**/*.py' + - 'content-gen/src/backend/requirements*.txt' + - '.github/workflows/test.yml' + +permissions: + contents: read + actions: read + +jobs: + backend_tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Install Backend Dependencies + run: | + python -m pip install --upgrade pip + pip install -r content-gen/src/backend/requirements.txt + pip install pytest-cov + pip install pytest-asyncio + + - name: Check if Backend Test Files Exist + id: check_backend_tests + run: | + if [ -z "$(find content-gen/src/tests -type f -name 'test_*.py' 2>/dev/null)" ]; then + echo "No backend test files found, skipping backend tests." + echo "skip_backend_tests=true" >> $GITHUB_ENV + else + echo "Backend test files found, running tests." + echo "skip_backend_tests=false" >> $GITHUB_ENV + fi + + - name: Run Backend Tests with Coverage + if: env.skip_backend_tests == 'false' + run: | + pytest --cov=. --cov-report=term-missing --cov-report=xml ./content-gen/src/tests + + - name: Skip Backend Tests + if: env.skip_backend_tests == 'true' + run: | + echo "Skipping backend tests because no test files were found." From bb0e8cec5943750173997cbc7f03989d70b31bfd Mon Sep 17 00:00:00 2001 From: Kingshuk-Microsoft Date: Wed, 11 Feb 2026 16:37:11 +0530 Subject: [PATCH 09/44] fix: update test input values and error messages for clarity; refine Azure identity import handling --- .../ChatHistory/chatHistoryListItem.test.tsx | 4 ++-- content-gen/scripts/post_deploy.py | 2 +- content-gen/src/backend/orchestrator.py | 5 ++++- content-gen/tests/rai_testing.py | 12 ++++++++++-- docs/generate_architecture.py | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx index 05dcb18f5..bacd0734d 100644 --- a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx +++ b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx @@ -569,12 +569,12 @@ describe('ChatHistoryListItemCell', () => { }) const inputItem = screen.getByPlaceholderText(conversation.title) - fireEvent.change(inputItem, { target: { value: 'Test Chat' } }) + fireEvent.change(inputItem, { target: { value: 'Another Existing Chat' } }) fireEvent.keyDown(inputItem, { key: 'Enter', code: 'Enter', charCode: 13 }) await waitFor(() => { - expect(screen.getByText(/Error: Enter a new title to proceed./i)).toBeInTheDocument() + expect(screen.getByText(/Error: could not rename item/i)).toBeInTheDocument() }) }) diff --git a/content-gen/scripts/post_deploy.py b/content-gen/scripts/post_deploy.py index c5da80892..12b0d3a75 100644 --- a/content-gen/scripts/post_deploy.py +++ b/content-gen/scripts/post_deploy.py @@ -40,7 +40,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import List, Dict, Any +from typing import Dict try: import httpx diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index f097df086..7d9e5be1f 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1514,7 +1514,10 @@ async def generate_content( except Exception as parse_error: # Best-effort JSON extraction from markdown code block; on failure, # fall back to the original prompt_text without interrupting image generation. - logger.debug(f"Failed to parse JSON from markdown code block for image prompt: {parse_error}") + logger.debug( + "Failed to parse JSON from markdown code block for image prompt: %s", + parse_error, + ) # Build product description for DALL-E context # Include detailed image descriptions if available for better color accuracy diff --git a/content-gen/tests/rai_testing.py b/content-gen/tests/rai_testing.py index f61346255..996131730 100644 --- a/content-gen/tests/rai_testing.py +++ b/content-gen/tests/rai_testing.py @@ -40,8 +40,16 @@ try: from azure.identity import AzureCliCredential, InteractiveBrowserCredential except ImportError: - # Azure Identity is optional; authentication features depending on it will be unavailable. - pass + # Azure Identity is optional; provide stubs that fail clearly if Azure auth is requested. + class _MissingAzureIdentityCredential: + def __init__(self, *args, **kwargs) -> None: + raise RuntimeError( + "The 'azure-identity' package is required to use '--use-azure-auth'. " + "Install it with 'pip install azure-identity' and try again." + ) + + AzureCliCredential = _MissingAzureIdentityCredential + InteractiveBrowserCredential = _MissingAzureIdentityCredential class TestCategory(Enum): diff --git a/docs/generate_architecture.py b/docs/generate_architecture.py index 13071e3d0..ca86d588d 100644 --- a/docs/generate_architecture.py +++ b/docs/generate_architecture.py @@ -15,7 +15,7 @@ from diagrams.azure.compute import ContainerInstances, AppServices, ContainerRegistries from diagrams.azure.database import CosmosDb, BlobStorage from diagrams.azure.ml import CognitiveServices -from diagrams.azure.network import PrivateEndpoint, DNSZones +from diagrams.azure.network import PrivateEndpoint from diagrams.azure.analytics import AnalysisServices from diagrams.onprem.client import User From 2cf96469a87257831d0ce074236cae0f8893f869 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 11 Feb 2026 21:24:03 +0530 Subject: [PATCH 10/44] Add documentation for Azure account setup and quota checks; include scripts for quota verification --- README.md | 4 +- content-gen/docs/AzureAccountSetUp.md | 14 + content-gen/docs/QuotaCheck.md | 105 ++++++++ content-gen/docs/images/git_bash.png | Bin 0 -> 30005 bytes .../docs/images/quota-check-output.png | Bin 0 -> 25868 bytes .../infra/script/quota_check_params.sh | 244 ++++++++++++++++++ 6 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 content-gen/docs/AzureAccountSetUp.md create mode 100644 content-gen/docs/QuotaCheck.md create mode 100644 content-gen/docs/images/git_bash.png create mode 100644 content-gen/docs/images/quota-check-output.png create mode 100644 content-gen/infra/script/quota_check_params.sh diff --git a/README.md b/README.md index c8438f615..9055a8dba 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,13 @@ Follow the quick deploy steps on the deployment guide to deploy this solution to
> ⚠️ **Important: Check Azure OpenAI Quota Availability** -
To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./docs/QuotaCheck.md) before you deploy the solution. +
To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./content-gen/docs/QuotaCheck.md) before you deploy the solution.
### Prerequisites and costs -To deploy this solution accelerator, ensure you have access to an [Azure subscription](https://azure.microsoft.com/free/) with the necessary permissions to create **resource groups, resources, app registrations, and assign roles at the resource group level**. This should include Contributor role at the subscription level and Role Based Access Control role on the subscription and/or resource group level. Follow the steps in [Azure Account Set Up](./docs/AzureAccountSetUp.md). +To deploy this solution accelerator, ensure you have access to an [Azure subscription](https://azure.microsoft.com/free/) with the necessary permissions to create **resource groups, resources, app registrations, and assign roles at the resource group level**. This should include Contributor role at the subscription level and Role Based Access Control role on the subscription and/or resource group level. Follow the steps in [Azure Account Set Up](./content-gen/docs/AzureAccountSetUp.md). Check the [Azure Products by Region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=all®ions=all) page and select a **region** where the following services are available. diff --git a/content-gen/docs/AzureAccountSetUp.md b/content-gen/docs/AzureAccountSetUp.md new file mode 100644 index 000000000..ce56466c8 --- /dev/null +++ b/content-gen/docs/AzureAccountSetUp.md @@ -0,0 +1,14 @@ +## Azure account setup + +1. Sign up for a [free Azure account](https://azure.microsoft.com/free/) and create an Azure Subscription. +2. Check that you have the necessary permissions: + * Your Azure account must have `Microsoft.Authorization/roleAssignments/write` permissions, such as [Role Based Access Control Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview), [User Access Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#user-access-administrator), or [Owner](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner). + * Your Azure account also needs `Microsoft.Resources/deployments/write` permissions on the subscription level. + +You can view the permissions for your account and subscription by following the steps below: +- Navigate to the [Azure Portal](https://portal.azure.com/) and click on `Subscriptions` under 'Navigation' +- Select the subscription you are using for this accelerator from the list. + - If you try to search for your subscription and it does not come up, make sure no filters are selected. +- Select `Access control (IAM)` and you can see the roles that are assigned to your account for this subscription. + - If you want to see more information about the roles, you can go to the `Role assignments` + tab and search by your account name and then click the role you want to view more information about. diff --git a/content-gen/docs/QuotaCheck.md b/content-gen/docs/QuotaCheck.md new file mode 100644 index 000000000..c0cd6c689 --- /dev/null +++ b/content-gen/docs/QuotaCheck.md @@ -0,0 +1,105 @@ +## Check Quota Availability Before Deployment + +Before deploying the Content Generation Solution Accelerator, **ensure sufficient quota availability** for the required models. + +> **For Global Standard | GPT-5.1 - ensure capacity to at least 150 tokens post-deployment for optimal performance.** + +> **For Global Standard | GPT-Image-1 - ensure capacity to at least 1 RPM (Requests Per Minute) for image generation.** + + +### Login if you have not done so already +``` +azd auth login +``` + + +### 📌 Default Models & Capacities: +``` +gpt-5.1:150, gpt-image-1:1 +``` +**Note:** GPT-5.1 capacity is in tokens, GPT-Image-1 capacity is in RPM (Requests Per Minute). +### 📌 Default Regions: +``` +australiaeast, centralus, eastasia, eastus, eastus2, japaneast, northeurope, southeastasia, swedencentral, uksouth, westus, westus3 +``` +### Usage Scenarios: +- No parameters passed → Default models and capacities will be checked in default regions. +- Only model(s) provided → The script will check for those models in the default regions. +- Only region(s) provided → The script will check default models in the specified regions. +- Both models and regions provided → The script will check those models in the specified regions. +- `--verbose` passed → Enables detailed logging output for debugging and traceability. + +### **Input Formats** +> Use the --models, --regions, and --verbose options for parameter handling: + +✔️ Run without parameters to check default models & regions without verbose logging: + ``` + ./quota_check_params.sh + ``` +✔️ Enable verbose logging: + ``` + ./quota_check_params.sh --verbose + ``` +✔️ Check specific model(s) in default regions: + ``` + ./quota_check_params.sh --models gpt-5.1:150,gpt-image-1:1 + ``` +✔️ Check default models in specific region(s): + ``` +./quota_check_params.sh --regions eastus,swedencentral + ``` +✔️ Passing Both models and regions: + ``` + ./quota_check_params.sh --models gpt-5.1:150,gpt-image-1:1 --regions eastus,swedencentral + ``` +✔️ All parameters combined: + ``` + ./quota_check_params.sh --models gpt-5.1:150,gpt-image-1:1 --regions eastus,swedencentral --verbose + ``` + +### **Sample Output** +The final table lists regions with available quota. You can select any of these regions for deployment. + +![quota-check-ouput](images/quota-check-output.png) + +--- +### **If using Azure Portal and Cloud Shell** + +1. Navigate to the [Azure Portal](https://portal.azure.com). +2. Click on **Azure Cloud Shell** in the top right navigation menu. +3. Run the appropriate command based on your requirement: + + **To check quota for the deployment** + + ```sh + curl -L -o quota_check_params.sh "https://raw.githubusercontent.com/microsoft/content-generation-solution-accelerator/main/content-gen/infra/script/quota_check_params.sh" + chmod +x quota_check_params.sh + ./quota_check_params.sh + ``` + - Refer to [Input Formats](#input-formats) for detailed commands. + +### **If using VS Code or Codespaces** +1. Open the terminal in VS Code or Codespaces. +2. If you're using VS Code, click the dropdown on the right side of the terminal window, and select `Git Bash`. + ![git_bash](images/git_bash.png) +3. Navigate to the `content-gen/infra/script` folder where the script files are located and make the script as executable: + ```sh + cd content-gen/infra/script + chmod +x quota_check_params.sh + ``` +4. Run the appropriate script based on your requirement: + + **To check quota for the deployment** + + ```sh + ./quota_check_params.sh + ``` + - Refer to [Input Formats](#input-formats) for detailed commands. + +5. If you see the error `_bash: az: command not found_`, install Azure CLI: + + ```sh + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + az login + ``` +6. Rerun the script after installing Azure CLI. diff --git a/content-gen/docs/images/git_bash.png b/content-gen/docs/images/git_bash.png new file mode 100644 index 0000000000000000000000000000000000000000..0e9f53a1233e4060da6a9ad52f2536ea69099bb3 GIT binary patch literal 30005 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 P7Em_nY)sg&_w4@w7%@{I literal 0 HcmV?d00001 diff --git a/content-gen/docs/images/quota-check-output.png b/content-gen/docs/images/quota-check-output.png new file mode 100644 index 0000000000000000000000000000000000000000..9e2eb8bf2cc719821e3c058bcd7c3403f7dddfa6 GIT binary patch literal 25868 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 literal 0 HcmV?d00001 diff --git a/content-gen/infra/script/quota_check_params.sh b/content-gen/infra/script/quota_check_params.sh new file mode 100644 index 000000000..f58bd975c --- /dev/null +++ b/content-gen/infra/script/quota_check_params.sh @@ -0,0 +1,244 @@ +#!/bin/bash +# VERBOSE=false + +MODELS="" +REGIONS="" +VERBOSE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --models) + MODELS="$2" + shift 2 + ;; + --regions) + REGIONS="$2" + shift 2 + ;; + --verbose) + VERBOSE=true + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Fallback to defaults if not provided +[[ -z "$MODELS" ]] +[[ -z "$REGIONS" ]] + +echo "Models: $MODELS" +echo "Regions: $REGIONS" +echo "Verbose: $VERBOSE" + +for arg in "$@"; do + if [ "$arg" = "--verbose" ]; then + VERBOSE=true + fi +done + +log_verbose() { + if [ "$VERBOSE" = true ]; then + echo "$1" + fi +} + +# Default Models and Capacities for Content Generation +# GPT-5.1: 150 tokens, GPT-Image-1: 1 RPM (Requests Per Minute) +DEFAULT_MODEL_CAPACITY="gpt-5.1:150,gpt-image-1:1" + +# Convert the comma-separated string into an array +IFS=',' read -r -a MODEL_CAPACITY_PAIRS <<< "$DEFAULT_MODEL_CAPACITY" + +echo "🔄 Fetching available Azure subscriptions..." +SUBSCRIPTIONS=$(az account list --query "[?state=='Enabled'].{Name:name, ID:id}" --output tsv) +SUB_COUNT=$(echo "$SUBSCRIPTIONS" | wc -l) + +if [ "$SUB_COUNT" -eq 0 ]; then + echo "❌ ERROR: No active Azure subscriptions found. Please log in using 'az login' and ensure you have an active subscription." + exit 1 +elif [ "$SUB_COUNT" -eq 1 ]; then + # If only one subscription, automatically select it + AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | awk '{print $2}') + if [ -z "$AZURE_SUBSCRIPTION_ID" ]; then + echo "❌ ERROR: No active Azure subscriptions found. Please log in using 'az login' and ensure you have an active subscription." + exit 1 + fi + echo "✅ Using the only available subscription: $AZURE_SUBSCRIPTION_ID" +else + # If multiple subscriptions exist, prompt the user to choose one + echo "Multiple subscriptions found:" + echo "$SUBSCRIPTIONS" | awk '{print NR")", $1, "-", $2}' + + while true; do + echo "Enter the number of the subscription to use:" + read SUB_INDEX + + # Validate user input + if [[ "$SUB_INDEX" =~ ^[0-9]+$ ]] && [ "$SUB_INDEX" -ge 1 ] && [ "$SUB_INDEX" -le "$SUB_COUNT" ]; then + AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | awk -v idx="$SUB_INDEX" 'NR==idx {print $2}') + echo "✅ Selected Subscription: $AZURE_SUBSCRIPTION_ID" + break + else + echo "❌ Invalid selection. Please enter a valid number from the list." + fi + done +fi + + +# Set the selected subscription +az account set --subscription "$AZURE_SUBSCRIPTION_ID" +echo "🎯 Active Subscription: $(az account show --query '[name, id]' --output tsv)" + +# Default Regions supporting GPT-5.1 and GPT-Image-1 with GlobalStandard SKU +DEFAULT_REGIONS="australiaeast,centralus,eastasia,eastus,eastus2,japaneast,northeurope,southeastasia,swedencentral,uksouth,westus,westus3" +IFS=',' read -r -a DEFAULT_REGION_ARRAY <<< "$DEFAULT_REGIONS" + +# Read parameters (if any) +IFS=',' read -r -a USER_PROVIDED_PAIRS <<< "$MODELS" +USER_REGION="$REGIONS" + +IS_USER_PROVIDED_PAIRS=false + +if [ ${#USER_PROVIDED_PAIRS[@]} -lt 1 ]; then + echo "No parameters provided, using default model-capacity pairs: ${MODEL_CAPACITY_PAIRS[*]}" +else + echo "Using provided model and capacity pairs: ${USER_PROVIDED_PAIRS[*]}" + IS_USER_PROVIDED_PAIRS=true + MODEL_CAPACITY_PAIRS=("${USER_PROVIDED_PAIRS[@]}") +fi + +declare -a FINAL_MODEL_NAMES +declare -a FINAL_CAPACITIES +declare -a TABLE_ROWS + +for PAIR in "${MODEL_CAPACITY_PAIRS[@]}"; do + MODEL_NAME=$(echo "$PAIR" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]') + CAPACITY=$(echo "$PAIR" | cut -d':' -f2) + + if [ -z "$MODEL_NAME" ] || [ -z "$CAPACITY" ]; then + echo "❌ ERROR: Invalid model and capacity pair '$PAIR'. Both model and capacity must be specified." + exit 1 + fi + + FINAL_MODEL_NAMES+=("$MODEL_NAME") + FINAL_CAPACITIES+=("$CAPACITY") + +done + +echo "🔄 Using Models: ${FINAL_MODEL_NAMES[*]} with respective Capacities: ${FINAL_CAPACITIES[*]}" +echo "----------------------------------------" + +# Check if the user provided a region, if not, use the default regions +if [ -n "$USER_REGION" ]; then + echo "🔍 User provided region: $USER_REGION" + IFS=',' read -r -a REGIONS <<< "$USER_REGION" +else + echo "No region specified, using default regions: ${DEFAULT_REGION_ARRAY[*]}" + REGIONS=("${DEFAULT_REGION_ARRAY[@]}") + APPLY_OR_CONDITION=true +fi + +echo "✅ Retrieved Azure regions. Checking availability..." +INDEX=1 + +VALID_REGIONS=() +for REGION in "${REGIONS[@]}"; do + log_verbose "----------------------------------------" + log_verbose "🔍 Checking region: $REGION" + + QUOTA_INFO=$(az cognitiveservices usage list --location "$REGION" --output json | tr '[:upper:]' '[:lower:]') + if [ -z "$QUOTA_INFO" ]; then + log_verbose "⚠️ WARNING: Failed to retrieve quota for region $REGION. Skipping." + continue + fi + + AT_LEAST_ONE_MODEL_AVAILABLE=false + TEMP_TABLE_ROWS=() + + for index in "${!FINAL_MODEL_NAMES[@]}"; do + MODEL_NAME="${FINAL_MODEL_NAMES[$index]}" + REQUIRED_CAPACITY="${FINAL_CAPACITIES[$index]}" + FOUND=false + INSUFFICIENT_QUOTA=false + + MODEL_TYPES=("openai.standard.$MODEL_NAME" "openai.globalstandard.$MODEL_NAME") + + for MODEL_TYPE in "${MODEL_TYPES[@]}"; do + FOUND=false + INSUFFICIENT_QUOTA=false + log_verbose "🔍 Checking model: $MODEL_NAME with required capacity: $REQUIRED_CAPACITY ($MODEL_TYPE)" + + MODEL_INFO=$(echo "$QUOTA_INFO" | awk -v model="\"value\": \"$MODEL_TYPE\"" ' + BEGIN { RS="},"; FS="," } + $0 ~ model { print $0 } + ') + + if [ -z "$MODEL_INFO" ]; then + FOUND=false + log_verbose "⚠️ WARNING: No quota information found for model: $MODEL_NAME in region: $REGION for model type: $MODEL_TYPE." + continue + fi + + if [ -n "$MODEL_INFO" ]; then + FOUND=true + CURRENT_VALUE=$(echo "$MODEL_INFO" | awk -F': ' '/"currentvalue"/ {print $2}' | tr -d ',' | tr -d ' ') + LIMIT=$(echo "$MODEL_INFO" | awk -F': ' '/"limit"/ {print $2}' | tr -d ',' | tr -d ' ') + + CURRENT_VALUE=${CURRENT_VALUE:-0} + LIMIT=${LIMIT:-0} + + CURRENT_VALUE=$(echo "$CURRENT_VALUE" | cut -d'.' -f1) + LIMIT=$(echo "$LIMIT" | cut -d'.' -f1) + + AVAILABLE=$((LIMIT - CURRENT_VALUE)) + log_verbose "✅ Model: $MODEL_TYPE | Used: $CURRENT_VALUE | Limit: $LIMIT | Available: $AVAILABLE" + + if [ "$AVAILABLE" -ge "$REQUIRED_CAPACITY" ]; then + FOUND=true + AT_LEAST_ONE_MODEL_AVAILABLE=true + TEMP_TABLE_ROWS+=("$(printf "| %-4s | %-20s | %-43s | %-10s | %-10s | %-10s |" "$INDEX" "$REGION" "$MODEL_TYPE" "$LIMIT" "$CURRENT_VALUE" "$AVAILABLE")") + else + INSUFFICIENT_QUOTA=true + fi + fi + + if [ "$FOUND" = false ]; then + log_verbose "❌ No models found for model: $MODEL_NAME in region: $REGION (${MODEL_TYPES[*]})" + + elif [ "$INSUFFICIENT_QUOTA" = true ]; then + log_verbose "⚠️ Model $MODEL_NAME in region: $REGION has insufficient quota (${MODEL_TYPES[*]})." + fi + done + done + + # For content-gen, we only need GPT-5.1, so check if at least one model is available + if [ "$AT_LEAST_ONE_MODEL_AVAILABLE" = true ] && [ "$INSUFFICIENT_QUOTA" = false ] && [ "$FOUND" = true ]; then + VALID_REGIONS+=("$REGION") + TABLE_ROWS+=("${TEMP_TABLE_ROWS[@]}") + INDEX=$((INDEX + 1)) + elif [ ${#USER_PROVIDED_PAIRS[@]} -eq 0 ]; then + echo "🚫 Skipping $REGION as it does not meet quota requirements." + fi + +done + +if [ ${#TABLE_ROWS[@]} -eq 0 ]; then + echo "--------------------------------------------------------------------------------------------------------------------" + + echo "❌ No regions have sufficient quota for all required models. Please request a quota increase: https://aka.ms/oai/stuquotarequest" +else + echo "---------------------------------------------------------------------------------------------------------------------" + printf "| %-4s | %-20s | %-43s | %-10s | %-10s | %-10s |\n" "No." "Region" "Model Name" "Limit" "Used" "Available" + echo "---------------------------------------------------------------------------------------------------------------------" + for ROW in "${TABLE_ROWS[@]}"; do + echo "$ROW" + done + echo "---------------------------------------------------------------------------------------------------------------------" + echo "➡️ To request a quota increase, visit: https://aka.ms/oai/stuquotarequest" +fi + +echo "✅ Script completed." From 15ef292add8d3c201b2716ba6e838f74535c28a5 Mon Sep 17 00:00:00 2001 From: Kanchan-Microsoft Date: Thu, 12 Feb 2026 13:36:16 +0530 Subject: [PATCH 11/44] updated waf steps in azd_deployment.md file --- content-gen/docs/AZD_DEPLOYMENT.md | 29 ++++++++++++++++++----------- content-gen/docs/DEPLOYMENT.md | 23 ----------------------- 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/content-gen/docs/AZD_DEPLOYMENT.md b/content-gen/docs/AZD_DEPLOYMENT.md index 0348c9de6..4c1514de6 100644 --- a/content-gen/docs/AZD_DEPLOYMENT.md +++ b/content-gen/docs/AZD_DEPLOYMENT.md @@ -101,21 +101,28 @@ azd env set embeddingDeploymentCapacity 50 azd env set azureOpenaiAPIVersion 2024-12-01-preview ``` -### 4. Enable Optional Features (WAF Pillars) +### 4. Choose Deployment Configuration -```bash -# Enable private networking (VNet integration) -azd env set enablePrivateNetworking true +The [`infra`](../infra) folder contains the [`main.bicep`](../infra/main.bicep) Bicep script, which defines all Azure infrastructure components for this solution. -# Enable monitoring (Log Analytics + App Insights) -azd env set enableMonitoring true +By default, the `azd up` command uses the [`main.parameters.json`](../infra/main.parameters.json) file to deploy the solution. This file is pre-configured for a **sandbox environment**. -# Enable scalability (auto-scaling, higher SKUs) -azd env set enableScalability true +For **production deployments**, the repository also provides [`main.waf.parameters.json`](../infra/main.waf.parameters.json), which applies a [Well-Architected Framework (WAF) aligned](https://learn.microsoft.com/en-us/azure/well-architected/) configuration. This can be used for Production scenarios. -# Enable redundancy (zone redundancy, geo-replication) -azd env set enableRedundancy true -``` +**How to choose your deployment configuration:** + +* **To use sandbox/dev environment** — Use the default `main.parameters.json` file. + +* **To use production configuration:** + +Before running `azd up`, copy the contents from the production configuration file to your main parameters file: + +1. Navigate to the `infra` folder in your project. +2. Open `main.waf.parameters.json` in a text editor (like Notepad, VS Code, etc.). +3. Select all content (Ctrl+A) and copy it (Ctrl+C). +4. Open `main.parameters.json` in the same text editor. +5. Select all existing content (Ctrl+A) and paste the copied content (Ctrl+V). +6. Save the file (Ctrl+S). ### 5. Deploy diff --git a/content-gen/docs/DEPLOYMENT.md b/content-gen/docs/DEPLOYMENT.md index 2c4f25618..c1f3fbd85 100644 --- a/content-gen/docs/DEPLOYMENT.md +++ b/content-gen/docs/DEPLOYMENT.md @@ -161,29 +161,6 @@ Depending on your subscription quota and capacity, you can adjust quota settings -### Deployment Options - -The [`infra`](../infra) folder of the Content Generation Solution Accelerator contains the [`main.bicep`](../infra/main.bicep) Bicep script, which defines all Azure infrastructure components for this solution. - -By default, the `azd up` command uses the [`main.parameters.json`](../infra/main.parameters.json) file to deploy the solution. This file is pre-configured for a **sandbox environment**. - -For **production deployments**, the repository also provides [`main.waf.parameters.json`](../infra/main.waf.parameters.json), which applies a [Well-Architected Framework (WAF) aligned](https://learn.microsoft.com/en-us/azure/well-architected/) configuration. This can be used for Production scenarios. - -**How to choose your deployment configuration:** - -* **To use sandbox/dev environment** — Use the default `main.parameters.json` file. - -* **To use production configuration:** - -Before running `azd up`, copy the contents from the production configuration file to your main parameters file: - -1. Navigate to the `infra` folder in your project. -2. Open `main.waf.parameters.json` in a text editor (like Notepad, VS Code, etc.). -3. Select all content (Ctrl+A) and copy it (Ctrl+C). -4. Open `main.parameters.json` in the same text editor. -5. Select all existing content (Ctrl+A) and paste the copied content (Ctrl+V). -6. Save the file (Ctrl+S). - ### Deploying with AZD Once you've opened the project in [Codespaces](#github-codespaces), [Dev Containers](#vs-code-dev-containers), or [locally](#local-environment), you can deploy it to Azure by following the steps in the [AZD Deployment Guide](AZD_DEPLOYMENT.md). From 2d8c1348fa267bfdf53db6d12f50ca882842af4f Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 12 Feb 2026 15:27:07 +0530 Subject: [PATCH 12/44] Enhance error handling in orchestrator.py with debug logging for JSON parsing failures --- content-gen/docs/QuotaCheck.md | 2 +- content-gen/src/backend/orchestrator.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/content-gen/docs/QuotaCheck.md b/content-gen/docs/QuotaCheck.md index c0cd6c689..97d8df7e0 100644 --- a/content-gen/docs/QuotaCheck.md +++ b/content-gen/docs/QuotaCheck.md @@ -60,7 +60,7 @@ australiaeast, centralus, eastasia, eastus, eastus2, japaneast, northeurope, sou ### **Sample Output** The final table lists regions with available quota. You can select any of these regions for deployment. -![quota-check-ouput](images/quota-check-output.png) +![quota-check-output](images/quota-check-output.png) --- ### **If using Azure Portal and Cloud Shell** diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index 8ea9538c1..f30690af9 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1512,7 +1512,11 @@ async def generate_content( prompt_data = json.loads(json_match.group(1)) prompt_text = prompt_data.get('prompt', prompt_data.get('image_prompt', prompt_text)) except Exception: - pass + logger.debug( + "Failed to parse JSON image prompt from markdown code block; " + "continuing with original prompt_text.", + exc_info=True + ) # Build product description for DALL-E context # Include detailed image descriptions if available for better color accuracy @@ -1745,7 +1749,13 @@ async def regenerate_image( prompt_text = prompt_data.get('prompt', prompt_text) change_summary = prompt_data.get('change_summary', modification_request) except Exception: - pass + # If JSON extraction fails here, fall back to the original + # prompt_text and change_summary values set earlier. + logger.debug( + "Failed to parse JSON from markdown in regenerate_image; " + "using original prompt_text and modification_request.", + exc_info=True + ) results["image_prompt"] = prompt_text results["message"] = f"Regenerating image: {change_summary}" From 79ab14758c129f112ed38d09b9a499df71f0e248 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 12 Feb 2026 16:13:49 +0530 Subject: [PATCH 13/44] Enhance compliance JSON parsing error handling with debug logging in orchestrator.py; add model availability check in quota_check_params.sh --- content-gen/infra/script/quota_check_params.sh | 7 +++++-- content-gen/src/backend/orchestrator.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/content-gen/infra/script/quota_check_params.sh b/content-gen/infra/script/quota_check_params.sh index f58bd975c..21249013c 100644 --- a/content-gen/infra/script/quota_check_params.sh +++ b/content-gen/infra/script/quota_check_params.sh @@ -156,6 +156,7 @@ for REGION in "${REGIONS[@]}"; do continue fi + IMAGE_MODEL_AVAILABLE=false AT_LEAST_ONE_MODEL_AVAILABLE=false TEMP_TABLE_ROWS=() @@ -199,6 +200,9 @@ for REGION in "${REGIONS[@]}"; do if [ "$AVAILABLE" -ge "$REQUIRED_CAPACITY" ]; then FOUND=true + if [ "$MODEL_NAME" = "gpt-image-1" ]; then + IMAGE_MODEL_AVAILABLE=true + fi AT_LEAST_ONE_MODEL_AVAILABLE=true TEMP_TABLE_ROWS+=("$(printf "| %-4s | %-20s | %-43s | %-10s | %-10s | %-10s |" "$INDEX" "$REGION" "$MODEL_TYPE" "$LIMIT" "$CURRENT_VALUE" "$AVAILABLE")") else @@ -215,8 +219,7 @@ for REGION in "${REGIONS[@]}"; do done done - # For content-gen, we only need GPT-5.1, so check if at least one model is available - if [ "$AT_LEAST_ONE_MODEL_AVAILABLE" = true ] && [ "$INSUFFICIENT_QUOTA" = false ] && [ "$FOUND" = true ]; then +if { [ "$IS_USER_PROVIDED_PAIRS" = true ] && [ "$INSUFFICIENT_QUOTA" = false ] && [ "$FOUND" = true ]; } || { [ "$IMAGE_MODEL_AVAILABLE" = true ] && { [ "$APPLY_OR_CONDITION" != true ] || [ "$AT_LEAST_ONE_MODEL_AVAILABLE" = true ]; }; }; then VALID_REGIONS+=("$REGION") TABLE_ROWS+=("${TEMP_TABLE_ROWS[@]}") INDEX=$((INDEX + 1)) diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index f30690af9..68131cc9d 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1580,7 +1580,8 @@ async def generate_content( for v in results["violations"] ) except (json.JSONDecodeError, KeyError): - pass + # Failed to parse compliance response JSON; violations will remain empty + logger.debug("Could not parse compliance violations from response", exc_info=True) except Exception as e: logger.exception(f"Error generating content: {e}") From e5d1420aa9af920b9da401f6ccd2903e0f053004 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 12 Feb 2026 16:16:37 +0530 Subject: [PATCH 14/44] Refactor quota_check_params.sh to simplify argument handling and remove unused fallback logic --- content-gen/infra/script/quota_check_params.sh | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/content-gen/infra/script/quota_check_params.sh b/content-gen/infra/script/quota_check_params.sh index 21249013c..2143eee58 100644 --- a/content-gen/infra/script/quota_check_params.sh +++ b/content-gen/infra/script/quota_check_params.sh @@ -26,20 +26,10 @@ while [[ $# -gt 0 ]]; do esac done -# Fallback to defaults if not provided -[[ -z "$MODELS" ]] -[[ -z "$REGIONS" ]] - echo "Models: $MODELS" echo "Regions: $REGIONS" echo "Verbose: $VERBOSE" -for arg in "$@"; do - if [ "$arg" = "--verbose" ]; then - VERBOSE=true - fi -done - log_verbose() { if [ "$VERBOSE" = true ]; then echo "$1" From 6b74c1b79bffef6923bcfc626e5a02839590554e Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 12 Feb 2026 16:25:23 +0530 Subject: [PATCH 15/44] Update stale-bot.yml to improve PR branch listing by using closed state and pagination for better accuracy --- .github/workflows/stale-bot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 32d9d75df..99bbca6d8 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -33,7 +33,7 @@ jobs: run: | echo "Branch Name,Last Commit Date,Committer,Committed In Branch,Action" > merged_branches_report.csv for branch in $(git for-each-ref --format '%(refname:short) %(committerdate:unix)' refs/remotes/origin | awk -v date=$(date -d '3 months ago' +%s) '$2 < date {print $1}'); do - if [[ "$branch" != "origin/main" && "$branch" != "origin/dev" ]]; then + if [[ "$branch" != "origin/main" && "$branch" != "origin/dev" && "$branch" != "origin/demo" ]]; then branch_name=${branch#origin/} git fetch origin "$branch_name" || echo "Could not fetch branch: $branch_name" last_commit_date=$(git log -1 --format=%ci "origin/$branch_name" || echo "Unknown") @@ -44,7 +44,7 @@ jobs: done - name: List PR Approved and Merged Branches Older Than 30 Days run: | - for branch in $(gh api repos/${{ github.repository }}/pulls --jq '.[] | select(.merged_at != null and (.base.ref == "main" or .base.ref == "dev")) | select(.merged_at | fromdateiso8601 < (now - 2592000)) | .head.ref'); do + for branch in $(gh api repos/${{ github.repository }}/pulls --state closed --paginate --jq '.[] | select(.merged_at != null and (.base.ref == "main" or .base.ref == "dev" or .base.ref == "demo")) | select(.merged_at | fromdateiso8601 < (now - 2592000)) | .head.ref'); do git fetch origin "$branch" || echo "Could not fetch branch: $branch" last_commit_date=$(git log -1 --format=%ci origin/$branch || echo "Unknown") committer_name=$(git log -1 --format=%cn origin/$branch || echo "Unknown") @@ -55,7 +55,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: List Open PR Branches With No Activity in Last 3 Months run: | - for branch in $(gh api repos/${{ github.repository }}/pulls --state open --jq '.[] | select(.base.ref == "main" or .base.ref == "dev") | .head.ref'); do + for branch in $(gh api repos/${{ github.repository }}/pulls --state open --jq '.[] | select(.base.ref == "main" or .base.ref == "dev" or .base.ref == "demo") | .head.ref'); do git fetch origin "$branch" || echo "Could not fetch branch: $branch" last_commit_date=$(git log -1 --format=%ci origin/$branch || echo "Unknown") committer_name=$(git log -1 --format=%cn origin/$branch || echo "Unknown") From 73e39f3364c7936fdc9bad8833250510e85b6afe Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 12 Feb 2026 16:29:19 +0530 Subject: [PATCH 16/44] Update pylint.yml to rename job step for clarity, focusing on flake8 execution --- .github/workflows/pylint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 282e5af1d..c9e3f788a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -32,6 +32,6 @@ jobs: pip install -r content-gen/src/backend/requirements.txt pip install flake8 # Ensure flake8 is installed explicitly - - name: Run flake8 and pylint + - name: Run flake8 run: | flake8 --config=.flake8 content-gen/src/backend # Specify the directory to lint From b35a3e9f3553ce2b492d90b67c60ff45b83be26e Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Thu, 12 Feb 2026 16:41:37 +0530 Subject: [PATCH 17/44] Fix shell script to correctly parse tab-separated subscription output and update documentation for login command --- content-gen/docs/QuotaCheck.md | 6 +++--- content-gen/infra/script/quota_check_params.sh | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/content-gen/docs/QuotaCheck.md b/content-gen/docs/QuotaCheck.md index 97d8df7e0..f19e3d2a2 100644 --- a/content-gen/docs/QuotaCheck.md +++ b/content-gen/docs/QuotaCheck.md @@ -8,14 +8,14 @@ Before deploying the Content Generation Solution Accelerator, **ensure sufficien ### Login if you have not done so already -``` -azd auth login +```sh +az login ``` ### 📌 Default Models & Capacities: ``` -gpt-5.1:150, gpt-image-1:1 +gpt-5.1:150,gpt-image-1:1 ``` **Note:** GPT-5.1 capacity is in tokens, GPT-Image-1 capacity is in RPM (Requests Per Minute). ### 📌 Default Regions: diff --git a/content-gen/infra/script/quota_check_params.sh b/content-gen/infra/script/quota_check_params.sh index 2143eee58..9baa18b6e 100644 --- a/content-gen/infra/script/quota_check_params.sh +++ b/content-gen/infra/script/quota_check_params.sh @@ -52,7 +52,7 @@ if [ "$SUB_COUNT" -eq 0 ]; then exit 1 elif [ "$SUB_COUNT" -eq 1 ]; then # If only one subscription, automatically select it - AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | awk '{print $2}') + AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | awk -F '\t' '{print $2}') if [ -z "$AZURE_SUBSCRIPTION_ID" ]; then echo "❌ ERROR: No active Azure subscriptions found. Please log in using 'az login' and ensure you have an active subscription." exit 1 @@ -61,7 +61,7 @@ elif [ "$SUB_COUNT" -eq 1 ]; then else # If multiple subscriptions exist, prompt the user to choose one echo "Multiple subscriptions found:" - echo "$SUBSCRIPTIONS" | awk '{print NR")", $1, "-", $2}' + echo "$SUBSCRIPTIONS" | awk -F '\t' '{print NR")", $1, "-", $2}' while true; do echo "Enter the number of the subscription to use:" @@ -69,7 +69,7 @@ else # Validate user input if [[ "$SUB_INDEX" =~ ^[0-9]+$ ]] && [ "$SUB_INDEX" -ge 1 ] && [ "$SUB_INDEX" -le "$SUB_COUNT" ]; then - AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | awk -v idx="$SUB_INDEX" 'NR==idx {print $2}') + AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | awk -F '\t' -v idx="$SUB_INDEX" 'NR==idx {print $2}') echo "✅ Selected Subscription: $AZURE_SUBSCRIPTION_ID" break else From c9475992959a0999c9b49befb3d149ff2b20dd70 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Thu, 12 Feb 2026 19:21:16 +0530 Subject: [PATCH 18/44] Updated the deployment readme --- content-gen/docs/AZD_DEPLOYMENT.md | 112 +++-------------------------- content-gen/docs/DEPLOYMENT.md | 80 +++++---------------- 2 files changed, 26 insertions(+), 166 deletions(-) diff --git a/content-gen/docs/AZD_DEPLOYMENT.md b/content-gen/docs/AZD_DEPLOYMENT.md index 4c1514de6..d2c0309da 100644 --- a/content-gen/docs/AZD_DEPLOYMENT.md +++ b/content-gen/docs/AZD_DEPLOYMENT.md @@ -58,6 +58,11 @@ azd auth login # Login to Azure CLI (required for some post-deployment scripts) az login +``` + Alternatively, login to Azure using a device code (recommended when using VS Code Web): + +``` +az login --use-device-code ``` ### 2. Initialize Environment @@ -72,36 +77,7 @@ azd env new azd env new content-gen-dev ``` -### 3. Configure Parameters (Optional) - -The deployment has sensible defaults, but you can customize: - -```bash -# Set the Azure region (default: eastus) -azd env set AZURE_LOCATION swedencentral - -# Set AI Services region (must support your models) -azd env set azureAiServiceLocation swedencentral - -# GPT Model configuration -azd env set gptModelName gpt-4o -azd env set gptModelVersion 2024-11-20 -azd env set gptModelDeploymentType GlobalStandard -azd env set gptModelCapacity 50 - -# Image generation model (dalle-3 or gpt-image-1) -azd env set imageModelChoice gpt-image-1 -azd env set dalleModelCapacity 1 - -# Embedding model -azd env set embeddingModel text-embedding-3-large -azd env set embeddingDeploymentCapacity 50 - -# Azure OpenAI API version -azd env set azureOpenaiAPIVersion 2024-12-01-preview -``` - -### 4. Choose Deployment Configuration +### 3. Choose Deployment Configuration The [`infra`](../infra) folder contains the [`main.bicep`](../infra/main.bicep) Bicep script, which defines all Azure infrastructure components for this solution. @@ -124,7 +100,7 @@ Before running `azd up`, copy the contents from the production configuration fil 5. Select all existing content (Ctrl+A) and paste the copied content (Ctrl+V). 6. Save the file (Ctrl+S). -### 5. Deploy +### 4. Deploy ```bash azd up @@ -139,25 +115,6 @@ This single command will: 6. **Configure** RBAC and Cosmos DB roles 7. **Upload** sample data and create the search index -## Deployment Parameters Reference - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `AZURE_LOCATION` | eastus | Primary Azure region | -| `azureAiServiceLocation` | eastus | Region for AI Services (must support chosen models) | -| `gptModelName` | gpt-4o | GPT model for content generation | -| `gptModelVersion` | 2024-11-20 | Model version | -| `gptModelDeploymentType` | GlobalStandard | Deployment type | -| `gptModelCapacity` | 50 | TPM capacity (in thousands) | -| `imageModelChoice` | dalle-3 | Image model: `dalle-3` or `gpt-image-1` | -| `dalleModelCapacity` | 1 | Image model capacity | -| `embeddingModel` | text-embedding-3-large | Embedding model | -| `embeddingDeploymentCapacity` | 50 | Embedding TPM capacity | -| `enablePrivateNetworking` | false | Enable VNet and private endpoints | -| `enableMonitoring` | false | Enable Log Analytics + App Insights | -| `enableScalability` | false | Enable auto-scaling | -| `enableRedundancy` | false | Enable zone/geo redundancy | - ## Using Existing Resources ### Reuse Existing AI Foundry Project @@ -174,13 +131,6 @@ azd env set azureExistingAIProjectResourceId "/subscriptions//resourceGr azd env set existingLogAnalyticsWorkspaceId "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" ``` -### Use Existing Container Registry - -```bash -# Set the name of your existing ACR -azd env set acrName myexistingacr -``` - ## Post-Deployment After `azd up` completes, you'll see output like: @@ -212,51 +162,6 @@ azd env get-value WEB_APP_URL azd env get-value RESOURCE_GROUP_NAME ``` -## Day-2 Operations - -### Update the Application - -After making code changes: - -```bash -# Rebuild and redeploy everything -azd up - -# Or just redeploy (no infra changes) -azd deploy -``` - -### Update Only the Backend (Container) - -```bash -# Get ACR and ACI names -ACR_NAME=$(azd env get-value ACR_NAME) -ACI_NAME=$(azd env get-value CONTAINER_INSTANCE_NAME) -RG_NAME=$(azd env get-value RESOURCE_GROUP_NAME) - -# Build and push new image -az acr build --registry $ACR_NAME --image content-gen-app:latest --file ./src/WebApp.Dockerfile ./src - -# Restart ACI to pull new image -az container restart --name $ACI_NAME --resource-group $RG_NAME -``` - -### Update Only the Frontend - -```bash -cd src/app/frontend -npm install && npm run build - -cd ../frontend-server -zip -r frontend-deploy.zip static/ server.js package.json package-lock.json - -az webapp deploy \ - --resource-group $(azd env get-value RESOURCE_GROUP_NAME) \ - --name $(azd env get-value APP_SERVICE_NAME) \ - --src-path frontend-deploy.zip \ - --type zip -``` - ### View Logs ```bash @@ -331,7 +236,7 @@ Error: az acr build failed **Solution**: Check the Dockerfile and ensure all required files are present: ```bash # Manual build for debugging -cd src +cd src/app docker build -f WebApp.Dockerfile -t content-gen-app:test . ``` @@ -403,6 +308,5 @@ When `enablePrivateNetworking` is enabled: ## Related Documentation -- [Manual Deployment Guide](DEPLOYMENT.md) - [Image Generation Configuration](IMAGE_GENERATION.md) - [Azure Developer CLI Documentation](https://learn.microsoft.com/azure/developer/azure-developer-cli/) diff --git a/content-gen/docs/DEPLOYMENT.md b/content-gen/docs/DEPLOYMENT.md index c1f3fbd85..8d4bf2ee5 100644 --- a/content-gen/docs/DEPLOYMENT.md +++ b/content-gen/docs/DEPLOYMENT.md @@ -134,13 +134,12 @@ When you start the deployment, most parameters will have **default values**, but | ------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------- | | **Azure Region** | The region where resources will be created. | *(empty)* | | **Environment Name** | A **3–20 character alphanumeric value** used to generate a unique ID to prefix the resources. | env\_name | -| **GPT Model** | Choose from **gpt-4, gpt-4o, gpt-4o-mini**. | gpt-4o-mini | -| **GPT Model Version** | The version of the selected GPT model. | 2024-07-18 | +| **GPT Model** | Choose from **gpt-4, gpt-4o, gpt-4o-mini, gpt-5.1**. | gpt-5.1 | +| **GPT Model Version** | The version of the selected GPT model. | 2025-11-13 | | **OpenAI API Version** | The Azure OpenAI API version to use. | 2025-01-01-preview | -| **GPT Model Deployment Capacity** | Configure capacity for **GPT models** (in thousands). | 30k | -| **DALL-E Model** | DALL-E model for image generation. | dall-e-3 | +| **GPT Model Deployment Capacity** | Configure capacity for **GPT models** (in thousands). | 150k | +| **Image Model** | Choose from **dall-e-3, gpt-image-1, gpt-image-1.5** | gpt-image-1 | | **Image Tag** | Docker image tag to deploy. Common values: `latest`, `dev`, `hotfix`. | latest | -| **Use Local Build** | Boolean flag to determine if local container builds should be used. | false | | **Existing Log Analytics Workspace** | To reuse an existing Log Analytics Workspace ID. | *(empty)* | | **Existing Azure AI Foundry Project** | To reuse an existing Azure AI Foundry Project ID instead of creating a new one. | *(empty)* | @@ -171,15 +170,7 @@ Once you've opened the project in [Codespaces](#github-codespaces), [Dev Contain Follow steps in [App Authentication](./AppAuthentication.md) to configure authentication in app service. Note: Authentication changes can take up to 10 minutes. -2. **Assign RBAC Roles (if needed)** - - If you encounter 401/403 errors, run the RBAC assignment script and wait 5-10 minutes for propagation: - - ```shell - bash ./scripts/assign_rbac_roles.sh - ``` - -3. **Deleting Resources After a Failed Deployment** +2. **Deleting Resources After a Failed Deployment** - Follow steps in [Delete Resource Group](./DeleteResourceGroup.md) if your deployment fails and/or you need to clean up the resources. ## Troubleshooting @@ -187,14 +178,6 @@ Once you've opened the project in [Codespaces](#github-codespaces), [Dev Contain

Common Issues and Solutions -### 401 Unauthorized Errors - -**Symptom**: API calls return 401 errors - -**Cause**: Missing RBAC role assignments - -**Solution**: Run `assign_rbac_roles.sh` and wait 5-10 minutes for propagation - ### 403 Forbidden from Cosmos DB **Symptom**: Cosmos DB operations fail with 403 @@ -230,56 +213,29 @@ az webapp config set -g $RESOURCE_GROUP -n --http20-enabled false ### Image Generation Not Working -**Symptom**: DALL-E requests fail +**Symptom**: DALL-E/GPT-Image requests fail -**Cause**: Missing DALL-E model deployment or incorrect endpoint +**Cause**: Missing DALL-E/GPT-Image model deployment or incorrect endpoint **Solution**: -1. Verify DALL-E 3 deployment exists in Azure OpenAI resource -2. Check `AZURE_OPENAI_DALLE_ENDPOINT` and `AZURE_OPENAI_DALLE_DEPLOYMENT` environment variables +1. Verify DALL-E 3 or GPT-Image-1 or GPT-Image-1.5 deployment exists in Azure OpenAI resource +2. Check `AZURE_OPENAI_IMAGE_MODEL` environment variable
-## Environment Variables Reference - -
- Backend Environment Variables (ACI) - -| Variable | Description | Example | -|----------|-------------|---------| -| AZURE_OPENAI_ENDPOINT | GPT model endpoint | https://ai-account.cognitiveservices.azure.com/ | -| AZURE_OPENAI_DEPLOYMENT_NAME | GPT deployment name | gpt-4o-mini | -| AZURE_OPENAI_DALLE_ENDPOINT | DALL-E endpoint | https://dalle-account.cognitiveservices.azure.com/ | -| AZURE_OPENAI_DALLE_DEPLOYMENT | DALL-E deployment name | dall-e-3 | -| COSMOS_ENDPOINT | Cosmos DB endpoint | https://cosmos.documents.azure.com:443/ | -| COSMOS_DATABASE | Database name | content-generation | -| AZURE_STORAGE_ACCOUNT_NAME | Storage account | storagecontentgen | -| AZURE_STORAGE_CONTAINER | Product images container | product-images | -| AZURE_STORAGE_GENERATED_CONTAINER | Generated images container | generated-images | - -
- -
- Frontend Environment Variables (App Service) - -| Variable | Description | Example | -|----------|-------------|---------| -| BACKEND_URL | Backend API URL | http://backend.contentgen.internal:8000 | -| WEBSITES_PORT | App Service port | 3000 | - -
+## Sample Workflow -## Sample Prompts +To get started with the Content Generation solution, follow these steps: -To help you get started, here are some **sample prompts** you can use with the Content Generation Solution: +1. **Task:** From the welcome screen, select one of the suggested prompts. Sample prompts include: + - *"I need to create a social media post about paint products for home remodels. The campaign is titled 'Brighten Your Springtime' and the audience is new homeowners. I need marketing copy plus an image."* + - *"Generate a social media campaign with ad copy and an image. This is for 'Back to School' and the audience is parents of school age children. Tone is playful and humorous."* -- "Create a product description for a new eco-friendly water bottle" -- "Generate marketing copy for a summer sale campaign" -- "Write social media posts promoting our latest product launch" -- "Create an image for a blog post about sustainable living" -- "Generate a product image showing a modern office setup" +2. **Task:** Click the **"Confirm Brief"** button. + > **Observe:** The system analyzes the creative brief to provide suggestions later. -These prompts serve as a great starting point to explore the solution's capabilities with text generation, image generation, and content management. +3. **Task:** Select a product from the product list, then click **"Generate Content"**. + > **Observe:** Enters "Thinking Process" with a "Generating Content.." spinner. Once complete, the detailed output is displayed. ## Architecture Overview From 19b302fe83ec7eb9a6992470fc334334abde6109 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Thu, 12 Feb 2026 19:21:27 +0530 Subject: [PATCH 19/44] added delete rg readme --- content-gen/docs/DeleteResourceGroup.md | 51 ++++++++++++++++++ content-gen/docs/images/DeleteRG.png | Bin 0 -> 78459 bytes .../docs/images/DeleteResourceGroup.md | 51 ++++++++++++++++++ content-gen/docs/images/deleteservices.png | Bin 0 -> 118313 bytes content-gen/docs/images/resource-groups.png | Bin 0 -> 52735 bytes content-gen/docs/images/resourcegroup.png | Bin 0 -> 31099 bytes 6 files changed, 102 insertions(+) create mode 100644 content-gen/docs/DeleteResourceGroup.md create mode 100644 content-gen/docs/images/DeleteRG.png create mode 100644 content-gen/docs/images/DeleteResourceGroup.md create mode 100644 content-gen/docs/images/deleteservices.png create mode 100644 content-gen/docs/images/resource-groups.png create mode 100644 content-gen/docs/images/resourcegroup.png diff --git a/content-gen/docs/DeleteResourceGroup.md b/content-gen/docs/DeleteResourceGroup.md new file mode 100644 index 000000000..9c88e228b --- /dev/null +++ b/content-gen/docs/DeleteResourceGroup.md @@ -0,0 +1,51 @@ +# Deleting Resources After a Failed Deployment in Azure Portal + +If your deployment fails and you need to clean up the resources manually, follow these steps in the Azure Portal. + +--- + +## **1. Navigate to the Azure Portal** +1. Open [Azure Portal](https://portal.azure.com/). +2. Sign in with your Azure account. + +--- + +## **2. Find the Resource Group** +1. In the search bar at the top, type **"Resource groups"** and select it. +2. Locate the **resource group** associated with the failed deployment. + +![Resource Groups](images/resourcegroup.png) + +![Resource Groups](images/resource-groups.png) + +--- + +## **3. Delete the Resource Group** +1. Click on the **resource group name** to open it. +2. Click the **Delete resource group** button at the top. + +![Delete Resource Group](images/DeleteRG.png) + +3. Type the resource group name in the confirmation box and click **Delete**. + +📌 **Note:** Deleting a resource group will remove all resources inside it. + +--- + +## **4. Delete Individual Resources (If Needed)** +If you don’t want to delete the entire resource group, follow these steps: + +1. Open **Azure Portal** and go to the **Resource groups** section. +2. Click on the specific **resource group**. +3. Select the **resource** you want to delete (e.g., App Service, Storage Account). +4. Click **Delete** at the top. + +![Delete Individual Resource](images/deleteservices.png) + +--- + +## **5. Verify Deletion** +- After a few minutes, refresh the **Resource groups** page. +- Ensure the deleted resource or group no longer appears. + +📌 **Tip:** If a resource fails to delete, check if it's **locked** under the **Locks** section and remove the lock. diff --git a/content-gen/docs/images/DeleteRG.png b/content-gen/docs/images/DeleteRG.png new file mode 100644 index 0000000000000000000000000000000000000000..c435ecf1771c80434a274ce87fb31421b612ca3d GIT binary patch literal 78459 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�kRqA0?*x%d*OMZVa|U@014Rl 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

5W);&}oJOB`V+4DxFF~n~GT7#G(nif<=Qz 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 Qy~*in8s5j=ef08w0Whw$W&i*H literal 0 HcmV?d00001 diff --git a/content-gen/docs/images/resourcegroup.png b/content-gen/docs/images/resourcegroup.png new file mode 100644 index 0000000000000000000000000000000000000000..67b058bcc128be096ab8029648e1908f255b196c GIT binary patch literal 31099 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 Date: Fri, 13 Feb 2026 10:15:55 +0530 Subject: [PATCH 20/44] Added validation for rename button --- .../frontend/src/components/ChatHistory.tsx | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/content-gen/src/app/frontend/src/components/ChatHistory.tsx b/content-gen/src/app/frontend/src/components/ChatHistory.tsx index 58fe12a5b..ed9f97762 100644 --- a/content-gen/src/app/frontend/src/components/ChatHistory.tsx +++ b/content-gen/src/app/frontend/src/components/ChatHistory.tsx @@ -322,21 +322,29 @@ function ConversationItem({ const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [renameValue, setRenameValue] = useState(conversation.title || ''); + const [renameError, setRenameError] = useState(''); const renameInputRef = useRef(null); const handleRenameClick = () => { setRenameValue(conversation.title || ''); + setRenameError(''); setIsRenameDialogOpen(true); setIsMenuOpen(false); }; const handleRenameConfirm = async () => { const trimmedValue = renameValue.trim(); - if (trimmedValue && trimmedValue !== conversation.title) { - await onRename(conversation.id, trimmedValue); - onRefresh(); + + if (trimmedValue === conversation.title) { + setIsRenameDialogOpen(false); + setRenameError(''); + return; } + + await onRename(conversation.id, trimmedValue); + onRefresh(); setIsRenameDialogOpen(false); + setRenameError(''); }; const handleDeleteClick = () => { @@ -446,9 +454,17 @@ function ConversationItem({ setRenameValue(e.target.value)} + onChange={(e) => { + const newValue = e.target.value; + setRenameValue(newValue); + if (newValue.trim() === '') { + setRenameError('Conversation name cannot be empty or contain only spaces'); + } else { + setRenameError(''); + } + }} onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === 'Enter' && renameValue.trim()) { handleRenameConfirm(); } else if (e.key === 'Escape') { setIsRenameDialogOpen(false); @@ -457,13 +473,32 @@ function ConversationItem({ placeholder="Enter conversation name" style={{ width: '100%' }} /> + {renameError && ( + + {renameError} + + )} - - From 2217d133ae9d96ec387904c4838c2427aef49e36 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Fri, 13 Feb 2026 15:34:57 +0530 Subject: [PATCH 21/44] updated broken links and removed the not required readme --- content-gen/README.md | 3 +- .../docs/images/DeleteResourceGroup.md | 51 -- .../images/create_new_app_registration.md | 35 -- docs/LocalDevelopmentSetup.md | 506 ------------------ docs/LogAnalyticsReplicationDisable.md | 2 +- 5 files changed, 2 insertions(+), 595 deletions(-) delete mode 100644 content-gen/docs/images/DeleteResourceGroup.md delete mode 100644 content-gen/docs/images/create_new_app_registration.md delete mode 100644 docs/LocalDevelopmentSetup.md diff --git a/content-gen/README.md b/content-gen/README.md index df7179765..810e0e2ad 100644 --- a/content-gen/README.md +++ b/content-gen/README.md @@ -183,8 +183,7 @@ BRAND_SECONDARY_COLOR=#107C10 - [AZD Deployment Guide](docs/AZD_DEPLOYMENT.md) - Deploy with Azure Developer CLI - [Manual Deployment Guide](docs/DEPLOYMENT.md) - Step-by-step manual deployment - [Image Generation Configuration](docs/IMAGE_GENERATION.md) - DALL-E 3 and GPT-Image-1 setup -- [API Reference](docs/API.md) ## License -MIT License - See [LICENSE](LICENSE) for details. +MIT License - See [LICENSE](../LICENSE) for details. diff --git a/content-gen/docs/images/DeleteResourceGroup.md b/content-gen/docs/images/DeleteResourceGroup.md deleted file mode 100644 index 9c88e228b..000000000 --- a/content-gen/docs/images/DeleteResourceGroup.md +++ /dev/null @@ -1,51 +0,0 @@ -# Deleting Resources After a Failed Deployment in Azure Portal - -If your deployment fails and you need to clean up the resources manually, follow these steps in the Azure Portal. - ---- - -## **1. Navigate to the Azure Portal** -1. Open [Azure Portal](https://portal.azure.com/). -2. Sign in with your Azure account. - ---- - -## **2. Find the Resource Group** -1. In the search bar at the top, type **"Resource groups"** and select it. -2. Locate the **resource group** associated with the failed deployment. - -![Resource Groups](images/resourcegroup.png) - -![Resource Groups](images/resource-groups.png) - ---- - -## **3. Delete the Resource Group** -1. Click on the **resource group name** to open it. -2. Click the **Delete resource group** button at the top. - -![Delete Resource Group](images/DeleteRG.png) - -3. Type the resource group name in the confirmation box and click **Delete**. - -📌 **Note:** Deleting a resource group will remove all resources inside it. - ---- - -## **4. Delete Individual Resources (If Needed)** -If you don’t want to delete the entire resource group, follow these steps: - -1. Open **Azure Portal** and go to the **Resource groups** section. -2. Click on the specific **resource group**. -3. Select the **resource** you want to delete (e.g., App Service, Storage Account). -4. Click **Delete** at the top. - -![Delete Individual Resource](images/deleteservices.png) - ---- - -## **5. Verify Deletion** -- After a few minutes, refresh the **Resource groups** page. -- Ensure the deleted resource or group no longer appears. - -📌 **Tip:** If a resource fails to delete, check if it's **locked** under the **Locks** section and remove the lock. diff --git a/content-gen/docs/images/create_new_app_registration.md b/content-gen/docs/images/create_new_app_registration.md deleted file mode 100644 index 7dcf2c402..000000000 --- a/content-gen/docs/images/create_new_app_registration.md +++ /dev/null @@ -1,35 +0,0 @@ -# Creating a new App Registration - -1. Click on `Home` and select `Microsoft Entra ID`. - -![Microsoft Entra ID](images/MicrosoftEntraID.png) - -2. Click on `App registrations`. - -![App registrations](images/Appregistrations.png) - -3. Click on `+ New registration`. - -![New Registrations](images/NewRegistration.png) - -4. Provide the `Name`, select supported account types as `Accounts in this organizational directory only(Contoso only - Single tenant)`, select platform as `Web`, enter/select the `URL` and register. - -![Add Details](images/AddDetails.png) - -5. After application is created successfully, then click on `Add a Redirect URL`. - -![Redirect URL](images/AddRedirectURL.png) - -6. Click on `+ Add a platform`. - -![+ Add platform](images/AddPlatform.png) - -7. Click on `Web`. - -![Web](images/Web.png) - -8. Enter the `web app URL` (Provide the app service name in place of XXXX) and Save. Then go back to [Set Up Authentication in Azure App Service](AppAuthentication.md) Step 1 page and follow from _Point 4_ choose `Pick an existing app registration in this directory` from the Add an Identity Provider page and provide the newly registered App Name. - -E.g. <>.azurewebsites.net/.auth/login/aad/callback>> - -![Add Details](images/WebAppURL.png) diff --git a/docs/LocalDevelopmentSetup.md b/docs/LocalDevelopmentSetup.md deleted file mode 100644 index 4635b89e8..000000000 --- a/docs/LocalDevelopmentSetup.md +++ /dev/null @@ -1,506 +0,0 @@ -# Local Development Setup Guide - -This guide provides comprehensive instructions for setting up the Document Generation Solution Accelerator for local development across Windows and Linux platforms. - -## Important Setup Notes - -### Multi-Service Architecture - -This application consists of **two separate services** that run independently: - -1. **Backend API** - REST API server for the frontend -2. **Frontend** - React-based user interface - -> **⚠️ Critical: Each service must run in its own terminal/console window** -> -> - **Do NOT close terminals** while services are running -> - Open **2 separate terminal windows** for local development -> - Each service will occupy its terminal and show live logs - - -### Path Conventions - -**All paths in this guide are relative to the repository root directory:** - -```bash -document-generation-solution-accelerator/ ← Repository root (start here) -├── src/ -│ ├── backend/ -│ │ ├── api/ ← API endpoints and routes -│ │ ├── auth/ ← Authentication modules -│ │ ├── helpers/ ← Utility and helper functions -│ │ ├── history/ ← Chat/session history management -│ │ ├── security/ ← Security-related modules -│ │ └── settings.py ← Backend configuration -│ ├── frontend/ -│ │ ├── src/ ← React/TypeScript source -│ │ └── package.json ← Frontend dependencies -│ ├── static/ ← Static web assets -│ ├── tests/ ← Unit and integration tests -│ ├── app.py ← Main Flask application entry point -│ ├── .env ← Main application config file -│ └── requirements.txt ← Python dependencies -├── scripts/ -│ ├── prepdocs.py ← Document processing script -│ ├── auth_init.py ← Authentication setup -│ ├── data_preparation.py ← Data pipeline scripts -│ └── config.json ← Scripts configuration -├── infra/ -│ ├── main.bicep ← Main infrastructure template -│ ├── scripts/ ← Infrastructure scripts -│ └── main.parameters.json ← Deployment parameters -├── docs/ ← Documentation (you are here) -└── tests/ ← End-to-end tests - └── e2e-test/ -``` - -**Before starting any step, ensure you are in the repository root directory:** - -```bash -# Verify you're in the correct location -pwd # Linux/macOS - should show: .../document-generation-solution-accelerator -Get-Location # Windows PowerShell - should show: ...\document-generation-solution-accelerator - -# If not, navigate to repository root -cd path/to/document-generation-solution-accelerator -``` - -## Step 1: Prerequisites - Install Required Tools - -Install these tools before you start: -- [Visual Studio Code](https://code.visualstudio.com/) with the following extensions: - - [Azure Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack) - - [Bicep](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-bicep) - - [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) -- [Python 3.11](https://www.python.org/downloads/). **Important:** Check "Add Python to PATH" during installation. -- [PowerShell 7.0+](https://github.com/PowerShell/PowerShell#get-powershell). -- [Node.js (LTS)](https://nodejs.org/en). -- [Git](https://git-scm.com/downloads). -- [Azure Developer CLI (azd) v1.18.0+](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd). -- [Microsoft ODBC Driver 17](https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver16) for SQL Server. - - -### Windows Development - -#### Option 1: Native Windows (PowerShell) - -```powershell -# Install Python 3.11+ and Git -winget install Python.Python.3.11 -winget install Git.Git - -# Install Node.js for frontend -winget install OpenJS.NodeJS.LTS - -# Install uv package manager -py -3.11 -m pip install uv -``` - -**Note**: On Windows, use `py -3.11 -m uv` instead of `uv` for all commands to ensure you're using Python 3.11. - -#### Option 2: Windows with WSL2 (Recommended) - -```bash -# Install WSL2 first (run in PowerShell as Administrator): -# wsl --install -d Ubuntu - -# Then in WSL2 Ubuntu terminal: -sudo apt update && sudo apt install python3.11 python3.11-venv git curl nodejs npm -y - -# Install uv -curl -LsSf https://astral.sh/uv/install.sh | sh -source ~/.bashrc -``` - -### Linux Development - -#### Ubuntu/Debian - -```bash -# Install prerequisites -sudo apt update && sudo apt install python3.11 python3.11-venv git curl nodejs npm -y - -# Install uv package manager -curl -LsSf https://astral.sh/uv/install.sh | sh -source ~/.bashrc -``` - -#### RHEL/CentOS/Fedora - -```bash -# Install prerequisites -sudo dnf install python3.11 python3.11-devel git curl gcc nodejs npm -y - -# Install uv -curl -LsSf https://astral.sh/uv/install.sh | sh -source ~/.bashrc -``` - - -## Step 2: Clone the Repository - -Choose a location on your local machine where you want to store the project files. We recommend creating a dedicated folder for your development projects. - -#### Using Command Line/Terminal - -1. **Open your terminal or command prompt. Navigate to your desired directory and Clone the repository:** - ```bash - git clone https://github.com/microsoft/document-generation-solution-accelerator.git - ``` - -2. **Navigate to the project directory:** - ```bash - cd document-generation-solution-accelerator - ``` - -3. **Open the project in Visual Studio Code:** - ```bash - code . - ``` - - -## Step 3: Development Tools Setup - -### Visual Studio Code (Recommended) - -#### Required Extensions - -Create `.vscode/extensions.json` in the workspace root and copy the following JSON: - -```json -{ - "recommendations": [ - "ms-python.python", - "ms-python.pylint", - "ms-python.black-formatter", - "ms-python.isort", - "ms-vscode-remote.remote-wsl", - "ms-vscode-remote.remote-containers", - "redhat.vscode-yaml", - "ms-vscode.azure-account", - "ms-python.mypy-type-checker" - ] -} -``` - -VS Code will prompt you to install these recommended extensions when you open the workspace. - -#### Settings Configuration - -Create `.vscode/settings.json` and copy the following JSON: - -```json -{ - "python.defaultInterpreterPath": "./.venv/bin/python", - "python.terminal.activateEnvironment": true, - "python.formatting.provider": "black", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "files.associations": { - "*.yaml": "yaml", - "*.yml": "yaml" - } -} -``` - -## Step 4: Azure Authentication Setup - -Before configuring services, authenticate with Azure: - -```bash -# Login to Azure CLI -az login - -# Set your subscription -az account set --subscription "your-subscription-id" - -# Verify authentication -az account show -``` - -## Step 5: Local Setup/Deployment - -Follow these steps to set up and run the application locally: - -## Local Deployment: - -You can refer the local deployment guide here: [Local Deployment Guide](https://github.com/microsoft/document-generation-solution-accelerator/blob/main/docs/DeploymentGuide.md) - -### 5.1. Open the App Folder -Navigate to the `src` directory of the repository using Visual Studio Code. - -### 5.2. Configure Environment Variables -- Copy the `.env.sample` file to a new file named `.env`. -- Update the `.env` file with the required values from your Azure resource group in Azure Portal App Service environment variables. -- You can get all env value in your deployed resource group under App Service: -![Environment Variables](images/Enviorment_variables.png) -- Alternatively, if resources were -provisioned using `azd provision` or `azd up`, a `.env` file is automatically generated in the `.azure//.env` -file. To get your `` run `azd env list` to see which env is default. - -> **Note**: After adding all environment variables to the .env file, update the value of **'APP_ENV'** from: -``` -APP_ENV="Prod" -``` -**to:** -``` -APP_ENV="Dev" -``` - -This change is required for running the application in local development mode. - - -### 5.3. Required Azure RBAC Permissions - -To run the application locally, your Azure account needs the following role assignments on the deployed resources: - -#### 5.3.1. App Configuration Access -```bash -# Get your principal ID -PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) - -# Assign App Configuration Data Reader role -az role assignment create \ - --assignee $PRINCIPAL_ID \ - --role "App Configuration Data Reader" \ - --scope "/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/" -``` - -#### 5.3.2. Cosmos DB Access -```bash -# Assign Cosmos DB Built-in Data Contributor role -az cosmosdb sql role assignment create \ - --account-name \ - --resource-group \ - --role-definition-name "Cosmos DB Built-in Data Contributor" \ - --principal-id $PRINCIPAL_ID \ - --scope "/" -``` -> **Note**: After local deployment is complete, you need to execute the post-deployment script so that all the required roles will be assigned automatically. - -### 5.4. Running with Automated Script - -For convenience, you can use the provided startup scripts that handle environment setup and start both services: - -**Windows:** -```cmd -cd src -.\start.cmd -``` - -**macOS/Linux:** -```bash -cd src -chmod +x start.sh -./start.sh -``` -### 5.5. Start the Application -- Run `start.cmd` (Windows) or `start.sh` (Linux/Mac) to: - - Install backend dependencies. - - Install frontend dependencies. - - Build the frontend. - - Start the backend server. -- Alternatively, you can run the backend in debug mode using the VS Code debug configuration defined in `.vscode/launch.json`. - - -## Step 6: Running Backend and Frontend Separately - -> **📋 Terminal Reminder**: This section requires **two separate terminal windows** - one for the Backend API and one for the Frontend. Keep both terminals open while running. All commands assume you start from the **repository root directory**. - -### 6.1. Create Virtual Environment (Recommended) - -Open your terminal and navigate to the root folder of the project, then create the virtual environment: - -```bash -# Navigate to the project root folder -cd document-generation-solution-accelerator - -# Create virtual environment in the root folder -python -m venv .venv - -# Activate virtual environment (Windows) -.venv/Scripts/activate - -# Activate virtual environment (macOS/Linux) -source .venv/bin/activate -``` - -> **Note**: After activation, you should see `(.venv)` in your terminal prompt indicating the virtual environment is active. - -### 6.2. Install Dependencies and Run - -To develop and run the backend API locally: - -```bash -# Navigate to the API folder (while virtual environment is activated) -cd src/ - -# Upgrade pip -python -m pip install --upgrade pip - -# Install Python dependencies -pip install -r requirements.txt - -# Install Frontend Packages -cd frontend - -npm install -npm run build - -# Run the backend API (Windows) -cd .. - -start http://127.0.0.1:50505 -call python -m uvicorn app:app --port 50505 --reload - -# Run the backend API (MacOs) -cd .. - -open http://127.0.0.1:50505 -python -m uvicorn app:app --port 50505 --reload - -# Run the backend API (Linux) -cd .. - -xdg-open http://127.0.0.1:50505 -python -m uvicorn app:app --port 50505 --reload - -``` - -> **Note**: Make sure your virtual environment is activated before running these commands. You should see `(.venv)` in your terminal prompt when the virtual environment is active. - -The App will run on `http://127.0.0.1:50505/#/` by default. - -## Step 7: Verify All Services Are Running - -Before using the application, confirm all services are running correctly: - -### 7.1. Terminal Status Checklist - -| Terminal | Service | Command | Expected Output | URL | -|----------|---------|---------|-----------------|-----| -| **Terminal 1** | Backend API | `python -m uvicorn app:app --port 50505 --reload` | `INFO: Application startup complete` | http://127.0.0.1:50505 | -| **Terminal 2** | Frontend (Dev) | `npm run dev` | `Local: http://localhost:5173/` | http://localhost:5173 | - -### 7.2. Quick Verification - -**1. Check Backend API:** -```bash -# In a new terminal -curl http://127.0.0.1:50505/health -# Expected: {"status":"healthy"} or similar JSON response -``` - -**2. Check Frontend:** -- Open browser to http://127.0.0.1:50505 (production build) or http://localhost:5173 (dev server) -- Should see the Document Generation UI -- If authentication is configured, you'll be redirected to Azure AD login - -### 7.3. Common Issues - -**Service not starting?** -- Ensure you're in the correct directory (`src/` for backend) -- Verify virtual environment is activated (you should see `(.venv)` in prompt) -- Check that port is not already in use (50505 for API, 5173 for frontend dev) -- Review error messages in the terminal - -**Can't access services?** -- Verify firewall isn't blocking ports 50505 or 5173 -- Try `http://localhost:port` instead of `http://127.0.0.1:port` -- Ensure services show "startup complete" messages - -## Step 8: Next Steps - -Once all services are running (as confirmed in Step 7), you can: - -1. **Access the Application**: Open `http://127.0.0.1:50505` in your browser to explore the Document Generation UI -2. **Explore Sample Questions**: Follow [SampleQuestions.md](SampleQuestions.md) for example prompts and use cases -3. **Understand the Architecture**: Review the codebase starting with `src/backend/` directory - -## Troubleshooting - -### Common Issues - -#### Python Version Issues - -```bash -# Check available Python versions -python3 --version -python3.11 --version - -# If python3.11 not found, install it: -# Ubuntu: sudo apt install python3.11 -# macOS: brew install python@3.11 -# Windows: winget install Python.Python.3.11 -``` - -#### Virtual Environment Issues - -```bash -# Recreate virtual environment -rm -rf .venv # Linux/macOS -# or Remove-Item -Recurse .venv # Windows PowerShell - -uv venv .venv -# Activate and reinstall -source .venv/bin/activate # Linux/macOS -# or .\.venv\Scripts\Activate.ps1 # Windows -uv sync --python 3.11 -``` - -#### Permission Issues (Linux/macOS) - -```bash -# Fix ownership of files -sudo chown -R $USER:$USER . - -# Fix uv permissions -chmod +x ~/.local/bin/uv -``` - -#### Windows-Specific Issues - -```powershell -# PowerShell execution policy -Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -# Long path support (Windows 10 1607+, run as Administrator) -New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force - -# SSL certificate issues -python -m pip install uv -``` - -### Azure Authentication Issues - -```bash -# Login to Azure CLI -az login - -# Set subscription -az account set --subscription "your-subscription-id" - -# Test authentication -az account show -``` - -### Environment Variable Issues - -```bash -# Check environment variables are loaded -env | grep AZURE # Linux/macOS -Get-ChildItem Env:AZURE* # Windows PowerShell - -# Validate .env file format -cat .env | grep -v '^#' | grep '=' # Should show key=value pairs -``` - -## Related Documentation - -- [Deployment Guide](DeploymentGuide.md) - Instructions for production deployment. -- [Delete Resource Group](DeleteResourceGroup.md) - Steps to safely delete the Azure resource group created for the solution. -- [App Authentication Setup](AppAuthentication.md) - Guide to configure application authentication and add support for additional platforms. -- [Powershell Setup](PowershellSetup.md) - Instructions for setting up PowerShell and required scripts. -- [Quota Check](QuotaCheck.md) - Steps to verify Azure quotas and ensure required limits before deployment. diff --git a/docs/LogAnalyticsReplicationDisable.md b/docs/LogAnalyticsReplicationDisable.md index f4379a84a..1f62aa441 100644 --- a/docs/LogAnalyticsReplicationDisable.md +++ b/docs/LogAnalyticsReplicationDisable.md @@ -25,4 +25,4 @@ You can safely delete: - The resource group (manual), or - All provisioned resources via `azd down` -Return to: [Deployment Guide](./DeploymentGuide.md) +Return to: [Deployment Guide](../content-gen/docs/DEPLOYMENT.md) From 639d7c1dfa05fdae71749ba077999ef739fbef7f Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Fri, 13 Feb 2026 15:35:25 +0530 Subject: [PATCH 22/44] moved the quota check file to scripts folder --- content-gen/docs/QuotaCheck.md | 6 +++--- content-gen/infra/{script => scripts}/quota_check_params.sh | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename content-gen/infra/{script => scripts}/quota_check_params.sh (100%) diff --git a/content-gen/docs/QuotaCheck.md b/content-gen/docs/QuotaCheck.md index f19e3d2a2..2fb9b45b9 100644 --- a/content-gen/docs/QuotaCheck.md +++ b/content-gen/docs/QuotaCheck.md @@ -72,7 +72,7 @@ The final table lists regions with available quota. You can select any of these **To check quota for the deployment** ```sh - curl -L -o quota_check_params.sh "https://raw.githubusercontent.com/microsoft/content-generation-solution-accelerator/main/content-gen/infra/script/quota_check_params.sh" + curl -L -o quota_check_params.sh "https://raw.githubusercontent.com/microsoft/content-generation-solution-accelerator/main/content-gen/infra/scripts/quota_check_params.sh" chmod +x quota_check_params.sh ./quota_check_params.sh ``` @@ -82,9 +82,9 @@ The final table lists regions with available quota. You can select any of these 1. Open the terminal in VS Code or Codespaces. 2. If you're using VS Code, click the dropdown on the right side of the terminal window, and select `Git Bash`. ![git_bash](images/git_bash.png) -3. Navigate to the `content-gen/infra/script` folder where the script files are located and make the script as executable: +3. Navigate to the `content-gen/infra/scripts` folder where the script files are located and make the script as executable: ```sh - cd content-gen/infra/script + cd content-gen/infra/scripts chmod +x quota_check_params.sh ``` 4. Run the appropriate script based on your requirement: diff --git a/content-gen/infra/script/quota_check_params.sh b/content-gen/infra/scripts/quota_check_params.sh similarity index 100% rename from content-gen/infra/script/quota_check_params.sh rename to content-gen/infra/scripts/quota_check_params.sh From 7ec373410d169802369174d9f61c36efca051829 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Fri, 13 Feb 2026 16:20:48 +0530 Subject: [PATCH 23/44] updated the default region for ai models --- content-gen/infra/scripts/quota_check_params.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content-gen/infra/scripts/quota_check_params.sh b/content-gen/infra/scripts/quota_check_params.sh index 9baa18b6e..c10e28f61 100644 --- a/content-gen/infra/scripts/quota_check_params.sh +++ b/content-gen/infra/scripts/quota_check_params.sh @@ -84,7 +84,7 @@ az account set --subscription "$AZURE_SUBSCRIPTION_ID" echo "🎯 Active Subscription: $(az account show --query '[name, id]' --output tsv)" # Default Regions supporting GPT-5.1 and GPT-Image-1 with GlobalStandard SKU -DEFAULT_REGIONS="australiaeast,centralus,eastasia,eastus,eastus2,japaneast,northeurope,southeastasia,swedencentral,uksouth,westus,westus3" +DEFAULT_REGIONS="australiaeast,canadaeast,eastus2,japaneast,koreacentral,swedencentral,switzerlandnorth,uksouth" IFS=',' read -r -a DEFAULT_REGION_ARRAY <<< "$DEFAULT_REGIONS" # Read parameters (if any) From 4078e794b368a793dd5ff143031de2995193ab71 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Fri, 13 Feb 2026 16:25:28 +0530 Subject: [PATCH 24/44] Fix formatting issues in orchestrator.py by removing unnecessary blank lines --- content-gen/src/backend/orchestrator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index e1ad74ab5..a5df65421 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1253,7 +1253,7 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non api_version = app_settings.azure_openai.preview_api_version or "2024-02-01" else: api_version = app_settings.azure_openai.image_api_version or "2025-04-01-preview" - + logger.info(f"Calling Foundry direct image API: {image_api_url}") logger.info(f"Prompt: {image_prompt[:200]}...") @@ -1261,7 +1261,7 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non "Authorization": f"Bearer {token.token}", "Content-Type": "application/json", } - + # Build model-appropriate payload if is_dalle3: # dall-e-3: quality must be "standard" or "hd"; needs response_format; 4000-char prompt limit @@ -1280,7 +1280,7 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non "size": "1024x1024", "quality": "medium", } - + async with httpx.AsyncClient(timeout=120.0) as client: response = await client.post( f"{image_api_url}?api-version={api_version}", From 02cd8f1c1aee46172daa92fb0b143f799e868d06 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Fri, 13 Feb 2026 16:35:19 +0530 Subject: [PATCH 25/44] feat: Update AI service location parameters and metadata for deployment --- content-gen/infra/main.bicep | 19 +++- content-gen/infra/main.json | 167 ++++++++++++++++++++++++++++++----- 2 files changed, 162 insertions(+), 24 deletions(-) diff --git a/content-gen/infra/main.bicep b/content-gen/infra/main.bicep index dc6f99946..1c83c573e 100644 --- a/content-gen/infra/main.bicep +++ b/content-gen/infra/main.bicep @@ -36,8 +36,19 @@ param location string @description('Optional. Secondary location for databases creation.') param secondaryLocation string = 'uksouth' +// NOTE: Metadata must be compile-time constants. Update usageName manually if you change model parameters. +// Format: 'OpenAI..,' +@metadata({ + azd: { + type: 'location' + usageName: [ + 'OpenAI.GlobalStandard.gpt-5.1,150' + 'OpenAI.GlobalStandard.gpt-image-1,1' + ] + } +}) @description('Optional. Location for AI deployments. If not specified, uses the main location.') -param azureAiServiceLocation string = '' +param azureAiServiceLocation string @minLength(1) @allowed([ @@ -463,7 +474,7 @@ module aiFoundryAiServices 'br/public:avm/res/cognitive-services/account:0.14.0' name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) params: { name: aiFoundryAiServicesResourceName - location: aiServiceLocation + location: azureAiServiceLocation tags: tags sku: 'S0' kind: 'AIServices' @@ -557,7 +568,7 @@ module aiFoundryAiServicesProject 'modules/ai-project.bicep' = if (!useExistingA name: take('module.ai-project.${aiFoundryAiProjectResourceName}', 64) params: { name: aiFoundryAiProjectResourceName - location: aiServiceLocation + location: azureAiServiceLocation tags: tags desc: aiFoundryAiProjectDescription aiServicesName: aiFoundryAiServicesResourceName @@ -1003,7 +1014,7 @@ output AZURE_AI_AGENT_API_VERSION string = azureAiAgentApiVersion output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = (enableMonitoring && !useExistingLogAnalytics) ? applicationInsights!.outputs.connectionString : '' @description('Contains the location used for AI Services deployment') -output AI_SERVICE_LOCATION string = aiServiceLocation +output AI_SERVICE_LOCATION string = azureAiServiceLocation @description('Contains Container Instance Name') output CONTAINER_INSTANCE_NAME string = containerInstance.outputs.name diff --git a/content-gen/infra/main.json b/content-gen/infra/main.json index 82f7c9b9a..1431a54db 100644 --- a/content-gen/infra/main.json +++ b/content-gen/infra/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "12629934330443935294" + "version": "0.40.2.10011", + "templateHash": "16442623120600133305" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -62,8 +62,14 @@ }, "azureAiServiceLocation": { "type": "string", - "defaultValue": "", "metadata": { + "azd": { + "type": "location", + "usageName": [ + "OpenAI.GlobalStandard.gpt-5.1,150", + "OpenAI.GlobalStandard.gpt-image-1,1" + ] + }, "description": "Optional. Location for AI deployments. If not specified, uses the main location." } }, @@ -321,6 +327,7 @@ "useExistingLogAnalytics": "[not(empty(parameters('existingLogAnalyticsWorkspaceId')))]", "useExistingAiFoundryAiProject": "[not(empty(parameters('azureExistingAIProjectResourceId')))]", "aiFoundryAiServicesResourceGroupName": "[if(variables('useExistingAiFoundryAiProject'), split(parameters('azureExistingAIProjectResourceId'), '/')[4], format('rg-{0}', variables('solutionSuffix')))]", + "aiFoundryAiServicesSubscriptionId": "[if(variables('useExistingAiFoundryAiProject'), split(parameters('azureExistingAIProjectResourceId'), '/')[2], subscription().subscriptionId)]", "aiFoundryAiServicesResourceName": "[if(variables('useExistingAiFoundryAiProject'), split(parameters('azureExistingAIProjectResourceId'), '/')[8], format('aif-{0}', variables('solutionSuffix')))]", "aiFoundryAiProjectResourceName": "[if(variables('useExistingAiFoundryAiProject'), split(parameters('azureExistingAIProjectResourceId'), '/')[10], format('proj-{0}', variables('solutionSuffix')))]", "baseModelDeployments": [ @@ -4868,8 +4875,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "16294706585455769047" + "version": "0.40.2.10011", + "templateHash": "18254349806162838802" } }, "parameters": { @@ -10809,7 +10816,7 @@ "value": "[variables('aiFoundryAiServicesResourceName')]" }, "location": { - "value": "[variables('aiServiceLocation')]" + "value": "[parameters('azureAiServiceLocation')]" }, "tags": { "value": "[parameters('tags')]" @@ -10874,8 +10881,7 @@ }, { "roleDefinitionIdOrName": "53ca6127-db72-4b80-b1b0-d745d6d5456d", - "principalId": "[deployer().objectId]", - "principalType": "User" + "principalId": "[deployer().objectId]" } ] }, @@ -14073,8 +14079,8 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, @@ -14093,7 +14099,7 @@ "value": "[variables('aiFoundryAiProjectResourceName')]" }, "location": { - "value": "[variables('aiServiceLocation')]" + "value": "[parameters('azureAiServiceLocation')]" }, "tags": { "value": "[parameters('tags')]" @@ -14114,8 +14120,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "12336056765515184474" + "version": "0.40.2.10011", + "templateHash": "12454422624133946770" } }, "parameters": { @@ -14223,6 +14229,127 @@ "aiFoundryAiServices" ] }, + "existingAiServicesRoleAssignments": { + "condition": "[variables('useExistingAiFoundryAiProject')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.foundry-role-assignment.{0}', variables('aiFoundryAiServicesResourceName')), 64)]", + "subscriptionId": "[variables('aiFoundryAiServicesSubscriptionId')]", + "resourceGroup": "[variables('aiFoundryAiServicesResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiServicesName": { + "value": "[variables('aiFoundryAiServicesResourceName')]" + }, + "principalId": { + "value": "[reference('userAssignedIdentity').outputs.principalId.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.40.2.10011", + "templateHash": "5825952797275206162" + } + }, + "parameters": { + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the managed identity to grant access." + } + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Required. The name of the existing AI Services account." + } + }, + "aiProjectName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The name of the existing AI Project." + } + }, + "principalType": { + "type": "string", + "defaultValue": "ServicePrincipal", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "metadata": { + "description": "Optional. The principal type of the identity." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d'))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')]", + "principalId": "[parameters('principalId')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]", + "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), parameters('principalId'), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalId": "[parameters('principalId')]", + "principalType": "[parameters('principalType')]" + } + } + ], + "outputs": { + "aiServicesResourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the existing AI Services account." + }, + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName'))]" + }, + "aiServicesEndpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the existing AI Services account." + }, + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('aiServicesName')), '2025-04-01-preview').endpoint]" + }, + "aiProjectPrincipalId": { + "type": "string", + "metadata": { + "description": "The principal ID of the existing AI Project (if provided)." + }, + "value": "[if(not(empty(parameters('aiProjectName'))), reference(resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('aiServicesName'), parameters('aiProjectName')), '2025-04-01-preview', 'full').identity.principalId, '')]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, "aiSearch": { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", @@ -31148,8 +31275,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "11911473200605315360" + "version": "0.40.2.10011", + "templateHash": "4220244191786935721" } }, "definitions": { @@ -32094,7 +32221,7 @@ }, "type": "Microsoft.Insights/diagnosticSettings", "apiVersion": "2021-05-01-preview", - "scope": "[format('Microsoft.Web/sites/{0}', parameters('name'))]", + "scope": "[resourceId('Microsoft.Web/sites', parameters('name'))]", "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", "properties": { "copy": [ @@ -32172,8 +32299,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "13592577410661714505" + "version": "0.40.2.10011", + "templateHash": "11071988482502090044" }, "name": "Site App Settings", "description": "This module deploys a Site App Setting." @@ -33293,8 +33420,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.39.26.7824", - "templateHash": "11247487291315089538" + "version": "0.40.2.10011", + "templateHash": "5126970369332146090" } }, "parameters": { @@ -33687,7 +33814,7 @@ "metadata": { "description": "Contains the location used for AI Services deployment" }, - "value": "[variables('aiServiceLocation')]" + "value": "[parameters('azureAiServiceLocation')]" }, "CONTAINER_INSTANCE_NAME": { "type": "string", From e5dcba5bddbb74ba06d73d8e6d539763f9a2e522 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Fri, 13 Feb 2026 16:35:22 +0530 Subject: [PATCH 26/44] readme update --- content-gen/docs/AZD_DEPLOYMENT.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content-gen/docs/AZD_DEPLOYMENT.md b/content-gen/docs/AZD_DEPLOYMENT.md index f4c5b188e..5901ed689 100644 --- a/content-gen/docs/AZD_DEPLOYMENT.md +++ b/content-gen/docs/AZD_DEPLOYMENT.md @@ -128,7 +128,7 @@ azd env set AZURE_EXISTING_AI_PROJECT_RESOURCE_ID "/subscriptions//resou ```bash # Set the resource ID of your existing Log Analytics workspace -azd env set existingLogAnalyticsWorkspaceId "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" +azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID "/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/" ``` ## Post-Deployment @@ -308,5 +308,6 @@ When `enablePrivateNetworking` is enabled: ## Related Documentation +- [Deployment Guide](DEPLOYMENT.md) - [Image Generation Configuration](IMAGE_GENERATION.md) - [Azure Developer CLI Documentation](https://learn.microsoft.com/azure/developer/azure-developer-cli/) From 43a60c163097319900e6279419c0ccb362c838d4 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Fri, 13 Feb 2026 16:45:08 +0530 Subject: [PATCH 27/44] feat: Update AI service location references in configuration files --- content-gen/azure.yaml | 4 ++-- content-gen/infra/main.bicep | 11 +---------- content-gen/infra/main.json | 8 +++----- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/content-gen/azure.yaml b/content-gen/azure.yaml index 424fb2923..1898b4e37 100644 --- a/content-gen/azure.yaml +++ b/content-gen/azure.yaml @@ -66,7 +66,7 @@ hooks: Write-Host "AI Search Index: " -NoNewline Write-Host "$env:AZURE_AI_SEARCH_PRODUCTS_INDEX" -ForegroundColor Cyan Write-Host "AI Service Location: " -NoNewline - Write-Host "$env:AI_SERVICE_LOCATION" -ForegroundColor Cyan + Write-Host "$env:AZURE_ENV_OPENAI_LOCATION" -ForegroundColor Cyan Write-Host "Container Instance: " -NoNewline Write-Host "$env:CONTAINER_INSTANCE_NAME" -ForegroundColor Cyan @@ -112,7 +112,7 @@ hooks: echo "Storage Account: $AZURE_BLOB_ACCOUNT_NAME" echo "AI Search Service: $AI_SEARCH_SERVICE_NAME" echo "AI Search Index: $AZURE_AI_SEARCH_PRODUCTS_INDEX" - echo "AI Service Location: $AI_SERVICE_LOCATION" + echo "AI Service Location: $AZURE_ENV_OPENAI_LOCATION" echo "Container Instance: $CONTAINER_INSTANCE_NAME" echo "" diff --git a/content-gen/infra/main.bicep b/content-gen/infra/main.bicep index 1c83c573e..bc91b949a 100644 --- a/content-gen/infra/main.bicep +++ b/content-gen/infra/main.bicep @@ -190,15 +190,6 @@ var aiServiceRegionFallback = { westus3: 'westus3' } -// Determine effective AI service location: -// 1. If explicitly set via parameter, use that (user override) -// 2. If main location is valid for AI services, use it -// 3. Otherwise, use the fallback mapping -var requestedAiLocation = empty(azureAiServiceLocation) ? solutionLocation : azureAiServiceLocation -var aiServiceLocation = contains(validAiServiceRegions, requestedAiLocation) - ? requestedAiLocation - : (aiServiceRegionFallback[?solutionLocation] ?? 'eastus2') - // acrName is required - points to existing ACR with pre-built images var acrResourceName = acrName var solutionSuffix = toLower(trim(replace( @@ -1014,7 +1005,7 @@ output AZURE_AI_AGENT_API_VERSION string = azureAiAgentApiVersion output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = (enableMonitoring && !useExistingLogAnalytics) ? applicationInsights!.outputs.connectionString : '' @description('Contains the location used for AI Services deployment') -output AI_SERVICE_LOCATION string = azureAiServiceLocation +output AZURE_ENV_OPENAI_LOCATION string = azureAiServiceLocation @description('Contains Container Instance Name') output CONTAINER_INSTANCE_NAME string = containerInstance.outputs.name diff --git a/content-gen/infra/main.json b/content-gen/infra/main.json index 1431a54db..0a5eee54c 100644 --- a/content-gen/infra/main.json +++ b/content-gen/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.40.2.10011", - "templateHash": "16442623120600133305" + "templateHash": "9008126217899627179" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -289,8 +289,6 @@ "westus2": "westus", "westus3": "westus3" }, - "requestedAiLocation": "[if(empty(parameters('azureAiServiceLocation')), variables('solutionLocation'), parameters('azureAiServiceLocation'))]", - "aiServiceLocation": "[if(contains(variables('validAiServiceRegions'), variables('requestedAiLocation')), variables('requestedAiLocation'), coalesce(tryGet(variables('aiServiceRegionFallback'), variables('solutionLocation')), 'eastus2'))]", "acrResourceName": "[parameters('acrName')]", "solutionSuffix": "[toLower(trim(replace(replace(replace(replace(replace(replace(format('{0}{1}', parameters('solutionName'), parameters('solutionUniqueText')), '-', ''), '_', ''), '.', ''), '/', ''), ' ', ''), '*', '')))]", "cosmosDbZoneRedundantHaRegionPairs": { @@ -14079,8 +14077,8 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", "virtualNetwork" ] }, @@ -33809,7 +33807,7 @@ }, "value": "[if(and(parameters('enableMonitoring'), not(variables('useExistingLogAnalytics'))), reference('applicationInsights').outputs.connectionString.value, '')]" }, - "AI_SERVICE_LOCATION": { + "AZURE_ENV_OPENAI_LOCATION": { "type": "string", "metadata": { "description": "Contains the location used for AI Services deployment" From 00b407184844639f1ce8143aa18cec02ac21d18a Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Fri, 13 Feb 2026 17:01:26 +0530 Subject: [PATCH 28/44] feat: Update AI deployment location parameter to required and remove deprecated region mappings --- content-gen/infra/main.bicep | 59 +----------------------------------- content-gen/infra/main.json | 58 ++--------------------------------- 2 files changed, 4 insertions(+), 113 deletions(-) diff --git a/content-gen/infra/main.bicep b/content-gen/infra/main.bicep index bc91b949a..21bc07425 100644 --- a/content-gen/infra/main.bicep +++ b/content-gen/infra/main.bicep @@ -47,7 +47,7 @@ param secondaryLocation string = 'uksouth' ] } }) -@description('Optional. Location for AI deployments. If not specified, uses the main location.') +@description('Required. Location for AI deployments.') param azureAiServiceLocation string @minLength(1) @@ -133,63 +133,6 @@ param createdBy string = contains(deployer(), 'userPrincipalName')? split(deploy var solutionLocation = empty(location) ? resourceGroup().location : location -// Regions that support GPT-5.1, GPT-Image-1, and text-embedding models with GlobalStandard SKU -// Update this list as Azure expands model availability -var validAiServiceRegions = [ - 'australiaeast' - 'eastus' - 'eastus2' - 'francecentral' - 'japaneast' - 'koreacentral' - 'swedencentral' - 'switzerlandnorth' - 'uaenorth' - 'uksouth' - 'westus' - 'westus3' -] - -// Map regions to recommended AI service regions (for when main region lacks model support) -var aiServiceRegionFallback = { - australiaeast: 'australiaeast' - australiasoutheast: 'australiaeast' - brazilsouth: 'eastus2' - canadacentral: 'eastus2' - canadaeast: 'eastus2' - centralindia: 'uksouth' - centralus: 'eastus2' - eastasia: 'japaneast' - eastus: 'eastus' - eastus2: 'eastus2' - francecentral: 'francecentral' - germanywestcentral: 'swedencentral' - japaneast: 'japaneast' - japanwest: 'japaneast' - koreacentral: 'koreacentral' - koreasouth: 'koreacentral' - northcentralus: 'eastus2' - northeurope: 'swedencentral' - norwayeast: 'swedencentral' - polandcentral: 'swedencentral' - qatarcentral: 'uaenorth' - southafricanorth: 'uksouth' - southcentralus: 'eastus2' - southeastasia: 'japaneast' - southindia: 'uksouth' - swedencentral: 'swedencentral' - switzerlandnorth: 'switzerlandnorth' - uaenorth: 'uaenorth' - uksouth: 'uksouth' - ukwest: 'uksouth' - westcentralus: 'westus' - westeurope: 'swedencentral' - westindia: 'uksouth' - westus: 'westus' - westus2: 'westus' - westus3: 'westus3' -} - // acrName is required - points to existing ACR with pre-built images var acrResourceName = acrName var solutionSuffix = toLower(trim(replace( diff --git a/content-gen/infra/main.json b/content-gen/infra/main.json index 0a5eee54c..e4a9bb7b7 100644 --- a/content-gen/infra/main.json +++ b/content-gen/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.40.2.10011", - "templateHash": "9008126217899627179" + "templateHash": "13692784047172844036" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -70,7 +70,7 @@ "OpenAI.GlobalStandard.gpt-image-1,1" ] }, - "description": "Optional. Location for AI deployments. If not specified, uses the main location." + "description": "Required. Location for AI deployments." } }, "gptModelDeploymentType": { @@ -237,58 +237,6 @@ }, "variables": { "solutionLocation": "[if(empty(parameters('location')), resourceGroup().location, parameters('location'))]", - "validAiServiceRegions": [ - "australiaeast", - "eastus", - "eastus2", - "francecentral", - "japaneast", - "koreacentral", - "swedencentral", - "switzerlandnorth", - "uaenorth", - "uksouth", - "westus", - "westus3" - ], - "aiServiceRegionFallback": { - "australiaeast": "australiaeast", - "australiasoutheast": "australiaeast", - "brazilsouth": "eastus2", - "canadacentral": "eastus2", - "canadaeast": "eastus2", - "centralindia": "uksouth", - "centralus": "eastus2", - "eastasia": "japaneast", - "eastus": "eastus", - "eastus2": "eastus2", - "francecentral": "francecentral", - "germanywestcentral": "swedencentral", - "japaneast": "japaneast", - "japanwest": "japaneast", - "koreacentral": "koreacentral", - "koreasouth": "koreacentral", - "northcentralus": "eastus2", - "northeurope": "swedencentral", - "norwayeast": "swedencentral", - "polandcentral": "swedencentral", - "qatarcentral": "uaenorth", - "southafricanorth": "uksouth", - "southcentralus": "eastus2", - "southeastasia": "japaneast", - "southindia": "uksouth", - "swedencentral": "swedencentral", - "switzerlandnorth": "switzerlandnorth", - "uaenorth": "uaenorth", - "uksouth": "uksouth", - "ukwest": "uksouth", - "westcentralus": "westus", - "westeurope": "swedencentral", - "westindia": "uksouth", - "westus": "westus", - "westus2": "westus", - "westus3": "westus3" - }, "acrResourceName": "[parameters('acrName')]", "solutionSuffix": "[toLower(trim(replace(replace(replace(replace(replace(replace(format('{0}{1}', parameters('solutionName'), parameters('solutionUniqueText')), '-', ''), '_', ''), '.', ''), '/', ''), ' ', ''), '*', '')))]", "cosmosDbZoneRedundantHaRegionPairs": { @@ -14077,8 +14025,8 @@ }, "dependsOn": [ "aiFoundryAiServices", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, From 231f14e0abe46740dc3c0a26cb4cfe15e9120b8d Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Mon, 16 Feb 2026 12:45:44 +0530 Subject: [PATCH 29/44] updated bicep code for dall-e removal --- content-gen/infra/main.bicep | 12 +++--------- content-gen/infra/main.json | 14 ++++---------- content-gen/infra/main.parameters.json | 4 ++-- content-gen/infra/main.waf.parameters.json | 4 ++-- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/content-gen/infra/main.bicep b/content-gen/infra/main.bicep index 21bc07425..b350f59ec 100644 --- a/content-gen/infra/main.bicep +++ b/content-gen/infra/main.bicep @@ -65,11 +65,10 @@ param gptModelName string = 'gpt-5.1' @description('Optional. Version of the GPT model to deploy.') param gptModelVersion string = '2025-11-13' -@description('Optional. Image model to deploy: gpt-image-1, gpt-image-1.5, dall-e-3, or none to skip.') +@description('Optional. Image model to deploy: gpt-image-1, gpt-image-1.5, or none to skip.') @allowed([ 'gpt-image-1' 'gpt-image-1.5' - 'dall-e-3' 'none' ]) param imageModelChoice string = 'gpt-image-1' @@ -86,7 +85,7 @@ param gptModelCapacity int = 150 @minValue(1) @description('Optional. Image model deployment capacity (RPM).') -param dalleModelCapacity int = 1 +param imageModelCapacity int = 1 @description('Optional. Existing Log Analytics Workspace Resource ID.') param existingLogAnalyticsWorkspaceId string = '' @@ -222,11 +221,6 @@ var imageModelConfig = { version: '2025-12-16' sku: 'GlobalStandard' } - 'dall-e-3': { - name: 'dall-e-3' - version: '3.0' - sku: 'Standard' - } none: { name: '' version: '' @@ -242,7 +236,7 @@ var imageModelDeployment = imageModelChoice != 'none' ? [ model: imageModelConfig[imageModelChoice].name sku: { name: imageModelConfig[imageModelChoice].sku - capacity: dalleModelCapacity + capacity: imageModelCapacity } version: imageModelConfig[imageModelChoice].version raiPolicyName: 'Microsoft.Default' diff --git a/content-gen/infra/main.json b/content-gen/infra/main.json index e4a9bb7b7..638e4188f 100644 --- a/content-gen/infra/main.json +++ b/content-gen/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.40.2.10011", - "templateHash": "13692784047172844036" + "templateHash": "8403017938611050215" }, "name": "Intelligent Content Generation Accelerator", "description": "Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework.\n" @@ -106,11 +106,10 @@ "allowedValues": [ "gpt-image-1", "gpt-image-1.5", - "dall-e-3", "none" ], "metadata": { - "description": "Optional. Image model to deploy: gpt-image-1, gpt-image-1.5, dall-e-3, or none to skip." + "description": "Optional. Image model to deploy: gpt-image-1, gpt-image-1.5, or none to skip." } }, "azureOpenaiAPIVersion": { @@ -135,7 +134,7 @@ "description": "Optional. AI model deployment token capacity." } }, - "dalleModelCapacity": { + "imageModelCapacity": { "type": "int", "defaultValue": 1, "minValue": 1, @@ -300,18 +299,13 @@ "version": "2025-12-16", "sku": "GlobalStandard" }, - "dall-e-3": { - "name": "dall-e-3", - "version": "3.0", - "sku": "Standard" - }, "none": { "name": "", "version": "", "sku": "" } }, - "imageModelDeployment": "[if(not(equals(parameters('imageModelChoice'), 'none')), createArray(createObject('format', 'OpenAI', 'name', variables('imageModelConfig')[parameters('imageModelChoice')].name, 'model', variables('imageModelConfig')[parameters('imageModelChoice')].name, 'sku', createObject('name', variables('imageModelConfig')[parameters('imageModelChoice')].sku, 'capacity', parameters('dalleModelCapacity')), 'version', variables('imageModelConfig')[parameters('imageModelChoice')].version, 'raiPolicyName', 'Microsoft.Default')), createArray())]", + "imageModelDeployment": "[if(not(equals(parameters('imageModelChoice'), 'none')), createArray(createObject('format', 'OpenAI', 'name', variables('imageModelConfig')[parameters('imageModelChoice')].name, 'model', variables('imageModelConfig')[parameters('imageModelChoice')].name, 'sku', createObject('name', variables('imageModelConfig')[parameters('imageModelChoice')].sku, 'capacity', parameters('imageModelCapacity')), 'version', variables('imageModelConfig')[parameters('imageModelChoice')].version, 'raiPolicyName', 'Microsoft.Default')), createArray())]", "aiFoundryAiServicesModelDeployment": "[concat(variables('baseModelDeployments'), variables('imageModelDeployment'))]", "aiFoundryAiProjectDescription": "Content Generation AI Foundry Project", "logAnalyticsWorkspaceResourceName": "[format('log-{0}', variables('solutionSuffix'))]", diff --git a/content-gen/infra/main.parameters.json b/content-gen/infra/main.parameters.json index 9370c6250..075a266ee 100644 --- a/content-gen/infra/main.parameters.json +++ b/content-gen/infra/main.parameters.json @@ -26,8 +26,8 @@ "imageModelChoice": { "value": "${imageModelChoice}" }, - "dalleModelCapacity": { - "value": "${dalleModelCapacity}" + "imageModelCapacity": { + "value": "${imageModelCapacity}" }, "embeddingModel": { "value": "${embeddingModel}" diff --git a/content-gen/infra/main.waf.parameters.json b/content-gen/infra/main.waf.parameters.json index d5d8438a2..e34bc97cc 100644 --- a/content-gen/infra/main.waf.parameters.json +++ b/content-gen/infra/main.waf.parameters.json @@ -26,8 +26,8 @@ "imageModelChoice": { "value": "${imageModelChoice}" }, - "dalleModelCapacity": { - "value": "${dalleModelCapacity}" + "imageModelCapacity": { + "value": "${imageModelCapacity}" }, "embeddingModel": { "value": "${embeddingModel}" From 69869bc03ff62eac7524ca7ee700b3a82859b39e Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Mon, 16 Feb 2026 12:46:08 +0530 Subject: [PATCH 30/44] updated workflows for removal of dall-e --- .github/workflows/deploy-ci.yml | 3 +-- .github/workflows/deploy-orchestrator.yml | 2 +- .github/workflows/job-deploy.yml | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-ci.yml b/.github/workflows/deploy-ci.yml index c8562fe08..75453fdb2 100644 --- a/.github/workflows/deploy-ci.yml +++ b/.github/workflows/deploy-ci.yml @@ -109,7 +109,6 @@ on: options: - 'gpt-image-1' - 'gpt-image-1.5' - - 'dall-e-3' - 'none' schedule: @@ -311,7 +310,7 @@ jobs: # Validate and output image_model_choice IMAGE_MODEL="${INPUT_IMAGE_MODEL_CHOICE:-gpt-image-1}" - ALLOWED_MODELS=("gpt-image-1" "gpt-image-1.5" "dall-e-3" "none") + ALLOWED_MODELS=("gpt-image-1" "gpt-image-1.5" "none") if [[ ! " ${ALLOWED_MODELS[@]} " =~ " ${IMAGE_MODEL} " ]]; then echo "❌ ERROR: image_model_choice '$IMAGE_MODEL' is invalid. Allowed: ${ALLOWED_MODELS[*]}" exit 1 diff --git a/.github/workflows/deploy-orchestrator.yml b/.github/workflows/deploy-orchestrator.yml index a48f50aee..81d2db410 100644 --- a/.github/workflows/deploy-orchestrator.yml +++ b/.github/workflows/deploy-orchestrator.yml @@ -62,7 +62,7 @@ on: required: true type: string image_model_choice: - description: 'Image model to deploy (gpt-image-1, gpt-image-1.5, dall-e-3, none)' + description: 'Image model to deploy (gpt-image-1, gpt-image-1.5, none)' required: false default: 'gpt-image-1' type: string diff --git a/.github/workflows/job-deploy.yml b/.github/workflows/job-deploy.yml index 37dfa8395..f021b7d78 100644 --- a/.github/workflows/job-deploy.yml +++ b/.github/workflows/job-deploy.yml @@ -67,7 +67,7 @@ on: default: '' type: string image_model_choice: - description: 'Image model to deploy (gpt-image-1, gpt-image-1.5, dall-e-3, none)' + description: 'Image model to deploy (gpt-image-1, gpt-image-1.5, none)' required: false default: 'gpt-image-1' type: string From 7b4ff40a64b91439667742d3f0c184dcfbfcb911 Mon Sep 17 00:00:00 2001 From: Kingshuk-Microsoft Date: Mon, 16 Feb 2026 16:07:37 +0530 Subject: [PATCH 31/44] fix: Remove unnecessary whitespace in orchestrator and cosmos_service files --- content-gen/src/backend/orchestrator.py | 6 +++--- content-gen/src/backend/services/cosmos_service.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index f61e61c6d..e9bc5120c 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1536,7 +1536,7 @@ async def generate_content( "Failed to parse JSON from markdown code block for image prompt: %s", parse_error, ) - + # Build product description for DALL-E context # Include detailed image descriptions if available for better color accuracy product_description = detailed_image_context if detailed_image_context else product_context @@ -1606,7 +1606,7 @@ async def generate_content( "proceeding without 'violations' / 'requires_modification'.", exc_info=True, ) - + except Exception as e: logger.exception(f"Error generating content: {e}") results["error"] = str(e) @@ -1778,7 +1778,7 @@ async def regenerate_image( "Failed to parse JSON from image modification response fallback: %s", parse_error, ) - + results["image_prompt"] = prompt_text results["message"] = f"Regenerating image: {change_summary}" diff --git a/content-gen/src/backend/services/cosmos_service.py b/content-gen/src/backend/services/cosmos_service.py index 344f9257f..432083075 100644 --- a/content-gen/src/backend/services/cosmos_service.py +++ b/content-gen/src/backend/services/cosmos_service.py @@ -315,7 +315,7 @@ async def get_conversation( user_id, exc, ) - + return None async def save_conversation( From 9551afbc24076f414bebea0b7a83ff102f4fceae Mon Sep 17 00:00:00 2001 From: Kingshuk-Microsoft Date: Mon, 16 Feb 2026 16:18:03 +0530 Subject: [PATCH 32/44] fix: Specify OSError in font loading exception handling --- docs/generate_architecture_png.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/generate_architecture_png.py b/docs/generate_architecture_png.py index 6a684cddd..f2458514a 100644 --- a/docs/generate_architecture_png.py +++ b/docs/generate_architecture_png.py @@ -68,7 +68,7 @@ def draw_service_box(draw, x, y, w, h, title, subtitle="", icon_type="default", try: font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 13) font_sub = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) - except Exception: + except OSError: font_title = ImageFont.load_default() font_sub = ImageFont.load_default() @@ -208,7 +208,7 @@ def main(): # Copyright try: font_copy = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10) - except Exception: + except OSError: font_copy = ImageFont.load_default() draw.text((50, HEIGHT-30), "© 2024 Microsoft Corporation All rights reserved.", fill=TEXT_GRAY, font=font_copy) From 37dd5b001db4dcee789997c4c0b7316b52b5510a Mon Sep 17 00:00:00 2001 From: Kingshuk-Microsoft Date: Mon, 16 Feb 2026 16:40:53 +0530 Subject: [PATCH 33/44] fix: reverted "archive-doc-gen" changes --- archive-doc-gen/src/backend/settings.py | 4 -- .../ChatHistory/ChatHistoryListItem.tsx | 1 + .../ChatHistory/chatHistoryListItem.test.tsx | 3 +- .../src/frontend/src/pages/chat/Chat.tsx | 3 ++ .../tests/e2e-test/pages/draftPage.py | 1 + .../tests/e2e-test/tests/test_st_docgen_tc.py | 39 ++++++++++--------- 6 files changed, 28 insertions(+), 23 deletions(-) diff --git a/archive-doc-gen/src/backend/settings.py b/archive-doc-gen/src/backend/settings.py index 189f3b716..165665428 100644 --- a/archive-doc-gen/src/backend/settings.py +++ b/archive-doc-gen/src/backend/settings.py @@ -267,10 +267,6 @@ class _AzureSearchSettings(BaseSettings, DatasourcePayloadConstructor): env_ignore_empty=True, ) - def __init__(self, *args, settings: "_AppSettings", **data): - # Ensure DatasourcePayloadConstructor.__init__ runs so that _settings is initialized - super().__init__(*args, settings=settings, **data) - _type: Literal["azure_search"] = PrivateAttr(default="azure_search") top_k: int = Field(default=5, serialization_alias="top_n_documents") strictness: int = 3 diff --git a/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx index d62fdb2b3..b14b50039 100644 --- a/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx +++ b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx @@ -7,6 +7,7 @@ import { DialogType, IconButton, ITextField, + ITooltipHostStyles, List, PrimaryButton, Separator, diff --git a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx index bacd0734d..cc61169a8 100644 --- a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx +++ b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx @@ -553,7 +553,8 @@ describe('ChatHistoryListItemCell', () => { }, 10000) test('shows error when trying to rename to an existing title', async () => { - (historyRename as jest.Mock).mockResolvedValueOnce({ + const existingTitle = 'Existing Chat Title' + ;(historyRename as jest.Mock).mockResolvedValueOnce({ ok: false, json: async () => ({ message: 'Title already exists' }) }) diff --git a/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx index 3520e075e..a13b5b569 100644 --- a/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx +++ b/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx @@ -6,6 +6,9 @@ import { Dialog, DialogType, Stack, + IStackTokens, + mergeStyleSets, + IModalStyles, Spinner, SpinnerSize } from '@fluentui/react' diff --git a/archive-doc-gen/tests/e2e-test/pages/draftPage.py b/archive-doc-gen/tests/e2e-test/pages/draftPage.py index 3ddebfd0b..760a54e55 100644 --- a/archive-doc-gen/tests/e2e-test/pages/draftPage.py +++ b/archive-doc-gen/tests/e2e-test/pages/draftPage.py @@ -1,4 +1,5 @@ import time +import os from base.base import BasePage from pytest_check import check from playwright.sync_api import expect diff --git a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py index 172199607..fe3f761eb 100644 --- a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py +++ b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py @@ -6,7 +6,7 @@ import pytest from pytest_check import check from playwright.sync_api import expect -from config.constants import (add_section, browse_question1, browse_question2, browse_question3, +from config.constants import (URL, add_section, browse_question1, browse_question2, browse_question3, browse_question4, browse_question5, generate_question1, invalid_response, invalid_response1, remove_section) from pages.browsePage import BrowsePage @@ -538,6 +538,7 @@ def test_show_hide_chat_history(login_logout, request): page = login_logout home_page = HomePage(page) + browse_page = BrowsePage(page) generate_page = GeneratePage(page) log_capture = io.StringIO() @@ -2502,13 +2503,15 @@ def test_bug_7571_removed_sections_not_returning(request, login_logout): logger.info("Step 4: Enter a prompt to remove sections one by one 'Remove (section name)'") start = time.time() - # Select up to 3 sections to remove from the initial list (avoid removing first section for stability) - indices_to_remove = [1, 2, 3] if initial_count > 3 else list(range(1, initial_count)) - sections_to_remove = [ - initial_sections[idx] - for idx in indices_to_remove - if idx < len(initial_sections) - ] + # Select 3 sections to remove from the initial list + sections_to_remove = [] + + if initial_count >= 3: + # Remove sections at positions 1, 2, and 3 (avoid removing first section for stability) + indices_to_remove = [1, 2, 3] if initial_count > 3 else list(range(1, initial_count)) + for idx in indices_to_remove: + if idx < len(initial_sections): + sections_to_remove.append(initial_sections[idx]) logger.info("Sections selected for removal: %s", sections_to_remove) @@ -3399,7 +3402,7 @@ def test_bug_10177_edit_delete_icons_disabled_during_response(login_logout, requ try: threads.first.wait_for(state="visible", timeout=10000) logger.info("✅ Chat history created and displayed with %d thread(s)", threads.count()) - except Exception: + except: logger.error("❌ Chat history threads not visible after creation") # Try alternative locator threads_alt = page.locator('div[data-list-index]') @@ -3449,7 +3452,7 @@ def test_bug_10177_edit_delete_icons_disabled_during_response(login_logout, requ try: delete_icon.wait_for(state="visible", timeout=2000) is_delete_visible = True - except Exception: + except: is_delete_visible = False is_delete_enabled = delete_icon.is_enabled() if is_delete_visible else False @@ -3461,7 +3464,7 @@ def test_bug_10177_edit_delete_icons_disabled_during_response(login_logout, requ try: edit_icon.wait_for(state="visible", timeout=2000) is_edit_visible = True - except Exception: + except: is_edit_visible = False is_edit_enabled = edit_icon.is_enabled() if is_edit_visible else False @@ -3915,9 +3918,7 @@ def test_bug_16106_tooltip_on_chat_history_hover(login_logout, request): with check: assert thread_count > 0, "No chat history threads found to hover over" - if thread_count <= 0: - logger.warning("Skipping hover action: no chat history threads available.") - else: + if thread_count > 0: # Hover over the first chat thread to trigger tooltip first_thread = history_threads.nth(0) first_thread.hover() @@ -3992,6 +3993,8 @@ def test_bug_16106_tooltip_on_chat_history_hover(login_logout, request): if tooltip_found: logger.info("✅ Tooltip displayed successfully on chat history hover") logger.info("Tooltip text length: %d characters", len(tooltip_text)) + else: + logger.error("❌ BUG FOUND: No tooltip displayed when hovering over chat history") duration = time.time() - start logger.info("Execution Time for 'Verify tooltip': %.2fs", duration) @@ -4008,7 +4011,7 @@ def test_bug_16106_tooltip_on_chat_history_hover(login_logout, request): page.keyboard.press("Escape") page.wait_for_timeout(1000) logger.info("Closed chat history using Escape key") - except Exception: + except: logger.warning("Chat history panel may still be open") logger.info("\n%s", "="*80) @@ -4103,10 +4106,10 @@ def test_bug_26031_validate_empty_spaces_chat_input(login_logout, request): assert current_responses_empty == initial_responses, \ f"BUG: System accepted empty query. Response count changed from {initial_responses} to {current_responses_empty}" - if current_responses_empty != initial_responses: - logger.error("❌ BUG: System accepted empty query and generated response") - else: + if current_responses_empty == initial_responses: logger.info("✅ System did not accept empty query - no response generated") + else: + logger.error("❌ BUG: System accepted empty query and generated response") else: logger.info("✅ Send button is properly disabled for empty input") From 46179e5c58388ad34560488a849fcb7db8d82293 Mon Sep 17 00:00:00 2001 From: Avijit-Microsoft Date: Mon, 16 Feb 2026 18:10:28 +0530 Subject: [PATCH 34/44] Fix formatting and remove unnecessary newline --- archive-doc-gen/src/backend/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/archive-doc-gen/src/backend/settings.py b/archive-doc-gen/src/backend/settings.py index 165665428..87c589c86 100644 --- a/archive-doc-gen/src/backend/settings.py +++ b/archive-doc-gen/src/backend/settings.py @@ -266,7 +266,6 @@ class _AzureSearchSettings(BaseSettings, DatasourcePayloadConstructor): extra="ignore", env_ignore_empty=True, ) - _type: Literal["azure_search"] = PrivateAttr(default="azure_search") top_k: int = Field(default=5, serialization_alias="top_n_documents") strictness: int = 3 From c944258574497631435cd26e04e6096d8cc79a03 Mon Sep 17 00:00:00 2001 From: Avijit-Microsoft Date: Mon, 16 Feb 2026 18:12:39 +0530 Subject: [PATCH 35/44] Fix test for renaming chat title with new error message --- .../ChatHistory/chatHistoryListItem.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx index cc61169a8..a7eccb4c9 100644 --- a/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx +++ b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx @@ -554,10 +554,10 @@ describe('ChatHistoryListItemCell', () => { test('shows error when trying to rename to an existing title', async () => { const existingTitle = 'Existing Chat Title' - ;(historyRename as jest.Mock).mockResolvedValueOnce({ - ok: false, - json: async () => ({ message: 'Title already exists' }) - }) + ; (historyRename as jest.Mock).mockResolvedValueOnce({ + ok: false, + json: async () => ({ message: 'Title already exists' }) + }) renderWithContext(, mockAppState) @@ -570,12 +570,12 @@ describe('ChatHistoryListItemCell', () => { }) const inputItem = screen.getByPlaceholderText(conversation.title) - fireEvent.change(inputItem, { target: { value: 'Another Existing Chat' } }) + fireEvent.change(inputItem, { target: { value: 'Test Chat' } }) fireEvent.keyDown(inputItem, { key: 'Enter', code: 'Enter', charCode: 13 }) await waitFor(() => { - expect(screen.getByText(/Error: could not rename item/i)).toBeInTheDocument() + expect(screen.getByText(/Error: Enter a new title to proceed./i)).toBeInTheDocument() }) }) From 0b5741d475b7897a08ec3ffe985279fad3bd3bb9 Mon Sep 17 00:00:00 2001 From: Avijit-Microsoft Date: Mon, 16 Feb 2026 18:14:12 +0530 Subject: [PATCH 36/44] Clean up test_st_docgen_tc.py by removing unused code Removed unused variables and logging for execution time. --- archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py index fe3f761eb..179bf3f68 100644 --- a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py +++ b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py @@ -2505,7 +2505,6 @@ def test_bug_7571_removed_sections_not_returning(request, login_logout): # Select 3 sections to remove from the initial list sections_to_remove = [] - if initial_count >= 3: # Remove sections at positions 1, 2, and 3 (avoid removing first section for stability) indices_to_remove = [1, 2, 3] if initial_count > 3 else list(range(1, initial_count)) @@ -2740,9 +2739,6 @@ def test_bug_9825_navigate_between_sections(request, login_logout): page.wait_for_timeout(1000) - duration = time.time() - start - logger.info("Execution Time for Step 6: %.2fs", duration) - logger.info("\n" + "="*80) logger.info("✅ TC 10157 Test Summary - Navigate between sections") logger.info("="*80) @@ -4226,4 +4222,4 @@ def test_bug_26031_validate_empty_spaces_chat_input(login_logout, request): logger.info("Test TC 26031 - Empty/Spaces Chat Input Validation completed successfully") finally: - logger.removeHandler(handler) \ No newline at end of file + logger.removeHandler(handler) From c14a2589ffc35bef4db5c485b3c63d6265e83801 Mon Sep 17 00:00:00 2001 From: Avijit-Microsoft Date: Mon, 16 Feb 2026 18:16:22 +0530 Subject: [PATCH 37/44] Fix logger handler removal in test case Ensure logger handler is removed in all cases. From f451a20cce8b7e01539cd1a00d2a5e896a9e80b3 Mon Sep 17 00:00:00 2001 From: Avijit-Microsoft Date: Mon, 16 Feb 2026 18:17:22 +0530 Subject: [PATCH 38/44] Fix logger handler removal in test case From 559611c10bb26fd4d2210dcc353f16dc082216d9 Mon Sep 17 00:00:00 2001 From: Avijit-Microsoft Date: Mon, 16 Feb 2026 18:19:12 +0530 Subject: [PATCH 39/44] Fix logger handler removal in test case From 726e0b4a2a709653a63c6328b657857ab122c205 Mon Sep 17 00:00:00 2001 From: Kingshuk-Microsoft Date: Mon, 16 Feb 2026 18:21:09 +0530 Subject: [PATCH 40/44] fix: remove unnecessary whitespace and logging in test cases --- archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py index fe3f761eb..de55c599a 100644 --- a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py +++ b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py @@ -2505,7 +2505,6 @@ def test_bug_7571_removed_sections_not_returning(request, login_logout): # Select 3 sections to remove from the initial list sections_to_remove = [] - if initial_count >= 3: # Remove sections at positions 1, 2, and 3 (avoid removing first section for stability) indices_to_remove = [1, 2, 3] if initial_count > 3 else list(range(1, initial_count)) @@ -2740,9 +2739,6 @@ def test_bug_9825_navigate_between_sections(request, login_logout): page.wait_for_timeout(1000) - duration = time.time() - start - logger.info("Execution Time for Step 6: %.2fs", duration) - logger.info("\n" + "="*80) logger.info("✅ TC 10157 Test Summary - Navigate between sections") logger.info("="*80) From c9546974b008c5748f778ad01d9b900dcdfa700e Mon Sep 17 00:00:00 2001 From: Kingshuk-Microsoft Date: Mon, 16 Feb 2026 18:25:46 +0530 Subject: [PATCH 41/44] fix: ensure logger handler removal in test case --- archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py index 179bf3f68..de55c599a 100644 --- a/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py +++ b/archive-doc-gen/tests/e2e-test/tests/test_st_docgen_tc.py @@ -4222,4 +4222,4 @@ def test_bug_26031_validate_empty_spaces_chat_input(login_logout, request): logger.info("Test TC 26031 - Empty/Spaces Chat Input Validation completed successfully") finally: - logger.removeHandler(handler) + logger.removeHandler(handler) \ No newline at end of file From 487b0689fbe1709be3ebe282491f7492d27c4f5b Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Mon, 16 Feb 2026 18:26:04 +0530 Subject: [PATCH 42/44] updated backend code for removal of dall-e --- content-gen/src/backend/agents/__init__.py | 3 +- .../src/backend/agents/image_content_agent.py | 77 +++++++++---------- content-gen/src/backend/models.py | 2 +- content-gen/src/backend/orchestrator.py | 47 ++++++----- .../src/backend/services/blob_service.py | 5 +- content-gen/src/backend/settings.py | 24 ++---- 6 files changed, 71 insertions(+), 87 deletions(-) diff --git a/content-gen/src/backend/agents/__init__.py b/content-gen/src/backend/agents/__init__.py index 733e69b7e..ddb833046 100644 --- a/content-gen/src/backend/agents/__init__.py +++ b/content-gen/src/backend/agents/__init__.py @@ -4,9 +4,8 @@ This package provides utility functions used by the orchestrator. """ -from agents.image_content_agent import generate_dalle_image, generate_image +from agents.image_content_agent import generate_image __all__ = [ - "generate_dalle_image", "generate_image", ] diff --git a/content-gen/src/backend/agents/image_content_agent.py b/content-gen/src/backend/agents/image_content_agent.py index 2340aa0a9..c5604ff9c 100644 --- a/content-gen/src/backend/agents/image_content_agent.py +++ b/content-gen/src/backend/agents/image_content_agent.py @@ -1,7 +1,7 @@ -"""Image Content Agent - Generates marketing images via DALL-E 3, gpt-image-1, or gpt-image-1.5. +"""Image Content Agent - Generates marketing images. Provides the generate_image function used by the orchestrator -to create marketing images using either DALL-E 3, gpt-image-1, or gpt-image-1.5. +to create marketing images using the image generation model. """ import logging @@ -14,9 +14,9 @@ logger = logging.getLogger(__name__) -def _truncate_for_dalle(product_description: str, max_chars: int = 1500) -> str: +def _truncate_for_image(product_description: str, max_chars: int = 1500) -> str: """ - Truncate product descriptions to fit DALL-E's 4000 character limit. + Truncate product descriptions for image-generation prompt limits. Extracts the most visually relevant information (colors, hex codes, finishes). Args: @@ -59,12 +59,12 @@ def _truncate_for_dalle(product_description: str, max_chars: int = 1500) -> str: # If still too long, just truncate with ellipsis if len(result) > max_chars: - result = result[:max_chars - 50] + '\n\n[Additional details truncated for DALL-E]' + result = result[:max_chars - 50] + '\n\n[Additional details truncated for image generation]' return result -async def generate_dalle_image( +async def generate_image( prompt: str, product_description: str = "", scene_description: str = "", @@ -95,10 +95,10 @@ async def generate_dalle_image( logger.info(f"Using image generation model: {image_model}") # Use appropriate generator based on model - if image_model in ["gpt-image-1", "gpt-image-1.5"]: - return await _generate_gpt_image(prompt, product_description, scene_description, size, quality) - else: + if image_model.lower().startswith("dall-e"): return await _generate_dalle_image(prompt, product_description, scene_description, size, quality) + else: + return await _generate_gpt_image(prompt, product_description, scene_description, size, quality) async def _generate_dalle_image( @@ -127,9 +127,23 @@ async def _generate_dalle_image( size = size or app_settings.azure_openai.image_size quality = quality or app_settings.azure_openai.image_quality - # DALL-E 3 has a 4000 character limit for prompts + # Map gpt-image values to DALL-E compatible values when needed + quality_mapping = { + "low": "standard", + "medium": "standard", + "high": "hd", + "auto": "standard", + } + quality = quality_mapping.get(quality, quality) + + size_mapping = { + "1536x1024": "1792x1024", + "1024x1536": "1024x1792", + } + size = size_mapping.get(size, size) + # Truncate product descriptions to essential visual info - truncated_product_desc = _truncate_for_dalle(product_description, max_chars=1500) + truncated_product_desc = _truncate_for_image(product_description, max_chars=1500) # Also truncate the main prompt if it's too long main_prompt = prompt[:1000] if len(prompt) > 1000 else prompt @@ -163,11 +177,11 @@ async def _generate_dalle_image( ✓ Professional, polished marketing image """ - # Final safety check - DALL-E 3 has 4000 char limit + # Final safety check before sending to image generation - if prompt is too long, truncate further and warn if len(full_prompt) > 3900: logger.warning(f"Prompt too long ({len(full_prompt)} chars), truncating...") # Reduce product context further - truncated_product_desc = _truncate_for_dalle(product_description, max_chars=800) + truncated_product_desc = _truncate_for_image(product_description, max_chars=800) full_prompt = f"""⚠️ ZERO TEXT IN IMAGE. NO WORDS. NO LETTERS. NO PRODUCT NAMES. Create a PURELY VISUAL marketing image with no text whatsoever. @@ -194,19 +208,18 @@ async def _generate_dalle_image( # Get token for Azure OpenAI token = await credential.get_token("https://cognitiveservices.azure.com/.default") - # Use the dedicated DALL-E endpoint if configured, otherwise fall back to main endpoint - dalle_endpoint = app_settings.azure_openai.dalle_endpoint or app_settings.azure_openai.endpoint - logger.info(f"Using DALL-E endpoint: {dalle_endpoint}") + image_endpoint = app_settings.azure_openai.image_endpoint or app_settings.azure_openai.endpoint + logger.info(f"Using endpoint: {image_endpoint}") client = AsyncAzureOpenAI( - azure_endpoint=dalle_endpoint, + azure_endpoint=image_endpoint, azure_ad_token=token.token, api_version=app_settings.azure_openai.preview_api_version, ) try: response = await client.images.generate( - model=app_settings.azure_openai.dalle_model, + model=app_settings.azure_openai.image_model, prompt=full_prompt, size=size, quality=quality, @@ -247,7 +260,7 @@ async def _generate_gpt_image( """ Generate a marketing image using gpt-image-1 or gpt-image-1.5. - gpt-image models have different capabilities than DALL-E 3: + gpt-image models: - Supports larger prompt sizes - Different size options: 1024x1024, 1536x1024, 1024x1536, auto - Different quality options: low, medium, high, auto @@ -265,27 +278,12 @@ async def _generate_gpt_image( """ brand = app_settings.brand_guidelines - # Use defaults from settings if not provided - # Map DALL-E quality settings to gpt-image-1 or gpt-image-1.5 equivalents if needed + # Image settings size = size or app_settings.azure_openai.image_size quality = quality or app_settings.azure_openai.image_quality - # Map DALL-E quality values to gpt-image-1 or gpt-image-1.5 equivalents - quality_mapping = { - "standard": "medium", - "hd": "high", - } - quality = quality_mapping.get(quality, quality) - - # Map DALL-E sizes to gpt-image-1 or gpt-image-1.5 equivalents if needed - size_mapping = { - "1024x1792": "1024x1536", # Closest equivalent - "1792x1024": "1536x1024", # Closest equivalent - } - size = size_mapping.get(size, size) - # gpt-image-1 can handle larger prompts, so we can include more context - truncated_product_desc = _truncate_for_dalle(product_description, max_chars=3000) + truncated_product_desc = _truncate_for_image(product_description, max_chars=3000) main_prompt = prompt[:2000] if len(prompt) > 2000 else prompt scene_desc = scene_description[:1000] if scene_description and len(scene_description) > 1000 else scene_description @@ -330,9 +328,8 @@ async def _generate_gpt_image( # Get token for Azure OpenAI token = await credential.get_token("https://cognitiveservices.azure.com/.default") - # Use gpt-image-1 specific endpoint if configured, otherwise DALL-E endpoint, otherwise main endpoint + # Use gpt-image-1 specific endpoint if configured, otherwise main endpoint image_endpoint = (app_settings.azure_openai.gpt_image_endpoint - or app_settings.azure_openai.dalle_endpoint or app_settings.azure_openai.endpoint) logger.info(f"Using gpt-image-1 endpoint: {image_endpoint}") @@ -398,5 +395,5 @@ async def _generate_gpt_image( } -# Alias for backwards compatibility -generate_image = generate_dalle_image +# Backward-compatible alias +generate_dalle_image = generate_image diff --git a/content-gen/src/backend/models.py b/content-gen/src/backend/models.py index 5b9a5c79a..f050b30f6 100644 --- a/content-gen/src/backend/models.py +++ b/content-gen/src/backend/models.py @@ -108,7 +108,7 @@ class GeneratedImageContent(BaseModel): """Generated marketing image content with compliance status.""" image_base64: str = Field(description="Base64-encoded image data") image_url: Optional[str] = Field(default=None, description="URL if saved to Blob Storage") - prompt_used: str = Field(description="DALL-E prompt that generated the image") + prompt_used: str = Field(description="Image generation prompt that generated the image") alt_text: str = Field(description="Accessibility alt text for the image") compliance: ComplianceResult = Field(default_factory=ComplianceResult) diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index a5df65421..9402346af 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -432,7 +432,7 @@ def _filter_system_prompt_from_response(response_text: str) -> str: """ IMAGE_CONTENT_INSTRUCTIONS = f"""You are an Image Content Agent for MARKETING IMAGE GENERATION ONLY. -Create detailed image prompts for DALL-E based on marketing requirements. +Create detailed image prompts for GPT-Image based on marketing requirements. Your scope is strictly limited to marketing visuals: product images, ads, social media graphics, and promotional materials. Do not generate images for non-marketing purposes such as personal art, entertainment, or general creative projects. @@ -445,7 +445,7 @@ def _filter_system_prompt_from_response(response_text: str) -> str: - Ensure the prompt aligns with campaign objectives Return JSON with: -- "prompt": Detailed DALL-E prompt +- "prompt": Detailed GPT-Image prompt - "style": Visual style description - "aspect_ratio": Recommended aspect ratio - "notes": Additional considerations @@ -1249,11 +1249,6 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non # Adapt API version and payload to the deployed image model is_dalle3 = image_deployment.lower().startswith("dall-e") - if is_dalle3: - api_version = app_settings.azure_openai.preview_api_version or "2024-02-01" - else: - api_version = app_settings.azure_openai.image_api_version or "2025-04-01-preview" - logger.info(f"Calling Foundry direct image API: {image_api_url}") logger.info(f"Prompt: {image_prompt[:200]}...") @@ -1265,6 +1260,7 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non # Build model-appropriate payload if is_dalle3: # dall-e-3: quality must be "standard" or "hd"; needs response_format; 4000-char prompt limit + api_version = app_settings.azure_openai.preview_api_version or "2024-02-01" payload = { "prompt": image_prompt[:4000], "n": 1, @@ -1274,11 +1270,12 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non } else: # gpt-image-1 / gpt-image-1.5: quality is low/medium/high/auto; no response_format + api_version = app_settings.azure_openai.image_api_version or "2025-04-01-preview" payload = { "prompt": image_prompt, "n": 1, - "size": "1024x1024", - "quality": "medium", + "size": app_settings.azure_openai.image_size or "1024x1024", + "quality": app_settings.azure_openai.image_quality or "medium", } async with httpx.AsyncClient(timeout=120.0) as client: @@ -1505,13 +1502,13 @@ async def generate_content( logger.info("Generating image via Foundry direct API...") await self._generate_foundry_image(image_prompt, results) else: - # Direct mode: use image agent to create prompt, then generate via DALL-E + # Direct mode: use image agent to create prompt, then generate via image generation model image_response = await self._agents["image_content"].run(image_request) results["image_prompt"] = str(image_response) # Extract clean prompt from the response and generate actual image try: - from agents.image_content_agent import generate_dalle_image + from agents.image_content_agent import generate_image # Try to extract a clean prompt from the agent response prompt_text = str(image_response) @@ -1536,13 +1533,13 @@ async def generate_content( exc_info=True ) - # Build product description for DALL-E context + # Build product description for image generation context # Include detailed image descriptions if available for better color accuracy product_description = detailed_image_context if detailed_image_context else product_context - # Generate the actual image using DALL-E - logger.info(f"Generating DALL-E image with prompt: {prompt_text[:200]}...") - image_result = await generate_dalle_image( + # Generate the actual image using image generation model + logger.info(f"Generating image with prompt: {prompt_text[:200]}...") + image_result = await generate_image( prompt=prompt_text, product_description=product_description, scene_description=brief.visual_guidelines @@ -1551,16 +1548,16 @@ async def generate_content( if image_result.get("success"): image_base64 = image_result.get("image_base64") results["image_revised_prompt"] = image_result.get("revised_prompt") - logger.info("DALL-E image generated successfully") + logger.info("Image generated successfully") # Save to blob storage await self._save_image_to_blob(image_base64, results) else: - logger.warning(f"DALL-E image generation failed: {image_result.get('error')}") + logger.warning(f"Image generation failed: {image_result.get('error')}") results["image_error"] = image_result.get("error") except Exception as img_error: - logger.exception(f"Error generating DALL-E image: {img_error}") + logger.exception(f"Error generating image: {img_error}") results["image_error"] = str(img_error) # Run compliance check @@ -1713,7 +1710,7 @@ async def regenerate_image( 3. Maintains the campaign's tone and objectives Return JSON with: -- "prompt": The new DALL-E prompt incorporating the modification +- "prompt": The new image generation prompt incorporating the modification - "style": Visual style description - "change_summary": Brief summary of what was changed """ @@ -1781,12 +1778,12 @@ async def regenerate_image( # Generate the actual image try: - from agents.image_content_agent import generate_dalle_image + from agents.image_content_agent import generate_image product_description = detailed_image_context if detailed_image_context else product_context - logger.info(f"Generating modified DALL-E image: {prompt_text[:200]}...") - image_result = await generate_dalle_image( + logger.info(f"Generating modified image: {prompt_text[:200]}...") + image_result = await generate_image( prompt=prompt_text, product_description=product_description, scene_description=brief.visual_guidelines @@ -1795,14 +1792,14 @@ async def regenerate_image( if image_result.get("success"): image_base64 = image_result.get("image_base64") results["image_revised_prompt"] = image_result.get("revised_prompt") - logger.info("Modified DALL-E image generated successfully") + logger.info("Modified image generated successfully") await self._save_image_to_blob(image_base64, results) else: - logger.warning(f"Modified DALL-E image generation failed: {image_result.get('error')}") + logger.warning(f"Modified image generation failed: {image_result.get('error')}") results["image_error"] = image_result.get("error") except Exception as img_error: - logger.exception(f"Error generating modified DALL-E image: {img_error}") + logger.exception(f"Error generating modified image: {img_error}") results["image_error"] = str(img_error) logger.info(f"Image regeneration complete. Has image: {bool(results.get('image_base64') or results.get('image_blob_url'))}") diff --git a/content-gen/src/backend/services/blob_service.py b/content-gen/src/backend/services/blob_service.py index ae91c53e9..286aa929b 100644 --- a/content-gen/src/backend/services/blob_service.py +++ b/content-gen/src/backend/services/blob_service.py @@ -143,7 +143,7 @@ async def save_generated_image( content_type: str = "image/png" ) -> str: """ - Save a DALL-E generated image to blob storage. + Save the generated image to blob storage. Args: conversation_id: ID of the conversation that generated the image @@ -205,8 +205,7 @@ async def generate_image_description(self, image_data: bytes) -> str: Generate a detailed text description of an image using GPT-5 Vision. This is used to create descriptions of product images that can be - used as context for DALL-E 3 image generation (since DALL-E 3 - cannot accept image inputs directly). + used as context for image generation. Args: image_data: Raw image bytes diff --git a/content-gen/src/backend/settings.py b/content-gen/src/backend/settings.py index 08680ede8..873aedbe0 100644 --- a/content-gen/src/backend/settings.py +++ b/content-gen/src/backend/settings.py @@ -64,12 +64,10 @@ class _AzureOpenAISettings(BaseSettings): model: str = "gpt-5" # Image generation model settings - # Supported models: "dall-e-3" or "gpt-image-1" or "gpt-image-1.5" - image_model: str = Field(default="dall-e-3", alias="AZURE_OPENAI_IMAGE_MODEL") - dalle_model: str = Field(default="dall-e-3", alias="AZURE_OPENAI_DALLE_MODEL") # Legacy alias - dalle_endpoint: Optional[str] = Field(default=None, alias="AZURE_OPENAI_DALLE_ENDPOINT") + # Supported models: "gpt-image-1" or "gpt-image-1.5" + image_model: str = Field(default="gpt-image-1", alias="AZURE_OPENAI_IMAGE_MODEL") - # gpt-image-1 or gpt-image-1.5 specific endpoint (if different from DALL-E endpoint) + # gpt-image-1 or gpt-image-1.5 specific endpoint gpt_image_endpoint: Optional[str] = Field(default=None, alias="AZURE_OPENAI_GPT_IMAGE_ENDPOINT") resource: Optional[str] = None @@ -83,31 +81,25 @@ class _AzureOpenAISettings(BaseSettings): image_api_version: str = Field(default="2025-04-01-preview", alias="AZURE_OPENAI_IMAGE_API_VERSION") # Image generation settings - # For dall-e-3: 1024x1024, 1024x1792, 1792x1024 # For gpt-image-1: 1024x1024, 1536x1024, 1024x1536, auto image_size: str = "1024x1024" - image_quality: str = "hd" # dall-e-3: standard/hd, gpt-image-1: low/medium/high/auto + image_quality: str = "medium" # gpt-image-1/1.5: low/medium/high/auto @property def effective_image_model(self) -> str: - """Get the effective image model, preferring image_model over dalle_model.""" - # If image_model is explicitly set and not the default, use it - # Otherwise fall back to dalle_model for backwards compatibility - return self.image_model if self.image_model else self.dalle_model + """Get the effective image model""" + return self.image_model @property def image_endpoint(self) -> Optional[str]: """Get the appropriate endpoint for the configured image model.""" - if self.effective_image_model in ["gpt-image-1", "gpt-image-1.5"] and self.gpt_image_endpoint: - return self.gpt_image_endpoint - return self.dalle_endpoint + return self.gpt_image_endpoint or self.endpoint @property def image_generation_enabled(self) -> bool: """Check if image generation is available. Image generation requires either: - - A DALL-E endpoint configured, OR - A gpt-image-1 or gpt-image-1.5 endpoint configured, OR - Using the main OpenAI endpoint with an image model configured @@ -119,7 +111,7 @@ def image_generation_enabled(self) -> bool: # Check if we have an endpoint that can handle image generation # Either a dedicated image endpoint or the main OpenAI endpoint - has_image_endpoint = bool(self.dalle_endpoint or self.gpt_image_endpoint or self.endpoint) + has_image_endpoint = bool(self.gpt_image_endpoint or self.endpoint) return has_image_endpoint From d3ae4e07aac95af1d13596885ebaa21ca095cac1 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Tue, 17 Feb 2026 11:28:57 +0530 Subject: [PATCH 43/44] updated the quality and size settings for dall-e --- content-gen/src/backend/orchestrator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py index 9402346af..4c0e58c64 100644 --- a/content-gen/src/backend/orchestrator.py +++ b/content-gen/src/backend/orchestrator.py @@ -1264,8 +1264,8 @@ async def _generate_foundry_image(self, image_prompt: str, results: dict) -> Non payload = { "prompt": image_prompt[:4000], "n": 1, - "size": app_settings.azure_openai.image_size or "1024x1024", - "quality": app_settings.azure_openai.image_quality or "hd", + "size": "1024x1024", + "quality": "hd", "response_format": "b64_json", } else: From d9e7f891f4c517c197cce3cf8914d1c8957d2cd9 Mon Sep 17 00:00:00 2001 From: Ragini-Microsoft Date: Tue, 17 Feb 2026 11:57:04 +0530 Subject: [PATCH 44/44] Updated readme for dall-e removal --- README.md | 6 +- content-gen/.env.sample | 7 +- content-gen/README.md | 16 +- content-gen/docs/AZD_DEPLOYMENT.md | 9 +- content-gen/docs/CustomizingAzdParameters.md | 6 +- content-gen/docs/DEPLOYMENT.md | 19 ++- content-gen/docs/IMAGE_GENERATION.md | 149 ++++++++---------- content-gen/docs/LOCAL_DEPLOYMENT.md | 11 +- content-gen/docs/TRANSPARENCY_FAQ.md | 2 +- content-gen/scripts/checkquota.sh | 4 +- content-gen/scripts/deploy.ps1 | 6 +- content-gen/scripts/deploy.sh | 6 +- .../scripts/sample_image_generation.py | 23 ++- docs/generate_architecture_png.py | 4 +- docs/images/readme/solution_architecture.html | 2 +- 15 files changed, 122 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 9055a8dba..f1435e0b0 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The sample data includes synthetic product catalogs and brand guidelines. The da Parse free-text creative briefs into structured fields (overview, objectives, target audience, key message, tone/style, deliverable, timelines, visual guidelines, CTA). - **Multimodal Content Generation**
- Generate marketing copy and images using GPT models and DALL-E 3 grounded in enterprise product data. + Generate marketing copy and images using GPT models grounded in enterprise product data. - **Brand Compliance Validation**
Validate all generated content against brand guidelines with severity-categorized feedback (Error, Warning, Info). @@ -102,7 +102,7 @@ _Note: This is not meant to outline all costs as selected SKUs, scaled use, cust |---|---|---| | [Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/) | Free tier. Build generative AI applications on an enterprise-grade platform. | [Pricing](https://azure.microsoft.com/pricing/details/ai-studio/) | | [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/blobs/) | Standard tier, LRS. Pricing is based on storage and operations. Blob storage for product images and generated content. | [Pricing](https://azure.microsoft.com/pricing/details/storage/blobs/) | -| [Azure AI Services](https://learn.microsoft.com/en-us/azure/ai-services/) | S0 tier, defaults to gpt-5.1 (GPT) and gpt-image-1 (DALL-E 3) models. Pricing is based on token count. | [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/) | +| [Azure AI Services](https://learn.microsoft.com/en-us/azure/ai-services/) | S0 tier, defaults to gpt-5.1 (GPT) and gpt-image-1 models. Pricing is based on token count. | [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/) | | [Azure Container Instance](https://learn.microsoft.com/en-us/azure/container-instances/) | Backend API hosting with private VNet integration. Pricing is based on resource allocation. | [Pricing](https://azure.microsoft.com/pricing/details/container-instances/) | | [Azure App Service](https://learn.microsoft.com/en-us/azure/app-service/) | B1 tier. Frontend hosting with Node.js proxy server. | [Pricing](https://azure.microsoft.com/pricing/details/app-service/) | | [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/) | Basic tier. Build, store, and manage container images and artifacts in a private registry for all types of container deployments | [Pricing](https://azure.microsoft.com/pricing/details/container-registry/) | @@ -140,7 +140,7 @@ Accelerate your marketing content creation by leveraging AI to interpret creativ Parse unstructured creative briefs into structured fields automatically, ensuring all campaign requirements are captured. - **Generate multimodal content**
- Create marketing copy and images that align with your brand voice and product catalog using GPT and DALL-E 3. + Create marketing copy and images that align with your brand voice and product catalog using GPT models (text and image generation models). - **Ensure brand compliance**
Validate all generated content against brand guidelines with severity-categorized feedback before publication. diff --git a/content-gen/.env.sample b/content-gen/.env.sample index fddde5c59..55c03bc59 100644 --- a/content-gen/.env.sample +++ b/content-gen/.env.sample @@ -31,15 +31,14 @@ AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/ AZURE_OPENAI_GPT_MODEL=gpt-5.1 # Image Generation Model Configuration -# Supported models: dall-e-3 or gpt-image-1 +# Supported models: gpt-image-1 or gpt-image-1.5 AZURE_OPENAI_IMAGE_MODEL=gpt-image-1 -# For gpt-image-1 (if using a different endpoint than DALL-E) +# For gpt-image-1 or gpt-image-1.5, the endpoint is the same as the main OpenAI endpoint, but you can specify a different one if needed AZURE_OPENAI_GPT_IMAGE_ENDPOINT=https://your-openai.openai.azure.com # Image generation settings -# For dall-e-3: sizes are 1024x1024, 1024x1792, 1792x1024; quality is standard or hd -# For gpt-image-1: sizes are 1024x1024, 1536x1024, 1024x1536, auto; quality is low, medium, high, auto +# For gpt-image-1/1.5: sizes are 1024x1024, 1536x1024, 1024x1536, auto; quality is low, medium, high, auto AZURE_OPENAI_IMAGE_SIZE=1024x1024 AZURE_OPENAI_IMAGE_QUALITY=medium diff --git a/content-gen/README.md b/content-gen/README.md index 810e0e2ad..d05fcee8a 100644 --- a/content-gen/README.md +++ b/content-gen/README.md @@ -7,7 +7,7 @@ A multimodal content generation solution for retail marketing campaigns using Mi This accelerator provides an internal chatbot that can: - **Interpret Creative Briefs**: Parse free-text creative briefs into structured fields (overview, objectives, target audience, key message, tone/style, deliverable, timelines, visual guidelines, CTA) -- **Generate Multimodal Content**: Create marketing copy and images using GPT models and DALL-E 3 +- **Generate Multimodal Content**: Create marketing copy and images using GPT models (text and image generation models) - **Ensure Brand Compliance**: Validate all content against brand guidelines with severity-categorized warnings - **Ground in Enterprise Data**: Leverage product information, product images, and brand guidelines stored in Azure services @@ -33,7 +33,7 @@ The solution uses **HandoffBuilder** orchestration with 6 specialized agents: | **PlanningAgent** | Parses creative briefs, develops content strategy, returns for user confirmation | | **ResearchAgent** | Retrieves products from CosmosDB, fetches brand guidelines, assembles grounding data | | **TextContentAgent** | Generates marketing copy (headlines, body, CTAs) using GPT | -| **ImageContentAgent** | Creates marketing images via DALL-E 3 with product context | +| **ImageContentAgent** | Creates marketing images via GPT image models with product context | | **ComplianceAgent** | Validates content against brand guidelines, categorizes violations | ### Compliance Severity Levels @@ -49,7 +49,7 @@ The solution uses **HandoffBuilder** orchestration with 6 specialized agents: | Service | Purpose | |---------|---------| | Azure OpenAI (GPT) | Text generation and content creation | -| Azure OpenAI (DALL-E 3) | Image generation (can be separate resource) | +| Azure OpenAI (GPT Image) | Image generation (can be separate resource) | | Azure Cosmos DB | Products catalog, chat conversations | | Azure Blob Storage | Product images, generated images | | Azure Container Instance | Backend API hosting | @@ -92,7 +92,7 @@ The system extracts the following fields from free-text creative briefs: - Azure subscription with access to: - Azure OpenAI (GPT model - GPT-4 or higher recommended) - - Azure OpenAI (DALL-E 3 - can be same or different resource) + - Azure OpenAI (GPT Image model - can be same or different resource) - Azure Cosmos DB - Azure Blob Storage - Azure Container Instance @@ -156,9 +156,9 @@ See `src/backend/settings.py` for all configuration options. Key settings: | Variable | Description | |----------|-------------| | `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint for GPT model | -| `AZURE_OPENAI_DEPLOYMENT_NAME` | GPT model deployment name | -| `AZURE_OPENAI_DALLE_ENDPOINT` | Azure OpenAI endpoint for DALL-E (if separate) | -| `AZURE_OPENAI_DALLE_DEPLOYMENT` | DALL-E deployment name (dall-e-3) | +| `AZURE_OPENAI_GPT_MODEL` | GPT model deployment name | +| `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` | Azure OpenAI endpoint for GPT image model (if separate) | +| `AZURE_OPENAI_IMAGE_MODEL` | GPT image model deployment name (gpt-image-1) | | `COSMOS_ENDPOINT` | Azure Cosmos DB endpoint | | `COSMOS_DATABASE` | Cosmos DB database name | | `AZURE_STORAGE_ACCOUNT_NAME` | Storage account name | @@ -182,7 +182,7 @@ BRAND_SECONDARY_COLOR=#107C10 - [Local Development Guide](docs/LOCAL_DEPLOYMENT.md) - Run locally for development - [AZD Deployment Guide](docs/AZD_DEPLOYMENT.md) - Deploy with Azure Developer CLI - [Manual Deployment Guide](docs/DEPLOYMENT.md) - Step-by-step manual deployment -- [Image Generation Configuration](docs/IMAGE_GENERATION.md) - DALL-E 3 and GPT-Image-1 setup +- [Image Generation Configuration](docs/IMAGE_GENERATION.md) - GPT image model setup ## License diff --git a/content-gen/docs/AZD_DEPLOYMENT.md b/content-gen/docs/AZD_DEPLOYMENT.md index 5901ed689..45adc1e6a 100644 --- a/content-gen/docs/AZD_DEPLOYMENT.md +++ b/content-gen/docs/AZD_DEPLOYMENT.md @@ -38,15 +38,14 @@ This guide covers deploying the Content Generation Solution Accelerator using Az - An Azure subscription with the following permissions: - Create Resource Groups - - Deploy Azure AI Services (GPT-4o, DALL-E 3 or GPT-Image-1, Text Embeddings) + - Deploy Azure AI Services (GPT-5.1, GPT-Image-1) - Create Container Registry, Container Instances, App Service - Create Cosmos DB, Storage Account, AI Search - Assign RBAC roles - **Quota**: Ensure you have sufficient quota for: - - GPT-4o (or your chosen model) - - DALL-E 3 or GPT-Image-1 (for image generation) - - Text-embedding-3-large + - GPT-5.1 (or your chosen model) + - GPT-Image-1 (or GPT-Image-1.5 - for image generation) ## Quick Start @@ -301,7 +300,7 @@ When `enablePrivateNetworking` is enabled: │ │ │ │ ┌───────▼──────────┐ ┌───────────────────────────────┐ │ │ │ Storage Account │ │ Azure AI Services │ │ -│ └──────────────────┘ │ (GPT-4o, DALL-E, Embeddings) │ │ +│ └──────────────────┘ │ (GPT-5.1, GPT-Image-1) │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` diff --git a/content-gen/docs/CustomizingAzdParameters.md b/content-gen/docs/CustomizingAzdParameters.md index 4198278e8..588698b12 100644 --- a/content-gen/docs/CustomizingAzdParameters.md +++ b/content-gen/docs/CustomizingAzdParameters.md @@ -15,8 +15,8 @@ By default this template will use the environment name as the prefix to prevent | `gptModelVersion` | string | `2025-11-13` | Sets the GPT model version. | | `gptModelDeploymentType` | string | `GlobalStandard` | Defines the model deployment type (allowed: `Standard`, `GlobalStandard`). | | `gptModelCapacity` | integer | `150` | Sets the GPT model token capacity (minimum: `10`). | -| `imageModelChoice` | string | `gpt-image-1` | Image model to deploy (allowed: `gpt-image-1`, `gpt-image-1.5`, `dall-e-3`, `none`). | -| `dalleModelCapacity` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | +| `imageModelChoice` | string | `gpt-image-1` | Image model to deploy (allowed: `gpt-image-1`, `gpt-image-1.5`, `none`). | +| `imageModelCapacity` | integer | `1` | Sets the image model deployment capacity in RPM (minimum: `1`). | | `azureOpenaiAPIVersion` | string | `2025-01-01-preview` | Specifies the API version for Azure OpenAI service. | | `AZURE_ENV_OPENAI_LOCATION` | string | `` | Sets the Azure region for OpenAI resource deployment. | | `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `""` | Reuses an existing Log Analytics Workspace instead of creating a new one. | @@ -38,6 +38,6 @@ azd env set azd env set AZURE_LOCATION westus2 azd env set gptModelName gpt-5.1 azd env set gptModelDeploymentType Standard -azd env set imageModelChoice dall-e-3 +azd env set imageModelChoice gpt-image-1 azd env set ACR_NAME contentgencontainerreg ``` diff --git a/content-gen/docs/DEPLOYMENT.md b/content-gen/docs/DEPLOYMENT.md index 8d4bf2ee5..8f613484b 100644 --- a/content-gen/docs/DEPLOYMENT.md +++ b/content-gen/docs/DEPLOYMENT.md @@ -8,7 +8,6 @@ Check the [Azure Products by Region](https://azure.microsoft.com/en-us/explore/g - [Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry) - [GPT Model Capacity](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models) -- [DALL-E 3 Model Capacity](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#dall-e-models) - [Azure App Service](https://learn.microsoft.com/en-us/azure/app-service/) - [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/) - [Azure Container Instance](https://learn.microsoft.com/en-us/azure/container-instances/) @@ -138,7 +137,7 @@ When you start the deployment, most parameters will have **default values**, but | **GPT Model Version** | The version of the selected GPT model. | 2025-11-13 | | **OpenAI API Version** | The Azure OpenAI API version to use. | 2025-01-01-preview | | **GPT Model Deployment Capacity** | Configure capacity for **GPT models** (in thousands). | 150k | -| **Image Model** | Choose from **dall-e-3, gpt-image-1, gpt-image-1.5** | gpt-image-1 | +| **Image Model** | Choose from **gpt-image-1, gpt-image-1.5** | gpt-image-1 | | **Image Tag** | Docker image tag to deploy. Common values: `latest`, `dev`, `hotfix`. | latest | | **Existing Log Analytics Workspace** | To reuse an existing Log Analytics Workspace ID. | *(empty)* | | **Existing Azure AI Foundry Project** | To reuse an existing Azure AI Foundry Project ID instead of creating a new one. | *(empty)* | @@ -148,11 +147,11 @@ When you start the deployment, most parameters will have **default values**, but

[Optional] Quota Recommendations -By default, the **GPT-4o-mini model capacity** in deployment is set to **30k tokens**, so we recommend updating the following: +By default, the **GPT-5.1 model capacity** in deployment is set to **150k tokens**, so we recommend updating the following: -> **For GPT-4o-mini - increase the capacity to at least 150k tokens post-deployment for optimal performance.** +> **For GPT-5.1 - increase the capacity post-deployment for optimal performance if required.** -> **For DALL-E 3 - ensure you have sufficient capacity for image generation requests.** +> **For GPT-Image-1 - ensure you have sufficient capacity for image generation requests.** Depending on your subscription quota and capacity, you can adjust quota settings to better meet your specific needs. @@ -213,13 +212,13 @@ az webapp config set -g $RESOURCE_GROUP -n --http20-enabled false ### Image Generation Not Working -**Symptom**: DALL-E/GPT-Image requests fail +**Symptom**: GPT-Image requests fail -**Cause**: Missing DALL-E/GPT-Image model deployment or incorrect endpoint +**Cause**: Missing GPT-Image model deployment or incorrect endpoint **Solution**: -1. Verify DALL-E 3 or GPT-Image-1 or GPT-Image-1.5 deployment exists in Azure OpenAI resource -2. Check `AZURE_OPENAI_IMAGE_MODEL` environment variable +1. Verify GPT-Image-1 or GPT-Image-1.5 deployment exists in Azure OpenAI resource +2. Check `AZURE_OPENAI_IMAGE_MODEL` and `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` environment variables
@@ -245,7 +244,7 @@ The solution consists of: - **Frontend**: React + Vite + TypeScript + Fluent UI running on Azure App Service with Node.js proxy - **AI Services**: - Azure OpenAI (GPT model for text generation) - - Azure OpenAI (DALL-E 3 for image generation) + - Azure OpenAI (GPT Image model for image generation) - **Data Services**: - Azure Cosmos DB (products catalog, conversations) - Azure Blob Storage (product images, generated images) diff --git a/content-gen/docs/IMAGE_GENERATION.md b/content-gen/docs/IMAGE_GENERATION.md index 23dfe21f2..51e3e6dcb 100644 --- a/content-gen/docs/IMAGE_GENERATION.md +++ b/content-gen/docs/IMAGE_GENERATION.md @@ -1,39 +1,35 @@ -# DALL-E 3 Image Generation: Limitations and Workarounds +# Image Generation (gpt-image-1 / gpt-image-1.5) ## Overview -This document describes the limitations of DALL-E 3 for image generation in the Intelligent Content Generation Accelerator and the workarounds implemented to achieve product-seeded marketing image generation. +The accelerator supports image generation through Azure OpenAI image models: -## DALL-E 3 Limitations +- `gpt-image-1` +- `gpt-image-1.5` -### Text-Only Input +Both models are used through `images.generate()` in the backend image agent. The selected model is controlled by `AZURE_OPENAI_IMAGE_MODEL`. -**DALL-E 3 only accepts text prompts**. Unlike newer models such as GPT-image-1, DALL-E 3 does not support: +## Current Model Behavior -- Image-to-image generation -- Reference/seed images as input -- Image editing or inpainting with image inputs +### Supported Models -This means you cannot directly pass a product image to DALL-E 3 and ask it to create a marketing image featuring that product. +| Model | Status | Primary Use | +|-------|--------|-------------| +| `gpt-image-1` | Supported | General marketing image generation | +| `gpt-image-1.5` | Supported | Higher-quality marketing image generation | -### API Capabilities +### Prompting Strategy -| Capability | DALL-E 3 | GPT-image-1 | -|------------|----------|-------------| -| Text prompts | ✅ | ✅ | -| Image input | ❌ | ✅ | -| Image editing | ❌ | ✅ | -| Inpainting | ❌ | ✅ | -| Multiple images per request | 1 only | 1-10 | -| Output format | URL or base64 | base64 only | +The `ImageContentAgent` builds a single consolidated prompt from: -## Implemented Workaround +- Product context (including ingestion-time image descriptions) +- Creative brief visual guidance +- Brand guidelines +- Safety and style constraints -### GPT-5 Vision for Product Descriptions +The agent enforces no-text-in-image constraints and color fidelity requirements in the prompt instructions. -To work around DALL-E 3's text-only limitation, we use **GPT-5 Vision** to generate detailed text descriptions of product images during the product ingestion process. - -#### Workflow +## End-to-End Workflow ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ @@ -43,14 +39,14 @@ To work around DALL-E 3's text-only limitation, we use **GPT-5 Vision** to gener │ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Marketing Image │◀────│ DALL-E 3 │◀────│ Combined Prompt │ +│ Marketing Image │◀────│ gpt-image-1/1.5 │◀────│ Combined Prompt │ │ (Output) │ │ (Generate) │ │ (Desc + Brief) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` #### Step 1: Product Image Ingestion -When a product image is uploaded to Blob Storage, the `ProductIngestionService` automatically: +When a product image is uploaded to Blob Storage, ingestion logic can generate a detailed visual description that is later used as image-generation context. 1. Sends the image to GPT-5 Vision 2. Generates a detailed text description including: @@ -89,7 +85,7 @@ async def generate_image_description(image_url: str) -> str: #### Step 2: Marketing Image Generation -The `ImageContentAgent` combines the stored product description with: +The image generation flow combines product description context with: - Creative brief visual guidelines - Brand guidelines (colors, style, composition rules) @@ -100,10 +96,10 @@ async def generate_marketing_image( product: Product, creative_brief: CreativeBrief, brand_guidelines: BrandGuidelines -) -> bytes: - """Generate marketing image using DALL-E 3 with product context.""" +) -> dict: + """Generate marketing image using the configured GPT image model.""" - prompt = f""" + full_prompt = f""" Create a professional marketing image for a retail campaign. PRODUCT (maintain accuracy): @@ -122,23 +118,52 @@ async def generate_marketing_image( - Clean, modern aesthetic - Suitable for {creative_brief.deliverable} """ + + model_name = app_settings.azure_openai.effective_image_model + size = app_settings.azure_openai.image_size or "1024x1024" + quality = app_settings.azure_openai.image_quality or "medium" response = await openai_client.images.generate( - model="dall-e-3", - prompt=prompt, - size="1024x1024", - quality="hd", - n=1 + model=model_name, + prompt=full_prompt, + size=size, + quality=quality, + n=1, ) - - return response.data[0].url + + return {"image_base64": response.data[0].b64_json} ``` +## Configuration + +### Required Environment Variables + +- `AZURE_OPENAI_ENDPOINT` +- `AZURE_OPENAI_GPT_MODEL` +- `AZURE_OPENAI_IMAGE_MODEL` (`gpt-image-1`, `gpt-image-1.5`, or `none`) +- `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` (optional if same as main endpoint) +- `AZURE_OPENAI_API_VERSION` +- `AZURE_OPENAI_IMAGE_API_VERSION` + +### Optional Image Controls + +- `AZURE_OPENAI_IMAGE_SIZE` (for example: `1024x1024`, `1536x1024`, `1024x1536`, `auto`) +- `AZURE_OPENAI_IMAGE_QUALITY` (`low`, `medium`, `high`, `auto`) + +## API Usage Pattern + +The backend image generator calls Azure OpenAI with: + +- `images.generate()` +- `model` set from `AZURE_OPENAI_IMAGE_MODEL` +- prompt text assembled from brief + product + brand constraints +- `size` and `quality` from app settings (or request overrides) + ## Limitations of the Workaround ### Accuracy Trade-offs -1. **Product Representation**: The generated product in the marketing image may not be an exact match to the original product. DALL-E 3 interprets the text description and creates its own version. +1. **Product Representation**: The generated product in the marketing image may not be an exact match to the original product. The image model interprets the text description and creates its own version. 2. **Brand-Specific Details**: Logos, specific patterns, or unique design elements may not be accurately reproduced. @@ -155,53 +180,13 @@ async def generate_marketing_image( | Exact product photography replacement | ❌ Not recommended | | Catalog/technical images | ❌ Not recommended | -## Future Upgrade Path: GPT-image-1 - -### When Available - -GPT-image-1 (currently in limited access preview) will enable true image-to-image generation: - -```python -# Future implementation with GPT-image-1 -async def generate_marketing_image_with_seed( - product_image_path: str, - scene_description: str, - brand_style: str -) -> bytes: - """Generate marketing image seeded with actual product photo.""" - - response = await openai_client.images.edit( - model="gpt-image-1", - image=open(product_image_path, "rb"), # Actual product image as input - prompt=f""" - Create a marketing image featuring the product shown. - Scene: {scene_description} - Brand Style: {brand_style} - Maintain product accuracy. - """, - size="1024x1024", - quality="high", - input_fidelity="high" # Preserve product details - ) - - return base64.b64decode(response.data[0].b64_json) -``` - -### How to Request Access - -1. Visit [GPT-image-1 Access Request](https://aka.ms/oai/gptimage1access) -2. Complete the application form -3. Wait for approval (typically 1-2 weeks) -4. Update the `ImageContentAgent` to use the Image Edit API - -### Migration Steps -When GPT-image-1 access is granted: +### Model Availability Notes -1. Update `AZURE_DALLE_MODEL` environment variable to `gpt-image-1` -2. Modify `ImageContentAgent` to use `images.edit()` instead of `images.generate()` -3. Update Blob Storage retrieval to pass actual image bytes -4. Test with sample products before production deployment +1. Deploy either `gpt-image-1` or `gpt-image-1.5` based on quota and regional availability. +2. Set `AZURE_OPENAI_IMAGE_MODEL` to the deployed model name. +3. If using a separate image endpoint, set `AZURE_OPENAI_GPT_IMAGE_ENDPOINT`. +4. Keep `AZURE_OPENAI_IMAGE_API_VERSION` aligned with the image model API version required by your deployment. ## Best Practices diff --git a/content-gen/docs/LOCAL_DEPLOYMENT.md b/content-gen/docs/LOCAL_DEPLOYMENT.md index 6a12e0f39..ec3daf5ad 100644 --- a/content-gen/docs/LOCAL_DEPLOYMENT.md +++ b/content-gen/docs/LOCAL_DEPLOYMENT.md @@ -148,10 +148,9 @@ Changes to source files will automatically trigger a reload. |----------|----------|-------------| | `AZURE_OPENAI_ENDPOINT` | Yes | Azure OpenAI endpoint URL (e.g., `https://your-resource.openai.azure.com/`) | | `AZURE_OPENAI_GPT_MODEL` | Yes | GPT model deployment name (e.g., `gpt-4o`, `gpt-5.1`) | -| `AZURE_OPENAI_IMAGE_MODEL` | Yes | Image generation model (`dall-e-3` or `gpt-image-1`) | +| `AZURE_OPENAI_IMAGE_MODEL` | Yes | Image generation model (`gpt-image-1` or `gpt-image-1.5`) | | `AZURE_OPENAI_GPT_IMAGE_ENDPOINT` | No | Separate endpoint for gpt-image-1 (if different from main endpoint) | | `AZURE_OPENAI_API_VERSION` | Yes | API version (e.g., `2024-06-01`) | -| `AZURE_OPENAI_PREVIEW_API_VERSION` | No | Preview API version for new features | | `AZURE_OPENAI_TEMPERATURE` | No | Generation temperature (default: `0.7`) | | `AZURE_OPENAI_MAX_TOKENS` | No | Max tokens for generation (default: `2000`) | @@ -162,11 +161,7 @@ Changes to source files will automatically trigger a reload. | `AZURE_OPENAI_IMAGE_SIZE` | `1024x1024` | Image dimensions | | `AZURE_OPENAI_IMAGE_QUALITY` | `medium` | Image quality setting | -**DALL-E 3 Options:** -- Sizes: `1024x1024`, `1024x1792`, `1792x1024` -- Quality: `standard`, `hd` - -**GPT-Image-1 Options:** +**GPT-Image-1/1.5 Options:** - Sizes: `1024x1024`, `1536x1024`, `1024x1536`, `auto` - Quality: `low`, `medium`, `high`, `auto` @@ -376,4 +371,4 @@ az role assignment create \ - [AZD Deployment Guide](AZD_DEPLOYMENT.md) - Deploy to Azure with `azd up` - [Manual Deployment Guide](DEPLOYMENT.md) - Step-by-step Azure deployment -- [Image Generation Configuration](IMAGE_GENERATION.md) - DALL-E 3 and GPT-Image-1 setup +- [Image Generation Configuration](IMAGE_GENERATION.md) - GPT Image model setup diff --git a/content-gen/docs/TRANSPARENCY_FAQ.md b/content-gen/docs/TRANSPARENCY_FAQ.md index 06974bb12..e85fdeffb 100644 --- a/content-gen/docs/TRANSPARENCY_FAQ.md +++ b/content-gen/docs/TRANSPARENCY_FAQ.md @@ -8,7 +8,7 @@ **Key Capabilities:** - Parse free-text creative briefs into structured fields - Generate marketing copy using GPT models - - Generate marketing images using DALL-E 3 + - Generate marketing images using GPT image generation models - Validate content against brand guidelines with severity-categorized compliance checks - Ground content in product catalog data from Cosmos DB diff --git a/content-gen/scripts/checkquota.sh b/content-gen/scripts/checkquota.sh index 9a798b297..21264c17f 100644 --- a/content-gen/scripts/checkquota.sh +++ b/content-gen/scripts/checkquota.sh @@ -11,7 +11,6 @@ # Usage (local): # bash checkquota.sh [image_model_choice] # bash checkquota.sh gpt-image-1 -# bash checkquota.sh dall-e-3 # bash checkquota.sh none # # Usage (CI - via env vars): @@ -49,12 +48,11 @@ declare -A IMAGE_MODEL_QUOTA_NAME IMAGE_MODEL_QUOTA_NAME=( ["gpt-image-1"]="OpenAI.GlobalStandard.gpt-image-1" ["gpt-image-1.5"]="OpenAI.GlobalStandard.gpt-image-1.5" - ["dall-e-3"]="OpenAI.Standard.dall-e-3" ["none"]="" ) # ---- Validate image model choice ---- -ALLOWED_MODELS=("gpt-image-1" "gpt-image-1.5" "dall-e-3" "none") +ALLOWED_MODELS=("gpt-image-1" "gpt-image-1.5" "none") if [[ ! " ${ALLOWED_MODELS[@]} " =~ " ${IMAGE_MODEL_CHOICE} " ]]; then echo "❌ ERROR: Invalid image model choice: '$IMAGE_MODEL_CHOICE'" echo " Allowed values: ${ALLOWED_MODELS[*]}" diff --git a/content-gen/scripts/deploy.ps1 b/content-gen/scripts/deploy.ps1 index 62947c72d..bed9b90a9 100644 --- a/content-gen/scripts/deploy.ps1 +++ b/content-gen/scripts/deploy.ps1 @@ -84,7 +84,7 @@ Write-Host " - Azure App Service (frontend)" Write-Host " - Azure Cosmos DB (products, conversations containers)" Write-Host " - Azure Blob Storage (product-images, generated-images containers)" Write-Host " - Azure OpenAI (GPT model for text generation)" -Write-Host " - Azure OpenAI (DALL-E 3 for image generation - can be separate resource)" +Write-Host " - Azure OpenAI (GPT image model for image generation - can be separate resource)" Write-Host "" $continue = Read-Host "Continue with deployment? (y/n)" @@ -174,11 +174,11 @@ if ($continue -eq "y" -or $continue -eq "Y") { Write-Host " --role 'Cognitive Services OpenAI User' ``" Write-Host " --scope " Write-Host "" - Write-Host "2. Azure OpenAI (DALL-E model - if separate resource):" -ForegroundColor Cyan + Write-Host "2. Azure OpenAI (GPT image model - if separate resource):" -ForegroundColor Cyan Write-Host " Role: Cognitive Services OpenAI User" Write-Host " az role assignment create --assignee $principalId ``" Write-Host " --role 'Cognitive Services OpenAI User' ``" - Write-Host " --scope " + Write-Host " --scope " Write-Host "" Write-Host "3. Azure Cosmos DB:" -ForegroundColor Cyan Write-Host " Role: Cosmos DB Built-in Data Contributor (data plane)" diff --git a/content-gen/scripts/deploy.sh b/content-gen/scripts/deploy.sh index fb5b60c20..10bbbbfa4 100644 --- a/content-gen/scripts/deploy.sh +++ b/content-gen/scripts/deploy.sh @@ -81,7 +81,7 @@ echo " - Azure App Service (frontend)" echo " - Azure Cosmos DB (products, conversations containers)" echo " - Azure Blob Storage (product-images, generated-images containers)" echo " - Azure OpenAI (GPT model for text generation)" -echo " - Azure OpenAI (DALL-E 3 for image generation - can be separate resource)" +echo " - Azure OpenAI (GPT image model for image generation - can be separate resource)" echo "" read -p "Continue with deployment? (y/n) " -n 1 -r @@ -169,11 +169,11 @@ if [[ $REPLY =~ ^[Yy]$ ]]; then echo " --role 'Cognitive Services OpenAI User' \\" echo " --scope " echo "" - echo "2. Azure OpenAI (DALL-E model - if separate resource):" + echo "2. Azure OpenAI (GPT image model - if separate resource):" echo " Role: Cognitive Services OpenAI User" echo " az role assignment create --assignee $PRINCIPAL_ID \\" echo " --role 'Cognitive Services OpenAI User' \\" - echo " --scope " + echo " --scope " echo "" echo "3. Azure Cosmos DB:" echo " Role: Cosmos DB Built-in Data Contributor (data plane)" diff --git a/content-gen/scripts/sample_image_generation.py b/content-gen/scripts/sample_image_generation.py index aa4bc9363..d0520e09b 100644 --- a/content-gen/scripts/sample_image_generation.py +++ b/content-gen/scripts/sample_image_generation.py @@ -3,14 +3,13 @@ Sample Image Generation Script This script demonstrates how to generate marketing images using the -content-gen image generation capabilities (DALL-E 3 or gpt-image-1). +content-gen image generation capabilities (gpt-image-1 or gpt-image-1.5). Prerequisites: 1. Set up environment variables (or use a .env file): - AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint - - AZURE_OPENAI_DALLE_ENDPOINT: (Optional) Dedicated DALL-E endpoint - - AZURE_OPENAI_DALLE_MODEL: Model name (default: dall-e-3) - - AZURE_OPENAI_IMAGE_MODEL: (Optional) Use "gpt-image-1" for GPT Image model + - AZURE_OPENAI_GPT_IMAGE_ENDPOINT: (Optional) Dedicated GPT image endpoint + - AZURE_OPENAI_IMAGE_MODEL: Use "gpt-image-1" or "gpt-image-1.5" 2. Ensure you have RBAC access: - "Cognitive Services OpenAI User" role on the Azure OpenAI resource @@ -34,7 +33,7 @@ sys.path.insert(0, str(backend_path)) # Now import the image generation function -from agents.image_content_agent import generate_dalle_image +from agents.image_content_agent import generate_image from settings import app_settings @@ -64,7 +63,7 @@ async def generate_sample_image( print("IMAGE GENERATION SAMPLE") print(f"{'='*60}") print(f"\nModel: {app_settings.azure_openai.effective_image_model}") - print(f"Endpoint: {app_settings.azure_openai.dalle_endpoint or app_settings.azure_openai.endpoint}") + print(f"Endpoint: {app_settings.azure_openai.image_endpoint or app_settings.azure_openai.endpoint}") print(f"Size: {size or app_settings.azure_openai.image_size}") print(f"Quality: {quality or app_settings.azure_openai.image_quality}") print(f"\nPrompt: {prompt[:200]}{'...' if len(prompt) > 200 else ''}") @@ -79,7 +78,7 @@ async def generate_sample_image( print(f"{'='*60}\n") # Call the image generation function - result = await generate_dalle_image( + result = await generate_image( prompt=prompt, product_description=product_description, scene_description=scene_description, @@ -129,7 +128,7 @@ async def generate_sample_image( async def main(): """Main entry point for the sample script.""" parser = argparse.ArgumentParser( - description="Generate marketing images using DALL-E 3 or gpt-image-1" + description="Generate marketing images using gpt-image-1 or gpt-image-1.5" ) parser.add_argument( "--prompt", "-p", @@ -152,14 +151,14 @@ async def main(): parser.add_argument( "--size", type=str, - choices=["1024x1024", "1024x1792", "1792x1024", "1536x1024", "1024x1536"], + choices=["1024x1024", "1536x1024", "1024x1536", "auto"], default=None, help="Image size (default from settings)" ) parser.add_argument( "--quality", "-q", type=str, - choices=["standard", "hd", "low", "medium", "high"], + choices=["low", "medium", "high", "auto"], default=None, help="Image quality (default from settings)" ) @@ -175,8 +174,8 @@ async def main(): # Check if image generation is enabled if not app_settings.azure_openai.image_generation_enabled: print("❌ Image generation is not configured.") - print(" Please set AZURE_OPENAI_DALLE_ENDPOINT or AZURE_OPENAI_ENDPOINT") - print(" and ensure you have access to a DALL-E 3 or gpt-image-1 model.") + print(" Please set AZURE_OPENAI_GPT_IMAGE_ENDPOINT or AZURE_OPENAI_ENDPOINT") + print(" and ensure you have access to a gpt-image-1 or gpt-image-1.5 model.") sys.exit(1) # Generate the image diff --git a/docs/generate_architecture_png.py b/docs/generate_architecture_png.py index 0f441e1b9..e0a11acdd 100644 --- a/docs/generate_architecture_png.py +++ b/docs/generate_architecture_png.py @@ -153,7 +153,7 @@ def main(): draw_service_box(draw, COL2_X, ROW2_Y, BOX_W, BOX_H, "Container Instance", "Python/Quart API\nBackend", "container", highlight=True) # Azure OpenAI Service - draw_service_box(draw, COL4_X, ROW2_Y, BOX_W+50, BOX_H, "Azure OpenAI", "GPT & DALL-E 3", "ai") + draw_service_box(draw, COL4_X, ROW2_Y, BOX_W+50, BOX_H, "Azure OpenAI", "GPT & GPT-Image", "ai") # === ROW 3: Data Storage === # Blob Storage @@ -217,7 +217,7 @@ def main(): draw.text((50, HEIGHT-30), "© 2024 Microsoft Corporation All rights reserved.", fill=TEXT_GRAY, font=font_copy) # Save image - output_path = "/home/jahunte/content-generation-solution-accelerator/docs/images/readme/solution_architecture.png" + output_path = os.path.join(os.path.dirname(__file__), "images", "readme", "solution_architecture.png") os.makedirs(os.path.dirname(output_path), exist_ok=True) img.save(output_path, "PNG") print(f"Architecture diagram saved to: {output_path}") diff --git a/docs/images/readme/solution_architecture.html b/docs/images/readme/solution_architecture.html index 4759fbde8..93831da41 100644 --- a/docs/images/readme/solution_architecture.html +++ b/docs/images/readme/solution_architecture.html @@ -261,7 +261,7 @@

Content Generation Solution Architecture

Azure OpenAI
-
DALL-E 3 (Image Gen)
+
Gpt-image-1