Skip to content

Commit 10276a4

Browse files
2 parents d261ba0 + 36fcb57 commit 10276a4

File tree

11 files changed

+484
-163
lines changed

11 files changed

+484
-163
lines changed

.github/workflows/broken-links-checker.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
uses: lycheeverse/[email protected]
3838
with:
3939
args: >
40-
--verbose --exclude-mail --no-progress --exclude ^https?://
40+
--verbose --no-progress --exclude ^https?://
4141
${{ steps.changed-markdown-files.outputs.all_changed_files }}
4242
failIfEmpty: false
4343
env:
@@ -50,7 +50,7 @@ jobs:
5050
uses: lycheeverse/[email protected]
5151
with:
5252
args: >
53-
--verbose --exclude-mail --no-progress --exclude ^https?://
53+
--verbose --no-progress --exclude ^https?://
5454
'**/*.md'
5555
failIfEmpty: false
5656
env:

documents/DeploymentGuide.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,7 @@ Follow steps in [Local Debugging Setup](./LocalDebuggingSetup.md) to configure y
243243
244244
## Sample Questions
245245
246-
To help you get started, here are some **Sample Questions** you can ask in the app:
247-
248-
- Total number of calls by date for the last 7 days
249-
- Show average handling time by topics in minutes
250-
- What are the top 7 challenges users reported?
251-
- Give a summary of billing issues
252-
- When customers call in about unexpected charges, what types of charges are they seeing?
253-
254-
These questions serve as a great starting point to explore insights from the data.
246+
To help you get started, here are some [Sample Questions](./SampleQuestions.md) you can follow to try it out.
255247
256248
## Next Steps:
257249
Now that you've completed your deployment, you can start using the solution. Try out these things to start getting familiar with the capabilities:
231 KB
Loading

documents/SampleQuestions.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Sample Questions
2+
3+
To help you get started, here are some **Sample Prompts** you can ask in the app:
4+
5+
> Note: To avoid rate limit errors, pause for 2–3 seconds after a response before submitting the next question. <br>
6+
Average response time is 8–14 seconds.
7+
8+
1. Ask the following questions:
9+
- Total number of calls by date for last 7 days.
10+
- To view the response data as a graph, just prompt "Generate Chart".
11+
- Show average handling time by topics in minutes.
12+
- What are top 7 challenges user reported.
13+
- Give a summary of billing issues.
14+
- When customers call in about unexpected charges, what types of charges are they seeing?
15+
16+
17+
![GenerateDraft](Images/Samplequestions1.png)
18+
19+
20+
This structured approach helps users quickly extract actionable insights from client conversations to help users understand priorities, trends, and opportunities for better engagement.

infra/main.bicep

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -506,11 +506,11 @@ module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-id
506506

