diff --git a/ClientAdvisor/App/WebApp.Dockerfile b/ClientAdvisor/App/WebApp.Dockerfile index 83bc3075..8d14a1dd 100644 --- a/ClientAdvisor/App/WebApp.Dockerfile +++ b/ClientAdvisor/App/WebApp.Dockerfile @@ -18,11 +18,16 @@ RUN apk add --no-cache --virtual .build-deps \ libffi-dev \ openssl-dev \ curl \ + unixodbc-dev \ && apk add --no-cache \ - libpq + libpq \ + && curl -O https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/msodbcsql18_18.4.1.1-1_amd64.apk \ + && apk add --allow-untrusted msodbcsql18_18.4.1.1-1_amd64.apk \ + && rm msodbcsql18_18.4.1.1-1_amd64.apk COPY ./ClientAdvisor/App/requirements.txt /usr/src/app/ -RUN pip install --upgrade pip setuptools wheel \ + +RUN pip install --upgrade pip setuptools wheel \ && pip install --no-cache-dir -r /usr/src/app/requirements.txt \ && rm -rf /root/.cache diff --git a/ClientAdvisor/App/app.py b/ClientAdvisor/App/app.py index 2755d75b..4cc9011d 100644 --- a/ClientAdvisor/App/app.py +++ b/ClientAdvisor/App/app.py @@ -24,6 +24,7 @@ format_stream_response, generateFilterString, parse_multi_columns) from db import get_connection +from db import dict_cursor bp = Blueprint("routes", __name__, static_folder="static", template_folder="static") @@ -1583,7 +1584,8 @@ def get_users(): ORDER BY NextMeeting ASC; """ cursor.execute(sql_stmt) - rows = cursor.fetchall() + # Since pyodbc returns query results as a list of tuples, using `dict_cursor` function to convert these tuples into a list of dictionaries + rows = dict_cursor(cursor) if len(rows) <= 6: # update ClientMeetings,Assets,Retirement tables sample data to current date @@ -1618,7 +1620,8 @@ def get_users(): FROM DaysDifference """ cursor.execute(combined_stmt) - date_diff_rows = cursor.fetchall() + # Since pyodbc returns query results as a list of tuples, using `dict_cursor` function to convert these tuples into a list of dictionaries + date_diff_rows = dict_cursor(cursor) client_days = ( date_diff_rows[0]["ClientMeetingDaysDifference"] diff --git a/ClientAdvisor/App/db.py b/ClientAdvisor/App/db.py index 536bb940..d0a81bec 100644 --- a/ClientAdvisor/App/db.py +++ b/ClientAdvisor/App/db.py @@ -1,20 +1,60 @@ # db.py import os -import pymssql from dotenv import load_dotenv +from azure.identity import DefaultAzureCredential +import pyodbc +import struct +import logging + load_dotenv() +driver = "{ODBC Driver 18 for SQL Server}" server = os.environ.get("SQLDB_SERVER") database = os.environ.get("SQLDB_DATABASE") username = os.environ.get("SQLDB_USERNAME") password = os.environ.get("SQLDB_PASSWORD") +mid_id = os.environ.get("SQLDB_USER_MID") + + +def dict_cursor(cursor): + """ + Converts rows fetched by the cursor into a list of dictionaries. + + Args: + cursor: A database cursor object. + + Returns: + A list of dictionaries representing rows. + """ + columns = [column[0] for column in cursor.description] + return [dict(zip(columns, row)) for row in cursor.fetchall()] def get_connection(): + try: + credential = DefaultAzureCredential(managed_identity_client_id=mid_id) + + token_bytes = credential.get_token( + "https://database.windows.net/.default" + ).token.encode("utf-16-LE") + token_struct = struct.pack(f" { test('renders the chat history item', () => { renderWithContext(, mockAppState) - const titleElement = screen.getByText(/Test Chat/i) - expect(titleElement).toBeInTheDocument() + const titleElement = screen.getAllByText(/Test Chat/i) + expect(titleElement.length).toBeGreaterThan(1) }) test('truncates long title', () => { @@ -50,7 +50,7 @@ describe('ChatHistoryListItemCell', () => { renderWithContext(, mockAppState) - const truncatedTitle = screen.getByText(/A very long title that shoul .../i) + const truncatedTitle = screen.getByText(/A very long title that s .../i) expect(truncatedTitle).toBeInTheDocument() }) diff --git a/ClientAdvisor/App/frontend/src/components/ChatHistory/ChatHistoryListItemCell.tsx b/ClientAdvisor/App/frontend/src/components/ChatHistory/ChatHistoryListItemCell.tsx index b9b2017d..e4209908 100644 --- a/ClientAdvisor/App/frontend/src/components/ChatHistory/ChatHistoryListItemCell.tsx +++ b/ClientAdvisor/App/frontend/src/components/ChatHistory/ChatHistoryListItemCell.tsx @@ -7,6 +7,7 @@ import { DialogType, IconButton, ITextField, + ITooltipHostStyles, List, PrimaryButton, Separator, @@ -14,7 +15,9 @@ import { SpinnerSize, Stack, Text, - TextField + TextField, + TooltipHost, + DirectionalHint, } from '@fluentui/react' import { useBoolean } from '@fluentui/react-hooks' @@ -107,7 +110,7 @@ export const ChatHistoryListItemCell: React.FC = ( appStateContext?.dispatch({ type: 'UPDATE_CURRENT_CHAT', payload: item }) } - const truncatedTitle = item?.title?.length > 28 ? `${item.title.substring(0, 28)} ...` : item.title + const truncatedTitle = item?.title?.length > 24 ? `${item.title.substring(0, 24)} ...` : item.title const handleSaveEdit = async (e: any) => { e.preventDefault() @@ -236,8 +239,15 @@ export const ChatHistoryListItemCell: React.FC = ( ) : ( <> - -
{truncatedTitle}
+ + + +
{truncatedTitle}
+
+
{(isSelected || isHovered) && ( =8.2,<9 # Compatible version for pytest-asyncio -pytest-asyncio==0.25.3 -pytest-cov==6.0.0 \ No newline at end of file +pytest-asyncio==0.24.0 +pytest-cov==5.0.0 + +pyodbc==5.2.0 \ No newline at end of file diff --git a/ClientAdvisor/App/tests/test_app.py b/ClientAdvisor/App/tests/test_app.py index dd7f9347..0740c6a9 100644 --- a/ClientAdvisor/App/tests/test_app.py +++ b/ClientAdvisor/App/tests/test_app.py @@ -200,50 +200,60 @@ async def test_ensure_cosmos_generic_exception(mock_init_cosmosdb_client, client @pytest.mark.asyncio -async def test_get_users_success(client): +@patch("app.get_connection") +@patch("app.dict_cursor") +async def test_get_users_success(mock_dict_cursor, mock_get_connection, client): + # Mock database connection and cursor mock_conn = MagicMock() mock_cursor = MagicMock() + mock_get_connection.return_value = mock_conn mock_conn.cursor.return_value = mock_cursor - mock_cursor.fetchall.return_value = [ - { - "ClientId": 1, - "ndays": 10, - "ClientMeetingDaysDifference": 1, - "AssetMonthsDifference": 1, - "StatusMonthsDifference": 1, - "DaysDifference": 1, - "Client": "Client A", - "Email": "clienta@example.com", - "AssetValue": "1,000,000", - "ClientSummary": "Summary A", - "LastMeetingDateFormatted": "Monday January 1, 2023", - "LastMeetingStartTime": "10:00 AM", - "LastMeetingEndTime": "10:30 AM", - "NextMeetingFormatted": "Monday January 8, 2023", - "NextMeetingStartTime": "11:00 AM", - "NextMeetingEndTime": "11:30 AM", - } - ] - with patch("app.get_connection", return_value=mock_conn): - response = await client.get("/api/users") - assert response.status_code == 200 - res_text = await response.get_data(as_text=True) - assert json.loads(res_text) == [ + # Mock query results + mock_dict_cursor.side_effect = [ + [ # First call (client data) { "ClientId": 1, - "ClientName": "Client A", - "ClientEmail": "clienta@example.com", + "Client": "Client A", + "Email": "clienta@example.com", "AssetValue": "1,000,000", - "NextMeeting": "Monday January 8, 2023", - "NextMeetingTime": "11:00 AM", - "NextMeetingEndTime": "11:30 AM", - "LastMeeting": "Monday January 1, 2023", + "ClientSummary": "Summary A", + "LastMeetingDateFormatted": "Monday January 1, 2023", "LastMeetingStartTime": "10:00 AM", "LastMeetingEndTime": "10:30 AM", - "ClientSummary": "Summary A", + "NextMeetingFormatted": "Monday January 8, 2023", + "NextMeetingStartTime": "11:00 AM", + "NextMeetingEndTime": "11:30 AM", + } + ], + [ # Second call (date difference query) + { + "ClientMeetingDaysDifference": 5, + "AssetMonthsDifference": 1, + "StatusMonthsDifference": 1 } ] + ] + + # Call the function + response = await client.get("/api/users") + assert response.status_code == 200 + res_text = await response.get_data(as_text=True) + assert json.loads(res_text) == [ + { + "ClientId": 1, + "ClientName": "Client A", + "ClientEmail": "clienta@example.com", + "AssetValue": "1,000,000", + "NextMeeting": "Monday January 8, 2023", + "NextMeetingTime": "11:00 AM", + "NextMeetingEndTime": "11:30 AM", + "LastMeeting": "Monday January 1, 2023", + "LastMeetingStartTime": "10:00 AM", + "LastMeetingEndTime": "10:30 AM", + "ClientSummary": "Summary A", + } + ] @pytest.mark.asyncio diff --git a/ClientAdvisor/App/tests/test_db.py b/ClientAdvisor/App/tests/test_db.py index e0ac75c2..19e0dc2e 100644 --- a/ClientAdvisor/App/tests/test_db.py +++ b/ClientAdvisor/App/tests/test_db.py @@ -1,15 +1,27 @@ +import struct from unittest.mock import MagicMock, patch import db +import pyodbc +# Mock configuration db.server = "mock_server" db.username = "mock_user" db.password = "mock_password" db.database = "mock_database" +db.driver = "mock_driver" +db.mid_id = "mock_mid_id" # Managed identity client ID if needed -@patch("db.pymssql.connect") -def test_get_connection(mock_connect): +@patch("db.pyodbc.connect") # Mock pyodbc.connect +@patch("db.DefaultAzureCredential") # Mock DefaultAzureCredential +def test_get_connection(mock_credential_class, mock_connect): + # Mock the DefaultAzureCredential and get_token method + mock_credential = MagicMock() + mock_credential_class.return_value = mock_credential + mock_token = MagicMock() + mock_token.token = "mock_token" + mock_credential.get_token.return_value = mock_token # Create a mock connection object mock_conn = MagicMock() mock_connect.return_value = mock_conn @@ -17,14 +29,64 @@ def test_get_connection(mock_connect): # Call the function conn = db.get_connection() - # Assert that pymssql.connect was called with the correct parameters + # Assert that DefaultAzureCredential and get_token were called correctly + mock_credential_class.assert_called_once_with(managed_identity_client_id=db.mid_id) + mock_credential.get_token.assert_called_once_with("https://database.windows.net/.default") + + # Assert that pyodbc.connect was called with the correct parameters, including the token + expected_attrs_before = { + 1256: struct.pack(f" str: + """ + Connects to your SQL database and returns the client name for the given client_id. + """ + + conn = get_connection() + cursor = conn.cursor() + + sql = "SELECT Client FROM Clients WHERE ClientId = ?" + cursor.execute(sql, (client_id,)) + row = cursor.fetchone() + conn.close() + + if row: + return row[0] # The 'Client' column + else: + return "" + +# -------------------------- +# ChatWithDataPlugin Class +# -------------------------- class ChatWithDataPlugin: - @kernel_function(name="Greeting", description="Respond to any greeting or general questions") + @kernel_function(name="GreetingsResponse", description="Respond to any greeting or general questions") def greeting(self, input: Annotated[str, "the question"]) -> Annotated[str, "The output is a string"]: - query = input.split(':::')[0] - endpoint = os.environ.get("AZURE_OPEN_AI_ENDPOINT") - api_key = os.environ.get("AZURE_OPEN_AI_API_KEY") - client = openai.AzureOpenAI( - azure_endpoint=endpoint, - api_key=api_key, - api_version="2023-09-01-preview" - ) - deployment = os.environ.get("AZURE_OPEN_AI_DEPLOYMENT_MODEL") + """ + Simple greeting handler using Azure OpenAI. + """ try: + client = openai.AzureOpenAI( + azure_endpoint=endpoint, + api_key=api_key, + api_version=api_version + ) completion = client.chat.completions.create( model=deployment, messages=[ - {"role": "system", "content": "You are a helpful assistant to repond to any greeting or general questions."}, - {"role": "user", "content": query}, + { + "role": "system", + "content": "You are a helpful assistant to respond to greetings or general questions." + }, + { + "role": "user", + "content": input + }, ], temperature=0, + top_p=1, + n=1 ) answer = completion.choices[0].message.content except Exception as e: - answer = str(e) # 'Information from database could not be retrieved. Please try again later.' + answer = f"Error retrieving greeting response: {str(e)}" return answer - - @kernel_function(name="ChatWithSQLDatabase", description="Given a query about client assets, investements and meeting dates or times, get details from the database") + @kernel_function(name="ChatWithSQLDatabase", description="Generate and run a T-SQL query based on the provided question and client id") def get_SQL_Response( self, input: Annotated[str, "the question"], ClientId: Annotated[str, "the ClientId"] - ): - - # clientid = input.split(':::')[-1] - # query = input.split(':::')[0] + ' . ClientId = ' + input.split(':::')[-1] + ) -> Annotated[str, "The output is a string"]: + """ + Dynamically generates a T-SQL query using the Azure OpenAI chat endpoint + and then executes it against the SQL database. + """ clientid = ClientId query = input - endpoint = os.environ.get("AZURE_OPEN_AI_ENDPOINT") - api_key = os.environ.get("AZURE_OPEN_AI_API_KEY") + # Initialize the Azure OpenAI client client = openai.AzureOpenAI( azure_endpoint=endpoint, api_key=api_key, - api_version="2023-09-01-preview" + api_version=api_version ) - deployment = os.environ.get("AZURE_OPEN_AI_DEPLOYMENT_MODEL") + # Retrieve the SQL prompt from environment variables (if available) sql_prompt = os.environ.get("AZURE_SQL_SYSTEM_PROMPT") if sql_prompt: - sql_prompt = sql_prompt.replace("{query}", query) - sql_prompt = sql_prompt.replace("{clientid}", clientid) + sql_prompt = sql_prompt.replace("{query}", query).replace("{clientid}", clientid) else: - sql_prompt = f'''A valid T-SQL query to find {query} for tables and columns provided below: + # Fallback prompt if not set in environment + sql_prompt = f'''Generate a valid T-SQL query to find {query} for tables and columns provided below: 1. Table: Clients - Columns: ClientId,Client,Email,Occupation,MaritalStatus,Dependents + Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents 2. Table: InvestmentGoals - Columns: ClientId,InvestmentGoal + Columns: ClientId, InvestmentGoal 3. Table: Assets - Columns: ClientId,AssetDate,Investment,ROI,Revenue,AssetType + Columns: ClientId, AssetDate, Investment, ROI, Revenue, AssetType 4. Table: ClientSummaries - Columns: ClientId,ClientSummary + Columns: ClientId, ClientSummary 5. Table: InvestmentGoalsDetails - Columns: ClientId,InvestmentGoal,TargetAmount,Contribution + Columns: ClientId, InvestmentGoal, TargetAmount, Contribution 6. Table: Retirement - Columns: ClientId,StatusDate,RetirementGoalProgress,EducationGoalProgress - 7.Table: ClientMeetings - Columns: ClientId,ConversationId,Title,StartTime,EndTime,Advisor,ClientEmail - Use Investement column from Assets table as value always. + Columns: ClientId, StatusDate, RetirementGoalProgress, EducationGoalProgress + 7. Table: ClientMeetings + Columns: ClientId, ConversationId, Title, StartTime, EndTime, Advisor, ClientEmail + Always use the Investment column from the Assets table as the value. Assets table has snapshots of values by date. Do not add numbers across different dates for total values. - Do not use client name in filter. + Do not use client name in filters. Do not include assets values unless asked for. - Always use ClientId = {clientid} in the query filter. - Always return client name in the query. - Only return the generated sql query. do not return anything else''' - try: + ALWAYS use ClientId = {clientid} in the query filter. + ALWAYS select Client Name (Column: Client) in the query. + Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed. + If the result might return more than 100 rows, include TOP 100 to limit the row count. + Only return the generated SQL query. Do not return anything else.''' + try: completion = client.chat.completions.create( model=deployment, messages=[ @@ -115,178 +145,231 @@ def get_SQL_Response( {"role": "user", "content": sql_prompt}, ], temperature=0, + top_p=1, + n=1 ) + sql_query = completion.choices[0].message.content - sql_query = sql_query.replace("```sql",'').replace("```",'') - #print(sql_query) - - connectionString = os.environ.get("SQLDB_CONNECTION_STRING") - server = os.environ.get("SQLDB_SERVER") - database = os.environ.get("SQLDB_DATABASE") - username = os.environ.get("SQLDB_USERNAME") - password = os.environ.get("SQLDB_PASSWORD") - conn = pymssql.connect(server, username, password, database) + # Remove any triple backticks if present + sql_query = sql_query.replace("```sql", "").replace("```", "") + + print("Generated SQL:", sql_query) + + conn = get_connection() # conn = pyodbc.connect(connectionString) cursor = conn.cursor() cursor.execute(sql_query) - answer = '' - for row in cursor.fetchall(): - answer += str(row) + + rows = cursor.fetchall() + if not rows: + answer = "No data found for that client." + else: + answer = "" + for row in rows: + answer += str(row) + "\n" + + conn.close() + except Exception as e: - answer = str(e) # 'Information from database could not be retrieved. Please try again later.' + answer = f"Error retrieving data from SQL: {str(e)}" return answer - #return sql_query - - @kernel_function(name="ChatWithCallTranscripts", description="given a query about meetings summary or actions or notes, get answer from search index for a given ClientId") + @kernel_function(name="ChatWithCallTranscripts", description="Retrieve answers from call transcript search for a given client") def get_answers_from_calltranscripts( self, question: Annotated[str, "the question"], ClientId: Annotated[str, "the ClientId"] - ): + ) -> Annotated[str, "The output is a string"]: + """ + Uses Azure Cognitive Search (via the Azure OpenAI extension) to find relevant call transcripts. + """ + try: + client = openai.AzureOpenAI( + azure_endpoint=endpoint, + api_key=api_key, + api_version="2024-02-01" + ) - endpoint=os.environ.get("AZURE_OPEN_AI_ENDPOINT") - deployment=os.environ.get("AZURE_OPEN_AI_DEPLOYMENT_MODEL") - apikey=os.environ.get("AZURE_OPEN_AI_API_KEY") + system_message = os.environ.get("AZURE_CALL_TRANSCRIPT_SYSTEM_PROMPT") + if not system_message: + system_message = ( + "You are an assistant who supports wealth advisors in preparing for client meetings. " + "You have access to the client’s past meeting call transcripts. " + "When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. " + "If no data is available, state 'No relevant data found for previous meetings.'" + ) - search_endpoint = os.environ.get("AZURE_AI_SEARCH_ENDPOINT") - search_key = os.environ.get("AZURE_AI_SEARCH_API_KEY") - index_name = os.environ.get("AZURE_SEARCH_INDEX") + completion = client.chat.completions.create( + model=deployment, + messages=[ + {"role": "system", "content": system_message}, + {"role": "user", "content": question} + ], + seed=42, + temperature=0, + top_p=1, + n=1, + max_tokens=800, + extra_body={ + "data_sources": [ + { + "type": "azure_search", + "parameters": { + "endpoint": search_endpoint, + "index_name": os.environ.get("AZURE_SEARCH_INDEX"), + "semantic_configuration": "default", + "query_type": "vector_simple_hybrid", + "fields_mapping": { + "content_fields_separator": "\n", + "content_fields": ["content"], + "filepath_field": "chunk_id", + "title_field": "", + "url_field": "sourceurl", + "vector_fields": ["contentVector"] + }, + "semantic_configuration": 'my-semantic-config', + "in_scope": "true", + "role_information": system_message, + "filter": f"client_id eq '{ClientId}'", + "strictness": 3, + "top_n_documents": 5, + "authentication": { + "type": "api_key", + "key": search_key + }, + "embedding_dependency": { + "type": "deployment_name", + "deployment_name": "text-embedding-ada-002" + }, + } + } + ] + } + ) - client = openai.AzureOpenAI( - azure_endpoint= endpoint, #f"{endpoint}/openai/deployments/{deployment}/extensions", - api_key=apikey, - api_version="2024-02-01" - ) + if not completion.choices: + return "No data found for that client." - query = question - system_message = os.environ.get("AZURE_CALL_TRANSCRIPT_SYSTEM_PROMPT") - if not system_message: - system_message = '''You are an assistant who provides wealth advisors with helpful information to prepare for client meetings. - You have access to the client’s meeting call transcripts. - You can use this information to answer questions about the clients''' - - completion = client.chat.completions.create( - model = deployment, - messages = [ - { - "role": "system", - "content": system_message - }, - { - "role": "user", - "content": query - } - ], - seed = 42, - temperature = 0, - max_tokens = 800, - extra_body = { - "data_sources": [ - { - "type": "azure_search", - "parameters": { - "endpoint": search_endpoint, - "index_name": index_name, - "semantic_configuration": "default", - "query_type": "vector_simple_hybrid", #"vector_semantic_hybrid" - "fields_mapping": { - "content_fields_separator": "\n", - "content_fields": ["content"], - "filepath_field": "chunk_id", - "title_field": "", #null, - "url_field": "sourceurl", - "vector_fields": ["contentVector"] - }, - "semantic_configuration": 'my-semantic-config', - "in_scope": "true", - "role_information": system_message, - # "vector_filter_mode": "preFilter", #VectorFilterMode.PRE_FILTER, - "filter": f"client_id eq '{ClientId}'", #"", #null, - "strictness": 3, - "top_n_documents": 5, - "authentication": { - "type": "api_key", - "key": search_key - }, - "embedding_dependency": { - "type": "deployment_name", - "deployment_name": "text-embedding-ada-002" - }, + response_text = completion.choices[0].message.content + if not response_text.strip(): + return "No data found for that client." + return response_text - } - } - ] - } + except Exception as e: + return f"Error retrieving data from call transcripts: {str(e)}" + + +def get_connection(): + driver = "{ODBC Driver 18 for SQL Server}" + server = os.environ.get("SQLDB_SERVER") + database = os.environ.get("SQLDB_DATABASE") + username = os.environ.get("SQLDB_USERNAME") + password = os.environ.get("SQLDB_PASSWORD") + mid_id = os.environ.get("SQLDB_USER_MID") + try : + credential = DefaultAzureCredential(managed_identity_client_id=mid_id) + + token_bytes = credential.get_token( + "https://database.windows.net/.default" + ).token.encode("utf-16-LE") + token_struct = struct.pack(f" StreamingResponse: - + """ + The main Azure Function endpoint. + Receives a query of the form: ?query=::: + Example: ?query=Give summary of previous meetings:::10001 + """ query = req.query_params.get("query", None) - if not query: - query = "please pass a query:::00000" + query = "please pass a query:::00000" # default if none provided + + #Parse user query and client id + user_query = query.split(":::")[0] + client_id = query.split(":::")[-1] + + #Dynamically get the name from the database + selected_client_name = get_client_name_from_db(client_id) + + #Prepare fallback instructions with the single-line prompt + HOST_INSTRUCTIONS = os.environ.get("AZURE_OPENAI_STREAM_TEXT_SYSTEM_PROMPT") + if not HOST_INSTRUCTIONS: + # Insert the name in the prompt: + HOST_INSTRUCTIONS = ( + "You are a helpful assistant to a Wealth Advisor." + "The currently selected client's name is '{SelectedClientName}' (in any variation: ignoring punctuation, apostrophes, and case)." + "If the user mentions no name, assume they are asking about '{SelectedClientName}'." + "If the user references a name that clearly differs from '{SelectedClientName}', respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts." + "If no data is found, respond with 'No data found for that client.' Remove any client identifiers from the final response." + ) + #Create the agent using the Semantic Kernel Assistant Agent kernel = Kernel() + kernel.add_plugin(ChatWithDataPlugin(), plugin_name="ChatWithData") - service_id = "function_calling" + service_id = "agent" + HOST_NAME = "WealthAdvisor" - # Please make sure your AzureOpenAI Deployment allows for function calling - ai_service = AzureChatCompletion( + agent = await AzureAssistantAgent.create( + kernel=kernel, service_id=service_id, - endpoint=endpoint, + name=HOST_NAME, + instructions=HOST_INSTRUCTIONS, api_key=api_key, + deployment_name=deployment, + endpoint=endpoint, api_version=api_version, - deployment_name=deployment ) - kernel.add_service(ai_service) + #Create a conversation thread and add the user's message + thread_id = await agent.create_thread() + message = ChatMessageContent(role=AuthorRole.USER, content=user_query) + await agent.add_chat_message(thread_id=thread_id, message=message) - kernel.add_plugin(ChatWithDataPlugin(), plugin_name="ChatWithData") + #dditional instructions: pass the clientId + ADDITIONAL_INSTRUCTIONS = f"Always send clientId as {client_id}" - settings: OpenAIChatPromptExecutionSettings = kernel.get_prompt_execution_settings_from_service_id( - service_id=service_id - ) - settings.function_call_behavior = FunctionCallBehavior.EnableFunctions( - auto_invoke=True, filters={"included_plugins": ["ChatWithData"]} + #Invoke the streaming response + sk_response = agent.invoke_stream( + thread_id=thread_id, + additional_instructions=ADDITIONAL_INSTRUCTIONS ) - settings.seed = 42 - settings.max_tokens = 800 - settings.temperature = 0 - - system_message = os.environ.get("AZURE_OPENAI_STREAM_TEXT_SYSTEM_PROMPT") - if not system_message: - system_message = '''you are a helpful assistant to a wealth advisor. - Do not answer any questions not related to wealth advisors queries. - If the client name and client id do not match, only return - Please only ask questions about the selected client or select another client to inquire about their details. do not return any other information. - Only use the client name returned from database in the response. - If you cannot answer the question, always return - I cannot answer this question from the data available. Please rephrase or add more details. - ** Remove any client identifiers or ids or numbers or ClientId in the final response. - ''' - - user_query = query.replace('?',' ') - - user_query_prompt = f'''{user_query}. Always send clientId as {user_query.split(':::')[-1]} ''' - query_prompt = f'''{system_message}{user_query_prompt}''' - - - sk_response = kernel.invoke_prompt_stream( - function_name="prompt_test", - plugin_name="weather_test", - prompt=query_prompt, - settings=settings - ) - - return StreamingResponse(stream_processor(sk_response), media_type="text/event-stream") \ No newline at end of file + + return StreamingResponse(stream_processor(sk_response), media_type="text/event-stream") diff --git a/ClientAdvisor/AzureFunction/requirements.txt b/ClientAdvisor/AzureFunction/requirements.txt index fed21c9f..bfaaa5ab 100644 --- a/ClientAdvisor/AzureFunction/requirements.txt +++ b/ClientAdvisor/AzureFunction/requirements.txt @@ -4,7 +4,10 @@ azure-functions azurefunctions-extensions-http-fastapi==1.0.0b1 -openai==1.63.0 -semantic_kernel==1.0.4 +openai==1.64.0 +semantic_kernel==1.21.3 pymssql==2.3.2 azure-search-documents==11.6.0b9 + +pyodbc==5.2.0 +azure-identity==1.20.0 diff --git a/ClientAdvisor/Deployment/bicep/deploy_app_service.bicep b/ClientAdvisor/Deployment/bicep/deploy_app_service.bicep index 0999d728..a4aa1886 100644 --- a/ClientAdvisor/Deployment/bicep/deploy_app_service.bicep +++ b/ClientAdvisor/Deployment/bicep/deploy_app_service.bicep @@ -67,7 +67,7 @@ param AzureOpenAIResource string param AzureOpenAIModel string @description('Azure OpenAI Model Name') -param AzureOpenAIModelName string = 'gpt-4' +param AzureOpenAIModelName string = 'gpt-4o-mini' @description('Azure Open AI Endpoint') param AzureOpenAIEndpoint string = '' @@ -165,6 +165,9 @@ param VITE_POWERBI_EMBED_URL string = '' param Appversion string +param userassignedIdentityId string +param userassignedIdentityClientId string + // var WebAppImageName = 'DOCKER|byoaiacontainer.azurecr.io/byoaia-app:latest' // var WebAppImageName = 'DOCKER|ncwaappcontainerreg1.azurecr.io/ncqaappimage:v1.0.0' @@ -188,7 +191,10 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { name: WebsiteName location: resourceGroup().location identity: { - type: 'SystemAssigned' + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${userassignedIdentityId}': {} + } } properties: { serverFarmId: HostingPlanName @@ -381,6 +387,10 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { name: 'UWSGI_THREADS' value: '2' } + { + name: 'SQLDB_USER_MID' + value: userassignedIdentityClientId + } ] linuxFxVersion: WebAppImageName } diff --git a/ClientAdvisor/Deployment/bicep/deploy_azure_function.bicep b/ClientAdvisor/Deployment/bicep/deploy_azure_function.bicep index ba800153..9a18ba14 100644 --- a/ClientAdvisor/Deployment/bicep/deploy_azure_function.bicep +++ b/ClientAdvisor/Deployment/bicep/deploy_azure_function.bicep @@ -21,9 +21,11 @@ param sqlSystemPrompt string param callTranscriptSystemPrompt string @description('Azure Function App Stream Text System Prompt') param streamTextSystemPrompt string +param userassignedIdentityId string +param userassignedIdentityClientId string var functionAppName = '${solutionName}fn' -var azureOpenAIDeploymentModel = 'gpt-4' +var azureOpenAIDeploymentModel = 'gpt-4o-mini' var azureOpenAIEmbeddingDeployment = 'text-embedding-ada-002' var valueOne = '1' @@ -78,7 +80,10 @@ resource functionApp 'Microsoft.Web/sites@2024-04-01' = { location: solutionLocation kind: 'functionapp' identity: { - type: 'SystemAssigned' + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${userassignedIdentityId}': {} + } } properties: { managedEnvironmentId: containerAppEnv.id @@ -161,7 +166,13 @@ resource functionApp 'Microsoft.Web/sites@2024-04-01' = { name: 'AZURE_OPENAI_STREAM_TEXT_SYSTEM_PROMPT' value: streamTextSystemPrompt } + { + name: 'SQLDB_USER_MID' + value: userassignedIdentityClientId + } ] } } } + +output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.name diff --git a/ClientAdvisor/Deployment/bicep/deploy_azure_open_ai.bicep b/ClientAdvisor/Deployment/bicep/deploy_azure_open_ai.bicep index c8a710fa..a8eeeb28 100644 --- a/ClientAdvisor/Deployment/bicep/deploy_azure_open_ai.bicep +++ b/ClientAdvisor/Deployment/bicep/deploy_azure_open_ai.bicep @@ -45,16 +45,16 @@ resource accounts_byc_openai_name_resource 'Microsoft.CognitiveServices/accounts resource accounts_byc_openai_name_gpt_4 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = { parent: accounts_byc_openai_name_resource - name: 'gpt-4' + name: 'gpt-4o-mini' sku: { - name: 'Standard' - capacity: 10 + name: 'GlobalStandard' + capacity: 45 } properties: { model: { format: 'OpenAI' - name: 'gpt-4' - version: '0125-Preview' + name: 'gpt-4o-mini' + version: '2024-07-18' } versionUpgradeOption: 'OnceCurrentVersionExpired' raiPolicyName: 'Microsoft.Default' diff --git a/ClientAdvisor/Deployment/bicep/deploy_managed_identity.bicep b/ClientAdvisor/Deployment/bicep/deploy_managed_identity.bicep index ad9b95c7..926563fe 100644 --- a/ClientAdvisor/Deployment/bicep/deploy_managed_identity.bicep +++ b/ClientAdvisor/Deployment/bicep/deploy_managed_identity.bicep @@ -36,6 +36,25 @@ resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { } } +resource managedIdentityFnApp 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${solutionName}-fn-mi' + location: solutionLocation + tags: { + app: solutionName + location: solutionLocation + } +} + +resource managedIdentityWebApp 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: '${solutionName}-webapp-mi' + location: solutionLocation + tags: { + app: solutionName + location: solutionLocation + } +} + + // @description('Array of actions for the roleDefinition') // param actions array = [ // 'Microsoft.Synapse/workspaces/write' @@ -83,5 +102,20 @@ resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { output managedIdentityOutput object = { id: managedIdentity.id objectId: managedIdentity.properties.principalId + clientId: managedIdentity.properties.clientId name: miName } + +output managedIdentityFnAppOutput object = { + id: managedIdentityFnApp.id + objectId: managedIdentityFnApp.properties.principalId + clientId: managedIdentityFnApp.properties.clientId + name: managedIdentityFnApp.name +} + +output managedIdentityWebAppOutput object = { + id: managedIdentityWebApp.id + objectId: managedIdentityWebApp.properties.principalId + clientId: managedIdentityWebApp.properties.clientId + name: managedIdentityWebApp.name +} diff --git a/ClientAdvisor/Deployment/bicep/deploy_post_deployment_scripts.bicep b/ClientAdvisor/Deployment/bicep/deploy_post_deployment_scripts.bicep new file mode 100644 index 00000000..ed451b09 --- /dev/null +++ b/ClientAdvisor/Deployment/bicep/deploy_post_deployment_scripts.bicep @@ -0,0 +1,93 @@ +@description('Solution Name') +param solutionName string +@description('Specifies the location for resources.') +param solutionLocation string +param baseUrl string +param managedIdentityObjectId string +param managedIdentityClientId string +param storageAccountName string +param containerName string +param containerAppName string = '${ solutionName }containerapp' +param environmentName string = '${ solutionName }containerappenv' +param imageName string = 'python:3.11-alpine' +param setupCopyKbFiles string = '${baseUrl}Deployment/scripts/copy_kb_files.sh' +param setupCreateIndexScriptsUrl string = '${baseUrl}Deployment/scripts/run_create_index_scripts.sh' +param createSqlUserAndRoleScriptsUrl string = '${baseUrl}Deployment/scripts/add_user_scripts/create-sql-user-and-role.ps1' +param keyVaultName string +param sqlServerName string +param sqlDbName string +param sqlUsers array = [ +] +param logAnalyticsWorkspaceResourceName string +var resourceGroupName = resourceGroup().name + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2020-10-01' existing = { + name: logAnalyticsWorkspaceResourceName + scope: resourceGroup() +} + +resource containerAppEnv 'Microsoft.App/managedEnvironments@2022-03-01' = { + name: environmentName + location: solutionLocation + properties: { + zoneRedundant: false + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + } +} + +resource containerApp 'Microsoft.App/containerApps@2022-03-01' = { + name: containerAppName + location: solutionLocation + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentityObjectId}': {} + } + } + properties: { + managedEnvironmentId: containerAppEnv.id + configuration: { + ingress: null + activeRevisionsMode: 'Single' + } + template: { + scale:{ + minReplicas: 1 + maxReplicas: 1 + } + containers: [ + { + name: containerAppName + image: imageName + resources: { + cpu: 2 + memory: '4.0Gi' + } + command: [ + '/bin/sh', '-c', 'mkdir -p /scripts && apk add --no-cache curl bash jq py3-pip gcc musl-dev libffi-dev openssl-dev python3-dev && pip install --upgrade azure-cli && apk add --no-cache --virtual .build-deps build-base unixodbc-dev && curl -s -o msodbcsql18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/msodbcsql18_18.4.1.1-1_amd64.apk && curl -s -o mssql-tools18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_amd64.apk && apk add --allow-untrusted msodbcsql18_18.4.1.1-1_amd64.apk && apk add --allow-untrusted mssql-tools18_18.4.1.1-1_amd64.apk && curl -s -o /scripts/copy_kb_files.sh ${setupCopyKbFiles} && chmod +x /scripts/copy_kb_files.sh && sh -x /scripts/copy_kb_files.sh ${storageAccountName} ${containerName} ${baseUrl} ${managedIdentityClientId} && curl -s -o /scripts/run_create_index_scripts.sh ${setupCreateIndexScriptsUrl} && chmod +x /scripts/run_create_index_scripts.sh && sh -x /scripts/run_create_index_scripts.sh ${baseUrl} ${keyVaultName} ${managedIdentityClientId} && apk add --no-cache ca-certificates less ncurses-terminfo-base krb5-libs libgcc libintl libssl3 libstdc++ tzdata userspace-rcu zlib icu-libs curl && apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache lttng-ust openssh-client && curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz && mkdir -p /opt/microsoft/powershell/7 && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 && chmod +x /opt/microsoft/powershell/7/pwsh && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh && curl -s -o /scripts/create-sql-user-and-role.ps1 ${createSqlUserAndRoleScriptsUrl} && chmod +x /scripts/create-sql-user-and-role.ps1 && pwsh -File /scripts/create-sql-user-and-role.ps1 -SqlServerName ${sqlServerName} -SqlDatabaseName ${sqlDbName} -ClientId ${sqlUsers[0].principalId} -DisplayName ${sqlUsers[0].principalName} -ManagedIdentityClientId ${managedIdentityClientId} -DatabaseRole ${sqlUsers[0].databaseRoles[0]} && pwsh -File /scripts/create-sql-user-and-role.ps1 -SqlServerName ${sqlServerName} -SqlDatabaseName ${sqlDbName} -ClientId ${sqlUsers[1].principalId} -DisplayName ${sqlUsers[1].principalName} -ManagedIdentityClientId ${managedIdentityClientId} -DatabaseRole ${sqlUsers[1].databaseRoles[0]} && pwsh -File /scripts/create-sql-user-and-role.ps1 -SqlServerName ${sqlServerName} -SqlDatabaseName ${sqlDbName} -ClientId ${sqlUsers[1].principalId} -DisplayName ${sqlUsers[1].principalName} -ManagedIdentityClientId ${managedIdentityClientId} -DatabaseRole ${sqlUsers[1].databaseRoles[1]} && az login --identity --client-id ${managedIdentityClientId} && az containerapp update --name ${containerAppName} --resource-group ${resourceGroupName} --min-replicas 0 --cpu 0.25 --memory 0.5Gi && az containerapp revision deactivate -g ${resourceGroupName} --revision $(az containerapp revision list -n ${containerAppName} -g ${resourceGroupName} --query "[0].name" -o tsv) && echo "Container app setup completed successfully."' + ] + env: [ + { + name: 'STORAGE_ACCOUNT_NAME' + value: storageAccountName + } + { + name: 'CONTAINER_NAME' + value: containerName + } + { + name:'APPSETTING_WEBSITE_SITE_NAME' + value:'DUMMY' + } + ] + } + ] + } + } +} diff --git a/ClientAdvisor/Deployment/bicep/deploy_sql_db.bicep b/ClientAdvisor/Deployment/bicep/deploy_sql_db.bicep index 8988b26c..e9d551f2 100644 --- a/ClientAdvisor/Deployment/bicep/deploy_sql_db.bicep +++ b/ClientAdvisor/Deployment/bicep/deploy_sql_db.bicep @@ -4,6 +4,7 @@ param solutionName string param solutionLocation string param managedIdentityObjectId string +param managedIdentityName string @description('The name of the SQL logical server.') param serverName string = '${ solutionName }-sql-server' @@ -26,11 +27,16 @@ resource sqlServer 'Microsoft.Sql/servers@2023-08-01-preview' = { location: location kind:'v12.0' properties: { - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword publicNetworkAccess: 'Enabled' version: '12.0' restrictOutboundNetworkAccess: 'Disabled' + administrators: { + login: managedIdentityName + sid: managedIdentityObjectId + tenantId: subscription().tenantId + administratorType: 'ActiveDirectory' + azureADOnlyAuthentication: true + } } } diff --git a/ClientAdvisor/Deployment/bicep/main.bicep b/ClientAdvisor/Deployment/bicep/main.bicep index 3e5a110e..7e514430 100644 --- a/ClientAdvisor/Deployment/bicep/main.bicep +++ b/ClientAdvisor/Deployment/bicep/main.bicep @@ -12,6 +12,11 @@ param cosmosLocation string // @description('Fabric Workspace Id if you have one, else leave it empty. ') // param fabricWorkspaceId string +//restricting to these regions because assistants api for gpt-4o-mini is available only in these regions +// @allowed(['eastus', 'eastus2', 'westus', 'westus3', 'swedencentral']) +// @description('Azure OpenAI Location') +// param AzureOpenAILocation string + var resourceGroupLocation = resourceGroup().location // var subscriptionId = subscription().subscriptionId @@ -19,39 +24,41 @@ var solutionLocation = resourceGroupLocation var baseUrl = 'https://raw.githubusercontent.com/microsoft/Build-your-own-copilot-Solution-Accelerator/main/ClientAdvisor/' var appversion = 'latest' -var functionAppSqlPrompt = '''A valid T-SQL query to find {query} for tables and columns provided below: - 1. Table: Clients - Columns: ClientId,Client,Email,Occupation,MaritalStatus,Dependents - 2. Table: InvestmentGoals - Columns: ClientId,InvestmentGoal - 3. Table: Assets - Columns: ClientId,AssetDate,Investment,ROI,Revenue,AssetType - 4. Table: ClientSummaries - Columns: ClientId,ClientSummary - 5. Table: InvestmentGoalsDetails - Columns: ClientId,InvestmentGoal,TargetAmount,Contribution - 6. Table: Retirement - Columns: ClientId,StatusDate,RetirementGoalProgress,EducationGoalProgress - 7.Table: ClientMeetings - Columns: ClientId,ConversationId,Title,StartTime,EndTime,Advisor,ClientEmail - Use Investement column from Assets table as value always. - Assets table has snapshots of values by date. Do not add numbers across different dates for total values. - Do not use client name in filter. - Do not include assets values unless asked for. - Always use ClientId = {clientid} in the query filter. - Always return client name in the query. - Only return the generated sql query. do not return anything else''' - -var functionAppCallTranscriptSystemPrompt = '''You are an assistant who provides wealth advisors with helpful information to prepare for client meetings. - You have access to the client’s meeting call transcripts. - You can use this information to answer questions about the clients''' +var functionAppSqlPrompt ='''Generate a valid T-SQL query to find {query} for tables and columns provided below: + 1. Table: Clients + Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents + 2. Table: InvestmentGoals + Columns: ClientId, InvestmentGoal + 3. Table: Assets + Columns: ClientId, AssetDate, Investment, ROI, Revenue, AssetType + 4. Table: ClientSummaries + Columns: ClientId, ClientSummary + 5. Table: InvestmentGoalsDetails + Columns: ClientId, InvestmentGoal, TargetAmount, Contribution + 6. Table: Retirement + Columns: ClientId, StatusDate, RetirementGoalProgress, EducationGoalProgress + 7. Table: ClientMeetings + Columns: ClientId, ConversationId, Title, StartTime, EndTime, Advisor, ClientEmail + Always use the Investment column from the Assets table as the value. + Assets table has snapshots of values by date. Do not add numbers across different dates for total values. + Do not use client name in filters. + Do not include assets values unless asked for. + ALWAYS use ClientId = {clientid} in the query filter. + ALWAYS select Client Name (Column: Client) in the query. + Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed. + If the result might return more than 100 rows, include TOP 100 to limit the row count. + Only return the generated SQL query. Do not return anything else.''' + +var functionAppCallTranscriptSystemPrompt = '''You are an assistant who supports wealth advisors in preparing for client meetings. + You have access to the client’s past meeting call transcripts. + When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. + If no data is available, state 'No relevant data found for previous meetings.''' -var functionAppStreamTextSystemPrompt = '''You are a helpful assistant to a wealth advisor. - Do not answer any questions not related to wealth advisors queries. - If the client name and client id do not match, only return - Please only ask questions about the selected client or select another client to inquire about their details. do not return any other information. - Only use the client name returned from database in the response. - If you cannot answer the question, always return - I cannot answer this question from the data available. Please rephrase or add more details. - ** Remove any client identifiers or ids or numbers or ClientId in the final response.''' +var functionAppStreamTextSystemPrompt = '''You are a helpful assistant to a Wealth Advisor. + The currently selected client's name is '{SelectedClientName}' (in any variation: ignoring punctuation, apostrophes, and case). + If the user mentions no name, assume they are asking about '{SelectedClientName}'. + If the user references a name that clearly differs from '{SelectedClientName}', respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts. + If no data is found, respond with 'No data found for that client.' Remove any client identifiers from the final response.''' // ========== Managed Identity ========== // module managedIdentityModule 'deploy_managed_identity.bicep' = { @@ -91,6 +98,7 @@ module sqlDBModule 'deploy_sql_db.bicep' = { solutionName: solutionPrefix solutionLocation: solutionLocation managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId + managedIdentityName:managedIdentityModule.outputs.managedIdentityOutput.name } scope: resourceGroup(resourceGroup().name) } @@ -122,16 +130,34 @@ module azOpenAI 'deploy_azure_open_ai.bicep' = { } } -module uploadFiles 'deploy_upload_files_script.bicep' = { - name : 'deploy_upload_files_script' +//========== Deployment script to upload sample data ========== // +module uploadFiles 'deploy_post_deployment_scripts.bicep' = { + name : 'deploy_post_deployment_scripts' params:{ - storageAccountName:storageAccountModule.outputs.storageAccountOutput.name - solutionLocation: solutionLocation - containerName:storageAccountModule.outputs.storageAccountOutput.dataContainer - identity:managedIdentityModule.outputs.managedIdentityOutput.id - baseUrl:baseUrl + solutionName: solutionPrefix + solutionLocation: resourceGroupLocation + baseUrl: baseUrl + storageAccountName: storageAccountModule.outputs.storageAccountOutput.storageAccountName + containerName: storageAccountModule.outputs.storageAccountOutput.dataContainer + managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.id + managedIdentityClientId:managedIdentityModule.outputs.managedIdentityOutput.clientId + keyVaultName:keyvaultModule.outputs.keyvaultOutput.name + logAnalyticsWorkspaceResourceName: azureFunctions.outputs.logAnalyticsWorkspaceName + sqlServerName: sqlDBModule.outputs.sqlDbOutput.sqlServerName + sqlDbName: sqlDBModule.outputs.sqlDbOutput.sqlDbName + sqlUsers: [ + { + principalId: managedIdentityModule.outputs.managedIdentityFnAppOutput.clientId // Replace with actual Principal ID + principalName: managedIdentityModule.outputs.managedIdentityFnAppOutput.name // Replace with actual user email or name + databaseRoles: ['db_datareader'] + } + { + principalId: managedIdentityModule.outputs.managedIdentityWebAppOutput.clientId // Replace with actual Principal ID + principalName: managedIdentityModule.outputs.managedIdentityWebAppOutput.name // Replace with actual user email or name + databaseRoles: ['db_datareader', 'db_datawriter'] + } + ] } - dependsOn:[storageAccountModule] } module azureFunctions 'deploy_azure_function.bicep' = { @@ -149,10 +175,12 @@ module azureFunctions 'deploy_azure_function.bicep' = { sqlDbName:sqlDBModule.outputs.sqlDbOutput.sqlDbName sqlDbUser:sqlDBModule.outputs.sqlDbOutput.sqlDbUser sqlDbPwd:sqlDBModule.outputs.sqlDbOutput.sqlDbPwd - functionAppVersion: appversion + functionAppVersion: appversion sqlSystemPrompt: functionAppSqlPrompt callTranscriptSystemPrompt: functionAppCallTranscriptSystemPrompt streamTextSystemPrompt: functionAppStreamTextSystemPrompt + userassignedIdentityClientId:managedIdentityModule.outputs.managedIdentityFnAppOutput.clientId + userassignedIdentityId:managedIdentityModule.outputs.managedIdentityFnAppOutput.id } dependsOn:[storageAccountModule] } @@ -198,16 +226,16 @@ module keyvaultModule 'deploy_keyvault.bicep' = { dependsOn:[storageAccountModule,azOpenAI,azSearchService,sqlDBModule] } -module createIndex 'deploy_index_scripts.bicep' = { - name : 'deploy_index_scripts' - params:{ - solutionLocation: solutionLocation - identity:managedIdentityModule.outputs.managedIdentityOutput.id - baseUrl:baseUrl - keyVaultName:keyvaultModule.outputs.keyvaultOutput.name - } - dependsOn:[keyvaultModule] -} +// module createIndex 'deploy_index_scripts.bicep' = { +// name : 'deploy_index_scripts' +// params:{ +// solutionLocation: solutionLocation +// identity:managedIdentityModule.outputs.managedIdentityOutput.id +// baseUrl:baseUrl +// keyVaultName:keyvaultModule.outputs.keyvaultOutput.name +// } +// dependsOn:[keyvaultModule] +// } // module createaihub 'deploy_aihub_scripts.bicep' = { // name : 'deploy_aihub_scripts' @@ -241,9 +269,9 @@ module appserviceModule 'deploy_app_service.bicep' = { AzureSearchUrlColumn:'sourceurl' AzureOpenAIResource:azOpenAI.outputs.openAIOutput.openAPIEndpoint AzureOpenAIEndpoint:azOpenAI.outputs.openAIOutput.openAPIEndpoint - AzureOpenAIModel:'gpt-4' + AzureOpenAIModel:'gpt-4o-mini' AzureOpenAIKey:azOpenAI.outputs.openAIOutput.openAPIKey - AzureOpenAIModelName:'gpt-4' + AzureOpenAIModelName:'gpt-4o-mini' AzureOpenAITemperature:'0' AzureOpenAITopP:'1' AzureOpenAIMaxTokens:'1000' @@ -270,6 +298,8 @@ module appserviceModule 'deploy_app_service.bicep' = { AZURE_COSMOSDB_ENABLE_FEEDBACK: 'True' VITE_POWERBI_EMBED_URL: 'TBD' Appversion: appversion + userassignedIdentityClientId:managedIdentityModule.outputs.managedIdentityWebAppOutput.clientId + userassignedIdentityId:managedIdentityModule.outputs.managedIdentityWebAppOutput.id } scope: resourceGroup(resourceGroup().name) dependsOn:[azOpenAI,azAIMultiServiceAccount,azSearchService,sqlDBModule,azureFunctionURL,cosmosDBModule] diff --git a/ClientAdvisor/Deployment/bicep/main.json b/ClientAdvisor/Deployment/bicep/main.json index 007e39f1..facf9bae 100644 --- a/ClientAdvisor/Deployment/bicep/main.json +++ b/ClientAdvisor/Deployment/bicep/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "8614046715488453239" + "version": "0.34.44.8038", + "templateHash": "6567721562027613849" } }, "parameters": { @@ -29,9 +29,9 @@ "solutionLocation": "[variables('resourceGroupLocation')]", "baseUrl": "https://raw.githubusercontent.com/microsoft/Build-your-own-copilot-Solution-Accelerator/main/ClientAdvisor/", "appversion": "latest", - "functionAppSqlPrompt": "A valid T-SQL query to find {query} for tables and columns provided below:\r\n 1. Table: Clients\r\n Columns: ClientId,Client,Email,Occupation,MaritalStatus,Dependents\r\n 2. Table: InvestmentGoals\r\n Columns: ClientId,InvestmentGoal\r\n 3. Table: Assets\r\n Columns: ClientId,AssetDate,Investment,ROI,Revenue,AssetType\r\n 4. Table: ClientSummaries\r\n Columns: ClientId,ClientSummary\r\n 5. Table: InvestmentGoalsDetails\r\n Columns: ClientId,InvestmentGoal,TargetAmount,Contribution\r\n 6. Table: Retirement\r\n Columns: ClientId,StatusDate,RetirementGoalProgress,EducationGoalProgress\r\n 7.Table: ClientMeetings\r\n Columns: ClientId,ConversationId,Title,StartTime,EndTime,Advisor,ClientEmail\r\n Use Investement column from Assets table as value always.\r\n Assets table has snapshots of values by date. Do not add numbers across different dates for total values.\r\n Do not use client name in filter.\r\n Do not include assets values unless asked for.\r\n Always use ClientId = {clientid} in the query filter.\r\n Always return client name in the query.\r\n Only return the generated sql query. do not return anything else", - "functionAppCallTranscriptSystemPrompt": "You are an assistant who provides wealth advisors with helpful information to prepare for client meetings.\r\n You have access to the client’s meeting call transcripts.\r\n You can use this information to answer questions about the clients", - "functionAppStreamTextSystemPrompt": "You are a helpful assistant to a wealth advisor.\r\n Do not answer any questions not related to wealth advisors queries.\r\n If the client name and client id do not match, only return - Please only ask questions about the selected client or select another client to inquire about their details. do not return any other information.\r\n Only use the client name returned from database in the response.\r\n If you cannot answer the question, always return - I cannot answer this question from the data available. Please rephrase or add more details.\r\n ** Remove any client identifiers or ids or numbers or ClientId in the final response." + "functionAppSqlPrompt": "Generate a valid T-SQL query to find {query} for tables and columns provided below:\r\n 1. Table: Clients\r\n Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents\r\n 2. Table: InvestmentGoals\r\n Columns: ClientId, InvestmentGoal\r\n 3. Table: Assets\r\n Columns: ClientId, AssetDate, Investment, ROI, Revenue, AssetType\r\n 4. Table: ClientSummaries\r\n Columns: ClientId, ClientSummary\r\n 5. Table: InvestmentGoalsDetails\r\n Columns: ClientId, InvestmentGoal, TargetAmount, Contribution\r\n 6. Table: Retirement\r\n Columns: ClientId, StatusDate, RetirementGoalProgress, EducationGoalProgress\r\n 7. Table: ClientMeetings\r\n Columns: ClientId, ConversationId, Title, StartTime, EndTime, Advisor, ClientEmail\r\n Always use the Investment column from the Assets table as the value.\r\n Assets table has snapshots of values by date. Do not add numbers across different dates for total values.\r\n Do not use client name in filters.\r\n Do not include assets values unless asked for.\r\n ALWAYS use ClientId = {clientid} in the query filter.\r\n ALWAYS select Client Name (Column: Client) in the query.\r\n Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed.\r\n If the result might return more than 100 rows, include TOP 100 to limit the row count.\r\n Only return the generated SQL query. Do not return anything else.", + "functionAppCallTranscriptSystemPrompt": "You are an assistant who supports wealth advisors in preparing for client meetings. \r\n You have access to the client’s past meeting call transcripts. \r\n When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. \r\n If no data is available, state 'No relevant data found for previous meetings.", + "functionAppStreamTextSystemPrompt": "You are a helpful assistant to a Wealth Advisor. \r\n The currently selected client's name is '{SelectedClientName}' (in any variation: ignoring punctuation, apostrophes, and case). \r\n If the user mentions no name, assume they are asking about '{SelectedClientName}'. \r\n If the user references a name that clearly differs from '{SelectedClientName}', respond only with: 'Please only ask questions about the selected client or select another client.' Otherwise, provide thorough answers for every question using only data from SQL or call transcripts. \r\n If no data is found, respond with 'No data found for that client.' Remove any client identifiers from the final response." }, "resources": [ { @@ -58,8 +58,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "9540019694218374629" + "version": "0.34.44.8038", + "templateHash": "4201070185547993881" } }, "parameters": { @@ -108,6 +108,26 @@ "dependsOn": [ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName'))]" ] + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}-fn-mi', parameters('solutionName'))]", + "location": "[parameters('solutionLocation')]", + "tags": { + "app": "[parameters('solutionName')]", + "location": "[parameters('solutionLocation')]" + } + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}-webapp-mi', parameters('solutionName'))]", + "location": "[parameters('solutionLocation')]", + "tags": { + "app": "[parameters('solutionName')]", + "location": "[parameters('solutionLocation')]" + } } ], "outputs": { @@ -116,8 +136,27 @@ "value": { "id": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName'))]", "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), '2023-01-31').principalId]", + "clientId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), '2023-01-31').clientId]", "name": "[parameters('miName')]" } + }, + "managedIdentityFnAppOutput": { + "type": "object", + "value": { + "id": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-fn-mi', parameters('solutionName')))]", + "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-fn-mi', parameters('solutionName'))), '2023-01-31').principalId]", + "clientId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-fn-mi', parameters('solutionName'))), '2023-01-31').clientId]", + "name": "[format('{0}-fn-mi', parameters('solutionName'))]" + } + }, + "managedIdentityWebAppOutput": { + "type": "object", + "value": { + "id": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-webapp-mi', parameters('solutionName')))]", + "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-webapp-mi', parameters('solutionName'))), '2023-01-31').principalId]", + "clientId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-webapp-mi', parameters('solutionName'))), '2023-01-31').clientId]", + "name": "[format('{0}-webapp-mi', parameters('solutionName'))]" + } } } } @@ -147,8 +186,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "12718237112242025023" + "version": "0.34.44.8038", + "templateHash": "4003838080878788531" } }, "parameters": { @@ -205,7 +244,7 @@ "resources": [ { "copy": { - "name": "list", + "name": "database::list", "count": "[length(parameters('containers'))]" }, "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", @@ -310,8 +349,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "13214455762521164459" + "version": "0.34.44.8038", + "templateHash": "11698875401555850644" } }, "parameters": { @@ -461,6 +500,9 @@ }, "managedIdentityObjectId": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.objectId]" + }, + "managedIdentityName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.name]" } }, "template": { @@ -469,8 +511,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "12781397079288954316" + "version": "0.34.44.8038", + "templateHash": "17273043613283621136" } }, "parameters": { @@ -488,6 +530,9 @@ "managedIdentityObjectId": { "type": "string" }, + "managedIdentityName": { + "type": "string" + }, "serverName": { "type": "string", "defaultValue": "[format('{0}-sql-server', parameters('solutionName'))]", @@ -532,11 +577,16 @@ "location": "[parameters('location')]", "kind": "v12.0", "properties": { - "administratorLogin": "[parameters('administratorLogin')]", - "administratorLoginPassword": "[parameters('administratorLoginPassword')]", "publicNetworkAccess": "Enabled", "version": "12.0", - "restrictOutboundNetworkAccess": "Disabled" + "restrictOutboundNetworkAccess": "Disabled", + "administrators": { + "login": "[parameters('managedIdentityName')]", + "sid": "[parameters('managedIdentityObjectId')]", + "tenantId": "[subscription().tenantId]", + "administratorType": "ActiveDirectory", + "azureADOnlyAuthentication": true + } } }, { @@ -627,8 +677,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "6507317467445174187" + "version": "0.34.44.8038", + "templateHash": "4680446175273955277" } }, "parameters": { @@ -709,8 +759,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "13153152178869896502" + "version": "0.34.44.8038", + "templateHash": "4948070379347901160" } }, "parameters": { @@ -797,8 +847,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "10512077094934475379" + "version": "0.34.44.8038", + "templateHash": "15880203484874305929" } }, "parameters": { @@ -841,16 +891,16 @@ { "type": "Microsoft.CognitiveServices/accounts/deployments", "apiVersion": "2023-05-01", - "name": "[format('{0}/{1}', parameters('accounts_byc_openai_name'), 'gpt-4')]", + "name": "[format('{0}/{1}', parameters('accounts_byc_openai_name'), 'gpt-4o-mini')]", "sku": { - "name": "Standard", - "capacity": 10 + "name": "GlobalStandard", + "capacity": 45 }, "properties": { "model": { "format": "OpenAI", - "name": "gpt-4", - "version": "0125-Preview" + "name": "gpt-4o-mini", + "version": "2024-07-18" }, "versionUpgradeOption": "OnceCurrentVersionExpired", "raiPolicyName": "Microsoft.Default" @@ -877,7 +927,7 @@ "raiPolicyName": "Microsoft.Default" }, "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts/deployments', parameters('accounts_byc_openai_name'), 'gpt-4')]", + "[resourceId('Microsoft.CognitiveServices/accounts/deployments', parameters('accounts_byc_openai_name'), 'gpt-4o-mini')]", "[resourceId('Microsoft.CognitiveServices/accounts', parameters('accounts_byc_openai_name'))]" ] } @@ -899,27 +949,64 @@ { "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", - "name": "deploy_upload_files_script", + "name": "deploy_post_deployment_scripts", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "storageAccountName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_storage_account'), '2022-09-01').outputs.storageAccountOutput.value.name]" + "solutionName": { + "value": "[parameters('solutionPrefix')]" }, "solutionLocation": { - "value": "[variables('solutionLocation')]" + "value": "[variables('resourceGroupLocation')]" + }, + "baseUrl": { + "value": "[variables('baseUrl')]" + }, + "storageAccountName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_storage_account'), '2022-09-01').outputs.storageAccountOutput.value.storageAccountName]" }, "containerName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_storage_account'), '2022-09-01').outputs.storageAccountOutput.value.dataContainer]" }, - "identity": { + "managedIdentityObjectId": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.id]" }, - "baseUrl": { - "value": "[variables('baseUrl')]" + "managedIdentityClientId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.clientId]" + }, + "keyVaultName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_keyvault'), '2022-09-01').outputs.keyvaultOutput.value.name]" + }, + "logAnalyticsWorkspaceResourceName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_azure_function'), '2022-09-01').outputs.logAnalyticsWorkspaceName.value]" + }, + "sqlServerName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_sql_db'), '2022-09-01').outputs.sqlDbOutput.value.sqlServerName]" + }, + "sqlDbName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_sql_db'), '2022-09-01').outputs.sqlDbOutput.value.sqlDbName]" + }, + "sqlUsers": { + "value": [ + { + "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityFnAppOutput.value.clientId]", + "principalName": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityFnAppOutput.value.name]", + "databaseRoles": [ + "db_datareader" + ] + }, + { + "principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityWebAppOutput.value.clientId]", + "principalName": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityWebAppOutput.value.name]", + "databaseRoles": [ + "db_datareader", + "db_datawriter" + ] + } + ] } }, "template": { @@ -928,57 +1015,164 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "11104800647186344148" + "version": "0.34.44.8038", + "templateHash": "5018000626328317665" } }, "parameters": { + "solutionName": { + "type": "string", + "metadata": { + "description": "Solution Name" + } + }, "solutionLocation": { "type": "string", "metadata": { "description": "Specifies the location for resources." } }, + "baseUrl": { + "type": "string" + }, + "managedIdentityObjectId": { + "type": "string" + }, + "managedIdentityClientId": { + "type": "string" + }, "storageAccountName": { "type": "string" }, "containerName": { "type": "string" }, - "identity": { + "containerAppName": { + "type": "string", + "defaultValue": "[format('{0}containerapp', parameters('solutionName'))]" + }, + "environmentName": { + "type": "string", + "defaultValue": "[format('{0}containerappenv', parameters('solutionName'))]" + }, + "imageName": { + "type": "string", + "defaultValue": "python:3.11-alpine" + }, + "setupCopyKbFiles": { + "type": "string", + "defaultValue": "[format('{0}Deployment/scripts/copy_kb_files.sh', parameters('baseUrl'))]" + }, + "setupCreateIndexScriptsUrl": { + "type": "string", + "defaultValue": "[format('{0}Deployment/scripts/run_create_index_scripts.sh', parameters('baseUrl'))]" + }, + "createSqlUserAndRoleScriptsUrl": { + "type": "string", + "defaultValue": "[format('{0}Deployment/scripts/add_user_scripts/create-sql-user-and-role.ps1', parameters('baseUrl'))]" + }, + "keyVaultName": { "type": "string" }, - "baseUrl": { + "sqlServerName": { + "type": "string" + }, + "sqlDbName": { + "type": "string" + }, + "sqlUsers": { + "type": "array", + "defaultValue": [] + }, + "logAnalyticsWorkspaceResourceName": { "type": "string" } }, + "variables": { + "resourceGroupName": "[resourceGroup().name]" + }, "resources": [ { - "type": "Microsoft.Resources/deploymentScripts", - "apiVersion": "2020-10-01", - "name": "copy_demo_Data", - "kind": "AzureCLI", + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2022-03-01", + "name": "[parameters('environmentName')]", + "location": "[parameters('solutionLocation')]", + "properties": { + "zoneRedundant": false, + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceResourceName')), '2020-10-01').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceResourceName')), '2020-10-01').primarySharedKey]" + } + } + } + }, + { + "type": "Microsoft.App/containerApps", + "apiVersion": "2022-03-01", + "name": "[parameters('containerAppName')]", "location": "[parameters('solutionLocation')]", "identity": { "type": "UserAssigned", "userAssignedIdentities": { - "[format('{0}', parameters('identity'))]": {} + "[format('{0}', parameters('managedIdentityObjectId'))]": {} } }, "properties": { - "azCliVersion": "2.50.0", - "primaryScriptUri": "[format('{0}Deployment/scripts/copy_kb_files.sh', parameters('baseUrl'))]", - "arguments": "[format('{0} {1} {2}', parameters('storageAccountName'), parameters('containerName'), parameters('baseUrl'))]", - "timeout": "PT1H", - "retentionInterval": "PT1H", - "cleanupPreference": "OnSuccess" - } + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', parameters('environmentName'))]", + "configuration": { + "ingress": null, + "activeRevisionsMode": "Single" + }, + "template": { + "scale": { + "minReplicas": 1, + "maxReplicas": 1 + }, + "containers": [ + { + "name": "[parameters('containerAppName')]", + "image": "[parameters('imageName')]", + "resources": { + "cpu": 2, + "memory": "4.0Gi" + }, + "command": [ + "/bin/sh", + "-c", + "[format('mkdir -p /scripts && apk add --no-cache curl bash jq py3-pip gcc musl-dev libffi-dev openssl-dev python3-dev && pip install --upgrade azure-cli && apk add --no-cache --virtual .build-deps build-base unixodbc-dev && curl -s -o msodbcsql18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/msodbcsql18_18.4.1.1-1_amd64.apk && curl -s -o mssql-tools18_18.4.1.1-1_amd64.apk https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_18.4.1.1-1_amd64.apk && apk add --allow-untrusted msodbcsql18_18.4.1.1-1_amd64.apk && apk add --allow-untrusted mssql-tools18_18.4.1.1-1_amd64.apk && curl -s -o /scripts/copy_kb_files.sh {0} && chmod +x /scripts/copy_kb_files.sh && sh -x /scripts/copy_kb_files.sh {1} {2} {3} {4} && curl -s -o /scripts/run_create_index_scripts.sh {5} && chmod +x /scripts/run_create_index_scripts.sh && sh -x /scripts/run_create_index_scripts.sh {6} {7} {8} && apk add --no-cache ca-certificates less ncurses-terminfo-base krb5-libs libgcc libintl libssl3 libstdc++ tzdata userspace-rcu zlib icu-libs curl && apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache lttng-ust openssh-client && curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz && mkdir -p /opt/microsoft/powershell/7 && tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7 && chmod +x /opt/microsoft/powershell/7/pwsh && ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh && curl -s -o /scripts/create-sql-user-and-role.ps1 {9} && chmod +x /scripts/create-sql-user-and-role.ps1 && pwsh -File /scripts/create-sql-user-and-role.ps1 -SqlServerName {10} -SqlDatabaseName {11} -ClientId {12} -DisplayName {13} -ManagedIdentityClientId {14} -DatabaseRole {15} && pwsh -File /scripts/create-sql-user-and-role.ps1 -SqlServerName {16} -SqlDatabaseName {17} -ClientId {18} -DisplayName {19} -ManagedIdentityClientId {20} -DatabaseRole {21} && pwsh -File /scripts/create-sql-user-and-role.ps1 -SqlServerName {22} -SqlDatabaseName {23} -ClientId {24} -DisplayName {25} -ManagedIdentityClientId {26} -DatabaseRole {27} && az login --identity --client-id {28} && az containerapp update --name {29} --resource-group {30} --min-replicas 0 --cpu 0.25 --memory 0.5Gi && az containerapp revision deactivate -g {31} --revision $(az containerapp revision list -n {32} -g {33} --query \"[0].name\" -o tsv) && echo \"Container app setup completed successfully.\"', parameters('setupCopyKbFiles'), parameters('storageAccountName'), parameters('containerName'), parameters('baseUrl'), parameters('managedIdentityClientId'), parameters('setupCreateIndexScriptsUrl'), parameters('baseUrl'), parameters('keyVaultName'), parameters('managedIdentityClientId'), parameters('createSqlUserAndRoleScriptsUrl'), parameters('sqlServerName'), parameters('sqlDbName'), parameters('sqlUsers')[0].principalId, parameters('sqlUsers')[0].principalName, parameters('managedIdentityClientId'), parameters('sqlUsers')[0].databaseRoles[0], parameters('sqlServerName'), parameters('sqlDbName'), parameters('sqlUsers')[1].principalId, parameters('sqlUsers')[1].principalName, parameters('managedIdentityClientId'), parameters('sqlUsers')[1].databaseRoles[0], parameters('sqlServerName'), parameters('sqlDbName'), parameters('sqlUsers')[1].principalId, parameters('sqlUsers')[1].principalName, parameters('managedIdentityClientId'), parameters('sqlUsers')[1].databaseRoles[1], parameters('managedIdentityClientId'), parameters('containerAppName'), variables('resourceGroupName'), variables('resourceGroupName'), parameters('containerAppName'), variables('resourceGroupName'))]" + ], + "env": [ + { + "name": "STORAGE_ACCOUNT_NAME", + "value": "[parameters('storageAccountName')]" + }, + { + "name": "CONTAINER_NAME", + "value": "[parameters('containerName')]" + }, + { + "name": "APPSETTING_WEBSITE_SITE_NAME", + "value": "DUMMY" + } + ] + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', parameters('environmentName'))]" + ] } ] } }, "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'deploy_azure_function')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_sql_db')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_storage_account')]" ] }, @@ -1039,6 +1233,12 @@ }, "streamTextSystemPrompt": { "value": "[variables('functionAppStreamTextSystemPrompt')]" + }, + "userassignedIdentityClientId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityFnAppOutput.value.clientId]" + }, + "userassignedIdentityId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityFnAppOutput.value.id]" } }, "template": { @@ -1047,8 +1247,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "11955391860946221428" + "version": "0.34.44.8038", + "templateHash": "14133672307118323245" } }, "parameters": { @@ -1111,11 +1311,17 @@ "metadata": { "description": "Azure Function App Stream Text System Prompt" } + }, + "userassignedIdentityId": { + "type": "string" + }, + "userassignedIdentityClientId": { + "type": "string" } }, "variables": { "functionAppName": "[format('{0}fn', parameters('solutionName'))]", - "azureOpenAIDeploymentModel": "gpt-4", + "azureOpenAIDeploymentModel": "gpt-4o-mini", "azureOpenAIEmbeddingDeployment": "text-embedding-ada-002", "valueOne": "1" }, @@ -1183,7 +1389,10 @@ "location": "[parameters('solutionLocation')]", "kind": "functionapp", "identity": { - "type": "SystemAssigned" + "type": "SystemAssigned, UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('userassignedIdentityId'))]": {} + } }, "properties": { "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', format('{0}env', parameters('solutionName')))]", @@ -1265,6 +1474,10 @@ { "name": "AZURE_OPENAI_STREAM_TEXT_SYSTEM_PROMPT", "value": "[parameters('streamTextSystemPrompt')]" + }, + { + "name": "SQLDB_USER_MID", + "value": "[parameters('userassignedIdentityClientId')]" } ] } @@ -1274,12 +1487,19 @@ "[resourceId('Microsoft.App/managedEnvironments', format('{0}env', parameters('solutionName')))]" ] } - ] + ], + "outputs": { + "logAnalyticsWorkspaceName": { + "type": "string", + "value": "[format('workspace-{0}', parameters('solutionName'))]" + } + } } }, "dependsOn": [ "[resourceId('Microsoft.Resources/deployments', 'deploy_azure_open_ai')]", "[resourceId('Microsoft.Resources/deployments', 'deploy_ai_search_service')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_sql_db')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_storage_account')]" ] @@ -1307,8 +1527,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "11501780755841251697" + "version": "0.34.44.8038", + "templateHash": "12214749843316064247" } }, "parameters": { @@ -1421,8 +1641,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "16538647807599840496" + "version": "0.34.44.8038", + "templateHash": "9249647694914976345" } }, "parameters": { @@ -1884,86 +2104,6 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_storage_account')]" ] }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2022-09-01", - "name": "deploy_index_scripts", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "solutionLocation": { - "value": "[variables('solutionLocation')]" - }, - "identity": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.id]" - }, - "baseUrl": { - "value": "[variables('baseUrl')]" - }, - "keyVaultName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_keyvault'), '2022-09-01').outputs.keyvaultOutput.value.name]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "9968723784632879247" - } - }, - "parameters": { - "solutionLocation": { - "type": "string", - "metadata": { - "description": "Specifies the location for resources." - } - }, - "baseUrl": { - "type": "string" - }, - "keyVaultName": { - "type": "string" - }, - "identity": { - "type": "string" - } - }, - "resources": [ - { - "type": "Microsoft.Resources/deploymentScripts", - "apiVersion": "2020-10-01", - "name": "create_search_indexes", - "kind": "AzureCLI", - "location": "[parameters('solutionLocation')]", - "identity": { - "type": "UserAssigned", - "userAssignedIdentities": { - "[format('{0}', parameters('identity'))]": {} - } - }, - "properties": { - "azCliVersion": "2.52.0", - "primaryScriptUri": "[format('{0}Deployment/scripts/run_create_index_scripts.sh', parameters('baseUrl'))]", - "arguments": "[format('{0} {1}', parameters('baseUrl'), parameters('keyVaultName'))]", - "timeout": "PT1H", - "retentionInterval": "PT1H", - "cleanupPreference": "OnSuccess" - } - } - ] - } - }, - "dependsOn": [ - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_keyvault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]" - ] - }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", @@ -2018,13 +2158,13 @@ "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_azure_open_ai'), '2022-09-01').outputs.openAIOutput.value.openAPIEndpoint]" }, "AzureOpenAIModel": { - "value": "gpt-4" + "value": "gpt-4o-mini" }, "AzureOpenAIKey": { "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_azure_open_ai'), '2022-09-01').outputs.openAIOutput.value.openAPIKey]" }, "AzureOpenAIModelName": { - "value": "gpt-4" + "value": "gpt-4o-mini" }, "AzureOpenAITemperature": { "value": "0" @@ -2103,6 +2243,12 @@ }, "Appversion": { "value": "[variables('appversion')]" + }, + "userassignedIdentityClientId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityWebAppOutput.value.clientId]" + }, + "userassignedIdentityId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityWebAppOutput.value.id]" } }, "template": { @@ -2111,8 +2257,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "4502112701228496974" + "version": "0.34.44.8038", + "templateHash": "16559957349527009263" } }, "parameters": { @@ -2265,7 +2411,7 @@ }, "AzureOpenAIModelName": { "type": "string", - "defaultValue": "gpt-4", + "defaultValue": "gpt-4o-mini", "metadata": { "description": "Azure OpenAI Model Name" } @@ -2488,6 +2634,12 @@ }, "Appversion": { "type": "string" + }, + "userassignedIdentityId": { + "type": "string" + }, + "userassignedIdentityClientId": { + "type": "string" } }, "variables": { @@ -2514,7 +2666,10 @@ "name": "[parameters('WebsiteName')]", "location": "[resourceGroup().location]", "identity": { - "type": "SystemAssigned" + "type": "SystemAssigned, UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('userassignedIdentityId'))]": {} + } }, "properties": { "serverFarmId": "[parameters('HostingPlanName')]", @@ -2707,6 +2862,10 @@ { "name": "UWSGI_THREADS", "value": "2" + }, + { + "name": "SQLDB_USER_MID", + "value": "[parameters('userassignedIdentityClientId')]" } ], "linuxFxVersion": "[variables('WebAppImageName')]" @@ -2756,8 +2915,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.32.4.45862", - "templateHash": "2813064152180428298" + "version": "0.34.44.8038", + "templateHash": "14262313259972528331" }, "description": "Creates a SQL role assignment under an Azure Cosmos DB account." }, @@ -2800,6 +2959,7 @@ "[resourceId('Microsoft.Resources/deployments', 'deploy_ai_search_service')]", "[resourceId('Microsoft.Resources/deployments', 'deploy_azure_function_script_url')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_cosmos_db')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_sql_db')]" ] } diff --git a/ClientAdvisor/Deployment/scripts/add_user_scripts/create-sql-user-and-role.ps1 b/ClientAdvisor/Deployment/scripts/add_user_scripts/create-sql-user-and-role.ps1 new file mode 100644 index 00000000..2e3686b2 --- /dev/null +++ b/ClientAdvisor/Deployment/scripts/add_user_scripts/create-sql-user-and-role.ps1 @@ -0,0 +1,77 @@ +#Requires -Version 7.0 + +<# +.SYNOPSIS + Creates a SQL user and assigns the user account to one or more roles. + +.DESCRIPTION + During an application deployment, the managed identity (and potentially the developer identity) + must be added to the SQL database as a user and assigned to one or more roles. This script + accomplishes this task using the owner-managed identity for authentication. + +.PARAMETER SqlServerName + The name of the Azure SQL Server resource. + +.PARAMETER SqlDatabaseName + The name of the Azure SQL Database where the user will be created. + +.PARAMETER ClientId + The Client (Principal) ID (GUID) of the identity to be added. + +.PARAMETER DisplayName + The Object (Principal) display name of the identity to be added. + +.PARAMETER ManagedIdentityClientId + The Client ID of the managed identity that will authenticate to the SQL database. + +.PARAMETER DatabaseRole + The database role that should be assigned to the user (e.g., db_datareader, db_datawriter, db_owner). +#> + +Param( + [string] $SqlServerName, + [string] $SqlDatabaseName, + [string] $ClientId, + [string] $DisplayName, + [string] $ManagedIdentityClientId, + [string] $DatabaseRole +) + +function Resolve-Module($moduleName) { + # If module is imported; say that and do nothing + if (Get-Module | Where-Object { $_.Name -eq $moduleName }) { + Write-Debug "Module $moduleName is already imported" + } elseif (Get-Module -ListAvailable | Where-Object { $_.Name -eq $moduleName }) { + Import-Module $moduleName + } elseif (Find-Module -Name $moduleName | Where-Object { $_.Name -eq $moduleName }) { + Install-Module $moduleName -Force -Scope CurrentUser + Import-Module $moduleName + } else { + Write-Error "Module $moduleName not found" + [Environment]::exit(1) + } +} + +### +### MAIN SCRIPT +### +Resolve-Module -moduleName Az.Resources +Resolve-Module -moduleName SqlServer + +$sql = @" +DECLARE @username nvarchar(max) = N'$($DisplayName)'; +DECLARE @clientId uniqueidentifier = '$($ClientId)'; +DECLARE @sid NVARCHAR(max) = CONVERT(VARCHAR(max), CONVERT(VARBINARY(16), @clientId), 1); +DECLARE @cmd NVARCHAR(max) = N'CREATE USER [' + @username + '] WITH SID = ' + @sid + ', TYPE = E;'; +IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = @username) +BEGIN + EXEC(@cmd) +END +EXEC sp_addrolemember '$($DatabaseRole)', @username; +"@ + +Write-Output "`nSQL:`n$($sql)`n`n" + +Connect-AzAccount -Identity -AccountId $ManagedIdentityClientId +$token = (Get-AzAccessToken -ResourceUrl https://database.windows.net/).Token +Invoke-SqlCmd -ServerInstance "$SqlServerName" -Database $SqlDatabaseName -AccessToken $token -Query $sql -ErrorAction 'Stop' \ No newline at end of file diff --git a/ClientAdvisor/Deployment/scripts/copy_kb_files.sh b/ClientAdvisor/Deployment/scripts/copy_kb_files.sh index 415089b9..9e14442f 100644 --- a/ClientAdvisor/Deployment/scripts/copy_kb_files.sh +++ b/ClientAdvisor/Deployment/scripts/copy_kb_files.sh @@ -4,6 +4,7 @@ storageAccount="$1" fileSystem="$2" baseUrl="$3" +managedIdentityClientId="$4" zipFileName1="clientdata.zip" extractedFolder1="clientdata" @@ -13,18 +14,24 @@ zipFileName2="clienttranscripts.zip" extractedFolder2="clienttranscripts" zipUrl2=${baseUrl}"Deployment/data/clienttranscripts.zip" +# Create folders if they do not exist +mkdir -p "/mnt/azscripts/azscriptinput/$extractedFolder1" +mkdir -p "/mnt/azscripts/azscriptinput/$extractedFolder2" + # Download the zip file -curl --output "$zipFileName1" "$zipUrl1" -curl --output "$zipFileName2" "$zipUrl2" +curl --output /mnt/azscripts/azscriptinput/"$zipFileName1" "$zipUrl1" +curl --output /mnt/azscripts/azscriptinput/"$zipFileName2" "$zipUrl2" # Extract the zip file unzip /mnt/azscripts/azscriptinput/"$zipFileName1" -d /mnt/azscripts/azscriptinput/"$extractedFolder1" unzip /mnt/azscripts/azscriptinput/"$zipFileName2" -d /mnt/azscripts/azscriptinput/"$extractedFolder2" +echo "Script Started" + # Authenticate with Azure using managed identity -az login --identity +az login --identity --client-id ${managedIdentityClientId} # Using az storage blob upload-batch to upload files with managed identity authentication, as the az storage fs directory upload command is not working with managed identity authentication. -az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder1" --source /mnt/azscripts/azscriptinput/"$extractedFolder1" --auth-mode login --pattern '*' -az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder2" --source /mnt/azscripts/azscriptinput/"$extractedFolder2" --auth-mode login --pattern '*' +az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder1" --source /mnt/azscripts/azscriptinput/"$extractedFolder1" --auth-mode login --pattern '*' --overwrite +az storage blob upload-batch --account-name "$storageAccount" --destination data/"$extractedFolder2" --source /mnt/azscripts/azscriptinput/"$extractedFolder2" --auth-mode login --pattern '*' --overwrite # az storage fs directory upload -f "$fileSystem" --account-name "$storageAccount" -s "$extractedFolder1" --account-key "$accountKey" --recursive # az storage fs directory upload -f "$fileSystem" --account-name "$storageAccount" -s "$extractedFolder2" --account-key "$accountKey" --recursive diff --git a/ClientAdvisor/Deployment/scripts/index_scripts/create_search_index.py b/ClientAdvisor/Deployment/scripts/index_scripts/create_search_index.py index af89d88c..b363fcfb 100644 --- a/ClientAdvisor/Deployment/scripts/index_scripts/create_search_index.py +++ b/ClientAdvisor/Deployment/scripts/index_scripts/create_search_index.py @@ -1,5 +1,6 @@ #Get Azure Key Vault Client key_vault_name = 'kv_to-be-replaced' #'nc6262-kv-2fpeafsylfd2e' +managed_identity_client_id = 'mici_to-be-replaced' index_name = "transcripts_index" @@ -14,7 +15,7 @@ def get_secrets_from_kv(kv_name, secret_name): # Set the name of the Azure Key Vault key_vault_name = kv_name - credential = DefaultAzureCredential() + credential = DefaultAzureCredential(managed_identity_client_id=managed_identity_client_id) # Create a secret client object using the credential and Key Vault name secret_client = SecretClient(vault_url=f"https://{key_vault_name}.vault.azure.net/", credential=credential) @@ -179,7 +180,7 @@ def chunk_data(text): ) account_name = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-NAME") -credential = DefaultAzureCredential() +credential = DefaultAzureCredential(managed_identity_client_id=managed_identity_client_id) account_url = f"https://{account_name}.dfs.core.windows.net" diff --git a/ClientAdvisor/Deployment/scripts/index_scripts/create_sql_tables.py b/ClientAdvisor/Deployment/scripts/index_scripts/create_sql_tables.py index cb43e8e8..adaa95a4 100644 --- a/ClientAdvisor/Deployment/scripts/index_scripts/create_sql_tables.py +++ b/ClientAdvisor/Deployment/scripts/index_scripts/create_sql_tables.py @@ -1,4 +1,5 @@ key_vault_name = 'kv_to-be-replaced' +managed_identity_client_id = 'mici_to-be-replaced' import pandas as pd import pymssql @@ -7,10 +8,13 @@ from azure.keyvault.secrets import SecretClient from azure.identity import DefaultAzureCredential +from azure.identity import DefaultAzureCredential +import pyodbc +import struct def get_secrets_from_kv(kv_name, secret_name): key_vault_name = kv_name # Set the name of the Azure Key Vault - credential = DefaultAzureCredential() + credential = DefaultAzureCredential(managed_identity_client_id=managed_identity_client_id) secret_client = SecretClient(vault_url=f"https://{key_vault_name}.vault.azure.net/", credential=credential) # Create a secret client object using the credential and Key Vault name return(secret_client.get_secret(secret_name).value) # Retrieve the secret value @@ -18,8 +22,25 @@ def get_secrets_from_kv(kv_name, secret_name): database = get_secrets_from_kv(key_vault_name,"SQLDB-DATABASE") username = get_secrets_from_kv(key_vault_name,"SQLDB-USERNAME") password = get_secrets_from_kv(key_vault_name,"SQLDB-PASSWORD") +driver = "{ODBC Driver 18 for SQL Server}" -conn = pymssql.connect(server, username, password, database) + +#conn = pymssql.connect(server, username, password, database) +credential = DefaultAzureCredential(managed_identity_client_id=managed_identity_client_id) + +token_bytes = credential.get_token( + "https://database.windows.net/.default" + ).token.encode("utf-16-LE") +token_struct = struct.pack(f"