507507
// ========== SQL Operations User Assigned Identity ========== //
508508
// Dedicated identity for backend SQL operations with limited permissions (db_datareader, db_datawriter)
509-
var sqlUserAssignedIdentityResourceName = 'id-sql-${solutionSuffix}'
510-
module sqlUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
511-
name: take('avm.res.managed-identity.user-assigned-identity.${sqlUserAssignedIdentityResourceName}', 64)
509+
var backendUserAssignedIdentityResourceName = 'id-backend-${solutionSuffix}'
510+
module backendUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
511+
name: take('avm.res.managed-identity.user-assigned-identity.${backendUserAssignedIdentityResourceName}', 64)
512512
params: {
513-
name: sqlUserAssignedIdentityResourceName
513+
name: backendUserAssignedIdentityResourceName
514514
location: location
515515
tags: tags
516516
enableTelemetry: enableTelemetry
@@ -767,6 +767,11 @@ module aiFoundryAiServices 'modules/ai-services.bicep' = if (aiFoundryAIservices
767767
principalId: userAssignedIdentity.outputs.principalId
768768
principalType: 'ServicePrincipal'
769769
}
770+
{
771+
roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User
772+
principalId: backendUserAssignedIdentity.outputs.principalId
773+
principalType: 'ServicePrincipal'
774+
}
770775
{
771776
roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer
772777
principalId: userAssignedIdentity.outputs.principalId
@@ -777,6 +782,16 @@ module aiFoundryAiServices 'modules/ai-services.bicep' = if (aiFoundryAIservices
777782
principalId: userAssignedIdentity.outputs.principalId
778783
principalType: 'ServicePrincipal'
779784
}
785+
{
786+
roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer
787+
principalId: backendUserAssignedIdentity.outputs.principalId
788+
principalType: 'ServicePrincipal'
789+
}
790+
{
791+
roleDefinitionIdOrName: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User
792+
principalId: backendUserAssignedIdentity.outputs.principalId
793+
principalType: 'ServicePrincipal'
794+
}
780795
]
781796
// WAF aligned configuration for Monitoring
782797
diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null
@@ -946,6 +961,11 @@ module searchSearchServices 'br/public:avm/res/search/search-service:0.11.1' = {
946961
principalId: userAssignedIdentity.outputs.principalId
947962
principalType: 'ServicePrincipal'
948963
}
964+
{
965+
roleDefinitionIdOrName: '1407120a-92aa-4202-b7e9-c0e197c71c8f'
966+
principalId: backendUserAssignedIdentity.outputs.principalId
967+
principalType: 'ServicePrincipal'
968+
}
949969
{
950970
roleDefinitionIdOrName: '1407120a-92aa-4202-b7e9-c0e197c71c8f' // Search Index Data Reader
951971
principalId: !useExistingAiFoundryAiProject ? aiFoundryAiServices.outputs.aiProjectInfo.aiprojectSystemAssignedMIPrincipalId : existingAiFoundryAiServicesProject!.identity.principalId
@@ -1182,7 +1202,7 @@ module cosmosDb 'br/public:avm/res/document-db/database-account:0.15.0' = {
11821202
'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*'
11831203
'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*'
11841204
]
1185-
assignments: [{ principalId: userAssignedIdentity.outputs.principalId }]
1205+
assignments: [{ principalId: backendUserAssignedIdentity.outputs.principalId }]
11861206
}
11871207
]
11881208
// WAF aligned configuration for Monitoring
@@ -1278,6 +1298,7 @@ module sqlDBModule 'br/public:avm/res/sql/server:0.20.1' = {
12781298
systemAssigned: true
12791299
userAssignedResourceIds: [
12801300
userAssignedIdentity.outputs.resourceId
1301+
backendUserAssignedIdentity.outputs.resourceId
12811302
]
12821303
}
12831304
primaryUserAssignedIdentityResourceId: userAssignedIdentity.outputs.resourceId
@@ -1396,8 +1417,8 @@ module createSqlUserAndRole 'br/public:avm/res/resources/deployment-script:0.5.1
13961417
[
13971418
'-SqlServerName \'${sqlServerResourceName}\''
13981419
'-SqlDatabaseName \'${sqlDbModuleName}\''
1399-
'-ClientId \'${sqlUserAssignedIdentity.outputs.clientId}\''
1400-
'-DisplayName \'${sqlUserAssignedIdentity.outputs.name}\''
1420+
'-ClientId \'${backendUserAssignedIdentity.outputs.clientId}\''
1421+
'-DisplayName \'${backendUserAssignedIdentity.outputs.name}\''
14011422
'-DatabaseRoles \'${join(databaseRoles, ',')}\''
14021423
],
14031424
' '
@@ -1432,7 +1453,7 @@ module webServerFarm 'br/public:avm/res/web/serverfarm:0.5.0' = {
14321453
diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null
14331454
// WAF aligned configuration for Scalability
14341455
skuName: enableScalability || enableRedundancy ? 'P1v3' : 'B3'
1435-
skuCapacity: enableScalability ? 3 : 1
1456+
skuCapacity: enableScalability ? 1 : 1
14361457
// WAF aligned configuration for Redundancy
14371458
zoneRedundant: enableRedundancy ? true : false
14381459
}
@@ -1513,7 +1534,7 @@ module webSiteBackend 'modules/web-sites.bicep' = {
15131534
managedIdentities: {
15141535
systemAssigned: true
15151536
userAssignedResourceIds: [
1516-
userAssignedIdentity.outputs.resourceId
1537+
backendUserAssignedIdentity.outputs.resourceId
15171538
]
15181539
}
15191540
siteConfig: {
@@ -1539,7 +1560,7 @@ module webSiteBackend 'modules/web-sites.bicep' = {
15391560
AZURE_COSMOSDB_ENABLE_FEEDBACK: 'True'
15401561
SQLDB_DATABASE: 'sqldb-${solutionSuffix}'
15411562
SQLDB_SERVER: '${sqlDBModule.outputs.name }${environment().suffixes.sqlServerHostname}'
1542-
SQLDB_USER_MID: sqlUserAssignedIdentity.outputs.clientId
1563+
SQLDB_USER_MID: backendUserAssignedIdentity.outputs.clientId
15431564
AZURE_AI_SEARCH_ENDPOINT: 'https://${aiSearchName}.search.windows.net'
15441565
AZURE_AI_SEARCH_INDEX: 'call_transcripts_index'
15451566
AZURE_AI_SEARCH_CONNECTION_NAME: aiSearchName
@@ -1549,7 +1570,7 @@ module webSiteBackend 'modules/web-sites.bicep' = {
15491570
DUMMY_TEST: 'True'
15501571
SOLUTION_NAME: solutionSuffix
15511572
APP_ENV: 'Prod'
1552-
AZURE_CLIENT_ID: userAssignedIdentity.outputs.clientId
1573+
AZURE_CLIENT_ID: backendUserAssignedIdentity.outputs.clientId
15531574
}
15541575
// WAF aligned configuration for Monitoring
15551576
applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null
@@ -1685,7 +1706,7 @@ output SQLDB_DATABASE string = 'sqldb-${solutionSuffix}'
16851706
output SQLDB_SERVER string = '${sqlDBModule.outputs.name }${environment().suffixes.sqlServerHostname}'
16861707

16871708
@description('Contains SQL database user managed identity client ID.')
1688-
output SQLDB_USER_MID string = sqlUserAssignedIdentity.outputs.clientId
1709+
output SQLDB_USER_MID string = backendUserAssignedIdentity.outputs.clientId
16891710

16901711
@description('Contains AI project client usage setting.')
16911712
output USE_AI_PROJECT_CLIENT string = 'False'

tests/e2e-test/base/base.py

Lines changed: 116 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,141 @@
1-
from config.constants import *
2-
import requests
1+
"""
2+
BasePage Module
3+
Contains base page object class with common methods
4+
"""
35
import json
4-
from dotenv import load_dotenv
6+
import logging
57
import os
8+
import time
69
import uuid
10+
from dotenv import load_dotenv
11+
from config.constants import API_URL
12+
13+
logger = logging.getLogger(__name__)
14+
715

816
class BasePage:
17+
"""Base class for all page objects"""
18+
919
def __init__(self, page):
20+
"""Initialize BasePage with page instance"""
1021
self.page = page
1122

12-
def scroll_into_view(self,locator):
23+
def scroll_into_view(self, locator):
24+
"""Scroll element into view"""
1325
reference_list = locator
1426
locator.nth(reference_list.count()-1).scroll_into_view_if_needed()
1527

16-
def is_visible(self,locator):
17-
locator.is_visible()
28+
def is_visible(self, locator):
29+
"""Check if element is visible"""
30+
return locator.is_visible()
1831

19-
def validate_response_status(self,questions):
32+
def validate_response_status(self, question): # pylint: disable=too-many-locals,too-many-statements
33+
"""
34+
Validate that the API responds with status 200 for the given question.
35+
Uses Playwright's request context which maintains authentication from the browser session.
36+
"""
2037
load_dotenv()
21-
WEB_URL = os.getenv("web_url")
22-
23-
url = f"{API_URL}/api/chat"
24-
38+
39+
url = f"{API_URL}/history/update"
2540

2641
user_message_id = str(uuid.uuid4())
27-
assistant_message_id = str(uuid.uuid4())
2842
conversation_id = str(uuid.uuid4())
2943

3044
payload = {
31-
"messages": [{"role": "user", "content": questions,
32-
"id": user_message_id}],
45+
"messages": [{"role": "assistant", "content": question, "id": user_message_id}],
3346
"conversation_id": conversation_id,
3447
}
35-
# Serialize the payload to JSON
36-
payload_json = json.dumps(payload)
48+
3749
headers = {
38-
"Content-Type": "application/json-lines",
50+
"Content-Type": "application/json",
3951
"Accept": "*/*"
4052
}
41-
response = self.page.request.post(url, headers=headers, data=payload_json)
42-
# Check the response status code
43-
assert response.status == 200, "response code is " + str(response.status)
4453

45-
self.page.wait_for_timeout(10000)
54+
# Log request details for debugging
55+
logger.info("=" * 80)
56+
logger.info("🔍 API REQUEST DEBUG INFO")
57+
logger.info("=" * 80)
58+
logger.info("URL: %s", url)
59+
logger.info("Method: POST")
60+
logger.info("Headers: %s", json.dumps(headers, indent=2))
61+
logger.info("Payload: %s", json.dumps(payload, indent=2))
62+
logger.info("Question: %s", question)
63+
64+
start = time.time()
65+
66+
try:
67+
# Using Playwright's request context to leverage browser's authentication
68+
response = self.page.request.post(
69+
url,
70+
headers=headers,
71+
data=json.dumps(payload),
72+
timeout=90000
73+
)
74+
75+
duration = time.time() - start
76+
77+
# Log response details for debugging
78+
logger.info("-" * 80)
79+
logger.info("📥 API RESPONSE DEBUG INFO")
80+
logger.info("-" * 80)
81+
logger.info("Status Code: %s", response.status)
82+
logger.info("Response Time: %.2fs", duration)
83+
84+
# Log response headers
85+
try:
86+
response_headers = response.headers
87+
logger.info("Response Headers: %s", json.dumps(dict(response_headers), indent=2))
88+
except Exception as exc: # pylint: disable=broad-exception-caught
89+
logger.warning("Could not get response headers: %s", str(exc))
90+
91+
# Get response body for debugging
92+
try:
93+
response_body = response.json()
94+
logger.info("Response Body (JSON): %s", json.dumps(response_body, indent=2))
95+
96+
# If there's an error in the response, log it prominently
97+
if "error" in response_body:
98+
logger.error("🚨 API ERROR MESSAGE: %s", response_body.get("error"))
99+
if "detail" in response_body:
100+
logger.error("🚨 API ERROR DETAIL: %s", response_body.get("detail"))
101+
102+
except Exception as exc: # pylint: disable=broad-exception-caught
103+
logger.warning("Could not parse response body as JSON: %s", str(exc))
104+
try:
105+
response_text = response.text()
106+
# First 500 chars
107+
logger.info("Response Body (Text): %s", response_text[:500])
108+
except Exception as text_error: # pylint: disable=broad-exception-caught
109+
logger.error("Could not get response text: %s", str(text_error))
110+
111+
# Assert successful response
112+
if response.status != 200:
113+
error_msg = f"API returned status {response.status} instead of 200"
114+
logger.error("❌ %s", error_msg)
115+
logger.error("💡 POSSIBLE REASONS FOR 500 ERROR:")
116+
logger.error(
117+
" 1. Missing 'conversation_id' in payload (endpoint expects existing conversation)"
118+
)
119+
logger.error(" 2. Authentication/authorization issue")
120+
logger.error(" 3. Invalid payload structure")
121+
logger.error(" 4. Backend service error")
122+
logger.error(" 5. Database connection issue")
123+
logger.warning(
124+
"⚠️ Warning: %s - Continuing with test (UI validation is primary)",
125+
error_msg
126+
)
127+
else:
128+
logger.info("✅ API succeeded in %.2fs", duration)
129+
130+
except Exception as exc: # pylint: disable=broad-exception-caught
131+
duration = time.time() - start
132+
logger.error("❌ API request failed after %.2fs", duration)
133+
logger.error("Exception Type: %s", type(exc).__name__)
134+
logger.error("Exception Message: %s", str(exc))
135+
import traceback # pylint: disable=import-outside-toplevel
136+
logger.error("Stack Trace:\n%s", traceback.format_exc())
137+
logger.warning("⚠️ Warning: API validation failed - Continuing with test")
46138

139+
logger.info("=" * 80)
140+
# Wait for UI to settle
141+
self.page.wait_for_timeout(6000)

tests/e2e-test/config/constants.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
from dotenv import load_dotenv
2-
import os
1+
"""
2+
Constants Module
3+
Contains configuration constants and loads test data
4+
"""
35
import json
6+
import os
7+
from dotenv import load_dotenv
48

59
load_dotenv()
610
URL = os.getenv('url')
@@ -19,7 +23,6 @@
1923
#note: may have to remove 'tests/e2e-test' from below when running locally
2024
json_file_path = os.path.join(repo_root, 'tests/e2e-test', 'testdata', 'prompts.json')
2125

22-
with open(json_file_path, 'r') as file:
26+
with open(json_file_path, 'r', encoding='utf-8') as file:
2327
data = json.load(file)
2428
questions = data['questions']
25-

0 commit comments

Comments
 (0)