From c5f069f11b7e8d0cc4e99cda06460427226e8bdb Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Thu, 7 Nov 2024 17:40:19 -0500 Subject: [PATCH 001/107] add postgressql lib --- poetry.lock | 68 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index c321375ed..52ae77b5b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -355,6 +355,72 @@ files = [ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, + {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"}, + {file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"}, + {file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"}, + {file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"}, + {file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, + {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, + {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34"}, + {file = "asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4"}, + {file = "asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547"}, + {file = "asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a"}, + {file = "asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} + +[package.extras] +docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] +gssauth = ["gssapi", "sspilib"] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] + [[package]] name = "attrs" version = "23.2.0" @@ -6627,4 +6693,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "826226f49f211954e1a565360e48f0e655807b7e7f370780bd1fed30f2bccac4" +content-hash = "9fdda3cad1996d13d27fce05f7b9a7e13b60937b5c2fcfa58cb9bd1b93ddc97f" diff --git a/pyproject.toml b/pyproject.toml index c99e50d78..4885a0345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ jsonschema = "^4.23.0" semantic-kernel = {version = "1.3.0", python = "<3.13"} azure-ai-ml = "^1.21.1" azure-cosmos = "^4.7.0" +asyncpg = "^0.30.0" [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" From 4fa9aa9bc8d2e4a13942ec08d052b051f894ad66 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Thu, 7 Nov 2024 18:10:17 -0500 Subject: [PATCH 002/107] Create postgresdbservice.py --- .../chat_history/postgresdbservice.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 code/backend/batch/utilities/chat_history/postgresdbservice.py diff --git a/code/backend/batch/utilities/chat_history/postgresdbservice.py b/code/backend/batch/utilities/chat_history/postgresdbservice.py new file mode 100644 index 000000000..6343448dc --- /dev/null +++ b/code/backend/batch/utilities/chat_history/postgresdbservice.py @@ -0,0 +1,106 @@ +import asyncpg +import uuid +from datetime import datetime, timezone +class PostgresConversationClient: + + def __init__(self, dsn: str, enable_message_feedback: bool = False): + self.dsn = dsn + self.enable_message_feedback = enable_message_feedback + self.conn = None + + async def connect(self): + self.conn = await asyncpg.connect(self.dsn) + async def close(self): + if self.conn: + await self.conn.close() + async def ensure(self): + if not self.conn: + return False, "PostgreSQL client not initialized correctly" + return True, "PostgreSQL client initialized successfully" + async def create_conversation(self, user_id, title=''): + conversation_id = str(uuid.uuid4()) + utc_now = datetime.now(timezone.utc) + created_at = utc_now.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' + query = """ + INSERT INTO conversations (id, type, created_at, updated_at, user_id, title) + VALUES ($1, 'conversation', $2, $2, $3, $4) + RETURNING * + """ + conversation = await self.conn.fetchrow(query, conversation_id, created_at, user_id, title) + return dict(conversation) if conversation else False + async def upsert_conversation(self, conversation): + query = """ + INSERT INTO conversations (id, type, created_at, updated_at, user_id, title) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (id) DO UPDATE SET + updated_at = EXCLUDED.updated_at, + title = EXCLUDED.title + RETURNING * + """ + updated_conversation = await self.conn.fetchrow( + query, conversation['id'], conversation['type'], conversation['createdAt'], + conversation['updatedAt'], conversation['userId'], conversation['title'] + ) + return dict(updated_conversation) if updated_conversation else False + async def delete_conversation(self, user_id, conversation_id): + query = "DELETE FROM conversations WHERE id = $1 AND user_id = $2" + await self.conn.execute(query, conversation_id, user_id) + return True + async def delete_messages(self, conversation_id, user_id): + query = "DELETE FROM messages WHERE conversation_id = $1 AND user_id = $2 RETURNING *" + messages = await self.conn.fetch(query, conversation_id, user_id) + return [dict(message) for message in messages] + async def get_conversations(self, user_id, limit=None, sort_order='DESC', offset=0): + try: + offset = int(offset) # Ensure offset is an integer + except ValueError: + raise ValueError("Offset must be an integer.") + # Base query without LIMIT and OFFSET + query = f""" + SELECT * FROM conversations + WHERE user_id = $1 AND type = 'conversation' + ORDER BY updated_at {sort_order} + """ + # Append LIMIT and OFFSET to the query if limit is specified + if limit is not None: + try: + limit = int(limit) # Ensure limit is an integer + query += " LIMIT $2 OFFSET $3" + # Fetch records with LIMIT and OFFSET + conversations = await self.conn.fetch(query, user_id, limit, offset) + except ValueError: + raise ValueError("Limit must be an integer.") + else: + # Fetch records without LIMIT and OFFSET + conversations = await self.conn.fetch(query, user_id) + return [dict(conversation) for conversation in conversations] + async def get_conversation(self, user_id, conversation_id): + query = "SELECT * FROM conversations WHERE id = $1 AND user_id = $2 AND type = 'conversation'" + conversation = await self.conn.fetchrow(query, conversation_id, user_id) + return dict(conversation) if conversation else None + async def create_message(self, uuid, conversation_id, user_id, input_message: dict): + message_id = uuid + utc_now = datetime.now(timezone.utc) + created_at = utc_now.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' + query = """ + INSERT INTO messages (id, type, created_at, updated_at, user_id, conversation_id, role, content, feedback) + VALUES ($1, 'message', $2, $2, $3, $4, $5, $6, $7) + RETURNING * + """ + feedback = '' if self.enable_message_feedback else None + message = await self.conn.fetchrow(query, message_id, created_at, user_id, conversation_id, input_message['role'], input_message['content'], feedback) + + if message: + update_query = "UPDATE conversations SET updated_at = $1 WHERE id = $2 AND user_id = $3 RETURNING *" + await self.conn.execute(update_query, created_at, conversation_id, user_id) + return dict(message) + else: + return False + async def update_message_feedback(self, user_id, message_id, feedback): + query = "UPDATE messages SET feedback = $1 WHERE id = $2 AND user_id = $3 RETURNING *" + message = await self.conn.fetchrow(query, feedback, message_id, user_id) + return dict(message) if message else False + async def get_messages(self, user_id, conversation_id): + query = "SELECT * FROM messages WHERE conversation_id = $1 AND user_id = $2 ORDER BY created_at ASC" + messages = await self.conn.fetch(query, conversation_id, user_id) + return [dict(message) for message in messages] \ No newline at end of file From 42eca2ec6d13cf2f34e9ba55b291a54aab2e37ae Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Thu, 7 Nov 2024 23:12:59 -0500 Subject: [PATCH 003/107] update ayncgp lib --- poetry.lock | 86 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 52ae77b5b..c7c10ce29 100644 --- a/poetry.lock +++ b/poetry.lock @@ -415,6 +415,8 @@ files = [ [package.dependencies] async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} +gssapi = {version = "*", optional = true, markers = "platform_system != \"Windows\" and extra == \"gssauth\""} +sspilib = {version = "*", optional = true, markers = "platform_system == \"Windows\" and extra == \"gssauth\""} [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] @@ -1835,6 +1837,43 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] +[[package]] +name = "gssapi" +version = "1.9.0" +description = "Python GSSAPI Wrapper" +optional = false +python-versions = ">=3.8" +files = [ + {file = "gssapi-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:261e00ac426d840055ddb2199f4989db7e3ce70fa18b1538f53e392b4823e8f1"}, + {file = "gssapi-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14a1ae12fdf1e4c8889206195ba1843de09fe82587fa113112887cd5894587c6"}, + {file = "gssapi-1.9.0-cp310-cp310-win32.whl", hash = "sha256:2a9c745255e3a810c3e8072e267b7b302de0705f8e9a0f2c5abc92fe12b9475e"}, + {file = "gssapi-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfc1b4c0bfe9f539537601c9f187edc320daf488f694e50d02d0c1eb37416962"}, + {file = "gssapi-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67d9be5e34403e47fb5749d5a1ad4e5a85b568e6a9add1695edb4a5b879f7560"}, + {file = "gssapi-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11e9b92cef11da547fc8c210fa720528fd854038504103c1b15ae2a89dce5fcd"}, + {file = "gssapi-1.9.0-cp311-cp311-win32.whl", hash = "sha256:6c5f8a549abd187687440ec0b72e5b679d043d620442b3637d31aa2766b27cbe"}, + {file = "gssapi-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:59e1a1a9a6c5dc430dc6edfcf497f5ca00cf417015f781c9fac2e85652cd738f"}, + {file = "gssapi-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b66a98827fbd2864bf8993677a039d7ba4a127ca0d2d9ed73e0ef4f1baa7fd7f"}, + {file = "gssapi-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bddd1cc0c9859c5e0fd96d4d88eb67bd498fdbba45b14cdccfe10bfd329479f"}, + {file = "gssapi-1.9.0-cp312-cp312-win32.whl", hash = "sha256:10134db0cf01bd7d162acb445762dbcc58b5c772a613e17c46cf8ad956c4dfec"}, + {file = "gssapi-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:e28c7d45da68b7e36ed3fb3326744bfe39649f16e8eecd7b003b082206039c76"}, + {file = "gssapi-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cea344246935b5337e6f8a69bb6cc45619ab3a8d74a29fcb0a39fd1e5843c89c"}, + {file = "gssapi-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a5786bd9fcf435bd0c87dc95ae99ad68cefcc2bcc80c71fef4cb0ccdfb40f1e"}, + {file = "gssapi-1.9.0-cp313-cp313-win32.whl", hash = "sha256:c99959a9dd62358e370482f1691e936cb09adf9a69e3e10d4f6a097240e9fd28"}, + {file = "gssapi-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a2e43f50450e81fe855888c53df70cdd385ada979db79463b38031710a12acd9"}, + {file = "gssapi-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0e378d62b2fc352ca0046030cda5911d808a965200f612fdd1d74501b83e98f"}, + {file = "gssapi-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b74031c70864d04864b7406c818f41be0c1637906fb9654b06823bcc79f151dc"}, + {file = "gssapi-1.9.0-cp38-cp38-win32.whl", hash = "sha256:f2f3a46784d8127cc7ef10d3367dedcbe82899ea296710378ccc9b7cefe96f4c"}, + {file = "gssapi-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:a81f30cde21031e7b1f8194a3eea7285e39e551265e7744edafd06eadc1c95bc"}, + {file = "gssapi-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbc93fdadd5aab9bae594538b2128044b8c5cdd1424fe015a465d8a8a587411a"}, + {file = "gssapi-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b2a3c0a9beb895942d4b8e31f515e52c17026e55aeaa81ee0df9bbfdac76098"}, + {file = "gssapi-1.9.0-cp39-cp39-win32.whl", hash = "sha256:060b58b455d29ab8aca74770e667dca746264bee660ac5b6a7a17476edc2c0b8"}, + {file = "gssapi-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:11c9fe066edb0fa0785697eb0cecf2719c7ad1d9f2bf27be57b647a617bcfaa5"}, + {file = "gssapi-1.9.0.tar.gz", hash = "sha256:f468fac8f3f5fca8f4d1ca19e3cd4d2e10bd91074e7285464b22715d13548afe"}, +] + +[package.dependencies] +decorator = "*" + [[package]] name = "h11" version = "0.14.0" @@ -5951,6 +5990,51 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] +[[package]] +name = "sspilib" +version = "0.2.0" +description = "SSPI API bindings for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sspilib-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34f566ba8b332c91594e21a71200de2d4ce55ca5a205541d4128ed23e3c98777"}, + {file = "sspilib-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b11e4f030de5c5de0f29bcf41a6e87c9fd90cb3b0f64e446a6e1d1aef4d08f5"}, + {file = "sspilib-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e82f87d77a9da62ce1eac22f752511a99495840177714c772a9d27b75220f78"}, + {file = "sspilib-0.2.0-cp310-cp310-win32.whl", hash = "sha256:e436fa09bcf353a364a74b3ef6910d936fa8cd1493f136e517a9a7e11b319c57"}, + {file = "sspilib-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:850a17c98d2b8579b183ce37a8df97d050bc5b31ab13f5a6d9e39c9692fe3754"}, + {file = "sspilib-0.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:a4d788a53b8db6d1caafba36887d5ac2087e6b6be6f01eb48f8afea6b646dbb5"}, + {file = "sspilib-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e0943204c8ba732966fdc5b69e33cf61d8dc6b24e6ed875f32055d9d7e2f76cd"}, + {file = "sspilib-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1cdfc5ec2f151f26e21aa50ccc7f9848c969d6f78264ae4f38347609f6722df"}, + {file = "sspilib-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6c33495a3de1552120c4a99219ebdd70e3849717867b8cae3a6a2f98fef405"}, + {file = "sspilib-0.2.0-cp311-cp311-win32.whl", hash = "sha256:400d5922c2c2261009921157c4b43d868e84640ad86e4dc84c95b07e5cc38ac6"}, + {file = "sspilib-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3e7d19c16ba9189ef8687b591503db06cfb9c5eb32ab1ca3bb9ebc1a8a5f35c"}, + {file = "sspilib-0.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f65c52ead8ce95eb78a79306fe4269ee572ef3e4dcc108d250d5933da2455ecc"}, + {file = "sspilib-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:abac93a90335590b49ef1fc162b538576249c7f58aec0c7bcfb4b860513979b4"}, + {file = "sspilib-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1208720d8e431af674c5645cec365224d035f241444d5faa15dc74023ece1277"}, + {file = "sspilib-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48dceb871ecf9cf83abdd0e6db5326e885e574f1897f6ae87d736ff558f4bfa"}, + {file = "sspilib-0.2.0-cp312-cp312-win32.whl", hash = "sha256:bdf9a4f424add02951e1f01f47441d2e69a9910471e99c2c88660bd8e184d7f8"}, + {file = "sspilib-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:40a97ca83e503a175d1dc9461836994e47e8b9bcf56cab81a2c22e27f1993079"}, + {file = "sspilib-0.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8ffc09819a37005c66a580ff44f544775f9745d5ed1ceeb37df4e5ff128adf36"}, + {file = "sspilib-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:40ff410b64198cf1d704718754fc5fe7b9609e0c49bf85c970f64c6fc2786db4"}, + {file = "sspilib-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:02d8e0b6033de8ccf509ba44fdcda7e196cdedc0f8cf19eb22c5e4117187c82f"}, + {file = "sspilib-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7943fe14f8f6d72623ab6401991aa39a2b597bdb25e531741b37932402480f"}, + {file = "sspilib-0.2.0-cp313-cp313-win32.whl", hash = "sha256:b9044d6020aa88d512e7557694fe734a243801f9a6874e1c214451eebe493d92"}, + {file = "sspilib-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c39a698491f43618efca8776a40fb7201d08c415c507f899f0df5ada15abefaa"}, + {file = "sspilib-0.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:863b7b214517b09367511c0ef931370f0386ed2c7c5613092bf9b106114c4a0e"}, + {file = "sspilib-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0ede7afba32f2b681196c0b8520617d99dc5d0691d04884d59b476e31b41286"}, + {file = "sspilib-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bd95df50efb6586054963950c8fa91ef994fb73c5c022c6f85b16f702c5314da"}, + {file = "sspilib-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9460258d3dc3f71cc4dcfd6ac078e2fe26f272faea907384b7dd52cb91d9ddcc"}, + {file = "sspilib-0.2.0-cp38-cp38-win32.whl", hash = "sha256:6fa9d97671348b97567020d82fe36c4211a2cacf02abbccbd8995afbf3a40bfc"}, + {file = "sspilib-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:32422ad7406adece12d7c385019b34e3e35ff88a7c8f3d7c062da421772e7bfa"}, + {file = "sspilib-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6944a0d7fe64f88c9bde3498591acdb25b178902287919b962c398ed145f71b9"}, + {file = "sspilib-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0216344629b0f39c2193adb74d7e1bed67f1bbd619e426040674b7629407eba9"}, + {file = "sspilib-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5f84b9f614447fc451620c5c44001ed48fead3084c7c9f2b9cefe1f4c5c3d0"}, + {file = "sspilib-0.2.0-cp39-cp39-win32.whl", hash = "sha256:b290eb90bf8b8136b0a61b189629442052e1a664bd78db82928ec1e81b681fb5"}, + {file = "sspilib-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:404c16e698476e500a7fe67be5457fadd52d8bdc9aeb6c554782c8f366cc4fc9"}, + {file = "sspilib-0.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:8697e5dd9229cd3367bca49fba74e02f867759d1d416a717e26c3088041b9814"}, + {file = "sspilib-0.2.0.tar.gz", hash = "sha256:4d6cd4290ca82f40705efeb5e9107f7abcd5e647cb201a3d04371305938615b8"}, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -6693,4 +6777,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "9fdda3cad1996d13d27fce05f7b9a7e13b60937b5c2fcfa58cb9bd1b93ddc97f" +content-hash = "580417bd1651d69e9ecedc7a3a55dc3d1c9b55506c9e51d4df41b2519035c4cf" diff --git a/pyproject.toml b/pyproject.toml index 4885a0345..fd6521bbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ jsonschema = "^4.23.0" semantic-kernel = {version = "1.3.0", python = "<3.13"} azure-ai-ml = "^1.21.1" azure-cosmos = "^4.7.0" -asyncpg = "^0.30.0" +asyncpg = {extras = ["gssauth"], version = "^0.30.0"} [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" From 42f66b90eb7ac760537764336dbeabf77be0fce3 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Tue, 12 Nov 2024 01:22:07 -0500 Subject: [PATCH 004/107] add psycopg2 to poetry --- poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index c7c10ce29..edee6f2d6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4658,6 +4658,24 @@ files = [ [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +[[package]] +name = "psycopg2" +version = "2.9.10" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg2-2.9.10-cp310-cp310-win32.whl", hash = "sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716"}, + {file = "psycopg2-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:c6f7b8561225f9e711a9c47087388a97fdc948211c10a4bccbf0ba68ab7b3b5a"}, + {file = "psycopg2-2.9.10-cp311-cp311-win32.whl", hash = "sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2"}, + {file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"}, + {file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"}, + {file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"}, + {file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"}, + {file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"}, + {file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"}, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -6777,4 +6795,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "580417bd1651d69e9ecedc7a3a55dc3d1c9b55506c9e51d4df41b2519035c4cf" +content-hash = "41fb9d5ae8267aebea11570dab6d8d436851d7bab8671e4d5750b93f1869d4cd" diff --git a/pyproject.toml b/pyproject.toml index fd6521bbb..91935c526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ semantic-kernel = {version = "1.3.0", python = "<3.13"} azure-ai-ml = "^1.21.1" azure-cosmos = "^4.7.0" asyncpg = {extras = ["gssauth"], version = "^0.30.0"} +psycopg2 = "^2.9.10" [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" From 1db0d65d5847485da2fdfde0c5dd94a9f14befff Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 12 Nov 2024 17:23:30 +0530 Subject: [PATCH 005/107] User Story 10379: Refactor: Group Azure Blob Storage and Form Recognizer Configurations in JSON Environment Variables --- .../batch/utilities/helpers/env_helper.py | 50 +- code/tests/functional/app_config.py | 9 +- .../test_advanced_image_processing.py | 16 +- ...egrated_vectorization_resource_creation.py | 12 +- infra/app/adminweb.bicep | 52 ++- infra/app/function.bicep | 52 ++- infra/app/web.bicep | 52 ++- infra/main.bicep | 60 +-- infra/main.bicepparam | 4 +- infra/main.json | 442 +++++++++--------- 10 files changed, 385 insertions(+), 364 deletions(-) diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index 63c5d52d9..c6f77f35c 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -205,22 +205,43 @@ def __load_config(self, **kwargs) -> None: "DOCUMENT_PROCESSING_QUEUE_NAME", "doc-processing" ) # Azure Blob Storage - self.AZURE_BLOB_ACCOUNT_NAME = os.getenv("AZURE_BLOB_ACCOUNT_NAME", "") - self.AZURE_BLOB_ACCOUNT_KEY = self.secretHelper.get_secret( - "AZURE_BLOB_ACCOUNT_KEY" - ) - self.AZURE_BLOB_CONTAINER_NAME = os.getenv("AZURE_BLOB_CONTAINER_NAME", "") + azure_blob_storage_info = self.get_info_from_env("AZURE_BLOB_STORAGE_INFO", "") + if azure_blob_storage_info: + # If AZURE_BLOB_STORAGE_INFO exists + self.AZURE_BLOB_ACCOUNT_NAME = azure_blob_storage_info.get("accountName", "") + self.AZURE_BLOB_ACCOUNT_KEY = self.secretHelper.get_secret_from_json( + azure_blob_storage_info.get("accountKey", "") + ) + self.AZURE_BLOB_CONTAINER_NAME = azure_blob_storage_info.get("containerName", "") + else: + # Otherwise, fallback to individual environment variables + self.AZURE_BLOB_ACCOUNT_NAME = os.getenv("AZURE_BLOB_ACCOUNT_NAME", "") + self.AZURE_BLOB_ACCOUNT_KEY = self.secretHelper.get_secret( + "AZURE_BLOB_ACCOUNT_KEY" + ) + self.AZURE_BLOB_CONTAINER_NAME = os.getenv("AZURE_BLOB_CONTAINER_NAME", "") self.AZURE_STORAGE_ACCOUNT_ENDPOINT = os.getenv( "AZURE_STORAGE_ACCOUNT_ENDPOINT", f"https://{self.AZURE_BLOB_ACCOUNT_NAME}.blob.core.windows.net/", ) + # Azure Form Recognizer - self.AZURE_FORM_RECOGNIZER_ENDPOINT = os.getenv( - "AZURE_FORM_RECOGNIZER_ENDPOINT", "" - ) - self.AZURE_FORM_RECOGNIZER_KEY = self.secretHelper.get_secret( - "AZURE_FORM_RECOGNIZER_KEY" - ) + azure_form_recognizer_info = self.get_info_from_env("AZURE_FORM_RECOGNIZER_INFO", "") + if azure_form_recognizer_info: + # If AZURE_FORM_RECOGNIZER_INFO exists + self.AZURE_FORM_RECOGNIZER_ENDPOINT = azure_form_recognizer_info.get("endpoint", "") + self.AZURE_FORM_RECOGNIZER_KEY = self.secretHelper.get_secret_from_json( + azure_form_recognizer_info.get("key", "") + ) + else: + # Otherwise, fallback to individual environment variables + self.AZURE_FORM_RECOGNIZER_ENDPOINT = os.getenv( + "AZURE_FORM_RECOGNIZER_ENDPOINT", "" + ) + self.AZURE_FORM_RECOGNIZER_KEY = self.secretHelper.get_secret( + "AZURE_FORM_RECOGNIZER_KEY" + ) + # Azure App Insights # APPLICATIONINSIGHTS_ENABLED will be True when the application runs in App Service self.APPLICATIONINSIGHTS_ENABLED = self.get_env_var_bool( @@ -363,3 +384,10 @@ def get_secret(self, secret_name: str) -> str: if self.USE_KEY_VAULT and secret_name_value else os.getenv(secret_name, "") ) + + def get_secret_from_json(self, secret_name: str) -> str: + return ( + self.secret_client.get_secret(secret_name).value + if self.USE_KEY_VAULT and secret_name + else secret_name + ) diff --git a/code/tests/functional/app_config.py b/code/tests/functional/app_config.py index c4f2b6d8c..d8d66d54a 100644 --- a/code/tests/functional/app_config.py +++ b/code/tests/functional/app_config.py @@ -12,17 +12,12 @@ class AppConfig: config: dict[str, str | None] = { "APPLICATIONINSIGHTS_ENABLED": "False", "AZURE_AUTH_TYPE": "keys", - "AZURE_BLOB_ACCOUNT_KEY": str( - base64.b64encode(b"some-blob-account-key"), "utf-8" - ), - "AZURE_BLOB_ACCOUNT_NAME": "some-blob-account-name", - "AZURE_BLOB_CONTAINER_NAME": "some-blob-container-name", + "AZURE_BLOB_STORAGE_INFO":'{"accountName": "some-blob-account-name","containerName": "some-blob-container-name","accountKey": "some-blob-account-key"}', "AZURE_COMPUTER_VISION_KEY": "some-computer-vision-key", "AZURE_CONTENT_SAFETY_ENDPOINT": "some-content-safety-endpoint", "AZURE_CONTENT_SAFETY_KEY": "some-content-safety-key", "AZURE_FORM_RECOGNIZER_ENDPOINT": "some-form-recognizer-endpoint", - "AZURE_FORM_RECOGNIZER_KEY": "some-form-recognizer-key", - "AZURE_KEY_VAULT_ENDPOINT": "some-key-vault-endpoint", + "AZURE_FORM_RECOGNIZER_INFO": '{"endpoint":"some-key-vault-endpoint","key":"some-key-vault-endpoint"}', "AZURE_OPENAI_API_KEY": "some-azure-openai-api-key", "AZURE_OPENAI_API_VERSION": "2024-02-01", "AZURE_OPENAI_EMBEDDING_MODEL_INFO": '{"model":"some-embedding-model","modelName":"some-embedding-model-name","modelVersion":"some-embedding-model-version"}', diff --git a/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py b/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py index 31ecb697f..124806dad 100644 --- a/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py +++ b/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py @@ -26,7 +26,7 @@ def message(app_config: AppConfig): body=json.dumps( { "topic": "topic", - "subject": f"/blobServices/default/{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/documents/blobs/{FILE_NAME}", + "subject": f"/blobServices/default/{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/documents/blobs/{FILE_NAME}", "eventType": "Microsoft.Storage.BlobCreated", "id": "id", "data": { @@ -37,7 +37,7 @@ def message(app_config: AppConfig): "contentType": "image/jpeg", "contentLength": 115310, "blobType": "BlockBlob", - "url": f"https://{app_config.get('AZURE_BLOB_ACCOUNT_NAME')}.blob.core.windows.net/documents/{FILE_NAME}", + "url": f"https://{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','accountName')}.blob.core.windows.net/documents/{FILE_NAME}", "sequencer": "00000000000000000000000000005E450000000000001f49", "storageDiagnostics": { "batchId": "952bdc2e-6006-0000-00bb-a20860000000" @@ -54,12 +54,12 @@ def message(app_config: AppConfig): @pytest.fixture(autouse=True) def setup_blob_metadata_mocking(httpserver: HTTPServer, app_config: AppConfig): httpserver.expect_request( - f"/{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}", + f"/{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}", method="HEAD", ).respond_with_data() httpserver.expect_request( - f"/{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}", + f"/{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}", method="PUT", ).respond_with_data() @@ -141,7 +141,7 @@ def test_image_passed_to_computer_vision_to_generate_image_embeddings( )[0] assert request.get_json()["url"].startswith( - f"{app_config.get('AZURE_STORAGE_ACCOUNT_ENDPOINT')}{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}" + f"{app_config.get('AZURE_STORAGE_ACCOUNT_ENDPOINT')}{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}" ) @@ -195,7 +195,7 @@ def test_image_passed_to_llm_to_generate_caption( assert request.get_json()["messages"][1]["content"][1]["image_url"][ "url" ].startswith( - f"{app_config.get('AZURE_STORAGE_ACCOUNT_ENDPOINT')}{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}" + f"{app_config.get('AZURE_STORAGE_ACCOUNT_ENDPOINT')}{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}" ) @@ -240,7 +240,7 @@ def test_metadata_is_updated_after_processing( verify_request_made( mock_httpserver=httpserver, request_matcher=RequestMatcher( - path=f"/{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}", + path=f"/{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}", method="PUT", headers={ "Authorization": ANY, @@ -439,7 +439,7 @@ def test_makes_correct_call_to_store_documents_in_search_index( batch_push_results.build().get_user_function()(message) # then - expected_file_path = f"{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}" + expected_file_path = f"{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}" expected_source_url = ( f"{app_config.get('AZURE_STORAGE_ACCOUNT_ENDPOINT')}{expected_file_path}" ) diff --git a/code/tests/functional/tests/functions/integrated_vectorization/test_integrated_vectorization_resource_creation.py b/code/tests/functional/tests/functions/integrated_vectorization/test_integrated_vectorization_resource_creation.py index ed374b181..32be05562 100644 --- a/code/tests/functional/tests/functions/integrated_vectorization/test_integrated_vectorization_resource_creation.py +++ b/code/tests/functional/tests/functions/integrated_vectorization/test_integrated_vectorization_resource_creation.py @@ -20,12 +20,12 @@ @pytest.fixture(autouse=True) def setup_blob_metadata_mocking(httpserver: HTTPServer, app_config: AppConfig): httpserver.expect_request( - f"/{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}", + f"/{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}", method="HEAD", ).respond_with_data() httpserver.expect_request( - f"/{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/{FILE_NAME}", + f"/{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/{FILE_NAME}", method="PUT", ).respond_with_data() @@ -36,7 +36,7 @@ def message(app_config: AppConfig): body=json.dumps( { "topic": "topic", - "subject": f"/blobServices/default/{app_config.get('AZURE_BLOB_CONTAINER_NAME')}/documents/blobs/{FILE_NAME}", + "subject": f"/blobServices/default/{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}/documents/blobs/{FILE_NAME}", "eventType": "Microsoft.Storage.BlobCreated", "id": "id", "data": { @@ -47,7 +47,7 @@ def message(app_config: AppConfig): "contentType": "application/pdf", "contentLength": 544811, "blobType": "BlockBlob", - "url": f"https://{app_config.get('AZURE_BLOB_ACCOUNT_NAME')}.blob.core.windows.net/documents/{FILE_NAME}", + "url": f"https://{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','accountName')}.blob.core.windows.net/documents/{FILE_NAME}", "sequencer": "00000000000000000000000000036029000000000017251c", "storageDiagnostics": { "batchId": "c98008b9-e006-007c-00bb-a2ae9f000000" @@ -97,9 +97,9 @@ def test_integrated_vectorization_datasouce_created( "name": app_config.get("AZURE_SEARCH_DATASOURCE_NAME"), "type": "azureblob", "credentials": { - "connectionString": f"DefaultEndpointsProtocol=https;AccountName={app_config.get('AZURE_BLOB_ACCOUNT_NAME')};AccountKey={app_config.get('AZURE_BLOB_ACCOUNT_KEY')};EndpointSuffix=core.windows.net" + "connectionString": f"DefaultEndpointsProtocol=https;AccountName={app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','accountName')};AccountKey={app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','accountKey')};EndpointSuffix=core.windows.net" }, - "container": {"name": f"{app_config.get('AZURE_BLOB_CONTAINER_NAME')}"}, + "container": {"name": f"{app_config.get_from_json('AZURE_BLOB_STORAGE_INFO','containerName')}"}, "dataDeletionDetectionPolicy": { "@odata.type": "#Microsoft.Azure.Search.NativeBlobSoftDeleteDeletionDetectionPolicy" }, diff --git a/infra/app/adminweb.bicep b/infra/app/adminweb.bicep index d2e993282..56397ed6f 100644 --- a/infra/app/adminweb.bicep +++ b/infra/app/adminweb.bicep @@ -19,8 +19,8 @@ param computerVisionName string = '' param appSettings object = {} param useKeyVault bool param openAIKeyName string = '' -param storageAccountKeyName string = '' -param formRecognizerKeyName string = '' +param azureBlobStorageInfo string = '' +param azureFormRecognizerInfo string = '' param searchKeyName string = '' param computerVisionKeyName string = '' param contentSafetyKeyName string = '' @@ -29,6 +29,30 @@ param authType string param dockerFullImageName string = '' param useDocker bool = dockerFullImageName != '' +var azureFormRecognizerInfoUpdated = useKeyVault + ? azureFormRecognizerInfo + : replace(azureFormRecognizerInfo, '$FORM_RECOGNIZER_KEY', listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + formRecognizerName + ), + '2023-05-01' + ).key1) + +var azureBlobStorageInfoUpdated = useKeyVault + ? azureBlobStorageInfo + : replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY', listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.Storage/storageAccounts', + storageAccountName + ), + '2021-09-01' + ).keys[0].value) + module adminweb '../core/host/appservice.bicep' = { name: '${name}-app-module' params: { @@ -69,28 +93,8 @@ module adminweb '../core/host/appservice.bicep' = { ), '2021-04-01-preview' ).primaryKey - AZURE_BLOB_ACCOUNT_KEY: useKeyVault - ? storageAccountKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.Storage/storageAccounts', - storageAccountName - ), - '2021-09-01' - ).keys[0].value - AZURE_FORM_RECOGNIZER_KEY: useKeyVault - ? formRecognizerKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - formRecognizerName - ), - '2023-05-01' - ).key1 + AZURE_BLOB_STORAGE_INFO: azureBlobStorageInfoUpdated + AZURE_FORM_RECOGNIZER_INFO: azureFormRecognizerInfoUpdated AZURE_CONTENT_SAFETY_KEY: useKeyVault ? contentSafetyKeyName : listKeys( diff --git a/infra/app/function.bicep b/infra/app/function.bicep index b3e70b816..cb6a5c292 100644 --- a/infra/app/function.bicep +++ b/infra/app/function.bicep @@ -19,8 +19,8 @@ param speechServiceName string = '' param computerVisionName string = '' param useKeyVault bool param openAIKeyName string = '' -param storageAccountKeyName string = '' -param formRecognizerKeyName string = '' +param azureBlobStorageInfo string = '' +param azureFormRecognizerInfo string = '' param searchKeyName string = '' param computerVisionKeyName string = '' param contentSafetyKeyName string = '' @@ -29,6 +29,30 @@ param authType string param dockerFullImageName string = '' param cosmosDBKeyName string = '' +var azureFormRecognizerInfoUpdated = useKeyVault + ? azureFormRecognizerInfo + : replace(azureFormRecognizerInfo, '$FORM_RECOGNIZER_KEY', listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + formRecognizerName + ), + '2023-05-01' + ).key1) + +var azureBlobStorageInfoUpdated = useKeyVault + ? azureBlobStorageInfo + : replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY', listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.Storage/storageAccounts', + storageAccountName + ), + '2021-09-01' + ).keys[0].value) + module function '../core/host/functions.bicep' = { name: '${name}-app-module' params: { @@ -68,28 +92,8 @@ module function '../core/host/functions.bicep' = { ), '2021-04-01-preview' ).primaryKey - AZURE_BLOB_ACCOUNT_KEY: useKeyVault - ? storageAccountKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.Storage/storageAccounts', - storageAccountName - ), - '2021-09-01' - ).keys[0].value - AZURE_FORM_RECOGNIZER_KEY: useKeyVault - ? formRecognizerKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - formRecognizerName - ), - '2023-05-01' - ).key1 + AZURE_BLOB_STORAGE_INFO: azureBlobStorageInfoUpdated + AZURE_FORM_RECOGNIZER_INFO: azureFormRecognizerInfoUpdated AZURE_CONTENT_SAFETY_KEY: useKeyVault ? contentSafetyKeyName : listKeys( diff --git a/infra/app/web.bicep b/infra/app/web.bicep index c1734cd2a..526bd513c 100644 --- a/infra/app/web.bicep +++ b/infra/app/web.bicep @@ -19,8 +19,8 @@ param computerVisionName string = '' param appSettings object = {} param useKeyVault bool param openAIKeyName string = '' -param storageAccountKeyName string = '' -param formRecognizerKeyName string = '' +param azureBlobStorageInfo string = '' +param azureFormRecognizerInfo string = '' param searchKeyName string = '' param computerVisionKeyName string = '' param contentSafetyKeyName string = '' @@ -31,6 +31,30 @@ param useDocker bool = dockerFullImageName != '' param healthCheckPath string = '' param cosmosDBKeyName string = '' +var azureFormRecognizerInfoUpdated = useKeyVault + ? azureFormRecognizerInfo + : replace(azureFormRecognizerInfo, '$FORM_RECOGNIZER_KEY', listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + formRecognizerName + ), + '2023-05-01' + ).key1) + +var azureBlobStorageInfoUpdated = useKeyVault + ? azureBlobStorageInfo + : replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY', listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.Storage/storageAccounts', + storageAccountName + ), + '2021-09-01' + ).keys[0].value) + module web '../core/host/appservice.bicep' = { name: '${name}-app-module' params: { @@ -66,28 +90,8 @@ module web '../core/host/appservice.bicep' = { ), '2021-04-01-preview' ).primaryKey - AZURE_BLOB_ACCOUNT_KEY: useKeyVault - ? storageAccountKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.Storage/storageAccounts', - storageAccountName - ), - '2021-09-01' - ).keys[0].value - AZURE_FORM_RECOGNIZER_KEY: useKeyVault - ? formRecognizerKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - formRecognizerName - ), - '2023-05-01' - ).key1 + AZURE_BLOB_STORAGE_INFO: azureBlobStorageInfoUpdated + AZURE_FORM_RECOGNIZER_INFO: azureFormRecognizerInfoUpdated AZURE_CONTENT_SAFETY_KEY: useKeyVault ? contentSafetyKeyName : listKeys( diff --git a/infra/main.bicep b/infra/main.bicep index 61aa0aa2c..d87a51f19 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -553,8 +553,8 @@ module web './app/web.bicep' = if (hostingModel == 'code') { speechServiceName: speechService.outputs.name computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' - storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' - formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + azureBlobStorageInfo: azureBlobStorageInfo + azureFormRecognizerInfo: azureFormRecognizerInfo searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' @@ -564,13 +564,10 @@ module web './app/web.bicep' = if (hostingModel == 'code') { keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType appSettings: { - AZURE_BLOB_ACCOUNT_NAME: storageAccountName - AZURE_BLOB_CONTAINER_NAME: blobContainerName AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_FORM_RECOGNIZER_ENDPOINT: formrecognizer.outputs.endpoint AZURE_OPENAI_RESOURCE: azureOpenAIResourceName AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature @@ -635,8 +632,8 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { speechServiceName: speechService.outputs.name computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' - storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' - formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + azureBlobStorageInfo: azureBlobStorageInfo + azureFormRecognizerInfo: azureFormRecognizerInfo searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' computerVisionKeyName: useKeyVault ? storekeys.outputs.COMPUTER_VISION_KEY_NAME : '' contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' @@ -646,13 +643,10 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType appSettings: { - AZURE_BLOB_ACCOUNT_NAME: storageAccountName - AZURE_BLOB_CONTAINER_NAME: blobContainerName AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_FORM_RECOGNIZER_ENDPOINT: formrecognizer.outputs.endpoint AZURE_OPENAI_RESOURCE: azureOpenAIResourceName AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature @@ -717,8 +711,8 @@ module adminweb './app/adminweb.bicep' = if (hostingModel == 'code') { speechServiceName: speechService.outputs.name computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' - storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' - formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + azureBlobStorageInfo: azureBlobStorageInfo + azureFormRecognizerInfo: azureFormRecognizerInfo searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' computerVisionKeyName: useKeyVault ? storekeys.outputs.COMPUTER_VISION_KEY_NAME : '' contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' @@ -727,13 +721,10 @@ module adminweb './app/adminweb.bicep' = if (hostingModel == 'code') { keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType appSettings: { - AZURE_BLOB_ACCOUNT_NAME: storageAccountName - AZURE_BLOB_CONTAINER_NAME: blobContainerName AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_FORM_RECOGNIZER_ENDPOINT: formrecognizer.outputs.endpoint AZURE_OPENAI_RESOURCE: azureOpenAIResourceName AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature @@ -794,8 +785,8 @@ module adminweb_docker './app/adminweb.bicep' = if (hostingModel == 'container') speechServiceName: speechService.outputs.name computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' - storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' - formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + azureBlobStorageInfo: azureBlobStorageInfo + azureFormRecognizerInfo: azureFormRecognizerInfo searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' @@ -804,13 +795,10 @@ module adminweb_docker './app/adminweb.bicep' = if (hostingModel == 'container') keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType appSettings: { - AZURE_BLOB_ACCOUNT_NAME: storageAccountName - AZURE_BLOB_CONTAINER_NAME: blobContainerName AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_FORM_RECOGNIZER_ENDPOINT: formrecognizer.outputs.endpoint AZURE_OPENAI_RESOURCE: azureOpenAIResourceName AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature @@ -907,8 +895,8 @@ module function './app/function.bicep' = if (hostingModel == 'code') { computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' clientKey: clientKey openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' - storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' - formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + azureBlobStorageInfo: azureBlobStorageInfo + azureFormRecognizerInfo: azureFormRecognizerInfo searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' @@ -917,13 +905,10 @@ module function './app/function.bicep' = if (hostingModel == 'code') { keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType appSettings: { - AZURE_BLOB_ACCOUNT_NAME: storageAccountName - AZURE_BLOB_CONTAINER_NAME: blobContainerName AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_FORM_RECOGNIZER_ENDPOINT: formrecognizer.outputs.endpoint AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo AZURE_OPENAI_RESOURCE: azureOpenAIResourceName @@ -970,8 +955,8 @@ module function_docker './app/function.bicep' = if (hostingModel == 'container') computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' clientKey: clientKey openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' - storageAccountKeyName: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' - formRecognizerKeyName: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' + azureBlobStorageInfo: azureBlobStorageInfo + azureFormRecognizerInfo: azureFormRecognizerInfo searchKeyName: useKeyVault ? storekeys.outputs.SEARCH_KEY_NAME : '' contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' @@ -980,13 +965,10 @@ module function_docker './app/function.bicep' = if (hostingModel == 'container') keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType appSettings: { - AZURE_BLOB_ACCOUNT_NAME: storageAccountName - AZURE_BLOB_CONTAINER_NAME: blobContainerName AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_FORM_RECOGNIZER_ENDPOINT: formrecognizer.outputs.endpoint AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo AZURE_OPENAI_RESOURCE: azureOpenAIResourceName @@ -1025,6 +1007,11 @@ module formrecognizer 'core/ai/cognitiveservices.bicep' = { } } +var azureFormRecognizerInfo = string({ + endpoint: formrecognizer.outputs.endpoint + key: useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '$FORM_RECOGNIZER_KEY' +}) + module contentsafety 'core/ai/cognitiveservices.bicep' = { name: contentSafetyName scope: resourceGroup() @@ -1096,6 +1083,12 @@ module storageRoleUser 'core/security/role.bicep' = if (authType == 'rbac') { } } +var azureBlobStorageInfo = string({ + containerName: blobContainerName + accountName: storageAccountName + accountKey: useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '$STORAGE_ACCOUNT_KEY' +}) + // Cognitive Services User module openaiRoleUser 'core/security/role.bicep' = if (authType == 'rbac') { scope: resourceGroup() @@ -1147,9 +1140,7 @@ module machineLearning 'app/machinelearning.bicep' = if (orchestrationStrategy = output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString output AZURE_APP_SERVICE_HOSTING_MODEL string = hostingModel -output AZURE_BLOB_CONTAINER_NAME string = blobContainerName -output AZURE_BLOB_ACCOUNT_NAME string = storageAccountName -output AZURE_BLOB_ACCOUNT_KEY string = useKeyVault ? storekeys.outputs.STORAGE_ACCOUNT_KEY_NAME : '' +output AZURE_BLOB_STORAGE_INFO string = replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY','') output AZURE_COMPUTER_VISION_ENDPOINT string = useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' output AZURE_COMPUTER_VISION_LOCATION string = useAdvancedImageProcessing ? computerVision.outputs.location : '' output AZURE_COMPUTER_VISION_KEY string = useKeyVault ? storekeys.outputs.COMPUTER_VISION_KEY_NAME : '' @@ -1157,8 +1148,7 @@ output AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION string = computerVision output AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION string = computerVisionVectorizeImageModelVersion output AZURE_CONTENT_SAFETY_ENDPOINT string = contentsafety.outputs.endpoint output AZURE_CONTENT_SAFETY_KEY string = useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' -output AZURE_FORM_RECOGNIZER_ENDPOINT string = formrecognizer.outputs.endpoint -output AZURE_FORM_RECOGNIZER_KEY string = useKeyVault ? storekeys.outputs.FORM_RECOGNIZER_KEY_NAME : '' +output AZURE_FORM_RECOGNIZER_INFO string = replace(azureFormRecognizerInfo, '$FORM_RECOGNIZER_KEY','') output AZURE_KEY_VAULT_ENDPOINT string = useKeyVault ? keyvault.outputs.endpoint : '' output AZURE_KEY_VAULT_NAME string = useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' output AZURE_LOCATION string = location diff --git a/infra/main.bicepparam b/infra/main.bicepparam index ce0418abb..02b7083c0 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -83,4 +83,6 @@ param azureAISearchName = searchServiceName == '' ? 'search-${resourceToken}' : param azureSearchIndex = readEnvironmentVariable('AZURE_SEARCH_INDEX', 'index-${resourceToken}') param azureOpenAIResourceName = readEnvironmentVariable('AZURE_OPENAI_RESOURCE', 'openai-${resourceToken}') -param storageAccountName = readEnvironmentVariable('AZURE_BLOB_ACCOUNT_NAME', 'str${resourceToken}') +var azureBlobStorageInfo = readEnvironmentVariable('AZURE_BLOB_STORAGE_INFO', '{"containerName": "documents", "accountName": "${resourceToken}", "accountKey": ""}') +var azureBlobStorageInfoParsed = json(replace(azureBlobStorageInfo, '\\', '')) // Remove escape characters +param storageAccountName = azureBlobStorageInfoParsed.accountName diff --git a/infra/main.json b/infra/main.json index ac292309b..612909829 100644 --- a/infra/main.json +++ b/infra/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "4843965256692050769" + "version": "0.30.23.60470", + "templateHash": "16553051506563672417" } }, "parameters": { @@ -682,8 +682,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "10162367437414363838" + "version": "0.30.23.60470", + "templateHash": "14453122839528928942" } }, "parameters": { @@ -847,8 +847,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "17778708028830863146" + "version": "0.30.23.60470", + "templateHash": "12121357715793816510" }, "description": "Creates an Azure Key Vault." }, @@ -940,8 +940,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "13930677902562058633" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1095,8 +1095,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "13930677902562058633" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1244,8 +1244,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1313,8 +1313,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1382,8 +1382,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1451,8 +1451,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1524,8 +1524,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "13930677902562058633" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1692,8 +1692,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "7149284552544081554" + "version": "0.30.23.60470", + "templateHash": "15528430944298201007" } }, "parameters": { @@ -1924,8 +1924,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "3186814620975722299" + "version": "0.30.23.60470", + "templateHash": "13584246975784398226" }, "description": "Creates an Azure AI Search instance." }, @@ -2089,8 +2089,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "11910849835579950339" + "version": "0.30.23.60470", + "templateHash": "9286637480882627742" }, "description": "Creates an Azure App Service plan." }, @@ -2200,8 +2200,12 @@ }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", @@ -2216,13 +2220,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", @@ -2273,8 +2274,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "15380721951101386205" + "version": "0.30.23.60470", + "templateHash": "12750836139153854515" } }, "parameters": { @@ -2355,11 +2356,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -2430,7 +2431,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" + "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -2455,8 +2456,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "16756175373379165193" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -2682,8 +2683,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "103667315154160978" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -2760,8 +2761,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -2829,8 +2830,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -2898,8 +2899,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -2967,8 +2968,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3033,8 +3034,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "1133867179681914334" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -3165,8 +3166,12 @@ }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", @@ -3181,13 +3186,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", @@ -3238,8 +3240,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "15380721951101386205" + "version": "0.30.23.60470", + "templateHash": "12750836139153854515" } }, "parameters": { @@ -3320,11 +3322,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -3395,7 +3397,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" + "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -3420,8 +3422,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "16756175373379165193" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -3647,8 +3649,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "103667315154160978" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -3725,8 +3727,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3794,8 +3796,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3863,8 +3865,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3932,8 +3934,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3998,8 +4000,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "1133867179681914334" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -4130,8 +4132,12 @@ }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", @@ -4145,13 +4151,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", @@ -4199,8 +4202,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "17327750493694934622" + "version": "0.30.23.60470", + "templateHash": "12567732396765618168" } }, "parameters": { @@ -4281,11 +4284,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -4361,7 +4364,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -4370,8 +4373,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "16756175373379165193" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -4597,8 +4600,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "103667315154160978" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -4675,8 +4678,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4744,8 +4747,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4813,8 +4816,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4882,8 +4885,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4948,8 +4951,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "1133867179681914334" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -5076,8 +5079,12 @@ }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", @@ -5091,13 +5098,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", @@ -5145,8 +5149,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "17327750493694934622" + "version": "0.30.23.60470", + "templateHash": "12567732396765618168" } }, "parameters": { @@ -5227,11 +5231,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -5307,7 +5311,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -5316,8 +5320,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "16756175373379165193" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -5543,8 +5547,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "103667315154160978" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -5621,8 +5625,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5690,8 +5694,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5759,8 +5763,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5828,8 +5832,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5894,8 +5898,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "1133867179681914334" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -6007,8 +6011,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "14824672405902859193" + "version": "0.30.23.60470", + "templateHash": "2390666818608223959" }, "description": "Creates an Application Insights instance and a Log Analytics workspace." }, @@ -6059,8 +6063,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "3321614781233750399" + "version": "0.30.23.60470", + "templateHash": "19694557100387265" }, "description": "Creates a Log Analytics workspace." }, @@ -6140,8 +6144,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "53484624287673645" + "version": "0.30.23.60470", + "templateHash": "16993757720869129667" }, "description": "Creates an Application Insights instance based on an existing Log Analytics workspace." }, @@ -6205,8 +6209,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "14911212182563532861" + "version": "0.30.23.60470", + "templateHash": "12524466040979787143" }, "description": "Creates a dashboard for an Application Insights instance." }, @@ -7540,8 +7544,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "2751741336760825109" + "version": "0.30.23.60470", + "templateHash": "15151749822990864279" } }, "parameters": { @@ -7623,8 +7627,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "20443133125141617" + "version": "0.30.23.60470", + "templateHash": "15030863077610448627" } }, "parameters": { @@ -7763,8 +7767,12 @@ "value": "[variables('clientKey')]" }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", @@ -7778,13 +7786,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", @@ -7817,8 +7822,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "17585935035261876784" + "version": "0.30.23.60470", + "templateHash": "829406794789220597" } }, "parameters": { @@ -7894,11 +7899,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -8001,7 +8006,7 @@ "value": "[parameters('dockerFullImageName')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -8010,8 +8015,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "15094851132007588437" + "version": "0.30.23.60470", + "templateHash": "3077544357242613291" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -8218,8 +8223,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "16756175373379165193" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -8445,8 +8450,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "103667315154160978" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -8541,8 +8546,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8610,8 +8615,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8679,8 +8684,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8748,8 +8753,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8817,8 +8822,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8883,8 +8888,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "1133867179681914334" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -9010,8 +9015,12 @@ "value": "[variables('clientKey')]" }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", @@ -9025,13 +9034,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", @@ -9064,8 +9070,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "17585935035261876784" + "version": "0.30.23.60470", + "templateHash": "829406794789220597" } }, "parameters": { @@ -9141,11 +9147,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -9248,7 +9254,7 @@ "value": "[parameters('dockerFullImageName')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -9257,8 +9263,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "15094851132007588437" + "version": "0.30.23.60470", + "templateHash": "3077544357242613291" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -9465,8 +9471,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "16756175373379165193" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -9692,8 +9698,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "103667315154160978" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -9788,8 +9794,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -9857,8 +9863,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -9926,8 +9932,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -9995,8 +10001,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10064,8 +10070,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10130,8 +10136,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "1133867179681914334" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -10234,8 +10240,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "13930677902562058633" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10385,8 +10391,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "13930677902562058633" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10539,8 +10545,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "10992796846575118308" + "version": "0.30.23.60470", + "templateHash": "6699069410959282929" } }, "parameters": { @@ -10667,8 +10673,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "6009030871838517804" + "version": "0.30.23.60470", + "templateHash": "7157574004190707979" }, "description": "Creates an Azure storage account." }, @@ -10888,8 +10894,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10954,8 +10960,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11020,8 +11026,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11086,8 +11092,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "12421327006867392541" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11168,8 +11174,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "648404900818606545" + "version": "0.30.23.60470", + "templateHash": "17372485166957435450" } }, "parameters": { @@ -11282,17 +11288,9 @@ "type": "string", "value": "[parameters('hostingModel')]" }, - "AZURE_BLOB_CONTAINER_NAME": { - "type": "string", - "value": "[variables('blobContainerName')]" - }, - "AZURE_BLOB_ACCOUNT_NAME": { + "AZURE_BLOB_STORAGE_INFO": { "type": "string", - "value": "[parameters('storageAccountName')]" - }, - "AZURE_BLOB_ACCOUNT_KEY": { - "type": "string", - "value": "[if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '')]" + "value": "[replace(string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY'))), '$STORAGE_ACCOUNT_KEY', '')]" }, "AZURE_COMPUTER_VISION_ENDPOINT": { "type": "string", @@ -11322,13 +11320,9 @@ "type": "string", "value": "[if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value, '')]" }, - "AZURE_FORM_RECOGNIZER_ENDPOINT": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]" - }, - "AZURE_FORM_RECOGNIZER_KEY": { + "AZURE_FORM_RECOGNIZER_INFO": { "type": "string", - "value": "[if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '')]" + "value": "[replace(string(createObject('endpoint', reference(resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY'))), '$FORM_RECOGNIZER_KEY', '')]" }, "AZURE_KEY_VAULT_ENDPOINT": { "type": "string", From 87f557ebc581283caad129b44382eea87f7b9023 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Tue, 12 Nov 2024 18:33:18 +0530 Subject: [PATCH 006/107] Update app_config.py to fix testcase failure --- code/tests/functional/app_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/tests/functional/app_config.py b/code/tests/functional/app_config.py index d8d66d54a..fa2829fb9 100644 --- a/code/tests/functional/app_config.py +++ b/code/tests/functional/app_config.py @@ -5,14 +5,14 @@ from backend.batch.utilities.helpers.config.conversation_flow import ConversationFlow logger = logging.getLogger(__name__) - +encoded_account_key = str(base64.b64encode(b"some-blob-account-key"), "utf-8") class AppConfig: before_config: dict[str, str] = {} config: dict[str, str | None] = { "APPLICATIONINSIGHTS_ENABLED": "False", "AZURE_AUTH_TYPE": "keys", - "AZURE_BLOB_STORAGE_INFO":'{"accountName": "some-blob-account-name","containerName": "some-blob-container-name","accountKey": "some-blob-account-key"}', + "AZURE_BLOB_STORAGE_INFO": '{"accountName": "some-blob-account-name", "containerName": "some-blob-container-name", "accountKey": "' + encoded_account_key + '"}', "AZURE_COMPUTER_VISION_KEY": "some-computer-vision-key", "AZURE_CONTENT_SAFETY_ENDPOINT": "some-content-safety-endpoint", "AZURE_CONTENT_SAFETY_KEY": "some-content-safety-key", From 52abaa3e6597e4529bc4e131a92ffdfb0b9a74b1 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 12 Nov 2024 16:39:10 +0000 Subject: [PATCH 007/107] readme update for Group Azure Blob Storage and Form Recognizer Configurations in JSON Environment Variables --- .env.sample | 7 ++----- docs/LOCAL_DEPLOYMENT.md | 15 ++++----------- docs/TEAMS_LOCAL_DEPLOYMENT.md | 12 ++++-------- docs/contract_assistance.md | 4 +--- docs/employee_assistance.md | 4 +--- docs/model_configuration.md | 32 +++++++++++++------------------- 6 files changed, 25 insertions(+), 49 deletions(-) diff --git a/.env.sample b/.env.sample index 1c46d3451..12e43e9e3 100644 --- a/.env.sample +++ b/.env.sample @@ -36,12 +36,9 @@ AzureWebJobsStorage= BACKEND_URL=http://localhost:7071 DOCUMENT_PROCESSING_QUEUE_NAME= # Azure Blob Storage for storing the original documents to be processed -AZURE_BLOB_ACCOUNT_NAME= -AZURE_BLOB_ACCOUNT_KEY= -AZURE_BLOB_CONTAINER_NAME= +AZURE_BLOB_STORAGE_INFO="{\"containerName\":\"documents\",\"accountName\":\"accountName\",\"accountKey\":\"accountKey\"}" # Azure Form Recognizer for extracting the text from the documents -AZURE_FORM_RECOGNIZER_ENDPOINT= -AZURE_FORM_RECOGNIZER_KEY= +AZURE_FORM_RECOGNIZER_INFO="{\"endpoint\":\"endpoint\",\"key\":\"key\"}" # Azure AI Content Safety for filtering out the inappropriate questions or answers AZURE_CONTENT_SAFETY_ENDPOINT= AZURE_CONTENT_SAFETY_KEY= diff --git a/docs/LOCAL_DEPLOYMENT.md b/docs/LOCAL_DEPLOYMENT.md index b10e2eed8..51d8e0844 100644 --- a/docs/LOCAL_DEPLOYMENT.md +++ b/docs/LOCAL_DEPLOYMENT.md @@ -190,13 +190,9 @@ Execute the above [shell command](#L81) to run the function locally. You may nee |AZURE_SEARCH_FILTER||Filter to apply to search queries.| |AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION ||Whether to use [Integrated Vectorization](https://learn.microsoft.com/en-us/azure/search/vector-search-integrated-vectorization)| |AZURE_OPENAI_RESOURCE||the name of your Azure OpenAI resource| -|AZURE_OPENAI_MODEL||The name of your model deployment| -|AZURE_OPENAI_MODEL_NAME|gpt-35-turbo|The name of the model| -|AZURE_OPENAI_MODEL_VERSION|0613|The version of the model to use| +|AZURE_OPENAI_MODEL_INFO|{"model":"gpt-35-turbo","modelName":"gpt-35-turbo","modelVersion":"0613"}|`model`: The name of your model deployment.
`modelName`: The name of the model.
`modelVersion`: The version of the model to use.| |AZURE_OPENAI_API_KEY||One of the API keys of your Azure OpenAI resource| -|AZURE_OPENAI_EMBEDDING_MODEL|text-embedding-ada-002|The name of your Azure OpenAI embeddings model deployment| -|AZURE_OPENAI_EMBEDDING_MODEL_NAME|text-embedding-ada-002|The name of the embeddings model (can be found in Azure AI Studio)| -|AZURE_OPENAI_EMBEDDING_MODEL_VERSION|2|The version of the embeddings model to use (can be found in Azure AI Studio)| +|AZURE_OPENAI_EMBEDDING_MODEL_INFO|{"model":"text-embedding-ada-002","modelName":"text-embedding-ada-002","modelVersion":"2"}|`model`: The name of your Azure OpenAI embeddings model deployment.
`modelName`: The name of the embeddings model (can be found in Azure AI Studio).
`modelVersion`: The version of the embeddings model to use (can be found in Azure AI Studio).| |AZURE_OPENAI_TEMPERATURE|0|What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. A value of 0 is recommended when using your data.| |AZURE_OPENAI_TOP_P|1.0|An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. We recommend setting this to 1.0 when using your data.| |AZURE_OPENAI_MAX_TOKENS|1000|The maximum number of tokens allowed for the generated answer.| @@ -206,11 +202,8 @@ Execute the above [shell command](#L81) to run the function locally. You may nee |AzureWebJobsStorage||The connection string to the Azure Blob Storage for the Azure Functions Batch processing| |BACKEND_URL||The URL for the Backend Batch Azure Function. Use http://localhost:7071 for local execution| |DOCUMENT_PROCESSING_QUEUE_NAME|doc-processing|The name of the Azure Queue to handle the Batch processing| -|AZURE_BLOB_ACCOUNT_NAME||The name of the Azure Blob Storage for storing the original documents to be processed| -|AZURE_BLOB_ACCOUNT_KEY||The key of the Azure Blob Storage for storing the original documents to be processed| -|AZURE_BLOB_CONTAINER_NAME||The name of the Container in the Azure Blob Storage for storing the original documents to be processed| -|AZURE_FORM_RECOGNIZER_ENDPOINT||The name of the Azure Form Recognizer for extracting the text from the documents| -|AZURE_FORM_RECOGNIZER_KEY||The key of the Azure Form Recognizer for extracting the text from the documents| +|AZURE_BLOB_STORAGE_INFO|{"containerName":"documents","accountName":"accountName","accountKey":"accountKey"}"|`containerName`: The name of the Container in the Azure Blob Storage for storing the original documents to be processed.
`accountName`: The name of the Azure Blob Storage for storing the original documents to be processed.
`accountKey`: The key of the Azure Blob Storage for storing the original documents to be processed.| +|AZURE_FORM_RECOGNIZER_INFO|{"endpoint":"endpoint","key":"key"}|`endpoint`: The name of the Azure Form Recognizer for extracting the text from the documents.
`key`: The key of the Azure Form Recognizer for extracting the text from the documents.| |APPLICATIONINSIGHTS_CONNECTION_STRING||The Application Insights connection string to store the application logs| |ORCHESTRATION_STRATEGY | openai_function | Orchestration strategy. Use Azure OpenAI Functions (openai_function), Semantic Kernel (semantic_kernel), LangChain (langchain) or Prompt Flow (prompt_flow) for messages orchestration. If you are using a new model version 0613 select any strategy, if you are using a 0314 model version select "langchain". Note that both `openai_function` and `semantic_kernel` use OpenAI function calling. Prompt Flow option is still in development and does not support RBAC or integrated vectorization as of yet.| |AZURE_CONTENT_SAFETY_ENDPOINT | | The endpoint of the Azure AI Content Safety service | diff --git a/docs/TEAMS_LOCAL_DEPLOYMENT.md b/docs/TEAMS_LOCAL_DEPLOYMENT.md index e712fddd7..ae7e84cd3 100644 --- a/docs/TEAMS_LOCAL_DEPLOYMENT.md +++ b/docs/TEAMS_LOCAL_DEPLOYMENT.md @@ -64,10 +64,9 @@ Or use the [Azure Functions VS Code extension](https://marketplace.visualstudio. |AZURE_SEARCH_FIELDS_METADATA|metadata|Field from your Azure AI Search index that contains metadata for the document. `metadata` if you don't have a specific requirement.| |AZURE_SEARCH_FILTER||Filter to apply to search queries.| |AZURE_OPENAI_RESOURCE||the name of your Azure OpenAI resource| -|AZURE_OPENAI_MODEL||The name of your model deployment| -|AZURE_OPENAI_MODEL_NAME|gpt-35-turbo|The name of the model| +|AZURE_OPENAI_MODEL_INFO|{"model":"gpt-35-turbo","modelName":"gpt-35-turbo","modelVersion":"0613"}|`model`: The name of your model deployment.
`modelName`: The name of the model.
`modelVersion`: The version of the model to use.| |AZURE_OPENAI_API_KEY||One of the API keys of your Azure OpenAI resource| -|AZURE_OPENAI_EMBEDDING_MODEL|text-embedding-ada-002|The name of you Azure OpenAI embeddings model deployment| +|AZURE_OPENAI_EMBEDDING_MODEL_INFO|{"model":"text-embedding-ada-002","modelName":"text-embedding-ada-002","modelVersion":"2"}|`model`: The name of your Azure OpenAI embeddings model deployment.
`modelName`: The name of the embeddings model (can be found in Azure AI Studio).
`modelVersion`: The version of the embeddings model to use (can be found in Azure AI Studio).| |AZURE_OPENAI_TEMPERATURE|0|What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. A value of 0 is recommended when using your data.| |AZURE_OPENAI_TOP_P|1.0|An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. We recommend setting this to 1.0 when using your data.| |AZURE_OPENAI_MAX_TOKENS|1000|The maximum number of tokens allowed for the generated answer.| @@ -77,11 +76,8 @@ Or use the [Azure Functions VS Code extension](https://marketplace.visualstudio. |AzureWebJobsStorage||The connection string to the Azure Blob Storage for the Azure Functions Batch processing| |BACKEND_URL||The URL for the Backend Batch Azure Function. Use http://localhost:7071 for local execution| |DOCUMENT_PROCESSING_QUEUE_NAME|doc-processing|The name of the Azure Queue to handle the Batch processing| -|AZURE_BLOB_ACCOUNT_NAME||The name of the Azure Blob Storage for storing the original documents to be processed| -|AZURE_BLOB_ACCOUNT_KEY||The key of the Azure Blob Storage for storing the original documents to be processed| -|AZURE_BLOB_CONTAINER_NAME||The name of the Container in the Azure Blob Storage for storing the original documents to be processed| -|AZURE_FORM_RECOGNIZER_ENDPOINT||The name of the Azure Form Recognizer for extracting the text from the documents| -|AZURE_FORM_RECOGNIZER_KEY||The key of the Azure Form Recognizer for extracting the text from the documents| +|AZURE_BLOB_STORAGE_INFO|{"containerName":"documents","accountName":"accountName","accountKey":"accountKey"}"|`containerName`: The name of the Container in the Azure Blob Storage for storing the original documents to be processed.
`accountName`: The name of the Azure Blob Storage for storing the original documents to be processed.
`accountKey`: The key of the Azure Blob Storage for storing the original documents to be processed.| +|AZURE_FORM_RECOGNIZER_INFO|{"endpoint":"endpoint","key":"key"}|`endpoint`: The name of the Azure Form Recognizer for extracting the text from the documents.
`key`: The key of the Azure Form Recognizer for extracting the text from the documents.| |APPLICATIONINSIGHTS_CONNECTION_STRING||The Application Insights connection string to store the application logs| |ORCHESTRATION_STRATEGY | openai_function | Orchestration strategy. Use Azure OpenAI Functions (openai_function), Semantic Kernel (semantic_kernel), LangChain (langchain) or Prompt Flow (prompt_flow) for messages orchestration. If you are using a new model version 0613 select any strategy, if you are using a 0314 model version select "langchain". Note that both `openai_function` and `semantic_kernel` use OpenAI function calling. Prompt Flow option is still in development and does not support RBAC or integrated vectorization as of yet.| |AZURE_CONTENT_SAFETY_ENDPOINT | | The endpoint of the Azure AI Content Safety service | diff --git a/docs/contract_assistance.md b/docs/contract_assistance.md index ce980a164..8c6c355e6 100644 --- a/docs/contract_assistance.md +++ b/docs/contract_assistance.md @@ -22,9 +22,7 @@ To apply the suggested configurations in your deployment, update the following f - **Azure Semantic Search**: Set `AZURE_SEARCH_USE_SEMANTIC_SEARCH` to `true` - **Azure Cognitive Search Top K 15**: Set `AZURE_SEARCH_TOP_K` to `15`. - **Azure Search Integrated Vectorization**: Set `AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION` to `true`. -- **Azure OpenAI Model**: Set `AZURE_OPENAI_MODEL` to `gpt-4o`. -- **Azure OpenAI Model Name**: Set `AZURE_OPENAI_MODEL_NAME` to `gpt-4o`. (could be different based on the name of the Azure OpenAI model deployment) -- **Azure OpenAI Model Name Version**: Set `AZURE_OPENAI_MODEL_VERSION` to `2024-05-13`. +- **Azure OpenAI Model Info**: Set `AZURE_OPENAI_MODEL_INFO` to `{"model":"gpt-4o","modelName":"gpt-4o","modelVersion":"2024-05-13"}`.(model could be different based on the name of the Azure OpenAI model deployment) - **Conversation Flow Options**: Set `CONVERSATION_FLOW` to `byod` - **Orchestration Strategy**: Set `ORCHESTRATION_STRATEGY` to `Semantic Kernel`. diff --git a/docs/employee_assistance.md b/docs/employee_assistance.md index e23616684..1af072d01 100644 --- a/docs/employee_assistance.md +++ b/docs/employee_assistance.md @@ -22,9 +22,7 @@ To apply the suggested configurations in your deployment, update the following f - **Azure Semantic Search**: Set `AZURE_SEARCH_USE_SEMANTIC_SEARCH` to `true` - **Azure Cognitive Search Top K 15**: Set `AZURE_SEARCH_TOP_K` to `15`. - **Azure Search Integrated Vectorization**: Set `AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION` to `true`. -- **Azure OpenAI Model**: Set `AZURE_OPENAI_MODEL` to `gpt-4o`. -- **Azure OpenAI Model Name**: Set `AZURE_OPENAI_MODEL_NAME` to `gpt-4o`. (could be different based on the name of the Azure OpenAI model deployment) -- **Azure OpenAI Model Name Version**: Set `AZURE_OPENAI_MODEL_VERSION` to `2024-05-13`. +- **Azure OpenAI Model Info**: Set `AZURE_OPENAI_MODEL_INFO` to `{"model":"gpt-4o","modelName":"gpt-4o","modelVersion":"2024-05-13"}`.(model could be different based on the name of the Azure OpenAI model deployment). - **Conversation Flow Options**: Set `CONVERSATION_FLOW` to `byod` - **Orchestration Strategy**: Set `ORCHESTRATION_STRATEGY` to `Semantic Kernel`. diff --git a/docs/model_configuration.md b/docs/model_configuration.md index 0eeeef4d1..f9cc93853 100644 --- a/docs/model_configuration.md +++ b/docs/model_configuration.md @@ -11,15 +11,14 @@ This document outlines the necessary steps and configurations required for setti - For a list of available models, see the [Microsoft Azure AI Services - OpenAI Models documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). ## Environment Variables (as listed in Azure AI Studio) -- You can access the Environment Variables section of the `LOCAL_DEPLOYMENT.md` file by clicking on this link: [Environment Variables section in LOCAL_DEPLOYMENT.md](docs/LOCAL_DEPLOYMENT.md#environment-variables). +- You can access the Environment Variables section of the `LOCAL_DEPLOYMENT.md` file by clicking on this link: [Environment Variables section in LOCAL_DEPLOYMENT.md](LOCAL_DEPLOYMENT.md#environment-variables). ### LLM -- `AZURE_OPENAI_MODEL`: The Azure OpenAI Model Deployment Name - - example: `my-gpt-35-turbo-16k` -- `AZURE_OPENAI_MODEL_NAME`: The Azure OpenAI Model Name - - example: `gpt-35-turbo-16k` -- `AZURE_OPENAI_MODEL_VERSION`: The Azure OpenAI Model Version - - example: `0613` +- `AZURE_OPENAI_MODEL_INFO`: The Azure OpenAI Model Info + - example: `{"model":"gpt-35-turbo-16k","modelName":"gpt-35-turbo-16k","modelVersion":"0613"}` + - `model` - The Azure OpenAI Model Deployment Name + - `modelName` - The Azure OpenAI Model Name + - `modelVersion` - The Azure OpenAI Model Version - `AZURE_OPENAI_MODEL_CAPACITY`: The Tokens per Minute Rate Limit (thousands) - example: `30` @@ -34,12 +33,11 @@ This document outlines the necessary steps and configurations required for setti - example: `10` ### EMBEDDINGS -- `AZURE_OPENAI_EMBEDDING_MODEL`: The Azure OpenAI Model Deployment Name - - example: `my-text-embedding-ada-002` -- `AZURE_OPENAI_EMBEDDING_MODEL_NAME`: The Azure OpenAI Model Name - - example: `text-embedding-ada-002` -- `AZURE_OPENAI_EMBEDDING_MODEL_VERSION`: The Azure OpenAI Model Version - - example: `2` +- `AZURE_OPENAI_EMBEDDING_MODEL_INFO`: The Azure OpenAI Model Deployment Name + - example: `{"model":"text-embedding-ada-002","modelName":"text-embedding-ada-002","modelVersion":"2"}` + - `model` - The name of your Azure OpenAI embeddings model deployment. + - `modelName` - The name of the embeddings model (can be found in Azure AI Studio). + - `modelVersion` - The version of the embeddings model to use (can be found in Azure AI Studio). - `AZURE_OPENAI_EMBEDDING_MODEL_CAPACITY`: The Tokens per Minute Rate Limit (thousands) - example: `30` - `AZURE_SEARCH_DIMENSIONS`: Azure OpenAI Embeddings dimensions. A full list of dimensions can be found [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#embeddings-models). @@ -65,12 +63,8 @@ This document outlines the necessary steps and configurations required for setti ## GPT-4o & Text-Embeddings-3-Large - The following environment variables are set for the GPT-4o and Text-Embeddings-3-Large models: - `AZURE_OPENAI_API_VERSION`: `2024-05-01-preview` - - `AZURE_OPENAI_MODEL`: `my-gpt-4o` - - `AZURE_OPENAI_MODEL_NAME`: `gpt-4o` - - `AZURE_OPENAI_MODEL_VERSION`: `2024-05-13` - - `AZURE_OPENAI_EMBEDDING_MODEL`: `my-text-embedding-3-large` - - `AZURE_OPENAI_EMBEDDING_MODEL_NAME`: `text-embedding-3-large` - - `AZURE_OPENAI_EMBEDDING_MODEL_VERSION`: `1` + - `AZURE_OPENAI_MODEL_INFO`: `{"model":"my-gpt-4o","modelName":"gpt-4o","modelVersion":"2024-05-13"}` + - `AZURE_OPENAI_EMBEDDING_MODEL_INFO`: `{"model":"my-text-embedding-3-large","modelName":"text-embedding-3-large","modelVersion":"1"}` - `AZURE_SEARCH_DIMENSIONS`: `3072` - `AZURE_MAX_TOKENS`: `4096` From 857e7c9ef344d2a0cffad5577d070d5cf1996ce9 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 13 Nov 2024 16:42:23 +0530 Subject: [PATCH 008/107] Update LOCAL_DEPLOYMENT.md --- docs/LOCAL_DEPLOYMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/LOCAL_DEPLOYMENT.md b/docs/LOCAL_DEPLOYMENT.md index 51d8e0844..1575f481e 100644 --- a/docs/LOCAL_DEPLOYMENT.md +++ b/docs/LOCAL_DEPLOYMENT.md @@ -202,8 +202,8 @@ Execute the above [shell command](#L81) to run the function locally. You may nee |AzureWebJobsStorage||The connection string to the Azure Blob Storage for the Azure Functions Batch processing| |BACKEND_URL||The URL for the Backend Batch Azure Function. Use http://localhost:7071 for local execution| |DOCUMENT_PROCESSING_QUEUE_NAME|doc-processing|The name of the Azure Queue to handle the Batch processing| -|AZURE_BLOB_STORAGE_INFO|{"containerName":"documents","accountName":"accountName","accountKey":"accountKey"}"|`containerName`: The name of the Container in the Azure Blob Storage for storing the original documents to be processed.
`accountName`: The name of the Azure Blob Storage for storing the original documents to be processed.
`accountKey`: The key of the Azure Blob Storage for storing the original documents to be processed.| -|AZURE_FORM_RECOGNIZER_INFO|{"endpoint":"endpoint","key":"key"}|`endpoint`: The name of the Azure Form Recognizer for extracting the text from the documents.
`key`: The key of the Azure Form Recognizer for extracting the text from the documents.| +|AZURE_BLOB_STORAGE_INFO|{"containerName":"documents","accountName":"","accountKey":""}"|`containerName`: The name of the Container in the Azure Blob Storage for storing the original documents to be processed.
`accountName`: The name of the Azure Blob Storage for storing the original documents to be processed.
`accountKey`: The key of the Azure Blob Storage for storing the original documents to be processed.| +|AZURE_FORM_RECOGNIZER_INFO|{"endpoint":"","key":""}|`endpoint`: The name of the Azure Form Recognizer for extracting the text from the documents.
`key`: The key of the Azure Form Recognizer for extracting the text from the documents.| |APPLICATIONINSIGHTS_CONNECTION_STRING||The Application Insights connection string to store the application logs| |ORCHESTRATION_STRATEGY | openai_function | Orchestration strategy. Use Azure OpenAI Functions (openai_function), Semantic Kernel (semantic_kernel), LangChain (langchain) or Prompt Flow (prompt_flow) for messages orchestration. If you are using a new model version 0613 select any strategy, if you are using a 0314 model version select "langchain". Note that both `openai_function` and `semantic_kernel` use OpenAI function calling. Prompt Flow option is still in development and does not support RBAC or integrated vectorization as of yet.| |AZURE_CONTENT_SAFETY_ENDPOINT | | The endpoint of the Azure AI Content Safety service | From 3557a004588772ba63c0e49d2bf8c09fa162c255 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 13 Nov 2024 16:42:57 +0530 Subject: [PATCH 009/107] Update TEAMS_LOCAL_DEPLOYMENT.md --- docs/TEAMS_LOCAL_DEPLOYMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/TEAMS_LOCAL_DEPLOYMENT.md b/docs/TEAMS_LOCAL_DEPLOYMENT.md index ae7e84cd3..f9234830d 100644 --- a/docs/TEAMS_LOCAL_DEPLOYMENT.md +++ b/docs/TEAMS_LOCAL_DEPLOYMENT.md @@ -76,8 +76,8 @@ Or use the [Azure Functions VS Code extension](https://marketplace.visualstudio. |AzureWebJobsStorage||The connection string to the Azure Blob Storage for the Azure Functions Batch processing| |BACKEND_URL||The URL for the Backend Batch Azure Function. Use http://localhost:7071 for local execution| |DOCUMENT_PROCESSING_QUEUE_NAME|doc-processing|The name of the Azure Queue to handle the Batch processing| -|AZURE_BLOB_STORAGE_INFO|{"containerName":"documents","accountName":"accountName","accountKey":"accountKey"}"|`containerName`: The name of the Container in the Azure Blob Storage for storing the original documents to be processed.
`accountName`: The name of the Azure Blob Storage for storing the original documents to be processed.
`accountKey`: The key of the Azure Blob Storage for storing the original documents to be processed.| -|AZURE_FORM_RECOGNIZER_INFO|{"endpoint":"endpoint","key":"key"}|`endpoint`: The name of the Azure Form Recognizer for extracting the text from the documents.
`key`: The key of the Azure Form Recognizer for extracting the text from the documents.| +|AZURE_BLOB_STORAGE_INFO|{"containerName":"documents","accountName":"","accountKey":""}"|`containerName`: The name of the Container in the Azure Blob Storage for storing the original documents to be processed.
`accountName`: The name of the Azure Blob Storage for storing the original documents to be processed.
`accountKey`: The key of the Azure Blob Storage for storing the original documents to be processed.| +|AZURE_FORM_RECOGNIZER_INFO|{"endpoint":"","key":""}|`endpoint`: The name of the Azure Form Recognizer for extracting the text from the documents.
`key`: The key of the Azure Form Recognizer for extracting the text from the documents.| |APPLICATIONINSIGHTS_CONNECTION_STRING||The Application Insights connection string to store the application logs| |ORCHESTRATION_STRATEGY | openai_function | Orchestration strategy. Use Azure OpenAI Functions (openai_function), Semantic Kernel (semantic_kernel), LangChain (langchain) or Prompt Flow (prompt_flow) for messages orchestration. If you are using a new model version 0613 select any strategy, if you are using a 0314 model version select "langchain". Note that both `openai_function` and `semantic_kernel` use OpenAI function calling. Prompt Flow option is still in development and does not support RBAC or integrated vectorization as of yet.| |AZURE_CONTENT_SAFETY_ENDPOINT | | The endpoint of the Azure AI Content Safety service | From b25b585da418e65bf8d832989e631a693742c32c Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 13 Nov 2024 16:48:47 +0530 Subject: [PATCH 010/107] Update .env.sample --- .env.sample | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index 12e43e9e3..b3026f13b 100644 --- a/.env.sample +++ b/.env.sample @@ -36,9 +36,9 @@ AzureWebJobsStorage= BACKEND_URL=http://localhost:7071 DOCUMENT_PROCESSING_QUEUE_NAME= # Azure Blob Storage for storing the original documents to be processed -AZURE_BLOB_STORAGE_INFO="{\"containerName\":\"documents\",\"accountName\":\"accountName\",\"accountKey\":\"accountKey\"}" +AZURE_BLOB_STORAGE_INFO="{\"containerName\":\"documents\",\"accountName\":\"\",\"accountKey\":\"\"}" # Azure Form Recognizer for extracting the text from the documents -AZURE_FORM_RECOGNIZER_INFO="{\"endpoint\":\"endpoint\",\"key\":\"key\"}" +AZURE_FORM_RECOGNIZER_INFO="{\"endpoint\":\"\",\"key\":\"\"}" # Azure AI Content Safety for filtering out the inappropriate questions or answers AZURE_CONTENT_SAFETY_ENDPOINT= AZURE_CONTENT_SAFETY_KEY= From 28e2513969b904239542850e2c2b28766945eb6d Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 13 Nov 2024 21:09:41 +0530 Subject: [PATCH 011/107] User Story 10017: Configure Environment Variables for Database Connection PostgresSQL --- .env.sample | 1 + .../batch/utilities/helpers/env_helper.py | 36 ++-- poetry.lock | 172 ++++++++---------- pyproject.toml | 6 +- 4 files changed, 97 insertions(+), 118 deletions(-) diff --git a/.env.sample b/.env.sample index 1c46d3451..7015fa1ea 100644 --- a/.env.sample +++ b/.env.sample @@ -66,3 +66,4 @@ CONVERSATION_FLOW= AZURE_COSMOSDB_INFO="{\"accountName\":\"cosmos-abc123\",\"databaseName\":\"db_conversation_history\",\"containerName\":\"conversations\"}" AZURE_COSMOSDB_ACCOUNT_KEY= AZURE_COSMOSDB_ENABLE_FEEDBACK= +AZURE_POSTGRESQL_INFO="{\"postgresdbuser\":\"\",\"postgresdbname\":\"postgres\",\"postgresdbhost\":\"\"}" diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index c6f77f35c..d7e19450b 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -285,22 +285,26 @@ def __load_config(self, **kwargs) -> None: self.PROMPT_FLOW_DEPLOYMENT_NAME = os.getenv("PROMPT_FLOW_DEPLOYMENT_NAME", "") - # Chat History CosmosDB Integration Settings - azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") - self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "") - self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "") - self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get( - "containerName", "" - ) - self.AZURE_COSMOSDB_ACCOUNT_KEY = self.secretHelper.get_secret( - "AZURE_COSMOSDB_ACCOUNT_KEY" - ) - self.AZURE_COSMOSDB_ENABLE_FEEDBACK = ( - os.getenv("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true" - ) - self.CHAT_HISTORY_ENABLED = self.get_env_var_bool( - "CHAT_HISTORY_ENABLED", "true" - ) + # Chat History DB Integration Settings + # Set default values based on DATABASE_TYPE + self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "CosmosDB") + self.CHAT_HISTORY_ENABLED = self.get_env_var_bool("CHAT_HISTORY_ENABLED", "true") + # Cosmos DB configuration + if self.DATABASE_TYPE == "CosmosDB": + azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") + self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "") + self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "") + self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get("containerName", "") + self.AZURE_COSMOSDB_ACCOUNT_KEY = self.secretHelper.get_secret("AZURE_COSMOSDB_ACCOUNT_KEY") + self.AZURE_COSMOSDB_ENABLE_FEEDBACK = (os.getenv("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true") + # PostgreSQL configuration + elif self.DATABASE_TYPE == "PostgreSQL": + azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") + self.POSTGRESQL_USER = azure_postgresql_info.get("postgresdbuser", "") + self.POSTGRESQL_DATABASE = azure_postgresql_info.get("postgresdbname", "") + self.POSTGRESQL_HOST = azure_postgresql_info.get("postgresdbhost", "") + else: + raise ValueError("Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'.") def is_chat_model(self): if "gpt-4" in self.AZURE_OPENAI_MODEL_NAME.lower(): diff --git a/poetry.lock b/poetry.lock index edee6f2d6..ce20e3f69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -415,8 +415,6 @@ files = [ [package.dependencies] async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} -gssapi = {version = "*", optional = true, markers = "platform_system != \"Windows\" and extra == \"gssauth\""} -sspilib = {version = "*", optional = true, markers = "platform_system == \"Windows\" and extra == \"gssauth\""} [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] @@ -1837,43 +1835,6 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] -[[package]] -name = "gssapi" -version = "1.9.0" -description = "Python GSSAPI Wrapper" -optional = false -python-versions = ">=3.8" -files = [ - {file = "gssapi-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:261e00ac426d840055ddb2199f4989db7e3ce70fa18b1538f53e392b4823e8f1"}, - {file = "gssapi-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:14a1ae12fdf1e4c8889206195ba1843de09fe82587fa113112887cd5894587c6"}, - {file = "gssapi-1.9.0-cp310-cp310-win32.whl", hash = "sha256:2a9c745255e3a810c3e8072e267b7b302de0705f8e9a0f2c5abc92fe12b9475e"}, - {file = "gssapi-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:dfc1b4c0bfe9f539537601c9f187edc320daf488f694e50d02d0c1eb37416962"}, - {file = "gssapi-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67d9be5e34403e47fb5749d5a1ad4e5a85b568e6a9add1695edb4a5b879f7560"}, - {file = "gssapi-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11e9b92cef11da547fc8c210fa720528fd854038504103c1b15ae2a89dce5fcd"}, - {file = "gssapi-1.9.0-cp311-cp311-win32.whl", hash = "sha256:6c5f8a549abd187687440ec0b72e5b679d043d620442b3637d31aa2766b27cbe"}, - {file = "gssapi-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:59e1a1a9a6c5dc430dc6edfcf497f5ca00cf417015f781c9fac2e85652cd738f"}, - {file = "gssapi-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b66a98827fbd2864bf8993677a039d7ba4a127ca0d2d9ed73e0ef4f1baa7fd7f"}, - {file = "gssapi-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2bddd1cc0c9859c5e0fd96d4d88eb67bd498fdbba45b14cdccfe10bfd329479f"}, - {file = "gssapi-1.9.0-cp312-cp312-win32.whl", hash = "sha256:10134db0cf01bd7d162acb445762dbcc58b5c772a613e17c46cf8ad956c4dfec"}, - {file = "gssapi-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:e28c7d45da68b7e36ed3fb3326744bfe39649f16e8eecd7b003b082206039c76"}, - {file = "gssapi-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cea344246935b5337e6f8a69bb6cc45619ab3a8d74a29fcb0a39fd1e5843c89c"}, - {file = "gssapi-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a5786bd9fcf435bd0c87dc95ae99ad68cefcc2bcc80c71fef4cb0ccdfb40f1e"}, - {file = "gssapi-1.9.0-cp313-cp313-win32.whl", hash = "sha256:c99959a9dd62358e370482f1691e936cb09adf9a69e3e10d4f6a097240e9fd28"}, - {file = "gssapi-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a2e43f50450e81fe855888c53df70cdd385ada979db79463b38031710a12acd9"}, - {file = "gssapi-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c0e378d62b2fc352ca0046030cda5911d808a965200f612fdd1d74501b83e98f"}, - {file = "gssapi-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b74031c70864d04864b7406c818f41be0c1637906fb9654b06823bcc79f151dc"}, - {file = "gssapi-1.9.0-cp38-cp38-win32.whl", hash = "sha256:f2f3a46784d8127cc7ef10d3367dedcbe82899ea296710378ccc9b7cefe96f4c"}, - {file = "gssapi-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:a81f30cde21031e7b1f8194a3eea7285e39e551265e7744edafd06eadc1c95bc"}, - {file = "gssapi-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbc93fdadd5aab9bae594538b2128044b8c5cdd1424fe015a465d8a8a587411a"}, - {file = "gssapi-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b2a3c0a9beb895942d4b8e31f515e52c17026e55aeaa81ee0df9bbfdac76098"}, - {file = "gssapi-1.9.0-cp39-cp39-win32.whl", hash = "sha256:060b58b455d29ab8aca74770e667dca746264bee660ac5b6a7a17476edc2c0b8"}, - {file = "gssapi-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:11c9fe066edb0fa0785697eb0cecf2719c7ad1d9f2bf27be57b647a617bcfaa5"}, - {file = "gssapi-1.9.0.tar.gz", hash = "sha256:f468fac8f3f5fca8f4d1ca19e3cd4d2e10bd91074e7285464b22715d13548afe"}, -] - -[package.dependencies] -decorator = "*" - [[package]] name = "h11" version = "0.14.0" @@ -4659,21 +4620,79 @@ files = [ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] -name = "psycopg2" +name = "psycopg2-binary" version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" files = [ - {file = "psycopg2-2.9.10-cp310-cp310-win32.whl", hash = "sha256:5df2b672140f95adb453af93a7d669d7a7bf0a56bcd26f1502329166f4a61716"}, - {file = "psycopg2-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:c6f7b8561225f9e711a9c47087388a97fdc948211c10a4bccbf0ba68ab7b3b5a"}, - {file = "psycopg2-2.9.10-cp311-cp311-win32.whl", hash = "sha256:47c4f9875125344f4c2b870e41b6aad585901318068acd01de93f3677a6522c2"}, - {file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"}, - {file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"}, - {file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"}, - {file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"}, - {file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"}, - {file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"}, + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, ] [[package]] @@ -6008,51 +6027,6 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] -[[package]] -name = "sspilib" -version = "0.2.0" -description = "SSPI API bindings for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sspilib-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34f566ba8b332c91594e21a71200de2d4ce55ca5a205541d4128ed23e3c98777"}, - {file = "sspilib-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b11e4f030de5c5de0f29bcf41a6e87c9fd90cb3b0f64e446a6e1d1aef4d08f5"}, - {file = "sspilib-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e82f87d77a9da62ce1eac22f752511a99495840177714c772a9d27b75220f78"}, - {file = "sspilib-0.2.0-cp310-cp310-win32.whl", hash = "sha256:e436fa09bcf353a364a74b3ef6910d936fa8cd1493f136e517a9a7e11b319c57"}, - {file = "sspilib-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:850a17c98d2b8579b183ce37a8df97d050bc5b31ab13f5a6d9e39c9692fe3754"}, - {file = "sspilib-0.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:a4d788a53b8db6d1caafba36887d5ac2087e6b6be6f01eb48f8afea6b646dbb5"}, - {file = "sspilib-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e0943204c8ba732966fdc5b69e33cf61d8dc6b24e6ed875f32055d9d7e2f76cd"}, - {file = "sspilib-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1cdfc5ec2f151f26e21aa50ccc7f9848c969d6f78264ae4f38347609f6722df"}, - {file = "sspilib-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6c33495a3de1552120c4a99219ebdd70e3849717867b8cae3a6a2f98fef405"}, - {file = "sspilib-0.2.0-cp311-cp311-win32.whl", hash = "sha256:400d5922c2c2261009921157c4b43d868e84640ad86e4dc84c95b07e5cc38ac6"}, - {file = "sspilib-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3e7d19c16ba9189ef8687b591503db06cfb9c5eb32ab1ca3bb9ebc1a8a5f35c"}, - {file = "sspilib-0.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f65c52ead8ce95eb78a79306fe4269ee572ef3e4dcc108d250d5933da2455ecc"}, - {file = "sspilib-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:abac93a90335590b49ef1fc162b538576249c7f58aec0c7bcfb4b860513979b4"}, - {file = "sspilib-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1208720d8e431af674c5645cec365224d035f241444d5faa15dc74023ece1277"}, - {file = "sspilib-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48dceb871ecf9cf83abdd0e6db5326e885e574f1897f6ae87d736ff558f4bfa"}, - {file = "sspilib-0.2.0-cp312-cp312-win32.whl", hash = "sha256:bdf9a4f424add02951e1f01f47441d2e69a9910471e99c2c88660bd8e184d7f8"}, - {file = "sspilib-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:40a97ca83e503a175d1dc9461836994e47e8b9bcf56cab81a2c22e27f1993079"}, - {file = "sspilib-0.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8ffc09819a37005c66a580ff44f544775f9745d5ed1ceeb37df4e5ff128adf36"}, - {file = "sspilib-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:40ff410b64198cf1d704718754fc5fe7b9609e0c49bf85c970f64c6fc2786db4"}, - {file = "sspilib-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:02d8e0b6033de8ccf509ba44fdcda7e196cdedc0f8cf19eb22c5e4117187c82f"}, - {file = "sspilib-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7943fe14f8f6d72623ab6401991aa39a2b597bdb25e531741b37932402480f"}, - {file = "sspilib-0.2.0-cp313-cp313-win32.whl", hash = "sha256:b9044d6020aa88d512e7557694fe734a243801f9a6874e1c214451eebe493d92"}, - {file = "sspilib-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c39a698491f43618efca8776a40fb7201d08c415c507f899f0df5ada15abefaa"}, - {file = "sspilib-0.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:863b7b214517b09367511c0ef931370f0386ed2c7c5613092bf9b106114c4a0e"}, - {file = "sspilib-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0ede7afba32f2b681196c0b8520617d99dc5d0691d04884d59b476e31b41286"}, - {file = "sspilib-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bd95df50efb6586054963950c8fa91ef994fb73c5c022c6f85b16f702c5314da"}, - {file = "sspilib-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9460258d3dc3f71cc4dcfd6ac078e2fe26f272faea907384b7dd52cb91d9ddcc"}, - {file = "sspilib-0.2.0-cp38-cp38-win32.whl", hash = "sha256:6fa9d97671348b97567020d82fe36c4211a2cacf02abbccbd8995afbf3a40bfc"}, - {file = "sspilib-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:32422ad7406adece12d7c385019b34e3e35ff88a7c8f3d7c062da421772e7bfa"}, - {file = "sspilib-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6944a0d7fe64f88c9bde3498591acdb25b178902287919b962c398ed145f71b9"}, - {file = "sspilib-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0216344629b0f39c2193adb74d7e1bed67f1bbd619e426040674b7629407eba9"}, - {file = "sspilib-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5f84b9f614447fc451620c5c44001ed48fead3084c7c9f2b9cefe1f4c5c3d0"}, - {file = "sspilib-0.2.0-cp39-cp39-win32.whl", hash = "sha256:b290eb90bf8b8136b0a61b189629442052e1a664bd78db82928ec1e81b681fb5"}, - {file = "sspilib-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:404c16e698476e500a7fe67be5457fadd52d8bdc9aeb6c554782c8f366cc4fc9"}, - {file = "sspilib-0.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:8697e5dd9229cd3367bca49fba74e02f867759d1d416a717e26c3088041b9814"}, - {file = "sspilib-0.2.0.tar.gz", hash = "sha256:4d6cd4290ca82f40705efeb5e9107f7abcd5e647cb201a3d04371305938615b8"}, -] - [[package]] name = "stack-data" version = "0.6.3" @@ -6091,13 +6065,13 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7 [[package]] name = "streamlit" -version = "1.39.0" +version = "1.40.0" description = "A faster way to build and share data apps" optional = false python-versions = "!=3.9.7,>=3.8" files = [ - {file = "streamlit-1.39.0-py2.py3-none-any.whl", hash = "sha256:a359fc54ed568b35b055ff1d453c320735539ad12e264365a36458aef55a5fba"}, - {file = "streamlit-1.39.0.tar.gz", hash = "sha256:fef9de7983c4ee65c08e85607d7ffccb56b00482b1041fa62f90e4815d39df3a"}, + {file = "streamlit-1.40.0-py2.py3-none-any.whl", hash = "sha256:05d22bc111d682ef4deaf7ededeec2305051b99dd6d7d564788705e4ce6f8029"}, + {file = "streamlit-1.40.0.tar.gz", hash = "sha256:6e4d3b90c4934951f97d790daf7953df5beb2916e447ac9f78e1b76a9ef83327"}, ] [package.dependencies] @@ -6109,7 +6083,7 @@ gitpython = ">=3.0.7,<3.1.19 || >3.1.19,<4" numpy = ">=1.20,<3" packaging = ">=20,<25" pandas = ">=1.4.0,<3" -pillow = ">=7.1.0,<11" +pillow = ">=7.1.0,<12" protobuf = ">=3.20,<6" pyarrow = ">=7.0" pydeck = ">=0.8.0b4,<1" @@ -6795,4 +6769,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "41fb9d5ae8267aebea11570dab6d8d436851d7bab8671e4d5750b93f1869d4cd" +content-hash = "6b9ab8ba01b4a246ec4cdd28450595a0ae4d626c5446f66e1aaeeae8e13d0d75" diff --git a/pyproject.toml b/pyproject.toml index 91935c526..f3c742829 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" azure-functions = "1.21.0" -streamlit = "1.39.0" +streamlit = "1.40.0" python-dotenv = "1.0.1" azure-ai-formrecognizer = "3.3.3" azure-storage-blob = "12.20.0" @@ -40,8 +40,8 @@ jsonschema = "^4.23.0" semantic-kernel = {version = "1.3.0", python = "<3.13"} azure-ai-ml = "^1.21.1" azure-cosmos = "^4.7.0" -asyncpg = {extras = ["gssauth"], version = "^0.30.0"} -psycopg2 = "^2.9.10" +asyncpg = "^0.30.0" +psycopg2-binary = "^2.9.10" [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" From 055ef0d1a5d02adbe0689a545f05e12769048712 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 13 Nov 2024 21:18:54 +0530 Subject: [PATCH 012/107] DATABASE_TYPE --- .env.sample | 3 ++- code/backend/batch/utilities/helpers/env_helper.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.env.sample b/.env.sample index 7015fa1ea..5b0729a4d 100644 --- a/.env.sample +++ b/.env.sample @@ -66,4 +66,5 @@ CONVERSATION_FLOW= AZURE_COSMOSDB_INFO="{\"accountName\":\"cosmos-abc123\",\"databaseName\":\"db_conversation_history\",\"containerName\":\"conversations\"}" AZURE_COSMOSDB_ACCOUNT_KEY= AZURE_COSMOSDB_ENABLE_FEEDBACK= -AZURE_POSTGRESQL_INFO="{\"postgresdbuser\":\"\",\"postgresdbname\":\"postgres\",\"postgresdbhost\":\"\"}" +AZURE_POSTGRESQL_INFO="{\"user\":\"\",\"dbname\":\"postgres\",\"host\":\"\"}" +DATABASE_TYPE="CosmosDB" diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index d7e19450b..86b847558 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -300,9 +300,9 @@ def __load_config(self, **kwargs) -> None: # PostgreSQL configuration elif self.DATABASE_TYPE == "PostgreSQL": azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") - self.POSTGRESQL_USER = azure_postgresql_info.get("postgresdbuser", "") - self.POSTGRESQL_DATABASE = azure_postgresql_info.get("postgresdbname", "") - self.POSTGRESQL_HOST = azure_postgresql_info.get("postgresdbhost", "") + self.POSTGRESQL_USER = azure_postgresql_info.get("user", "") + self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "") + self.POSTGRESQL_HOST = azure_postgresql_info.get("host", "") else: raise ValueError("Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'.") From 0bee33631ed7a65f8d4283856d785d6fc6a05032 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Wed, 13 Nov 2024 14:56:41 -0500 Subject: [PATCH 013/107] Update poetry.lock --- poetry.lock | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 4b66baf42..ce20e3f69 100644 --- a/poetry.lock +++ b/poetry.lock @@ -355,6 +355,72 @@ files = [ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, + {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"}, + {file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"}, + {file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"}, + {file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"}, + {file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, + {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, + {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34"}, + {file = "asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4"}, + {file = "asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547"}, + {file = "asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a"}, + {file = "asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} + +[package.extras] +docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] +gssauth = ["gssapi", "sspilib"] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] + [[package]] name = "attrs" version = "23.2.0" @@ -4553,6 +4619,82 @@ files = [ [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -6627,4 +6769,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "4e6cf838156c064a0f78d858f4be59c7cf15a1498431f34199071fd3b0374126" +content-hash = "6b9ab8ba01b4a246ec4cdd28450595a0ae4d626c5446f66e1aaeeae8e13d0d75" From 428130e1358859ce8226b9d1464b8ff793e5b9cf Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 14 Nov 2024 23:33:11 +0530 Subject: [PATCH 014/107] Load Respective Database Class Dynamically - either CosmosDB or PostgreSQL --- code/backend/api/chat_history.py | 110 ++++++++---------- .../batch/utilities/chat_history/cosmosdb.py | 10 +- .../chat_history/database_client_base.py | 82 +++++++++++++ .../chat_history/database_factory.py | 37 ++++++ .../chat_history/postgresdbservice.py | 106 ++++++++++++----- 5 files changed, 251 insertions(+), 94 deletions(-) create mode 100644 code/backend/batch/utilities/chat_history/database_client_base.py create mode 100644 code/backend/batch/utilities/chat_history/database_factory.py diff --git a/code/backend/api/chat_history.py b/code/backend/api/chat_history.py index 2aba1a8a4..79c572d0b 100644 --- a/code/backend/api/chat_history.py +++ b/code/backend/api/chat_history.py @@ -4,13 +4,12 @@ from dotenv import load_dotenv from flask import request, jsonify, Blueprint from openai import AsyncAzureOpenAI -from backend.batch.utilities.chat_history.cosmosdb import CosmosConversationClient from backend.batch.utilities.chat_history.auth_utils import ( get_authenticated_user_details, ) from backend.batch.utilities.helpers.config.config_helper import ConfigHelper -from azure.identity.aio import DefaultAzureCredential from backend.batch.utilities.helpers.env_helper import EnvHelper +from backend.batch.utilities.chat_history.database_factory import DatabaseFactory load_dotenv() bp_chat_history_response = Blueprint("chat_history", __name__) @@ -20,35 +19,13 @@ env_helper: EnvHelper = EnvHelper() -def init_cosmosdb_client(): - cosmos_conversation_client = None - config = ConfigHelper.get_active_config_or_default() - if config.enable_chat_history: - try: - cosmos_endpoint = ( - f"https://{env_helper.AZURE_COSMOSDB_ACCOUNT}.documents.azure.com:443/" - ) - - if not env_helper.AZURE_COSMOSDB_ACCOUNT_KEY: - credential = DefaultAzureCredential() - else: - credential = env_helper.AZURE_COSMOSDB_ACCOUNT_KEY - - cosmos_conversation_client = CosmosConversationClient( - cosmosdb_endpoint=cosmos_endpoint, - credential=credential, - database_name=env_helper.AZURE_COSMOSDB_DATABASE, - container_name=env_helper.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER, - enable_message_feedback=env_helper.AZURE_COSMOSDB_ENABLE_FEEDBACK, - ) - except Exception as e: - logger.exception("Exception in CosmosDB initialization: %s", e) - cosmos_conversation_client = None - raise e - else: - logger.debug("CosmosDB not configured") - - return cosmos_conversation_client +def init_database_client(): + try: + conversation_client = DatabaseFactory.get_conversation_client() + return conversation_client + except Exception as e: + logger.exception("Exception in database initialization: %s", e) + raise e def init_openai_client(): @@ -83,12 +60,13 @@ async def list_conversations(): request_headers=request.headers ) user_id = authenticated_user["user_principal_id"] - cosmos_conversation_client = init_cosmosdb_client() - if not cosmos_conversation_client: + conversation_client = init_database_client() + if not conversation_client: return (jsonify({"error": "database not available"}), 500) + await conversation_client.connect() # get the conversations from cosmos - conversations = await cosmos_conversation_client.get_conversations( + conversations = await conversation_client.get_conversations( user_id, offset=offset, limit=25 ) if not isinstance(conversations, list): @@ -96,7 +74,7 @@ async def list_conversations(): jsonify({"error": f"No conversations for {user_id} were found"}), 400, ) - + await conversation_client.close() return (jsonify(conversations), 200) except Exception as e: @@ -123,12 +101,13 @@ async def rename_conversation(): return (jsonify({"error": "conversation_id is required"}), 400) # make sure cosmos is configured - cosmos_conversation_client = init_cosmosdb_client() - if not cosmos_conversation_client: + conversation_client = init_database_client() + if not conversation_client: return (jsonify({"error": "database not available"}), 500) + await conversation_client.connect() # get the conversation from cosmos - conversation = await cosmos_conversation_client.get_conversation( + conversation = await conversation_client.get_conversation( user_id, conversation_id ) if not conversation: @@ -146,9 +125,10 @@ async def rename_conversation(): if not title or title.strip() == "": return jsonify({"error": "title is required"}), 400 conversation["title"] = title - updated_conversation = await cosmos_conversation_client.upsert_conversation( + updated_conversation = await conversation_client.upsert_conversation( conversation ) + await conversation_client.close() return (jsonify(updated_conversation), 200) except Exception as e: @@ -176,12 +156,13 @@ async def get_conversation(): return (jsonify({"error": "conversation_id is required"}), 400) # make sure cosmos is configured - cosmos_conversation_client = init_cosmosdb_client() - if not cosmos_conversation_client: + conversation_client = init_database_client() + if not conversation_client: return (jsonify({"error": "database not available"}), 500) + await conversation_client.connect() # get the conversation object and the related messages from cosmos - conversation = await cosmos_conversation_client.get_conversation( + conversation = await conversation_client.get_conversation( user_id, conversation_id ) # return the conversation id and the messages in the bot frontend format @@ -196,7 +177,7 @@ async def get_conversation(): ) # get the messages for the conversation from cosmos - conversation_messages = await cosmos_conversation_client.get_messages( + conversation_messages = await conversation_client.get_messages( user_id, conversation_id ) @@ -212,6 +193,7 @@ async def get_conversation(): for msg in conversation_messages ] + await conversation_client.close() return ( jsonify({"conversation_id": conversation_id, "messages": messages}), 200, @@ -246,16 +228,18 @@ async def delete_conversation(): 400, ) - cosmos_conversation_client = init_cosmosdb_client() - if not cosmos_conversation_client: + conversation_client = init_database_client() + if not conversation_client: return (jsonify({"error": "database not available"}), 500) + await conversation_client.connect() # delete the conversation messages from cosmos first - await cosmos_conversation_client.delete_messages(conversation_id, user_id) + await conversation_client.delete_messages(conversation_id, user_id) # Now delete the conversation - await cosmos_conversation_client.delete_conversation(user_id, conversation_id) + await conversation_client.delete_conversation(user_id, conversation_id) + await conversation_client.close() return ( jsonify( { @@ -285,11 +269,12 @@ async def delete_all_conversations(): # get conversations for user # make sure cosmos is configured - cosmos_conversation_client = init_cosmosdb_client() - if not cosmos_conversation_client: + conversation_client = init_database_client() + if not conversation_client: return (jsonify({"error": "database not available"}), 500) + conversation_client.connect() - conversations = await cosmos_conversation_client.get_conversations( + conversations = await conversation_client.get_conversations( user_id, offset=0, limit=None ) if not conversations: @@ -301,15 +286,12 @@ async def delete_all_conversations(): # delete each conversation for conversation in conversations: # delete the conversation messages from cosmos first - await cosmos_conversation_client.delete_messages( - conversation["id"], user_id - ) + await conversation_client.delete_messages(conversation["id"], user_id) # Now delete the conversation - await cosmos_conversation_client.delete_conversation( - user_id, conversation["id"] - ) + await conversation_client.delete_conversation(user_id, conversation["id"]) + await conversation_client.close() return ( jsonify( { @@ -343,17 +325,18 @@ async def update_conversation(): return (jsonify({"error": "conversation_id is required"}), 400) # make sure cosmos is configured - cosmos_conversation_client = init_cosmosdb_client() - if not cosmos_conversation_client: + conversation_client = init_database_client() + if not conversation_client: return jsonify({"error": "database not available"}), 500 + await conversation_client.connect() # check for the conversation_id, if the conversation is not set, we will create a new one - conversation = await cosmos_conversation_client.get_conversation( + conversation = await conversation_client.get_conversation( user_id, conversation_id ) if not conversation: title = await generate_title(request_json["messages"]) - conversation = await cosmos_conversation_client.create_conversation( + conversation = await conversation_client.create_conversation( user_id=user_id, conversation_id=conversation_id, title=title ) conversation_id = conversation["id"] @@ -370,7 +353,7 @@ async def update_conversation(): ), None, ) - createdMessageValue = await cosmos_conversation_client.create_message( + createdMessageValue = await conversation_client.create_message( uuid=str(uuid4()), conversation_id=conversation_id, user_id=user_id, @@ -384,14 +367,14 @@ async def update_conversation(): if len(messages) > 0 and messages[-1]["role"] == "assistant": if len(messages) > 1 and messages[-2].get("role", None) == "tool": # write the tool message first - await cosmos_conversation_client.create_message( + await conversation_client.create_message( uuid=str(uuid4()), conversation_id=conversation_id, user_id=user_id, input_message=messages[-2], ) # write the assistant message - await cosmos_conversation_client.create_message( + await conversation_client.create_message( uuid=str(uuid4()), conversation_id=conversation_id, user_id=user_id, @@ -400,6 +383,7 @@ async def update_conversation(): else: return (jsonify({"error": "no conversationbot"}), 400) + await conversation_client.close() return ( jsonify( { diff --git a/code/backend/batch/utilities/chat_history/cosmosdb.py b/code/backend/batch/utilities/chat_history/cosmosdb.py index 7c3bb70c8..5cac5fc8c 100644 --- a/code/backend/batch/utilities/chat_history/cosmosdb.py +++ b/code/backend/batch/utilities/chat_history/cosmosdb.py @@ -2,8 +2,10 @@ from azure.cosmos.aio import CosmosClient from azure.cosmos import exceptions +from .database_client_base import DatabaseClientBase -class CosmosConversationClient: + +class CosmosConversationClient(DatabaseClientBase): def __init__( self, @@ -42,6 +44,12 @@ def __init__( except exceptions.CosmosResourceNotFoundError: raise ValueError("Invalid CosmosDB container name") + async def connect(self): + pass + + async def close(self): + pass + async def ensure(self): if ( not self.cosmosdb_client diff --git a/code/backend/batch/utilities/chat_history/database_client_base.py b/code/backend/batch/utilities/chat_history/database_client_base.py new file mode 100644 index 000000000..ebbf70fc2 --- /dev/null +++ b/code/backend/batch/utilities/chat_history/database_client_base.py @@ -0,0 +1,82 @@ +from abc import ABC, abstractmethod +from typing import List, Optional, Dict, Any + + +class DatabaseClientBase(ABC): + @abstractmethod + async def connect(self): + """Establish a connection to the database.""" + pass + + @abstractmethod + async def close(self): + """Close the connection to the database.""" + pass + + @abstractmethod + async def ensure(self): + """Verify that the database and required tables/collections exist.""" + pass + + @abstractmethod + async def create_conversation( + self, user_id: str, conversation_id: str, title: str = "" + ) -> bool: + """Create a new conversation entry.""" + pass + + @abstractmethod + async def upsert_conversation(self, conversation: Dict[str, Any]) -> bool: + """Update or insert a conversation entry.""" + pass + + @abstractmethod + async def delete_conversation(self, user_id: str, conversation_id: str) -> bool: + """Delete a specific conversation.""" + pass + + @abstractmethod + async def delete_messages( + self, conversation_id: str, user_id: str + ) -> List[Dict[str, Any]]: + """Delete all messages associated with a conversation.""" + pass + + @abstractmethod + async def get_conversations( + self, user_id: str, limit: int, sort_order: str = "DESC", offset: int = 0 + ) -> List[Dict[str, Any]]: + """Retrieve a list of conversations for a user.""" + pass + + @abstractmethod + async def get_conversation( + self, user_id: str, conversation_id: str + ) -> Optional[Dict[str, Any]]: + """Retrieve a specific conversation by ID.""" + pass + + @abstractmethod + async def create_message( + self, + uuid: str, + conversation_id: str, + user_id: str, + input_message: Dict[str, Any], + ) -> bool: + """Create a new message within a conversation.""" + pass + + @abstractmethod + async def update_message_feedback( + self, user_id: str, message_id: str, feedback: str + ) -> bool: + """Update feedback for a specific message.""" + pass + + @abstractmethod + async def get_messages( + self, user_id: str, conversation_id: str + ) -> List[Dict[str, Any]]: + """Retrieve all messages within a conversation.""" + pass diff --git a/code/backend/batch/utilities/chat_history/database_factory.py b/code/backend/batch/utilities/chat_history/database_factory.py new file mode 100644 index 000000000..91eb23338 --- /dev/null +++ b/code/backend/batch/utilities/chat_history/database_factory.py @@ -0,0 +1,37 @@ +# database_factory.py +from ..helpers.env_helper import EnvHelper +from .cosmosdb import CosmosConversationClient +from .postgresdbservice import PostgresConversationClient +from azure.identity import DefaultAzureCredential + + +class DatabaseFactory: + @staticmethod + def get_conversation_client(): + env_helper: EnvHelper = EnvHelper() + if env_helper.DATABASE_TYPE == "CosmosDB": + cosmos_endpoint = ( + f"https://{env_helper.AZURE_COSMOSDB_ACCOUNT}.documents.azure.com:443/" + ) + credential = ( + DefaultAzureCredential() + if not env_helper.AZURE_COSMOSDB_ACCOUNT_KEY + else env_helper.AZURE_COSMOSDB_ACCOUNT_KEY + ) + return CosmosConversationClient( + cosmosdb_endpoint=cosmos_endpoint, + credential=credential, + database_name=env_helper.AZURE_COSMOSDB_DATABASE, + container_name=env_helper.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER, + enable_message_feedback=env_helper.AZURE_COSMOSDB_ENABLE_FEEDBACK, + ) + elif env_helper.DATABASE_TYPE == "PostgreSQL": + return PostgresConversationClient( + user=env_helper.POSTGRESQL_USER, + host=env_helper.POSTGRESQL_HOST, + database=env_helper.POSTGRESQL_DATABASE, + ) + else: + raise ValueError( + "Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'." + ) diff --git a/code/backend/batch/utilities/chat_history/postgresdbservice.py b/code/backend/batch/utilities/chat_history/postgresdbservice.py index 6343448dc..97dde920e 100644 --- a/code/backend/batch/utilities/chat_history/postgresdbservice.py +++ b/code/backend/batch/utilities/chat_history/postgresdbservice.py @@ -1,56 +1,89 @@ import asyncpg -import uuid from datetime import datetime, timezone -class PostgresConversationClient: - - def __init__(self, dsn: str, enable_message_feedback: bool = False): - self.dsn = dsn +from azure.identity import DefaultAzureCredential + +from .database_client_base import DatabaseClientBase + + +class PostgresConversationClient(DatabaseClientBase): + + def __init__( + self, user: str, host: str, database: str, enable_message_feedback: bool = False + ): + self.user = user + self.host = host + self.database = database self.enable_message_feedback = enable_message_feedback self.conn = None - + async def connect(self): - self.conn = await asyncpg.connect(self.dsn) + credential = DefaultAzureCredential() + token = credential.get_token( + "https://ossrdbms-aad.database.windows.net/.default" + ).token + self.conn = await asyncpg.connect( + user=self.user, + host=self.host, + database=self.database, + password=token, + port=5432, + ssl="require", + ) + async def close(self): if self.conn: await self.conn.close() + async def ensure(self): if not self.conn: return False, "PostgreSQL client not initialized correctly" return True, "PostgreSQL client initialized successfully" - async def create_conversation(self, user_id, title=''): - conversation_id = str(uuid.uuid4()) + + async def create_conversation(self, conversation_id, user_id, title=""): utc_now = datetime.now(timezone.utc) - created_at = utc_now.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' + createdAt = utc_now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" query = """ - INSERT INTO conversations (id, type, created_at, updated_at, user_id, title) - VALUES ($1, 'conversation', $2, $2, $3, $4) + INSERT INTO conversations (id, conversation_id, type, "createdAt", "updatedAt", user_id, title) + VALUES ($1, $2, 'conversation', $3, $3, $4, $5) RETURNING * """ - conversation = await self.conn.fetchrow(query, conversation_id, created_at, user_id, title) + conversation = await self.conn.fetchrow( + query, conversation_id, conversation_id, createdAt, user_id, title + ) return dict(conversation) if conversation else False + async def upsert_conversation(self, conversation): query = """ - INSERT INTO conversations (id, type, created_at, updated_at, user_id, title) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO conversations (id, conversation_id, type, "createdAt", "updatedAt", user_id, title) + VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO UPDATE SET - updated_at = EXCLUDED.updated_at, + "updatedAt" = EXCLUDED."updatedAt", title = EXCLUDED.title RETURNING * """ updated_conversation = await self.conn.fetchrow( - query, conversation['id'], conversation['type'], conversation['createdAt'], - conversation['updatedAt'], conversation['userId'], conversation['title'] + query, + conversation["id"], + conversation["conversation_id"], + conversation["type"], + conversation["createdAt"], + conversation["updatedAt"], + conversation["user_id"], + conversation["title"], ) return dict(updated_conversation) if updated_conversation else False + async def delete_conversation(self, user_id, conversation_id): - query = "DELETE FROM conversations WHERE id = $1 AND user_id = $2" + query = "DELETE FROM conversations WHERE conversation_id = $1 AND user_id = $2" await self.conn.execute(query, conversation_id, user_id) return True + async def delete_messages(self, conversation_id, user_id): query = "DELETE FROM messages WHERE conversation_id = $1 AND user_id = $2 RETURNING *" messages = await self.conn.fetch(query, conversation_id, user_id) return [dict(message) for message in messages] - async def get_conversations(self, user_id, limit=None, sort_order='DESC', offset=0): + + async def get_conversations(self, user_id, limit=None, sort_order="DESC", offset=0): try: offset = int(offset) # Ensure offset is an integer except ValueError: @@ -59,7 +92,7 @@ async def get_conversations(self, user_id, limit=None, sort_order='DESC', offset query = f""" SELECT * FROM conversations WHERE user_id = $1 AND type = 'conversation' - ORDER BY updated_at {sort_order} + ORDER BY "updatedAt" {sort_order} """ # Append LIMIT and OFFSET to the query if limit is specified if limit is not None: @@ -74,33 +107,46 @@ async def get_conversations(self, user_id, limit=None, sort_order='DESC', offset # Fetch records without LIMIT and OFFSET conversations = await self.conn.fetch(query, user_id) return [dict(conversation) for conversation in conversations] + async def get_conversation(self, user_id, conversation_id): query = "SELECT * FROM conversations WHERE id = $1 AND user_id = $2 AND type = 'conversation'" conversation = await self.conn.fetchrow(query, conversation_id, user_id) return dict(conversation) if conversation else None + async def create_message(self, uuid, conversation_id, user_id, input_message: dict): message_id = uuid utc_now = datetime.now(timezone.utc) - created_at = utc_now.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' + createdAt = utc_now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" query = """ - INSERT INTO messages (id, type, created_at, updated_at, user_id, conversation_id, role, content, feedback) + INSERT INTO messages (id, type, "createdAt", "updatedAt", user_id, conversation_id, role, content, feedback) VALUES ($1, 'message', $2, $2, $3, $4, $5, $6, $7) RETURNING * """ - feedback = '' if self.enable_message_feedback else None - message = await self.conn.fetchrow(query, message_id, created_at, user_id, conversation_id, input_message['role'], input_message['content'], feedback) - + feedback = "" if self.enable_message_feedback else None + message = await self.conn.fetchrow( + query, + message_id, + createdAt, + user_id, + conversation_id, + input_message["role"], + input_message["content"], + feedback, + ) + if message: - update_query = "UPDATE conversations SET updated_at = $1 WHERE id = $2 AND user_id = $3 RETURNING *" - await self.conn.execute(update_query, created_at, conversation_id, user_id) + update_query = 'UPDATE conversations SET "updatedAt" = $1 WHERE id = $2 AND user_id = $3 RETURNING *' + await self.conn.execute(update_query, createdAt, conversation_id, user_id) return dict(message) else: return False + async def update_message_feedback(self, user_id, message_id, feedback): query = "UPDATE messages SET feedback = $1 WHERE id = $2 AND user_id = $3 RETURNING *" message = await self.conn.fetchrow(query, feedback, message_id, user_id) return dict(message) if message else False + async def get_messages(self, user_id, conversation_id): - query = "SELECT * FROM messages WHERE conversation_id = $1 AND user_id = $2 ORDER BY created_at ASC" + query = 'SELECT * FROM messages WHERE conversation_id = $1 AND user_id = $2 ORDER BY "createdAt" ASC' messages = await self.conn.fetch(query, conversation_id, user_id) - return [dict(message) for message in messages] \ No newline at end of file + return [dict(message) for message in messages] From 3a60e6fe53184b2a8f32c73060aadff0a1e04b05 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 15 Nov 2024 14:30:21 +0530 Subject: [PATCH 015/107] Error handling and close conversation --- code/backend/api/chat_history.py | 412 +++++++++++++++++-------------- 1 file changed, 223 insertions(+), 189 deletions(-) diff --git a/code/backend/api/chat_history.py b/code/backend/api/chat_history.py index 79c572d0b..3b1010de4 100644 --- a/code/backend/api/chat_history.py +++ b/code/backend/api/chat_history.py @@ -52,7 +52,7 @@ def init_openai_client(): async def list_conversations(): config = ConfigHelper.get_active_config_or_default() if not config.enable_chat_history: - return (jsonify({"error": "Chat history is not avaliable"}), 400) + return jsonify({"error": "Chat history is not available"}), 400 try: offset = request.args.get("offset", 0) @@ -62,31 +62,35 @@ async def list_conversations(): user_id = authenticated_user["user_principal_id"] conversation_client = init_database_client() if not conversation_client: - return (jsonify({"error": "database not available"}), 500) - await conversation_client.connect() + return jsonify({"error": "Database not available"}), 500 - # get the conversations from cosmos - conversations = await conversation_client.get_conversations( - user_id, offset=offset, limit=25 - ) - if not isinstance(conversations, list): - return ( - jsonify({"error": f"No conversations for {user_id} were found"}), - 400, + await conversation_client.connect() + try: + # Fetch conversations + conversations = await conversation_client.get_conversations( + user_id, offset=offset, limit=25 ) - await conversation_client.close() - return (jsonify(conversations), 200) + if not isinstance(conversations, list): + return ( + jsonify({"error": f"No conversations found for user {user_id}"}), + 404, + ) + + return jsonify(conversations), 200 + finally: + await conversation_client.close() except Exception as e: - logger.exception("Exception in /list" + str(e)) - return (jsonify({"error": "Error While listing historical conversations"}), 500) + logger.exception(f"Exception in /history/list: {e}") + return jsonify({"error": "Error while listing historical conversations"}), 500 @bp_chat_history_response.route("/history/rename", methods=["POST"]) async def rename_conversation(): config = ConfigHelper.get_active_config_or_default() if not config.enable_chat_history: - return (jsonify({"error": "Chat history is not avaliable"}), 400) + return jsonify({"error": "Chat history is not available"}), 400 + try: authenticated_user = get_authenticated_user_details( request_headers=request.headers @@ -100,47 +104,51 @@ async def rename_conversation(): if not conversation_id: return (jsonify({"error": "conversation_id is required"}), 400) - # make sure cosmos is configured + title = request_json.get("title", None) + if not title or title.strip() == "": + return jsonify({"error": "A non-empty title is required"}), 400 + + # Initialize and connect to the database client conversation_client = init_database_client() if not conversation_client: - return (jsonify({"error": "database not available"}), 500) - await conversation_client.connect() + return jsonify({"error": "Database not available"}), 500 - # get the conversation from cosmos - conversation = await conversation_client.get_conversation( - user_id, conversation_id - ) - if not conversation: - return ( - jsonify( - { - "error": f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it." - } - ), - 400, + await conversation_client.connect() + try: + # Retrieve conversation from database + conversation = await conversation_client.get_conversation( + user_id, conversation_id ) + if not conversation: + return ( + jsonify( + { + "error": f"Conversation {conversation_id} was not found. " + "It may not exist or the user may not have access." + } + ), + 404, + ) - # update the title - title = request_json.get("title", None) - if not title or title.strip() == "": - return jsonify({"error": "title is required"}), 400 - conversation["title"] = title - updated_conversation = await conversation_client.upsert_conversation( - conversation - ) - await conversation_client.close() - return (jsonify(updated_conversation), 200) + # Update the title and save changes + conversation["title"] = title + updated_conversation = await conversation_client.upsert_conversation( + conversation + ) + return jsonify(updated_conversation), 200 + finally: + await conversation_client.close() except Exception as e: - logger.exception("Exception in /rename" + str(e)) - return (jsonify({"error": "Error renaming is fail"}), 500) + logger.exception(f"Exception in /history/rename: {e}") + return jsonify({"error": "Error while renaming conversation"}), 500 @bp_chat_history_response.route("/history/read", methods=["POST"]) async def get_conversation(): config = ConfigHelper.get_active_config_or_default() if not config.enable_chat_history: - return (jsonify({"error": "Chat history is not avaliable"}), 400) + return jsonify({"error": "Chat history is not available"}), 400 try: authenticated_user = get_authenticated_user_details( @@ -151,66 +159,66 @@ async def get_conversation(): # check request for conversation_id request_json = request.get_json() conversation_id = request_json.get("conversation_id", None) - if not conversation_id: - return (jsonify({"error": "conversation_id is required"}), 400) + return jsonify({"error": "conversation_id is required"}), 400 - # make sure cosmos is configured + # Initialize and connect to the database client conversation_client = init_database_client() if not conversation_client: - return (jsonify({"error": "database not available"}), 500) - await conversation_client.connect() + return jsonify({"error": "Database not available"}), 500 - # get the conversation object and the related messages from cosmos - conversation = await conversation_client.get_conversation( - user_id, conversation_id - ) - # return the conversation id and the messages in the bot frontend format - if not conversation: - return ( - jsonify( - { - "error": f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it." - } - ), - 400, + await conversation_client.connect() + try: + # Retrieve conversation + conversation = await conversation_client.get_conversation( + user_id, conversation_id ) + if not conversation: + return ( + jsonify( + { + "error": f"Conversation {conversation_id} was not found or the user lacks access." + } + ), + 404, + ) - # get the messages for the conversation from cosmos - conversation_messages = await conversation_client.get_messages( - user_id, conversation_id - ) + # Fetch conversation messages + conversation_messages = await conversation_client.get_messages( + user_id, conversation_id + ) + messages = [ + { + "id": msg["id"], + "role": msg["role"], + "content": msg["content"], + "createdAt": msg["createdAt"], + "feedback": msg.get("feedback"), + } + for msg in conversation_messages + ] - # format the messages in the bot frontend format - messages = [ - { - "id": msg["id"], - "role": msg["role"], - "content": msg["content"], - "createdAt": msg["createdAt"], - "feedback": msg.get("feedback"), - } - for msg in conversation_messages - ] + # Return formatted conversation and messages + return ( + jsonify({"conversation_id": conversation_id, "messages": messages}), + 200, + ) + finally: + await conversation_client.close() - await conversation_client.close() - return ( - jsonify({"conversation_id": conversation_id, "messages": messages}), - 200, - ) except Exception as e: - logger.exception("Exception in /read" + str(e)) - return (jsonify({"error": "Error while fetching history conversation"}), 500) + logger.exception(f"Exception in /history/read: {e}") + return jsonify({"error": "Error while fetching conversation history"}), 500 @bp_chat_history_response.route("/history/delete", methods=["DELETE"]) async def delete_conversation(): config = ConfigHelper.get_active_config_or_default() if not config.enable_chat_history: - return (jsonify({"error": "Chat history is not avaliable"}), 400) + return jsonify({"error": "Chat history is not available"}), 400 try: - # get the user id from the request headers + # Get the user ID from the request headers authenticated_user = get_authenticated_user_details( request_headers=request.headers ) @@ -219,161 +227,171 @@ async def delete_conversation(): request_json = request.get_json() conversation_id = request_json.get("conversation_id", None) if not conversation_id: - return ( - jsonify( - { - "error": f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it." - } - ), - 400, - ) + return jsonify({"error": "conversation_id is required"}), 400 + # Initialize and connect to the database client conversation_client = init_database_client() if not conversation_client: - return (jsonify({"error": "database not available"}), 500) + return jsonify({"error": "Database not available"}), 500 + await conversation_client.connect() + try: + # Delete conversation messages from database + await conversation_client.delete_messages(conversation_id, user_id) - # delete the conversation messages from cosmos first - await conversation_client.delete_messages(conversation_id, user_id) + # Delete the conversation itself + await conversation_client.delete_conversation(user_id, conversation_id) - # Now delete the conversation - await conversation_client.delete_conversation(user_id, conversation_id) + return ( + jsonify( + { + "message": "Successfully deleted conversation and messages", + "conversation_id": conversation_id, + } + ), + 200, + ) + finally: + await conversation_client.close() - await conversation_client.close() - return ( - jsonify( - { - "message": "Successfully deleted conversation and messages", - "conversation_id": conversation_id, - } - ), - 200, - ) except Exception as e: - logger.exception("Exception in /delete" + str(e)) - return (jsonify({"error": "Error while deleting history conversation"}), 500) + logger.exception(f"Exception in /history/delete: {e}") + return jsonify({"error": "Error while deleting conversation history"}), 500 @bp_chat_history_response.route("/history/delete_all", methods=["DELETE"]) async def delete_all_conversations(): config = ConfigHelper.get_active_config_or_default() + + # Check if chat history is available if not config.enable_chat_history: - return (jsonify({"error": "Chat history is not avaliable"}), 400) + return jsonify({"error": "Chat history is not available"}), 400 try: - # get the user id from the request headers + # Get the user ID from the request headers (ensure authentication is successful) authenticated_user = get_authenticated_user_details( request_headers=request.headers ) user_id = authenticated_user["user_principal_id"] - - # get conversations for user - # make sure cosmos is configured + # Initialize the database client conversation_client = init_database_client() if not conversation_client: - return (jsonify({"error": "database not available"}), 500) - conversation_client.connect() + return jsonify({"error": "Database not available"}), 500 - conversations = await conversation_client.get_conversations( - user_id, offset=0, limit=None - ) - if not conversations: - return ( - jsonify({"error": f"No conversations for {user_id} were found"}), - 400, + await conversation_client.connect() + try: + # Get all conversations for the user + conversations = await conversation_client.get_conversations( + user_id, offset=0, limit=None ) + if not conversations: + return ( + jsonify({"error": f"No conversations found for user {user_id}"}), + 400, + ) - # delete each conversation - for conversation in conversations: - # delete the conversation messages from cosmos first - await conversation_client.delete_messages(conversation["id"], user_id) + # Delete each conversation and its associated messages + for conversation in conversations: + try: + # Delete messages associated with the conversation + await conversation_client.delete_messages( + conversation["id"], user_id + ) + + # Delete the conversation itself + await conversation_client.delete_conversation( + user_id, conversation["id"] + ) + + except Exception as e: + # Log and continue with the next conversation if one fails + logger.exception( + f"Error deleting conversation {conversation['id']} for user {user_id}: {e}" + ) + continue + return ( + jsonify( + { + "message": f"Successfully deleted all conversations and messages for user {user_id}" + } + ), + 200, + ) - # Now delete the conversation - await conversation_client.delete_conversation(user_id, conversation["id"]) - - await conversation_client.close() - return ( - jsonify( - { - "message": f"Successfully deleted all conversation and messages for user {user_id} " - } - ), - 200, - ) + finally: + await conversation_client.close() except Exception as e: - logger.exception("Exception in /delete" + str(e)) - return ( - jsonify({"error": "Error while deleting all history conversation"}), - 500, - ) + logger.exception(f"Exception in /history/delete_all: {e}") + return jsonify({"error": "Error while deleting all conversation history"}), 500 @bp_chat_history_response.route("/history/update", methods=["POST"]) async def update_conversation(): config = ConfigHelper.get_active_config_or_default() if not config.enable_chat_history: - return (jsonify({"error": "Chat history is not avaliable"}), 400) + return jsonify({"error": "Chat history is not available"}), 400 - authenticated_user = get_authenticated_user_details(request_headers=request.headers) - user_id = authenticated_user["user_principal_id"] try: - # check request for conversation_id + # Get user details from request headers + authenticated_user = get_authenticated_user_details( + request_headers=request.headers + ) + user_id = authenticated_user["user_principal_id"] request_json = request.get_json() conversation_id = request_json.get("conversation_id", None) if not conversation_id: - return (jsonify({"error": "conversation_id is required"}), 400) + return jsonify({"error": "conversation_id is required"}), 400 + + messages = request_json["messages"] + if not messages or len(messages) == 0: + return jsonify({"error": "Messages are required"}), 400 - # make sure cosmos is configured + # Initialize conversation client conversation_client = init_database_client() if not conversation_client: - return jsonify({"error": "database not available"}), 500 + return jsonify({"error": "Database not available"}), 500 await conversation_client.connect() - # check for the conversation_id, if the conversation is not set, we will create a new one + # Get or create the conversation conversation = await conversation_client.get_conversation( user_id, conversation_id ) if not conversation: - title = await generate_title(request_json["messages"]) + title = await generate_title(messages) conversation = await conversation_client.create_conversation( user_id=user_id, conversation_id=conversation_id, title=title ) - conversation_id = conversation["id"] - # Format the incoming message object in the "chat/completions" messages format then write it to the - # conversation history in cosmos - messages = request_json["messages"] - if len(messages) > 0 and messages[0]["role"] == "user": + # Process and save user and assistant messages + # Process user message + if messages[0]["role"] == "user": user_message = next( - ( - message - for message in reversed(messages) - if message["role"] == "user" - ), - None, + (msg for msg in reversed(messages) if msg["role"] == "user"), None ) - createdMessageValue = await conversation_client.create_message( + if not user_message: + return jsonify({"error": "User message not found"}), 400 + + created_message = await conversation_client.create_message( uuid=str(uuid4()), conversation_id=conversation_id, user_id=user_id, input_message=user_message, ) - if createdMessageValue == "Conversation not found": - return (jsonify({"error": "Conversation not found"}), 400) - else: - return (jsonify({"error": "User not found"}), 400) + if created_message == "Conversation not found": + return jsonify({"error": "Conversation not found"}), 400 - if len(messages) > 0 and messages[-1]["role"] == "assistant": - if len(messages) > 1 and messages[-2].get("role", None) == "tool": - # write the tool message first + # Process assistant and tool messages if available + if messages[-1]["role"] == "assistant": + if len(messages) > 1 and messages[-2].get("role") == "tool": + # Write the tool message first if it exists await conversation_client.create_message( uuid=str(uuid4()), conversation_id=conversation_id, user_id=user_id, input_message=messages[-2], ) - # write the assistant message + # Write the assistant message await conversation_client.create_message( uuid=str(uuid4()), conversation_id=conversation_id, @@ -381,7 +399,7 @@ async def update_conversation(): input_message=messages[-1], ) else: - return (jsonify({"error": "no conversationbot"}), 400) + return jsonify({"error": "No assistant message found"}), 400 await conversation_client.close() return ( @@ -399,29 +417,36 @@ async def update_conversation(): ) except Exception as e: - logger.exception("Exception in /update" + str(e)) - return (jsonify({"error": "Error while update the history conversation"}), 500) + logger.exception(f"Exception in /history/update: {e}") + return jsonify({"error": "Error while updating the conversation history"}), 500 @bp_chat_history_response.route("/history/frontend_settings", methods=["GET"]) def get_frontend_settings(): try: + # Clear the cache for the config helper method ConfigHelper.get_active_config_or_default.cache_clear() + + # Retrieve active config config = ConfigHelper.get_active_config_or_default() - chat_history_enabled = ( - config.enable_chat_history.lower() == "true" - if isinstance(config.enable_chat_history, str) - else config.enable_chat_history - ) + + # Ensure `enable_chat_history` is processed correctly + if isinstance(config.enable_chat_history, str): + chat_history_enabled = config.enable_chat_history.strip().lower() == "true" + else: + chat_history_enabled = bool(config.enable_chat_history) + return jsonify({"CHAT_HISTORY_ENABLED": chat_history_enabled}), 200 + except Exception as e: - logger.exception("Exception in /frontend_settings" + str(e)) - return (jsonify({"error": "Error while getting frontend settings"}), 500) + logger.exception(f"Exception in /history/frontend_settings: {e}") + return jsonify({"error": "Error while getting frontend settings"}), 500 async def generate_title(conversation_messages): title_prompt = "Summarize the conversation so far into a 4-word or less title. Do not use any quotation marks or punctuation. Do not include any other commentary or description." + # Filter only the user messages, but consider including system or assistant context if necessary messages = [ {"role": msg["role"], "content": msg["content"]} for msg in conversation_messages @@ -431,6 +456,8 @@ async def generate_title(conversation_messages): try: azure_openai_client = init_openai_client() + + # Create a chat completion with the Azure OpenAI client response = await azure_openai_client.chat.completions.create( model=env_helper.AZURE_OPENAI_MODEL, messages=messages, @@ -438,7 +465,14 @@ async def generate_title(conversation_messages): max_tokens=64, ) - title = response.choices[0].message.content - return title - except Exception: - return messages[-2]["content"] + # Ensure response contains valid choices and content + if response and response.choices and len(response.choices) > 0: + title = response.choices[0].message.content.strip() + return title + else: + raise ValueError("No valid choices in response") + + except Exception as e: + logger.exception(f"Error generating title: {str(e)}") + # Fallback: return the content of the second to last message if something goes wrong + return messages[-2]["content"] if len(messages) > 1 else "Untitled" From 1012e820467b6dc486895fc4fc00042e6f7b3699 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 15 Nov 2024 17:56:34 +0530 Subject: [PATCH 016/107] alignment fix --- code/backend/api/chat_history.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/code/backend/api/chat_history.py b/code/backend/api/chat_history.py index 3b1010de4..873466b52 100644 --- a/code/backend/api/chat_history.py +++ b/code/backend/api/chat_history.py @@ -72,8 +72,8 @@ async def list_conversations(): ) if not isinstance(conversations, list): return ( - jsonify({"error": f"No conversations found for user {user_id}"}), - 404, + jsonify({"error": f"No conversations for {user_id} were found"}), + 400, ) return jsonify(conversations), 200 @@ -123,11 +123,10 @@ async def rename_conversation(): return ( jsonify( { - "error": f"Conversation {conversation_id} was not found. " - "It may not exist or the user may not have access." + "error": f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it." } ), - 404, + 400, ) # Update the title and save changes @@ -177,10 +176,10 @@ async def get_conversation(): return ( jsonify( { - "error": f"Conversation {conversation_id} was not found or the user lacks access." + "error": f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it." } ), - 404, + 400, ) # Fetch conversation messages @@ -227,7 +226,14 @@ async def delete_conversation(): request_json = request.get_json() conversation_id = request_json.get("conversation_id", None) if not conversation_id: - return jsonify({"error": "conversation_id is required"}), 400 + return ( + jsonify( + { + "error": f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it." + } + ), + 400, + ) # Initialize and connect to the database client conversation_client = init_database_client() From bde750acba275ab0f9f4c3207179fc30b188b3ee Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 15 Nov 2024 18:30:51 +0530 Subject: [PATCH 017/107] Set Default Configuration to CosmosDB --- .../batch/utilities/helpers/env_helper.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index 86b847558..2654270a5 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -208,11 +208,15 @@ def __load_config(self, **kwargs) -> None: azure_blob_storage_info = self.get_info_from_env("AZURE_BLOB_STORAGE_INFO", "") if azure_blob_storage_info: # If AZURE_BLOB_STORAGE_INFO exists - self.AZURE_BLOB_ACCOUNT_NAME = azure_blob_storage_info.get("accountName", "") + self.AZURE_BLOB_ACCOUNT_NAME = azure_blob_storage_info.get( + "accountName", "" + ) self.AZURE_BLOB_ACCOUNT_KEY = self.secretHelper.get_secret_from_json( azure_blob_storage_info.get("accountKey", "") ) - self.AZURE_BLOB_CONTAINER_NAME = azure_blob_storage_info.get("containerName", "") + self.AZURE_BLOB_CONTAINER_NAME = azure_blob_storage_info.get( + "containerName", "" + ) else: # Otherwise, fallback to individual environment variables self.AZURE_BLOB_ACCOUNT_NAME = os.getenv("AZURE_BLOB_ACCOUNT_NAME", "") @@ -226,10 +230,14 @@ def __load_config(self, **kwargs) -> None: ) # Azure Form Recognizer - azure_form_recognizer_info = self.get_info_from_env("AZURE_FORM_RECOGNIZER_INFO", "") + azure_form_recognizer_info = self.get_info_from_env( + "AZURE_FORM_RECOGNIZER_INFO", "" + ) if azure_form_recognizer_info: # If AZURE_FORM_RECOGNIZER_INFO exists - self.AZURE_FORM_RECOGNIZER_ENDPOINT = azure_form_recognizer_info.get("endpoint", "") + self.AZURE_FORM_RECOGNIZER_ENDPOINT = azure_form_recognizer_info.get( + "endpoint", "" + ) self.AZURE_FORM_RECOGNIZER_KEY = self.secretHelper.get_secret_from_json( azure_form_recognizer_info.get("key", "") ) @@ -287,16 +295,24 @@ def __load_config(self, **kwargs) -> None: # Chat History DB Integration Settings # Set default values based on DATABASE_TYPE - self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "CosmosDB") - self.CHAT_HISTORY_ENABLED = self.get_env_var_bool("CHAT_HISTORY_ENABLED", "true") + self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "").strip() or "CosmosDB" + self.CHAT_HISTORY_ENABLED = self.get_env_var_bool( + "CHAT_HISTORY_ENABLED", "true" + ) # Cosmos DB configuration if self.DATABASE_TYPE == "CosmosDB": azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "") self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "") - self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get("containerName", "") - self.AZURE_COSMOSDB_ACCOUNT_KEY = self.secretHelper.get_secret("AZURE_COSMOSDB_ACCOUNT_KEY") - self.AZURE_COSMOSDB_ENABLE_FEEDBACK = (os.getenv("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true") + self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get( + "containerName", "" + ) + self.AZURE_COSMOSDB_ACCOUNT_KEY = self.secretHelper.get_secret( + "AZURE_COSMOSDB_ACCOUNT_KEY" + ) + self.AZURE_COSMOSDB_ENABLE_FEEDBACK = ( + os.getenv("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true" + ) # PostgreSQL configuration elif self.DATABASE_TYPE == "PostgreSQL": azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") @@ -304,7 +320,9 @@ def __load_config(self, **kwargs) -> None: self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "") self.POSTGRESQL_HOST = azure_postgresql_info.get("host", "") else: - raise ValueError("Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'.") + raise ValueError( + "Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'." + ) def is_chat_model(self): if "gpt-4" in self.AZURE_OPENAI_MODEL_NAME.lower(): From ed1327e4fbf125436814c2571e2bcccf00e7e2b7 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Sun, 17 Nov 2024 19:20:01 -0500 Subject: [PATCH 018/107] postgress table scripts --- scripts/data_scripts/create_tables.py | 355 ++++++++++++++++++++++++++ scripts/data_scripts/load_vectors.py | 231 +++++++++++++++++ 2 files changed, 586 insertions(+) create mode 100644 scripts/data_scripts/create_tables.py create mode 100644 scripts/data_scripts/load_vectors.py diff --git a/scripts/data_scripts/create_tables.py b/scripts/data_scripts/create_tables.py new file mode 100644 index 000000000..eb50ca6b6 --- /dev/null +++ b/scripts/data_scripts/create_tables.py @@ -0,0 +1,355 @@ +key_vault_name = 'kv_to-be-replaced' + +import pandas as pd +# import pymssql +import os +from datetime import datetime + +import urllib.parse +import psycopg2 + +from azure.keyvault.secrets import SecretClient +from azure.identity import DefaultAzureCredential + +def get_secrets_from_kv(kv_name, secret_name): + key_vault_name = kv_name # Set the name of the Azure Key Vault + credential = DefaultAzureCredential() + 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 + +server = get_secrets_from_kv(key_vault_name,"POSTGRESQL-SERVER") +database = get_secrets_from_kv(key_vault_name,"POSTGRESQL-DATABASENAME") +username = get_secrets_from_kv(key_vault_name,"POSTGRESQL-USER") +password = get_secrets_from_kv(key_vault_name,"POSTGRESQL-PASSWORD") +sslmode = 'require' + +# Construct connection URI +db_uri = f"postgresql://{username}:{password}@{server}/{database}?sslmode={sslmode}" +# conn = pymssql.connect(server, username, password, database) + +conn = psycopg2.connect(db_uri) +print("Connection established") + +cursor = conn.cursor() + +from azure.storage.filedatalake import ( + DataLakeServiceClient +) + +account_name = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-NAME") +account_key = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-KEY") + +account_url = f"https://{account_name}.dfs.core.windows.net" + +service_client = DataLakeServiceClient(account_url, credential=account_key,api_version='2023-01-03') + +file_system_client_name = "data" +directory = 'clientdata' + +file_system_client = service_client.get_file_system_client(file_system_client_name) +directory_name = directory + +cursor = conn.cursor() + +cursor.execute('DROP TABLE IF EXISTS Clients') +conn.commit() + +create_client_sql = """CREATE TABLE Clients ( + ClientId int NOT NULL PRIMARY KEY, + Client varchar(255), + Email varchar(255), + Occupation varchar(255), + MaritalStatus varchar(255), + Dependents int + );""" +cursor.execute(create_client_sql) +conn.commit() + +# Read the CSV file into a Pandas DataFrame +file_path = directory + '/Clients.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +for index, item in df.iterrows(): + cursor.execute(f"INSERT INTO Clients (ClientId,Client, Email, Occupation, MaritalStatus, Dependents) VALUES (%s,%s,%s,%s,%s,%s)", (item.ClientId, item.Client, item.Email, item.Occupation, item.MaritalStatus, item.Dependents)) +conn.commit() + + +cursor = conn.cursor() + +# #ClientInvestmentPortfolio +# cursor.execute('DROP TABLE IF EXISTS ClientInvestmentPortfolio') +# conn.commit() + +# create_client_sql = """CREATE TABLE ClientInvestmentPortfolio ( +# ClientId int, +# AssetDate date, +# AssetType varchar(255), +# Investment float, +# ROI float, +# RevenueWithoutStrategy float +# );""" + +# cursor.execute(create_client_sql) +# conn.commit() + + +# file_path = directory + '/ClientInvestmentPortfolio.csv' +# file_client = file_system_client.get_file_client(file_path) +# csv_file = file_client.download_file() +# df = pd.read_csv(csv_file, encoding='utf-8') + +# for index, item in df.iterrows(): +# cursor.execute(f"INSERT INTO ClientInvestmentPortfolio (ClientId, AssetDate, AssetType, Investment, ROI, RevenueWithoutStrategy) VALUES (%s,%s, %s,%s, %s, %s)", (item.ClientId, item.AssetDate, item.AssetType, item.Investment, item.ROI, item.RevenueWithoutStrategy)) + +# conn.commit() + + +from decimal import Decimal + +cursor.execute('DROP TABLE IF EXISTS Assets') +conn.commit() + +create_assets_sql = """CREATE TABLE Assets ( + ClientId int NOT NULL, + AssetDate Date, + Investment Decimal(18,2), + ROI Decimal(18,2), + Revenue Decimal(18,2), + AssetType varchar(255) + );""" + +cursor.execute(create_assets_sql) +conn.commit() + +file_path = directory + '/Assets.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +# # to adjust the dates to current date +df['AssetDate'] = pd.to_datetime(df['AssetDate']) +today = datetime.today() +days_difference = (today - max(df['AssetDate'])).days - 30 +months_difference = int(days_difference/30) +# print(months_difference) +# df['AssetDate'] = df['AssetDate'] + pd.Timedelta(days=days_difference) +df['AssetDate'] = df['AssetDate'] + pd.DateOffset(months=months_difference) + +df['AssetDate'] = pd.to_datetime(df['AssetDate'], format='%m/%d/%Y') # %Y-%m-%d') +df['ClientId'] = df['ClientId'].astype(int) +df['Investment'] = df['Investment'].astype(float) +df['ROI'] = df['ROI'].astype(float) +df['Revenue'] = df['Revenue'].astype(float) + + +for index, item in df.iterrows(): + cursor.execute(f"INSERT INTO Assets (ClientId,AssetDate, Investment, ROI, Revenue, AssetType) VALUES (%s,%s,%s,%s,%s,%s)", (item.ClientId, item.AssetDate, item.Investment, item.ROI, item.Revenue, item.AssetType)) +conn.commit() + + +#InvestmentGoals +cursor.execute('DROP TABLE IF EXISTS InvestmentGoals') +conn.commit() + +create_ig_sql = """CREATE TABLE InvestmentGoals ( + ClientId int NOT NULL, + InvestmentGoal varchar(255) + );""" + +cursor.execute(create_ig_sql) +conn.commit() + +file_path = directory + '/InvestmentGoals.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +df['ClientId'] = df['ClientId'].astype(int) + +for index, item in df.iterrows(): + cursor.execute(f"INSERT INTO InvestmentGoals (ClientId,InvestmentGoal) VALUES (%s,%s)", (item.ClientId, item.InvestmentGoal)) +conn.commit() + + +cursor.execute('DROP TABLE IF EXISTS InvestmentGoalsDetails') +conn.commit() + +create_ig_sql = """CREATE TABLE InvestmentGoalsDetails ( + ClientId int NOT NULL, + InvestmentGoal varchar(255), + TargetAmount Decimal(18,2), + Contribution Decimal(18,2) + );""" + +cursor.execute(create_ig_sql) +conn.commit() + +file_path = directory + '/InvestmentGoalsDetails.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +df['ClientId'] = df['ClientId'].astype(int) + +for index, item in df.iterrows(): + cursor.execute(f"INSERT INTO InvestmentGoalsDetails (ClientId,InvestmentGoal, TargetAmount, Contribution) VALUES (%s,%s,%s,%s)", (item.ClientId, item.InvestmentGoal, item.TargetAmount, item.Contribution)) +conn.commit() + +#ClientSummaries +cursor.execute('DROP TABLE IF EXISTS ClientSummaries') +conn.commit() + +create_cs_sql = """CREATE TABLE ClientSummaries ( + ClientId int NOT NULL, + ClientSummary varchar(255) + );""" + +cursor.execute(create_cs_sql) +conn.commit() + +file_path = directory + '/ClientSummaries.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +df['ClientId'] = df['ClientId'].astype(int) + +for index, item in df.iterrows(): + cursor.execute(f"INSERT INTO ClientSummaries (ClientId,ClientSummary) VALUES (%s,%s)", (item.ClientId, item.ClientSummary)) +conn.commit() + +# Retirement +cursor.execute('DROP TABLE IF EXISTS Retirement') +conn.commit() + +create_cs_sql = """CREATE TABLE Retirement ( + ClientId int NOT NULL, + StatusDate Date, + RetirementGoalProgress Decimal(18,2), + EducationGoalProgress Decimal(18,2) + );""" + +cursor.execute(create_cs_sql) +conn.commit() + + +file_path = directory + '/Retirement.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +df['ClientId'] = df['ClientId'].astype(int) + +# to adjust the dates to current date +df['StatusDate'] = pd.to_datetime(df['StatusDate']) +today = datetime.today() +days_difference = (today - max(df['StatusDate'])).days - 30 +months_difference = int(days_difference/30) +df['StatusDate'] = df['StatusDate'] + pd.DateOffset(months=months_difference) +df['StatusDate'] = pd.to_datetime(df['StatusDate']).dt.date + +for index, item in df.iterrows(): + cursor.execute(f"INSERT INTO Retirement (ClientId,StatusDate, RetirementGoalProgress, EducationGoalProgress) VALUES (%s,%s,%s,%s)", (item.ClientId, item.StatusDate, item.RetirementGoalProgress, item.EducationGoalProgress)) +conn.commit() + + +import pandas as pd +cursor = conn.cursor() + +cursor.execute('DROP TABLE IF EXISTS ClientMeetings') +conn.commit() + +create_cs_sql = """CREATE TABLE ClientMeetings ( + ClientId int NOT NULL, + ConversationId varchar(255), + Title varchar(255), + StartTime DateTime, + EndTime DateTime, + Advisor varchar(255), + ClientEmail varchar(255) + );""" + +cursor.execute(create_cs_sql) +conn.commit() + + +file_path = directory + '/ClientMeetingsMetadata.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +# to adjust the dates to current date +df['StartTime'] = pd.to_datetime(df['StartTime']) +df['EndTime'] = pd.to_datetime(df['EndTime']) +today = datetime.today() +days_difference = (today - min(df['StartTime'])).days - 30 +days_difference + +df['StartTime'] = df['StartTime'] + pd.Timedelta(days=days_difference) +df['EndTime'] = df['EndTime'] + pd.Timedelta(days=days_difference) + +for index, item in df.iterrows(): + + cursor.execute(f"INSERT INTO ClientMeetings (ClientId,ConversationId,Title,StartTime,EndTime,Advisor,ClientEmail) VALUES (%s,%s,%s,%s,%s,%s,%s)", (item.ClientId, item.ConversationId, item.Title, item.StartTime, item.EndTime, item.Advisor, item.ClientEmail)) +conn.commit() + + +file_path = directory + '/ClientFutureMeetings.csv' +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df = pd.read_csv(csv_file, encoding='utf-8') + +# to adjust the dates to current date +df['StartTime'] = pd.to_datetime(df['StartTime']) +df['EndTime'] = pd.to_datetime(df['EndTime']) +today = datetime.today() +days_difference = (today - min(df['StartTime'])).days + 1 +df['StartTime'] = df['StartTime'] + pd.Timedelta(days=days_difference) +df['EndTime'] = df['EndTime'] + pd.Timedelta(days=days_difference) + +df['ClientId'] = df['ClientId'].astype(int) +df['ConversationId'] = '' + +for index, item in df.iterrows(): + cursor.execute(f"INSERT INTO ClientMeetings (ClientId,ConversationId,Title,StartTime,EndTime,Advisor,ClientEmail) VALUES (%s,%s,%s,%s,%s,%s,%s)", (item.ClientId, item.ConversationId, item.Title, item.StartTime, item.EndTime, item.Advisor, item.ClientEmail)) +conn.commit() + + +cursor = conn.cursor() + +cursor.execute('DROP TABLE IF EXISTS conversations') +conn.commit() + +create_cs_sql = """CREATE TABLE conversations ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + created_at TEXT, + updated_at TEXT, + user_id TEXT NOT NULL, + title TEXT + );""" + +cursor.execute(create_cs_sql) +conn.commit() + +cursor = conn.cursor() + +cursor.execute('DROP TABLE IF EXISTS messages') +conn.commit() + +create_cs_sql = """CREATE TABLE messages ( + id TEXT PRIMARY KEY, + type VARCHAR(50) NOT NULL, + created_at TEXT, + updated_at TEXT, + user_id TEXT NOT NULL, + conversation_id TEXT NOT NULL, + role VARCHAR(50), + content TEXT NOT NULL, + feedback TEXT + );""" + +cursor.execute(create_cs_sql) +conn.commit() diff --git a/scripts/data_scripts/load_vectors.py b/scripts/data_scripts/load_vectors.py new file mode 100644 index 000000000..5e2f33e6e --- /dev/null +++ b/scripts/data_scripts/load_vectors.py @@ -0,0 +1,231 @@ +import openai +import os +import pandas as pd +import numpy as np +import json +# import tiktoken +import psycopg2 +import ast +import pgvector +import math +from psycopg2.extras import execute_values +from pgvector.psycopg2 import register_vector + +#TODO may this file is not needed for CWYD postgress Integration + +#Get Azure Key Vault Client +key_vault_name = 'kv_to-be-replaced' + +index_name = "transcripts_index" + +file_system_client_name = "data" +directory = 'clienttranscripts/meeting_transcripts' +csv_file_name = 'clienttranscripts/meeting_transcripts_metadata/transcripts_metadata.csv' + +from azure.keyvault.secrets import SecretClient +from azure.identity import DefaultAzureCredential + +def get_secrets_from_kv(kv_name, secret_name): + + # Set the name of the Azure Key Vault + key_vault_name = kv_name + credential = DefaultAzureCredential() + + # 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) + + # Retrieve the secret value + return(secret_client.get_secret(secret_name).value) + +# openai_api_type = get_secrets_from_kv(key_vault_name,"OPENAI-API-TYPE") +openai_api_key = get_secrets_from_kv(key_vault_name,"AZURE-OPENAI-KEY") +openai_api_base = get_secrets_from_kv(key_vault_name,"AZURE-OPENAI-ENDPOINT") +openai_api_version = get_secrets_from_kv(key_vault_name,"AZURE-OPENAI-PREVIEW-API-VERSION") + +# Connect to PostgreSQL database using connection string +server = get_secrets_from_kv(key_vault_name,"POSTGRESQL-SERVER") +database = get_secrets_from_kv(key_vault_name,"POSTGRESQL-DATABASENAME") +username = get_secrets_from_kv(key_vault_name,"POSTGRESQL-USER") +password = get_secrets_from_kv(key_vault_name,"POSTGRESQL-PASSWORD") +sslmode = 'require' + +# Construct connection URI +db_uri = f"postgresql://{username}:{password}@{server}/{database}?sslmode={sslmode}" + +conn = psycopg2.connect(db_uri) +cur = conn.cursor() + +#install pgvector +cur = conn.cursor() +cur.execute("CREATE EXTENSION IF NOT EXISTS vector") +conn.commit() + +# Register the vector type with psycopg2 +register_vector(conn) + +cur.execute('DROP TABLE IF EXISTS calltranscripts;') +# Create table to store embeddings and metadata +table_create_command = """ +CREATE TABLE IF NOT EXISTS calltranscripts ( + id text, + chunk_id text, + content text, + sourceurl text, + client_id integer, + contentVector vector(1536) + ); + """ + +cur.execute(table_create_command) +cur.close() +conn.commit() + +from openai import AzureOpenAI + +# Function: Get Embeddings +def get_embeddings(text: str,openai_api_base,openai_api_version,openai_api_key): + model_id = "text-embedding-ada-002" + client = AzureOpenAI( + api_version=openai_api_version, + azure_endpoint=openai_api_base, + api_key = openai_api_key + ) + + embedding = client.embeddings.create(input=text, model=model_id).data[0].embedding + + return embedding + +import re + +def clean_spaces_with_regex(text): + # Use a regular expression to replace multiple spaces with a single space + cleaned_text = re.sub(r'\s+', ' ', text) + # Use a regular expression to replace consecutive dots with a single dot + cleaned_text = re.sub(r'\.{2,}', '.', cleaned_text) + return cleaned_text + +def chunk_data(text): + tokens_per_chunk = 1024 #500 + text = clean_spaces_with_regex(text) + SENTENCE_ENDINGS = [".", "!", "?"] + WORDS_BREAKS = ['\n', '\t', '}', '{', ']', '[', ')', '(', ' ', ':', ';', ','] + + sentences = text.split('. ') # Split text into sentences + chunks = [] + current_chunk = '' + current_chunk_token_count = 0 + + # Iterate through each sentence + for sentence in sentences: + # Split sentence into tokens + tokens = sentence.split() + + # Check if adding the current sentence exceeds tokens_per_chunk + if current_chunk_token_count + len(tokens) <= tokens_per_chunk: + # Add the sentence to the current chunk + if current_chunk: + current_chunk += '. ' + sentence + else: + current_chunk += sentence + current_chunk_token_count += len(tokens) + else: + # Add current chunk to chunks list and start a new chunk + chunks.append(current_chunk) + current_chunk = sentence + current_chunk_token_count = len(tokens) + + # Add the last chunk + if current_chunk: + chunks.append(current_chunk) + + return chunks + +#add documents to the index + +import json +import base64 +import time +import pandas as pd +from azure.search.documents import SearchClient +import os + +# foldername = 'clienttranscripts' +# path_name = f'Data/{foldername}/meeting_transcripts' +# # paths = mssparkutils.fs.ls(path_name) + +# paths = os.listdir(path_name) + +conn = psycopg2.connect(db_uri) +cur = conn.cursor() + +from azure.storage.filedatalake import ( + DataLakeServiceClient, + DataLakeDirectoryClient, + FileSystemClient +) + +file_system_client_name = "data" +directory = 'clienttranscripts/meeting_transcripts' +csv_file_name = 'clienttranscripts/meeting_transcripts_metadata/transcripts_metadata.csv' + +account_name = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-NAME") +account_key = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-KEY") + +account_url = f"https://{account_name}.dfs.core.windows.net" + +service_client = DataLakeServiceClient(account_url, credential=account_key,api_version='2023-01-03') + +file_system_client = service_client.get_file_system_client(file_system_client_name) +directory_name = directory +paths = file_system_client.get_paths(path=directory_name) +# print(paths) + + +import pandas as pd +# Read the CSV file into a Pandas DataFrame +file_path = csv_file_name +# print(file_path) +file_client = file_system_client.get_file_client(file_path) +csv_file = file_client.download_file() +df_metadata = pd.read_csv(csv_file, encoding='utf-8') + +docs = [] +counter = 0 +for path in paths: + file_client = file_system_client.get_file_client(path.name) + data_file = file_client.download_file() + data = json.load(data_file) + text = data['Content'] + + filename = path.name.split('/')[-1] + document_id = filename.replace('.json','').replace('convo_','') + # print(document_id) + df_file_metadata = df_metadata[df_metadata['ConversationId']==str(document_id)].iloc[0] + + chunks = chunk_data(text) + chunk_num = 0 + for chunk in chunks: + chunk_num += 1 + d = { + "chunk_id" : document_id + '_' + str(chunk_num).zfill(2), + "client_id": str(df_file_metadata['ClientId']), + "content": 'ClientId is ' + str(df_file_metadata['ClientId']) + ' . ' + chunk, + } + + counter += 1 + + try: + v_contentVector = get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) + except: + time.sleep(30) + v_contentVector = get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) + + + id = base64.urlsafe_b64encode(bytes(d["chunk_id"], encoding='utf-8')).decode('utf-8') + + cur.execute(f"INSERT INTO calltranscripts (id,chunk_id, client_id, content, sourceurl, contentVector) VALUES (%s,%s,%s,%s,%s,%s)", (id, d["chunk_id"], d["client_id"], d["content"], path.name.split('/')[-1], v_contentVector)) + #break + # break + +cur.close() +conn.commit() \ No newline at end of file From 05f386c2bf6a4844cfd492373c4ddf8d4bf0ca92 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Sun, 17 Nov 2024 19:21:18 -0500 Subject: [PATCH 019/107] Update create_tables.py --- scripts/data_scripts/create_tables.py | 269 +------------------------- 1 file changed, 2 insertions(+), 267 deletions(-) diff --git a/scripts/data_scripts/create_tables.py b/scripts/data_scripts/create_tables.py index eb50ca6b6..176b75da8 100644 --- a/scripts/data_scripts/create_tables.py +++ b/scripts/data_scripts/create_tables.py @@ -17,6 +17,7 @@ def get_secrets_from_kv(kv_name, secret_name): 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 +#TODO change connectivity with SFI server = get_secrets_from_kv(key_vault_name,"POSTGRESQL-SERVER") database = get_secrets_from_kv(key_vault_name,"POSTGRESQL-DATABASENAME") username = get_secrets_from_kv(key_vault_name,"POSTGRESQL-USER") @@ -51,273 +52,7 @@ def get_secrets_from_kv(kv_name, secret_name): cursor = conn.cursor() -cursor.execute('DROP TABLE IF EXISTS Clients') -conn.commit() - -create_client_sql = """CREATE TABLE Clients ( - ClientId int NOT NULL PRIMARY KEY, - Client varchar(255), - Email varchar(255), - Occupation varchar(255), - MaritalStatus varchar(255), - Dependents int - );""" -cursor.execute(create_client_sql) -conn.commit() - -# Read the CSV file into a Pandas DataFrame -file_path = directory + '/Clients.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -for index, item in df.iterrows(): - cursor.execute(f"INSERT INTO Clients (ClientId,Client, Email, Occupation, MaritalStatus, Dependents) VALUES (%s,%s,%s,%s,%s,%s)", (item.ClientId, item.Client, item.Email, item.Occupation, item.MaritalStatus, item.Dependents)) -conn.commit() - - -cursor = conn.cursor() - -# #ClientInvestmentPortfolio -# cursor.execute('DROP TABLE IF EXISTS ClientInvestmentPortfolio') -# conn.commit() - -# create_client_sql = """CREATE TABLE ClientInvestmentPortfolio ( -# ClientId int, -# AssetDate date, -# AssetType varchar(255), -# Investment float, -# ROI float, -# RevenueWithoutStrategy float -# );""" - -# cursor.execute(create_client_sql) -# conn.commit() - - -# file_path = directory + '/ClientInvestmentPortfolio.csv' -# file_client = file_system_client.get_file_client(file_path) -# csv_file = file_client.download_file() -# df = pd.read_csv(csv_file, encoding='utf-8') - -# for index, item in df.iterrows(): -# cursor.execute(f"INSERT INTO ClientInvestmentPortfolio (ClientId, AssetDate, AssetType, Investment, ROI, RevenueWithoutStrategy) VALUES (%s,%s, %s,%s, %s, %s)", (item.ClientId, item.AssetDate, item.AssetType, item.Investment, item.ROI, item.RevenueWithoutStrategy)) - -# conn.commit() - - -from decimal import Decimal - -cursor.execute('DROP TABLE IF EXISTS Assets') -conn.commit() - -create_assets_sql = """CREATE TABLE Assets ( - ClientId int NOT NULL, - AssetDate Date, - Investment Decimal(18,2), - ROI Decimal(18,2), - Revenue Decimal(18,2), - AssetType varchar(255) - );""" - -cursor.execute(create_assets_sql) -conn.commit() - -file_path = directory + '/Assets.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -# # to adjust the dates to current date -df['AssetDate'] = pd.to_datetime(df['AssetDate']) -today = datetime.today() -days_difference = (today - max(df['AssetDate'])).days - 30 -months_difference = int(days_difference/30) -# print(months_difference) -# df['AssetDate'] = df['AssetDate'] + pd.Timedelta(days=days_difference) -df['AssetDate'] = df['AssetDate'] + pd.DateOffset(months=months_difference) - -df['AssetDate'] = pd.to_datetime(df['AssetDate'], format='%m/%d/%Y') # %Y-%m-%d') -df['ClientId'] = df['ClientId'].astype(int) -df['Investment'] = df['Investment'].astype(float) -df['ROI'] = df['ROI'].astype(float) -df['Revenue'] = df['Revenue'].astype(float) - - -for index, item in df.iterrows(): - cursor.execute(f"INSERT INTO Assets (ClientId,AssetDate, Investment, ROI, Revenue, AssetType) VALUES (%s,%s,%s,%s,%s,%s)", (item.ClientId, item.AssetDate, item.Investment, item.ROI, item.Revenue, item.AssetType)) -conn.commit() - - -#InvestmentGoals -cursor.execute('DROP TABLE IF EXISTS InvestmentGoals') -conn.commit() - -create_ig_sql = """CREATE TABLE InvestmentGoals ( - ClientId int NOT NULL, - InvestmentGoal varchar(255) - );""" - -cursor.execute(create_ig_sql) -conn.commit() - -file_path = directory + '/InvestmentGoals.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -df['ClientId'] = df['ClientId'].astype(int) - -for index, item in df.iterrows(): - cursor.execute(f"INSERT INTO InvestmentGoals (ClientId,InvestmentGoal) VALUES (%s,%s)", (item.ClientId, item.InvestmentGoal)) -conn.commit() - - -cursor.execute('DROP TABLE IF EXISTS InvestmentGoalsDetails') -conn.commit() - -create_ig_sql = """CREATE TABLE InvestmentGoalsDetails ( - ClientId int NOT NULL, - InvestmentGoal varchar(255), - TargetAmount Decimal(18,2), - Contribution Decimal(18,2) - );""" - -cursor.execute(create_ig_sql) -conn.commit() - -file_path = directory + '/InvestmentGoalsDetails.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -df['ClientId'] = df['ClientId'].astype(int) - -for index, item in df.iterrows(): - cursor.execute(f"INSERT INTO InvestmentGoalsDetails (ClientId,InvestmentGoal, TargetAmount, Contribution) VALUES (%s,%s,%s,%s)", (item.ClientId, item.InvestmentGoal, item.TargetAmount, item.Contribution)) -conn.commit() - -#ClientSummaries -cursor.execute('DROP TABLE IF EXISTS ClientSummaries') -conn.commit() - -create_cs_sql = """CREATE TABLE ClientSummaries ( - ClientId int NOT NULL, - ClientSummary varchar(255) - );""" - -cursor.execute(create_cs_sql) -conn.commit() - -file_path = directory + '/ClientSummaries.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -df['ClientId'] = df['ClientId'].astype(int) - -for index, item in df.iterrows(): - cursor.execute(f"INSERT INTO ClientSummaries (ClientId,ClientSummary) VALUES (%s,%s)", (item.ClientId, item.ClientSummary)) -conn.commit() - -# Retirement -cursor.execute('DROP TABLE IF EXISTS Retirement') -conn.commit() - -create_cs_sql = """CREATE TABLE Retirement ( - ClientId int NOT NULL, - StatusDate Date, - RetirementGoalProgress Decimal(18,2), - EducationGoalProgress Decimal(18,2) - );""" - -cursor.execute(create_cs_sql) -conn.commit() - - -file_path = directory + '/Retirement.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -df['ClientId'] = df['ClientId'].astype(int) - -# to adjust the dates to current date -df['StatusDate'] = pd.to_datetime(df['StatusDate']) -today = datetime.today() -days_difference = (today - max(df['StatusDate'])).days - 30 -months_difference = int(days_difference/30) -df['StatusDate'] = df['StatusDate'] + pd.DateOffset(months=months_difference) -df['StatusDate'] = pd.to_datetime(df['StatusDate']).dt.date - -for index, item in df.iterrows(): - cursor.execute(f"INSERT INTO Retirement (ClientId,StatusDate, RetirementGoalProgress, EducationGoalProgress) VALUES (%s,%s,%s,%s)", (item.ClientId, item.StatusDate, item.RetirementGoalProgress, item.EducationGoalProgress)) -conn.commit() - - -import pandas as pd -cursor = conn.cursor() - -cursor.execute('DROP TABLE IF EXISTS ClientMeetings') -conn.commit() - -create_cs_sql = """CREATE TABLE ClientMeetings ( - ClientId int NOT NULL, - ConversationId varchar(255), - Title varchar(255), - StartTime DateTime, - EndTime DateTime, - Advisor varchar(255), - ClientEmail varchar(255) - );""" - -cursor.execute(create_cs_sql) -conn.commit() - - -file_path = directory + '/ClientMeetingsMetadata.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -# to adjust the dates to current date -df['StartTime'] = pd.to_datetime(df['StartTime']) -df['EndTime'] = pd.to_datetime(df['EndTime']) -today = datetime.today() -days_difference = (today - min(df['StartTime'])).days - 30 -days_difference - -df['StartTime'] = df['StartTime'] + pd.Timedelta(days=days_difference) -df['EndTime'] = df['EndTime'] + pd.Timedelta(days=days_difference) - -for index, item in df.iterrows(): - - cursor.execute(f"INSERT INTO ClientMeetings (ClientId,ConversationId,Title,StartTime,EndTime,Advisor,ClientEmail) VALUES (%s,%s,%s,%s,%s,%s,%s)", (item.ClientId, item.ConversationId, item.Title, item.StartTime, item.EndTime, item.Advisor, item.ClientEmail)) -conn.commit() - - -file_path = directory + '/ClientFutureMeetings.csv' -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df = pd.read_csv(csv_file, encoding='utf-8') - -# to adjust the dates to current date -df['StartTime'] = pd.to_datetime(df['StartTime']) -df['EndTime'] = pd.to_datetime(df['EndTime']) -today = datetime.today() -days_difference = (today - min(df['StartTime'])).days + 1 -df['StartTime'] = df['StartTime'] + pd.Timedelta(days=days_difference) -df['EndTime'] = df['EndTime'] + pd.Timedelta(days=days_difference) - -df['ClientId'] = df['ClientId'].astype(int) -df['ConversationId'] = '' - -for index, item in df.iterrows(): - cursor.execute(f"INSERT INTO ClientMeetings (ClientId,ConversationId,Title,StartTime,EndTime,Advisor,ClientEmail) VALUES (%s,%s,%s,%s,%s,%s,%s)", (item.ClientId, item.ConversationId, item.Title, item.StartTime, item.EndTime, item.Advisor, item.ClientEmail)) -conn.commit() - - -cursor = conn.cursor() +#TODO CWYD POSTGRES TABLES cursor.execute('DROP TABLE IF EXISTS conversations') conn.commit() From 055cd04da7366d143988bc5b5033e6f54de09bfd Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 19 Nov 2024 18:34:53 +0530 Subject: [PATCH 020/107] Restrict Admin Dashboard Options for PostgreSQL Deployment --- .../batch/utilities/helpers/config/config_helper.py | 9 ++++++++- .../batch/utilities/helpers/config/database_type.py | 6 ++++++ .../batch/utilities/helpers/config/default.json | 3 ++- code/backend/pages/04_Configuration.py | 13 ++++++++++--- 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 code/backend/batch/utilities/helpers/config/database_type.py diff --git a/code/backend/batch/utilities/helpers/config/config_helper.py b/code/backend/batch/utilities/helpers/config/config_helper.py index 05549ac04..3cbd2cb83 100644 --- a/code/backend/batch/utilities/helpers/config/config_helper.py +++ b/code/backend/batch/utilities/helpers/config/config_helper.py @@ -13,6 +13,7 @@ from ..env_helper import EnvHelper from .assistant_strategy import AssistantStrategy from .conversation_flow import ConversationFlow +from .database_type import DatabaseType CONFIG_CONTAINER_NAME = "config" CONFIG_FILE_NAME = "active.json" @@ -52,6 +53,7 @@ def __init__(self, config: dict): self.enable_chat_history = config.get( "enable_chat_history", self.env_helper.CHAT_HISTORY_ENABLED ) + self.database_type = config.get("database_type", self.env_helper.DATABASE_TYPE) def get_available_document_types(self) -> list[str]: document_types = { @@ -245,8 +247,13 @@ def get_default_config(): logger.info("Loading default config from %s", config_file_path) ConfigHelper._default_config = json.loads( Template(f.read()).substitute( - ORCHESTRATION_STRATEGY=env_helper.ORCHESTRATION_STRATEGY, + ORCHESTRATION_STRATEGY=( + OrchestrationStrategy.SEMANTIC_KERNEL.value + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value + else env_helper.ORCHESTRATION_STRATEGY + ), CHAT_HISTORY_ENABLED=env_helper.CHAT_HISTORY_ENABLED, + DATABASE_TYPE=env_helper.DATABASE_TYPE, ) ) if env_helper.USE_ADVANCED_IMAGE_PROCESSING: diff --git a/code/backend/batch/utilities/helpers/config/database_type.py b/code/backend/batch/utilities/helpers/config/database_type.py new file mode 100644 index 000000000..1b914d037 --- /dev/null +++ b/code/backend/batch/utilities/helpers/config/database_type.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class DatabaseType(Enum): + COSMOSDB = "CosmosDB" + POSTGRESQL = "PostgreSQL" diff --git a/code/backend/batch/utilities/helpers/config/default.json b/code/backend/batch/utilities/helpers/config/default.json index be50c1a4c..f5db4359f 100644 --- a/code/backend/batch/utilities/helpers/config/default.json +++ b/code/backend/batch/utilities/helpers/config/default.json @@ -142,5 +142,6 @@ "orchestrator": { "strategy": "${ORCHESTRATION_STRATEGY}" }, - "enable_chat_history": "${CHAT_HISTORY_ENABLED}" + "enable_chat_history": "${CHAT_HISTORY_ENABLED}", + "database_type": "${DATABASE_TYPE}" } diff --git a/code/backend/pages/04_Configuration.py b/code/backend/pages/04_Configuration.py index 1ac80215e..d6ddf93a8 100644 --- a/code/backend/pages/04_Configuration.py +++ b/code/backend/pages/04_Configuration.py @@ -8,6 +8,7 @@ from azure.core.exceptions import ResourceNotFoundError from batch.utilities.helpers.config.assistant_strategy import AssistantStrategy from batch.utilities.helpers.config.conversation_flow import ConversationFlow +from batch.utilities.helpers.config.database_type import DatabaseType sys.path.append(os.path.join(os.path.dirname(__file__), "..")) env_helper: EnvHelper = EnvHelper() @@ -69,13 +70,13 @@ def load_css(file_path): if "conversational_flow" not in st.session_state: st.session_state["conversational_flow"] = config.prompts.conversational_flow if "enable_chat_history" not in st.session_state: - st.session_state["enable_chat_history"] = st.session_state[ - "enable_chat_history" - ] = ( + st.session_state["enable_chat_history"] = ( config.enable_chat_history.lower() == "true" if isinstance(config.enable_chat_history, str) else config.enable_chat_history ) +if "database_type" not in st.session_state: + st.session_state["database_type"] = config.database_type if env_helper.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: if "max_page_length" not in st.session_state: @@ -196,6 +197,11 @@ def validate_documents(): key="conversational_flow", options=config.get_available_conversational_flows(), help=conversational_flow_help, + disabled=( + True + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value + else False + ), ) with st.expander("Orchestrator configuration", expanded=True): @@ -209,6 +215,7 @@ def validate_documents(): True if st.session_state["conversational_flow"] == ConversationFlow.BYOD.value + or env_helper.DATABASE_TYPE == "PostgreSQL" else False ), ) From 73c690c111b5673e4a5eb197e6af19acf49a24d1 Mon Sep 17 00:00:00 2001 From: gpickett <122489228+gpickett@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:36:44 -0800 Subject: [PATCH 021/107] added postgres option --- infra/app/storekeys.bicep | 36 +++++++-- infra/app/web.bicep | 40 ++++++---- infra/core/database/postgresdb.bicep | 109 +++++++++++++++++++++++++++ infra/main.bicep | 87 ++++++++++++++++++--- 4 files changed, 240 insertions(+), 32 deletions(-) create mode 100644 infra/core/database/postgresdb.bicep diff --git a/infra/app/storekeys.bicep b/infra/app/storekeys.bicep index 506087efb..dcf3c7309 100644 --- a/infra/app/storekeys.bicep +++ b/infra/app/storekeys.bicep @@ -7,6 +7,9 @@ param formRecognizerName string = '' param contentSafetyName string = '' param speechServiceName string = '' param computerVisionName string = '' +param postgresServerName string = '' // PostgreSQL server name +param postgresDatabaseName string = 'postgres' // Default database name +param postgresInfoName string = 'AZURE-POSTGRESQL-INFO' // Secret name for PostgreSQL info param storageAccountKeyName string = 'AZURE-STORAGE-ACCOUNT-KEY' param openAIKeyName string = 'AZURE-OPENAI-API-KEY' param searchKeyName string = 'AZURE-SEARCH-KEY' @@ -96,15 +99,33 @@ resource computerVisionKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' } } -// add cosmos db account key -resource cosmosDbAccountKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { +// Add PostgreSQL info in JSON format +resource postgresInfoSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = if (postgresServerName != '') { + parent: keyVault + name: postgresInfoName + properties: { + value: postgresServerName != '' + ? string({ + user: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.DBforPostgreSQL/flexibleServers', postgresServerName), '2022-12-01').username + dbname: postgresDatabaseName + host: '${postgresServerName}.postgres.database.azure.com' + password: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.DBforPostgreSQL/flexibleServers', postgresServerName), '2022-12-01').password + }) + : '' + } +} + +// Conditional CosmosDB key secret +resource cosmosDbAccountKey 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = if (cosmosAccountName != '') { parent: keyVault name: cosmosAccountKeyName properties: { - value: listKeys( - resourceId(subscription().subscriptionId, rgName, 'Microsoft.DocumentDB/databaseAccounts', cosmosAccountName), - '2022-08-15' - ).primaryMasterKey + value: cosmosAccountName != '' + ? listKeys( + resourceId(subscription().subscriptionId, rgName, 'Microsoft.DocumentDB/databaseAccounts', cosmosAccountName), + '2022-08-15' + ).primaryMasterKey + : '' } } @@ -119,4 +140,5 @@ output OPENAI_KEY_NAME string = openAIKeySecret.name output STORAGE_ACCOUNT_KEY_NAME string = storageAccountKeySecret.name output SPEECH_KEY_NAME string = speechKeySecret.name output COMPUTER_VISION_KEY_NAME string = computerVisionName != '' ? computerVisionKeySecret.name : '' -output COSMOS_ACCOUNT_KEY_NAME string = cosmosDbAccountKey.name +output COSMOS_ACCOUNT_KEY_NAME string = cosmosAccountName != '' ? cosmosDbAccountKey.name : '' +output POSTGRESQL_INFO_NAME string = postgresServerName != '' ? postgresInfoSecret.name : '' diff --git a/infra/app/web.bicep b/infra/app/web.bicep index 526bd513c..2a0da1a0c 100644 --- a/infra/app/web.bicep +++ b/infra/app/web.bicep @@ -29,7 +29,11 @@ param authType string param dockerFullImageName string = '' param useDocker bool = dockerFullImageName != '' param healthCheckPath string = '' + +// Database parameters +param databaseType string = 'cosmos' // 'cosmos' or 'postgres' param cosmosDBKeyName string = '' +param postgresInfoName string = '' var azureFormRecognizerInfoUpdated = useKeyVault ? azureFormRecognizerInfo @@ -55,6 +59,25 @@ var azureBlobStorageInfoUpdated = useKeyVault '2021-09-01' ).keys[0].value) +// Database-specific settings +var databaseSettings = databaseType == 'cosmos' ? { + DATABASE_TYPE: 'cosmos' + AZURE_COSMOSDB_ACCOUNT_KEY: (useKeyVault || cosmosDBKeyName == '') + ? cosmosDBKeyName + : listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.DocumentDB/databaseAccounts', + cosmosDBKeyName + ), + '2022-08-15' + ).primaryMasterKey +} : { + DATABASE_TYPE: 'postgres' + AZURE_POSTGRESQL_INFO: useKeyVault ? postgresInfoName : '' +} + module web '../core/host/appservice.bicep' = { name: '${name}-app-module' params: { @@ -65,7 +88,7 @@ module web '../core/host/appservice.bicep' = { appCommandLine: useDocker ? '' : appCommandLine applicationInsightsName: applicationInsightsName appServicePlanId: appServicePlanId - appSettings: union(appSettings, { + appSettings: union(appSettings, union(databaseSettings, { AZURE_AUTH_TYPE: authType USE_KEY_VAULT: useKeyVault ? useKeyVault : '' AZURE_OPENAI_API_KEY: useKeyVault @@ -125,18 +148,7 @@ module web '../core/host/appservice.bicep' = { ), '2023-05-01' ).key1 - AZURE_COSMOSDB_ACCOUNT_KEY: (useKeyVault || cosmosDBKeyName == '') - ? cosmosDBKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.DocumentDB/databaseAccounts', - cosmosDBKeyName - ), - '2022-08-15' - ).primaryMasterKey - }) + })) keyVaultName: keyVaultName runtimeName: runtimeName runtimeVersion: runtimeVersion @@ -167,8 +179,6 @@ module openAIRoleWeb '../core/security/role.bicep' = if (authType == 'rbac') { } // Contributor -// This role is used to grant the service principal contributor access to the resource group -// See if this is needed in the future. module openAIRoleWebContributor '../core/security/role.bicep' = if (authType == 'rbac') { name: 'openai-role-web-contributor' params: { diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep new file mode 100644 index 000000000..80cf9fc7e --- /dev/null +++ b/infra/core/database/postgresdb.bicep @@ -0,0 +1,109 @@ +param solutionName string +param solutionLocation string +@description('The name of the SQL logical server.') +param serverName string = '${solutionName}-postgres' + +param administratorLogin string = 'admintest' +@secure() +param administratorLoginPassword string = 'Initial_0524' +param serverEdition string = 'Burstable' +param skuSizeGB int = 32 +param dbInstanceType string = 'Standard_B1ms' +// param haMode string = 'ZoneRedundant' +param availabilityZone string = '1' +param allowAllIPsFirewall bool = true +param allowAzureIPsFirewall bool = true +@description('PostgreSQL version') +@allowed([ + '11' + '12' + '13' + '14' + '15' + '16' +]) +param version string = '16' + +resource serverName_resource 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12-01-preview' = { + name: serverName + location: solutionLocation + sku: { + name: dbInstanceType + tier: serverEdition + } + properties: { + version: version + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + + highAvailability: { + mode: 'Disabled' + } + storage: { + storageSizeGB: skuSizeGB + } + backup: { + backupRetentionDays: 7 + geoRedundantBackup: 'Disabled' + } + network: { + publicNetworkAccess: 'Enabled' + } + availabilityZone: availabilityZone + } +} + +// resource serverName_firewallrules 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2021-06-01' = [for rule in firewallrules: { +// parent: serverName_resource +// name: rule.Name +// properties: { +// startIpAddress: rule.StartIpAddress +// endIpAddress: rule.EndIpAddress +// } +// }] + + +resource firewall_all 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-12-01-preview' = if (allowAllIPsFirewall) { + parent: serverName_resource + name: 'allow-all-IPs' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + dependsOn: [ + serverName_resource + ] +} + +resource firewall_azure 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-12-01-preview' = if (allowAzureIPsFirewall) { + parent: serverName_resource + name: 'allow-all-azure-internal-IPs' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } + dependsOn: [ + firewall_all + ] +} + +resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-12-01-preview' = { + name: 'azure.extensions' + parent: serverName_resource + properties: { + value: 'vector' + source: 'user-override' + } + dependsOn: [ + firewall_all,firewall_azure + ] +} + + +output postgresDbOutput object = { + postgreSQLServerName: '${serverName_resource.name}.postgres.database.azure.com' + postgreSQLDatabaseName: 'postgres' + postgreSQLDbUser: administratorLogin + postgreSQLDbPwd: administratorLoginPassword + sslMode: 'Require' +} diff --git a/infra/main.bicep b/infra/main.bicep index d87a51f19..096112655 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -291,9 +291,20 @@ param recognizedLanguages string = 'en-US,fr-FR,de-DE,it-IT' @description('Azure Machine Learning Name') param azureMachineLearningName string = 'aml-${resourceToken}' +@description('The type of database to deploy (cosmos or postgres)') +@allowed([ + 'cosmos' + 'postgres' +]) +param databaseType string + + @description('Azure Cosmos DB Account Name') param azureCosmosDBAccountName string = 'cosmos-${resourceToken}' +@description('Azure Postgres DB Account Name') +param azurePostgresDBAccountName string = 'postgres-${resourceToken}' + @description('Whether or not to enable chat history') @allowed([ 'true' @@ -320,7 +331,7 @@ var azureOpenAIEmbeddingModelInfo = string({ modelVersion: azureOpenAIEmbeddingModelVersion }) -module cosmosDBModule './core/database/cosmosdb.bicep' = { +module cosmosDBModule './core/database/cosmosdb.bicep' = if (databaseType == 'cosmos') { name: 'deploy_cosmos_db' params: { name: azureCosmosDBAccountName @@ -329,6 +340,15 @@ module cosmosDBModule './core/database/cosmosdb.bicep' = { scope: resourceGroup() } +module postgresDBModule './core/database/postgresdb.bicep' = if (databaseType == 'postgres') { + name: 'deploy_postgres_sql' + params: { + solutionName: azurePostgresDBAccountName + solutionLocation: 'eastus2' + } + scope: resourceGroup(resourceGroup().name) +} + // Store secrets in a keyvault module keyvault './core/security/keyvault.bicep' = if (useKeyVault || authType == 'rbac') { name: 'keyvault' @@ -486,7 +506,9 @@ module storekeys './app/storekeys.bicep' = if (useKeyVault) { contentSafetyName: contentsafety.outputs.name speechServiceName: speechServiceName computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' - cosmosAccountName: cosmosDBModule.outputs.cosmosOutput.cosmosAccountName + cosmosAccountName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosAccountName : '' + postgresServerName: databaseType == 'postgres' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName : '' + postgresDatabaseName: databaseType == 'postgres' ? 'postgres' : '' rgName: resourceGroupName } } @@ -533,6 +555,13 @@ var azureCosmosDBInfo = string({ containerName: cosmosDBModule.outputs.cosmosOutput.cosmosContainerName }) +var azurePostgresDBInfo = string({ + serverName: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + databaseName: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + userName: postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser + password: postgresDBModule.outputs.postgresDbOutput.postgreSQLDbPwd +}) + module web './app/web.bicep' = if (hostingModel == 'code') { name: websiteName scope: resourceGroup() @@ -552,6 +581,11 @@ module web './app/web.bicep' = if (hostingModel == 'code') { contentSafetyName: contentsafety.outputs.name speechServiceName: speechService.outputs.name computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' + + // New database-related parameters + databaseType: databaseType // Add this parameter to specify 'postgres' or 'cosmos' + + // Conditional key vault key names openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' azureBlobStorageInfo: azureBlobStorageInfo azureFormRecognizerInfo: azureFormRecognizerInfo @@ -559,11 +593,17 @@ module web './app/web.bicep' = if (hostingModel == 'code') { contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' computerVisionKeyName: useKeyVault ? storekeys.outputs.COMPUTER_VISION_KEY_NAME : '' - cosmosDBKeyName: useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' + + // Conditionally set database key names + cosmosDBKeyName: databaseType == 'cosmos' && useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' + postgresInfoName: databaseType == 'postgres' && useKeyVault ? storekeys.outputs.POSTGRESQL_INFO_NAME : '' + useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType - appSettings: { + + appSettings: union({ + // Existing app settings AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion @@ -606,10 +646,17 @@ module web './app/web.bicep' = if (hostingModel == 'code') { ORCHESTRATION_STRATEGY: orchestrationStrategy CONVERSATION_FLOW: conversationFlow LOGLEVEL: logLevel + + // Add database type to settings + AZURE_DATABASE_TYPE: databaseType + }, + // Conditionally add database-specific settings + databaseType == 'cosmos' ? { AZURE_COSMOSDB_INFO: azureCosmosDBInfo AZURE_COSMOSDB_ENABLE_FEEDBACK: true - CHAT_HISTORY_ENABLED: chatHistoryEnabled - } + } : databaseType == 'postgres' ? { + AZURE_POSTGRES_INFO: azurePostgresDBInfo + } : {}) } } @@ -631,6 +678,11 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { contentSafetyName: contentsafety.outputs.name speechServiceName: speechService.outputs.name computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' + + // New database-related parameters + databaseType: databaseType + + // Conditional key vault key names openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' azureBlobStorageInfo: azureBlobStorageInfo azureFormRecognizerInfo: azureFormRecognizerInfo @@ -638,11 +690,17 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { computerVisionKeyName: useKeyVault ? storekeys.outputs.COMPUTER_VISION_KEY_NAME : '' contentSafetyKeyName: useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' - cosmosDBKeyName: useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' + + // Conditionally set database key names + cosmosDBKeyName: databaseType == 'cosmos' && useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' + postgresInfoName: databaseType == 'postgres' && useKeyVault ? storekeys.outputs.POSTGRESQL_INFO_NAME : '' + useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType - appSettings: { + + appSettings: union({ + // Existing app settings AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion @@ -685,10 +743,18 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { ORCHESTRATION_STRATEGY: orchestrationStrategy CONVERSATION_FLOW: conversationFlow LOGLEVEL: logLevel + CHAT_HISTORY_ENABLED: chatHistoryEnabled + + // Add database type to settings + AZURE_DATABASE_TYPE: databaseType + }, + // Conditionally add database-specific settings + databaseType == 'cosmos' ? { AZURE_COSMOSDB_INFO: azureCosmosDBInfo AZURE_COSMOSDB_ENABLE_FEEDBACK: true - CHAT_HISTORY_ENABLED: chatHistoryEnabled - } + } : databaseType == 'postgres' ? { + AZURE_POSTGRESDB_INFO: azurePostgresDBInfo + } : {}) } } @@ -1204,3 +1270,4 @@ output AZURE_ML_WORKSPACE_NAME string = orchestrationStrategy == 'prompt_flow' : '' output RESOURCE_TOKEN string = resourceToken output AZURE_COSMOSDB_INFO string = azureCosmosDBInfo +output AZURE_POSTGRESDB_INFO string = azurePostgresDBInfo From 5eb05131329b43380251fea7268381e7a9f3d2a9 Mon Sep 17 00:00:00 2001 From: gpickett <122489228+gpickett@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:45:24 -0800 Subject: [PATCH 022/107] updated main.json --- infra/main.json | 687 ++++++++++++++++++++++++++++++------------------ 1 file changed, 424 insertions(+), 263 deletions(-) diff --git a/infra/main.json b/infra/main.json index 612909829..975d1a122 100644 --- a/infra/main.json +++ b/infra/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16553051506563672417" + "version": "0.28.1.47646", + "templateHash": "7340432032010169542" } }, "parameters": { @@ -598,6 +598,16 @@ "description": "Azure Machine Learning Name" } }, + "databaseType": { + "type": "string", + "allowedValues": [ + "cosmos", + "postgres" + ], + "metadata": { + "description": "The type of database to deploy (cosmos or postgres)" + } + }, "azureCosmosDBAccountName": { "type": "string", "defaultValue": "[format('cosmos-{0}', parameters('resourceToken'))]", @@ -605,6 +615,13 @@ "description": "Azure Cosmos DB Account Name" } }, + "azurePostgresDBAccountName": { + "type": "string", + "defaultValue": "[format('postgres-{0}', parameters('resourceToken'))]", + "metadata": { + "description": "Azure Postgres DB Account Name" + } + }, "chatHistoryEnabled": { "type": "string", "defaultValue": "true", @@ -660,6 +677,7 @@ }, "resources": [ { + "condition": "[equals(parameters('databaseType'), 'cosmos')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", "name": "deploy_cosmos_db", @@ -682,8 +700,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14453122839528928942" + "version": "0.28.1.47646", + "templateHash": "16333775410276024912" } }, "parameters": { @@ -817,6 +835,184 @@ } } }, + { + "condition": "[equals(parameters('databaseType'), 'postgres')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_postgres_sql", + "resourceGroup": "[resourceGroup().name]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionName": { + "value": "[parameters('azurePostgresDBAccountName')]" + }, + "solutionLocation": { + "value": "eastus2" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.28.1.47646", + "templateHash": "7073717562657244530" + } + }, + "parameters": { + "solutionName": { + "type": "string" + }, + "solutionLocation": { + "type": "string" + }, + "serverName": { + "type": "string", + "defaultValue": "[format('{0}-postgres', parameters('solutionName'))]", + "metadata": { + "description": "The name of the SQL logical server." + } + }, + "administratorLogin": { + "type": "string", + "defaultValue": "admintest" + }, + "administratorLoginPassword": { + "type": "securestring", + "defaultValue": "Initial_0524" + }, + "serverEdition": { + "type": "string", + "defaultValue": "Burstable" + }, + "skuSizeGB": { + "type": "int", + "defaultValue": 32 + }, + "dbInstanceType": { + "type": "string", + "defaultValue": "Standard_B1ms" + }, + "availabilityZone": { + "type": "string", + "defaultValue": "1" + }, + "allowAllIPsFirewall": { + "type": "bool", + "defaultValue": true + }, + "allowAzureIPsFirewall": { + "type": "bool", + "defaultValue": true + }, + "version": { + "type": "string", + "defaultValue": "16", + "allowedValues": [ + "11", + "12", + "13", + "14", + "15", + "16" + ], + "metadata": { + "description": "PostgreSQL version" + } + } + }, + "resources": [ + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers", + "apiVersion": "2023-12-01-preview", + "name": "[parameters('serverName')]", + "location": "[parameters('solutionLocation')]", + "sku": { + "name": "[parameters('dbInstanceType')]", + "tier": "[parameters('serverEdition')]" + }, + "properties": { + "version": "[parameters('version')]", + "administratorLogin": "[parameters('administratorLogin')]", + "administratorLoginPassword": "[parameters('administratorLoginPassword')]", + "highAvailability": { + "mode": "Disabled" + }, + "storage": { + "storageSizeGB": "[parameters('skuSizeGB')]" + }, + "backup": { + "backupRetentionDays": 7, + "geoRedundantBackup": "Disabled" + }, + "network": { + "publicNetworkAccess": "Enabled" + }, + "availabilityZone": "[parameters('availabilityZone')]" + } + }, + { + "condition": "[parameters('allowAllIPsFirewall')]", + "type": "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', parameters('serverName'), 'allow-all-IPs')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "255.255.255.255" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, + { + "condition": "[parameters('allowAzureIPsFirewall')]", + "type": "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', parameters('serverName'), 'allow-all-azure-internal-IPs')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "0.0.0.0" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-IPs')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/configurations", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', parameters('serverName'), 'azure.extensions')]", + "properties": { + "value": "vector", + "source": "user-override" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-IPs')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-azure-internal-IPs')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + } + ], + "outputs": { + "postgresDbOutput": { + "type": "object", + "value": { + "postgreSQLServerName": "[format('{0}.postgres.database.azure.com', parameters('serverName'))]", + "postgreSQLDatabaseName": "postgres", + "postgreSQLDbUser": "[parameters('administratorLogin')]", + "postgreSQLDbPwd": "[parameters('administratorLoginPassword')]", + "sslMode": "Require" + } + } + } + } + } + }, { "condition": "[or(parameters('useKeyVault'), equals(parameters('authType'), 'rbac'))]", "type": "Microsoft.Resources/deployments", @@ -847,8 +1043,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12121357715793816510" + "version": "0.28.1.47646", + "templateHash": "3615364066329169756" }, "description": "Creates an Azure Key Vault." }, @@ -940,8 +1136,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "13123022401063321803" + "version": "0.28.1.47646", + "templateHash": "7514893090820700577" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1095,8 +1291,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "13123022401063321803" + "version": "0.28.1.47646", + "templateHash": "7514893090820700577" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1244,8 +1440,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -1313,8 +1509,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -1382,8 +1578,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -1451,8 +1647,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -1524,8 +1720,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "13123022401063321803" + "version": "0.28.1.47646", + "templateHash": "7514893090820700577" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1679,9 +1875,9 @@ "value": "[parameters('speechServiceName')]" }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", - "cosmosAccountName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName]" - }, + "cosmosAccountName": "[if(equals(parameters('databaseType'), 'cosmos'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName), createObject('value', ''))]", + "postgresServerName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), createObject('value', ''))]", + "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', 'postgres'), createObject('value', ''))]", "rgName": { "value": "[variables('resourceGroupName')]" } @@ -1692,8 +1888,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "15528430944298201007" + "version": "0.28.1.47646", + "templateHash": "11353489804824973855" } }, "parameters": { @@ -1733,6 +1929,18 @@ "type": "string", "defaultValue": "" }, + "postgresServerName": { + "type": "string", + "defaultValue": "" + }, + "postgresDatabaseName": { + "type": "string", + "defaultValue": "postgres" + }, + "postgresInfoName": { + "type": "string", + "defaultValue": "AZURE-POSTGRESQL-INFO" + }, "storageAccountKeyName": { "type": "string", "defaultValue": "AZURE-STORAGE-ACCOUNT-KEY" @@ -1829,11 +2037,21 @@ } }, { + "condition": "[not(equals(parameters('postgresServerName'), ''))]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2022-07-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('postgresInfoName'))]", + "properties": { + "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DBforPostgreSQL/flexibleServers', parameters('postgresServerName')), '2022-12-01').username, 'dbname', parameters('postgresDatabaseName'), 'host', format('{0}.postgres.database.azure.com', parameters('postgresServerName')), 'password', listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DBforPostgreSQL/flexibleServers', parameters('postgresServerName')), '2022-12-01').password)), '')]" + } + }, + { + "condition": "[not(equals(parameters('cosmosAccountName'), ''))]", "type": "Microsoft.KeyVault/vaults/secrets", "apiVersion": "2022-07-01", "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('cosmosAccountKeyName'))]", "properties": { - "value": "[listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosAccountName')), '2022-08-15').primaryMasterKey]" + "value": "[if(not(equals(parameters('cosmosAccountName'), '')), listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosAccountName')), '2022-08-15').primaryMasterKey, '')]" } } ], @@ -1868,7 +2086,11 @@ }, "COSMOS_ACCOUNT_KEY_NAME": { "type": "string", - "value": "[parameters('cosmosAccountKeyName')]" + "value": "[if(not(equals(parameters('cosmosAccountName'), '')), parameters('cosmosAccountKeyName'), '')]" + }, + "POSTGRESQL_INFO_NAME": { + "type": "string", + "value": "[if(not(equals(parameters('postgresServerName'), '')), parameters('postgresInfoName'), '')]" } } } @@ -1879,6 +2101,7 @@ "[resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db')]", "[resourceId('Microsoft.Resources/deployments', parameters('formRecognizerName'))]", "[resourceId('Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[resourceId('Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[resourceId('Microsoft.Resources/deployments', parameters('storageAccountName'))]" ] @@ -1924,8 +2147,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "13584246975784398226" + "version": "0.28.1.47646", + "templateHash": "9322258851357800042" }, "description": "Creates an Azure AI Search instance." }, @@ -2089,8 +2312,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "9286637480882627742" + "version": "0.28.1.47646", + "templateHash": "15465909238121035232" }, "description": "Creates an Azure App Service plan." }, @@ -2199,6 +2422,9 @@ "value": "[reference(resourceId('Microsoft.Resources/deployments', parameters('speechServiceName')), '2022-09-01').outputs.name.value]" }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", + "databaseType": { + "value": "[parameters('databaseType')]" + }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", "azureBlobStorageInfo": { "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" @@ -2210,7 +2436,8 @@ "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", - "cosmosDBKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'cosmos'), parameters('useKeyVault')), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'postgres'), parameters('useKeyVault')), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", "useKeyVault": { "value": "[parameters('useKeyVault')]" }, @@ -2219,53 +2446,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": { - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", - "AZURE_OPENAI_TOP_P": "[parameters('azureOpenAITopP')]", - "AZURE_OPENAI_MAX_TOKENS": "[parameters('azureOpenAIMaxTokens')]", - "AZURE_OPENAI_STOP_SEQUENCE": "[parameters('azureOpenAIStopSequence')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_OPENAI_STREAM": "[parameters('azureOpenAIStream')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_SEARCH_USE_SEMANTIC_SEARCH": "[parameters('azureSearchUseSemanticSearch')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_CONVERSATIONS_LOG_INDEX": "[parameters('azureSearchConversationLogIndex')]", - "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG": "[parameters('azureSearchSemanticSearchConfig')]", - "AZURE_SEARCH_INDEX_IS_PRECHUNKED": "[parameters('azureSearchIndexIsPrechunked')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_SEARCH_ENABLE_IN_DOMAIN": "[parameters('azureSearchEnableInDomain')]", - "AZURE_SEARCH_FILENAME_COLUMN": "[parameters('azureSearchFilenameColumn')]", - "AZURE_SEARCH_FILTER": "[parameters('azureSearchFilter')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "AZURE_SEARCH_URL_COLUMN": "[parameters('azureSearchUrlColumn')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "AZURE_SPEECH_SERVICE_NAME": "[parameters('speechServiceName')]", - "AZURE_SPEECH_SERVICE_REGION": "[variables('location')]", - "AZURE_SPEECH_RECOGNIZER_LANGUAGES": "[parameters('recognizedLanguages')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "ADVANCED_IMAGE_PROCESSING_MAX_IMAGES": "[parameters('advancedImageProcessingMaxImages')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "CONVERSATION_FLOW": "[parameters('conversationFlow')]", - "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_COSMOSDB_INFO": "[string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName))]", - "AZURE_COSMOSDB_ENABLE_FEEDBACK": true, - "CHAT_HISTORY_ENABLED": "[parameters('chatHistoryEnabled')]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName)), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRES_INFO', string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -2274,8 +2455,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12750836139153854515" + "version": "0.28.1.47646", + "templateHash": "12999310699317116765" } }, "parameters": { @@ -2395,9 +2576,17 @@ "type": "string", "defaultValue": "" }, + "databaseType": { + "type": "string", + "defaultValue": "cosmos" + }, "cosmosDBKeyName": { "type": "string", "defaultValue": "" + }, + "postgresInfoName": { + "type": "string", + "defaultValue": "" } }, "resources": [ @@ -2431,7 +2620,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'cosmos'), createObject('DATABASE_TYPE', 'cosmos', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'postgres', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -2456,8 +2645,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "7732628295698757767" + "version": "0.28.1.47646", + "templateHash": "14262267256571278851" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -2683,8 +2872,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16930852302813854027" + "version": "0.28.1.47646", + "templateHash": "1172957779666771475" }, "description": "Updates app settings for an Azure App Service." }, @@ -2761,8 +2950,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -2830,8 +3019,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -2899,8 +3088,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -2968,8 +3157,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -3034,8 +3223,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "465622386717580763" + "version": "0.28.1.47646", + "templateHash": "17016224891593731674" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -3108,6 +3297,7 @@ "[resourceId('Microsoft.Resources/deployments', 'keyvault')]", "[resourceId('Microsoft.Resources/deployments', 'monitoring')]", "[resourceId('Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[resourceId('Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[resourceId('Microsoft.Resources/deployments', parameters('speechServiceName'))]", "[resourceId('Microsoft.Resources/deployments', parameters('storageAccountName'))]", @@ -3165,6 +3355,9 @@ "value": "[reference(resourceId('Microsoft.Resources/deployments', parameters('speechServiceName')), '2022-09-01').outputs.name.value]" }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", + "databaseType": { + "value": "[parameters('databaseType')]" + }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", "azureBlobStorageInfo": { "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" @@ -3176,7 +3369,8 @@ "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", - "cosmosDBKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'cosmos'), parameters('useKeyVault')), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'postgres'), parameters('useKeyVault')), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", "useKeyVault": { "value": "[parameters('useKeyVault')]" }, @@ -3185,53 +3379,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": { - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", - "AZURE_OPENAI_TOP_P": "[parameters('azureOpenAITopP')]", - "AZURE_OPENAI_MAX_TOKENS": "[parameters('azureOpenAIMaxTokens')]", - "AZURE_OPENAI_STOP_SEQUENCE": "[parameters('azureOpenAIStopSequence')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_OPENAI_STREAM": "[parameters('azureOpenAIStream')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_SEARCH_USE_SEMANTIC_SEARCH": "[parameters('azureSearchUseSemanticSearch')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_CONVERSATIONS_LOG_INDEX": "[parameters('azureSearchConversationLogIndex')]", - "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG": "[parameters('azureSearchSemanticSearchConfig')]", - "AZURE_SEARCH_INDEX_IS_PRECHUNKED": "[parameters('azureSearchIndexIsPrechunked')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_SEARCH_ENABLE_IN_DOMAIN": "[parameters('azureSearchEnableInDomain')]", - "AZURE_SEARCH_FILENAME_COLUMN": "[parameters('azureSearchFilenameColumn')]", - "AZURE_SEARCH_FILTER": "[parameters('azureSearchFilter')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "AZURE_SEARCH_URL_COLUMN": "[parameters('azureSearchUrlColumn')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "AZURE_SPEECH_SERVICE_NAME": "[parameters('speechServiceName')]", - "AZURE_SPEECH_SERVICE_REGION": "[variables('location')]", - "AZURE_SPEECH_RECOGNIZER_LANGUAGES": "[parameters('recognizedLanguages')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "ADVANCED_IMAGE_PROCESSING_MAX_IMAGES": "[parameters('advancedImageProcessingMaxImages')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "CONVERSATION_FLOW": "[parameters('conversationFlow')]", - "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_COSMOSDB_INFO": "[string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName))]", - "AZURE_COSMOSDB_ENABLE_FEEDBACK": true, - "CHAT_HISTORY_ENABLED": "[parameters('chatHistoryEnabled')]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'CHAT_HISTORY_ENABLED', parameters('chatHistoryEnabled'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName)), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -3240,8 +3388,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12750836139153854515" + "version": "0.28.1.47646", + "templateHash": "12999310699317116765" } }, "parameters": { @@ -3361,9 +3509,17 @@ "type": "string", "defaultValue": "" }, + "databaseType": { + "type": "string", + "defaultValue": "cosmos" + }, "cosmosDBKeyName": { "type": "string", "defaultValue": "" + }, + "postgresInfoName": { + "type": "string", + "defaultValue": "" } }, "resources": [ @@ -3397,7 +3553,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'cosmos'), createObject('DATABASE_TYPE', 'cosmos', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'postgres', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -3422,8 +3578,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "7732628295698757767" + "version": "0.28.1.47646", + "templateHash": "14262267256571278851" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -3649,8 +3805,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16930852302813854027" + "version": "0.28.1.47646", + "templateHash": "1172957779666771475" }, "description": "Updates app settings for an Azure App Service." }, @@ -3727,8 +3883,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -3796,8 +3952,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -3865,8 +4021,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -3934,8 +4090,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -4000,8 +4156,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "465622386717580763" + "version": "0.28.1.47646", + "templateHash": "17016224891593731674" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -4074,6 +4230,7 @@ "[resourceId('Microsoft.Resources/deployments', 'keyvault')]", "[resourceId('Microsoft.Resources/deployments', 'monitoring')]", "[resourceId('Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[resourceId('Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[resourceId('Microsoft.Resources/deployments', parameters('speechServiceName'))]", "[resourceId('Microsoft.Resources/deployments', parameters('storageAccountName'))]", @@ -4202,8 +4359,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12567732396765618168" + "version": "0.28.1.47646", + "templateHash": "10287577223546471482" } }, "parameters": { @@ -4373,8 +4530,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "7732628295698757767" + "version": "0.28.1.47646", + "templateHash": "14262267256571278851" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -4600,8 +4757,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16930852302813854027" + "version": "0.28.1.47646", + "templateHash": "1172957779666771475" }, "description": "Updates app settings for an Azure App Service." }, @@ -4678,8 +4835,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -4747,8 +4904,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -4816,8 +4973,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -4885,8 +5042,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -4951,8 +5108,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "465622386717580763" + "version": "0.28.1.47646", + "templateHash": "17016224891593731674" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -5149,8 +5306,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12567732396765618168" + "version": "0.28.1.47646", + "templateHash": "10287577223546471482" } }, "parameters": { @@ -5320,8 +5477,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "7732628295698757767" + "version": "0.28.1.47646", + "templateHash": "14262267256571278851" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -5547,8 +5704,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16930852302813854027" + "version": "0.28.1.47646", + "templateHash": "1172957779666771475" }, "description": "Updates app settings for an Azure App Service." }, @@ -5625,8 +5782,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -5694,8 +5851,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -5763,8 +5920,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -5832,8 +5989,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -5898,8 +6055,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "465622386717580763" + "version": "0.28.1.47646", + "templateHash": "17016224891593731674" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -6011,8 +6168,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "2390666818608223959" + "version": "0.28.1.47646", + "templateHash": "6932660993264101661" }, "description": "Creates an Application Insights instance and a Log Analytics workspace." }, @@ -6063,8 +6220,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "19694557100387265" + "version": "0.28.1.47646", + "templateHash": "4411803171372203725" }, "description": "Creates a Log Analytics workspace." }, @@ -6144,8 +6301,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16993757720869129667" + "version": "0.28.1.47646", + "templateHash": "11643435303947380859" }, "description": "Creates an Application Insights instance based on an existing Log Analytics workspace." }, @@ -6209,8 +6366,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12524466040979787143" + "version": "0.28.1.47646", + "templateHash": "14575977876683967619" }, "description": "Creates a dashboard for an Application Insights instance." }, @@ -7544,8 +7701,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "15151749822990864279" + "version": "0.28.1.47646", + "templateHash": "15964344188120348249" } }, "parameters": { @@ -7627,8 +7784,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "15030863077610448627" + "version": "0.28.1.47646", + "templateHash": "1761446928932158043" } }, "parameters": { @@ -7822,8 +7979,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "829406794789220597" + "version": "0.28.1.47646", + "templateHash": "17966621184348323886" } }, "parameters": { @@ -8015,8 +8172,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "3077544357242613291" + "version": "0.28.1.47646", + "templateHash": "4659883628761739984" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -8223,8 +8380,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "7732628295698757767" + "version": "0.28.1.47646", + "templateHash": "14262267256571278851" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -8450,8 +8607,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16930852302813854027" + "version": "0.28.1.47646", + "templateHash": "1172957779666771475" }, "description": "Updates app settings for an Azure App Service." }, @@ -8546,8 +8703,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -8615,8 +8772,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -8684,8 +8841,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -8753,8 +8910,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -8822,8 +8979,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -8888,8 +9045,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "465622386717580763" + "version": "0.28.1.47646", + "templateHash": "17016224891593731674" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -9070,8 +9227,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "829406794789220597" + "version": "0.28.1.47646", + "templateHash": "17966621184348323886" } }, "parameters": { @@ -9263,8 +9420,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "3077544357242613291" + "version": "0.28.1.47646", + "templateHash": "4659883628761739984" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -9471,8 +9628,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "7732628295698757767" + "version": "0.28.1.47646", + "templateHash": "14262267256571278851" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -9698,8 +9855,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "16930852302813854027" + "version": "0.28.1.47646", + "templateHash": "1172957779666771475" }, "description": "Updates app settings for an Azure App Service." }, @@ -9794,8 +9951,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -9863,8 +10020,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -9932,8 +10089,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -10001,8 +10158,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -10070,8 +10227,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -10136,8 +10293,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "465622386717580763" + "version": "0.28.1.47646", + "templateHash": "17016224891593731674" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -10240,8 +10397,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "13123022401063321803" + "version": "0.28.1.47646", + "templateHash": "7514893090820700577" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10391,8 +10548,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "13123022401063321803" + "version": "0.28.1.47646", + "templateHash": "7514893090820700577" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10545,8 +10702,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "6699069410959282929" + "version": "0.28.1.47646", + "templateHash": "5895555282161133397" } }, "parameters": { @@ -10673,8 +10830,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "7157574004190707979" + "version": "0.28.1.47646", + "templateHash": "16998952628947606682" }, "description": "Creates an Azure storage account." }, @@ -10894,8 +11051,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -10960,8 +11117,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -11026,8 +11183,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -11092,8 +11249,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14973584850527407631" + "version": "0.28.1.47646", + "templateHash": "4781574865545118092" }, "description": "Creates a role assignment for a service principal." }, @@ -11174,8 +11331,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "17372485166957435450" + "version": "0.28.1.47646", + "templateHash": "18438613076567100409" } }, "parameters": { @@ -11519,6 +11676,10 @@ "AZURE_COSMOSDB_INFO": { "type": "string", "value": "[string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName))]" + }, + "AZURE_POSTGRESDB_INFO": { + "type": "string", + "value": "[string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))]" } } } \ No newline at end of file From 58a60e03bbc50ff9c075009ded1ab2a65dc84c02 Mon Sep 17 00:00:00 2001 From: gpickett <122489228+gpickett@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:46:51 -0800 Subject: [PATCH 023/107] updated oneclick deploy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1094a9b2a..0eada0adb 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ There are two choices; the "Deploy to Azure" offers a one click deployment where The demo, which uses containers pre-built from the main branch is available by clicking this button: -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fchat-with-your-data-solution-accelerator%2Fmain%2Finfra%2Fmain.json) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fchat-with-your-data-solution-accelerator%2FpostgresBicepChanges%2Finfra%2Fmain.json) **Note**: The default configuration deploys an OpenAI Model "gpt-35-turbo" with version 0613. However, not all locations support this version. If you're deploying to a location that doesn't support version 0613, you'll need to From 8cfd2760bccc69016fdd15816af50b9ae7d061b5 Mon Sep 17 00:00:00 2001 From: gpickett <122489228+gpickett@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:51:25 -0800 Subject: [PATCH 024/107] updated oneclick --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0eada0adb..e5cb75b79 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ There are two choices; the "Deploy to Azure" offers a one click deployment where The demo, which uses containers pre-built from the main branch is available by clicking this button: -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fchat-with-your-data-solution-accelerator%2FpostgresBicepChanges%2Finfra%2Fmain.json) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fchat-with-your-data-solution-accelerator%2Frefs%2Fheads%2FpostgresBicepChanges%2Finfra%2Fmain.json) **Note**: The default configuration deploys an OpenAI Model "gpt-35-turbo" with version 0613. However, not all locations support this version. If you're deploying to a location that doesn't support version 0613, you'll need to From f6a9fe2a4456756eaa70a9618241ab87d80c825c Mon Sep 17 00:00:00 2001 From: gpickett <122489228+gpickett@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:54:35 -0800 Subject: [PATCH 025/107] fixed oneclick again --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5cb75b79..27a2cc080 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ There are two choices; the "Deploy to Azure" offers a one click deployment where The demo, which uses containers pre-built from the main branch is available by clicking this button: -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Fchat-with-your-data-solution-accelerator%2Frefs%2Fheads%2FpostgresBicepChanges%2Finfra%2Fmain.json) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FFr4nc3%2Fchat-with-your-data-solution-accelerator%2Frefs%2Fheads%2FpostgresBicepChanges%2Finfra%2Fmain.json) **Note**: The default configuration deploys an OpenAI Model "gpt-35-turbo" with version 0613. However, not all locations support this version. If you're deploying to a location that doesn't support version 0613, you'll need to From a3e1d62990cc4d5ea7864a084a46797d97bed52f Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 20 Nov 2024 18:05:54 +0530 Subject: [PATCH 026/107] Configure Admin Options Based on PostgreSQL Selection --- .../utilities/helpers/config/config_helper.py | 21 ++++++++++++----- .../utilities/helpers/config/default.json | 6 ++--- .../batch/utilities/helpers/env_helper.py | 3 --- .../orchestrator/orchestrator_base.py | 4 ++-- code/backend/pages/04_Configuration.py | 23 +++++++++++++------ 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/code/backend/batch/utilities/helpers/config/config_helper.py b/code/backend/batch/utilities/helpers/config/config_helper.py index 3cbd2cb83..dca7c52ab 100644 --- a/code/backend/batch/utilities/helpers/config/config_helper.py +++ b/code/backend/batch/utilities/helpers/config/config_helper.py @@ -50,9 +50,7 @@ def __init__(self, config: dict): if self.env_helper.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION else None ) - self.enable_chat_history = config.get( - "enable_chat_history", self.env_helper.CHAT_HISTORY_ENABLED - ) + self.enable_chat_history = config["enable_chat_history"] self.database_type = config.get("database_type", self.env_helper.DATABASE_TYPE) def get_available_document_types(self) -> list[str]: @@ -120,8 +118,10 @@ def __init__(self, messages: dict): class Logging: def __init__(self, logging: dict): - self.log_user_interactions = logging["log_user_interactions"] - self.log_tokens = logging["log_tokens"] + self.log_user_interactions = ( + str(logging["log_user_interactions"]).lower() == "true" + ) + self.log_tokens = str(logging["log_tokens"]).lower() == "true" class IntegratedVectorizationConfig: @@ -252,7 +252,16 @@ def get_default_config(): if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value else env_helper.ORCHESTRATION_STRATEGY ), - CHAT_HISTORY_ENABLED=env_helper.CHAT_HISTORY_ENABLED, + LOG_USER_INTERACTIONS=( + False + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value + else True + ), + LOG_TOKENS=( + False + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value + else True + ), DATABASE_TYPE=env_helper.DATABASE_TYPE, ) ) diff --git a/code/backend/batch/utilities/helpers/config/default.json b/code/backend/batch/utilities/helpers/config/default.json index f5db4359f..45db5ee3c 100644 --- a/code/backend/batch/utilities/helpers/config/default.json +++ b/code/backend/batch/utilities/helpers/config/default.json @@ -136,12 +136,12 @@ "page_overlap_length": "100" }, "logging": { - "log_user_interactions": true, - "log_tokens": true + "log_user_interactions": "${LOG_USER_INTERACTIONS}", + "log_tokens": "${LOG_TOKENS}" }, "orchestrator": { "strategy": "${ORCHESTRATION_STRATEGY}" }, - "enable_chat_history": "${CHAT_HISTORY_ENABLED}", + "enable_chat_history": true, "database_type": "${DATABASE_TYPE}" } diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index 2654270a5..d46871c17 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -296,9 +296,6 @@ def __load_config(self, **kwargs) -> None: # Chat History DB Integration Settings # Set default values based on DATABASE_TYPE self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "").strip() or "CosmosDB" - self.CHAT_HISTORY_ENABLED = self.get_env_var_bool( - "CHAT_HISTORY_ENABLED", "true" - ) # Cosmos DB configuration if self.DATABASE_TYPE == "CosmosDB": azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") diff --git a/code/backend/batch/utilities/orchestrator/orchestrator_base.py b/code/backend/batch/utilities/orchestrator/orchestrator_base.py index 1073b9ec0..15539e305 100644 --- a/code/backend/batch/utilities/orchestrator/orchestrator_base.py +++ b/code/backend/batch/utilities/orchestrator/orchestrator_base.py @@ -70,7 +70,7 @@ async def handle_message( **kwargs: Optional[dict], ) -> dict: result = await self.orchestrate(user_message, chat_history, **kwargs) - if self.config.logging.log_tokens: + if str(self.config.logging.log_tokens).lower() == "true": custom_dimensions = { "conversation_id": conversation_id, "message_id": self.message_id, @@ -79,7 +79,7 @@ async def handle_message( "total_tokens": self.tokens["total"], } logger.info("Token Consumption", extra=custom_dimensions) - if self.config.logging.log_user_interactions: + if str(self.config.logging.log_user_interactions).lower() == "true": self.conversation_logger.log( messages=[ { diff --git a/code/backend/pages/04_Configuration.py b/code/backend/pages/04_Configuration.py index d6ddf93a8..c41d17aa5 100644 --- a/code/backend/pages/04_Configuration.py +++ b/code/backend/pages/04_Configuration.py @@ -59,10 +59,11 @@ def load_css(file_path): if "example_answer" not in st.session_state: st.session_state["example_answer"] = config.example.answer if "log_user_interactions" not in st.session_state: - st.session_state["log_user_interactions"] = config.logging.log_user_interactions + st.session_state["log_user_interactions"] = ( + str(config.logging.log_user_interactions).lower() == "true" + ) if "log_tokens" not in st.session_state: - st.session_state["log_tokens"] = config.logging.log_tokens - + st.session_state["log_tokens"] = str(config.logging.log_tokens).lower() == "true" if "orchestrator_strategy" not in st.session_state: st.session_state["orchestrator_strategy"] = config.orchestrator.strategy.value if "ai_assistant_type" not in st.session_state: @@ -71,9 +72,7 @@ def load_css(file_path): st.session_state["conversational_flow"] = config.prompts.conversational_flow if "enable_chat_history" not in st.session_state: st.session_state["enable_chat_history"] = ( - config.enable_chat_history.lower() == "true" - if isinstance(config.enable_chat_history, str) - else config.enable_chat_history + str(config.enable_chat_history).lower() == "true" ) if "database_type" not in st.session_state: st.session_state["database_type"] = config.database_type @@ -391,11 +390,21 @@ def validate_documents(): st.checkbox("Enable chat history", key="enable_chat_history") with st.expander("Logging configuration", expanded=True): + disable_checkboxes = ( + True + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value + else False + ) st.checkbox( "Log user input and output (questions, answers, conversation history, sources)", key="log_user_interactions", + disabled=disable_checkboxes, + ) + st.checkbox( + "Log tokens", + key="log_tokens", + disabled=disable_checkboxes, ) - st.checkbox("Log tokens", key="log_tokens") if st.form_submit_button("Save configuration"): document_processors = [] From 682e5d7454a59575e6388f42620c191ad02e9030 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Wed, 20 Nov 2024 19:32:45 -0500 Subject: [PATCH 027/107] scaffolder for postgress create search vectors --- .../search/postgres_search_handler.py | 73 ++++++ poetry.lock | 16 +- pyproject.toml | 1 + .../data_scripts/create_postgres_tables.py | 79 ++++++ scripts/data_scripts/create_tables.py | 90 ------- scripts/data_scripts/load_vectors.py | 231 ------------------ 6 files changed, 168 insertions(+), 322 deletions(-) create mode 100644 code/backend/batch/utilities/search/postgres_search_handler.py create mode 100644 scripts/data_scripts/create_postgres_tables.py delete mode 100644 scripts/data_scripts/create_tables.py delete mode 100644 scripts/data_scripts/load_vectors.py diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py new file mode 100644 index 000000000..5a4824b8b --- /dev/null +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -0,0 +1,73 @@ +from azure.identity import DefaultAzureCredential +import psycopg2 +from pgvector.psycopg2 import register_vector +from openai import AzureOpenAI +import time + +class PostgresSearchClient(): + + def __init__( + self, user: str, host: str, database: str + ): + self.user = user + self.host = host + self.database = database + self.conn = None + + async def connect(self): + """ + Create a new database connection. + + The connection parameters can be specified as a string: + + conn = psycopg2.connect("dbname=test user=postgres password=secret") + + or using a set of keyword arguments: + + conn = psycopg2.connect(database="test", user="postgres", password="secret") + + Or as a mix of both. The basic connection parameters are: + + - *dbname*: the database name + - *database*: the database name (only as keyword argument) + - *user*: user name used to authenticate + - *password*: password used to authenticate + - *host*: database host address (defaults to UNIX socket if not provided) + - *port*: connection port number (defaults to 5432 if not provided) + """ + credential = DefaultAzureCredential() + token = credential.get_token( + "https://ossrdbms-aad.database.windows.net/.default" + ).token + #TODO FIX THIS + conn_string = "host=your_postgresql_server.postgres.database.azure.com dbname=your_database " + self.conn = psycopg2.connect(conn_string + ' passwor=' + token) + + + async def get_embeddings(self, text: str,openai_api_base,openai_api_version,openai_api_key): + model_id = "text-embedding-ada-002" + client = AzureOpenAI( + api_version=openai_api_version, + azure_endpoint=openai_api_base, + api_key = openai_api_key + ) + + embedding = client.embeddings.create(input=text, model=model_id).data[0].embedding + return embedding + + async def create_vector(self): + try: + v_contentVector = self.get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) + except: + time.sleep(30) + v_contentVector = self.get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) + return v_contentVector + + async def insert_vector(self): + #TODO FIX THIS + self.connect() + cur = self.conn.cursor() + cur.execute(f"INSERT INTO search_index (id,chunk_id, client_id, content, sourceurl, contentVector) VALUES (%s,%s,%s,%s,%s,%s)", (id, d["chunk_id"], d["client_id"], d["content"], path.name.split('/')[-1], v_contentVector)) + cur.close() + self.conn.commit() + diff --git a/poetry.lock b/poetry.lock index ce20e3f69..0b80b1f4d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4216,6 +4216,20 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "pgvector" +version = "0.3.6" +description = "pgvector support for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pgvector-0.3.6-py3-none-any.whl", hash = "sha256:f6c269b3c110ccb7496bac87202148ed18f34b390a0189c783e351062400a75a"}, + {file = "pgvector-0.3.6.tar.gz", hash = "sha256:31d01690e6ea26cea8a633cde5f0f55f5b246d9c8292d68efdef8c22ec994ade"}, +] + +[package.dependencies] +numpy = "*" + [[package]] name = "pillow" version = "10.4.0" @@ -6769,4 +6783,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "6b9ab8ba01b4a246ec4cdd28450595a0ae4d626c5446f66e1aaeeae8e13d0d75" +content-hash = "97357d05111bd44511d0b014ee4a0bc3b1622d47142c0dc9c26032f267955972" diff --git a/pyproject.toml b/pyproject.toml index f3c742829..452675302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ azure-ai-ml = "^1.21.1" azure-cosmos = "^4.7.0" asyncpg = "^0.30.0" psycopg2-binary = "^2.9.10" +pgvector = "^0.3.6" [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py new file mode 100644 index 000000000..497122e2b --- /dev/null +++ b/scripts/data_scripts/create_postgres_tables.py @@ -0,0 +1,79 @@ +from azure.identity import DefaultAzureCredential +import psycopg2 +from pgvector.psycopg2 import register_vector + + +# Acquire the access token +credential = DefaultAzureCredential() +token = credential.get_token( + "https://ossrdbms-aad.database.windows.net/.default" +).token + +#TODO FIX THIS +conn_string = "host=your_postgresql_server.postgres.database.azure.com dbname=your_database " +conn = psycopg2.connect(conn_string + ' password=' + token) +cursor = conn.cursor() + +cursor.execute('DROP TABLE IF EXISTS conversations') +conn.commit() + +create_cs_sql = """CREATE TABLE conversations ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + created_at TEXT, + updated_at TEXT, + user_id TEXT NOT NULL, + title TEXT + );""" + +cursor.execute(create_cs_sql) +conn.commit() + +cursor = conn.cursor() + +cursor.execute('DROP TABLE IF EXISTS messages') +conn.commit() + +create_cs_sql = """CREATE TABLE messages ( + id TEXT PRIMARY KEY, + type VARCHAR(50) NOT NULL, + created_at TEXT, + updated_at TEXT, + user_id TEXT NOT NULL, + conversation_id TEXT NOT NULL, + role VARCHAR(50), + content TEXT NOT NULL, + feedback TEXT + );""" + +cursor.execute(create_cs_sql) +conn.commit() + +cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") +conn.commit() + +#TODO review if this command is necessary for creating the table +# Register the vector type with psycopg2 +register_vector(conn) + +cursor.execute('DROP TABLE IF EXISTS search_indexes;') +# Create table to store embeddings and metadata + +table_create_command = """ +CREATE TABLE IF NOT EXISTS search_indexes( + id text, + title text, + chunk integer, + chunk_id text, + offset integer, + page_number integer, + content text, + source text, + metadata text, + content_vector vector(1536) + ); + """ + +cursor.execute(table_create_command) +cursor.close() +conn.commit() \ No newline at end of file diff --git a/scripts/data_scripts/create_tables.py b/scripts/data_scripts/create_tables.py deleted file mode 100644 index 176b75da8..000000000 --- a/scripts/data_scripts/create_tables.py +++ /dev/null @@ -1,90 +0,0 @@ -key_vault_name = 'kv_to-be-replaced' - -import pandas as pd -# import pymssql -import os -from datetime import datetime - -import urllib.parse -import psycopg2 - -from azure.keyvault.secrets import SecretClient -from azure.identity import DefaultAzureCredential - -def get_secrets_from_kv(kv_name, secret_name): - key_vault_name = kv_name # Set the name of the Azure Key Vault - credential = DefaultAzureCredential() - 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 - -#TODO change connectivity with SFI -server = get_secrets_from_kv(key_vault_name,"POSTGRESQL-SERVER") -database = get_secrets_from_kv(key_vault_name,"POSTGRESQL-DATABASENAME") -username = get_secrets_from_kv(key_vault_name,"POSTGRESQL-USER") -password = get_secrets_from_kv(key_vault_name,"POSTGRESQL-PASSWORD") -sslmode = 'require' - -# Construct connection URI -db_uri = f"postgresql://{username}:{password}@{server}/{database}?sslmode={sslmode}" -# conn = pymssql.connect(server, username, password, database) - -conn = psycopg2.connect(db_uri) -print("Connection established") - -cursor = conn.cursor() - -from azure.storage.filedatalake import ( - DataLakeServiceClient -) - -account_name = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-NAME") -account_key = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-KEY") - -account_url = f"https://{account_name}.dfs.core.windows.net" - -service_client = DataLakeServiceClient(account_url, credential=account_key,api_version='2023-01-03') - -file_system_client_name = "data" -directory = 'clientdata' - -file_system_client = service_client.get_file_system_client(file_system_client_name) -directory_name = directory - -cursor = conn.cursor() - -#TODO CWYD POSTGRES TABLES - -cursor.execute('DROP TABLE IF EXISTS conversations') -conn.commit() - -create_cs_sql = """CREATE TABLE conversations ( - id TEXT PRIMARY KEY, - type TEXT NOT NULL, - created_at TEXT, - updated_at TEXT, - user_id TEXT NOT NULL, - title TEXT - );""" - -cursor.execute(create_cs_sql) -conn.commit() - -cursor = conn.cursor() - -cursor.execute('DROP TABLE IF EXISTS messages') -conn.commit() - -create_cs_sql = """CREATE TABLE messages ( - id TEXT PRIMARY KEY, - type VARCHAR(50) NOT NULL, - created_at TEXT, - updated_at TEXT, - user_id TEXT NOT NULL, - conversation_id TEXT NOT NULL, - role VARCHAR(50), - content TEXT NOT NULL, - feedback TEXT - );""" - -cursor.execute(create_cs_sql) -conn.commit() diff --git a/scripts/data_scripts/load_vectors.py b/scripts/data_scripts/load_vectors.py deleted file mode 100644 index 5e2f33e6e..000000000 --- a/scripts/data_scripts/load_vectors.py +++ /dev/null @@ -1,231 +0,0 @@ -import openai -import os -import pandas as pd -import numpy as np -import json -# import tiktoken -import psycopg2 -import ast -import pgvector -import math -from psycopg2.extras import execute_values -from pgvector.psycopg2 import register_vector - -#TODO may this file is not needed for CWYD postgress Integration - -#Get Azure Key Vault Client -key_vault_name = 'kv_to-be-replaced' - -index_name = "transcripts_index" - -file_system_client_name = "data" -directory = 'clienttranscripts/meeting_transcripts' -csv_file_name = 'clienttranscripts/meeting_transcripts_metadata/transcripts_metadata.csv' - -from azure.keyvault.secrets import SecretClient -from azure.identity import DefaultAzureCredential - -def get_secrets_from_kv(kv_name, secret_name): - - # Set the name of the Azure Key Vault - key_vault_name = kv_name - credential = DefaultAzureCredential() - - # 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) - - # Retrieve the secret value - return(secret_client.get_secret(secret_name).value) - -# openai_api_type = get_secrets_from_kv(key_vault_name,"OPENAI-API-TYPE") -openai_api_key = get_secrets_from_kv(key_vault_name,"AZURE-OPENAI-KEY") -openai_api_base = get_secrets_from_kv(key_vault_name,"AZURE-OPENAI-ENDPOINT") -openai_api_version = get_secrets_from_kv(key_vault_name,"AZURE-OPENAI-PREVIEW-API-VERSION") - -# Connect to PostgreSQL database using connection string -server = get_secrets_from_kv(key_vault_name,"POSTGRESQL-SERVER") -database = get_secrets_from_kv(key_vault_name,"POSTGRESQL-DATABASENAME") -username = get_secrets_from_kv(key_vault_name,"POSTGRESQL-USER") -password = get_secrets_from_kv(key_vault_name,"POSTGRESQL-PASSWORD") -sslmode = 'require' - -# Construct connection URI -db_uri = f"postgresql://{username}:{password}@{server}/{database}?sslmode={sslmode}" - -conn = psycopg2.connect(db_uri) -cur = conn.cursor() - -#install pgvector -cur = conn.cursor() -cur.execute("CREATE EXTENSION IF NOT EXISTS vector") -conn.commit() - -# Register the vector type with psycopg2 -register_vector(conn) - -cur.execute('DROP TABLE IF EXISTS calltranscripts;') -# Create table to store embeddings and metadata -table_create_command = """ -CREATE TABLE IF NOT EXISTS calltranscripts ( - id text, - chunk_id text, - content text, - sourceurl text, - client_id integer, - contentVector vector(1536) - ); - """ - -cur.execute(table_create_command) -cur.close() -conn.commit() - -from openai import AzureOpenAI - -# Function: Get Embeddings -def get_embeddings(text: str,openai_api_base,openai_api_version,openai_api_key): - model_id = "text-embedding-ada-002" - client = AzureOpenAI( - api_version=openai_api_version, - azure_endpoint=openai_api_base, - api_key = openai_api_key - ) - - embedding = client.embeddings.create(input=text, model=model_id).data[0].embedding - - return embedding - -import re - -def clean_spaces_with_regex(text): - # Use a regular expression to replace multiple spaces with a single space - cleaned_text = re.sub(r'\s+', ' ', text) - # Use a regular expression to replace consecutive dots with a single dot - cleaned_text = re.sub(r'\.{2,}', '.', cleaned_text) - return cleaned_text - -def chunk_data(text): - tokens_per_chunk = 1024 #500 - text = clean_spaces_with_regex(text) - SENTENCE_ENDINGS = [".", "!", "?"] - WORDS_BREAKS = ['\n', '\t', '}', '{', ']', '[', ')', '(', ' ', ':', ';', ','] - - sentences = text.split('. ') # Split text into sentences - chunks = [] - current_chunk = '' - current_chunk_token_count = 0 - - # Iterate through each sentence - for sentence in sentences: - # Split sentence into tokens - tokens = sentence.split() - - # Check if adding the current sentence exceeds tokens_per_chunk - if current_chunk_token_count + len(tokens) <= tokens_per_chunk: - # Add the sentence to the current chunk - if current_chunk: - current_chunk += '. ' + sentence - else: - current_chunk += sentence - current_chunk_token_count += len(tokens) - else: - # Add current chunk to chunks list and start a new chunk - chunks.append(current_chunk) - current_chunk = sentence - current_chunk_token_count = len(tokens) - - # Add the last chunk - if current_chunk: - chunks.append(current_chunk) - - return chunks - -#add documents to the index - -import json -import base64 -import time -import pandas as pd -from azure.search.documents import SearchClient -import os - -# foldername = 'clienttranscripts' -# path_name = f'Data/{foldername}/meeting_transcripts' -# # paths = mssparkutils.fs.ls(path_name) - -# paths = os.listdir(path_name) - -conn = psycopg2.connect(db_uri) -cur = conn.cursor() - -from azure.storage.filedatalake import ( - DataLakeServiceClient, - DataLakeDirectoryClient, - FileSystemClient -) - -file_system_client_name = "data" -directory = 'clienttranscripts/meeting_transcripts' -csv_file_name = 'clienttranscripts/meeting_transcripts_metadata/transcripts_metadata.csv' - -account_name = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-NAME") -account_key = get_secrets_from_kv(key_vault_name, "ADLS-ACCOUNT-KEY") - -account_url = f"https://{account_name}.dfs.core.windows.net" - -service_client = DataLakeServiceClient(account_url, credential=account_key,api_version='2023-01-03') - -file_system_client = service_client.get_file_system_client(file_system_client_name) -directory_name = directory -paths = file_system_client.get_paths(path=directory_name) -# print(paths) - - -import pandas as pd -# Read the CSV file into a Pandas DataFrame -file_path = csv_file_name -# print(file_path) -file_client = file_system_client.get_file_client(file_path) -csv_file = file_client.download_file() -df_metadata = pd.read_csv(csv_file, encoding='utf-8') - -docs = [] -counter = 0 -for path in paths: - file_client = file_system_client.get_file_client(path.name) - data_file = file_client.download_file() - data = json.load(data_file) - text = data['Content'] - - filename = path.name.split('/')[-1] - document_id = filename.replace('.json','').replace('convo_','') - # print(document_id) - df_file_metadata = df_metadata[df_metadata['ConversationId']==str(document_id)].iloc[0] - - chunks = chunk_data(text) - chunk_num = 0 - for chunk in chunks: - chunk_num += 1 - d = { - "chunk_id" : document_id + '_' + str(chunk_num).zfill(2), - "client_id": str(df_file_metadata['ClientId']), - "content": 'ClientId is ' + str(df_file_metadata['ClientId']) + ' . ' + chunk, - } - - counter += 1 - - try: - v_contentVector = get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) - except: - time.sleep(30) - v_contentVector = get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) - - - id = base64.urlsafe_b64encode(bytes(d["chunk_id"], encoding='utf-8')).decode('utf-8') - - cur.execute(f"INSERT INTO calltranscripts (id,chunk_id, client_id, content, sourceurl, contentVector) VALUES (%s,%s,%s,%s,%s,%s)", (id, d["chunk_id"], d["client_id"], d["content"], path.name.split('/')[-1], v_contentVector)) - #break - # break - -cur.close() -conn.commit() \ No newline at end of file From 70296f60fcddf3aa4dcfeae0681ea7b53f4c2ed6 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Thu, 21 Nov 2024 09:42:20 -0500 Subject: [PATCH 028/107] format code --- .../search/postgres_search_handler.py | 51 ++++++++++++------- .../data_scripts/create_postgres_tables.py | 22 ++++---- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py index 5a4824b8b..de809a88f 100644 --- a/code/backend/batch/utilities/search/postgres_search_handler.py +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -4,11 +4,10 @@ from openai import AzureOpenAI import time -class PostgresSearchClient(): - def __init__( - self, user: str, host: str, database: str - ): +class PostgresSearchClient: + + def __init__(self, user: str, host: str, database: str): self.user = user self.host = host self.database = database @@ -39,35 +38,51 @@ async def connect(self): token = credential.get_token( "https://ossrdbms-aad.database.windows.net/.default" ).token - #TODO FIX THIS + # TODO FIX THIS conn_string = "host=your_postgresql_server.postgres.database.azure.com dbname=your_database " - self.conn = psycopg2.connect(conn_string + ' passwor=' + token) - + self.conn = psycopg2.connect(conn_string + " passwor=" + token) - async def get_embeddings(self, text: str,openai_api_base,openai_api_version,openai_api_key): + async def get_embeddings( + self, text: str, openai_api_base, openai_api_version, openai_api_key + ): model_id = "text-embedding-ada-002" client = AzureOpenAI( api_version=openai_api_version, azure_endpoint=openai_api_base, - api_key = openai_api_key + api_key=openai_api_key, + ) + + embedding = ( + client.embeddings.create(input=text, model=model_id).data[0].embedding ) - - embedding = client.embeddings.create(input=text, model=model_id).data[0].embedding return embedding - + async def create_vector(self): try: - v_contentVector = self.get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) + v_contentVector = self.get_embeddings( + d["content"], openai_api_base, openai_api_version, openai_api_key + ) except: time.sleep(30) - v_contentVector = self.get_embeddings(d["content"],openai_api_base,openai_api_version,openai_api_key) + v_contentVector = self.get_embeddings( + d["content"], openai_api_base, openai_api_version, openai_api_key + ) return v_contentVector - + async def insert_vector(self): - #TODO FIX THIS + # TODO FIX THIS self.connect() cur = self.conn.cursor() - cur.execute(f"INSERT INTO search_index (id,chunk_id, client_id, content, sourceurl, contentVector) VALUES (%s,%s,%s,%s,%s,%s)", (id, d["chunk_id"], d["client_id"], d["content"], path.name.split('/')[-1], v_contentVector)) + cur.execute( + f"INSERT INTO search_index (id,chunk_id, client_id, content, sourceurl, contentVector) VALUES (%s,%s,%s,%s,%s,%s)", + ( + id, + d["chunk_id"], + d["client_id"], + d["content"], + path.name.split("/")[-1], + v_contentVector, + ), + ) cur.close() self.conn.commit() - diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 497122e2b..bdcdc7e3c 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -5,16 +5,16 @@ # Acquire the access token credential = DefaultAzureCredential() -token = credential.get_token( - "https://ossrdbms-aad.database.windows.net/.default" -).token +token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default").token -#TODO FIX THIS -conn_string = "host=your_postgresql_server.postgres.database.azure.com dbname=your_database " -conn = psycopg2.connect(conn_string + ' password=' + token) +# TODO FIX THIS +conn_string = ( + "host=your_postgresql_server.postgres.database.azure.com dbname=your_database " +) +conn = psycopg2.connect(conn_string + " password=" + token) cursor = conn.cursor() -cursor.execute('DROP TABLE IF EXISTS conversations') +cursor.execute("DROP TABLE IF EXISTS conversations") conn.commit() create_cs_sql = """CREATE TABLE conversations ( @@ -31,7 +31,7 @@ cursor = conn.cursor() -cursor.execute('DROP TABLE IF EXISTS messages') +cursor.execute("DROP TABLE IF EXISTS messages") conn.commit() create_cs_sql = """CREATE TABLE messages ( @@ -52,11 +52,11 @@ cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") conn.commit() -#TODO review if this command is necessary for creating the table +# TODO review if this command is necessary for creating the table # Register the vector type with psycopg2 register_vector(conn) -cursor.execute('DROP TABLE IF EXISTS search_indexes;') +cursor.execute("DROP TABLE IF EXISTS search_indexes;") # Create table to store embeddings and metadata table_create_command = """ @@ -76,4 +76,4 @@ cursor.execute(table_create_command) cursor.close() -conn.commit() \ No newline at end of file +conn.commit() From 8e05301422a9170abcac842023bd28e50948be98 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 21 Nov 2024 22:07:32 +0530 Subject: [PATCH 029/107] Implement retrieve Index Functionality in PostgreSQL - structure --- .../helpers/azure_postgres_helper.py | 31 ++++ .../search/postgres_search_handler.py | 138 +++++++++--------- code/backend/batch/utilities/search/search.py | 12 +- .../data_scripts/create_postgres_tables.py | 82 ++++++----- 4 files changed, 152 insertions(+), 111 deletions(-) create mode 100644 code/backend/batch/utilities/helpers/azure_postgres_helper.py diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py new file mode 100644 index 000000000..7a488f3f5 --- /dev/null +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -0,0 +1,31 @@ +import logging +import psycopg2 +from .llm_helper import LLMHelper +from .env_helper import EnvHelper +from azure.identity import DefaultAzureCredential + +logger = logging.getLogger(__name__) + + +class AzurePostgresHelper: + + def __init__(self): + self.llm_helper = LLMHelper() + self.env_helper = EnvHelper() + + def connect(self): + user = self.env_helper.POSTGRESQL_USER + host = self.env_helper.POSTGRESQL_HOST + dbname = self.env_helper.POSTGRESQL_DATABASE + cred = DefaultAzureCredential() + # Acquire the access token + accessToken = cred.get_token( + "https://ossrdbms-aad.database.windows.net/.default" + ) + + # Combine the token with the connection string to establish the connection. + conn_string = "host={0} user={1} dbname={2} password={3}".format( + host, user, dbname, accessToken.token + ) + conn = psycopg2.connect(conn_string) + return conn diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py index de809a88f..3d78b9108 100644 --- a/code/backend/batch/utilities/search/postgres_search_handler.py +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -1,88 +1,84 @@ -from azure.identity import DefaultAzureCredential -import psycopg2 -from pgvector.psycopg2 import register_vector -from openai import AzureOpenAI -import time +from typing import List +import numpy as np +from .search_handler_base import SearchHandlerBase +from ..helpers.azure_postgres_helper import AzurePostgresHelper +from ..common.source_document import SourceDocument -class PostgresSearchClient: - def __init__(self, user: str, host: str, database: str): - self.user = user - self.host = host - self.database = database - self.conn = None +class AzurePostgresHandler(SearchHandlerBase): - async def connect(self): - """ - Create a new database connection. + def __init__(self, env_helper): + super().__init__(env_helper) + self.azure_postgres_helper = AzurePostgresHelper() - The connection parameters can be specified as a string: + def query_search(self, question) -> List[SourceDocument]: + user_input = question + query_embedding = self.azure_postgres_helper.llm_helper.generate_embeddings( + user_input + ) + + embedding_array = np.array(query_embedding) - conn = psycopg2.connect("dbname=test user=postgres password=secret") + conn = self.azure_postgres_helper.connect() + try: + cur = conn.cursor() + cur.execute( + "SELECT * FROM search_indexes ORDER BY content_vector <=> %s LIMIT 5", + (embedding_array,), + ) + search_results = cur.fetchall() + return self._convert_to_source_documents(search_results) + finally: + conn.close() - or using a set of keyword arguments: + def _convert_to_source_documents(self, search_results) -> List[SourceDocument]: + source_documents = [] + for source in search_results: + source_documents.append( + SourceDocument( + id=source.get("id"), + content=source.get("content"), + title=source.get("title"), + source=source.get("source"), + chunk=source.get("chunk"), + offset=source.get("offset"), + page_number=source.get("page_number"), + ) + ) + return source_documents - conn = psycopg2.connect(database="test", user="postgres", password="secret") + def create_search_client(self): + raise NotImplementedError( + "The method create_search_client is not implemented in AzurePostgresHandler." + ) - Or as a mix of both. The basic connection parameters are: + def perform_search(self, filename): + raise NotImplementedError( + "The method perform_search is not implemented in AzurePostgresHandler." + ) - - *dbname*: the database name - - *database*: the database name (only as keyword argument) - - *user*: user name used to authenticate - - *password*: password used to authenticate - - *host*: database host address (defaults to UNIX socket if not provided) - - *port*: connection port number (defaults to 5432 if not provided) - """ - credential = DefaultAzureCredential() - token = credential.get_token( - "https://ossrdbms-aad.database.windows.net/.default" - ).token - # TODO FIX THIS - conn_string = "host=your_postgresql_server.postgres.database.azure.com dbname=your_database " - self.conn = psycopg2.connect(conn_string + " passwor=" + token) + def process_results(self, results): + raise NotImplementedError( + "The method process_results is not implemented in AzurePostgresHandler." + ) - async def get_embeddings( - self, text: str, openai_api_base, openai_api_version, openai_api_key - ): - model_id = "text-embedding-ada-002" - client = AzureOpenAI( - api_version=openai_api_version, - azure_endpoint=openai_api_base, - api_key=openai_api_key, + def get_files(self): + raise NotImplementedError( + "The method get_files is not implemented in AzurePostgresHandler." ) - embedding = ( - client.embeddings.create(input=text, model=model_id).data[0].embedding + def output_results(self, results): + raise NotImplementedError( + "The method output_results is not implemented in AzurePostgresHandler." ) - return embedding - async def create_vector(self): - try: - v_contentVector = self.get_embeddings( - d["content"], openai_api_base, openai_api_version, openai_api_key - ) - except: - time.sleep(30) - v_contentVector = self.get_embeddings( - d["content"], openai_api_base, openai_api_version, openai_api_key - ) - return v_contentVector + def delete_files(self, files): + raise NotImplementedError( + "The method delete_files is not implemented in AzurePostgresHandler." + ) - async def insert_vector(self): - # TODO FIX THIS - self.connect() - cur = self.conn.cursor() - cur.execute( - f"INSERT INTO search_index (id,chunk_id, client_id, content, sourceurl, contentVector) VALUES (%s,%s,%s,%s,%s,%s)", - ( - id, - d["chunk_id"], - d["client_id"], - d["content"], - path.name.split("/")[-1], - v_contentVector, - ), + def search_by_blob_url(self, blob_url): + raise NotImplementedError( + "The method search_by_blob_url is not implemented in AzurePostgresHandler." ) - cur.close() - self.conn.commit() diff --git a/code/backend/batch/utilities/search/search.py b/code/backend/batch/utilities/search/search.py index 6a5eed95e..d1a746a06 100644 --- a/code/backend/batch/utilities/search/search.py +++ b/code/backend/batch/utilities/search/search.py @@ -1,3 +1,5 @@ +from ..search.postgres_search_handler import AzurePostgresHandler +from ..helpers.config.database_type import DatabaseType from ..search.azure_search_handler import AzureSearchHandler from ..search.integrated_vectorization_search_handler import ( IntegratedVectorizationSearchHandler, @@ -10,10 +12,14 @@ class Search: @staticmethod def get_search_handler(env_helper: EnvHelper) -> SearchHandlerBase: - if env_helper.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: - return IntegratedVectorizationSearchHandler(env_helper) + # TODO Since the full workflow for PostgreSQL indexing is not yet complete, you can comment out env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value. + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: + return AzurePostgresHandler(env_helper) else: - return AzureSearchHandler(env_helper) + if env_helper.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: + return IntegratedVectorizationSearchHandler(env_helper) + else: + return AzureSearchHandler(env_helper) @staticmethod def get_source_documents( diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index bdcdc7e3c..37381dd96 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -1,79 +1,87 @@ +from azure.keyvault.secrets import SecretClient from azure.identity import DefaultAzureCredential import psycopg2 -from pgvector.psycopg2 import register_vector +key_vault_name = "kv_to-be-replaced" + + +def get_secrets_from_kv(kv_name, secret_name): + credential = DefaultAzureCredential() + secret_client = SecretClient( + vault_url=f"https://{kv_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 + + +host = get_secrets_from_kv(key_vault_name, "POSTGRESQL-HOST") +user = get_secrets_from_kv(key_vault_name, "POSTGRESQL-USERNAME") +dbname = get_secrets_from_kv(key_vault_name, "POSTGRESQL-DBNAME") # Acquire the access token -credential = DefaultAzureCredential() -token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default").token +cred = DefaultAzureCredential() +access_token = cred.get_token("https://ossrdbms-aad.database.windows.net/.default") -# TODO FIX THIS -conn_string = ( - "host=your_postgresql_server.postgres.database.azure.com dbname=your_database " +# Combine the token with the connection string to establish the connection. +conn_string = "host={0} user={1} dbname={2} password={3}".format( + host, user, dbname, access_token.token ) -conn = psycopg2.connect(conn_string + " password=" + token) +conn = psycopg2.connect(conn_string) cursor = conn.cursor() +# Drop and recreate the conversations table cursor.execute("DROP TABLE IF EXISTS conversations") conn.commit() create_cs_sql = """CREATE TABLE conversations ( id TEXT PRIMARY KEY, + conversation_id TEXT NOT NULL, type TEXT NOT NULL, - created_at TEXT, - updated_at TEXT, + "createdAt" TEXT, + "updatedAt" TEXT, user_id TEXT NOT NULL, title TEXT );""" - cursor.execute(create_cs_sql) conn.commit() -cursor = conn.cursor() - +# Drop and recreate the messages table cursor.execute("DROP TABLE IF EXISTS messages") conn.commit() -create_cs_sql = """CREATE TABLE messages ( +create_ms_sql = """CREATE TABLE messages ( id TEXT PRIMARY KEY, type VARCHAR(50) NOT NULL, - created_at TEXT, - updated_at TEXT, + "createdAt" TEXT, + "updatedAt" TEXT, user_id TEXT NOT NULL, conversation_id TEXT NOT NULL, role VARCHAR(50), content TEXT NOT NULL, feedback TEXT );""" - -cursor.execute(create_cs_sql) +cursor.execute(create_ms_sql) conn.commit() +# Add pg_diskann extension and search_indexes table cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") conn.commit() -# TODO review if this command is necessary for creating the table -# Register the vector type with psycopg2 -register_vector(conn) - cursor.execute("DROP TABLE IF EXISTS search_indexes;") -# Create table to store embeddings and metadata - -table_create_command = """ -CREATE TABLE IF NOT EXISTS search_indexes( - id text, - title text, - chunk integer, - chunk_id text, - offset integer, - page_number integer, - content text, - source text, - metadata text, - content_vector vector(1536) - ); - """ +conn.commit() +table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( + id text, + title text, + chunk integer, + chunk_id text, + "offset" integer, + page_number integer, + content text, + source text, + metadata text, + content_vector vector(1536) +);""" cursor.execute(table_create_command) cursor.close() conn.commit() +conn.close() From fb1e6b2b59ada0eded3795f1f8051cf9a7efe486 Mon Sep 17 00:00:00 2001 From: gpickett <122489228+gpickett@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:55:48 -0800 Subject: [PATCH 030/107] changed allowIps to false --- infra/core/database/postgresdb.bicep | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index 80cf9fc7e..9b35ff200 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -11,8 +11,8 @@ param skuSizeGB int = 32 param dbInstanceType string = 'Standard_B1ms' // param haMode string = 'ZoneRedundant' param availabilityZone string = '1' -param allowAllIPsFirewall bool = true -param allowAzureIPsFirewall bool = true +param allowAllIPsFirewall bool = false +param allowAzureIPsFirewall bool = false @description('PostgreSQL version') @allowed([ '11' From b9e4875ac0c8bf1218a4c6ccc52f2674404ad099 Mon Sep 17 00:00:00 2001 From: gpickett <122489228+gpickett@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:57:11 -0800 Subject: [PATCH 031/107] rebuilt main.json --- infra/main.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infra/main.json b/infra/main.json index 975d1a122..9fe80cee2 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.28.1.47646", - "templateHash": "7340432032010169542" + "templateHash": "2428538844691279334" } }, "parameters": { @@ -861,7 +861,7 @@ "_generator": { "name": "bicep", "version": "0.28.1.47646", - "templateHash": "7073717562657244530" + "templateHash": "6504630696495069188" } }, "parameters": { @@ -904,11 +904,11 @@ }, "allowAllIPsFirewall": { "type": "bool", - "defaultValue": true + "defaultValue": false }, "allowAzureIPsFirewall": { "type": "bool", - "defaultValue": true + "defaultValue": false }, "version": { "type": "string", From c6732610f1be5dfc5e12400b419f84fd8c27ef4e Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Thu, 21 Nov 2024 22:04:48 -0500 Subject: [PATCH 032/107] create scalfold for insert vector to postgress --- .../helpers/azure_postgres_helper.py | 3 + .../helpers/embedders/embedder_factory.py | 11 +- .../helpers/embedders/postgres_embedder.py | 122 ++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 code/backend/batch/utilities/helpers/embedders/postgres_embedder.py diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py index 7a488f3f5..243d0b6cc 100644 --- a/code/backend/batch/utilities/helpers/azure_postgres_helper.py +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -29,3 +29,6 @@ def connect(self): ) conn = psycopg2.connect(conn_string) return conn + + def get_search_client(self): + return self.connect() diff --git a/code/backend/batch/utilities/helpers/embedders/embedder_factory.py b/code/backend/batch/utilities/helpers/embedders/embedder_factory.py index 3a2336b99..d83ead1fe 100644 --- a/code/backend/batch/utilities/helpers/embedders/embedder_factory.py +++ b/code/backend/batch/utilities/helpers/embedders/embedder_factory.py @@ -1,6 +1,8 @@ from ..env_helper import EnvHelper +from ..config.database_type import DatabaseType from ..azure_blob_storage_client import AzureBlobStorageClient from .push_embedder import PushEmbedder +from .postgres_embedder import PostgresEmbedder from .integrated_vectorization_embedder import ( IntegratedVectorizationEmbedder, ) @@ -9,7 +11,10 @@ class EmbedderFactory: @staticmethod def create(env_helper: EnvHelper): - if env_helper.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: - return IntegratedVectorizationEmbedder(env_helper) + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: + return PostgresEmbedder(AzureBlobStorageClient(), env_helper) else: - return PushEmbedder(AzureBlobStorageClient(), env_helper) + if env_helper.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: + return IntegratedVectorizationEmbedder(env_helper) + else: + return PushEmbedder(AzureBlobStorageClient(), env_helper) diff --git a/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py new file mode 100644 index 000000000..a413f734a --- /dev/null +++ b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py @@ -0,0 +1,122 @@ +import hashlib +import json +import logging +from typing import List +from urllib.parse import urlparse + +from ...helpers.llm_helper import LLMHelper +from ...helpers.env_helper import EnvHelper +from ...helpers.azure_postgres_helper import AzurePostgresHandler +from ..azure_blob_storage_client import AzureBlobStorageClient + +from ..config.embedding_config import EmbeddingConfig +from ..config.config_helper import ConfigHelper + +from .embedder_base import EmbedderBase +from ..azure_postgres_helper import AzurePostgresHelper +from ..document_loading_helper import DocumentLoading +from ..document_chunking_helper import DocumentChunking +from ...common.source_document import SourceDocument + +logger = logging.getLogger(__name__) + + +class PostgresEmbedder(EmbedderBase): + def __init__(self, blob_client: AzureBlobStorageClient, env_helper: EnvHelper): + self.env_helper = env_helper + self.llm_helper = LLMHelper() + self.azure_postgres_helper = AzurePostgresHelper() + self.document_loading = DocumentLoading() + self.document_chunking = DocumentChunking() + self.blob_client = blob_client + self.config = ConfigHelper.get_active_config_or_default() + self.embedding_configs = {} + for processor in self.config.document_processors: + ext = processor.document_type.lower() + self.embedding_configs[ext] = processor + + def embed_file(self, source_url: str, file_name: str): + file_extension = file_name.split(".")[-1].lower() + embedding_config = self.embedding_configs.get(file_extension) + self.__embed( + source_url=source_url, + file_extension=file_extension, + embedding_config=embedding_config, + ) + if file_extension != "url": + self.blob_client.upsert_blob_metadata( + file_name, {"embeddings_added": "true"} + ) + + def __embed( + self, source_url: str, file_extension: str, embedding_config: EmbeddingConfig + ): + documents_to_upload: List[SourceDocument] = [] + if ( + embedding_config.use_advanced_image_processing + and file_extension + in self.config.get_advanced_image_processing_image_types() + ): + raise NotImplementedError( + "Advanced image processing is not supported in PostgresEmbedder." + ) + else: + documents: List[SourceDocument] = self.document_loading.load( + source_url, embedding_config.loading + ) + documents = self.document_chunking.chunk( + documents, embedding_config.chunking + ) + + for document in documents: + documents_to_upload.append(self.__convert_to_search_document(document)) + # TODO fix this + # Upload documents (which are chunks) to search index in batches + if documents_to_upload: + search_client = self.azure_postgres_handler.get_search_client() + cur = search_client.cursor() + for d in documents_to_upload: + # SourceDocument + # self.id = id + # self.content = content + # self.source = source + # self.title = title + # self.chunk = chunk + # self.offset = offset + # self.page_number = page_number + # self.chunk_id = chunk_id + + # table + # id text, + # title text, + # chunk integer, + # chunk_id text, + # "offset" integer, + # page_number integer, + # content text, + # source text, + # metadata text, + # content_vector vector(1536)O + # TODO FIX THIS + content_vector = get_embeddings( + d["content"], openai_api_base, openai_api_version, openai_api_key + ) + cur.execute( + f"INSERT INTO search_indexes (id,title,chunk,chunk_id,offset,page_number,content,source,metadata,content_vector) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + ( + d.id, + d.title, + d.chunk, + d.chunk_id, + d.offset, + d.page_number, + d.content, + d.source, + json.dumps("TBD"), + content_vector, + ), + ) + cur.close() + search_client.commit() + else: + logger.warning("No documents to upload.") From 4cc95073b73f403bbb5588d36fe88ae1ac33c5b1 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 22 Nov 2024 17:57:32 +0530 Subject: [PATCH 033/107] Create index and retrieve index changes --- .../helpers/azure_postgres_helper.py | 9 +- .../helpers/embedders/postgres_embedder.py | 104 +++++++++--------- .../search/postgres_search_handler.py | 31 +++--- 3 files changed, 76 insertions(+), 68 deletions(-) diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py index 243d0b6cc..4f0cba31a 100644 --- a/code/backend/batch/utilities/helpers/azure_postgres_helper.py +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -12,8 +12,9 @@ class AzurePostgresHelper: def __init__(self): self.llm_helper = LLMHelper() self.env_helper = EnvHelper() + self.conn = None - def connect(self): + def _create_search_client(self): user = self.env_helper.POSTGRESQL_USER host = self.env_helper.POSTGRESQL_HOST dbname = self.env_helper.POSTGRESQL_DATABASE @@ -27,8 +28,8 @@ def connect(self): conn_string = "host={0} user={1} dbname={2} password={3}".format( host, user, dbname, accessToken.token ) - conn = psycopg2.connect(conn_string) - return conn + self.conn = psycopg2.connect(conn_string) + return self.conn def get_search_client(self): - return self.connect() + return self._create_search_client() diff --git a/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py index a413f734a..74584cd38 100644 --- a/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py +++ b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py @@ -1,12 +1,9 @@ -import hashlib import json import logging from typing import List -from urllib.parse import urlparse from ...helpers.llm_helper import LLMHelper from ...helpers.env_helper import EnvHelper -from ...helpers.azure_postgres_helper import AzurePostgresHandler from ..azure_blob_storage_client import AzureBlobStorageClient from ..config.embedding_config import EmbeddingConfig @@ -70,53 +67,60 @@ def __embed( for document in documents: documents_to_upload.append(self.__convert_to_search_document(document)) - # TODO fix this - # Upload documents (which are chunks) to search index in batches - if documents_to_upload: - search_client = self.azure_postgres_handler.get_search_client() - cur = search_client.cursor() - for d in documents_to_upload: - # SourceDocument - # self.id = id - # self.content = content - # self.source = source - # self.title = title - # self.chunk = chunk - # self.offset = offset - # self.page_number = page_number - # self.chunk_id = chunk_id - # table - # id text, - # title text, - # chunk integer, - # chunk_id text, - # "offset" integer, - # page_number integer, - # content text, - # source text, - # metadata text, - # content_vector vector(1536)O - # TODO FIX THIS - content_vector = get_embeddings( - d["content"], openai_api_base, openai_api_version, openai_api_key - ) - cur.execute( - f"INSERT INTO search_indexes (id,title,chunk,chunk_id,offset,page_number,content,source,metadata,content_vector) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", - ( - d.id, - d.title, - d.chunk, - d.chunk_id, - d.offset, - d.page_number, - d.content, - d.source, - json.dumps("TBD"), - content_vector, - ), - ) - cur.close() - search_client.commit() + if documents_to_upload: + search_client = self.azure_postgres_helper.get_search_client() + try: + cur = search_client.cursor() + for d in documents_to_upload: + cur.execute( + """ + INSERT INTO search_indexes ( + id, title, chunk, chunk_id, "offset", page_number, + content, source, metadata, content_vector + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + d["id"], + d["title"], + d["chunk"], + d["chunk_id"], + d["offset"], + d["page_number"], + d["content"], + d["source"], + d["metadata"], + d["content_vector"], + ), + ) + cur.close() + search_client.commit() + finally: + search_client.close() else: logger.warning("No documents to upload.") + + def __convert_to_search_document(self, document: SourceDocument): + embedded_content = self.llm_helper.generate_embeddings(document.content) + metadata = { + "id": document.id, + "source": document.source, + "title": document.title, + "chunk": document.chunk, + "chunk_id": document.chunk_id, + "offset": document.offset, + "page_number": document.page_number, + } + return { + "id": document.id, + "content": document.content, + "content_vector": embedded_content, + "metadata": json.dumps(metadata), + "title": document.title, + "source": document.source, + "chunk": document.chunk, + "chunk_id": document.chunk_id, + "offset": document.offset, + "page_number": document.page_number, + } diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py index 3d78b9108..f243e111b 100644 --- a/code/backend/batch/utilities/search/postgres_search_handler.py +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -9,8 +9,8 @@ class AzurePostgresHandler(SearchHandlerBase): def __init__(self, env_helper): - super().__init__(env_helper) self.azure_postgres_helper = AzurePostgresHelper() + super().__init__(env_helper) def query_search(self, question) -> List[SourceDocument]: user_input = question @@ -18,13 +18,18 @@ def query_search(self, question) -> List[SourceDocument]: user_input ) - embedding_array = np.array(query_embedding) + embedding_array = np.array(query_embedding).tolist() # Convert to a list - conn = self.azure_postgres_helper.connect() + conn = self.azure_postgres_helper.get_search_client() try: cur = conn.cursor() cur.execute( - "SELECT * FROM search_indexes ORDER BY content_vector <=> %s LIMIT 5", + """ + SELECT id, title, chunk, "offset", page_number, content, source + FROM search_indexes + ORDER BY content_vector <=> %s::vector + LIMIT 3 + """, (embedding_array,), ) search_results = cur.fetchall() @@ -37,21 +42,19 @@ def _convert_to_source_documents(self, search_results) -> List[SourceDocument]: for source in search_results: source_documents.append( SourceDocument( - id=source.get("id"), - content=source.get("content"), - title=source.get("title"), - source=source.get("source"), - chunk=source.get("chunk"), - offset=source.get("offset"), - page_number=source.get("page_number"), + id=source[0], + title=source[1], + chunk=source[2], + offset=source[3], + page_number=source[4], + content=source[5], + source=source[6], ) ) return source_documents def create_search_client(self): - raise NotImplementedError( - "The method create_search_client is not implemented in AzurePostgresHandler." - ) + return self.azure_postgres_helper.get_search_client() def perform_search(self, filename): raise NotImplementedError( From 942220b138c10f4682a830a4b62dc732ad4734f6 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 22 Nov 2024 19:23:55 +0530 Subject: [PATCH 034/107] format fix --- .../helpers/azure_postgres_helper.py | 118 +++++++++++++++--- .../helpers/embedders/postgres_embedder.py | 30 +---- .../search/postgres_search_handler.py | 26 ++-- 3 files changed, 109 insertions(+), 65 deletions(-) diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py index 4f0cba31a..0ddb201b9 100644 --- a/code/backend/batch/utilities/helpers/azure_postgres_helper.py +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -1,35 +1,117 @@ import logging import psycopg2 +from psycopg2.extras import execute_values +from azure.identity import DefaultAzureCredential from .llm_helper import LLMHelper from .env_helper import EnvHelper -from azure.identity import DefaultAzureCredential logger = logging.getLogger(__name__) class AzurePostgresHelper: - def __init__(self): self.llm_helper = LLMHelper() self.env_helper = EnvHelper() self.conn = None def _create_search_client(self): - user = self.env_helper.POSTGRESQL_USER - host = self.env_helper.POSTGRESQL_HOST - dbname = self.env_helper.POSTGRESQL_DATABASE - cred = DefaultAzureCredential() - # Acquire the access token - accessToken = cred.get_token( - "https://ossrdbms-aad.database.windows.net/.default" - ) - - # Combine the token with the connection string to establish the connection. - conn_string = "host={0} user={1} dbname={2} password={3}".format( - host, user, dbname, accessToken.token - ) - self.conn = psycopg2.connect(conn_string) - return self.conn + """ + Establishes a connection to Azure PostgreSQL using AAD authentication. + """ + try: + user = self.env_helper.POSTGRESQL_USER + host = self.env_helper.POSTGRESQL_HOST + dbname = self.env_helper.POSTGRESQL_DATABASE + + # Acquire the access token + credential = DefaultAzureCredential() + access_token = credential.get_token( + "https://ossrdbms-aad.database.windows.net/.default" + ) + + # Use the token in the connection string + conn_string = ( + f"host={host} user={user} dbname={dbname} password={access_token.token}" + ) + self.conn = psycopg2.connect(conn_string) + logger.info("Connected to Azure PostgreSQL successfully.") + return self.conn + except Exception: + logger.error("Error establishing a connection to PostgreSQL", exc_info=True) + raise def get_search_client(self): - return self._create_search_client() + """ + Provides a reusable database connection. + """ + if self.conn is None or self.conn.closed != 0: # Ensure the connection is open + self.conn = self._create_search_client() + return self.conn + + def get_search_indexes(self, embedding_array): + """ + Fetches search indexes from PostgreSQL based on an embedding vector. + """ + conn = self.get_search_client() + try: + with conn.cursor() as cur: + cur.execute( + """ + SELECT id, title, chunk, "offset", page_number, content, source + FROM search_indexes + ORDER BY content_vector <=> %s::vector + LIMIT 3 + """, + (embedding_array,), + ) + search_results = cur.fetchall() + logger.info(f"Retrieved {len(search_results)} search results.") + return search_results + except Exception: + logger.error("Error executing search query", exc_info=True) + raise + finally: + conn.close() + + def create_search_indexes(self, documents_to_upload): + """ + Inserts documents into the `search_indexes` table in batch mode. + """ + conn = self.get_search_client() + try: + with conn.cursor() as cur: + data_to_insert = [ + ( + d["id"], + d["title"], + d["chunk"], + d["chunk_id"], + d["offset"], + d["page_number"], + d["content"], + d["source"], + d["metadata"], + d["content_vector"], + ) + for d in documents_to_upload + ] + + # Batch insert using execute_values for efficiency + query = """ + INSERT INTO search_indexes ( + id, title, chunk, chunk_id, "offset", page_number, + content, source, metadata, content_vector + ) VALUES %s + """ + execute_values(cur, query, data_to_insert) + logger.info( + f"Inserted {len(documents_to_upload)} documents successfully." + ) + + conn.commit() # Commit the transaction + except Exception: + logger.error("Error during index creation", exc_info=True) + conn.rollback() # Roll back transaction on error + raise + finally: + conn.close() diff --git a/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py index 74584cd38..7a6739484 100644 --- a/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py +++ b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py @@ -69,35 +69,7 @@ def __embed( documents_to_upload.append(self.__convert_to_search_document(document)) if documents_to_upload: - search_client = self.azure_postgres_helper.get_search_client() - try: - cur = search_client.cursor() - for d in documents_to_upload: - cur.execute( - """ - INSERT INTO search_indexes ( - id, title, chunk, chunk_id, "offset", page_number, - content, source, metadata, content_vector - ) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, - ( - d["id"], - d["title"], - d["chunk"], - d["chunk_id"], - d["offset"], - d["page_number"], - d["content"], - d["source"], - d["metadata"], - d["content_vector"], - ), - ) - cur.close() - search_client.commit() - finally: - search_client.close() + self.azure_postgres_helper.create_search_indexes(documents_to_upload) else: logger.warning("No documents to upload.") diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py index f243e111b..431f005f6 100644 --- a/code/backend/batch/utilities/search/postgres_search_handler.py +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -18,24 +18,11 @@ def query_search(self, question) -> List[SourceDocument]: user_input ) - embedding_array = np.array(query_embedding).tolist() # Convert to a list - - conn = self.azure_postgres_helper.get_search_client() - try: - cur = conn.cursor() - cur.execute( - """ - SELECT id, title, chunk, "offset", page_number, content, source - FROM search_indexes - ORDER BY content_vector <=> %s::vector - LIMIT 3 - """, - (embedding_array,), - ) - search_results = cur.fetchall() - return self._convert_to_source_documents(search_results) - finally: - conn.close() + embedding_array = np.array(query_embedding).tolist() + + search_results = self.azure_postgres_helper.get_search_indexes(embedding_array) + + return self._convert_to_source_documents(search_results) def _convert_to_source_documents(self, search_results) -> List[SourceDocument]: source_documents = [] @@ -56,6 +43,9 @@ def _convert_to_source_documents(self, search_results) -> List[SourceDocument]: def create_search_client(self): return self.azure_postgres_helper.get_search_client() + def create_search_indexes(self, documents_to_upload): + return self.azure_postgres_helper.create_search_indexes(documents_to_upload) + def perform_search(self, filename): raise NotImplementedError( "The method perform_search is not implemented in AzurePostgresHandler." From 9ab4539ad8746d263d2cb3f4c3129200f1b39060 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Fri, 22 Nov 2024 15:42:34 -0500 Subject: [PATCH 035/107] Update poetry.lock --- poetry.lock | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index c321375ed..c3042f502 100644 --- a/poetry.lock +++ b/poetry.lock @@ -355,6 +355,72 @@ files = [ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, + {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"}, + {file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"}, + {file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"}, + {file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"}, + {file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, + {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, + {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34"}, + {file = "asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4"}, + {file = "asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547"}, + {file = "asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a"}, + {file = "asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} + +[package.extras] +docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] +gssauth = ["gssapi", "sspilib"] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] + [[package]] name = "attrs" version = "23.2.0" @@ -4150,6 +4216,20 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "pgvector" +version = "0.3.6" +description = "pgvector support for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pgvector-0.3.6-py3-none-any.whl", hash = "sha256:f6c269b3c110ccb7496bac87202148ed18f34b390a0189c783e351062400a75a"}, + {file = "pgvector-0.3.6.tar.gz", hash = "sha256:31d01690e6ea26cea8a633cde5f0f55f5b246d9c8292d68efdef8c22ec994ade"}, +] + +[package.dependencies] +numpy = "*" + [[package]] name = "pillow" version = "10.4.0" @@ -4553,6 +4633,82 @@ files = [ [package.extras] test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -6627,4 +6783,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "826226f49f211954e1a565360e48f0e655807b7e7f370780bd1fed30f2bccac4" +content-hash = "2f68e50e5cc37578d95c47708f24e8b1ee8f3c2d20481d14514a8aead0eaf078" From 4d342968e0ebe60834c8bc19de5fd8db878a5fcb Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Fri, 22 Nov 2024 16:08:16 -0500 Subject: [PATCH 036/107] update type with enums --- .../batch/utilities/chat_history/database_factory.py | 6 +++--- code/backend/batch/utilities/helpers/env_helper.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/code/backend/batch/utilities/chat_history/database_factory.py b/code/backend/batch/utilities/chat_history/database_factory.py index 91eb23338..57232ff9a 100644 --- a/code/backend/batch/utilities/chat_history/database_factory.py +++ b/code/backend/batch/utilities/chat_history/database_factory.py @@ -3,13 +3,13 @@ from .cosmosdb import CosmosConversationClient from .postgresdbservice import PostgresConversationClient from azure.identity import DefaultAzureCredential - +from ..helpers.config.database_type import DatabaseType class DatabaseFactory: @staticmethod def get_conversation_client(): env_helper: EnvHelper = EnvHelper() - if env_helper.DATABASE_TYPE == "CosmosDB": + if env_helper.DATABASE_TYPE == DatabaseType.COSMOS_DB.value: cosmos_endpoint = ( f"https://{env_helper.AZURE_COSMOSDB_ACCOUNT}.documents.azure.com:443/" ) @@ -25,7 +25,7 @@ def get_conversation_client(): container_name=env_helper.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER, enable_message_feedback=env_helper.AZURE_COSMOSDB_ENABLE_FEEDBACK, ) - elif env_helper.DATABASE_TYPE == "PostgreSQL": + elif env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: return PostgresConversationClient( user=env_helper.POSTGRESQL_USER, host=env_helper.POSTGRESQL_HOST, diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index d46871c17..c6d586267 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -5,7 +5,7 @@ from dotenv import load_dotenv from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.keyvault.secrets import SecretClient - +from ..database_type import DatabaseType logger = logging.getLogger(__name__) @@ -297,7 +297,7 @@ def __load_config(self, **kwargs) -> None: # Set default values based on DATABASE_TYPE self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "").strip() or "CosmosDB" # Cosmos DB configuration - if self.DATABASE_TYPE == "CosmosDB": + if self.DATABASE_TYPE == DatabaseType.COSMOS_DB.value: azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "") self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "") @@ -311,7 +311,7 @@ def __load_config(self, **kwargs) -> None: os.getenv("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true" ) # PostgreSQL configuration - elif self.DATABASE_TYPE == "PostgreSQL": + elif self.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") self.POSTGRESQL_USER = azure_postgresql_info.get("user", "") self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "") From d0771835a23f19b76a0fda46dca0c615aea5f6f3 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Fri, 22 Nov 2024 17:56:11 -0500 Subject: [PATCH 037/107] Update create_postgres_tables.py --- scripts/data_scripts/create_postgres_tables.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 37381dd96..4c393f1d4 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -79,9 +79,14 @@ def get_secrets_from_kv(kv_name, secret_name): content text, source text, metadata text, - content_vector vector(1536) + content_vector public.vector(1536) );""" + cursor.execute(table_create_command) -cursor.close() conn.commit() + +cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") +conn.commit() + +cursor.close() conn.close() From a997f89c03d556a8f83abde0a0e5efdef77b97eb Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 25 Nov 2024 10:21:29 +0530 Subject: [PATCH 038/107] Delete PostgreSQL Search Indexes When Files Are Deleted --- code/backend/api/chat_history.py | 142 +++++++++++------- .../chat_history/database_factory.py | 24 ++- .../chat_history/postgresdbservice.py | 31 ++-- .../helpers/azure_postgres_helper.py | 97 ++++++++++-- .../batch/utilities/helpers/env_helper.py | 5 +- .../search/postgres_search_handler.py | 46 ++++-- code/backend/pages/03_Delete_Data.py | 6 +- 7 files changed, 252 insertions(+), 99 deletions(-) diff --git a/code/backend/api/chat_history.py b/code/backend/api/chat_history.py index 873466b52..8a86b8119 100644 --- a/code/backend/api/chat_history.py +++ b/code/backend/api/chat_history.py @@ -66,17 +66,19 @@ async def list_conversations(): await conversation_client.connect() try: - # Fetch conversations conversations = await conversation_client.get_conversations( user_id, offset=offset, limit=25 ) if not isinstance(conversations, list): return ( jsonify({"error": f"No conversations for {user_id} were found"}), - 400, + 404, ) return jsonify(conversations), 200 + except Exception as e: + logger.exception(f"Error fetching conversations: {e}") + raise finally: await conversation_client.close() @@ -135,9 +137,13 @@ async def rename_conversation(): conversation ) return jsonify(updated_conversation), 200 + except Exception as e: + logger.exception( + f"Error updating conversation: user_id={user_id}, conversation_id={conversation_id}, error={e}" + ) + raise finally: await conversation_client.close() - except Exception as e: logger.exception(f"Exception in /history/rename: {e}") return jsonify({"error": "Error while renaming conversation"}), 500 @@ -202,6 +208,11 @@ async def get_conversation(): jsonify({"conversation_id": conversation_id, "messages": messages}), 200, ) + except Exception as e: + logger.exception( + f"Error fetching conversation or messages: user_id={user_id}, conversation_id={conversation_id}, error={e}" + ) + raise finally: await conversation_client.close() @@ -257,6 +268,11 @@ async def delete_conversation(): ), 200, ) + except Exception as e: + logger.exception( + f"Error deleting conversation: user_id={user_id}, conversation_id={conversation_id}, error={e}" + ) + raise finally: await conversation_client.close() @@ -323,7 +339,11 @@ async def delete_all_conversations(): ), 200, ) - + except Exception as e: + logger.exception( + f"Error deleting all conversations for user {user_id}: {e}" + ) + raise finally: await conversation_client.close() @@ -358,69 +378,75 @@ async def update_conversation(): if not conversation_client: return jsonify({"error": "Database not available"}), 500 await conversation_client.connect() - - # Get or create the conversation - conversation = await conversation_client.get_conversation( - user_id, conversation_id - ) - if not conversation: - title = await generate_title(messages) - conversation = await conversation_client.create_conversation( - user_id=user_id, conversation_id=conversation_id, title=title + try: + # Get or create the conversation + conversation = await conversation_client.get_conversation( + user_id, conversation_id ) + if not conversation: + title = await generate_title(messages) + conversation = await conversation_client.create_conversation( + user_id=user_id, conversation_id=conversation_id, title=title + ) - # Process and save user and assistant messages - # Process user message - if messages[0]["role"] == "user": - user_message = next( - (msg for msg in reversed(messages) if msg["role"] == "user"), None - ) - if not user_message: - return jsonify({"error": "User message not found"}), 400 - - created_message = await conversation_client.create_message( - uuid=str(uuid4()), - conversation_id=conversation_id, - user_id=user_id, - input_message=user_message, - ) - if created_message == "Conversation not found": - return jsonify({"error": "Conversation not found"}), 400 + # Process and save user and assistant messages + # Process user message + if messages[0]["role"] == "user": + user_message = next( + (msg for msg in reversed(messages) if msg["role"] == "user"), None + ) + if not user_message: + return jsonify({"error": "User message not found"}), 400 - # Process assistant and tool messages if available - if messages[-1]["role"] == "assistant": - if len(messages) > 1 and messages[-2].get("role") == "tool": - # Write the tool message first if it exists + created_message = await conversation_client.create_message( + uuid=str(uuid4()), + conversation_id=conversation_id, + user_id=user_id, + input_message=user_message, + ) + if created_message == "Conversation not found": + return jsonify({"error": "Conversation not found"}), 400 + + # Process assistant and tool messages if available + if messages[-1]["role"] == "assistant": + if len(messages) > 1 and messages[-2].get("role") == "tool": + # Write the tool message first if it exists + await conversation_client.create_message( + uuid=str(uuid4()), + conversation_id=conversation_id, + user_id=user_id, + input_message=messages[-2], + ) + # Write the assistant message await conversation_client.create_message( uuid=str(uuid4()), conversation_id=conversation_id, user_id=user_id, - input_message=messages[-2], + input_message=messages[-1], ) - # Write the assistant message - await conversation_client.create_message( - uuid=str(uuid4()), - conversation_id=conversation_id, - user_id=user_id, - input_message=messages[-1], - ) - else: - return jsonify({"error": "No assistant message found"}), 400 + else: + return jsonify({"error": "No assistant message found"}), 400 - await conversation_client.close() - return ( - jsonify( - { - "success": True, - "data": { - "title": conversation["title"], - "date": conversation["updatedAt"], - "conversation_id": conversation["id"], - }, - } - ), - 200, - ) + return ( + jsonify( + { + "success": True, + "data": { + "title": conversation["title"], + "date": conversation["updatedAt"], + "conversation_id": conversation["id"], + }, + } + ), + 200, + ) + except Exception as e: + logger.exception( + f"Error updating conversation or messages: user_id={user_id}, conversation_id={conversation_id}, error={e}" + ) + raise + finally: + await conversation_client.close() except Exception as e: logger.exception(f"Exception in /history/update: {e}") diff --git a/code/backend/batch/utilities/chat_history/database_factory.py b/code/backend/batch/utilities/chat_history/database_factory.py index 57232ff9a..980c2cf82 100644 --- a/code/backend/batch/utilities/chat_history/database_factory.py +++ b/code/backend/batch/utilities/chat_history/database_factory.py @@ -5,11 +5,22 @@ from azure.identity import DefaultAzureCredential from ..helpers.config.database_type import DatabaseType + class DatabaseFactory: @staticmethod def get_conversation_client(): env_helper: EnvHelper = EnvHelper() - if env_helper.DATABASE_TYPE == DatabaseType.COSMOS_DB.value: + + if env_helper.DATABASE_TYPE == DatabaseType.COSMOSDB.value: + DatabaseFactory._validate_env_vars( + [ + "AZURE_COSMOSDB_ACCOUNT", + "AZURE_COSMOSDB_DATABASE", + "AZURE_COSMOSDB_CONVERSATIONS_CONTAINER", + ], + env_helper, + ) + cosmos_endpoint = ( f"https://{env_helper.AZURE_COSMOSDB_ACCOUNT}.documents.azure.com:443/" ) @@ -26,6 +37,11 @@ def get_conversation_client(): enable_message_feedback=env_helper.AZURE_COSMOSDB_ENABLE_FEEDBACK, ) elif env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: + DatabaseFactory._validate_env_vars( + ["POSTGRESQL_USER", "POSTGRESQL_HOST", "POSTGRESQL_DATABASE"], + env_helper, + ) + return PostgresConversationClient( user=env_helper.POSTGRESQL_USER, host=env_helper.POSTGRESQL_HOST, @@ -35,3 +51,9 @@ def get_conversation_client(): raise ValueError( "Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'." ) + + @staticmethod + def _validate_env_vars(required_vars, env_helper): + for var in required_vars: + if not getattr(env_helper, var, None): + raise ValueError(f"Environment variable {var} is required.") diff --git a/code/backend/batch/utilities/chat_history/postgresdbservice.py b/code/backend/batch/utilities/chat_history/postgresdbservice.py index 97dde920e..a758bb20c 100644 --- a/code/backend/batch/utilities/chat_history/postgresdbservice.py +++ b/code/backend/batch/utilities/chat_history/postgresdbservice.py @@ -1,9 +1,12 @@ +import logging import asyncpg from datetime import datetime, timezone from azure.identity import DefaultAzureCredential from .database_client_base import DatabaseClientBase +logger = logging.getLogger(__name__) + class PostgresConversationClient(DatabaseClientBase): @@ -17,18 +20,22 @@ def __init__( self.conn = None async def connect(self): - credential = DefaultAzureCredential() - token = credential.get_token( - "https://ossrdbms-aad.database.windows.net/.default" - ).token - self.conn = await asyncpg.connect( - user=self.user, - host=self.host, - database=self.database, - password=token, - port=5432, - ssl="require", - ) + try: + credential = DefaultAzureCredential() + token = credential.get_token( + "https://ossrdbms-aad.database.windows.net/.default" + ).token + self.conn = await asyncpg.connect( + user=self.user, + host=self.host, + database=self.database, + password=token, + port=5432, + ssl="require", + ) + except Exception as e: + logger.error("Failed to connect to PostgreSQL: %s", e) + raise async def close(self): if self.conn: diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py index 0ddb201b9..d1ac78afc 100644 --- a/code/backend/batch/utilities/helpers/azure_postgres_helper.py +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -1,6 +1,6 @@ import logging import psycopg2 -from psycopg2.extras import execute_values +from psycopg2.extras import execute_values, RealDictCursor from azure.identity import DefaultAzureCredential from .llm_helper import LLMHelper from .env_helper import EnvHelper @@ -36,8 +36,8 @@ def _create_search_client(self): self.conn = psycopg2.connect(conn_string) logger.info("Connected to Azure PostgreSQL successfully.") return self.conn - except Exception: - logger.error("Error establishing a connection to PostgreSQL", exc_info=True) + except Exception as e: + logger.error(f"Error establishing a connection to PostgreSQL: {e}") raise def get_search_client(self): @@ -54,7 +54,7 @@ def get_search_indexes(self, embedding_array): """ conn = self.get_search_client() try: - with conn.cursor() as cur: + with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT id, title, chunk, "offset", page_number, content, source @@ -67,8 +67,8 @@ def get_search_indexes(self, embedding_array): search_results = cur.fetchall() logger.info(f"Retrieved {len(search_results)} search results.") return search_results - except Exception: - logger.error("Error executing search query", exc_info=True) + except Exception as e: + logger.error(f"Error executing search query: {e}") raise finally: conn.close() @@ -79,7 +79,7 @@ def create_search_indexes(self, documents_to_upload): """ conn = self.get_search_client() try: - with conn.cursor() as cur: + with conn.cursor(cursor_factory=RealDictCursor) as cur: data_to_insert = [ ( d["id"], @@ -109,9 +109,88 @@ def create_search_indexes(self, documents_to_upload): ) conn.commit() # Commit the transaction - except Exception: - logger.error("Error during index creation", exc_info=True) + except Exception as e: + logger.error(f"Error during index creation: {e}") conn.rollback() # Roll back transaction on error raise finally: conn.close() + + def get_files(self): + """ + Fetches distinct titles from the PostgreSQL database. + + Returns: + list[dict] or None: A list of dictionaries (each with a single key 'title') + or None if no titles are found or an error occurs. + """ + conn = self.get_search_client() + try: + # Using a cursor to execute the query + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + query = """ + SELECT id, title + FROM search_indexes + WHERE title IS NOT NULL + ORDER BY title; + """ + cursor.execute(query) + # Fetch all results + results = cursor.fetchall() + # Return results or None if empty + return results if results else None + except psycopg2.Error as db_err: + logger.error(f"Database error while fetching titles: {db_err}") + raise + except Exception as e: + logger.error(f"Unexpected error while fetching titles: {e}") + raise + finally: + conn.close() + + def delete_documents(self, ids_to_delete): + """ + Deletes documents from the PostgreSQL database based on the provided ids. + + Args: + ids_to_delete (list): A list of document IDs to delete. + + Returns: + int: The number of deleted rows. + """ + conn = self.get_search_client() + try: + if not ids_to_delete: + logger.warning("No IDs provided for deletion.") + return 0 + + # Using a cursor to execute the query + with conn.cursor() as cursor: + # Construct the DELETE query with the list of ids_to_delete + query = """ + DELETE FROM search_indexes + WHERE id = ANY(%s) + """ + # Extract the 'id' values from the list of dictionaries (ids_to_delete) + ids_to_delete_values = [item["id"] for item in ids_to_delete] + + # Execute the query, passing the list of IDs as a parameter + cursor.execute(query, (ids_to_delete_values,)) + + # Commit the transaction + conn.commit() + + # Return the number of deleted rows + deleted_rows = cursor.rowcount + logger.info(f"Deleted {deleted_rows} documents.") + return deleted_rows + except psycopg2.Error as db_err: + logger.error(f"Database error while deleting documents: {db_err}") + conn.rollback() + raise + except Exception as e: + logger.error(f"Unexpected error while deleting documents: {e}") + conn.rollback() + raise + finally: + conn.close() diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index c6d586267..93838c56a 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -5,7 +5,8 @@ from dotenv import load_dotenv from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.keyvault.secrets import SecretClient -from ..database_type import DatabaseType +from ..helpers.config.database_type import DatabaseType + logger = logging.getLogger(__name__) @@ -297,7 +298,7 @@ def __load_config(self, **kwargs) -> None: # Set default values based on DATABASE_TYPE self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "").strip() or "CosmosDB" # Cosmos DB configuration - if self.DATABASE_TYPE == DatabaseType.COSMOS_DB.value: + if self.DATABASE_TYPE == DatabaseType.COSMOSDB.value: azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "") self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "") diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py index 431f005f6..58c6aaa46 100644 --- a/code/backend/batch/utilities/search/postgres_search_handler.py +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -29,13 +29,13 @@ def _convert_to_source_documents(self, search_results) -> List[SourceDocument]: for source in search_results: source_documents.append( SourceDocument( - id=source[0], - title=source[1], - chunk=source[2], - offset=source[3], - page_number=source[4], - content=source[5], - source=source[6], + id=source["id"], + title=source["title"], + chunk=source["chunk"], + offset=source["offset"], + page_number=source["page_number"], + content=source["content"], + source=source["source"], ) ) return source_documents @@ -57,19 +57,33 @@ def process_results(self, results): ) def get_files(self): - raise NotImplementedError( - "The method get_files is not implemented in AzurePostgresHandler." - ) + results = self.azure_postgres_helper.get_files() + if results is None or len(results) == 0: + return [] + return results def output_results(self, results): - raise NotImplementedError( - "The method output_results is not implemented in AzurePostgresHandler." - ) + files = {} + for result in results: + id = result["id"] + filename = result["title"] + if filename in files: + files[filename].append(id) + else: + files[filename] = [id] + + return files def delete_files(self, files): - raise NotImplementedError( - "The method delete_files is not implemented in AzurePostgresHandler." - ) + ids_to_delete = [] + files_to_delete = [] + + for filename, ids in files.items(): + files_to_delete.append(filename) + ids_to_delete += [{"id": id} for id in ids] + self.azure_postgres_helper.delete_documents(ids_to_delete) + + return ", ".join(files_to_delete) def search_by_blob_url(self, blob_url): raise NotImplementedError( diff --git a/code/backend/pages/03_Delete_Data.py b/code/backend/pages/03_Delete_Data.py index b92cf303c..98e086c49 100644 --- a/code/backend/pages/03_Delete_Data.py +++ b/code/backend/pages/03_Delete_Data.py @@ -46,7 +46,11 @@ def load_css(file_path): search_handler = Search.get_search_handler(env_helper) results = search_handler.get_files() - if results is None or results.get_count() == 0: + if ( + results is None + or (hasattr(results, "get_count") and results.get_count() == 0) + or len(results) == 0 + ): st.info("No files to delete") st.stop() else: From fa2276d6c19c939337b01cf93fe10ae17cc10524 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 26 Nov 2024 00:33:09 +0530 Subject: [PATCH 039/107] Set up a limit for postgres query results using an environment variable --- .../batch/utilities/helpers/azure_postgres_helper.py | 7 +++++-- code/backend/batch/utilities/helpers/env_helper.py | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py index d1ac78afc..065375579 100644 --- a/code/backend/batch/utilities/helpers/azure_postgres_helper.py +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -60,9 +60,12 @@ def get_search_indexes(self, embedding_array): SELECT id, title, chunk, "offset", page_number, content, source FROM search_indexes ORDER BY content_vector <=> %s::vector - LIMIT 3 + LIMIT %s """, - (embedding_array,), + ( + embedding_array, + self.env_helper.AZURE_POSTGRES_SEARCH_TOP_K, + ), ) search_results = cur.fetchall() logger.info(f"Retrieved {len(search_results)} search results.") diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index 93838c56a..eb897d878 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -313,6 +313,9 @@ def __load_config(self, **kwargs) -> None: ) # PostgreSQL configuration elif self.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: + self.AZURE_POSTGRE_SEARCH_TOP_K = self.get_env_var_int( + "AZURE_POSTGRES_SEARCH_TOP_K", 5 + ) azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") self.POSTGRESQL_USER = azure_postgresql_info.get("user", "") self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "") From ab37eae62fe3899de1d691bf35fc31669f831d60 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Tue, 26 Nov 2024 18:15:31 +0530 Subject: [PATCH 040/107] Code fix: Deploymen issue on adding Postgres details to keyvault --- infra/app/storekeys.bicep | 6 +- infra/core/database/postgresdb.bicep | 3 +- infra/main.bicep | 10 +- infra/main.json | 347 ++++++++++++++------------- 4 files changed, 191 insertions(+), 175 deletions(-) diff --git a/infra/app/storekeys.bicep b/infra/app/storekeys.bicep index dcf3c7309..b3ae8b480 100644 --- a/infra/app/storekeys.bicep +++ b/infra/app/storekeys.bicep @@ -10,6 +10,8 @@ param computerVisionName string = '' param postgresServerName string = '' // PostgreSQL server name param postgresDatabaseName string = 'postgres' // Default database name param postgresInfoName string = 'AZURE-POSTGRESQL-INFO' // Secret name for PostgreSQL info +param postgresDatabaseAdminUserName string = '' +param postgresDatabaseAdminPassword string = '' param storageAccountKeyName string = 'AZURE-STORAGE-ACCOUNT-KEY' param openAIKeyName string = 'AZURE-OPENAI-API-KEY' param searchKeyName string = 'AZURE-SEARCH-KEY' @@ -106,10 +108,10 @@ resource postgresInfoSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = if properties: { value: postgresServerName != '' ? string({ - user: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.DBforPostgreSQL/flexibleServers', postgresServerName), '2022-12-01').username + user: postgresDatabaseAdminUserName dbname: postgresDatabaseName host: '${postgresServerName}.postgres.database.azure.com' - password: listKeys(resourceId(subscription().subscriptionId, rgName, 'Microsoft.DBforPostgreSQL/flexibleServers', postgresServerName), '2022-12-01').password + password: postgresDatabaseAdminPassword }) : '' } diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index 9b35ff200..a9c39fa0a 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -101,7 +101,8 @@ resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configuration output postgresDbOutput object = { - postgreSQLServerName: '${serverName_resource.name}.postgres.database.azure.com' + postgresSQLName: serverName_resource.name + postgreSQLServerName: serverName_resource.name postgreSQLDatabaseName: 'postgres' postgreSQLDbUser: administratorLogin postgreSQLDbPwd: administratorLoginPassword diff --git a/infra/main.bicep b/infra/main.bicep index 096112655..0846d0776 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -509,6 +509,8 @@ module storekeys './app/storekeys.bicep' = if (useKeyVault) { cosmosAccountName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosAccountName : '' postgresServerName: databaseType == 'postgres' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName : '' postgresDatabaseName: databaseType == 'postgres' ? 'postgres' : '' + postgresDatabaseAdminUserName: databaseType == 'postgres' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser : '' + postgresDatabaseAdminPassword: databaseType == 'postgres' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbPwd : '' rgName: resourceGroupName } } @@ -550,13 +552,13 @@ module hostingplan './core/host/appserviceplan.bicep' = { } var azureCosmosDBInfo = string({ - accountName: cosmosDBModule.outputs.cosmosOutput.cosmosAccountName - databaseName: cosmosDBModule.outputs.cosmosOutput.cosmosDatabaseName - containerName: cosmosDBModule.outputs.cosmosOutput.cosmosContainerName + accountName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosAccountName : '' + databaseName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosDatabaseName : '' + containerName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosContainerName : '' }) var azurePostgresDBInfo = string({ - serverName: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + serverName: '${postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName}.postgres.database.azure.com' databaseName: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName userName: postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser password: postgresDBModule.outputs.postgresDbOutput.postgreSQLDbPwd diff --git a/infra/main.json b/infra/main.json index 9fe80cee2..466d4c68c 100644 --- a/infra/main.json +++ b/infra/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "2428538844691279334" + "version": "0.30.23.60470", + "templateHash": "16603120665121801714" } }, "parameters": { @@ -700,8 +700,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "16333775410276024912" + "version": "0.30.23.60470", + "templateHash": "14453122839528928942" } }, "parameters": { @@ -860,8 +860,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "6504630696495069188" + "version": "0.30.23.60470", + "templateHash": "17825575027061010483" } }, "parameters": { @@ -1002,7 +1002,8 @@ "postgresDbOutput": { "type": "object", "value": { - "postgreSQLServerName": "[format('{0}.postgres.database.azure.com', parameters('serverName'))]", + "postgresSQLName": "[parameters('serverName')]", + "postgreSQLServerName": "[parameters('serverName')]", "postgreSQLDatabaseName": "postgres", "postgreSQLDbUser": "[parameters('administratorLogin')]", "postgreSQLDbPwd": "[parameters('administratorLoginPassword')]", @@ -1043,8 +1044,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "3615364066329169756" + "version": "0.30.23.60470", + "templateHash": "12121357715793816510" }, "description": "Creates an Azure Key Vault." }, @@ -1136,8 +1137,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "7514893090820700577" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1291,8 +1292,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "7514893090820700577" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1440,8 +1441,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1509,8 +1510,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1578,8 +1579,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1647,8 +1648,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1720,8 +1721,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "7514893090820700577" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1878,6 +1879,8 @@ "cosmosAccountName": "[if(equals(parameters('databaseType'), 'cosmos'), createObject('value', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName), createObject('value', ''))]", "postgresServerName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), createObject('value', ''))]", "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', 'postgres'), createObject('value', ''))]", + "postgresDatabaseAdminUserName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser), createObject('value', ''))]", + "postgresDatabaseAdminPassword": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd), createObject('value', ''))]", "rgName": { "value": "[variables('resourceGroupName')]" } @@ -1888,8 +1891,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "11353489804824973855" + "version": "0.30.23.60470", + "templateHash": "9414716950134790266" } }, "parameters": { @@ -1941,6 +1944,14 @@ "type": "string", "defaultValue": "AZURE-POSTGRESQL-INFO" }, + "postgresDatabaseAdminUserName": { + "type": "string", + "defaultValue": "" + }, + "postgresDatabaseAdminPassword": { + "type": "string", + "defaultValue": "" + }, "storageAccountKeyName": { "type": "string", "defaultValue": "AZURE-STORAGE-ACCOUNT-KEY" @@ -2042,7 +2053,7 @@ "apiVersion": "2022-07-01", "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('postgresInfoName'))]", "properties": { - "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DBforPostgreSQL/flexibleServers', parameters('postgresServerName')), '2022-12-01').username, 'dbname', parameters('postgresDatabaseName'), 'host', format('{0}.postgres.database.azure.com', parameters('postgresServerName')), 'password', listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DBforPostgreSQL/flexibleServers', parameters('postgresServerName')), '2022-12-01').password)), '')]" + "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', parameters('postgresDatabaseAdminUserName'), 'dbname', parameters('postgresDatabaseName'), 'host', format('{0}.postgres.database.azure.com', parameters('postgresServerName')), 'password', parameters('postgresDatabaseAdminPassword'))), '')]" } }, { @@ -2147,8 +2158,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "9322258851357800042" + "version": "0.30.23.60470", + "templateHash": "13584246975784398226" }, "description": "Creates an Azure AI Search instance." }, @@ -2312,8 +2323,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "15465909238121035232" + "version": "0.30.23.60470", + "templateHash": "9286637480882627742" }, "description": "Creates an Azure App Service plan." }, @@ -2446,7 +2457,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName)), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRES_INFO', string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRES_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -2455,8 +2466,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "12999310699317116765" + "version": "0.30.23.60470", + "templateHash": "5948641620168664624" } }, "parameters": { @@ -2645,8 +2656,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "14262267256571278851" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -2872,8 +2883,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1172957779666771475" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -2950,8 +2961,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3019,8 +3030,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3088,8 +3099,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3157,8 +3168,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3223,8 +3234,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17016224891593731674" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -3379,7 +3390,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'CHAT_HISTORY_ENABLED', parameters('chatHistoryEnabled'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName)), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'CHAT_HISTORY_ENABLED', parameters('chatHistoryEnabled'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -3388,8 +3399,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "12999310699317116765" + "version": "0.30.23.60470", + "templateHash": "5948641620168664624" } }, "parameters": { @@ -3578,8 +3589,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "14262267256571278851" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -3805,8 +3816,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1172957779666771475" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -3883,8 +3894,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3952,8 +3963,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4021,8 +4032,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4090,8 +4101,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4156,8 +4167,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17016224891593731674" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -4359,8 +4370,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "10287577223546471482" + "version": "0.30.23.60470", + "templateHash": "12567732396765618168" } }, "parameters": { @@ -4530,8 +4541,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "14262267256571278851" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -4757,8 +4768,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1172957779666771475" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -4835,8 +4846,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4904,8 +4915,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4973,8 +4984,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5042,8 +5053,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5108,8 +5119,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17016224891593731674" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -5306,8 +5317,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "10287577223546471482" + "version": "0.30.23.60470", + "templateHash": "12567732396765618168" } }, "parameters": { @@ -5477,8 +5488,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "14262267256571278851" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -5704,8 +5715,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1172957779666771475" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -5782,8 +5793,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5851,8 +5862,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5920,8 +5931,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5989,8 +6000,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -6055,8 +6066,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17016224891593731674" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -6168,8 +6179,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "6932660993264101661" + "version": "0.30.23.60470", + "templateHash": "2390666818608223959" }, "description": "Creates an Application Insights instance and a Log Analytics workspace." }, @@ -6220,8 +6231,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4411803171372203725" + "version": "0.30.23.60470", + "templateHash": "19694557100387265" }, "description": "Creates a Log Analytics workspace." }, @@ -6301,8 +6312,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "11643435303947380859" + "version": "0.30.23.60470", + "templateHash": "16993757720869129667" }, "description": "Creates an Application Insights instance based on an existing Log Analytics workspace." }, @@ -6366,8 +6377,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "14575977876683967619" + "version": "0.30.23.60470", + "templateHash": "12524466040979787143" }, "description": "Creates a dashboard for an Application Insights instance." }, @@ -7701,8 +7712,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "15964344188120348249" + "version": "0.30.23.60470", + "templateHash": "15151749822990864279" } }, "parameters": { @@ -7784,8 +7795,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1761446928932158043" + "version": "0.30.23.60470", + "templateHash": "15030863077610448627" } }, "parameters": { @@ -7979,8 +7990,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17966621184348323886" + "version": "0.30.23.60470", + "templateHash": "829406794789220597" } }, "parameters": { @@ -8172,8 +8183,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4659883628761739984" + "version": "0.30.23.60470", + "templateHash": "3077544357242613291" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -8380,8 +8391,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "14262267256571278851" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -8607,8 +8618,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1172957779666771475" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -8703,8 +8714,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8772,8 +8783,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8841,8 +8852,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8910,8 +8921,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8979,8 +8990,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -9045,8 +9056,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17016224891593731674" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -9227,8 +9238,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17966621184348323886" + "version": "0.30.23.60470", + "templateHash": "829406794789220597" } }, "parameters": { @@ -9420,8 +9431,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4659883628761739984" + "version": "0.30.23.60470", + "templateHash": "3077544357242613291" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -9628,8 +9639,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "14262267256571278851" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -9855,8 +9866,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "1172957779666771475" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -9951,8 +9962,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10020,8 +10031,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10089,8 +10100,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10158,8 +10169,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10227,8 +10238,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10293,8 +10304,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "17016224891593731674" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -10397,8 +10408,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "7514893090820700577" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10548,8 +10559,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "7514893090820700577" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10702,8 +10713,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "5895555282161133397" + "version": "0.30.23.60470", + "templateHash": "6699069410959282929" } }, "parameters": { @@ -10830,8 +10841,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "16998952628947606682" + "version": "0.30.23.60470", + "templateHash": "7157574004190707979" }, "description": "Creates an Azure storage account." }, @@ -11051,8 +11062,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11117,8 +11128,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11183,8 +11194,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11249,8 +11260,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "4781574865545118092" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11331,8 +11342,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.28.1.47646", - "templateHash": "18438613076567100409" + "version": "0.30.23.60470", + "templateHash": "17372485166957435450" } }, "parameters": { @@ -11675,11 +11686,11 @@ }, "AZURE_COSMOSDB_INFO": { "type": "string", - "value": "[string(createObject('accountName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName))]" + "value": "[string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, '')))]" }, "AZURE_POSTGRESDB_INFO": { "type": "string", - "value": "[string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))]" + "value": "[string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))]" } } } \ No newline at end of file From f0f07d6dd106e261417324da87103d014173b11c Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Tue, 26 Nov 2024 18:27:56 +0530 Subject: [PATCH 041/107] Set default DB as Cosmos --- infra/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index 0846d0776..76824326c 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -296,7 +296,7 @@ param azureMachineLearningName string = 'aml-${resourceToken}' 'cosmos' 'postgres' ]) -param databaseType string +param databaseType string = 'cosmos' @description('Azure Cosmos DB Account Name') From 1fa77092d8c63f8ffa49b32b778ed7ed37a7d14b Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Tue, 26 Nov 2024 19:12:12 +0530 Subject: [PATCH 042/107] Updated Default values and removed Chat history flag from deployment --- infra/main.bicep | 12 +----------- infra/main.bicepparam | 1 - 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 76824326c..4cfc9bbf4 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -138,7 +138,7 @@ param azureOpenAIVisionModelCapacity int = 10 'langchain' 'prompt_flow' ]) -param orchestrationStrategy string = 'openai_function' +param orchestrationStrategy string = 'semantic_kernel' @description('Chat conversation type: custom or byod.') @allowed([ @@ -305,13 +305,6 @@ param azureCosmosDBAccountName string = 'cosmos-${resourceToken}' @description('Azure Postgres DB Account Name') param azurePostgresDBAccountName string = 'postgres-${resourceToken}' -@description('Whether or not to enable chat history') -@allowed([ - 'true' - 'false' -]) -param chatHistoryEnabled string = 'true' - var blobContainerName = 'documents' var queueName = 'doc-processing' var clientKey = '${uniqueString(guid(subscription().id, deployment().name))}${newGuidString}' @@ -745,7 +738,6 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { ORCHESTRATION_STRATEGY: orchestrationStrategy CONVERSATION_FLOW: conversationFlow LOGLEVEL: logLevel - CHAT_HISTORY_ENABLED: chatHistoryEnabled // Add database type to settings AZURE_DATABASE_TYPE: databaseType @@ -830,7 +822,6 @@ module adminweb './app/adminweb.bicep' = if (hostingModel == 'code') { FUNCTION_KEY: clientKey ORCHESTRATION_STRATEGY: orchestrationStrategy LOGLEVEL: logLevel - CHAT_HISTORY_ENABLED: chatHistoryEnabled } } } @@ -904,7 +895,6 @@ module adminweb_docker './app/adminweb.bicep' = if (hostingModel == 'container') FUNCTION_KEY: clientKey ORCHESTRATION_STRATEGY: orchestrationStrategy LOGLEVEL: logLevel - CHAT_HISTORY_ENABLED: chatHistoryEnabled } } } diff --git a/infra/main.bicepparam b/infra/main.bicepparam index 02b7083c0..eba34779f 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -18,7 +18,6 @@ param orchestrationStrategy = readEnvironmentVariable('ORCHESTRATION_STRATEGY', param logLevel = readEnvironmentVariable('LOGLEVEL', 'INFO') param recognizedLanguages = readEnvironmentVariable('AZURE_SPEECH_RECOGNIZER_LANGUAGES', 'en-US,fr-FR,de-DE,it-IT') param conversationFlow = readEnvironmentVariable('CONVERSATION_FLOW', 'custom') -param chatHistoryEnabled = readEnvironmentVariable('CHAT_HISTORY_ENABLED', 'true') //Azure Search param azureSearchFieldId = readEnvironmentVariable('AZURE_SEARCH_FIELDS_ID', 'id') From 5aad9cb8faac8410074c22078fcef39354bb79c9 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 10:58:53 +0530 Subject: [PATCH 043/107] Default values set --- infra/main.json | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/infra/main.json b/infra/main.json index 466d4c68c..9a267531b 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "16603120665121801714" + "templateHash": "9132274249307928589" } }, "parameters": { @@ -290,7 +290,7 @@ }, "orchestrationStrategy": { "type": "string", - "defaultValue": "openai_function", + "defaultValue": "semantic_kernel", "allowedValues": [ "openai_function", "semantic_kernel", @@ -600,6 +600,7 @@ }, "databaseType": { "type": "string", + "defaultValue": "cosmos", "allowedValues": [ "cosmos", "postgres" @@ -621,17 +622,6 @@ "metadata": { "description": "Azure Postgres DB Account Name" } - }, - "chatHistoryEnabled": { - "type": "string", - "defaultValue": "true", - "allowedValues": [ - "true", - "false" - ], - "metadata": { - "description": "Whether or not to enable chat history" - } } }, "variables": { @@ -3390,7 +3380,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'CHAT_HISTORY_ENABLED', parameters('chatHistoryEnabled'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(resourceId('Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(resourceId('Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', variables('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(resourceId('Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -4359,8 +4349,7 @@ "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", "FUNCTION_KEY": "[variables('clientKey')]", "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]", - "CHAT_HISTORY_ENABLED": "[parameters('chatHistoryEnabled')]" + "LOGLEVEL": "[parameters('logLevel')]" } } }, @@ -5306,8 +5295,7 @@ "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", "FUNCTION_KEY": "[variables('clientKey')]", "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]", - "CHAT_HISTORY_ENABLED": "[parameters('chatHistoryEnabled')]" + "LOGLEVEL": "[parameters('logLevel')]" } } }, From 84f56b98e1a686fb11024a31dd5c153c7999315f Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 14:50:49 +0530 Subject: [PATCH 044/107] Added changes for running Python script --- .../database/deploy_create_table_script.bicep | 26 +++++++++++ infra/core/security/managed-identity.bicep | 43 +++++++++++++++++++ infra/main.bicep | 22 ++++++++++ .../data_scripts/create_postgres_tables.py | 14 +++--- scripts/run_create_table_script.sh | 25 +++++++++++ 5 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 infra/core/database/deploy_create_table_script.bicep create mode 100644 infra/core/security/managed-identity.bicep create mode 100644 scripts/run_create_table_script.sh diff --git a/infra/core/database/deploy_create_table_script.bicep b/infra/core/database/deploy_create_table_script.bicep new file mode 100644 index 000000000..6edfe6d40 --- /dev/null +++ b/infra/core/database/deploy_create_table_script.bicep @@ -0,0 +1,26 @@ +@description('Specifies the location for resources.') +param solutionLocation string + +param baseUrl string +param keyVaultName string +param identity string + +resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + kind:'AzureCLI' + name: 'create_postgres_table' + location: solutionLocation // Replace with your desired location + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identity}' : {} + } + } + properties: { + azCliVersion: '2.52.0' + primaryScriptUri: '${baseUrl}scripts/run_create_table_script.sh' + arguments: '${baseUrl} ${keyVaultName}' // Specify any arguments for the script + timeout: 'PT1H' // Specify the desired timeout duration + retentionInterval: 'PT1H' // Specify the desired retention interval + cleanupPreference:'OnSuccess' + } +} diff --git a/infra/core/security/managed-identity.bicep b/infra/core/security/managed-identity.bicep new file mode 100644 index 000000000..ba7176b80 --- /dev/null +++ b/infra/core/security/managed-identity.bicep @@ -0,0 +1,43 @@ +// ========== Managed Identity ========== // +targetScope = 'resourceGroup' + +@minLength(3) +@maxLength(15) +@description('Solution Name') +param solutionName string + +@description('Solution Location') +param solutionLocation string + +@description('Name') +param miName string = '${ solutionName }-managed-identity' + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: miName + location: solutionLocation + tags: { + app: solutionName + location: solutionLocation + } +} + +@description('This is the built-in owner role. See https://docs.microsoft.com/azure/role-based-access-control/built-in-roles#owner') +resource ownerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + scope: resourceGroup() + name: '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, managedIdentity.id, ownerRoleDefinition.id) + properties: { + principalId: managedIdentity.properties.principalId + roleDefinitionId: ownerRoleDefinition.id + principalType: 'ServicePrincipal' + } +} + +output managedIdentityOutput object = { + id: managedIdentity.id + objectId: managedIdentity.properties.principalId + name: miName +} diff --git a/infra/main.bicep b/infra/main.bicep index 4cfc9bbf4..ae87e5e75 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -313,6 +313,7 @@ var resourceGroupName = resourceGroup().name var tags = { 'azd-env-name': resourceGroupName } var location = resourceGroup().location var keyVaultName = 'kv-${resourceToken}' +var baseUrl = 'https://raw.githubusercontent.com/Fr4nc3/chat-with-your-data-solution-accelerator/bicepdefaults/' var azureOpenAIModelInfo = string({ model: azureOpenAIModel modelName: azureOpenAIModelName @@ -324,6 +325,16 @@ var azureOpenAIEmbeddingModelInfo = string({ modelVersion: azureOpenAIEmbeddingModelVersion }) +// ========== Managed Identity ========== // +module managedIdentityModule './core/security/managed-identity.bicep' = if (databaseType == 'postgres') { + name: 'deploy_managed_identity' + params: { + solutionName: resourceToken + solutionLocation: location + } + scope: resourceGroup(resourceGroup().name) +} + module cosmosDBModule './core/database/cosmosdb.bicep' = if (databaseType == 'cosmos') { name: 'deploy_cosmos_db' params: { @@ -1196,6 +1207,17 @@ module machineLearning 'app/machinelearning.bicep' = if (orchestrationStrategy = } } +module createIndex './core/database/deploy_create_table_script.bicep' = { + name : 'deploy_create_table_script' + params:{ + solutionLocation: resourceToken + identity:managedIdentityModule.outputs.managedIdentityOutput.id + baseUrl:baseUrl + keyVaultName:keyvault.outputs.name + } + dependsOn:[keyvault] +} + output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString output AZURE_APP_SERVICE_HOSTING_MODEL string = hostingModel output AZURE_BLOB_STORAGE_INFO string = replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY','') diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 4c393f1d4..92df9fa93 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -1,21 +1,23 @@ +import json from azure.keyvault.secrets import SecretClient from azure.identity import DefaultAzureCredential import psycopg2 key_vault_name = "kv_to-be-replaced" - def get_secrets_from_kv(kv_name, secret_name): credential = DefaultAzureCredential() secret_client = SecretClient( - vault_url=f"https://{kv_name}.vault.azure.net/", credential=credential + 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 -host = get_secrets_from_kv(key_vault_name, "POSTGRESQL-HOST") -user = get_secrets_from_kv(key_vault_name, "POSTGRESQL-USERNAME") -dbname = get_secrets_from_kv(key_vault_name, "POSTGRESQL-DBNAME") +postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) +host = postgres_details.get("host", "") +user = postgres_details.get("user", "") +dbname = postgres_details.get("dbname", "") +password = postgres_details.get("password", "") # Acquire the access token cred = DefaultAzureCredential() @@ -23,7 +25,7 @@ def get_secrets_from_kv(kv_name, secret_name): # Combine the token with the connection string to establish the connection. conn_string = "host={0} user={1} dbname={2} password={3}".format( - host, user, dbname, access_token.token + host, user, dbname, password ) conn = psycopg2.connect(conn_string) cursor = conn.cursor() diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh new file mode 100644 index 000000000..02858c0ee --- /dev/null +++ b/scripts/run_create_table_script.sh @@ -0,0 +1,25 @@ +#!/bin/bash +echo "started the script" + +# Variables +baseUrl="$1" +keyvaultName="$2" +requirementFile="requirements.txt" +requirementFileUrl=${baseUrl}"scripts/data_scripts/requirements.txt" + +echo "Script Started" + +# Download the create table python file +curl --output "create_postgres_tables.py" ${baseUrl}"scripts/data_scripts/create_postgres_tables.py" + +# Download the requirement file +curl --output "$requirementFile" "$requirementFileUrl" + +echo "Download completed" + +#Replace key vault name +sed -i "s/kv_to-be-replaced/${keyvaultName}/g" "create_postgres_tables.py" + +pip install -r requirements.txt + +python create_postgres_tables.py From 658cf3c5cb2a1dff7d1c61f6412f1f2032bf0151 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 15:38:02 +0530 Subject: [PATCH 045/107] Added requirements.txt --- scripts/data_scripts/requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 scripts/data_scripts/requirements.txt diff --git a/scripts/data_scripts/requirements.txt b/scripts/data_scripts/requirements.txt new file mode 100644 index 000000000..be1b9187e --- /dev/null +++ b/scripts/data_scripts/requirements.txt @@ -0,0 +1,5 @@ +azure_storage==0.37.0 +psycopg2==2.9.10 +azure-identity +azure-keyvault-secrets +json From 5379ef1e485ba3f010971c89fe544a5c6d3b4872 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 15:48:26 +0530 Subject: [PATCH 046/107] Updated requirements.txt --- scripts/data_scripts/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/data_scripts/requirements.txt b/scripts/data_scripts/requirements.txt index be1b9187e..8fcba8dea 100644 --- a/scripts/data_scripts/requirements.txt +++ b/scripts/data_scripts/requirements.txt @@ -1,4 +1,3 @@ -azure_storage==0.37.0 psycopg2==2.9.10 azure-identity azure-keyvault-secrets From a7a4bbba3cfa5efeac0772e3a0425ad9d7225f91 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 16:06:19 +0530 Subject: [PATCH 047/107] Updated requirements.txt --- scripts/data_scripts/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/data_scripts/requirements.txt b/scripts/data_scripts/requirements.txt index 8fcba8dea..3f4852498 100644 --- a/scripts/data_scripts/requirements.txt +++ b/scripts/data_scripts/requirements.txt @@ -1,4 +1,4 @@ -psycopg2==2.9.10 -azure-identity -azure-keyvault-secrets +psycopg2-binary=2.9.10 +azure-identity=1.19.0 +azure-keyvault-secrets=4.9.0 json From 3f1cacf51c58a954cab0b34ab523aef166680b1d Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 16:20:24 +0530 Subject: [PATCH 048/107] Updated requiremnts.txt --- scripts/data_scripts/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/data_scripts/requirements.txt b/scripts/data_scripts/requirements.txt index 3f4852498..f08e2d912 100644 --- a/scripts/data_scripts/requirements.txt +++ b/scripts/data_scripts/requirements.txt @@ -1,4 +1,4 @@ -psycopg2-binary=2.9.10 -azure-identity=1.19.0 -azure-keyvault-secrets=4.9.0 +psycopg2-binary==2.9.10 +azure-identity==1.19.0 +azure-keyvault-secrets==4.9.0 json From 2b1670e3423252df6f894467a0c4b00f7676e55a Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 16:27:36 +0530 Subject: [PATCH 049/107] Updated script --- scripts/run_create_table_script.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 02858c0ee..0f80a56fd 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -22,4 +22,6 @@ sed -i "s/kv_to-be-replaced/${keyvaultName}/g" "create_postgres_tables.py" pip install -r requirements.txt +pip show azure-identity + python create_postgres_tables.py From baaacd86607c1d304011d0fe7472366f16e582aa Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 16:32:38 +0530 Subject: [PATCH 050/107] Updated requirements.txt --- scripts/data_scripts/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/data_scripts/requirements.txt b/scripts/data_scripts/requirements.txt index f08e2d912..3cb4d1b3e 100644 --- a/scripts/data_scripts/requirements.txt +++ b/scripts/data_scripts/requirements.txt @@ -1,4 +1,3 @@ psycopg2-binary==2.9.10 azure-identity==1.19.0 azure-keyvault-secrets==4.9.0 -json From 23249be781aef3ccd1f73934d7d679adf1d48b1e Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 27 Nov 2024 19:52:42 +0530 Subject: [PATCH 051/107] Retrieve PostgreSQL Search Indexes When Exploring Uploaded Files --- .../helpers/azure_postgres_helper.py | 76 +++++++++++++++++++ .../search/postgres_search_handler.py | 31 +++++--- code/backend/pages/02_Explore_Data.py | 13 +++- 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py index 065375579..e3519110b 100644 --- a/code/backend/batch/utilities/helpers/azure_postgres_helper.py +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -197,3 +197,79 @@ def delete_documents(self, ids_to_delete): raise finally: conn.close() + + def perform_search(self, title): + """ + Fetches search results from PostgreSQL based on the title. + """ + # Establish connection to PostgreSQL + conn = self.get_search_client() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + # Execute query to fetch title, content, and metadata + cur.execute( + """ + SELECT title, content, metadata + FROM search_indexes + WHERE title = %s + """, + (title,), + ) + results = cur.fetchall() # Fetch all matching results + logger.info(f"Retrieved {len(results)} search result(s).") + return results + except Exception as e: + logger.error(f"Error executing search query: {e}") + raise + finally: + conn.close() + + def get_unique_files(self): + """ + Fetches unique titles from PostgreSQL. + """ + # Establish connection to PostgreSQL + conn = self.get_search_client() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + # Execute query to fetch distinct titles + cur.execute( + """ + SELECT DISTINCT title + FROM search_indexes + """ + ) + results = cur.fetchall() # Fetch all results as RealDictRow objects + logger.info(f"Retrieved {len(results)} unique title(s).") + return results + except Exception as e: + logger.error(f"Error executing search query: {e}") + raise + finally: + conn.close() + + def search_by_blob_url(self, blob_url): + """ + Fetches unique titles from PostgreSQL based on a given blob URL. + """ + # Establish connection to PostgreSQL + conn = self.get_search_client() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + # Execute parameterized query to fetch results + cur.execute( + """ + SELECT id, title + FROM search_indexes + WHERE source = %s + """, + (f"{blob_url}_SAS_TOKEN_PLACEHOLDER_",), + ) + results = cur.fetchall() # Fetch all results as RealDictRow objects + logger.info(f"Retrieved {len(results)} unique title(s).") + return results + except Exception as e: + logger.error(f"Error executing search query: {e}") + raise + finally: + conn.close() diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py index 58c6aaa46..5be9bb5ec 100644 --- a/code/backend/batch/utilities/search/postgres_search_handler.py +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -1,3 +1,4 @@ +import json from typing import List import numpy as np @@ -47,14 +48,16 @@ def create_search_indexes(self, documents_to_upload): return self.azure_postgres_helper.create_search_indexes(documents_to_upload) def perform_search(self, filename): - raise NotImplementedError( - "The method perform_search is not implemented in AzurePostgresHandler." - ) + return self.azure_postgres_helper.perform_search(filename) def process_results(self, results): - raise NotImplementedError( - "The method process_results is not implemented in AzurePostgresHandler." - ) + if results is None: + return [] + data = [ + [json.loads(result["metadata"]).get("chunk", i), result["content"]] + for i, result in enumerate(results) + ] + return data def get_files(self): results = self.azure_postgres_helper.get_files() @@ -86,6 +89,16 @@ def delete_files(self, files): return ", ".join(files_to_delete) def search_by_blob_url(self, blob_url): - raise NotImplementedError( - "The method search_by_blob_url is not implemented in AzurePostgresHandler." - ) + return self.azure_postgres_helper.search_by_blob_url(blob_url) + + def delete_from_index(self, blob_url) -> None: + documents = self.search_by_blob_url(blob_url) + if documents is None or len(documents) == 0: + return + files_to_delete = self.output_results(documents) + self.delete_files(files_to_delete) + + def get_unique_files(self): + results = self.azure_postgres_helper.get_unique_files() + unique_titles = [row["title"] for row in results] + return unique_titles diff --git a/code/backend/pages/02_Explore_Data.py b/code/backend/pages/02_Explore_Data.py index 73ffde955..5ef55a774 100644 --- a/code/backend/pages/02_Explore_Data.py +++ b/code/backend/pages/02_Explore_Data.py @@ -40,8 +40,17 @@ def load_css(file_path): try: search_handler = Search.get_search_handler(env_helper) - results = search_handler.search_with_facets("*", "title", facet_count=0) - unique_files = search_handler.get_unique_files(results, "title") + # Determine unique files based on database type + if env_helper.DATABASE_TYPE == "PostgreSQL": + unique_files = search_handler.get_unique_files() + elif env_helper.DATABASE_TYPE == "CosmosDB": + results = search_handler.search_with_facets("*", "title", facet_count=0) + unique_files = search_handler.get_unique_files(results, "title") + else: + raise ValueError( + "Unsupported database type. Only 'PostgreSQL' and 'CosmosDB' are allowed." + ) + filename = st.selectbox("Select your file:", unique_files) st.write("Showing chunks for:", filename) From 6a73dd136c491306e36537ec70a06cba38baa686 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 20:57:29 +0530 Subject: [PATCH 052/107] Added IP whitelisting of depliyment environment --- scripts/run_create_table_script.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 0f80a56fd..0862165b0 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -7,8 +7,18 @@ keyvaultName="$2" requirementFile="requirements.txt" requirementFileUrl=${baseUrl}"scripts/data_scripts/requirements.txt" +# PostgreSQL server name and resource group +$serverName = "postgres-3ygkwbdjmavwa-postgres" +$resourceGroup = "rg-prdc-pgsql-py4" + echo "Script Started" +# Get the public IP address of the machine running the script +$publicIp = Invoke-RestMethod -Uri "https://api.ipify.org" + +# Use Azure CLI to add the public IP to the PostgreSQL firewall rule +az postgres server firewall-rule create --resource-group $resourceGroup --server-name $serverName --name "allowScriptIp" --start-ip-address $publicIp --end-ip-address $publicIp + # Download the create table python file curl --output "create_postgres_tables.py" ${baseUrl}"scripts/data_scripts/create_postgres_tables.py" From ef034f5a3b56d7d3bd5dd5f688034cc4a0a282fb Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 21:13:15 +0530 Subject: [PATCH 053/107] Updated script --- scripts/run_create_table_script.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 0862165b0..fb4117f6f 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -6,16 +6,17 @@ baseUrl="$1" keyvaultName="$2" requirementFile="requirements.txt" requirementFileUrl=${baseUrl}"scripts/data_scripts/requirements.txt" - -# PostgreSQL server name and resource group -$serverName = "postgres-3ygkwbdjmavwa-postgres" -$resourceGroup = "rg-prdc-pgsql-py4" +serverName="postgres-3ygkwbdjmavwa-postgres" +resourceGroup="rg-prdc-pgsql-py4" echo "Script Started" # Get the public IP address of the machine running the script $publicIp = Invoke-RestMethod -Uri "https://api.ipify.org" +echo "Publis Ip is" +echo $publicIp + # Use Azure CLI to add the public IP to the PostgreSQL firewall rule az postgres server firewall-rule create --resource-group $resourceGroup --server-name $serverName --name "allowScriptIp" --start-ip-address $publicIp --end-ip-address $publicIp From b2d6fcdb1ef678243a027a2171f6b4bef53da44d Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 21:25:45 +0530 Subject: [PATCH 054/107] Updated the command to get IP --- scripts/run_create_table_script.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index fb4117f6f..8c3f2df1e 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -12,10 +12,7 @@ resourceGroup="rg-prdc-pgsql-py4" echo "Script Started" # Get the public IP address of the machine running the script -$publicIp = Invoke-RestMethod -Uri "https://api.ipify.org" - -echo "Publis Ip is" -echo $publicIp +publicIp=$(curl -s https://api.ipify.org) # Use Azure CLI to add the public IP to the PostgreSQL firewall rule az postgres server firewall-rule create --resource-group $resourceGroup --server-name $serverName --name "allowScriptIp" --start-ip-address $publicIp --end-ip-address $publicIp From f8d6a9bfa72e71e484f32b4d11bb4559c58ad787 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 21:36:49 +0530 Subject: [PATCH 055/107] Updated the Script to add ip --- scripts/run_create_table_script.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 8c3f2df1e..63b3ca593 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -15,7 +15,7 @@ echo "Script Started" publicIp=$(curl -s https://api.ipify.org) # Use Azure CLI to add the public IP to the PostgreSQL firewall rule -az postgres server firewall-rule create --resource-group $resourceGroup --server-name $serverName --name "allowScriptIp" --start-ip-address $publicIp --end-ip-address $publicIp +az postgres flexible-server firewall-rule create --resource-group $resourceGroup --name $serverName --start-ip-address $publicIp --end-ip-address $publicIp # Download the create table python file curl --output "create_postgres_tables.py" ${baseUrl}"scripts/data_scripts/create_postgres_tables.py" From c5d94f3c6edbfcaff9cacee907b9526e08444314 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 21:48:13 +0530 Subject: [PATCH 056/107] Added the rule name --- scripts/run_create_table_script.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 63b3ca593..745f071b9 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -15,7 +15,7 @@ echo "Script Started" publicIp=$(curl -s https://api.ipify.org) # Use Azure CLI to add the public IP to the PostgreSQL firewall rule -az postgres flexible-server firewall-rule create --resource-group $resourceGroup --name $serverName --start-ip-address $publicIp --end-ip-address $publicIp +az postgres flexible-server firewall-rule create --resource-group $resourceGroup --name $serverName --rule-name "AllowScriptIp" --start-ip-address "$publicIp" --end-ip-address "$publicIp" # Download the create table python file curl --output "create_postgres_tables.py" ${baseUrl}"scripts/data_scripts/create_postgres_tables.py" From 6804348a54ee4ab18dc5a498a7f0f2731563bbb3 Mon Sep 17 00:00:00 2001 From: Pavan-Microsoft Date: Wed, 27 Nov 2024 23:12:12 +0530 Subject: [PATCH 057/107] Update env_helper.py --- code/backend/batch/utilities/helpers/env_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index eb897d878..6b4d44624 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -313,7 +313,7 @@ def __load_config(self, **kwargs) -> None: ) # PostgreSQL configuration elif self.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: - self.AZURE_POSTGRE_SEARCH_TOP_K = self.get_env_var_int( + self.AZURE_POSTGRES_SEARCH_TOP_K = self.get_env_var_int( "AZURE_POSTGRES_SEARCH_TOP_K", 5 ) azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") From 62679982f1e859c6c6f2d547106c411d51bae5a2 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 23:14:15 +0530 Subject: [PATCH 058/107] Updated the Script with correct values --- scripts/run_create_table_script.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 745f071b9..1d3bd2063 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -6,8 +6,8 @@ baseUrl="$1" keyvaultName="$2" requirementFile="requirements.txt" requirementFileUrl=${baseUrl}"scripts/data_scripts/requirements.txt" -serverName="postgres-3ygkwbdjmavwa-postgres" -resourceGroup="rg-prdc-pgsql-py4" +resourceGroup="$3" +serverName="$4" echo "Script Started" From e9f7c9d62ca41e6e40aeb8aa9052ed3987344653 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Wed, 27 Nov 2024 23:44:16 +0530 Subject: [PATCH 059/107] Added code to run the Postgre SQL script --- infra/app/storekeys.bicep | 2 +- .../database/deploy_create_table_script.bicep | 3 +- infra/core/database/postgresdb.bicep | 57 +++- infra/core/security/keyvault.bicep | 46 ++- infra/main.bicep | 10 +- infra/main.json | 262 +++++++++++++++++- 6 files changed, 349 insertions(+), 31 deletions(-) diff --git a/infra/app/storekeys.bicep b/infra/app/storekeys.bicep index b3ae8b480..deca84872 100644 --- a/infra/app/storekeys.bicep +++ b/infra/app/storekeys.bicep @@ -110,7 +110,7 @@ resource postgresInfoSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = if ? string({ user: postgresDatabaseAdminUserName dbname: postgresDatabaseName - host: '${postgresServerName}.postgres.database.azure.com' + host: postgresServerName password: postgresDatabaseAdminPassword }) : '' diff --git a/infra/core/database/deploy_create_table_script.bicep b/infra/core/database/deploy_create_table_script.bicep index 6edfe6d40..b1fc012df 100644 --- a/infra/core/database/deploy_create_table_script.bicep +++ b/infra/core/database/deploy_create_table_script.bicep @@ -4,6 +4,7 @@ param solutionLocation string param baseUrl string param keyVaultName string param identity string +param postgresSqlServerName string resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { kind:'AzureCLI' @@ -18,7 +19,7 @@ resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { properties: { azCliVersion: '2.52.0' primaryScriptUri: '${baseUrl}scripts/run_create_table_script.sh' - arguments: '${baseUrl} ${keyVaultName}' // Specify any arguments for the script + arguments: '${baseUrl} ${keyVaultName} ${resourceGroup().name} ${postgresSqlServerName}' // Specify any arguments for the script timeout: 'PT1H' // Specify the desired timeout duration retentionInterval: 'PT1H' // Specify the desired retention interval cleanupPreference:'OnSuccess' diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index a9c39fa0a..bd6ea5464 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -1,5 +1,7 @@ param solutionName string param solutionLocation string +param managedIdentityObjectId string +param managedIdentityObjectName string @description('The name of the SQL logical server.') param serverName string = '${solutionName}-postgres' @@ -35,7 +37,11 @@ resource serverName_resource 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12- version: version administratorLogin: administratorLogin administratorLoginPassword: administratorLoginPassword - + authConfig: { + tenantId: subscription().tenantId + activeDirectoryAuth: 'Enabled' + passwordAuth: 'Enabled' + } highAvailability: { mode: 'Disabled' } @@ -53,6 +59,47 @@ resource serverName_resource 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12- } } +resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: 'waitForServerReady' + location: resourceGroup().location + kind: 'AzureCLI' + dependsOn: [ + serverName_resource + ] + properties: { + azCliVersion: '2.38.0' // Adjust version if needed + timeout: 'PT5M' // 5 minutes timeout + scriptContent: ''' + echo "Waiting for PostgreSQL server to be ready..." + for i in {1..30}; do + state=$(az postgres flexible-server show --name ${serverName_resource.name} --resource-group ${resourceGroup().name} --query state -o tsv) + if [ "$state" == "Ready" ]; then + echo "Server is ready!" + exit 0 + fi + echo "Server state: $state. Retrying in 10 seconds..." + sleep 10 + done + echo "Server did not become ready in time." + exit 1 + ''' + retentionInterval: 'P1D' // Retain script logs for 1 day + } +} + +resource azureADAdministrator 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2022-12-01' = { + parent: serverName_resource + name: managedIdentityObjectId + properties: { + principalType: 'SERVICEPRINCIPAL' + principalName: managedIdentityObjectName + tenantId: subscription().tenantId + } + dependsOn: [ + delayScript + ] +} + // resource serverName_firewallrules 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2021-06-01' = [for rule in firewallrules: { // parent: serverName_resource // name: rule.Name @@ -71,7 +118,7 @@ resource firewall_all 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2 endIpAddress: '255.255.255.255' } dependsOn: [ - serverName_resource + delayScript ] } @@ -83,7 +130,7 @@ resource firewall_azure 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules endIpAddress: '0.0.0.0' } dependsOn: [ - firewall_all + delayScript ] } @@ -91,7 +138,7 @@ resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configuration name: 'azure.extensions' parent: serverName_resource properties: { - value: 'vector' + value: 'pg_diskann' source: 'user-override' } dependsOn: [ @@ -102,7 +149,7 @@ resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configuration output postgresDbOutput object = { postgresSQLName: serverName_resource.name - postgreSQLServerName: serverName_resource.name + postgreSQLServerName: '${serverName_resource.name}.postgres.database.azure.com' postgreSQLDatabaseName: 'postgres' postgreSQLDbUser: administratorLogin postgreSQLDbPwd: administratorLoginPassword diff --git a/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep index 3920c3b3a..491744904 100644 --- a/infra/core/security/keyvault.bicep +++ b/infra/core/security/keyvault.bicep @@ -2,6 +2,7 @@ metadata description = 'Creates an Azure Key Vault.' param name string param location string = resourceGroup().location param tags object = {} +param managedIdentityObjectId string param principalId string = '' @@ -12,18 +13,43 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { properties: { tenantId: subscription().tenantId sku: { family: 'A', name: 'standard' } - accessPolicies: !empty(principalId) - ? [ - { - objectId: principalId - permissions: { secrets: [ 'get', 'list' ] } - tenantId: subscription().tenantId - } - ] - : [] + accessPolicies: !empty(principalId) + ? [ + { + objectId: principalId + permissions: { secrets: [ 'get', 'list' ] } + tenantId: subscription().tenantId + }, { + objectId: managedIdentityObjectId + permissions: { secrets: [ 'get', 'list' ] } + tenantId: subscription().tenantId + } + ] + : [ + { + objectId: managedIdentityObjectId + permissions: { secrets: [ 'get', 'list' ] } + tenantId: subscription().tenantId + } + ] } } +// @description('This is the built-in Key Vault Administrator role.') +// resource kvAdminRole 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { +// scope: resourceGroup() +// name: '00482a5a-887f-4fb3-b363-3b7fe8e74483' +// } + +// resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +// name: guid(resourceGroup().id, managedIdentityObjectId, kvAdminRole.id) +// properties: { +// principalId: managedIdentityObjectId +// roleDefinitionId:kvAdminRole.id +// principalType: 'ServicePrincipal' +// } +// } + output endpoint string = keyVault.properties.vaultUri output name string = keyVault.name -output id string = keyVault.id \ No newline at end of file +output id string = keyVault.id diff --git a/infra/main.bicep b/infra/main.bicep index ae87e5e75..73262700b 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -349,6 +349,8 @@ module postgresDBModule './core/database/postgresdb.bicep' = if (databaseType == params: { solutionName: azurePostgresDBAccountName solutionLocation: 'eastus2' + managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId + managedIdentityObjectName: managedIdentityModule.outputs.managedIdentityOutput.name } scope: resourceGroup(resourceGroup().name) } @@ -362,6 +364,7 @@ module keyvault './core/security/keyvault.bicep' = if (useKeyVault || authType = location: location tags: tags principalId: principalId + managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId } } @@ -1207,15 +1210,16 @@ module machineLearning 'app/machinelearning.bicep' = if (orchestrationStrategy = } } -module createIndex './core/database/deploy_create_table_script.bicep' = { +module createIndex './core/database/deploy_create_table_script.bicep' = if (databaseType == 'postgres') { name : 'deploy_create_table_script' params:{ - solutionLocation: resourceToken + solutionLocation: location identity:managedIdentityModule.outputs.managedIdentityOutput.id baseUrl:baseUrl keyVaultName:keyvault.outputs.name + postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName } - dependsOn:[keyvault] + dependsOn:[keyvault, postgresDBModule, storekeys] } output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString diff --git a/infra/main.json b/infra/main.json index 9a267531b..8e0fbb96c 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "9132274249307928589" + "templateHash": "106067340422892734" } }, "parameters": { @@ -635,6 +635,7 @@ }, "location": "[resourceGroup().location]", "keyVaultName": "[format('kv-{0}', parameters('resourceToken'))]", + "baseUrl": "https://raw.githubusercontent.com/Fr4nc3/chat-with-your-data-solution-accelerator/bicepdefaults/", "azureOpenAIModelInfo": "[string(createObject('model', parameters('azureOpenAIModel'), 'modelName', parameters('azureOpenAIModelName'), 'modelVersion', parameters('azureOpenAIModelVersion')))]", "azureOpenAIEmbeddingModelInfo": "[string(createObject('model', parameters('azureOpenAIEmbeddingModel'), 'modelName', parameters('azureOpenAIEmbeddingModelName'), 'modelVersion', parameters('azureOpenAIEmbeddingModelVersion')))]", "defaultOpenAiDeployments": [ @@ -666,6 +667,96 @@ "openAiDeployments": "[concat(variables('defaultOpenAiDeployments'), if(parameters('useAdvancedImageProcessing'), createArray(createObject('name', parameters('azureOpenAIVisionModel'), 'model', createObject('format', 'OpenAI', 'name', parameters('azureOpenAIVisionModelName'), 'version', parameters('azureOpenAIVisionModelVersion')), 'sku', createObject('name', 'Standard', 'capacity', parameters('azureOpenAIVisionModelCapacity')))), createArray()))]" }, "resources": [ + { + "condition": "[equals(parameters('databaseType'), 'postgres')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_managed_identity", + "resourceGroup": "[resourceGroup().name]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionName": { + "value": "[parameters('resourceToken')]" + }, + "solutionLocation": { + "value": "[variables('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "8775325455752085588" + } + }, + "parameters": { + "solutionName": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "metadata": { + "description": "Solution Name" + } + }, + "solutionLocation": { + "type": "string", + "metadata": { + "description": "Solution Location" + } + }, + "miName": { + "type": "string", + "defaultValue": "[format('{0}-managed-identity', parameters('solutionName'))]", + "metadata": { + "description": "Name" + } + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[parameters('miName')]", + "location": "[parameters('solutionLocation')]", + "tags": { + "app": "[parameters('solutionName')]", + "location": "[parameters('solutionLocation')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), resourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), '2023-01-31').principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName'))]" + ] + } + ], + "outputs": { + "managedIdentityOutput": { + "type": "object", + "value": { + "id": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName'))]", + "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), '2023-01-31').principalId]", + "name": "[parameters('miName')]" + } + } + } + } + } + }, { "condition": "[equals(parameters('databaseType'), 'cosmos')]", "type": "Microsoft.Resources/deployments", @@ -842,6 +933,12 @@ }, "solutionLocation": { "value": "eastus2" + }, + "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]" + }, + "managedIdentityObjectName": { + "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": { @@ -851,7 +948,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "17825575027061010483" + "templateHash": "1846653496210046668" } }, "parameters": { @@ -861,6 +958,12 @@ "solutionLocation": { "type": "string" }, + "managedIdentityObjectId": { + "type": "string" + }, + "managedIdentityObjectName": { + "type": "string" + }, "serverName": { "type": "string", "defaultValue": "[format('{0}-postgres', parameters('solutionName'))]", @@ -930,6 +1033,11 @@ "version": "[parameters('version')]", "administratorLogin": "[parameters('administratorLogin')]", "administratorLoginPassword": "[parameters('administratorLoginPassword')]", + "authConfig": { + "tenantId": "[subscription().tenantId]", + "activeDirectoryAuth": "Enabled", + "passwordAuth": "Enabled" + }, "highAvailability": { "mode": "Disabled" }, @@ -946,6 +1054,36 @@ "availabilityZone": "[parameters('availabilityZone')]" } }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "waitForServerReady", + "location": "[resourceGroup().location]", + "kind": "AzureCLI", + "properties": { + "azCliVersion": "2.38.0", + "timeout": "PT5M", + "scriptContent": " echo \"Waiting for PostgreSQL server to be ready...\"\n for i in {1..30}; do\n state=$(az postgres flexible-server show --name ${serverName_resource.name} --resource-group ${resourceGroup().name} --query userVisibleState -o tsv)\n if [ \"$state\" == \"Ready\" ]; then\n echo \"Server is ready!\"\n exit 0\n fi\n echo \"Server state: $state. Retrying in 10 seconds...\"\n sleep 10\n done\n echo \"Server did not become ready in time.\"\n exit 1\n ", + "retentionInterval": "P1D" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/administrators", + "apiVersion": "2022-12-01", + "name": "[format('{0}/{1}', parameters('serverName'), parameters('managedIdentityObjectId'))]", + "properties": { + "principalType": "SERVICEPRINCIPAL", + "principalName": "[parameters('managedIdentityObjectName')]", + "tenantId": "[subscription().tenantId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, { "condition": "[parameters('allowAllIPsFirewall')]", "type": "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules", @@ -956,6 +1094,7 @@ "endIpAddress": "255.255.255.255" }, "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" ] }, @@ -969,7 +1108,7 @@ "endIpAddress": "0.0.0.0" }, "dependsOn": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-IPs')]", + "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" ] }, @@ -978,7 +1117,7 @@ "apiVersion": "2023-12-01-preview", "name": "[format('{0}/{1}', parameters('serverName'), 'azure.extensions')]", "properties": { - "value": "vector", + "value": "pg_diskann", "source": "user-override" }, "dependsOn": [ @@ -993,7 +1132,7 @@ "type": "object", "value": { "postgresSQLName": "[parameters('serverName')]", - "postgreSQLServerName": "[parameters('serverName')]", + "postgreSQLServerName": "[format('{0}.postgres.database.azure.com', parameters('serverName'))]", "postgreSQLDatabaseName": "postgres", "postgreSQLDbUser": "[parameters('administratorLogin')]", "postgreSQLDbPwd": "[parameters('administratorLoginPassword')]", @@ -1002,7 +1141,10 @@ } } } - } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]" + ] }, { "condition": "[or(parameters('useKeyVault'), equals(parameters('authType'), 'rbac'))]", @@ -1026,6 +1168,9 @@ }, "principalId": { "value": "[parameters('principalId')]" + }, + "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]" } }, "template": { @@ -1035,7 +1180,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12121357715793816510" + "templateHash": "12623823360057072578" }, "description": "Creates an Azure Key Vault." }, @@ -1051,6 +1196,9 @@ "type": "object", "defaultValue": {} }, + "managedIdentityObjectId": { + "type": "string" + }, "principalId": { "type": "string", "defaultValue": "" @@ -1069,7 +1217,7 @@ "family": "A", "name": "standard" }, - "accessPolicies": "[if(not(empty(parameters('principalId'))), createArray(createObject('objectId', parameters('principalId'), 'permissions', createObject('secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId)), createArray())]" + "accessPolicies": "[if(not(empty(parameters('principalId'))), createArray(createObject('objectId', parameters('principalId'), 'permissions', createObject('secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId), createObject('objectId', parameters('managedIdentityObjectId'), 'permissions', createObject('secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId)), createArray(createObject('objectId', parameters('managedIdentityObjectId'), 'permissions', createObject('secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId)))]" } } ], @@ -1088,7 +1236,10 @@ } } } - } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]" + ] }, { "type": "Microsoft.Resources/deployments", @@ -1882,7 +2033,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "9414716950134790266" + "templateHash": "4301444608123274670" } }, "parameters": { @@ -2043,7 +2194,7 @@ "apiVersion": "2022-07-01", "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('postgresInfoName'))]", "properties": { - "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', parameters('postgresDatabaseAdminUserName'), 'dbname', parameters('postgresDatabaseName'), 'host', format('{0}.postgres.database.azure.com', parameters('postgresServerName')), 'password', parameters('postgresDatabaseAdminPassword'))), '')]" + "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', parameters('postgresDatabaseAdminUserName'), 'dbname', parameters('postgresDatabaseName'), 'host', parameters('postgresServerName'), 'password', parameters('postgresDatabaseAdminPassword'))), '')]" } }, { @@ -11433,6 +11584,95 @@ "[resourceId('Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[resourceId('Microsoft.Resources/deployments', parameters('storageAccountName'))]" ] + }, + { + "condition": "[equals(parameters('databaseType'), 'postgres')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_create_table_script", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionLocation": { + "value": "[variables('location')]" + }, + "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(resourceId('Microsoft.Resources/deployments', 'keyvault'), '2022-09-01').outputs.name.value]" + }, + "postgresSqlServerName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "754015545513025215" + } + }, + "parameters": { + "solutionLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location for resources." + } + }, + "baseUrl": { + "type": "string" + }, + "keyVaultName": { + "type": "string" + }, + "identity": { + "type": "string" + }, + "postgresSqlServerName": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "create_postgres_table", + "kind": "AzureCLI", + "location": "[parameters('solutionLocation')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', parameters('identity'))]": {} + } + }, + "properties": { + "azCliVersion": "2.52.0", + "primaryScriptUri": "[format('{0}scripts/run_create_table_script.sh', parameters('baseUrl'))]", + "arguments": "[format('{0} {1} {2} {3}', parameters('baseUrl'), parameters('keyVaultName'), resourceGroup().name, parameters('postgresSqlServerName'))]", + "timeout": "PT1H", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess" + } + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', '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_postgres_sql')]", + "[resourceId('Microsoft.Resources/deployments', 'storekeys')]" + ] } ], "outputs": { From e3eb512044e56babea890e7987687691811f1807 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 10:06:55 +0530 Subject: [PATCH 060/107] Updated the Wai function --- infra/core/database/postgresdb.bicep | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index bd6ea5464..32a2af714 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -62,29 +62,16 @@ resource serverName_resource 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12- resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { name: 'waitForServerReady' location: resourceGroup().location - kind: 'AzureCLI' + kind: 'AzurePowerShell' + properties: { + azPowerShellVersion: '3.0' + scriptContent: 'start-sleep -Seconds 180' + cleanupPreference: 'Always' + retentionInterval: 'PT1H' + } dependsOn: [ serverName_resource ] - properties: { - azCliVersion: '2.38.0' // Adjust version if needed - timeout: 'PT5M' // 5 minutes timeout - scriptContent: ''' - echo "Waiting for PostgreSQL server to be ready..." - for i in {1..30}; do - state=$(az postgres flexible-server show --name ${serverName_resource.name} --resource-group ${resourceGroup().name} --query state -o tsv) - if [ "$state" == "Ready" ]; then - echo "Server is ready!" - exit 0 - fi - echo "Server state: $state. Retrying in 10 seconds..." - sleep 10 - done - echo "Server did not become ready in time." - exit 1 - ''' - retentionInterval: 'P1D' // Retain script logs for 1 day - } } resource azureADAdministrator 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2022-12-01' = { From 4019078c6ecb1f39a0af3464e4f416cd5863c667 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 10:12:10 +0530 Subject: [PATCH 061/107] Updated the main.json with latest changes --- infra/main.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/infra/main.json b/infra/main.json index 8e0fbb96c..62788211f 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "106067340422892734" + "templateHash": "6490762584259163057" } }, "parameters": { @@ -948,7 +948,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "1846653496210046668" + "templateHash": "7878676541226628436" } }, "parameters": { @@ -1059,12 +1059,12 @@ "apiVersion": "2020-10-01", "name": "waitForServerReady", "location": "[resourceGroup().location]", - "kind": "AzureCLI", + "kind": "AzurePowerShell", "properties": { - "azCliVersion": "2.38.0", - "timeout": "PT5M", - "scriptContent": " echo \"Waiting for PostgreSQL server to be ready...\"\n for i in {1..30}; do\n state=$(az postgres flexible-server show --name ${serverName_resource.name} --resource-group ${resourceGroup().name} --query userVisibleState -o tsv)\n if [ \"$state\" == \"Ready\" ]; then\n echo \"Server is ready!\"\n exit 0\n fi\n echo \"Server state: $state. Retrying in 10 seconds...\"\n sleep 10\n done\n echo \"Server did not become ready in time.\"\n exit 1\n ", - "retentionInterval": "P1D" + "azPowerShellVersion": "3.0", + "scriptContent": "start-sleep -Seconds 180", + "cleanupPreference": "Always", + "retentionInterval": "PT1H" }, "dependsOn": [ "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" From 9a173fda2ebdd697dd00e9bdf25cfb09cf0b75e8 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 13:44:49 +0530 Subject: [PATCH 062/107] Merge issue fix --- infra/main.bicep | 5 +- infra/main.json | 1004 ++++++++++++++++++++++++++++++---------------- 2 files changed, 656 insertions(+), 353 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 7b1ce6c5d..f4696a2a9 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -351,7 +351,7 @@ module managedIdentityModule './core/security/managed-identity.bicep' = if (data solutionName: resourceToken solutionLocation: location } - scope: resourceGroup(resourceGroup().name) + scope: rg } module cosmosDBModule './core/database/cosmosdb.bicep' = if (databaseType == 'cosmos') { @@ -371,7 +371,7 @@ module postgresDBModule './core/database/postgresdb.bicep' = if (databaseType == managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId managedIdentityObjectName: managedIdentityModule.outputs.managedIdentityOutput.name } - scope: resourceGroup(resourceGroup().name) + scope: rg } // Store secrets in a keyvault @@ -1239,6 +1239,7 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (dat keyVaultName:keyvault.outputs.name postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName } + scope: rg dependsOn:[keyvault, postgresDBModule, storekeys] } diff --git a/infra/main.json b/infra/main.json index d7d96051c..314c1f8d3 100644 --- a/infra/main.json +++ b/infra/main.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "9243131736696562084" + "version": "0.30.23.60470", + "templateHash": "16387401552448615008" } }, "parameters": { @@ -612,6 +612,17 @@ "description": "Azure Machine Learning Name" } }, + "databaseType": { + "type": "string", + "defaultValue": "cosmos", + "allowedValues": [ + "cosmos", + "postgres" + ], + "metadata": { + "description": "The type of database to deploy (cosmos or postgres)" + } + }, "azureCosmosDBAccountName": { "type": "string", "defaultValue": "[format('cosmos-{0}', parameters('resourceToken'))]", @@ -619,15 +630,11 @@ "description": "Azure Cosmos DB Account Name" } }, - "chatHistoryEnabled": { + "azurePostgresDBAccountName": { "type": "string", - "defaultValue": "true", - "allowedValues": [ - "true", - "false" - ], + "defaultValue": "[format('postgres-{0}', parameters('resourceToken'))]", "metadata": { - "description": "Whether or not to enable chat history" + "description": "Azure Postgres DB Account Name" } } }, @@ -683,6 +690,100 @@ "tags": "[variables('tags')]" }, { + "condition": "[equals(parameters('databaseType'), 'postgres')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_managed_identity", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionName": { + "value": "[parameters('resourceToken')]" + }, + "solutionLocation": { + "value": "[parameters('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "8775325455752085588" + } + }, + "parameters": { + "solutionName": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "metadata": { + "description": "Solution Name" + } + }, + "solutionLocation": { + "type": "string", + "metadata": { + "description": "Solution Location" + } + }, + "miName": { + "type": "string", + "defaultValue": "[format('{0}-managed-identity', parameters('solutionName'))]", + "metadata": { + "description": "Name" + } + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[parameters('miName')]", + "location": "[parameters('solutionLocation')]", + "tags": { + "app": "[parameters('solutionName')]", + "location": "[parameters('solutionLocation')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), resourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), '2023-01-31').principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName'))]" + ] + } + ], + "outputs": { + "managedIdentityOutput": { + "type": "object", + "value": { + "id": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName'))]", + "objectId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('miName')), '2023-01-31').principalId]", + "name": "[parameters('miName')]" + } + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + ] + }, + { + "condition": "[equals(parameters('databaseType'), 'cosmos')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", "name": "deploy_cosmos_db", @@ -706,8 +807,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "16376502235448567731" + "version": "0.30.23.60470", + "templateHash": "14453122839528928942" } }, "parameters": { @@ -844,6 +945,237 @@ "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" ] }, + { + "condition": "[equals(parameters('databaseType'), 'postgres')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_postgres_sql", + "resourceGroup": "[variables('rgName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionName": { + "value": "[parameters('azurePostgresDBAccountName')]" + }, + "solutionLocation": { + "value": "eastus2" + }, + "managedIdentityObjectId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.objectId]" + }, + "managedIdentityObjectName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.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.30.23.60470", + "templateHash": "7878676541226628436" + } + }, + "parameters": { + "solutionName": { + "type": "string" + }, + "solutionLocation": { + "type": "string" + }, + "managedIdentityObjectId": { + "type": "string" + }, + "managedIdentityObjectName": { + "type": "string" + }, + "serverName": { + "type": "string", + "defaultValue": "[format('{0}-postgres', parameters('solutionName'))]", + "metadata": { + "description": "The name of the SQL logical server." + } + }, + "administratorLogin": { + "type": "string", + "defaultValue": "admintest" + }, + "administratorLoginPassword": { + "type": "securestring", + "defaultValue": "Initial_0524" + }, + "serverEdition": { + "type": "string", + "defaultValue": "Burstable" + }, + "skuSizeGB": { + "type": "int", + "defaultValue": 32 + }, + "dbInstanceType": { + "type": "string", + "defaultValue": "Standard_B1ms" + }, + "availabilityZone": { + "type": "string", + "defaultValue": "1" + }, + "allowAllIPsFirewall": { + "type": "bool", + "defaultValue": false + }, + "allowAzureIPsFirewall": { + "type": "bool", + "defaultValue": false + }, + "version": { + "type": "string", + "defaultValue": "16", + "allowedValues": [ + "11", + "12", + "13", + "14", + "15", + "16" + ], + "metadata": { + "description": "PostgreSQL version" + } + } + }, + "resources": [ + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers", + "apiVersion": "2023-12-01-preview", + "name": "[parameters('serverName')]", + "location": "[parameters('solutionLocation')]", + "sku": { + "name": "[parameters('dbInstanceType')]", + "tier": "[parameters('serverEdition')]" + }, + "properties": { + "version": "[parameters('version')]", + "administratorLogin": "[parameters('administratorLogin')]", + "administratorLoginPassword": "[parameters('administratorLoginPassword')]", + "authConfig": { + "tenantId": "[subscription().tenantId]", + "activeDirectoryAuth": "Enabled", + "passwordAuth": "Enabled" + }, + "highAvailability": { + "mode": "Disabled" + }, + "storage": { + "storageSizeGB": "[parameters('skuSizeGB')]" + }, + "backup": { + "backupRetentionDays": 7, + "geoRedundantBackup": "Disabled" + }, + "network": { + "publicNetworkAccess": "Enabled" + }, + "availabilityZone": "[parameters('availabilityZone')]" + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "waitForServerReady", + "location": "[resourceGroup().location]", + "kind": "AzurePowerShell", + "properties": { + "azPowerShellVersion": "3.0", + "scriptContent": "start-sleep -Seconds 180", + "cleanupPreference": "Always", + "retentionInterval": "PT1H" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/administrators", + "apiVersion": "2022-12-01", + "name": "[format('{0}/{1}', parameters('serverName'), parameters('managedIdentityObjectId'))]", + "properties": { + "principalType": "SERVICEPRINCIPAL", + "principalName": "[parameters('managedIdentityObjectName')]", + "tenantId": "[subscription().tenantId]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, + { + "condition": "[parameters('allowAllIPsFirewall')]", + "type": "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', parameters('serverName'), 'allow-all-IPs')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "255.255.255.255" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, + { + "condition": "[parameters('allowAzureIPsFirewall')]", + "type": "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', parameters('serverName'), 'allow-all-azure-internal-IPs')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "0.0.0.0" + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, + { + "type": "Microsoft.DBforPostgreSQL/flexibleServers/configurations", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', parameters('serverName'), 'azure.extensions')]", + "properties": { + "value": "pg_diskann", + "source": "user-override" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-IPs')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-azure-internal-IPs')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + } + ], + "outputs": { + "postgresDbOutput": { + "type": "object", + "value": { + "postgresSQLName": "[parameters('serverName')]", + "postgreSQLServerName": "[format('{0}.postgres.database.azure.com', parameters('serverName'))]", + "postgreSQLDatabaseName": "postgres", + "postgreSQLDbUser": "[parameters('administratorLogin')]", + "postgreSQLDbPwd": "[parameters('administratorLoginPassword')]", + "sslMode": "Require" + } + } + } + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" + ] + }, { "condition": "[or(parameters('useKeyVault'), equals(parameters('authType'), 'rbac'))]", "type": "Microsoft.Resources/deployments", @@ -869,7 +1201,7 @@ "value": "[parameters('principalId')]" }, "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]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.objectId]" } }, "template": { @@ -878,8 +1210,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "13364147767022226969" + "version": "0.30.23.60470", + "templateHash": "12623823360057072578" }, "description": "Creates an Azure Key Vault." }, @@ -937,6 +1269,7 @@ } }, "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]" ] }, @@ -978,8 +1311,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5846053745240336221" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1137,8 +1470,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5846053745240336221" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1290,8 +1623,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1361,8 +1694,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1432,8 +1765,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1503,8 +1836,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -1578,8 +1911,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5846053745240336221" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -1737,9 +2070,11 @@ "value": "[parameters('speechServiceName')]" }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", - "cosmosAccountName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName]" - }, + "cosmosAccountName": "[if(equals(parameters('databaseType'), 'cosmos'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName), createObject('value', ''))]", + "postgresServerName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), createObject('value', ''))]", + "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', 'postgres'), createObject('value', ''))]", + "postgresDatabaseAdminUserName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser), createObject('value', ''))]", + "postgresDatabaseAdminPassword": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd), createObject('value', ''))]", "rgName": { "value": "[variables('rgName')]" } @@ -1750,8 +2085,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "9526483378278704058" + "version": "0.30.23.60470", + "templateHash": "4301444608123274670" } }, "parameters": { @@ -1791,6 +2126,26 @@ "type": "string", "defaultValue": "" }, + "postgresServerName": { + "type": "string", + "defaultValue": "" + }, + "postgresDatabaseName": { + "type": "string", + "defaultValue": "postgres" + }, + "postgresInfoName": { + "type": "string", + "defaultValue": "AZURE-POSTGRESQL-INFO" + }, + "postgresDatabaseAdminUserName": { + "type": "string", + "defaultValue": "" + }, + "postgresDatabaseAdminPassword": { + "type": "string", + "defaultValue": "" + }, "storageAccountKeyName": { "type": "string", "defaultValue": "AZURE-STORAGE-ACCOUNT-KEY" @@ -1887,11 +2242,21 @@ } }, { + "condition": "[not(equals(parameters('postgresServerName'), ''))]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2022-07-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('postgresInfoName'))]", + "properties": { + "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', parameters('postgresDatabaseAdminUserName'), 'dbname', parameters('postgresDatabaseName'), 'host', parameters('postgresServerName'), 'password', parameters('postgresDatabaseAdminPassword'))), '')]" + } + }, + { + "condition": "[not(equals(parameters('cosmosAccountName'), ''))]", "type": "Microsoft.KeyVault/vaults/secrets", "apiVersion": "2022-07-01", "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('cosmosAccountKeyName'))]", "properties": { - "value": "[listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosAccountName')), '2022-08-15').primaryMasterKey]" + "value": "[if(not(equals(parameters('cosmosAccountName'), '')), listKeys(resourceId(subscription().subscriptionId, parameters('rgName'), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosAccountName')), '2022-08-15').primaryMasterKey, '')]" } } ], @@ -1926,7 +2291,11 @@ }, "COSMOS_ACCOUNT_KEY_NAME": { "type": "string", - "value": "[parameters('cosmosAccountKeyName')]" + "value": "[if(not(equals(parameters('cosmosAccountName'), '')), parameters('cosmosAccountKeyName'), '')]" + }, + "POSTGRESQL_INFO_NAME": { + "type": "string", + "value": "[if(not(equals(parameters('postgresServerName'), '')), parameters('postgresInfoName'), '')]" } } } @@ -1937,6 +2306,7 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('storageAccountName'))]" @@ -1984,8 +2354,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "12402174270479558945" + "version": "0.30.23.60470", + "templateHash": "13584246975784398226" }, "description": "Creates an Azure AI Search instance." }, @@ -2153,8 +2523,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "11168587044178660695" + "version": "0.30.23.60470", + "templateHash": "9286637480882627742" }, "description": "Creates an Azure App Service plan." }, @@ -2267,14 +2637,22 @@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName')), '2022-09-01').outputs.name.value]" }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", + "databaseType": { + "value": "[parameters('databaseType')]" + }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", - "cosmosDBKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'cosmos'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'postgres'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", "useKeyVault": { "value": "[parameters('useKeyVault')]" }, @@ -2283,56 +2661,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", - "AZURE_OPENAI_TOP_P": "[parameters('azureOpenAITopP')]", - "AZURE_OPENAI_MAX_TOKENS": "[parameters('azureOpenAIMaxTokens')]", - "AZURE_OPENAI_STOP_SEQUENCE": "[parameters('azureOpenAIStopSequence')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_OPENAI_STREAM": "[parameters('azureOpenAIStream')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_SEARCH_USE_SEMANTIC_SEARCH": "[parameters('azureSearchUseSemanticSearch')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_CONVERSATIONS_LOG_INDEX": "[parameters('azureSearchConversationLogIndex')]", - "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG": "[parameters('azureSearchSemanticSearchConfig')]", - "AZURE_SEARCH_INDEX_IS_PRECHUNKED": "[parameters('azureSearchIndexIsPrechunked')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_SEARCH_ENABLE_IN_DOMAIN": "[parameters('azureSearchEnableInDomain')]", - "AZURE_SEARCH_FILENAME_COLUMN": "[parameters('azureSearchFilenameColumn')]", - "AZURE_SEARCH_FILTER": "[parameters('azureSearchFilter')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "AZURE_SEARCH_URL_COLUMN": "[parameters('azureSearchUrlColumn')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "AZURE_SPEECH_SERVICE_NAME": "[parameters('speechServiceName')]", - "AZURE_SPEECH_SERVICE_REGION": "[parameters('location')]", - "AZURE_SPEECH_RECOGNIZER_LANGUAGES": "[parameters('recognizedLanguages')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "ADVANCED_IMAGE_PROCESSING_MAX_IMAGES": "[parameters('advancedImageProcessingMaxImages')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "CONVERSATION_FLOW": "[parameters('conversationFlow')]", - "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_COSMOSDB_INFO": "[string(createObject('accountName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName))]", - "AZURE_COSMOSDB_ENABLE_FEEDBACK": true, - "CHAT_HISTORY_ENABLED": "[parameters('chatHistoryEnabled')]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRES_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -2341,8 +2670,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "9347651394814311894" + "version": "0.30.23.60470", + "templateHash": "64857797928052295" } }, "parameters": { @@ -2423,11 +2752,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -2462,9 +2791,17 @@ "type": "string", "defaultValue": "" }, + "databaseType": { + "type": "string", + "defaultValue": "cosmos" + }, "cosmosDBKeyName": { "type": "string", "defaultValue": "" + }, + "postgresInfoName": { + "type": "string", + "defaultValue": "" } }, "resources": [ @@ -2498,7 +2835,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'cosmos'), createObject('DATABASE_TYPE', 'cosmos', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'postgres', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -2523,8 +2860,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14818871229133632920" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -2750,8 +3087,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "3955925289075906039" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -2828,8 +3165,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -2897,8 +3234,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -2966,8 +3303,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3035,8 +3372,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3101,8 +3438,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "17352167468248267479" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -3175,8 +3512,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "8033637033572984239" + "version": "0.30.23.60470", + "templateHash": "2622922268469466870" }, "description": "Creates a SQL role assignment under an Azure Cosmos DB account." }, @@ -3236,6 +3573,7 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'monitoring')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName'))]", @@ -3295,14 +3633,22 @@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName')), '2022-09-01').outputs.name.value]" }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", + "databaseType": { + "value": "[parameters('databaseType')]" + }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", - "cosmosDBKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'cosmos'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'postgres'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", "useKeyVault": { "value": "[parameters('useKeyVault')]" }, @@ -3311,56 +3657,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", - "AZURE_OPENAI_TOP_P": "[parameters('azureOpenAITopP')]", - "AZURE_OPENAI_MAX_TOKENS": "[parameters('azureOpenAIMaxTokens')]", - "AZURE_OPENAI_STOP_SEQUENCE": "[parameters('azureOpenAIStopSequence')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_OPENAI_STREAM": "[parameters('azureOpenAIStream')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_SEARCH_USE_SEMANTIC_SEARCH": "[parameters('azureSearchUseSemanticSearch')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_CONVERSATIONS_LOG_INDEX": "[parameters('azureSearchConversationLogIndex')]", - "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG": "[parameters('azureSearchSemanticSearchConfig')]", - "AZURE_SEARCH_INDEX_IS_PRECHUNKED": "[parameters('azureSearchIndexIsPrechunked')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_SEARCH_ENABLE_IN_DOMAIN": "[parameters('azureSearchEnableInDomain')]", - "AZURE_SEARCH_FILENAME_COLUMN": "[parameters('azureSearchFilenameColumn')]", - "AZURE_SEARCH_FILTER": "[parameters('azureSearchFilter')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "AZURE_SEARCH_URL_COLUMN": "[parameters('azureSearchUrlColumn')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "AZURE_SPEECH_SERVICE_NAME": "[parameters('speechServiceName')]", - "AZURE_SPEECH_SERVICE_REGION": "[parameters('location')]", - "AZURE_SPEECH_RECOGNIZER_LANGUAGES": "[parameters('recognizedLanguages')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "ADVANCED_IMAGE_PROCESSING_MAX_IMAGES": "[parameters('advancedImageProcessingMaxImages')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "CONVERSATION_FLOW": "[parameters('conversationFlow')]", - "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_COSMOSDB_INFO": "[string(createObject('accountName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName))]", - "AZURE_COSMOSDB_ENABLE_FEEDBACK": true, - "CHAT_HISTORY_ENABLED": "[parameters('chatHistoryEnabled')]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -3369,8 +3666,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "9347651394814311894" + "version": "0.30.23.60470", + "templateHash": "64857797928052295" } }, "parameters": { @@ -3451,11 +3748,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -3490,9 +3787,17 @@ "type": "string", "defaultValue": "" }, + "databaseType": { + "type": "string", + "defaultValue": "cosmos" + }, "cosmosDBKeyName": { "type": "string", "defaultValue": "" + }, + "postgresInfoName": { + "type": "string", + "defaultValue": "" } }, "resources": [ @@ -3526,7 +3831,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1), 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'cosmos'), createObject('DATABASE_TYPE', 'cosmos', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'postgres', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -3551,8 +3856,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14818871229133632920" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -3778,8 +4083,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "3955925289075906039" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -3856,8 +4161,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3925,8 +4230,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -3994,8 +4299,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4063,8 +4368,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4129,8 +4434,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "17352167468248267479" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -4203,8 +4508,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "8033637033572984239" + "version": "0.30.23.60470", + "templateHash": "2622922268469466870" }, "description": "Creates a SQL role assignment under an Azure Cosmos DB account." }, @@ -4264,6 +4569,7 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'monitoring')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName'))]", @@ -4324,8 +4630,12 @@ }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", @@ -4339,13 +4649,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", @@ -4392,8 +4699,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "16426772879193976216" + "version": "0.30.23.60470", + "templateHash": "12567732396765618168" } }, "parameters": { @@ -4474,11 +4781,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -4554,7 +4861,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -4563,8 +4870,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14818871229133632920" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -4790,8 +5097,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "3955925289075906039" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -4868,8 +5175,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -4937,8 +5244,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5006,8 +5313,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5075,8 +5382,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5141,8 +5448,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "17352167468248267479" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -5271,8 +5578,12 @@ }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", @@ -5286,13 +5597,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", @@ -5339,8 +5647,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "16426772879193976216" + "version": "0.30.23.60470", + "templateHash": "12567732396765618168" } }, "parameters": { @@ -5421,11 +5729,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -5501,7 +5809,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -5510,8 +5818,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14818871229133632920" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -5737,8 +6045,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "3955925289075906039" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -5815,8 +6123,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5884,8 +6192,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -5953,8 +6261,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -6022,8 +6330,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -6088,8 +6396,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "17352167468248267479" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -6203,8 +6511,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "8473455776229346647" + "version": "0.30.23.60470", + "templateHash": "2390666818608223959" }, "description": "Creates an Application Insights instance and a Log Analytics workspace." }, @@ -6255,8 +6563,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "15449976264810996474" + "version": "0.30.23.60470", + "templateHash": "19694557100387265" }, "description": "Creates a Log Analytics workspace." }, @@ -6336,8 +6644,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "16358460762600875186" + "version": "0.30.23.60470", + "templateHash": "16993757720869129667" }, "description": "Creates an Application Insights instance based on an existing Log Analytics workspace." }, @@ -6401,8 +6709,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "1003060957409338499" + "version": "0.30.23.60470", + "templateHash": "12524466040979787143" }, "description": "Creates a dashboard for an Application Insights instance." }, @@ -7740,8 +8048,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "12632171944221294691" + "version": "0.30.23.60470", + "templateHash": "15151749822990864279" } }, "parameters": { @@ -7823,8 +8131,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "10154909114565024920" + "version": "0.30.23.60470", + "templateHash": "15030863077610448627" } }, "parameters": { @@ -7965,8 +8273,12 @@ "value": "[variables('clientKey')]" }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", @@ -7980,13 +8292,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", @@ -8019,8 +8328,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "9410273585702095132" + "version": "0.30.23.60470", + "templateHash": "17678067728265319370" } }, "parameters": { @@ -8096,11 +8405,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -8206,7 +8515,7 @@ "value": "[parameters('useKeyVault')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -8215,8 +8524,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "7133078529690530611" + "version": "0.30.23.60470", + "templateHash": "8206949151292074536" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -8426,8 +8735,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14818871229133632920" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -8653,8 +8962,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "3955925289075906039" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -8730,8 +9039,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8817,8 +9126,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8886,8 +9195,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -8955,8 +9264,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -9024,8 +9333,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -9093,8 +9402,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -9159,8 +9468,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "17352167468248267479" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -9288,8 +9597,12 @@ "value": "[variables('clientKey')]" }, "openAIKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.OPENAI_KEY_NAME.value), createObject('value', ''))]", - "storageAccountKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "formRecognizerKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value), createObject('value', ''))]", + "azureBlobStorageInfo": { + "value": "[string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY')))]" + }, + "azureFormRecognizerInfo": { + "value": "[string(createObject('endpoint', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY')))]" + }, "searchKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SEARCH_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", @@ -9303,13 +9616,10 @@ }, "appSettings": { "value": { - "AZURE_BLOB_ACCOUNT_NAME": "[parameters('storageAccountName')]", - "AZURE_BLOB_CONTAINER_NAME": "[variables('blobContainerName')]", "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_FORM_RECOGNIZER_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]", "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", @@ -9342,8 +9652,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "9410273585702095132" + "version": "0.30.23.60470", + "templateHash": "17678067728265319370" } }, "parameters": { @@ -9419,11 +9729,11 @@ "type": "string", "defaultValue": "" }, - "storageAccountKeyName": { + "azureBlobStorageInfo": { "type": "string", "defaultValue": "" }, - "formRecognizerKeyName": { + "azureFormRecognizerInfo": { "type": "string", "defaultValue": "" }, @@ -9529,7 +9839,7 @@ "value": "[parameters('useKeyVault')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_ACCOUNT_KEY', if(parameters('useKeyVault'), parameters('storageAccountKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value), 'AZURE_FORM_RECOGNIZER_KEY', if(parameters('useKeyVault'), parameters('formRecognizerKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" + "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } }, "template": { @@ -9538,8 +9848,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "7133078529690530611" + "version": "0.30.23.60470", + "templateHash": "8206949151292074536" }, "description": "Creates an Azure Function in an existing Azure App Service plan." }, @@ -9749,8 +10059,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14818871229133632920" + "version": "0.30.23.60470", + "templateHash": "7732628295698757767" }, "description": "Creates an Azure App Service in an existing Azure App Service plan." }, @@ -9976,8 +10286,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "3955925289075906039" + "version": "0.30.23.60470", + "templateHash": "16930852302813854027" }, "description": "Updates app settings for an Azure App Service." }, @@ -10053,8 +10363,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10140,8 +10450,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10209,8 +10519,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10278,8 +10588,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10347,8 +10657,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10416,8 +10726,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -10482,8 +10792,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "17352167468248267479" + "version": "0.30.23.60470", + "templateHash": "465622386717580763" }, "description": "Assigns an Azure Key Vault access policy." }, @@ -10588,8 +10898,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5846053745240336221" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10743,8 +11053,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5846053745240336221" + "version": "0.30.23.60470", + "templateHash": "13123022401063321803" }, "description": "Creates an Azure Cognitive Services instance." }, @@ -10901,8 +11211,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14787323190374281342" + "version": "0.30.23.60470", + "templateHash": "6699069410959282929" } }, "parameters": { @@ -11034,8 +11344,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "17192989974061212120" + "version": "0.30.23.60470", + "templateHash": "10401188783540495741" }, "description": "Creates an Azure storage account." }, @@ -11262,8 +11572,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11332,8 +11642,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11402,8 +11712,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11472,8 +11782,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "5620801774479515492" + "version": "0.30.23.60470", + "templateHash": "14973584850527407631" }, "description": "Creates a role assignment for a service principal." }, @@ -11558,8 +11868,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.31.34.60546", - "templateHash": "14309427698097244890" + "version": "0.30.23.60470", + "templateHash": "17372485166957435450" } }, "parameters": { @@ -11668,6 +11978,7 @@ "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", "name": "deploy_create_table_script", + "resourceGroup": "[variables('rgName')]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -11675,19 +11986,19 @@ "mode": "Incremental", "parameters": { "solutionLocation": { - "value": "[variables('location')]" + "value": "[parameters('location')]" }, "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]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.id]" }, "baseUrl": { "value": "[variables('baseUrl')]" }, "keyVaultName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'keyvault'), '2022-09-01').outputs.name.value]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault'), '2022-09-01').outputs.name.value]" }, "postgresSqlServerName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" } }, "template": { @@ -11747,11 +12058,10 @@ }, "dependsOn": [ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'monitoring')]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('storageAccountName'))]" + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys')]" ] } ], @@ -11764,17 +12074,9 @@ "type": "string", "value": "[parameters('hostingModel')]" }, - "AZURE_BLOB_CONTAINER_NAME": { + "AZURE_BLOB_STORAGE_INFO": { "type": "string", - "value": "[variables('blobContainerName')]" - }, - "AZURE_BLOB_ACCOUNT_NAME": { - "type": "string", - "value": "[parameters('storageAccountName')]" - }, - "AZURE_BLOB_ACCOUNT_KEY": { - "type": "string", - "value": "[if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '')]" + "value": "[replace(string(createObject('containerName', variables('blobContainerName'), 'accountName', parameters('storageAccountName'), 'accountKey', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.STORAGE_ACCOUNT_KEY_NAME.value, '$STORAGE_ACCOUNT_KEY'))), '$STORAGE_ACCOUNT_KEY', '')]" }, "AZURE_COMPUTER_VISION_ENDPOINT": { "type": "string", @@ -11804,13 +12106,9 @@ "type": "string", "value": "[if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value, '')]" }, - "AZURE_FORM_RECOGNIZER_ENDPOINT": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value]" - }, - "AZURE_FORM_RECOGNIZER_KEY": { + "AZURE_FORM_RECOGNIZER_INFO": { "type": "string", - "value": "[if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '')]" + "value": "[replace(string(createObject('endpoint', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('formRecognizerName')), '2022-09-01').outputs.endpoint.value, 'key', if(parameters('useKeyVault'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.FORM_RECOGNIZER_KEY_NAME.value, '$FORM_RECOGNIZER_KEY'))), '$FORM_RECOGNIZER_KEY', '')]" }, "AZURE_KEY_VAULT_ENDPOINT": { "type": "string", @@ -12006,7 +12304,11 @@ }, "AZURE_COSMOSDB_INFO": { "type": "string", - "value": "[string(createObject('accountName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, 'containerName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName))]" + "value": "[string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, '')))]" + }, + "AZURE_POSTGRESDB_INFO": { + "type": "string", + "value": "[string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))]" } } -} +} \ No newline at end of file From 7fc608db4d959b3e8e5869a3722d126d3a134d54 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 17:02:09 +0530 Subject: [PATCH 063/107] fix: Lint issue --- code/tests/functional/app_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/code/tests/functional/app_config.py b/code/tests/functional/app_config.py index fa2829fb9..0dd14b2f6 100644 --- a/code/tests/functional/app_config.py +++ b/code/tests/functional/app_config.py @@ -7,6 +7,7 @@ logger = logging.getLogger(__name__) encoded_account_key = str(base64.b64encode(b"some-blob-account-key"), "utf-8") + class AppConfig: before_config: dict[str, str] = {} config: dict[str, str | None] = { From 2ee0beb9f6a1d8d5f3da23c6313f6712db8e6527 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 20:10:29 +0530 Subject: [PATCH 064/107] Added Role based assignment to connect to db for web app --- infra/app/adminweb.bicep | 2 + infra/app/web.bicep | 1 + .../database/deploy_create_table_script.bicep | 4 +- infra/main.bicep | 2 + infra/main.json | 50 ++++++++++++++++--- .../data_scripts/create_postgres_tables.py | 38 ++++++++++++++ scripts/run_create_table_script.sh | 4 ++ 7 files changed, 92 insertions(+), 9 deletions(-) diff --git a/infra/app/adminweb.bicep b/infra/app/adminweb.bicep index 56397ed6f..82f4611cc 100644 --- a/infra/app/adminweb.bicep +++ b/infra/app/adminweb.bicep @@ -28,6 +28,7 @@ param speechKeyName string = '' param authType string param dockerFullImageName string = '' param useDocker bool = dockerFullImageName != '' +param databaseType string = 'cosmos' // 'cosmos' or 'postgres' var azureFormRecognizerInfoUpdated = useKeyVault ? azureFormRecognizerInfo @@ -68,6 +69,7 @@ module adminweb '../core/host/appservice.bicep' = { scmDoBuildDuringDeployment: useDocker ? false : true applicationInsightsName: applicationInsightsName appServicePlanId: appServicePlanId + managedIdentity: databaseType == 'postgres' appSettings: union(appSettings, { AZURE_AUTH_TYPE: authType USE_KEY_VAULT: useKeyVault ? useKeyVault : '' diff --git a/infra/app/web.bicep b/infra/app/web.bicep index 90e6cb1f3..4fb24d3c0 100644 --- a/infra/app/web.bicep +++ b/infra/app/web.bicep @@ -155,6 +155,7 @@ module web '../core/host/appservice.bicep' = { dockerFullImageName: dockerFullImageName scmDoBuildDuringDeployment: useDocker ? false : true healthCheckPath: healthCheckPath + managedIdentity: databaseType == 'postgres' } } diff --git a/infra/core/database/deploy_create_table_script.bicep b/infra/core/database/deploy_create_table_script.bicep index b1fc012df..afccc8cc8 100644 --- a/infra/core/database/deploy_create_table_script.bicep +++ b/infra/core/database/deploy_create_table_script.bicep @@ -5,6 +5,8 @@ param baseUrl string param keyVaultName string param identity string param postgresSqlServerName string +param webAppPrincipalName string +param adminAppPrincipalName string resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { kind:'AzureCLI' @@ -19,7 +21,7 @@ resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { properties: { azCliVersion: '2.52.0' primaryScriptUri: '${baseUrl}scripts/run_create_table_script.sh' - arguments: '${baseUrl} ${keyVaultName} ${resourceGroup().name} ${postgresSqlServerName}' // Specify any arguments for the script + arguments: '${baseUrl} ${keyVaultName} ${resourceGroup().name} ${postgresSqlServerName} ${webAppPrincipalName} ${adminAppPrincipalName}' // Specify any arguments for the script timeout: 'PT1H' // Specify the desired timeout duration retentionInterval: 'PT1H' // Specify the desired retention interval cleanupPreference:'OnSuccess' diff --git a/infra/main.bicep b/infra/main.bicep index f4696a2a9..1a3901656 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1238,6 +1238,8 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (dat baseUrl:baseUrl keyVaultName:keyvault.outputs.name postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName + webAppPrincipalName: web_docker.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID + adminAppPrincipalName: adminweb_docker.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID } scope: rg dependsOn:[keyvault, postgresDBModule, storekeys] diff --git a/infra/main.json b/infra/main.json index 314c1f8d3..dd98b37ff 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "16387401552448615008" + "templateHash": "9521961093814280778" } }, "parameters": { @@ -2671,7 +2671,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "64857797928052295" + "templateHash": "8216675081949619536" } }, "parameters": { @@ -2852,6 +2852,9 @@ "scmDoBuildDuringDeployment": "[if(parameters('useDocker'), createObject('value', false()), createObject('value', true()))]", "healthCheckPath": { "value": "[parameters('healthCheckPath')]" + }, + "managedIdentity": { + "value": "[equals(parameters('databaseType'), 'postgres')]" } }, "template": { @@ -3667,7 +3670,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "64857797928052295" + "templateHash": "8216675081949619536" } }, "parameters": { @@ -3848,6 +3851,9 @@ "scmDoBuildDuringDeployment": "[if(parameters('useDocker'), createObject('value', false()), createObject('value', true()))]", "healthCheckPath": { "value": "[parameters('healthCheckPath')]" + }, + "managedIdentity": { + "value": "[equals(parameters('databaseType'), 'postgres')]" } }, "template": { @@ -4700,7 +4706,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12567732396765618168" + "templateHash": "14757216181799932354" } }, "parameters": { @@ -4815,6 +4821,10 @@ "useDocker": { "type": "bool", "defaultValue": "[not(equals(parameters('dockerFullImageName'), ''))]" + }, + "databaseType": { + "type": "string", + "defaultValue": "cosmos" } }, "resources": [ @@ -4860,6 +4870,9 @@ "appServicePlanId": { "value": "[parameters('appServicePlanId')]" }, + "managedIdentity": { + "value": "[equals(parameters('databaseType'), 'postgres')]" + }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } @@ -5648,7 +5661,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12567732396765618168" + "templateHash": "14757216181799932354" } }, "parameters": { @@ -5763,6 +5776,10 @@ "useDocker": { "type": "bool", "defaultValue": "[not(equals(parameters('dockerFullImageName'), ''))]" + }, + "databaseType": { + "type": "string", + "defaultValue": "cosmos" } }, "resources": [ @@ -5808,6 +5825,9 @@ "appServicePlanId": { "value": "[parameters('appServicePlanId')]" }, + "managedIdentity": { + "value": "[equals(parameters('databaseType'), 'postgres')]" + }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } @@ -11999,6 +12019,12 @@ }, "postgresSqlServerName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" + }, + "webAppPrincipalName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID.value]" + }, + "adminAppPrincipalName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID.value]" } }, "template": { @@ -12008,7 +12034,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "754015545513025215" + "templateHash": "12297908402052822068" } }, "parameters": { @@ -12029,6 +12055,12 @@ }, "postgresSqlServerName": { "type": "string" + }, + "webAppPrincipalName": { + "type": "string" + }, + "adminAppPrincipalName": { + "type": "string" } }, "resources": [ @@ -12047,7 +12079,7 @@ "properties": { "azCliVersion": "2.52.0", "primaryScriptUri": "[format('{0}scripts/run_create_table_script.sh', parameters('baseUrl'))]", - "arguments": "[format('{0} {1} {2} {3}', parameters('baseUrl'), parameters('keyVaultName'), resourceGroup().name, parameters('postgresSqlServerName'))]", + "arguments": "[format('{0} {1} {2} {3} {4} {5}', parameters('baseUrl'), parameters('keyVaultName'), resourceGroup().name, parameters('postgresSqlServerName'), parameters('webAppPrincipalName'), parameters('adminAppPrincipalName'))]", "timeout": "PT1H", "retentionInterval": "PT1H", "cleanupPreference": "OnSuccess" @@ -12057,11 +12089,13 @@ } }, "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName')))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", - "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys')]" + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName')))]" ] } ], diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 92df9fa93..2f125644a 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -2,8 +2,11 @@ from azure.keyvault.secrets import SecretClient from azure.identity import DefaultAzureCredential import psycopg2 +from psycopg2 import sql key_vault_name = "kv_to-be-replaced" +principal_name = "webAppPrincipalName" +admin_principal_name = "adminAppPrincipalName" def get_secrets_from_kv(kv_name, secret_name): credential = DefaultAzureCredential() @@ -13,6 +16,37 @@ def get_secrets_from_kv(kv_name, secret_name): return secret_client.get_secret(secret_name).value +def grant_permissions(cursor, dbname, schema_name, principal_name): + """ + Grants database and schema-level permissions to a specified principal. + + Parameters: + - cursor: psycopg2 cursor object for database operations. + - dbname: Name of the database to grant CONNECT permission. + - schema_name: Name of the schema to grant table-level permissions. + - principal_name: Name of the principal (role or user) to grant permissions. + """ + # Grant CONNECT on database + grant_connect_query = sql.SQL("GRANT CONNECT ON DATABASE {database} TO {principal}") + cursor.execute( + grant_connect_query.format( + database=sql.Identifier(dbname), + principal=sql.Identifier(principal_name), + ) + ) + print(f"Granted CONNECT on database '{dbname}' to '{principal_name}'") + + # Grant SELECT, INSERT, UPDATE, DELETE on schema tables + grant_permissions_query = sql.SQL( + "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA {schema} TO {principal}" + ) + cursor.execute( + grant_permissions_query.format( + schema=sql.Identifier(schema_name), + principal=sql.Identifier(principal_name), + ) + ) + postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) host = postgres_details.get("host", "") user = postgres_details.get("user", "") @@ -30,6 +64,10 @@ def get_secrets_from_kv(kv_name, secret_name): conn = psycopg2.connect(conn_string) cursor = conn.cursor() +grant_permissions(cursor, dbname, "public", principal_name) +grant_permissions(cursor, dbname, "public", admin_principal_name) +conn.commit() + # Drop and recreate the conversations table cursor.execute("DROP TABLE IF EXISTS conversations") conn.commit() diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 1d3bd2063..6ed535644 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -8,6 +8,8 @@ requirementFile="requirements.txt" requirementFileUrl=${baseUrl}"scripts/data_scripts/requirements.txt" resourceGroup="$3" serverName="$4" +webAppPrincipalName = "$5" +adminAppPrincipalName = "$6" echo "Script Started" @@ -27,6 +29,8 @@ echo "Download completed" #Replace key vault name sed -i "s/kv_to-be-replaced/${keyvaultName}/g" "create_postgres_tables.py" +sed -i "s/webAppPrincipalName/${webAppPrincipalName}/g" "create_postgres_tables.py" +sed -i "s/adminAppPrincipalName/${adminAppPrincipalName}/g" "create_postgres_tables.py" pip install -r requirements.txt From 4e9d5ab83a2dc90d73040430ff0a2808d8b94b47 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 20:15:33 +0530 Subject: [PATCH 065/107] Added dependencies --- infra/main.bicep | 8 +++++--- infra/main.json | 12 +++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 1a3901656..9f4817b1e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1238,11 +1238,13 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (dat baseUrl:baseUrl keyVaultName:keyvault.outputs.name postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName - webAppPrincipalName: web_docker.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID - adminAppPrincipalName: adminweb_docker.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID + webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID : web_docker.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID + adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID : adminweb_docker.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID } scope: rg - dependsOn:[keyvault, postgresDBModule, storekeys] + dependsOn: hostingModel == 'code' ? [keyvault, postgresDBModule, storekeys, web, adminweb] : [ + [keyvault, postgresDBModule, storekeys, web_docker, adminweb_docker] + ] } output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString diff --git a/infra/main.json b/infra/main.json index dd98b37ff..e1ed4febf 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "9521961093814280778" + "templateHash": "831473305954833848" } }, "parameters": { @@ -12020,12 +12020,8 @@ "postgresSqlServerName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" }, - "webAppPrincipalName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID.value]" - }, - "adminAppPrincipalName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID.value]" - } + "webAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('websiteName')), '2022-09-01').outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID.value))]", + "adminAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName')), '2022-09-01').outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID.value))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -12089,12 +12085,14 @@ } }, "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName')))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('websiteName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName')))]" ] } From 68a968f7710f3f8c92037528c80622ae069d3a70 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 28 Nov 2024 20:32:22 +0530 Subject: [PATCH 066/107] Disable Computer Vision Client & Disable Integrated Vectorization When PostgreSQL Is Enabled --- .../batch/utilities/helpers/env_helper.py | 77 ++--- infra/app/storekeys.bicep | 6 +- infra/app/web.bicep | 203 ++++++------ infra/main.bicep | 299 +++++++++--------- infra/main.json | 54 ++-- 5 files changed, 336 insertions(+), 303 deletions(-) diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index 6b4d44624..e1d40a162 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -88,9 +88,46 @@ def __load_config(self, **kwargs) -> None: "AZURE_SEARCH_DATASOURCE_NAME", "" ) self.AZURE_SEARCH_INDEXER_NAME = os.getenv("AZURE_SEARCH_INDEXER_NAME", "") - self.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION = self.get_env_var_bool( - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION", "False" - ) + + # Chat History DB Integration Settings + # Set default values based on DATABASE_TYPE + self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "").strip() or "CosmosDB" + # Cosmos DB configuration + if self.DATABASE_TYPE == DatabaseType.COSMOSDB.value: + azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") + self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "") + self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "") + self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get( + "containerName", "" + ) + self.AZURE_COSMOSDB_ACCOUNT_KEY = self.secretHelper.get_secret( + "AZURE_COSMOSDB_ACCOUNT_KEY" + ) + self.AZURE_COSMOSDB_ENABLE_FEEDBACK = ( + os.getenv("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true" + ) + self.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION = self.get_env_var_bool( + "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION", "False" + ) + self.USE_ADVANCED_IMAGE_PROCESSING = self.get_env_var_bool( + "USE_ADVANCED_IMAGE_PROCESSING", "False" + ) + # PostgreSQL configuration + elif self.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: + self.AZURE_POSTGRES_SEARCH_TOP_K = self.get_env_var_int( + "AZURE_POSTGRES_SEARCH_TOP_K", 5 + ) + azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") + self.POSTGRESQL_USER = azure_postgresql_info.get("user", "") + self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "") + self.POSTGRESQL_HOST = azure_postgresql_info.get("host", "") + # Ensure integrated vectorization is disabled for PostgreSQL + self.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION = "False" + self.USE_ADVANCED_IMAGE_PROCESSING = "False" + else: + raise ValueError( + "Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'." + ) self.AZURE_AUTH_TYPE = os.getenv("AZURE_AUTH_TYPE", "keys") # Azure OpenAI @@ -147,9 +184,6 @@ def __load_config(self, **kwargs) -> None: self.AZURE_TOKEN_PROVIDER = get_bearer_token_provider( DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" ) - self.USE_ADVANCED_IMAGE_PROCESSING = self.get_env_var_bool( - "USE_ADVANCED_IMAGE_PROCESSING", "False" - ) self.ADVANCED_IMAGE_PROCESSING_MAX_IMAGES = self.get_env_var_int( "ADVANCED_IMAGE_PROCESSING_MAX_IMAGES", 1 ) @@ -294,37 +328,6 @@ def __load_config(self, **kwargs) -> None: self.PROMPT_FLOW_DEPLOYMENT_NAME = os.getenv("PROMPT_FLOW_DEPLOYMENT_NAME", "") - # Chat History DB Integration Settings - # Set default values based on DATABASE_TYPE - self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "").strip() or "CosmosDB" - # Cosmos DB configuration - if self.DATABASE_TYPE == DatabaseType.COSMOSDB.value: - azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") - self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "") - self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "") - self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get( - "containerName", "" - ) - self.AZURE_COSMOSDB_ACCOUNT_KEY = self.secretHelper.get_secret( - "AZURE_COSMOSDB_ACCOUNT_KEY" - ) - self.AZURE_COSMOSDB_ENABLE_FEEDBACK = ( - os.getenv("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true" - ) - # PostgreSQL configuration - elif self.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: - self.AZURE_POSTGRES_SEARCH_TOP_K = self.get_env_var_int( - "AZURE_POSTGRES_SEARCH_TOP_K", 5 - ) - azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "") - self.POSTGRESQL_USER = azure_postgresql_info.get("user", "") - self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "") - self.POSTGRESQL_HOST = azure_postgresql_info.get("host", "") - else: - raise ValueError( - "Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'." - ) - def is_chat_model(self): if "gpt-4" in self.AZURE_OPENAI_MODEL_NAME.lower(): return True diff --git a/infra/app/storekeys.bicep b/infra/app/storekeys.bicep index deca84872..fa9f54c02 100644 --- a/infra/app/storekeys.bicep +++ b/infra/app/storekeys.bicep @@ -7,9 +7,9 @@ param formRecognizerName string = '' param contentSafetyName string = '' param speechServiceName string = '' param computerVisionName string = '' -param postgresServerName string = '' // PostgreSQL server name -param postgresDatabaseName string = 'postgres' // Default database name -param postgresInfoName string = 'AZURE-POSTGRESQL-INFO' // Secret name for PostgreSQL info +param postgresServerName string = '' // PostgreSQL server name +param postgresDatabaseName string = 'PostgreSQL' // Default database name +param postgresInfoName string = 'AZURE-POSTGRESQL-INFO' // Secret name for PostgreSQL info param postgresDatabaseAdminUserName string = '' param postgresDatabaseAdminPassword string = '' param storageAccountKeyName string = 'AZURE-STORAGE-ACCOUNT-KEY' diff --git a/infra/app/web.bicep b/infra/app/web.bicep index 90e6cb1f3..90b50086c 100644 --- a/infra/app/web.bicep +++ b/infra/app/web.bicep @@ -31,52 +31,62 @@ param useDocker bool = dockerFullImageName != '' param healthCheckPath string = '' // Database parameters -param databaseType string = 'cosmos' // 'cosmos' or 'postgres' +param databaseType string = 'CosmosDB' // 'CosmosDB' or 'PostgreSQL' param cosmosDBKeyName string = '' param postgresInfoName string = '' var azureFormRecognizerInfoUpdated = useKeyVault ? azureFormRecognizerInfo - : replace(azureFormRecognizerInfo, '$FORM_RECOGNIZER_KEY', listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - formRecognizerName - ), - '2023-05-01' - ).key1) + : replace( + azureFormRecognizerInfo, + '$FORM_RECOGNIZER_KEY', + listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + formRecognizerName + ), + '2023-05-01' + ).key1 + ) var azureBlobStorageInfoUpdated = useKeyVault ? azureBlobStorageInfo - : replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY', listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.Storage/storageAccounts', - storageAccountName - ), - '2021-09-01' - ).keys[0].value) - -// Database-specific settings -var databaseSettings = databaseType == 'cosmos' ? { - DATABASE_TYPE: 'cosmos' - AZURE_COSMOSDB_ACCOUNT_KEY: (useKeyVault || cosmosDBKeyName == '') - ? cosmosDBKeyName - : listKeys( + : replace( + azureBlobStorageInfo, + '$STORAGE_ACCOUNT_KEY', + listKeys( resourceId( subscription().subscriptionId, resourceGroup().name, - 'Microsoft.DocumentDB/databaseAccounts', - cosmosDBKeyName + 'Microsoft.Storage/storageAccounts', + storageAccountName ), - '2022-08-15' - ).primaryMasterKey -} : { - DATABASE_TYPE: 'postgres' - AZURE_POSTGRESQL_INFO: useKeyVault ? postgresInfoName : '' -} + '2021-09-01' + ).keys[0].value + ) + +// Database-specific settings +var databaseSettings = databaseType == 'CosmosDB' + ? { + DATABASE_TYPE: 'CosmosDB' + AZURE_COSMOSDB_ACCOUNT_KEY: (useKeyVault || cosmosDBKeyName == '') + ? cosmosDBKeyName + : listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.DocumentDB/databaseAccounts', + cosmosDBKeyName + ), + '2022-08-15' + ).primaryMasterKey + } + : { + DATABASE_TYPE: 'PostgreSQL' + AZURE_POSTGRESQL_INFO: useKeyVault ? postgresInfoName : '' + } module web '../core/host/appservice.bicep' = { name: '${name}-app-module' @@ -88,67 +98,70 @@ module web '../core/host/appservice.bicep' = { appCommandLine: useDocker ? '' : appCommandLine applicationInsightsName: applicationInsightsName appServicePlanId: appServicePlanId - appSettings: union(appSettings, union(databaseSettings, { - AZURE_AUTH_TYPE: authType - USE_KEY_VAULT: useKeyVault ? useKeyVault : '' - AZURE_OPENAI_API_KEY: useKeyVault - ? openAIKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - azureOpenAIName - ), - '2023-05-01' - ).key1 - AZURE_SEARCH_KEY: useKeyVault - ? searchKeyName - : listAdminKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.Search/searchServices', - azureAISearchName - ), - '2021-04-01-preview' - ).primaryKey - AZURE_BLOB_STORAGE_INFO: azureBlobStorageInfoUpdated - AZURE_FORM_RECOGNIZER_INFO: azureFormRecognizerInfoUpdated - AZURE_CONTENT_SAFETY_KEY: useKeyVault - ? contentSafetyKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - contentSafetyName - ), - '2023-05-01' - ).key1 - AZURE_SPEECH_SERVICE_KEY: useKeyVault - ? speechKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - speechServiceName - ), - '2023-05-01' - ).key1 - AZURE_COMPUTER_VISION_KEY: (useKeyVault || computerVisionName == '') - ? computerVisionKeyName - : listKeys( - resourceId( - subscription().subscriptionId, - resourceGroup().name, - 'Microsoft.CognitiveServices/accounts', - computerVisionName - ), - '2023-05-01' - ).key1 - })) + appSettings: union( + appSettings, + union(databaseSettings, { + AZURE_AUTH_TYPE: authType + USE_KEY_VAULT: useKeyVault ? useKeyVault : '' + AZURE_OPENAI_API_KEY: useKeyVault + ? openAIKeyName + : listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + azureOpenAIName + ), + '2023-05-01' + ).key1 + AZURE_SEARCH_KEY: useKeyVault + ? searchKeyName + : listAdminKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.Search/searchServices', + azureAISearchName + ), + '2021-04-01-preview' + ).primaryKey + AZURE_BLOB_STORAGE_INFO: azureBlobStorageInfoUpdated + AZURE_FORM_RECOGNIZER_INFO: azureFormRecognizerInfoUpdated + AZURE_CONTENT_SAFETY_KEY: useKeyVault + ? contentSafetyKeyName + : listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + contentSafetyName + ), + '2023-05-01' + ).key1 + AZURE_SPEECH_SERVICE_KEY: useKeyVault + ? speechKeyName + : listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + speechServiceName + ), + '2023-05-01' + ).key1 + AZURE_COMPUTER_VISION_KEY: (useKeyVault || computerVisionName == '') + ? computerVisionKeyName + : listKeys( + resourceId( + subscription().subscriptionId, + resourceGroup().name, + 'Microsoft.CognitiveServices/accounts', + computerVisionName + ), + '2023-05-01' + ).key1 + }) + ) keyVaultName: keyVaultName runtimeName: runtimeName runtimeVersion: runtimeVersion diff --git a/infra/main.bicep b/infra/main.bicep index f4696a2a9..f164aeb4b 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -102,7 +102,7 @@ param azureSearchOffsetColumn string = 'offset' @description('Url column') param azureSearchUrlColumn string = 'url' -@description('Use Azure Search Integrated Vectorization') +@description('Whether to use Azure Search Integrated Vectorization. If the database type is PostgreSQL, set this to false.') param azureSearchUseIntegratedVectorization bool = false @description('Name of Azure OpenAI Resource') @@ -123,7 +123,7 @@ param azureOpenAIModelVersion string = '0613' @description('Azure OpenAI Model Capacity - See here for more info https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/quota') param azureOpenAIModelCapacity int = 30 -@description('Enables the use of a vision LLM and Computer Vision for embedding images') +@description('Whether to enable the use of a vision LLM and Computer Vision for embedding images. If the database type is PostgreSQL, set this to false.') param useAdvancedImageProcessing bool = false @description('The maximum number of images to pass to the vision model in a single request') @@ -303,11 +303,10 @@ param azureMachineLearningName string = 'aml-${resourceToken}' @description('The type of database to deploy (cosmos or postgres)') @allowed([ - 'cosmos' - 'postgres' + 'CosmosDB' + 'PostgreSQL' ]) -param databaseType string = 'cosmos' - +param databaseType string = 'CosmosDB' @description('Azure Cosmos DB Account Name') param azureCosmosDBAccountName string = 'cosmos-${resourceToken}' @@ -334,8 +333,8 @@ var azureOpenAIEmbeddingModelInfo = string({ modelVersion: azureOpenAIEmbeddingModelVersion }) -var appversion = 'latest' // Update GIT deployment branch -var registryName = 'fruoccopublic' // Update Registry name +var appversion = 'latest' // Update GIT deployment branch +var registryName = 'fruoccopublic' // Update Registry name // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -363,7 +362,7 @@ module cosmosDBModule './core/database/cosmosdb.bicep' = if (databaseType == 'co scope: rg } -module postgresDBModule './core/database/postgresdb.bicep' = if (databaseType == 'postgres') { +module postgresDBModule './core/database/postgresdb.bicep' = if (databaseType == 'PostgreSQL') { name: 'deploy_postgres_sql' params: { solutionName: azurePostgresDBAccountName @@ -532,11 +531,17 @@ module storekeys './app/storekeys.bicep' = if (useKeyVault) { contentSafetyName: contentsafety.outputs.name speechServiceName: speechServiceName computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' - cosmosAccountName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosAccountName : '' - postgresServerName: databaseType == 'postgres' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName : '' - postgresDatabaseName: databaseType == 'postgres' ? 'postgres' : '' - postgresDatabaseAdminUserName: databaseType == 'postgres' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser : '' - postgresDatabaseAdminPassword: databaseType == 'postgres' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbPwd : '' + cosmosAccountName: databaseType == 'CosmosDB' ? cosmosDBModule.outputs.cosmosOutput.cosmosAccountName : '' + postgresServerName: databaseType == 'PostgreSQL' + ? postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + : '' + postgresDatabaseName: databaseType == 'PostgreSQL' ? 'PostgreSQL' : '' + postgresDatabaseAdminUserName: databaseType == 'PostgreSQL' + ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser + : '' + postgresDatabaseAdminPassword: databaseType == 'PostgreSQL' + ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbPwd + : '' rgName: rgName } } @@ -578,9 +583,9 @@ module hostingplan './core/host/appserviceplan.bicep' = { } var azureCosmosDBInfo = string({ - accountName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosAccountName : '' - databaseName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosDatabaseName : '' - containerName: databaseType == 'cosmos' ? cosmosDBModule.outputs.cosmosOutput.cosmosContainerName : '' + accountName: databaseType == 'CosmosDB' ? cosmosDBModule.outputs.cosmosOutput.cosmosAccountName : '' + databaseName: databaseType == 'CosmosDB' ? cosmosDBModule.outputs.cosmosOutput.cosmosDatabaseName : '' + containerName: databaseType == 'CosmosDB' ? cosmosDBModule.outputs.cosmosOutput.cosmosContainerName : '' }) var azurePostgresDBInfo = string({ @@ -611,7 +616,7 @@ module web './app/web.bicep' = if (hostingModel == 'code') { computerVisionName: useAdvancedImageProcessing ? computerVision.outputs.name : '' // New database-related parameters - databaseType: databaseType // Add this parameter to specify 'postgres' or 'cosmos' + databaseType: databaseType // Add this parameter to specify 'PostgreSQL' or 'CosmosDB' // Conditional key vault key names openAIKeyName: useKeyVault ? storekeys.outputs.OPENAI_KEY_NAME : '' @@ -623,68 +628,74 @@ module web './app/web.bicep' = if (hostingModel == 'code') { computerVisionKeyName: useKeyVault ? storekeys.outputs.COMPUTER_VISION_KEY_NAME : '' // Conditionally set database key names - cosmosDBKeyName: databaseType == 'cosmos' && useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' - postgresInfoName: databaseType == 'postgres' && useKeyVault ? storekeys.outputs.POSTGRESQL_INFO_NAME : '' + cosmosDBKeyName: databaseType == 'CosmosDB' && useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' + postgresInfoName: databaseType == 'PostgreSQL' && useKeyVault ? storekeys.outputs.POSTGRESQL_INFO_NAME : '' useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType - appSettings: union({ - // Existing app settings - AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion - AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_OPENAI_RESOURCE: azureOpenAIResourceName - AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo - AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature - AZURE_OPENAI_TOP_P: azureOpenAITopP - AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens - AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence - AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_OPENAI_STREAM: azureOpenAIStream - AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo - AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch - AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' - AZURE_SEARCH_INDEX: azureSearchIndex - AZURE_SEARCH_CONVERSATIONS_LOG_INDEX: azureSearchConversationLogIndex - AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig - AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked - AZURE_SEARCH_TOP_K: azureSearchTopK - AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain - AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn - AZURE_SEARCH_FILTER: azureSearchFilter - AZURE_SEARCH_FIELDS_ID: azureSearchFieldId - AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn - AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn - AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn - AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata - AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn - AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn - AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn - AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn - AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization - AZURE_SPEECH_SERVICE_NAME: speechServiceName - AZURE_SPEECH_SERVICE_REGION: location - AZURE_SPEECH_RECOGNIZER_LANGUAGES: recognizedLanguages - USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing - ADVANCED_IMAGE_PROCESSING_MAX_IMAGES: advancedImageProcessingMaxImages - ORCHESTRATION_STRATEGY: orchestrationStrategy - CONVERSATION_FLOW: conversationFlow - LOGLEVEL: logLevel - - // Add database type to settings - AZURE_DATABASE_TYPE: databaseType - }, - // Conditionally add database-specific settings - databaseType == 'cosmos' ? { - AZURE_COSMOSDB_INFO: azureCosmosDBInfo - AZURE_COSMOSDB_ENABLE_FEEDBACK: true - } : databaseType == 'postgres' ? { - AZURE_POSTGRES_INFO: azurePostgresDBInfo - } : {}) + appSettings: union( + { + // Existing app settings + AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion + AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo + AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature + AZURE_OPENAI_TOP_P: azureOpenAITopP + AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens + AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_OPENAI_STREAM: azureOpenAIStream + AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo + AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_CONVERSATIONS_LOG_INDEX: azureSearchConversationLogIndex + AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig + AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked + AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain + AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn + AZURE_SEARCH_FILTER: azureSearchFilter + AZURE_SEARCH_FIELDS_ID: azureSearchFieldId + AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn + AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata + AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn + AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn + AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn + AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn + AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization + AZURE_SPEECH_SERVICE_NAME: speechServiceName + AZURE_SPEECH_SERVICE_REGION: location + AZURE_SPEECH_RECOGNIZER_LANGUAGES: recognizedLanguages + USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing + ADVANCED_IMAGE_PROCESSING_MAX_IMAGES: advancedImageProcessingMaxImages + ORCHESTRATION_STRATEGY: orchestrationStrategy + CONVERSATION_FLOW: conversationFlow + LOGLEVEL: logLevel + + // Add database type to settings + AZURE_DATABASE_TYPE: databaseType + }, + // Conditionally add database-specific settings + databaseType == 'CosmosDB' + ? { + AZURE_COSMOSDB_INFO: azureCosmosDBInfo + AZURE_COSMOSDB_ENABLE_FEEDBACK: true + } + : databaseType == 'PostgreSQL' + ? { + AZURE_POSTGRES_INFO: azurePostgresDBInfo + } + : {} + ) } } @@ -720,68 +731,74 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { speechKeyName: useKeyVault ? storekeys.outputs.SPEECH_KEY_NAME : '' // Conditionally set database key names - cosmosDBKeyName: databaseType == 'cosmos' && useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' - postgresInfoName: databaseType == 'postgres' && useKeyVault ? storekeys.outputs.POSTGRESQL_INFO_NAME : '' + cosmosDBKeyName: databaseType == 'CosmosDB' && useKeyVault ? storekeys.outputs.COSMOS_ACCOUNT_KEY_NAME : '' + postgresInfoName: databaseType == 'PostgreSQL' && useKeyVault ? storekeys.outputs.POSTGRESQL_INFO_NAME : '' useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType - appSettings: union({ - // Existing app settings - AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion - AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_OPENAI_RESOURCE: azureOpenAIResourceName - AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo - AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature - AZURE_OPENAI_TOP_P: azureOpenAITopP - AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens - AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence - AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_OPENAI_STREAM: azureOpenAIStream - AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo - AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch - AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' - AZURE_SEARCH_INDEX: azureSearchIndex - AZURE_SEARCH_CONVERSATIONS_LOG_INDEX: azureSearchConversationLogIndex - AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig - AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked - AZURE_SEARCH_TOP_K: azureSearchTopK - AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain - AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn - AZURE_SEARCH_FILTER: azureSearchFilter - AZURE_SEARCH_FIELDS_ID: azureSearchFieldId - AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn - AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn - AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn - AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata - AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn - AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn - AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn - AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn - AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization - AZURE_SPEECH_SERVICE_NAME: speechServiceName - AZURE_SPEECH_SERVICE_REGION: location - AZURE_SPEECH_RECOGNIZER_LANGUAGES: recognizedLanguages - USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing - ADVANCED_IMAGE_PROCESSING_MAX_IMAGES: advancedImageProcessingMaxImages - ORCHESTRATION_STRATEGY: orchestrationStrategy - CONVERSATION_FLOW: conversationFlow - LOGLEVEL: logLevel - - // Add database type to settings - AZURE_DATABASE_TYPE: databaseType - }, - // Conditionally add database-specific settings - databaseType == 'cosmos' ? { - AZURE_COSMOSDB_INFO: azureCosmosDBInfo - AZURE_COSMOSDB_ENABLE_FEEDBACK: true - } : databaseType == 'postgres' ? { - AZURE_POSTGRESDB_INFO: azurePostgresDBInfo - } : {}) + appSettings: union( + { + // Existing app settings + AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion + AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo + AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature + AZURE_OPENAI_TOP_P: azureOpenAITopP + AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens + AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_OPENAI_STREAM: azureOpenAIStream + AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo + AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_CONVERSATIONS_LOG_INDEX: azureSearchConversationLogIndex + AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig + AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked + AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain + AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn + AZURE_SEARCH_FILTER: azureSearchFilter + AZURE_SEARCH_FIELDS_ID: azureSearchFieldId + AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn + AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata + AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn + AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn + AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn + AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn + AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization + AZURE_SPEECH_SERVICE_NAME: speechServiceName + AZURE_SPEECH_SERVICE_REGION: location + AZURE_SPEECH_RECOGNIZER_LANGUAGES: recognizedLanguages + USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing + ADVANCED_IMAGE_PROCESSING_MAX_IMAGES: advancedImageProcessingMaxImages + ORCHESTRATION_STRATEGY: orchestrationStrategy + CONVERSATION_FLOW: conversationFlow + LOGLEVEL: logLevel + + // Add database type to settings + AZURE_DATABASE_TYPE: databaseType + }, + // Conditionally add database-specific settings + databaseType == 'CosmosDB' + ? { + AZURE_COSMOSDB_INFO: azureCosmosDBInfo + AZURE_COSMOSDB_ENABLE_FEEDBACK: true + } + : databaseType == 'PostgreSQL' + ? { + AZURE_POSTGRESDB_INFO: azurePostgresDBInfo + } + : {} + ) } } @@ -1230,22 +1247,22 @@ module machineLearning 'app/machinelearning.bicep' = if (orchestrationStrategy = } } -module createIndex './core/database/deploy_create_table_script.bicep' = if (databaseType == 'postgres') { - name : 'deploy_create_table_script' - params:{ +module createIndex './core/database/deploy_create_table_script.bicep' = if (databaseType == 'postgres') { + name: 'deploy_create_table_script' + params: { solutionLocation: location - identity:managedIdentityModule.outputs.managedIdentityOutput.id - baseUrl:baseUrl - keyVaultName:keyvault.outputs.name + identity: managedIdentityModule.outputs.managedIdentityOutput.id + baseUrl: baseUrl + keyVaultName: keyvault.outputs.name postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName } scope: rg - dependsOn:[keyvault, postgresDBModule, storekeys] + dependsOn: [keyvault, postgresDBModule, storekeys] } output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString output AZURE_APP_SERVICE_HOSTING_MODEL string = hostingModel -output AZURE_BLOB_STORAGE_INFO string = replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY','') +output AZURE_BLOB_STORAGE_INFO string = replace(azureBlobStorageInfo, '$STORAGE_ACCOUNT_KEY', '') output AZURE_COMPUTER_VISION_ENDPOINT string = useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' output AZURE_COMPUTER_VISION_LOCATION string = useAdvancedImageProcessing ? computerVision.outputs.location : '' output AZURE_COMPUTER_VISION_KEY string = useKeyVault ? storekeys.outputs.COMPUTER_VISION_KEY_NAME : '' @@ -1253,7 +1270,7 @@ output AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION string = computerVision output AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION string = computerVisionVectorizeImageModelVersion output AZURE_CONTENT_SAFETY_ENDPOINT string = contentsafety.outputs.endpoint output AZURE_CONTENT_SAFETY_KEY string = useKeyVault ? storekeys.outputs.CONTENT_SAFETY_KEY_NAME : '' -output AZURE_FORM_RECOGNIZER_INFO string = replace(azureFormRecognizerInfo, '$FORM_RECOGNIZER_KEY','') +output AZURE_FORM_RECOGNIZER_INFO string = replace(azureFormRecognizerInfo, '$FORM_RECOGNIZER_KEY', '') output AZURE_KEY_VAULT_ENDPOINT string = useKeyVault ? keyvault.outputs.endpoint : '' output AZURE_KEY_VAULT_NAME string = useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' output AZURE_LOCATION string = location diff --git a/infra/main.json b/infra/main.json index 314c1f8d3..cb7269c61 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "16387401552448615008" + "templateHash": "14534949706227721920" } }, "parameters": { @@ -215,7 +215,7 @@ "type": "bool", "defaultValue": false, "metadata": { - "description": "Use Azure Search Integrated Vectorization" + "description": "Whether to use Azure Search Integrated Vectorization. If the database type is PostgreSQL, set this to false." } }, "azureOpenAIResourceName": { @@ -264,7 +264,7 @@ "type": "bool", "defaultValue": false, "metadata": { - "description": "Enables the use of a vision LLM and Computer Vision for embedding images" + "description": "Whether to enable the use of a vision LLM and Computer Vision for embedding images. If the database type is PostgreSQL, set this to false." } }, "advancedImageProcessingMaxImages": { @@ -614,10 +614,10 @@ }, "databaseType": { "type": "string", - "defaultValue": "cosmos", + "defaultValue": "CosmosDB", "allowedValues": [ - "cosmos", - "postgres" + "CosmosDB", + "PostgreSQL" ], "metadata": { "description": "The type of database to deploy (cosmos or postgres)" @@ -946,7 +946,7 @@ ] }, { - "condition": "[equals(parameters('databaseType'), 'postgres')]", + "condition": "[equals(parameters('databaseType'), 'PostgreSQL')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", "name": "deploy_postgres_sql", @@ -2070,11 +2070,11 @@ "value": "[parameters('speechServiceName')]" }, "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", - "cosmosAccountName": "[if(equals(parameters('databaseType'), 'cosmos'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName), createObject('value', ''))]", - "postgresServerName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), createObject('value', ''))]", - "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', 'postgres'), createObject('value', ''))]", - "postgresDatabaseAdminUserName": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser), createObject('value', ''))]", - "postgresDatabaseAdminPassword": "[if(equals(parameters('databaseType'), 'postgres'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd), createObject('value', ''))]", + "cosmosAccountName": "[if(equals(parameters('databaseType'), 'CosmosDB'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName), createObject('value', ''))]", + "postgresServerName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), createObject('value', ''))]", + "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', 'PostgreSQL'), createObject('value', ''))]", + "postgresDatabaseAdminUserName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser), createObject('value', ''))]", + "postgresDatabaseAdminPassword": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd), createObject('value', ''))]", "rgName": { "value": "[variables('rgName')]" } @@ -2086,7 +2086,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "4301444608123274670" + "templateHash": "10141904934443597449" } }, "parameters": { @@ -2132,7 +2132,7 @@ }, "postgresDatabaseName": { "type": "string", - "defaultValue": "postgres" + "defaultValue": "PostgreSQL" }, "postgresInfoName": { "type": "string", @@ -2651,8 +2651,8 @@ "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", - "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'cosmos'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'postgres'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", + "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'CosmosDB'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'PostgreSQL'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", "useKeyVault": { "value": "[parameters('useKeyVault')]" }, @@ -2661,7 +2661,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRES_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRES_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -2671,7 +2671,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "64857797928052295" + "templateHash": "12192862160861363431" } }, "parameters": { @@ -2793,7 +2793,7 @@ }, "databaseType": { "type": "string", - "defaultValue": "cosmos" + "defaultValue": "CosmosDB" }, "cosmosDBKeyName": { "type": "string", @@ -2835,7 +2835,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'cosmos'), createObject('DATABASE_TYPE', 'cosmos', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'postgres', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'CosmosDB'), createObject('DATABASE_TYPE', 'CosmosDB', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'PostgreSQL', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -3647,8 +3647,8 @@ "computerVisionKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COMPUTER_VISION_KEY_NAME.value), createObject('value', ''))]", "contentSafetyKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.CONTENT_SAFETY_KEY_NAME.value), createObject('value', ''))]", "speechKeyName": "[if(parameters('useKeyVault'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.SPEECH_KEY_NAME.value), createObject('value', ''))]", - "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'cosmos'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", - "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'postgres'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", + "cosmosDBKeyName": "[if(and(equals(parameters('databaseType'), 'CosmosDB'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.COSMOS_ACCOUNT_KEY_NAME.value), createObject('value', ''))]", + "postgresInfoName": "[if(and(equals(parameters('databaseType'), 'PostgreSQL'), parameters('useKeyVault')), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'storekeys'), '2022-09-01').outputs.POSTGRESQL_INFO_NAME.value), createObject('value', ''))]", "useKeyVault": { "value": "[parameters('useKeyVault')]" }, @@ -3657,7 +3657,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'cosmos'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'postgres'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" } }, "template": { @@ -3667,7 +3667,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "64857797928052295" + "templateHash": "12192862160861363431" } }, "parameters": { @@ -3789,7 +3789,7 @@ }, "databaseType": { "type": "string", - "defaultValue": "cosmos" + "defaultValue": "CosmosDB" }, "cosmosDBKeyName": { "type": "string", @@ -3831,7 +3831,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'cosmos'), createObject('DATABASE_TYPE', 'cosmos', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'postgres', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'CosmosDB'), createObject('DATABASE_TYPE', 'CosmosDB', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'PostgreSQL', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -12304,7 +12304,7 @@ }, "AZURE_COSMOSDB_INFO": { "type": "string", - "value": "[string(createObject('accountName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'cosmos'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, '')))]" + "value": "[string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, '')))]" }, "AZURE_POSTGRESDB_INFO": { "type": "string", From e54f13ecbc28a0d8f5f45f12f655281a6350fb81 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 22:41:49 +0530 Subject: [PATCH 067/107] Modified the script --- scripts/run_create_table_script.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 6ed535644..7583013ea 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -8,8 +8,8 @@ requirementFile="requirements.txt" requirementFileUrl=${baseUrl}"scripts/data_scripts/requirements.txt" resourceGroup="$3" serverName="$4" -webAppPrincipalName = "$5" -adminAppPrincipalName = "$6" +webAppPrincipalName="$5" +adminAppPrincipalName="$6" echo "Script Started" From 0b841c1d911a1b1dd052e609d1d522fdde8cbbdc Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 22:51:56 +0530 Subject: [PATCH 068/107] Fix: Added Principal to DB --- scripts/data_scripts/create_postgres_tables.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 2f125644a..434692d66 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -26,6 +26,14 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): - schema_name: Name of the schema to grant table-level permissions. - principal_name: Name of the principal (role or user) to grant permissions. """ + + add_principal_user_query = "SELECT * FROM pgaadauth_create_principal({principal}, false, false)" + cursor.execute( + add_principal_user_query.format( + principal=sql.Identifier(principal_name), + ) + ) + # Grant CONNECT on database grant_connect_query = sql.SQL("GRANT CONNECT ON DATABASE {database} TO {principal}") cursor.execute( From 9079910462195045e15563d310e49d5aec7c9d96 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 23:01:11 +0530 Subject: [PATCH 069/107] Fix: Added Managed identity to DB --- infra/app/web.bicep | 2 +- infra/core/database/postgresdb.bicep | 2 +- infra/main.bicep | 6 +++-- infra/main.json | 22 +++++++++++++------ .../data_scripts/create_postgres_tables.py | 2 +- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/infra/app/web.bicep b/infra/app/web.bicep index 4fb24d3c0..755f6eb32 100644 --- a/infra/app/web.bicep +++ b/infra/app/web.bicep @@ -211,7 +211,7 @@ resource cosmosRoleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefi name: '${json(appSettings.AZURE_COSMOSDB_INFO).accountName}/00000000-0000-0000-0000-000000000002' } -module cosmosUserRole '../core/database/cosmos-sql-role-assign.bicep' = { +module cosmosUserRole '../core/database/cosmos-sql-role-assign.bicep' = if(databaseType == 'cosmos') { name: 'cosmos-sql-user-role-${web.name}' params: { accountName: json(appSettings.AZURE_COSMOSDB_INFO).accountName diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index 32a2af714..557732cd8 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -65,7 +65,7 @@ resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { kind: 'AzurePowerShell' properties: { azPowerShellVersion: '3.0' - scriptContent: 'start-sleep -Seconds 180' + scriptContent: 'start-sleep -Seconds 300' cleanupPreference: 'Always' retentionInterval: 'PT1H' } diff --git a/infra/main.bicep b/infra/main.bicep index 9f4817b1e..d71d08f85 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -813,6 +813,7 @@ module adminweb './app/adminweb.bicep' = if (hostingModel == 'code') { useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType + databaseType: databaseType appSettings: { AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion @@ -886,6 +887,7 @@ module adminweb_docker './app/adminweb.bicep' = if (hostingModel == 'container') useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType + databaseType: databaseType appSettings: { AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion @@ -1238,8 +1240,8 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (dat baseUrl:baseUrl keyVaultName:keyvault.outputs.name postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName - webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID : web_docker.outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID - adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID : adminweb_docker.outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID + webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_NAME : web_docker.outputs.FRONTEND_API_NAME + adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_NAME : adminweb_docker.outputs.WEBSITE_ADMIN_NAME } scope: rg dependsOn: hostingModel == 'code' ? [keyvault, postgresDBModule, storekeys, web, adminweb] : [ diff --git a/infra/main.json b/infra/main.json index e1ed4febf..c88fc267c 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "831473305954833848" + "templateHash": "8458218652856054228" } }, "parameters": { @@ -977,7 +977,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "7878676541226628436" + "templateHash": "17455459966134467610" } }, "parameters": { @@ -1091,7 +1091,7 @@ "kind": "AzurePowerShell", "properties": { "azPowerShellVersion": "3.0", - "scriptContent": "start-sleep -Seconds 180", + "scriptContent": "start-sleep -Seconds 300", "cleanupPreference": "Always", "retentionInterval": "PT1H" }, @@ -2671,7 +2671,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "8216675081949619536" + "templateHash": "8696900365541524252" } }, "parameters": { @@ -3490,6 +3490,7 @@ ] }, { + "condition": "[equals(parameters('databaseType'), 'cosmos')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", "name": "[format('cosmos-sql-user-role-{0}', format('{0}-app-module', parameters('name')))]", @@ -3670,7 +3671,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "8216675081949619536" + "templateHash": "8696900365541524252" } }, "parameters": { @@ -4489,6 +4490,7 @@ ] }, { + "condition": "[equals(parameters('databaseType'), 'cosmos')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", "name": "[format('cosmos-sql-user-role-{0}', format('{0}-app-module', parameters('name')))]", @@ -4653,6 +4655,9 @@ "authType": { "value": "[parameters('authType')]" }, + "databaseType": { + "value": "[parameters('databaseType')]" + }, "appSettings": { "value": { "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", @@ -5608,6 +5613,9 @@ "authType": { "value": "[parameters('authType')]" }, + "databaseType": { + "value": "[parameters('databaseType')]" + }, "appSettings": { "value": { "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", @@ -12020,8 +12028,8 @@ "postgresSqlServerName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" }, - "webAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('websiteName')), '2022-09-01').outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_IDENTITY_PRINCIPAL_ID.value))]", - "adminAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName')), '2022-09-01').outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_IDENTITY_PRINCIPAL_ID.value))]" + "webAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('websiteName')), '2022-09-01').outputs.FRONTEND_API_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_NAME.value))]", + "adminAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName')), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 434692d66..d5a57c284 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -27,7 +27,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): - principal_name: Name of the principal (role or user) to grant permissions. """ - add_principal_user_query = "SELECT * FROM pgaadauth_create_principal({principal}, false, false)" + add_principal_user_query = sql.SQL("SELECT * FROM pgaadauth_create_principal({principal}, false, false)") cursor.execute( add_principal_user_query.format( principal=sql.Identifier(principal_name), From 07ef813b5c8f78bf2eb4a141a69ab4c10c3c1da7 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 23:24:31 +0530 Subject: [PATCH 070/107] fix: Modified the paramater --- scripts/run_create_table_script.sh | 2 - scripts/test/create_postgres_tables.py | 140 +++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 scripts/test/create_postgres_tables.py diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 7583013ea..eeef0934e 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -34,6 +34,4 @@ sed -i "s/adminAppPrincipalName/${adminAppPrincipalName}/g" "create_postgres_tab pip install -r requirements.txt -pip show azure-identity - python create_postgres_tables.py diff --git a/scripts/test/create_postgres_tables.py b/scripts/test/create_postgres_tables.py new file mode 100644 index 000000000..187966522 --- /dev/null +++ b/scripts/test/create_postgres_tables.py @@ -0,0 +1,140 @@ +import json +from azure.keyvault.secrets import SecretClient +from azure.identity import DefaultAzureCredential +import psycopg2 +from psycopg2 import sql + +key_vault_name = "kv-wpvykucviclze" +principal_name = "web-wpvykucviclze-docker" +admin_principal_name = "web-wpvykucviclze-admin-docker" + +def get_secrets_from_kv(kv_name, secret_name): + credential = DefaultAzureCredential() + 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 + + +def grant_permissions(cursor, dbname, schema_name, principal_name): + """ + Grants database and schema-level permissions to a specified principal. + + Parameters: + - cursor: psycopg2 cursor object for database operations. + - dbname: Name of the database to grant CONNECT permission. + - schema_name: Name of the schema to grant table-level permissions. + - principal_name: Name of the principal (role or user) to grant permissions. + """ + + add_principal_user_query = sql.SQL("SELECT * FROM pgaadauth_create_principal({principal}, false, false)") + cursor.execute( + add_principal_user_query.format( + principal=sql.Literal(principal_name), + ) + ) + + # Grant CONNECT on database + grant_connect_query = sql.SQL("GRANT CONNECT ON DATABASE {database} TO {principal}") + cursor.execute( + grant_connect_query.format( + database=sql.Identifier(dbname), + principal=sql.Identifier(principal_name), + ) + ) + print(f"Granted CONNECT on database '{dbname}' to '{principal_name}'") + + # Grant SELECT, INSERT, UPDATE, DELETE on schema tables + grant_permissions_query = sql.SQL( + "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA {schema} TO {principal}" + ) + cursor.execute( + grant_permissions_query.format( + schema=sql.Identifier(schema_name), + principal=sql.Identifier(principal_name), + ) + ) + +postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) +host = postgres_details.get("host", "") +user = postgres_details.get("user", "") +dbname = postgres_details.get("dbname", "") +password = postgres_details.get("password", "") + +# Acquire the access token +cred = DefaultAzureCredential() +access_token = cred.get_token("https://ossrdbms-aad.database.windows.net/.default") + +# Combine the token with the connection string to establish the connection. +conn_string = "host={0} user={1} dbname={2} password={3}".format( + host, user, dbname, password +) +conn = psycopg2.connect(conn_string) +cursor = conn.cursor() + +grant_permissions(cursor, dbname, "public", principal_name) +grant_permissions(cursor, dbname, "public", admin_principal_name) +conn.commit() + +# Drop and recreate the conversations table +cursor.execute("DROP TABLE IF EXISTS conversations") +conn.commit() + +create_cs_sql = """CREATE TABLE conversations ( + id TEXT PRIMARY KEY, + conversation_id TEXT NOT NULL, + type TEXT NOT NULL, + "createdAt" TEXT, + "updatedAt" TEXT, + user_id TEXT NOT NULL, + title TEXT + );""" +cursor.execute(create_cs_sql) +conn.commit() + +# Drop and recreate the messages table +cursor.execute("DROP TABLE IF EXISTS messages") +conn.commit() + +create_ms_sql = """CREATE TABLE messages ( + id TEXT PRIMARY KEY, + type VARCHAR(50) NOT NULL, + "createdAt" TEXT, + "updatedAt" TEXT, + user_id TEXT NOT NULL, + conversation_id TEXT NOT NULL, + role VARCHAR(50), + content TEXT NOT NULL, + feedback TEXT + );""" +cursor.execute(create_ms_sql) +conn.commit() + +# Add pg_diskann extension and search_indexes table +cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") +conn.commit() + +cursor.execute("DROP TABLE IF EXISTS search_indexes;") +conn.commit() + +table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( + id text, + title text, + chunk integer, + chunk_id text, + "offset" integer, + page_number integer, + content text, + source text, + metadata text, + content_vector public.vector(1536) +);""" + +cursor.execute(table_create_command) +conn.commit() + +cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") +conn.commit() + +cursor.close() +conn.close() From 110d666a9d87cc671c930eea1ad0b41f7aa4e351 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 23:30:45 +0530 Subject: [PATCH 071/107] fix: parameter changes --- .../data_scripts/create_postgres_tables.py | 2 +- scripts/test/create_postgres_tables.py | 140 ------------------ 2 files changed, 1 insertion(+), 141 deletions(-) delete mode 100644 scripts/test/create_postgres_tables.py diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index d5a57c284..55dbf719a 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -30,7 +30,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): add_principal_user_query = sql.SQL("SELECT * FROM pgaadauth_create_principal({principal}, false, false)") cursor.execute( add_principal_user_query.format( - principal=sql.Identifier(principal_name), + principal=sql.Literal(principal_name), ) ) diff --git a/scripts/test/create_postgres_tables.py b/scripts/test/create_postgres_tables.py deleted file mode 100644 index 187966522..000000000 --- a/scripts/test/create_postgres_tables.py +++ /dev/null @@ -1,140 +0,0 @@ -import json -from azure.keyvault.secrets import SecretClient -from azure.identity import DefaultAzureCredential -import psycopg2 -from psycopg2 import sql - -key_vault_name = "kv-wpvykucviclze" -principal_name = "web-wpvykucviclze-docker" -admin_principal_name = "web-wpvykucviclze-admin-docker" - -def get_secrets_from_kv(kv_name, secret_name): - credential = DefaultAzureCredential() - 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 - - -def grant_permissions(cursor, dbname, schema_name, principal_name): - """ - Grants database and schema-level permissions to a specified principal. - - Parameters: - - cursor: psycopg2 cursor object for database operations. - - dbname: Name of the database to grant CONNECT permission. - - schema_name: Name of the schema to grant table-level permissions. - - principal_name: Name of the principal (role or user) to grant permissions. - """ - - add_principal_user_query = sql.SQL("SELECT * FROM pgaadauth_create_principal({principal}, false, false)") - cursor.execute( - add_principal_user_query.format( - principal=sql.Literal(principal_name), - ) - ) - - # Grant CONNECT on database - grant_connect_query = sql.SQL("GRANT CONNECT ON DATABASE {database} TO {principal}") - cursor.execute( - grant_connect_query.format( - database=sql.Identifier(dbname), - principal=sql.Identifier(principal_name), - ) - ) - print(f"Granted CONNECT on database '{dbname}' to '{principal_name}'") - - # Grant SELECT, INSERT, UPDATE, DELETE on schema tables - grant_permissions_query = sql.SQL( - "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA {schema} TO {principal}" - ) - cursor.execute( - grant_permissions_query.format( - schema=sql.Identifier(schema_name), - principal=sql.Identifier(principal_name), - ) - ) - -postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) -host = postgres_details.get("host", "") -user = postgres_details.get("user", "") -dbname = postgres_details.get("dbname", "") -password = postgres_details.get("password", "") - -# Acquire the access token -cred = DefaultAzureCredential() -access_token = cred.get_token("https://ossrdbms-aad.database.windows.net/.default") - -# Combine the token with the connection string to establish the connection. -conn_string = "host={0} user={1} dbname={2} password={3}".format( - host, user, dbname, password -) -conn = psycopg2.connect(conn_string) -cursor = conn.cursor() - -grant_permissions(cursor, dbname, "public", principal_name) -grant_permissions(cursor, dbname, "public", admin_principal_name) -conn.commit() - -# Drop and recreate the conversations table -cursor.execute("DROP TABLE IF EXISTS conversations") -conn.commit() - -create_cs_sql = """CREATE TABLE conversations ( - id TEXT PRIMARY KEY, - conversation_id TEXT NOT NULL, - type TEXT NOT NULL, - "createdAt" TEXT, - "updatedAt" TEXT, - user_id TEXT NOT NULL, - title TEXT - );""" -cursor.execute(create_cs_sql) -conn.commit() - -# Drop and recreate the messages table -cursor.execute("DROP TABLE IF EXISTS messages") -conn.commit() - -create_ms_sql = """CREATE TABLE messages ( - id TEXT PRIMARY KEY, - type VARCHAR(50) NOT NULL, - "createdAt" TEXT, - "updatedAt" TEXT, - user_id TEXT NOT NULL, - conversation_id TEXT NOT NULL, - role VARCHAR(50), - content TEXT NOT NULL, - feedback TEXT - );""" -cursor.execute(create_ms_sql) -conn.commit() - -# Add pg_diskann extension and search_indexes table -cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") -conn.commit() - -cursor.execute("DROP TABLE IF EXISTS search_indexes;") -conn.commit() - -table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( - id text, - title text, - chunk integer, - chunk_id text, - "offset" integer, - page_number integer, - content text, - source text, - metadata text, - content_vector public.vector(1536) -);""" - -cursor.execute(table_create_command) -conn.commit() - -cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") -conn.commit() - -cursor.close() -conn.close() From d697cd66c2943b3408de1a71e469cda5563ec2ff Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 23:37:31 +0530 Subject: [PATCH 072/107] test: for connectivity with aad --- scripts/data_scripts/create_postgres_tables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 55dbf719a..893b9bd63 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -57,7 +57,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) host = postgres_details.get("host", "") -user = postgres_details.get("user", "") +user = "wpvykucviclze-managed-identity" dbname = postgres_details.get("dbname", "") password = postgres_details.get("password", "") @@ -67,7 +67,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): # Combine the token with the connection string to establish the connection. conn_string = "host={0} user={1} dbname={2} password={3}".format( - host, user, dbname, password + host, user, dbname, access_token ) conn = psycopg2.connect(conn_string) cursor = conn.cursor() From 5ab585b77d8dde3759aa53f424dc413c2d6581ff Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 23:42:26 +0530 Subject: [PATCH 073/107] fix: access token issue --- scripts/data_scripts/create_postgres_tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 893b9bd63..c2d3d0927 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -67,7 +67,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): # Combine the token with the connection string to establish the connection. conn_string = "host={0} user={1} dbname={2} password={3}".format( - host, user, dbname, access_token + host, user, dbname, access_token.token ) conn = psycopg2.connect(conn_string) cursor = conn.cursor() From cd74dfdc6210719951ac63f8fb03ce9e5326c160 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 28 Nov 2024 23:50:54 +0530 Subject: [PATCH 074/107] Updated script --- scripts/data_scripts/create_postgres_tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index c2d3d0927..460edc9a6 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -66,7 +66,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): access_token = cred.get_token("https://ossrdbms-aad.database.windows.net/.default") # Combine the token with the connection string to establish the connection. -conn_string = "host={0} user={1} dbname={2} password={3}".format( +conn_string = "host={0} user={1} dbname={2} password={3} sslmode=require".format( host, user, dbname, access_token.token ) conn = psycopg2.connect(conn_string) From 5f29e5bb737693d2c8c222497ecb760a8c44f159 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 29 Nov 2024 00:05:22 +0530 Subject: [PATCH 075/107] fix: Updates script to run with managed identity --- .../core/database/deploy_create_table_script.bicep | 3 ++- infra/main.bicep | 1 + infra/main.json | 14 ++++++++++---- scripts/data_scripts/create_postgres_tables.py | 2 +- scripts/run_create_table_script.sh | 2 ++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/infra/core/database/deploy_create_table_script.bicep b/infra/core/database/deploy_create_table_script.bicep index afccc8cc8..4b2718af2 100644 --- a/infra/core/database/deploy_create_table_script.bicep +++ b/infra/core/database/deploy_create_table_script.bicep @@ -7,6 +7,7 @@ param identity string param postgresSqlServerName string param webAppPrincipalName string param adminAppPrincipalName string +param managedIdentityName string resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { kind:'AzureCLI' @@ -21,7 +22,7 @@ resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { properties: { azCliVersion: '2.52.0' primaryScriptUri: '${baseUrl}scripts/run_create_table_script.sh' - arguments: '${baseUrl} ${keyVaultName} ${resourceGroup().name} ${postgresSqlServerName} ${webAppPrincipalName} ${adminAppPrincipalName}' // Specify any arguments for the script + arguments: '${baseUrl} ${keyVaultName} ${resourceGroup().name} ${postgresSqlServerName} ${webAppPrincipalName} ${adminAppPrincipalName} ${managedIdentityName}' // Specify any arguments for the script timeout: 'PT1H' // Specify the desired timeout duration retentionInterval: 'PT1H' // Specify the desired retention interval cleanupPreference:'OnSuccess' diff --git a/infra/main.bicep b/infra/main.bicep index d71d08f85..d0c237d3f 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1242,6 +1242,7 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (dat postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_NAME : web_docker.outputs.FRONTEND_API_NAME adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_NAME : adminweb_docker.outputs.WEBSITE_ADMIN_NAME + managedIdentityName: managedIdentityModule.outputs.managedIdentityOutput.name } scope: rg dependsOn: hostingModel == 'code' ? [keyvault, postgresDBModule, storekeys, web, adminweb] : [ diff --git a/infra/main.json b/infra/main.json index c88fc267c..07e114325 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "8458218652856054228" + "templateHash": "936545603138294666" } }, "parameters": { @@ -12029,7 +12029,10 @@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" }, "webAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('websiteName')), '2022-09-01').outputs.FRONTEND_API_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_NAME.value))]", - "adminAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName')), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value))]" + "adminAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName')), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value))]", + "managedIdentityName": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.name]" + } }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -12038,7 +12041,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12297908402052822068" + "templateHash": "5666855677780801837" } }, "parameters": { @@ -12065,6 +12068,9 @@ }, "adminAppPrincipalName": { "type": "string" + }, + "managedIdentityName": { + "type": "string" } }, "resources": [ @@ -12083,7 +12089,7 @@ "properties": { "azCliVersion": "2.52.0", "primaryScriptUri": "[format('{0}scripts/run_create_table_script.sh', parameters('baseUrl'))]", - "arguments": "[format('{0} {1} {2} {3} {4} {5}', parameters('baseUrl'), parameters('keyVaultName'), resourceGroup().name, parameters('postgresSqlServerName'), parameters('webAppPrincipalName'), parameters('adminAppPrincipalName'))]", + "arguments": "[format('{0} {1} {2} {3} {4} {5} {6}', parameters('baseUrl'), parameters('keyVaultName'), resourceGroup().name, parameters('postgresSqlServerName'), parameters('webAppPrincipalName'), parameters('adminAppPrincipalName'), parameters('managedIdentityName'))]", "timeout": "PT1H", "retentionInterval": "PT1H", "cleanupPreference": "OnSuccess" diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 460edc9a6..247cb30ca 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -7,6 +7,7 @@ key_vault_name = "kv_to-be-replaced" principal_name = "webAppPrincipalName" admin_principal_name = "adminAppPrincipalName" +user = "managedIdentityName" def get_secrets_from_kv(kv_name, secret_name): credential = DefaultAzureCredential() @@ -57,7 +58,6 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) host = postgres_details.get("host", "") -user = "wpvykucviclze-managed-identity" dbname = postgres_details.get("dbname", "") password = postgres_details.get("password", "") diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index eeef0934e..31bc4cf61 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -10,6 +10,7 @@ resourceGroup="$3" serverName="$4" webAppPrincipalName="$5" adminAppPrincipalName="$6" +managedIdentityName="$7" echo "Script Started" @@ -31,6 +32,7 @@ echo "Download completed" sed -i "s/kv_to-be-replaced/${keyvaultName}/g" "create_postgres_tables.py" sed -i "s/webAppPrincipalName/${webAppPrincipalName}/g" "create_postgres_tables.py" sed -i "s/adminAppPrincipalName/${adminAppPrincipalName}/g" "create_postgres_tables.py" +sed -i "s/managedIdentityName/${managedIdentityName}/g" "create_postgres_tables.py" pip install -r requirements.txt From c9fe9ef60deca21649488f4b9efe39ab67b6e0b8 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 29 Nov 2024 16:20:06 +0530 Subject: [PATCH 076/107] Updated code for testing --- scripts/data_scripts/create_postgres_tables.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 247cb30ca..fa346e202 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -57,8 +57,8 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): ) postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) -host = postgres_details.get("host", "") -dbname = postgres_details.get("dbname", "") +host = postgres_details.get("POSTGRESQL_HOST", "") +dbname = postgres_details.get("POSTGRESQL_DATABASE", "") password = postgres_details.get("password", "") # Acquire the access token @@ -73,8 +73,9 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): cursor = conn.cursor() grant_permissions(cursor, dbname, "public", principal_name) -grant_permissions(cursor, dbname, "public", admin_principal_name) conn.commit() +# grant_permissions(cursor, dbname, "public", admin_principal_name) +# conn.commit() # Drop and recreate the conversations table cursor.execute("DROP TABLE IF EXISTS conversations") From 9b0ae47720523c14e9a9fa450e335c52d435a8c1 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 29 Nov 2024 16:27:50 +0530 Subject: [PATCH 077/107] Test script --- scripts/data_scripts/create_postgres_tables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index fa346e202..8ae2f7c6e 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -57,8 +57,8 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): ) postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) -host = postgres_details.get("POSTGRESQL_HOST", "") -dbname = postgres_details.get("POSTGRESQL_DATABASE", "") +host = postgres_details.get("host", "") +dbname = postgres_details.get("database", "") password = postgres_details.get("password", "") # Acquire the access token From 8d434f790f23b76cdf5e18900cb78fe4bae47fee Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 29 Nov 2024 20:19:01 +0530 Subject: [PATCH 078/107] Added Function app to DB --- infra/app/function.bicep | 3 +- infra/app/storekeys.bicep | 2 - .../database/deploy_create_table_script.bicep | 3 +- infra/main.bicep | 51 +++++++++++--- infra/main.json | 69 ++++++++++++------- .../data_scripts/create_postgres_tables.py | 15 ++-- scripts/run_create_table_script.sh | 4 +- 7 files changed, 104 insertions(+), 43 deletions(-) diff --git a/infra/app/function.bicep b/infra/app/function.bicep index 19325462a..6f265efca 100644 --- a/infra/app/function.bicep +++ b/infra/app/function.bicep @@ -27,7 +27,7 @@ param contentSafetyKeyName string = '' param speechKeyName string = '' param authType string param dockerFullImageName string = '' -param cosmosDBKeyName string = '' +param databaseType string var azureFormRecognizerInfoUpdated = useKeyVault ? azureFormRecognizerInfo @@ -67,6 +67,7 @@ module function '../core/host/functions.bicep' = { runtimeVersion: runtimeVersion dockerFullImageName: dockerFullImageName useKeyVault: useKeyVault + managedIdentity: databaseType == 'PostgreSQL' appSettings: union(appSettings, { WEBSITES_ENABLE_APP_SERVICE_STORAGE: 'false' AZURE_AUTH_TYPE: authType diff --git a/infra/app/storekeys.bicep b/infra/app/storekeys.bicep index fa9f54c02..6b5c3da12 100644 --- a/infra/app/storekeys.bicep +++ b/infra/app/storekeys.bicep @@ -11,7 +11,6 @@ param postgresServerName string = '' // PostgreSQL server name param postgresDatabaseName string = 'PostgreSQL' // Default database name param postgresInfoName string = 'AZURE-POSTGRESQL-INFO' // Secret name for PostgreSQL info param postgresDatabaseAdminUserName string = '' -param postgresDatabaseAdminPassword string = '' param storageAccountKeyName string = 'AZURE-STORAGE-ACCOUNT-KEY' param openAIKeyName string = 'AZURE-OPENAI-API-KEY' param searchKeyName string = 'AZURE-SEARCH-KEY' @@ -111,7 +110,6 @@ resource postgresInfoSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = if user: postgresDatabaseAdminUserName dbname: postgresDatabaseName host: postgresServerName - password: postgresDatabaseAdminPassword }) : '' } diff --git a/infra/core/database/deploy_create_table_script.bicep b/infra/core/database/deploy_create_table_script.bicep index 4b2718af2..9ca5ed0a1 100644 --- a/infra/core/database/deploy_create_table_script.bicep +++ b/infra/core/database/deploy_create_table_script.bicep @@ -8,6 +8,7 @@ param postgresSqlServerName string param webAppPrincipalName string param adminAppPrincipalName string param managedIdentityName string +param functionAppPrincipalName string resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { kind:'AzureCLI' @@ -22,7 +23,7 @@ resource create_index 'Microsoft.Resources/deploymentScripts@2020-10-01' = { properties: { azCliVersion: '2.52.0' primaryScriptUri: '${baseUrl}scripts/run_create_table_script.sh' - arguments: '${baseUrl} ${keyVaultName} ${resourceGroup().name} ${postgresSqlServerName} ${webAppPrincipalName} ${adminAppPrincipalName} ${managedIdentityName}' // Specify any arguments for the script + arguments: '${baseUrl} ${keyVaultName} ${resourceGroup().name} ${postgresSqlServerName} ${webAppPrincipalName} ${adminAppPrincipalName} ${functionAppPrincipalName} ${managedIdentityName}' // Specify any arguments for the script timeout: 'PT1H' // Specify the desired timeout duration retentionInterval: 'PT1H' // Specify the desired retention interval cleanupPreference:'OnSuccess' diff --git a/infra/main.bicep b/infra/main.bicep index 2d061e9d9..fcf5a00e8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -539,9 +539,6 @@ module storekeys './app/storekeys.bicep' = if (useKeyVault) { postgresDatabaseAdminUserName: databaseType == 'PostgreSQL' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser : '' - postgresDatabaseAdminPassword: databaseType == 'PostgreSQL' - ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbPwd - : '' rgName: rgName } } @@ -589,10 +586,9 @@ var azureCosmosDBInfo = string({ }) var azurePostgresDBInfo = string({ - serverName: '${postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName}.postgres.database.azure.com' + serverName: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName databaseName: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName - userName: postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser - password: postgresDBModule.outputs.postgresDbOutput.postgreSQLDbPwd + userName: '' }) module web './app/web.bicep' = if (hostingModel == 'code') { @@ -692,7 +688,11 @@ module web './app/web.bicep' = if (hostingModel == 'code') { } : databaseType == 'PostgreSQL' ? { - AZURE_POSTGRES_INFO: azurePostgresDBInfo + AZURE_POSTGRESDB_INFO: string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: websiteName + }) } : {} ) @@ -795,7 +795,11 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { } : databaseType == 'PostgreSQL' ? { - AZURE_POSTGRESDB_INFO: azurePostgresDBInfo + AZURE_POSTGRESDB_INFO: string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: '${websiteName}-docker' + }) } : {} ) @@ -873,6 +877,13 @@ module adminweb './app/adminweb.bicep' = if (hostingModel == 'code') { FUNCTION_KEY: clientKey ORCHESTRATION_STRATEGY: orchestrationStrategy LOGLEVEL: logLevel + AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: adminWebsiteName + }) + : {} } } } @@ -947,6 +958,13 @@ module adminweb_docker './app/adminweb.bicep' = if (hostingModel == 'container') FUNCTION_KEY: clientKey ORCHESTRATION_STRATEGY: orchestrationStrategy LOGLEVEL: logLevel + AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: '${adminWebsiteName}-docker' + }) + : {} } } } @@ -1014,6 +1032,7 @@ module function './app/function.bicep' = if (hostingModel == 'code') { useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType + databaseType: databaseType appSettings: { AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion @@ -1042,6 +1061,13 @@ module function './app/function.bicep' = if (hostingModel == 'code') { LOGLEVEL: logLevel AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: functionName + }) + : {} } } } @@ -1074,6 +1100,7 @@ module function_docker './app/function.bicep' = if (hostingModel == 'container') useKeyVault: useKeyVault keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType + databaseType: databaseType appSettings: { AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion @@ -1102,6 +1129,13 @@ module function_docker './app/function.bicep' = if (hostingModel == 'container') LOGLEVEL: logLevel AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: '${functionName}-docker' + }) + : {} } } } @@ -1259,6 +1293,7 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (data postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_NAME : web_docker.outputs.FRONTEND_API_NAME adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_NAME : adminweb_docker.outputs.WEBSITE_ADMIN_NAME + functionAppPrincipalName: hostingModel == 'code' ? function.outputs.functionName : function_docker.outputs.functionName managedIdentityName: managedIdentityModule.outputs.managedIdentityOutput.name } scope: rg diff --git a/infra/main.json b/infra/main.json index 328f4b9a1..ae7b677ec 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "15176273744623029817" + "templateHash": "13094848063702055322" } }, "parameters": { @@ -2074,7 +2074,6 @@ "postgresServerName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), createObject('value', ''))]", "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', 'PostgreSQL'), createObject('value', ''))]", "postgresDatabaseAdminUserName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser), createObject('value', ''))]", - "postgresDatabaseAdminPassword": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd), createObject('value', ''))]", "rgName": { "value": "[variables('rgName')]" } @@ -2086,7 +2085,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "10141904934443597449" + "templateHash": "3141925625583847916" } }, "parameters": { @@ -2142,10 +2141,6 @@ "type": "string", "defaultValue": "" }, - "postgresDatabaseAdminPassword": { - "type": "string", - "defaultValue": "" - }, "storageAccountKeyName": { "type": "string", "defaultValue": "AZURE-STORAGE-ACCOUNT-KEY" @@ -2247,7 +2242,7 @@ "apiVersion": "2022-07-01", "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('postgresInfoName'))]", "properties": { - "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', parameters('postgresDatabaseAdminUserName'), 'dbname', parameters('postgresDatabaseName'), 'host', parameters('postgresServerName'), 'password', parameters('postgresDatabaseAdminPassword'))), '')]" + "value": "[if(not(equals(parameters('postgresServerName'), '')), string(createObject('user', parameters('postgresDatabaseAdminUserName'), 'dbname', parameters('postgresDatabaseName'), 'host', parameters('postgresServerName'))), '')]" } }, { @@ -2661,7 +2656,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRES_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('websiteName')))), createObject())))]" } }, "template": { @@ -3661,7 +3656,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('websiteName'))))), createObject())))]" } }, "template": { @@ -4700,7 +4695,8 @@ "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", "FUNCTION_KEY": "[variables('clientKey')]", "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]" + "LOGLEVEL": "[parameters('logLevel')]", + "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('adminWebsiteName'))), createObject())]" } } }, @@ -5539,6 +5535,7 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'monitoring')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName'))]", @@ -5658,7 +5655,8 @@ "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", "FUNCTION_KEY": "[variables('clientKey')]", "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]" + "LOGLEVEL": "[parameters('logLevel')]", + "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('adminWebsiteName')))), createObject())]" } } }, @@ -6497,6 +6495,7 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'monitoring')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName'))]", @@ -8318,6 +8317,9 @@ "authType": { "value": "[parameters('authType')]" }, + "databaseType": { + "value": "[parameters('databaseType')]" + }, "appSettings": { "value": { "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", @@ -8346,7 +8348,8 @@ "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", "LOGLEVEL": "[parameters('logLevel')]", "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]" + "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", + "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('functionName'))), createObject())]" } } }, @@ -8357,7 +8360,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "17678067728265319370" + "templateHash": "11172260966600448386" } }, "parameters": { @@ -8464,9 +8467,8 @@ "type": "string", "defaultValue": "" }, - "cosmosDBKeyName": { - "type": "string", - "defaultValue": "" + "databaseType": { + "type": "string" } }, "resources": [ @@ -8542,6 +8544,9 @@ "useKeyVault": { "value": "[parameters('useKeyVault')]" }, + "managedIdentity": { + "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } @@ -9565,6 +9570,7 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'monitoring')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName'))]", @@ -9642,6 +9648,9 @@ "authType": { "value": "[parameters('authType')]" }, + "databaseType": { + "value": "[parameters('databaseType')]" + }, "appSettings": { "value": { "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", @@ -9670,7 +9679,8 @@ "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", "LOGLEVEL": "[parameters('logLevel')]", "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]" + "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", + "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('functionName')))), createObject())]" } } }, @@ -9681,7 +9691,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "17678067728265319370" + "templateHash": "11172260966600448386" } }, "parameters": { @@ -9788,9 +9798,8 @@ "type": "string", "defaultValue": "" }, - "cosmosDBKeyName": { - "type": "string", - "defaultValue": "" + "databaseType": { + "type": "string" } }, "resources": [ @@ -9866,6 +9875,9 @@ "useKeyVault": { "value": "[parameters('useKeyVault')]" }, + "managedIdentity": { + "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" } @@ -10889,6 +10901,7 @@ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'monitoring')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureOpenAIResourceName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", "[subscriptionResourceId('Microsoft.Resources/resourceGroups', variables('rgName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('azureAISearchName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('speechServiceName'))]", @@ -12030,6 +12043,7 @@ }, "webAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('websiteName')), '2022-09-01').outputs.FRONTEND_API_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_NAME.value))]", "adminAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName')), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value))]", + "functionAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('functionName')), '2022-09-01').outputs.functionName.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('functionName'))), '2022-09-01').outputs.functionName.value))]", "managedIdentityName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.name]" } @@ -12041,7 +12055,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "5666855677780801837" + "templateHash": "7476029520107410469" } }, "parameters": { @@ -12071,6 +12085,9 @@ }, "managedIdentityName": { "type": "string" + }, + "functionAppPrincipalName": { + "type": "string" } }, "resources": [ @@ -12089,7 +12106,7 @@ "properties": { "azCliVersion": "2.52.0", "primaryScriptUri": "[format('{0}scripts/run_create_table_script.sh', parameters('baseUrl'))]", - "arguments": "[format('{0} {1} {2} {3} {4} {5} {6}', parameters('baseUrl'), parameters('keyVaultName'), resourceGroup().name, parameters('postgresSqlServerName'), parameters('webAppPrincipalName'), parameters('adminAppPrincipalName'), parameters('managedIdentityName'))]", + "arguments": "[format('{0} {1} {2} {3} {4} {5} {6} {7}', parameters('baseUrl'), parameters('keyVaultName'), resourceGroup().name, parameters('postgresSqlServerName'), parameters('webAppPrincipalName'), parameters('adminAppPrincipalName'), parameters('functionAppPrincipalName'), parameters('managedIdentityName'))]", "timeout": "PT1H", "retentionInterval": "PT1H", "cleanupPreference": "OnSuccess" @@ -12101,6 +12118,8 @@ "dependsOn": [ "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName'))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('functionName'))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('functionName')))]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity')]", "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql')]", @@ -12354,7 +12373,7 @@ }, "AZURE_POSTGRESDB_INFO": { "type": "string", - "value": "[string(createObject('serverName', format('{0}.postgres.database.azure.com', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser, 'password', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbPwd))]" + "value": "[string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', ''))]" } } } \ No newline at end of file diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 8ae2f7c6e..4945b9332 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -7,6 +7,7 @@ key_vault_name = "kv_to-be-replaced" principal_name = "webAppPrincipalName" admin_principal_name = "adminAppPrincipalName" +function_app_principal_name = "functionAppPrincipalName" user = "managedIdentityName" def get_secrets_from_kv(kv_name, secret_name): @@ -72,11 +73,6 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): conn = psycopg2.connect(conn_string) cursor = conn.cursor() -grant_permissions(cursor, dbname, "public", principal_name) -conn.commit() -# grant_permissions(cursor, dbname, "public", admin_principal_name) -# conn.commit() - # Drop and recreate the conversations table cursor.execute("DROP TABLE IF EXISTS conversations") conn.commit() @@ -137,5 +133,14 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") conn.commit() +grant_permissions(cursor, dbname, "public", principal_name) +conn.commit() + +grant_permissions(cursor, dbname, "public", admin_principal_name) +conn.commit() + +grant_permissions(cursor, dbname, "public", function_app_principal_name) +conn.commit() + cursor.close() conn.close() diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 31bc4cf61..81210e4f5 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -10,7 +10,8 @@ resourceGroup="$3" serverName="$4" webAppPrincipalName="$5" adminAppPrincipalName="$6" -managedIdentityName="$7" +functionAppPrincipalName="$7" +managedIdentityName="$8" echo "Script Started" @@ -33,6 +34,7 @@ sed -i "s/kv_to-be-replaced/${keyvaultName}/g" "create_postgres_tables.py" sed -i "s/webAppPrincipalName/${webAppPrincipalName}/g" "create_postgres_tables.py" sed -i "s/adminAppPrincipalName/${adminAppPrincipalName}/g" "create_postgres_tables.py" sed -i "s/managedIdentityName/${managedIdentityName}/g" "create_postgres_tables.py" +sed -i "s/functionAppPrincipalName/${functionAppPrincipalName}/g" "create_postgres_tables.py" pip install -r requirements.txt From 029054ce6d15d393c7f40662bffa423e88da81d0 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 29 Nov 2024 21:00:10 +0530 Subject: [PATCH 079/107] fix: Updated python script --- infra/app/storekeys.bicep | 2 +- infra/main.json | 6 +++--- scripts/data_scripts/create_postgres_tables.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/infra/app/storekeys.bicep b/infra/app/storekeys.bicep index 6b5c3da12..b2f9b9f39 100644 --- a/infra/app/storekeys.bicep +++ b/infra/app/storekeys.bicep @@ -8,7 +8,7 @@ param contentSafetyName string = '' param speechServiceName string = '' param computerVisionName string = '' param postgresServerName string = '' // PostgreSQL server name -param postgresDatabaseName string = 'PostgreSQL' // Default database name +param postgresDatabaseName string = 'postgres' // Default database name param postgresInfoName string = 'AZURE-POSTGRESQL-INFO' // Secret name for PostgreSQL info param postgresDatabaseAdminUserName string = '' param storageAccountKeyName string = 'AZURE-STORAGE-ACCOUNT-KEY' diff --git a/infra/main.json b/infra/main.json index ae7b677ec..5286f6a99 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "13094848063702055322" + "templateHash": "12968861470765416454" } }, "parameters": { @@ -2085,7 +2085,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "3141925625583847916" + "templateHash": "1863464702640092735" } }, "parameters": { @@ -2131,7 +2131,7 @@ }, "postgresDatabaseName": { "type": "string", - "defaultValue": "PostgreSQL" + "defaultValue": "postgres" }, "postgresInfoName": { "type": "string", diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 4945b9332..d2ed48504 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -59,8 +59,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) host = postgres_details.get("host", "") -dbname = postgres_details.get("database", "") -password = postgres_details.get("password", "") +dbname = postgres_details.get("dbname", "") # Acquire the access token cred = DefaultAzureCredential() From f392082b4dbbe31341c19433ca4852f8129b21b9 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 29 Nov 2024 21:19:13 +0530 Subject: [PATCH 080/107] Commented PG_DISKANN as it is not available currently --- scripts/data_scripts/create_postgres_tables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index d2ed48504..6702676a8 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -107,8 +107,8 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): conn.commit() # Add pg_diskann extension and search_indexes table -cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") -conn.commit() +# cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") +# conn.commit() cursor.execute("DROP TABLE IF EXISTS search_indexes;") conn.commit() From 1887f64270f8bab89bc02d4f2351efd4475b43cb Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 29 Nov 2024 23:01:58 +0530 Subject: [PATCH 081/107] Unit Testcase Chathistory --- code/tests/test_chat_history.py | 605 ++++++++++++++++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 code/tests/test_chat_history.py diff --git a/code/tests/test_chat_history.py b/code/tests/test_chat_history.py new file mode 100644 index 000000000..4ac0cf567 --- /dev/null +++ b/code/tests/test_chat_history.py @@ -0,0 +1,605 @@ +""" +This module tests the entry point for the application. +""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from create_app import create_app + + +@pytest.fixture +def client(): + """Create a test client for the app.""" + app = create_app() + app.testing = True + return app.test_client() + + +@pytest.fixture +def mock_conversation_client(): + """Mock the database client.""" + with patch( + "backend.batch.utilities.chat_history.database_factory.DatabaseFactory.get_conversation_client" + ) as mock: + mock_conversation_client = AsyncMock() + mock.return_value = mock_conversation_client + yield mock_conversation_client + + +@pytest.fixture +def mock_config_helper(): + """Mock the ConfigHelper to control the config behavior.""" + with patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) as mock: + mock_config = MagicMock() + mock_config.enable_chat_history = ( + True # Ensure chat history is enabled for the test + ) + mock.return_value = mock_config + yield mock_config + + +class TestListConversations: + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_list_conversations_success( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the list_conversations endpoint works when everything is set up correctly.""" + # Given + get_active_config_or_default_mock.return_value.prompts.conversational_flow = ( + "custom" + ) + get_active_config_or_default_mock.enable_chat_history = True + mock_conversation_client.get_conversations = AsyncMock( + return_value=[{"conversation_id": "1", "content": "Hello, world!"}] + ) + + # When + response = client.get("/api/history/list?offset=0") + + # Then + assert response.status_code == 200 + assert response.json == [{"conversation_id": "1", "content": "Hello, world!"}] + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_list_conversations_no_history( + self, get_active_config_or_default_mock, client + ): + """Test that the list_conversations endpoint returns an error if chat history is not enabled.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = False + + # When + response = client.get("/api/history/list?offset=0") + + # Then + assert response.status_code == 400 + assert response.json == {"error": "Chat history is not available"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_list_conversations_db_error( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the list_conversations endpoint returns an error if the database is not available.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversations = AsyncMock( + side_effect=Exception("Database error") + ) + + # When + response = client.get("/api/history/list?offset=0") + + # Then + assert response.status_code == 500 + assert response.json == { + "error": "Error while listing historical conversations" + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_list_conversations_no_conversations( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the list_conversations endpoint returns an error if no conversations are found.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversations = AsyncMock( + return_value="invalid response" + ) + + # When + response = client.get("/api/history/list?offset=0") + + # Then + assert response.status_code == 404 + assert response.json == { + "error": "No conversations for 00000000-0000-0000-0000-000000000000 were found" + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_rename_conversation_success( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the rename_conversation endpoint works correctly.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversations = AsyncMock( + return_value={"conversation_id": "1", "title": "Old Title"} + ) + mock_conversation_client.upsert_conversation = AsyncMock( + return_value={"conversation_id": "1", "title": "New Title"} + ) + + request_json = {"conversation_id": "1", "title": "New Title"} + + # When + response = client.post("/api/history/rename", json=request_json) + + # Then + assert response.status_code == 200 + assert response.json == {"conversation_id": "1", "title": "New Title"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_rename_conversation_no_history( + self, get_active_config_or_default_mock, client + ): + """Test that the rename_conversation endpoint returns an error if chat history is not enabled.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = False + + request_json = {"conversation_id": "1", "title": "New Title"} + + # When + response = client.post("/api/history/rename", json=request_json) + + # Then + assert response.status_code == 400 + assert response.json == {"error": "Chat history is not available"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_rename_conversation_missing_conversation_id( + self, get_active_config_or_default_mock, client + ): + """Test that the rename_conversation endpoint returns an error if conversation_id is missing.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + + request_json = {"title": "New Title"} + + # When + response = client.post("/api/history/rename", json=request_json) + + # Then + assert response.status_code == 400 + assert response.json == {"error": "conversation_id is required"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_rename_conversation_empty_title( + self, get_active_config_or_default_mock, client + ): + """Test that the rename_conversation endpoint returns an error if the title is empty.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + + request_json = {"conversation_id": "1", "title": ""} + + # When + response = client.post("/api/history/rename", json=request_json) + + # Then + assert response.status_code == 400 + assert response.json == {"error": "A non-empty title is required"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + @patch( + "backend.batch.utilities.chat_history.database_factory.DatabaseFactory.get_conversation_client" + ) + def test_rename_conversation_db_error( + self, mock_conversation_client, get_active_config_or_default_mock, client + ): + """Test that the rename_conversation endpoint returns an error if the database is not available.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.return_value.get_conversation = AsyncMock( + side_effect=Exception("Database error") + ) + + request_json = {"conversation_id": "1", "title": "New Title"} + + # When + response = client.post("/api/history/rename", json=request_json) + + # Then + assert response.status_code == 500 + assert response.json == {"error": "Error while renaming conversation"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_rename_conversation_not_found( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the rename_conversation endpoint returns an error if the conversation is not found.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversation = AsyncMock(return_value=None) + + request_json = {"conversation_id": "1", "title": "New Title"} + + # When + response = client.post("/api/history/rename", json=request_json) + + # Then + assert response.status_code == 400 + assert response.json == { + "error": "Conversation 1 was not found. It either does not exist or the logged in user does not have access to it." + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_get_conversation_success( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the get_conversation endpoint works correctly.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversation = AsyncMock( + return_value={"conversation_id": "1", "title": "Sample Conversation"} + ) + mock_conversation_client.get_messages = AsyncMock( + return_value=[ + { + "id": "1", + "role": "user", + "content": "Hello, world!", + "createdAt": "2024-11-29T12:00:00Z", + } + ] + ) + + request_json = {"conversation_id": "1"} + + # When + response = client.post("/api/history/read", json=request_json) + + # Then + assert response.status_code == 200 + assert response.json == { + "conversation_id": "1", + "messages": [ + { + "id": "1", + "role": "user", + "content": "Hello, world!", + "createdAt": "2024-11-29T12:00:00Z", + "feedback": None, + } + ], + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_get_conversation_no_history( + self, get_active_config_or_default_mock, client + ): + """Test that the get_conversation endpoint returns an error if chat history is not enabled.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = False + + request_json = {"conversation_id": "1"} + + # When + response = client.post("/api/history/read", json=request_json) + + # Then + assert response.status_code == 400 + assert response.json == {"error": "Chat history is not available"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_get_conversation_missing_conversation_id( + self, get_active_config_or_default_mock, client + ): + """Test that the get_conversation endpoint returns an error if conversation_id is missing.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + + request_json = {} + + # When + response = client.post("/api/history/read", json=request_json) + + # Then + assert response.status_code == 400 + assert response.json == {"error": "conversation_id is required"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + @patch( + "backend.batch.utilities.chat_history.database_factory.DatabaseFactory.get_conversation_client" + ) + def test_get_conversation_db_error( + self, mock_conversation_client, get_active_config_or_default_mock, client + ): + """Test that the get_conversation endpoint returns an error if the database is not available.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.return_value.get_conversation = AsyncMock( + side_effect=Exception("Database error") + ) + + request_json = {"conversation_id": "1"} + + # When + response = client.post("/api/history/read", json=request_json) + + # Then + assert response.status_code == 500 + assert response.json == {"error": "Error while fetching conversation history"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_get_conversation_not_found( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the get_conversation endpoint returns an error if the conversation is not found.""" + # Given + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversation = AsyncMock(return_value=None) + + request_json = {"conversation_id": "1"} + + # When + response = client.post("/api/history/read", json=request_json) + + # Then + assert response.status_code == 400 + assert response.json == { + "error": "Conversation 1 was not found. It either does not exist or the logged in user does not have access to it." + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_delete_conversation_success( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test that the delete_conversation endpoint works correctly.""" + + # Setup mocks + get_active_config_or_default_mock.return_value.enable_chat_history = True + + # Mock the database client + mock_conversation_client.delete_messages = AsyncMock(return_value=None) + mock_conversation_client.delete_conversation = AsyncMock(return_value=None) + + # Define request data + request_json = {"conversation_id": "conv123"} + + # Make DELETE request to delete the conversation + response = client.delete("/api/history/delete", json=request_json) + + # Assert the response status and data + assert response.status_code == 200 + assert response.json == { + "message": "Successfully deleted conversation and messages", + "conversation_id": "conv123", + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_delete_conversation_no_chat_history( + self, get_active_config_or_default_mock, client + ): + """Test when chat history is not enabled in the configuration.""" + + # Setup mocks + get_active_config_or_default_mock.return_value.enable_chat_history = False + + # Define request data + request_json = {"conversation_id": "conv123"} + + # Make DELETE request to delete the conversation + response = client.delete("/api/history/delete", json=request_json) + + # Assert the response status and error message + assert response.status_code == 400 + assert response.json == {"error": "Chat history is not available"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_delete_conversation_missing_conversation_id( + self, get_active_config_or_default_mock, client + ): + """Test when the conversation_id is missing in the request.""" + + # Setup mocks + get_active_config_or_default_mock.return_value.enable_chat_history = True + + # Define request data (missing conversation_id) + request_json = {} + + # Make DELETE request to delete the conversation + response = client.delete("/api/history/delete", json=request_json) + + # Assert the response status and error message + assert response.status_code == 400 + assert response.json == { + "error": "Conversation None was not found. It either does not exist or the logged in user does not have access to it." + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_delete_conversation_database_error( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test when the database client connection fails.""" + + # Setup mocks + get_active_config_or_default_mock.return_value.enable_chat_history = True + + # Mock a failure in the database client connection + mock_conversation_client.connect.side_effect = Exception( + "Database not available" + ) + + # Define request data + request_json = {"conversation_id": "conv123"} + + # Make DELETE request to delete the conversation + response = client.delete("/api/history/delete", json=request_json) + + # Assert the response status and error message + assert response.status_code == 500 + assert response.json == {"error": "Error while deleting conversation history"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_delete_conversation_internal_error( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + """Test when an unexpected internal error occurs during conversation deletion.""" + + # Setup mocks + get_active_config_or_default_mock.return_value.enable_chat_history = True + + # Mock an unexpected error in the database client deletion + mock_conversation_client.delete_messages.side_effect = Exception( + "Unexpected error" + ) + + # Define request data + request_json = {"conversation_id": "conv123"} + + # Make DELETE request to delete the conversation + response = client.delete("/api/history/delete", json=request_json) + + # Assert the response status and error message + assert response.status_code == 500 + assert response.json == {"error": "Error while deleting conversation history"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_delete_all_conversations_success( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversation = AsyncMock( + return_value=[{"id": "conv1"}, {"id": "conv2"}] + ) + + response = client.delete("/api/history/delete_all") + assert response.status_code == 200 + assert response.json == { + "message": "Successfully deleted all conversations and messages for user 00000000-0000-0000-0000-000000000000" + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_delete_all_conversations_no_chat_history( + self, get_active_config_or_default_mock, client + ): + get_active_config_or_default_mock.return_value.enable_chat_history = False + response = client.delete("/api/history/delete_all") + assert response.status_code == 400 + assert response.json == {"error": "Chat history is not available"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_update_conversation_success( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversation.return_value = { + "title": "Test Title", + "updatedAt": "2024-12-01", + "id": "conv1", + } + mock_conversation_client.create_message.return_value = "success" + request_json = { + "conversation_id": "conv1", + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi!"}, + ], + } + + # When + response = client.post("/api/history/update", json=request_json) + + assert response.status_code == 200 + assert response.json == { + "data": { + "conversation_id": "conv1", + "date": "2024-12-01", + "title": "Test Title", + }, + "success": True, + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_update_conversation_no_chat_history( + self, get_active_config_or_default_mock, client + ): + get_active_config_or_default_mock.return_value.enable_chat_history = False + response = client.post( + "/api/history/update", json={}, headers={"Content-Type": "application/json"} + ) + assert response.status_code == 400 + assert response.json == {"error": "Chat history is not available"} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_get_frontend_settings_success( + self, get_active_config_or_default_mock, client + ): + get_active_config_or_default_mock.return_value.enable_chat_history = True + response = client.get("/api/history/frontend_settings") + assert response.status_code == 200 + assert response.json == {"CHAT_HISTORY_ENABLED": True} + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_get_frontend_settings_error( + self, get_active_config_or_default_mock, client + ): + get_active_config_or_default_mock.side_effect = Exception("Test Error") + response = client.get("/api/history/frontend_settings") + assert response.status_code == 500 + assert response.json == {"error": "Error while getting frontend settings"} From 36a29f651c7ed52a25e0c497af4ede502ce945d3 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Mon, 2 Dec 2024 18:25:36 +0530 Subject: [PATCH 082/107] Code changes to test E2E functionality --- infra/core/database/postgresdb.bicep | 31 +++++++-------- .../data_scripts/create_postgres_tables.py | 38 +++++++++---------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index 557732cd8..f173821ce 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -74,6 +74,17 @@ resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { ] } + +resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-12-01-preview' = { + name: 'azure.extensions' + parent: serverName_resource + properties: { + value: 'vector' + source: 'user-override' + } +} + + resource azureADAdministrator 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2022-12-01' = { parent: serverName_resource name: managedIdentityObjectId @@ -83,7 +94,7 @@ resource azureADAdministrator 'Microsoft.DBforPostgreSQL/flexibleServers/adminis tenantId: subscription().tenantId } dependsOn: [ - delayScript + configurations ] } @@ -105,7 +116,7 @@ resource firewall_all 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2 endIpAddress: '255.255.255.255' } dependsOn: [ - delayScript + azureADAdministrator ] } @@ -117,28 +128,14 @@ resource firewall_azure 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules endIpAddress: '0.0.0.0' } dependsOn: [ - delayScript + azureADAdministrator ] } -resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-12-01-preview' = { - name: 'azure.extensions' - parent: serverName_resource - properties: { - value: 'pg_diskann' - source: 'user-override' - } - dependsOn: [ - firewall_all,firewall_azure - ] -} - - output postgresDbOutput object = { postgresSQLName: serverName_resource.name postgreSQLServerName: '${serverName_resource.name}.postgres.database.azure.com' postgreSQLDatabaseName: 'postgres' postgreSQLDbUser: administratorLogin - postgreSQLDbPwd: administratorLoginPassword sslMode: 'Require' } diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 6702676a8..132780f7d 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -110,27 +110,27 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): # cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") # conn.commit() -cursor.execute("DROP TABLE IF EXISTS search_indexes;") -conn.commit() +# cursor.execute("DROP TABLE IF EXISTS search_indexes;") +# conn.commit() -table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( - id text, - title text, - chunk integer, - chunk_id text, - "offset" integer, - page_number integer, - content text, - source text, - metadata text, - content_vector public.vector(1536) -);""" - -cursor.execute(table_create_command) -conn.commit() +# table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( +# id text, +# title text, +# chunk integer, +# chunk_id text, +# "offset" integer, +# page_number integer, +# content text, +# source text, +# metadata text, +# content_vector public.vector(1536) +# );""" + +# cursor.execute(table_create_command) +# conn.commit() -cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") -conn.commit() +# cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") +# conn.commit() grant_permissions(cursor, dbname, "public", principal_name) conn.commit() From 63c16add4d3b34cc53a1a951f80e73a1d07483b8 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Mon, 2 Dec 2024 19:16:01 +0530 Subject: [PATCH 083/107] fix: Script issue & code refactoring --- infra/core/database/postgresdb.bicep | 30 ++++++------ infra/main.bicep | 4 +- infra/main.json | 43 +++++------------ .../data_scripts/create_postgres_tables.py | 48 +++++++++---------- 4 files changed, 52 insertions(+), 73 deletions(-) diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index f173821ce..b2f342c47 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -59,21 +59,20 @@ resource serverName_resource 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12- } } -resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { - name: 'waitForServerReady' - location: resourceGroup().location - kind: 'AzurePowerShell' - properties: { - azPowerShellVersion: '3.0' - scriptContent: 'start-sleep -Seconds 300' - cleanupPreference: 'Always' - retentionInterval: 'PT1H' - } - dependsOn: [ - serverName_resource - ] -} - +// resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { +// name: 'waitForServerReady' +// location: resourceGroup().location +// kind: 'AzurePowerShell' +// properties: { +// azPowerShellVersion: '3.0' +// scriptContent: 'start-sleep -Seconds 300' +// cleanupPreference: 'Always' +// retentionInterval: 'PT1H' +// } +// dependsOn: [ +// serverName_resource +// ] +// } resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-12-01-preview' = { name: 'azure.extensions' @@ -84,7 +83,6 @@ resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configuration } } - resource azureADAdministrator 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2022-12-01' = { parent: serverName_resource name: managedIdentityObjectId diff --git a/infra/main.bicep b/infra/main.bicep index fcf5a00e8..d4d8abea0 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -321,7 +321,7 @@ var eventGridSystemTopicName = 'doc-processing' var tags = { 'azd-env-name': environmentName } var rgName = 'rg-${environmentName}' var keyVaultName = 'kv-${resourceToken}' -var baseUrl = 'https://raw.githubusercontent.com/Fr4nc3/chat-with-your-data-solution-accelerator/bicepdefaults/' +var baseUrl = 'https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/' var azureOpenAIModelInfo = string({ model: azureOpenAIModel modelName: azureOpenAIModelName @@ -535,7 +535,7 @@ module storekeys './app/storekeys.bicep' = if (useKeyVault) { postgresServerName: databaseType == 'PostgreSQL' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName : '' - postgresDatabaseName: databaseType == 'PostgreSQL' ? 'PostgreSQL' : '' + postgresDatabaseName: databaseType == 'PostgreSQL' ? 'postgres' : '' postgresDatabaseAdminUserName: databaseType == 'PostgreSQL' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDbUser : '' diff --git a/infra/main.json b/infra/main.json index 5286f6a99..c85ab819a 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12968861470765416454" + "templateHash": "6080738341036036588" } }, "parameters": { @@ -648,7 +648,7 @@ }, "rgName": "[format('rg-{0}', parameters('environmentName'))]", "keyVaultName": "[format('kv-{0}', parameters('resourceToken'))]", - "baseUrl": "https://raw.githubusercontent.com/Fr4nc3/chat-with-your-data-solution-accelerator/bicepdefaults/", + "baseUrl": "https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/", "azureOpenAIModelInfo": "[string(createObject('model', parameters('azureOpenAIModel'), 'modelName', parameters('azureOpenAIModelName'), 'modelVersion', parameters('azureOpenAIModelVersion')))]", "azureOpenAIEmbeddingModelInfo": "[string(createObject('model', parameters('azureOpenAIEmbeddingModel'), 'modelName', parameters('azureOpenAIEmbeddingModelName'), 'modelVersion', parameters('azureOpenAIEmbeddingModelVersion')))]", "appversion": "latest", @@ -977,7 +977,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "17455459966134467610" + "templateHash": "10751990458550607112" } }, "parameters": { @@ -1084,16 +1084,12 @@ } }, { - "type": "Microsoft.Resources/deploymentScripts", - "apiVersion": "2020-10-01", - "name": "waitForServerReady", - "location": "[resourceGroup().location]", - "kind": "AzurePowerShell", + "type": "Microsoft.DBforPostgreSQL/flexibleServers/configurations", + "apiVersion": "2023-12-01-preview", + "name": "[format('{0}/{1}', parameters('serverName'), 'azure.extensions')]", "properties": { - "azPowerShellVersion": "3.0", - "scriptContent": "start-sleep -Seconds 300", - "cleanupPreference": "Always", - "retentionInterval": "PT1H" + "value": "vector", + "source": "user-override" }, "dependsOn": [ "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" @@ -1109,7 +1105,7 @@ "tenantId": "[subscription().tenantId]" }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/configurations', parameters('serverName'), 'azure.extensions')]", "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" ] }, @@ -1123,7 +1119,7 @@ "endIpAddress": "255.255.255.255" }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/administrators', parameters('serverName'), parameters('managedIdentityObjectId'))]", "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" ] }, @@ -1137,21 +1133,7 @@ "endIpAddress": "0.0.0.0" }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" - ] - }, - { - "type": "Microsoft.DBforPostgreSQL/flexibleServers/configurations", - "apiVersion": "2023-12-01-preview", - "name": "[format('{0}/{1}', parameters('serverName'), 'azure.extensions')]", - "properties": { - "value": "pg_diskann", - "source": "user-override" - }, - "dependsOn": [ - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-IPs')]", - "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/firewallRules', parameters('serverName'), 'allow-all-azure-internal-IPs')]", + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers/administrators', parameters('serverName'), parameters('managedIdentityObjectId'))]", "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" ] } @@ -1164,7 +1146,6 @@ "postgreSQLServerName": "[format('{0}.postgres.database.azure.com', parameters('serverName'))]", "postgreSQLDatabaseName": "postgres", "postgreSQLDbUser": "[parameters('administratorLogin')]", - "postgreSQLDbPwd": "[parameters('administratorLoginPassword')]", "sslMode": "Require" } } @@ -2072,7 +2053,7 @@ "computerVisionName": "[if(parameters('useAdvancedImageProcessing'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.name.value), createObject('value', ''))]", "cosmosAccountName": "[if(equals(parameters('databaseType'), 'CosmosDB'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName), createObject('value', ''))]", "postgresServerName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName), createObject('value', ''))]", - "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', 'PostgreSQL'), createObject('value', ''))]", + "postgresDatabaseName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', 'postgres'), createObject('value', ''))]", "postgresDatabaseAdminUserName": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDbUser), createObject('value', ''))]", "rgName": { "value": "[variables('rgName')]" diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 132780f7d..d2ed48504 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -107,30 +107,30 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): conn.commit() # Add pg_diskann extension and search_indexes table -# cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") -# conn.commit() - -# cursor.execute("DROP TABLE IF EXISTS search_indexes;") -# conn.commit() - -# table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( -# id text, -# title text, -# chunk integer, -# chunk_id text, -# "offset" integer, -# page_number integer, -# content text, -# source text, -# metadata text, -# content_vector public.vector(1536) -# );""" - -# cursor.execute(table_create_command) -# conn.commit() - -# cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") -# conn.commit() +cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") +conn.commit() + +cursor.execute("DROP TABLE IF EXISTS search_indexes;") +conn.commit() + +table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( + id text, + title text, + chunk integer, + chunk_id text, + "offset" integer, + page_number integer, + content text, + source text, + metadata text, + content_vector public.vector(1536) +);""" + +cursor.execute(table_create_command) +conn.commit() + +cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") +conn.commit() grant_permissions(cursor, dbname, "public", principal_name) conn.commit() From 335476256836e0ffc639c0e87f7262150b718bbf Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 2 Dec 2024 20:17:30 +0530 Subject: [PATCH 084/107] Unit Testing for Chat History Functionality - Utilities --- .../test_postgres_search_handler.py | 169 ++++ code/tests/test_chat_history.py | 16 +- .../helpers/test_azure_postgres_helper.py | 741 ++++++++++++++++++ .../helpers/test_postgress_embedder.py | 211 +++++ 4 files changed, 1122 insertions(+), 15 deletions(-) create mode 100644 code/tests/search_utilities/test_postgres_search_handler.py create mode 100644 code/tests/utilities/helpers/test_azure_postgres_helper.py create mode 100644 code/tests/utilities/helpers/test_postgress_embedder.py diff --git a/code/tests/search_utilities/test_postgres_search_handler.py b/code/tests/search_utilities/test_postgres_search_handler.py new file mode 100644 index 000000000..eead10dd3 --- /dev/null +++ b/code/tests/search_utilities/test_postgres_search_handler.py @@ -0,0 +1,169 @@ +import pytest +from unittest.mock import MagicMock, patch +from backend.batch.utilities.common.source_document import SourceDocument +from backend.batch.utilities.search.postgres_search_handler import AzurePostgresHandler + + +@pytest.fixture(autouse=True) +def env_helper_mock(): + mock = MagicMock() + mock.POSTGRESQL_USER = "test_user" + mock.POSTGRESQL_PASSWORD = "test_password" + mock.POSTGRESQL_HOST = "test_host" + mock.POSTGRESQL_DB = "test_db" + return mock + + +@pytest.fixture(autouse=True) +def mock_search_client(): + with patch( + "backend.batch.utilities.search.postgres_search_handler.AzurePostgresHelper" + ) as mock: + search_client = mock.return_value.get_search_client.return_value + yield search_client + + +@pytest.fixture +def handler(env_helper_mock, mock_search_client): + with patch( + "backend.batch.utilities.search.postgres_search_handler", + return_value=mock_search_client, + ): + return AzurePostgresHandler(env_helper_mock) + + +def test_query_search(handler, mock_search_client): + mock_llm_helper = MagicMock() + mock_search_client.llm_helper = mock_llm_helper + + mock_llm_helper.generate_embeddings.return_value = [1, 2, 3] + + mock_search_client.get_search_indexes.return_value = [ + { + "id": "1", + "title": "Title1", + "chunk": "Chunk1", + "offset": 0, + "page_number": 1, + "content": "Content1", + "source": "Source1", + }, + { + "id": "2", + "title": "Title2", + "chunk": "Chunk2", + "offset": 1, + "page_number": 2, + "content": "Content2", + "source": "Source2", + }, + ] + + mock_search_client.get_search_client.return_value = mock_search_client + handler.azure_postgres_helper = mock_search_client + + result = handler.query_search("Sample question") + + mock_llm_helper.generate_embeddings.assert_called_once_with("Sample question") + mock_search_client.get_search_indexes.assert_called_once() + assert len(result) == 2 + assert isinstance(result[0], SourceDocument) + assert result[0].id == "1" + assert result[0].title == "Title1" + assert result[1].content == "Content2" + + +def test_convert_to_source_documents(handler): + search_results = [ + { + "id": "1", + "title": "Title1", + "chunk": "Chunk1", + "offset": 0, + "page_number": 1, + "content": "Content1", + "source": "Source1", + }, + { + "id": "2", + "title": "Title2", + "chunk": "Chunk2", + "offset": 1, + "page_number": 2, + "content": "Content2", + "source": "Source2", + }, + ] + + result = handler._convert_to_source_documents(search_results) + + assert len(result) == 2 + assert result[0].id == "1" + assert result[0].content == "Content1" + assert result[1].page_number == 2 + + +def test_create_search_client(handler, mock_search_client): + handler.azure_postgres_helper.get_search_client = MagicMock( + return_value=mock_search_client + ) + + result = handler.create_search_client() + + assert result == mock_search_client + + +def test_get_files(handler): + mock_get_files = MagicMock(return_value=["test1.txt", "test2.txt"]) + handler.azure_postgres_helper.get_files = mock_get_files + + result = handler.get_files() + + assert len(result) == 2 + assert result[0] == "test1.txt" + assert result[1] == "test2.txt" + + +def test_delete_files(handler): + files_to_delete = {"test1.txt": [1, 2], "test2.txt": [3]} + mock_delete_documents = MagicMock() + handler.azure_postgres_helper.delete_documents = mock_delete_documents + + result = handler.delete_files(files_to_delete) + + mock_delete_documents.assert_called_once_with([{"id": 1}, {"id": 2}, {"id": 3}]) + assert "test1.txt" in result + + +# Test case for delete_from_index method +def test_delete_from_index(handler): + blob_url = "https://example.com/blob" + + # Mocking methods + mock_search_by_blob_url = MagicMock(return_value=[{"id": "1", "title": "Title1"}]) + mock_output_results = MagicMock(return_value={"test1.txt": ["1"]}) + mock_delete_files = MagicMock(return_value="test1.txt") + + handler.search_by_blob_url = mock_search_by_blob_url + handler.output_results = mock_output_results + handler.delete_files = mock_delete_files + + handler.delete_from_index(blob_url) + + mock_search_by_blob_url.assert_called_once_with(blob_url) + mock_output_results.assert_called_once() + mock_delete_files.assert_called_once_with({"test1.txt": ["1"]}) + + +# Test case for get_unique_files method +def test_get_unique_files(handler): + mock_get_unique_files = MagicMock( + return_value=[{"title": "test1.txt"}, {"title": "test2.txt"}] + ) + handler.azure_postgres_helper.get_unique_files = mock_get_unique_files + + result = handler.get_unique_files() + + assert len(result) == 2 + assert result[0] == "test1.txt" + assert result[1] == "test2.txt" diff --git a/code/tests/test_chat_history.py b/code/tests/test_chat_history.py index 4ac0cf567..f1b8bdcb1 100644 --- a/code/tests/test_chat_history.py +++ b/code/tests/test_chat_history.py @@ -2,7 +2,7 @@ This module tests the entry point for the application. """ -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch import pytest from create_app import create_app @@ -27,20 +27,6 @@ def mock_conversation_client(): yield mock_conversation_client -@pytest.fixture -def mock_config_helper(): - """Mock the ConfigHelper to control the config behavior.""" - with patch( - "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" - ) as mock: - mock_config = MagicMock() - mock_config.enable_chat_history = ( - True # Ensure chat history is enabled for the test - ) - mock.return_value = mock_config - yield mock_config - - class TestListConversations: @patch( "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" diff --git a/code/tests/utilities/helpers/test_azure_postgres_helper.py b/code/tests/utilities/helpers/test_azure_postgres_helper.py new file mode 100644 index 000000000..907d0bdfd --- /dev/null +++ b/code/tests/utilities/helpers/test_azure_postgres_helper.py @@ -0,0 +1,741 @@ +import unittest +from unittest.mock import MagicMock, patch +import psycopg2 +from backend.batch.utilities.helpers.azure_postgres_helper import AzurePostgresHelper + + +class TestAzurePostgresHelper(unittest.TestCase): + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + def test_create_search_client_success(self, mock_connect, mock_credential): + # Arrange + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + + mock_connection = MagicMock() + mock_connect.return_value = mock_connection + + helper = AzurePostgresHelper() + helper.env_helper.POSTGRESQL_USER = "mock_user" + helper.env_helper.POSTGRESQL_HOST = "mock_host" + helper.env_helper.POSTGRESQL_DATABASE = "mock_database" + + # Act + connection = helper._create_search_client() + + # Assert + self.assertEqual(connection, mock_connection) + mock_credential.return_value.get_token.assert_called_once_with( + "https://ossrdbms-aad.database.windows.net/.default" + ) + mock_connect.assert_called_once_with( + "host=mock_host user=mock_user dbname=mock_database password=mock-access-token" + ) + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + def test_get_search_client_reuses_connection(self, mock_connect): + # Arrange + mock_connection = MagicMock() + mock_connection.closed = 0 # Simulate an open connection + mock_connect.return_value = mock_connection + + helper = AzurePostgresHelper() + helper.conn = mock_connection + + # Act + connection = helper.get_search_client() + + # Assert + self.assertEqual(connection, mock_connection) + mock_connect.assert_not_called() # Ensure no new connection is created + + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.RealDictCursor") + def test_get_search_indexes_success( + self, mock_cursor, mock_connect, mock_credential + ): + # Arrange + # Mock the EnvHelper and set required attributes + mock_env_helper = MagicMock() + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + + # Mock the database connection and cursor + mock_connection = MagicMock() + mock_connect.return_value = mock_connection + mock_cursor_instance = MagicMock() + mock_cursor.return_value = mock_cursor_instance + + # Mock the behavior of the context manager for the cursor + mock_cursor_context = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor_context + mock_results = [{"id": 1, "title": "Test"}] + mock_cursor_context.fetchall.return_value = mock_results + + # Replace EnvHelper in AzurePostgresHelper with the mocked version + helper = AzurePostgresHelper() + helper.env_helper = mock_env_helper + + # Embedding vector for the test + embedding_vector = [1, 2, 3] + + # Act + results = helper.get_search_indexes(embedding_vector) + + # Assert + self.assertEqual(results, mock_results) + mock_connect.assert_called_once_with( + "host=mock_host user=mock_user dbname=mock_database password=mock-access-token" + ) + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + def test_get_search_indexes_query_error(self, mock_connect): + # Arrange + # Mock the EnvHelper and set required attributes + mock_env_helper = MagicMock() + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + mock_connection = MagicMock() + mock_connect.return_value = mock_connection + + def raise_exception(*args, **kwargs): + raise Exception("Query execution error") + + mock_cursor_instance = MagicMock() + mock_cursor_instance.execute.side_effect = raise_exception + + mock_connection.cursor.return_value.__enter__.return_value = ( + mock_cursor_instance + ) + + helper = AzurePostgresHelper() + helper.env_helper = mock_env_helper + embedding_vector = [1, 2, 3] + + # Act & Assert + with self.assertRaises(Exception) as context: + helper.get_search_indexes(embedding_vector) + + self.assertEqual(str(context.exception), "Query execution error") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + def test_create_search_client_connection_error(self, mock_connect): + # Arrange + # Mock the EnvHelper and set required attributes + mock_env_helper = MagicMock() + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + def raise_exception(*args, **kwargs): + raise Exception("Connection error") + + mock_connect.side_effect = raise_exception + + helper = AzurePostgresHelper() + helper.env_helper = mock_env_helper + + # Act & Assert + with self.assertRaises(Exception) as context: + helper._create_search_client() + + self.assertEqual(str(context.exception), "Connection error") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_get_files_success(self, mock_env_helper, mock_connect): + # Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Arrange: Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the result of the cursor's fetchall() method + mock_cursor.fetchall.return_value = [ + {"id": 1, "title": "Title 1"}, + {"id": 2, "title": "Title 2"}, + ] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.get_files() + + # Assert: Check that the correct results are returned + self.assertEqual( + result, [{"id": 1, "title": "Title 1"}, {"id": 2, "title": "Title 2"}] + ) + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_get_files_no_results(self, mock_env_helper, mock_connect): + # Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Arrange: Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the result of the cursor's fetchall() method to return an empty list + mock_cursor.fetchall.return_value = [] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.get_files() + + # Assert: Check that the result is None + self.assertIsNone(result) + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + def test_get_files_db_error(self, mock_logger, mock_env_helper, mock_connect): + # Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Arrange: Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Simulate a database error when executing the query + mock_cursor.fetchall.side_effect = psycopg2.Error("Database error") + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act & Assert: Ensure that the exception is raised and the error is logged + with self.assertRaises(psycopg2.Error): + helper.get_files() + + mock_logger.error.assert_called_with( + "Database error while fetching titles: Database error" + ) + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + def test_get_files_unexpected_error( + self, mock_logger, mock_env_helper, mock_connect + ): + # Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Arrange: Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Simulate an unexpected error + mock_cursor.fetchall.side_effect = Exception("Unexpected error") + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act & Assert: Ensure that the exception is raised and the error is logged + with self.assertRaises(Exception): + helper.get_files() + + mock_logger.error.assert_called_with( + "Unexpected error while fetching titles: Unexpected error" + ) + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_delete_documents_success(self, mock_env_helper, mock_logger, mock_connect): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the behavior of cursor.rowcount and execute + mock_cursor.rowcount = 3 # Simulate 3 rows deleted + mock_cursor.execute.return_value = None + + ids_to_delete = [{"id": 1}, {"id": 2}, {"id": 3}] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.delete_documents(ids_to_delete) + + # Assert: Check that the correct number of rows were deleted + self.assertEqual(result, 3) + mock_connection.commit.assert_called_once() + mock_connection.close.assert_called_once() + mock_logger.info.assert_called_with("Deleted 3 documents.") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_delete_documents_no_ids(self, mock_env_helper, mock_logger, mock_connect): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # No IDs to delete + ids_to_delete = [] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.delete_documents(ids_to_delete) + + # Assert: Check that no rows were deleted and a warning was logged + self.assertEqual(result, 0) + mock_logger.warning.assert_called_with("No IDs provided for deletion.") + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_delete_documents_db_error( + self, mock_env_helper, mock_logger, mock_connect + ): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Simulate a database error during execution + mock_cursor.execute.side_effect = psycopg2.Error("Database error") + + ids_to_delete = [{"id": 1}, {"id": 2}] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act & Assert: Ensure that the exception is raised and the error is logged + with self.assertRaises(psycopg2.Error): + helper.delete_documents(ids_to_delete) + + mock_logger.error.assert_called_with( + "Database error while deleting documents: Database error" + ) + mock_connection.rollback.assert_called_once() + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_delete_documents_unexpected_error( + self, mock_env_helper, mock_logger, mock_connect + ): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Simulate an unexpected error + mock_cursor.execute.side_effect = Exception("Unexpected error") + + ids_to_delete = [{"id": 1}, {"id": 2}] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act & Assert: Ensure that the exception is raised and the error is logged + with self.assertRaises(Exception): + helper.delete_documents(ids_to_delete) + + mock_logger.error.assert_called_with( + "Unexpected error while deleting documents: Unexpected error" + ) + mock_connection.rollback.assert_called_once() + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_perform_search_success(self, mock_env_helper, mock_logger, mock_connect): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the behavior of cursor's execute and fetchall + mock_cursor.fetchall.return_value = [ + { + "title": "Test Title", + "content": "Test Content", + "metadata": "Test Metadata", + } + ] + + title_to_search = "Test Title" + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.perform_search(title_to_search) + + # Assert: Check that the results match the expected data + self.assertEqual(len(result), 1) # One result returned + self.assertEqual(result[0]["title"], "Test Title") + self.assertEqual(result[0]["content"], "Test Content") + self.assertEqual(result[0]["metadata"], "Test Metadata") + + # Ensure the connection was closed + mock_connection.close.assert_called_once() + mock_logger.info.assert_called_with("Retrieved 1 search result(s).") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_perform_search_no_results( + self, mock_env_helper, mock_logger, mock_connect + ): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the behavior of cursor's execute and fetchall to return no results + mock_cursor.fetchall.return_value = [] + + title_to_search = "Nonexistent Title" + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.perform_search(title_to_search) + + # Assert: Check that no results were returned + self.assertEqual(result, []) # Empty list returned for no results + + # Ensure the connection was closed + mock_connection.close.assert_called_once() + mock_logger.info.assert_called_with("Retrieved 0 search result(s).") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_perform_search_error(self, mock_env_helper, mock_logger, mock_connect): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Simulate an error during the execution of the query + mock_cursor.execute.side_effect = Exception("Database error") + + title_to_search = "Test Title" + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act & Assert: Ensure that the exception is raised and the error is logged + with self.assertRaises(Exception): + helper.perform_search(title_to_search) + + mock_logger.error.assert_called_with( + "Error executing search query: Database error" + ) + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_get_unique_files_success(self, mock_env_helper, mock_logger, mock_connect): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the behavior of cursor's execute and fetchall + mock_cursor.fetchall.return_value = [ + {"title": "Unique Title 1"}, + {"title": "Unique Title 2"}, + ] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.get_unique_files() + + # Assert: Check that the results match the expected data + self.assertEqual(len(result), 2) # Two unique titles returned + self.assertEqual(result[0]["title"], "Unique Title 1") + self.assertEqual(result[1]["title"], "Unique Title 2") + + # Ensure the connection was closed + mock_connection.close.assert_called_once() + mock_logger.info.assert_called_with("Retrieved 2 unique title(s).") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_get_unique_files_no_results( + self, mock_env_helper, mock_logger, mock_connect + ): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the behavior of cursor's execute and fetchall to return no results + mock_cursor.fetchall.return_value = [] + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act: Call the method under test + result = helper.get_unique_files() + + # Assert: Check that no results were returned + self.assertEqual(result, []) # Empty list returned for no results + + # Ensure the connection was closed + mock_connection.close.assert_called_once() + mock_logger.info.assert_called_with("Retrieved 0 unique title(s).") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_get_unique_files_error(self, mock_env_helper, mock_logger, mock_connect): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Simulate an error during the execution of the query + mock_cursor.execute.side_effect = Exception("Database error") + + # Create an instance of the helper + helper = AzurePostgresHelper() + + # Act & Assert: Ensure that the exception is raised and the error is logged + with self.assertRaises(Exception): + helper.get_unique_files() + + mock_logger.error.assert_called_with( + "Error executing search query: Database error" + ) + mock_connection.close.assert_called_once() + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_search_by_blob_url_success( + self, mock_env_helper, mock_logger, mock_connect + ): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the behavior of cursor's execute and fetchall + mock_cursor.fetchall.return_value = [ + {"id": 1, "title": "Title 1"}, + {"id": 2, "title": "Title 2"}, + ] + + # Create an instance of the helper + helper = AzurePostgresHelper() + blob_url = "mock_blob_url" + + # Act: Call the method under test + result = helper.search_by_blob_url(blob_url) + + # Assert: Check that the results match the expected data + self.assertEqual(len(result), 2) # Two titles returned + self.assertEqual(result[0]["title"], "Title 1") + self.assertEqual(result[1]["title"], "Title 2") + + # Ensure the connection was closed + mock_connection.close.assert_called_once() + mock_logger.info.assert_called_with("Retrieved 2 unique title(s).") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_search_by_blob_url_no_results( + self, mock_env_helper, mock_logger, mock_connect + ): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Mock the behavior of cursor's execute and fetchall to return no results + mock_cursor.fetchall.return_value = [] + + # Create an instance of the helper + helper = AzurePostgresHelper() + blob_url = "mock_blob_url" + + # Act: Call the method under test + result = helper.search_by_blob_url(blob_url) + + # Assert: Check that no results were returned + self.assertEqual(result, []) # Empty list returned for no results + + # Ensure the connection was closed + mock_connection.close.assert_called_once() + mock_logger.info.assert_called_with("Retrieved 0 unique title(s).") + + @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") + @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") + def test_search_by_blob_url_error(self, mock_env_helper, mock_logger, mock_connect): + # Arrange: Mock the EnvHelper attributes + mock_env_helper.POSTGRESQL_USER = "mock_user" + mock_env_helper.POSTGRESQL_HOST = "mock_host" + mock_env_helper.POSTGRESQL_DATABASE = "mock_database" + mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + + # Mock the connection and cursor + mock_connection = MagicMock() + mock_cursor = MagicMock() + mock_connection.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_connection + + # Simulate an error during the execution of the query + mock_cursor.execute.side_effect = Exception("Database error") + + # Create an instance of the helper + helper = AzurePostgresHelper() + blob_url = "mock_blob_url" + + # Act & Assert: Ensure that the exception is raised and the error is logged + with self.assertRaises(Exception): + helper.search_by_blob_url(blob_url) + + mock_logger.error.assert_called_with( + "Error executing search query: Database error" + ) + mock_connection.close.assert_called_once() diff --git a/code/tests/utilities/helpers/test_postgress_embedder.py b/code/tests/utilities/helpers/test_postgress_embedder.py new file mode 100644 index 000000000..1f76bae3d --- /dev/null +++ b/code/tests/utilities/helpers/test_postgress_embedder.py @@ -0,0 +1,211 @@ +from unittest.mock import MagicMock, patch, call + +import pytest +from backend.batch.utilities.helpers.embedders.postgres_embedder import PostgresEmbedder +from backend.batch.utilities.common.source_document import SourceDocument +from backend.batch.utilities.helpers.config.embedding_config import EmbeddingConfig +from backend.batch.utilities.document_loading.strategies import LoadingStrategy +from backend.batch.utilities.document_loading import LoadingSettings +from backend.batch.utilities.document_chunking.chunking_strategy import ChunkingSettings + +CHUNKING_SETTINGS = ChunkingSettings({"strategy": "layout", "size": 1, "overlap": 0}) +LOADING_SETTINGS = LoadingSettings({"strategy": LoadingStrategy.LAYOUT}) + + +@pytest.fixture(autouse=True) +def llm_helper_mock(): + with patch( + "backend.batch.utilities.helpers.embedders.postgres_embedder.LLMHelper" + ) as mock: + llm_helper = mock.return_value + llm_helper.get_embedding_model.return_value.embed_query.return_value = [ + 0 + ] * 1536 + mock_completion = llm_helper.get_chat_completion.return_value + choice = MagicMock() + choice.message.content = "This is a caption for an image" + mock_completion.choices = [choice] + llm_helper.generate_embeddings.return_value = [123] + yield llm_helper + + +@pytest.fixture(autouse=True) +def env_helper_mock(): + with patch( + "backend.batch.utilities.helpers.embedders.push_embedder.EnvHelper" + ) as mock: + env_helper = mock.return_value + yield env_helper + + +@pytest.fixture(autouse=True) +def azure_postgres_helper_mock(): + with patch( + "backend.batch.utilities.helpers.embedders.postgres_embedder.AzurePostgresHelper" + ) as mock: + yield mock + + +@pytest.fixture(autouse=True) +def mock_config_helper(): + with patch( + "backend.batch.utilities.helpers.embedders.postgres_embedder.ConfigHelper" + ) as mock: + config_helper = mock.get_active_config_or_default.return_value + config_helper.document_processors = [ + EmbeddingConfig( + "jpg", + CHUNKING_SETTINGS, + LOADING_SETTINGS, + use_advanced_image_processing=True, + ), + EmbeddingConfig( + "pdf", + CHUNKING_SETTINGS, + LOADING_SETTINGS, + use_advanced_image_processing=False, + ), + ] + config_helper.get_advanced_image_processing_image_types.return_value = { + "jpeg", + "jpg", + "png", + } + yield config_helper + + +@pytest.fixture(autouse=True) +def document_loading_mock(): + with patch( + "backend.batch.utilities.helpers.embedders.postgres_embedder.DocumentLoading" + ) as mock: + expected_documents = [ + SourceDocument(content="some content", source="some source") + ] + mock.return_value.load.return_value = expected_documents + yield mock + + +@pytest.fixture(autouse=True) +def document_chunking_mock(): + with patch( + "backend.batch.utilities.helpers.embedders.postgres_embedder.DocumentChunking" + ) as mock: + expected_chunked_documents = [ + SourceDocument( + content="some content", + source="some source", + id="some id", + title="some-title", + offset=1, + chunk=1, + page_number=1, + chunk_id="some chunk id", + ), + SourceDocument( + content="some other content", + source="some other source", + id="some other id", + title="some other-title", + offset=2, + chunk=2, + page_number=2, + chunk_id="some other chunk id", + ), + ] + mock.return_value.chunk.return_value = expected_chunked_documents + yield mock + + +def test_embed_file( + document_chunking_mock, + document_loading_mock, + llm_helper_mock, + azure_postgres_helper_mock, +): + postgres_embedder = PostgresEmbedder(MagicMock(), MagicMock()) + # Setup test data + source_url = "https://example.com/document.pdf" + file_name = "document.pdf" + file_extension = "pdf" + embedding_config = MagicMock() + postgres_embedder.embedding_configs[file_extension] = ( + embedding_config # This needs to be adapted if `self.embedder` isn't set. + ) + + # Mock methods + llm_helper_mock.generate_embeddings.return_value = [0.1, 0.2, 0.3] + azure_postgres_helper_mock.create_search_indexes.return_value = True + + # Execute + postgres_embedder.embed_file(source_url, file_name) + + # Assert method calls + document_loading_mock.return_value.load.assert_called_once_with( + source_url, embedding_config.loading + ) + document_chunking_mock.return_value.chunk.assert_called_once_with( + document_loading_mock.return_value.load.return_value, embedding_config.chunking + ) + llm_helper_mock.generate_embeddings.assert_has_calls( + [call("some content"), call("some other content")] + ) + + +def test_advanced_image_processing_not_implemented(): + postgres_embedder = PostgresEmbedder(MagicMock(), MagicMock()) + # Test for unsupported advanced image processing + file_extension = "jpg" + embedding_config = MagicMock() + embedding_config.use_advanced_image_processing = True + postgres_embedder.embedding_configs[file_extension] = embedding_config + + # Mock config method + postgres_embedder.config.get_advanced_image_processing_image_types = MagicMock( + return_value=["jpg", "png"] + ) + + # Use pytest.raises to check the exception + with pytest.raises(NotImplementedError) as context: + postgres_embedder.embed_file("https://example.com/image.jpg", "image.jpg") + + # Assert that the exception message matches the expected one + assert ( + str(context.value) + == "Advanced image processing is not supported in PostgresEmbedder." + ) + + +def test_postgres_embed_file_loads_documents(document_loading_mock, env_helper_mock): + # given + push_embedder = PostgresEmbedder(MagicMock(), env_helper_mock) + source_url = "some-url" + + # when + push_embedder.embed_file( + source_url, + "some-file-name.pdf", + ) + + # then + document_loading_mock.return_value.load.assert_called_once_with( + source_url, LOADING_SETTINGS + ) + + +def test_postgres_embed_file_chunks_documents( + document_loading_mock, document_chunking_mock, env_helper_mock +): + # given + push_embedder = PostgresEmbedder(MagicMock(), env_helper_mock) + + # when + push_embedder.embed_file( + "some-url", + "some-file-name.pdf", + ) + + # then + document_chunking_mock.return_value.chunk.assert_called_once_with( + document_loading_mock.return_value.load.return_value, CHUNKING_SETTINGS + ) From 270386c0b7dbea2aaa748f4aed617fc03dc15fff Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 2 Dec 2024 21:41:25 +0530 Subject: [PATCH 085/107] Mock default credentials --- .../helpers/test_azure_postgres_helper.py | 174 ++++++++++++++++-- 1 file changed, 158 insertions(+), 16 deletions(-) diff --git a/code/tests/utilities/helpers/test_azure_postgres_helper.py b/code/tests/utilities/helpers/test_azure_postgres_helper.py index 907d0bdfd..404e6f6df 100644 --- a/code/tests/utilities/helpers/test_azure_postgres_helper.py +++ b/code/tests/utilities/helpers/test_azure_postgres_helper.py @@ -134,8 +134,11 @@ def raise_exception(*args, **kwargs): self.assertEqual(str(context.exception), "Query execution error") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") - def test_create_search_client_connection_error(self, mock_connect): + def test_create_search_client_connection_error(self, mock_connect, mock_credential): # Arrange # Mock the EnvHelper and set required attributes mock_env_helper = MagicMock() @@ -144,6 +147,11 @@ def test_create_search_client_connection_error(self, mock_connect): mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + def raise_exception(*args, **kwargs): raise Exception("Connection error") @@ -158,15 +166,23 @@ def raise_exception(*args, **kwargs): self.assertEqual(str(context.exception), "Connection error") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_get_files_success(self, mock_env_helper, mock_connect): + def test_get_files_success(self, mock_env_helper, mock_connect, mock_credential): # Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Arrange: Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -191,15 +207,23 @@ def test_get_files_success(self, mock_env_helper, mock_connect): ) mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_get_files_no_results(self, mock_env_helper, mock_connect): + def test_get_files_no_results(self, mock_env_helper, mock_connect, mock_credential): # Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Arrange: Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -219,16 +243,26 @@ def test_get_files_no_results(self, mock_env_helper, mock_connect): self.assertIsNone(result) mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") - def test_get_files_db_error(self, mock_logger, mock_env_helper, mock_connect): + def test_get_files_db_error( + self, mock_logger, mock_env_helper, mock_connect, mock_credential + ): # Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Arrange: Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -250,11 +284,14 @@ def test_get_files_db_error(self, mock_logger, mock_env_helper, mock_connect): ) mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") def test_get_files_unexpected_error( - self, mock_logger, mock_env_helper, mock_connect + self, mock_logger, mock_env_helper, mock_connect, mock_credential ): # Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" @@ -262,6 +299,11 @@ def test_get_files_unexpected_error( mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Arrange: Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -283,16 +325,26 @@ def test_get_files_unexpected_error( ) mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_delete_documents_success(self, mock_env_helper, mock_logger, mock_connect): + def test_delete_documents_success( + self, mock_env_helper, mock_logger, mock_connect, mock_credential + ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -317,16 +369,26 @@ def test_delete_documents_success(self, mock_env_helper, mock_logger, mock_conne mock_connection.close.assert_called_once() mock_logger.info.assert_called_with("Deleted 3 documents.") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_delete_documents_no_ids(self, mock_env_helper, mock_logger, mock_connect): + def test_delete_documents_no_ids( + self, mock_env_helper, mock_logger, mock_connect, mock_credential + ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -347,11 +409,14 @@ def test_delete_documents_no_ids(self, mock_env_helper, mock_logger, mock_connec mock_logger.warning.assert_called_with("No IDs provided for deletion.") mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") def test_delete_documents_db_error( - self, mock_env_helper, mock_logger, mock_connect + self, mock_env_helper, mock_logger, mock_connect, mock_credential ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" @@ -359,6 +424,11 @@ def test_delete_documents_db_error( mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -383,11 +453,14 @@ def test_delete_documents_db_error( mock_connection.rollback.assert_called_once() mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") def test_delete_documents_unexpected_error( - self, mock_env_helper, mock_logger, mock_connect + self, mock_env_helper, mock_logger, mock_connect, mock_credential ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" @@ -395,6 +468,11 @@ def test_delete_documents_unexpected_error( mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -419,16 +497,26 @@ def test_delete_documents_unexpected_error( mock_connection.rollback.assert_called_once() mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_perform_search_success(self, mock_env_helper, mock_logger, mock_connect): + def test_perform_search_success( + self, mock_env_helper, mock_logger, mock_connect, mock_credential + ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -462,11 +550,14 @@ def test_perform_search_success(self, mock_env_helper, mock_logger, mock_connect mock_connection.close.assert_called_once() mock_logger.info.assert_called_with("Retrieved 1 search result(s).") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") def test_perform_search_no_results( - self, mock_env_helper, mock_logger, mock_connect + self, mock_env_helper, mock_logger, mock_connect, mock_credential ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" @@ -474,6 +565,11 @@ def test_perform_search_no_results( mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -498,16 +594,26 @@ def test_perform_search_no_results( mock_connection.close.assert_called_once() mock_logger.info.assert_called_with("Retrieved 0 search result(s).") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_perform_search_error(self, mock_env_helper, mock_logger, mock_connect): + def test_perform_search_error( + self, mock_env_helper, mock_logger, mock_connect, mock_credential + ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -531,16 +637,26 @@ def test_perform_search_error(self, mock_env_helper, mock_logger, mock_connect): ) mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_get_unique_files_success(self, mock_env_helper, mock_logger, mock_connect): + def test_get_unique_files_success( + self, mock_env_helper, mock_logger, mock_connect, mock_credential + ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -568,11 +684,14 @@ def test_get_unique_files_success(self, mock_env_helper, mock_logger, mock_conne mock_connection.close.assert_called_once() mock_logger.info.assert_called_with("Retrieved 2 unique title(s).") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") def test_get_unique_files_no_results( - self, mock_env_helper, mock_logger, mock_connect + self, mock_env_helper, mock_logger, mock_connect, mock_credential ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" @@ -580,6 +699,11 @@ def test_get_unique_files_no_results( mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -602,16 +726,26 @@ def test_get_unique_files_no_results( mock_connection.close.assert_called_once() mock_logger.info.assert_called_with("Retrieved 0 unique title(s).") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_get_unique_files_error(self, mock_env_helper, mock_logger, mock_connect): + def test_get_unique_files_error( + self, mock_env_helper, mock_logger, mock_connect, mock_credential + ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -633,11 +767,14 @@ def test_get_unique_files_error(self, mock_env_helper, mock_logger, mock_connect ) mock_connection.close.assert_called_once() + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") def test_search_by_blob_url_success( - self, mock_env_helper, mock_logger, mock_connect + self, mock_env_helper, mock_logger, mock_connect, mock_credential ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" @@ -645,6 +782,11 @@ def test_search_by_blob_url_success( mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() From 6198de76bd7f008cb2a781c259f91ec76a0590dd Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 2 Dec 2024 22:10:15 +0530 Subject: [PATCH 086/107] mock default credentials --- .../helpers/test_azure_postgres_helper.py | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/code/tests/utilities/helpers/test_azure_postgres_helper.py b/code/tests/utilities/helpers/test_azure_postgres_helper.py index 404e6f6df..e2cb9c438 100644 --- a/code/tests/utilities/helpers/test_azure_postgres_helper.py +++ b/code/tests/utilities/helpers/test_azure_postgres_helper.py @@ -101,8 +101,11 @@ def test_get_search_indexes_success( "host=mock_host user=mock_user dbname=mock_database password=mock-access-token" ) + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") - def test_get_search_indexes_query_error(self, mock_connect): + def test_get_search_indexes_query_error(self, mock_connect, mock_credential): # Arrange # Mock the EnvHelper and set required attributes mock_env_helper = MagicMock() @@ -111,6 +114,11 @@ def test_get_search_indexes_query_error(self, mock_connect): mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + mock_connection = MagicMock() mock_connect.return_value = mock_connection @@ -815,11 +823,14 @@ def test_search_by_blob_url_success( mock_connection.close.assert_called_once() mock_logger.info.assert_called_with("Retrieved 2 unique title(s).") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") def test_search_by_blob_url_no_results( - self, mock_env_helper, mock_logger, mock_connect + self, mock_env_helper, mock_logger, mock_connect, mock_credential ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" @@ -827,6 +838,11 @@ def test_search_by_blob_url_no_results( mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() @@ -850,16 +866,26 @@ def test_search_by_blob_url_no_results( mock_connection.close.assert_called_once() mock_logger.info.assert_called_with("Retrieved 0 unique title(s).") + @patch( + "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" + ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.logger") @patch("backend.batch.utilities.helpers.azure_postgres_helper.EnvHelper") - def test_search_by_blob_url_error(self, mock_env_helper, mock_logger, mock_connect): + def test_search_by_blob_url_error( + self, mock_env_helper, mock_logger, mock_connect, mock_credential + ): # Arrange: Mock the EnvHelper attributes mock_env_helper.POSTGRESQL_USER = "mock_user" mock_env_helper.POSTGRESQL_HOST = "mock_host" mock_env_helper.POSTGRESQL_DATABASE = "mock_database" mock_env_helper.AZURE_POSTGRES_SEARCH_TOP_K = 5 + # Mock access token retrieval + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + # Mock the connection and cursor mock_connection = MagicMock() mock_cursor = MagicMock() From 1dcde971ad9128a781fb5d6725a09d5dee990bbe Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Tue, 3 Dec 2024 17:43:48 +0530 Subject: [PATCH 087/107] Changed from PG_DISAKNN to Vector --- infra/main.bicep | 5 ++++- infra/main.json | 11 +++++++++-- scripts/data_scripts/create_postgres_tables.py | 10 ++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index d4d8abea0..eedbbf5b8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,5 +1,8 @@ targetScope = 'subscription' +@description('Specify the name of the resource group to use. You can provide an existing resource group name, a new resource group name, or leave it blank to create a default resource group.') +param resourceGroupName string = '' + @minLength(1) @maxLength(20) @description('Name of the the environment which is used to generate a short unique hash used in all resources.') @@ -319,7 +322,7 @@ var queueName = 'doc-processing' var clientKey = '${uniqueString(guid(subscription().id, deployment().name))}${newGuidString}' var eventGridSystemTopicName = 'doc-processing' var tags = { 'azd-env-name': environmentName } -var rgName = 'rg-${environmentName}' +var rgName = resourceGroupName == '' ? 'rg-${environmentName}' : resourceGroupName var keyVaultName = 'kv-${resourceToken}' var baseUrl = 'https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/' var azureOpenAIModelInfo = string({ diff --git a/infra/main.json b/infra/main.json index c85ab819a..409859048 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,10 +5,17 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "6080738341036036588" + "templateHash": "362943193825732749" } }, "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Specify the name of the resource group to use. You can provide an existing resource group name, a new resource group name, or leave it blank to create a default resource group." + } + }, "environmentName": { "type": "string", "minLength": 1, @@ -646,7 +653,7 @@ "tags": { "azd-env-name": "[parameters('environmentName')]" }, - "rgName": "[format('rg-{0}', parameters('environmentName'))]", + "rgName": "[if(equals(parameters('resourceGroupName'), ''), format('rg-{0}', parameters('environmentName')), parameters('resourceGroupName'))]", "keyVaultName": "[format('kv-{0}', parameters('resourceToken'))]", "baseUrl": "https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/", "azureOpenAIModelInfo": "[string(createObject('model', parameters('azureOpenAIModel'), 'modelName', parameters('azureOpenAIModelName'), 'modelVersion', parameters('azureOpenAIModelVersion')))]", diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index d2ed48504..a406c43a5 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -107,7 +107,10 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): conn.commit() # Add pg_diskann extension and search_indexes table -cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") +# cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") + +# Add Vector extension +cursor.execute("CREATE EXTENSION IF NOT EXISTS vector CASCADE;") conn.commit() cursor.execute("DROP TABLE IF EXISTS search_indexes;") @@ -129,7 +132,10 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): cursor.execute(table_create_command) conn.commit() -cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") +# PG_DISKANN is not available yet +# cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") + +cursor.execute("CREATE INDEX search_indexes_content_vector_idx ON search_indexes USING vector (content_vector vector_cosine_ops);") conn.commit() grant_permissions(cursor, dbname, "public", principal_name) From 11d78d09b99c0a4818aa491fbade986e45f1e68a Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 3 Dec 2024 19:56:14 +0530 Subject: [PATCH 088/107] Set PostgreSQL as the Default Database for CWYD Deployment & unittestcases --- .../batch/utilities/helpers/env_helper.py | 4 +- .../test_postgres_search_handler.py | 49 ++++++++ code/tests/test_chat_history.py | 119 +++++++++++++++++- .../helpers/test_database_factory.py | 89 +++++++++++++ infra/main.bicep | 14 ++- infra/main.json | 4 +- 6 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 code/tests/utilities/helpers/test_database_factory.py diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index e1d40a162..db2f74ebb 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -91,7 +91,9 @@ def __load_config(self, **kwargs) -> None: # Chat History DB Integration Settings # Set default values based on DATABASE_TYPE - self.DATABASE_TYPE = os.getenv("DATABASE_TYPE", "").strip() or "CosmosDB" + self.DATABASE_TYPE = ( + os.getenv("DATABASE_TYPE", "").strip() or DatabaseType.POSTGRESQL.value + ) # Cosmos DB configuration if self.DATABASE_TYPE == DatabaseType.COSMOSDB.value: azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "") diff --git a/code/tests/search_utilities/test_postgres_search_handler.py b/code/tests/search_utilities/test_postgres_search_handler.py index eead10dd3..1c8117791 100644 --- a/code/tests/search_utilities/test_postgres_search_handler.py +++ b/code/tests/search_utilities/test_postgres_search_handler.py @@ -1,3 +1,4 @@ +import json import pytest from unittest.mock import MagicMock, patch from backend.batch.utilities.common.source_document import SourceDocument @@ -124,6 +125,54 @@ def test_get_files(handler): assert result[1] == "test2.txt" +def test_output_results(handler): + results = [ + {"id": "1", "title": "file1.txt"}, + {"id": "2", "title": "file2.txt"}, + {"id": "3", "title": "file1.txt"}, + {"id": "4", "title": "file3.txt"}, + {"id": "5", "title": "file2.txt"}, + ] + + expected_output = { + "file1.txt": ["1", "3"], + "file2.txt": ["2", "5"], + "file3.txt": ["4"], + } + + result = handler.output_results(results) + + assert result == expected_output + assert len(result) == 3 + assert "file1.txt" in result + assert result["file2.txt"] == ["2", "5"] + + +def test_process_results(handler): + results = [ + {"metadata": json.dumps({"chunk": "Chunk1"}), "content": "Content1"}, + {"metadata": json.dumps({"chunk": "Chunk2"}), "content": "Content2"}, + ] + expected_output = [["Chunk1", "Content1"], ["Chunk2", "Content2"]] + result = handler.process_results(results) + assert result == expected_output + + +def test_process_results_none(handler): + result = handler.process_results(None) + assert result == [] + + +def test_process_results_missing_chunk(handler): + results = [ + {"metadata": json.dumps({}), "content": "Content1"}, + {"metadata": json.dumps({"chunk": "Chunk2"}), "content": "Content2"}, + ] + expected_output = [[0, "Content1"], ["Chunk2", "Content2"]] + result = handler.process_results(results) + assert result == expected_output + + def test_delete_files(handler): files_to_delete = {"test1.txt": [1, 2], "test2.txt": [3]} mock_delete_documents = MagicMock() diff --git a/code/tests/test_chat_history.py b/code/tests/test_chat_history.py index f1b8bdcb1..6ef805d50 100644 --- a/code/tests/test_chat_history.py +++ b/code/tests/test_chat_history.py @@ -2,7 +2,7 @@ This module tests the entry point for the application. """ -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from create_app import create_app @@ -555,6 +555,54 @@ def test_update_conversation_success( "success": True, } + @patch("backend.api.chat_history.AsyncAzureOpenAI") + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_update_conversation_new_success( + self, + get_active_config_or_default_mock, + azure_openai_mock: MagicMock, + mock_conversation_client, + client, + ): + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversation.return_value = [] + mock_conversation_client.create_message.return_value = "success" + mock_conversation_client.create_conversation.return_value = { + "title": "Test Title", + "updatedAt": "2024-12-01", + "id": "conv1", + } + request_json = { + "conversation_id": "conv1", + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi!"}, + ], + } + + openai_client_mock = azure_openai_mock.return_value + + mock_response = MagicMock() + mock_response.choices = [MagicMock(message=MagicMock(content="Test Title"))] + + openai_client_mock.chat.completions.create = AsyncMock( + return_value=mock_response + ) + + response = client.post("/api/history/update", json=request_json) + + assert response.status_code == 200 + assert response.json == { + "data": { + "conversation_id": "conv1", + "date": "2024-12-01", + "title": "Test Title", + }, + "success": True, + } + @patch( "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" ) @@ -568,6 +616,75 @@ def test_update_conversation_no_chat_history( assert response.status_code == 400 assert response.json == {"error": "Chat history is not available"} + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_update_conversation_connect_error( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.get_conversation.return_value = { + "title": "Test Title", + "updatedAt": "2024-12-01", + "id": "conv1", + } + request_json = { + "conversation_id": "conv1", + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi!"}, + ], + } + mock_conversation_client.connect.side_effect = Exception("Unexpected error") + + # Make the API call + response = client.post( + "/api/history/update", + json=request_json, + headers={"Content-Type": "application/json"}, + ) + + # Assert response + assert response.status_code == 500 + assert response.json == { + "error": "Error while updating the conversation history" + } + + @patch( + "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" + ) + def test_update_conversation_error( + self, get_active_config_or_default_mock, mock_conversation_client, client + ): + get_active_config_or_default_mock.return_value.enable_chat_history = True + mock_conversation_client.create_message.side_effect = Exception( + "Unexpected error" + ) + mock_conversation_client.get_conversation.return_value = { + "title": "Test Title", + "updatedAt": "2024-12-01", + "id": "conv1", + } + request_json = { + "conversation_id": "conv1", + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi!"}, + ], + } + + response = client.post( + "/api/history/update", + json=request_json, + headers={"Content-Type": "application/json"}, + ) + + # Assert response + assert response.status_code == 500 + assert response.json == { + "error": "Error while updating the conversation history" + } + @patch( "backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default" ) diff --git a/code/tests/utilities/helpers/test_database_factory.py b/code/tests/utilities/helpers/test_database_factory.py new file mode 100644 index 000000000..0a1734171 --- /dev/null +++ b/code/tests/utilities/helpers/test_database_factory.py @@ -0,0 +1,89 @@ +import pytest +from unittest.mock import patch, MagicMock +from backend.batch.utilities.helpers.config.database_type import DatabaseType +from backend.batch.utilities.chat_history.cosmosdb import CosmosConversationClient +from backend.batch.utilities.chat_history.database_factory import DatabaseFactory +from backend.batch.utilities.chat_history.postgresdbservice import ( + PostgresConversationClient, +) + + +@patch("backend.batch.utilities.chat_history.database_factory.DefaultAzureCredential") +@patch("backend.batch.utilities.chat_history.database_factory.EnvHelper") +@patch( + "backend.batch.utilities.chat_history.database_factory.CosmosConversationClient", + autospec=True, +) +def test_get_conversation_client_cosmos( + mock_cosmos_client, mock_env_helper, mock_credential +): + # Configure the EnvHelper mock + mock_env_instance = mock_env_helper.return_value + mock_env_instance.DATABASE_TYPE = DatabaseType.COSMOSDB.value + mock_env_instance.AZURE_COSMOSDB_ACCOUNT = "cosmos_account" + mock_env_instance.AZURE_COSMOSDB_DATABASE = "cosmos_database" + mock_env_instance.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = "conversations_container" + mock_env_instance.AZURE_COSMOSDB_ENABLE_FEEDBACK = False + mock_env_instance.AZURE_COSMOSDB_ACCOUNT_KEY = None + + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + mock_credential_instance = mock_credential.return_value + + # Mock the CosmosConversationClient instance + mock_cosmos_instance = MagicMock(spec=CosmosConversationClient) + mock_cosmos_client.return_value = mock_cosmos_instance + + # Call the method + client = DatabaseFactory.get_conversation_client() + + # Assert the CosmosConversationClient was called with correct arguments + mock_cosmos_client.assert_called_once_with( + cosmosdb_endpoint="https://cosmos_account.documents.azure.com:443/", + credential=mock_credential_instance, + database_name="cosmos_database", + container_name="conversations_container", + enable_message_feedback=False, + ) + assert isinstance(client, CosmosConversationClient) + assert client == mock_cosmos_instance + + +@patch("backend.batch.utilities.chat_history.database_factory.DefaultAzureCredential") +@patch("backend.batch.utilities.chat_history.database_factory.EnvHelper") +@patch( + "backend.batch.utilities.chat_history.database_factory.PostgresConversationClient", + autospec=True, +) +def test_get_conversation_client_postgres( + mock_postgres_client, mock_env_helper, mock_credential +): + mock_env_instance = mock_env_helper.return_value + mock_env_instance.DATABASE_TYPE = DatabaseType.POSTGRESQL.value + mock_env_instance.POSTGRESQL_USER = "postgres_user" + mock_env_instance.POSTGRESQL_HOST = "postgres_host" + mock_env_instance.POSTGRESQL_DATABASE = "postgres_database" + + mock_access_token = MagicMock() + mock_access_token.token = "mock-access-token" + mock_credential.return_value.get_token.return_value = mock_access_token + + mock_postgres_instance = MagicMock(spec=PostgresConversationClient) + mock_postgres_client.return_value = mock_postgres_instance + + client = DatabaseFactory.get_conversation_client() + + mock_postgres_client.assert_called_once_with( + user="postgres_user", host="postgres_host", database="postgres_database" + ) + assert isinstance(client, PostgresConversationClient) + + +@patch("backend.batch.utilities.chat_history.database_factory.EnvHelper") +def test_get_conversation_client_invalid_database_type(mock_env_helper): + mock_env_instance = mock_env_helper.return_value + mock_env_instance.DATABASE_TYPE = "INVALID_DB" + + with pytest.raises(ValueError, match="Unsupported DATABASE_TYPE"): + DatabaseFactory.get_conversation_client() diff --git a/infra/main.bicep b/infra/main.bicep index 2d061e9d9..3a4f426a7 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -306,7 +306,7 @@ param azureMachineLearningName string = 'aml-${resourceToken}' 'CosmosDB' 'PostgreSQL' ]) -param databaseType string = 'CosmosDB' +param databaseType string = 'PostgreSQL' @description('Azure Cosmos DB Account Name') param azureCosmosDBAccountName string = 'cosmos-${resourceToken}' @@ -1258,13 +1258,17 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (data keyVaultName: keyvault.outputs.name postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_NAME : web_docker.outputs.FRONTEND_API_NAME - adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_NAME : adminweb_docker.outputs.WEBSITE_ADMIN_NAME + adminAppPrincipalName: hostingModel == 'code' + ? adminweb.outputs.WEBSITE_ADMIN_NAME + : adminweb_docker.outputs.WEBSITE_ADMIN_NAME managedIdentityName: managedIdentityModule.outputs.managedIdentityOutput.name } scope: rg - dependsOn: hostingModel == 'code' ? [keyvault, postgresDBModule, storekeys, web, adminweb] : [ - [keyvault, postgresDBModule, storekeys, web_docker, adminweb_docker] - ] + dependsOn: hostingModel == 'code' + ? [keyvault, postgresDBModule, storekeys, web, adminweb] + : [ + [keyvault, postgresDBModule, storekeys, web_docker, adminweb_docker] + ] } output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString diff --git a/infra/main.json b/infra/main.json index 328f4b9a1..c7bd6a8e7 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "15176273744623029817" + "templateHash": "10654587243799689217" } }, "parameters": { @@ -614,7 +614,7 @@ }, "databaseType": { "type": "string", - "defaultValue": "CosmosDB", + "defaultValue": "PostgreSQL", "allowedValues": [ "CosmosDB", "PostgreSQL" From dbba5883f43befdeda997e2d4321dea7e1402c0c Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 3 Dec 2024 20:29:26 +0530 Subject: [PATCH 089/107] fix DATABASE_TYPE for testcases --- code/tests/functional/app_config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/tests/functional/app_config.py b/code/tests/functional/app_config.py index 0dd14b2f6..a072d7f92 100644 --- a/code/tests/functional/app_config.py +++ b/code/tests/functional/app_config.py @@ -13,7 +13,9 @@ class AppConfig: config: dict[str, str | None] = { "APPLICATIONINSIGHTS_ENABLED": "False", "AZURE_AUTH_TYPE": "keys", - "AZURE_BLOB_STORAGE_INFO": '{"accountName": "some-blob-account-name", "containerName": "some-blob-container-name", "accountKey": "' + encoded_account_key + '"}', + "AZURE_BLOB_STORAGE_INFO": '{"accountName": "some-blob-account-name", "containerName": "some-blob-container-name", "accountKey": "' + + encoded_account_key + + '"}', "AZURE_COMPUTER_VISION_KEY": "some-computer-vision-key", "AZURE_CONTENT_SAFETY_ENDPOINT": "some-content-safety-endpoint", "AZURE_CONTENT_SAFETY_KEY": "some-content-safety-key", @@ -80,6 +82,7 @@ class AppConfig: "OPENAI_API_TYPE": None, "OPENAI_API_KEY": None, "OPENAI_API_VERSION": None, + "DATABASE_TYPE": "CosmosDB", } def __init__(self, config_overrides: dict[str, str | None] = {}) -> None: From dfb2fe61c6126777b0c5391114e94da110b53661 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 3 Dec 2024 20:46:01 +0530 Subject: [PATCH 090/107] fix boolean value --- code/backend/batch/utilities/helpers/env_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/backend/batch/utilities/helpers/env_helper.py b/code/backend/batch/utilities/helpers/env_helper.py index db2f74ebb..fa82e2796 100644 --- a/code/backend/batch/utilities/helpers/env_helper.py +++ b/code/backend/batch/utilities/helpers/env_helper.py @@ -124,8 +124,8 @@ def __load_config(self, **kwargs) -> None: self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "") self.POSTGRESQL_HOST = azure_postgresql_info.get("host", "") # Ensure integrated vectorization is disabled for PostgreSQL - self.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION = "False" - self.USE_ADVANCED_IMAGE_PROCESSING = "False" + self.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION = False + self.USE_ADVANCED_IMAGE_PROCESSING = False else: raise ValueError( "Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'." From 0e11306f83a52135f654284060567ec48f16f787 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 3 Dec 2024 21:23:42 +0530 Subject: [PATCH 091/107] fix testcase --- code/tests/utilities/helpers/test_env_helper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/code/tests/utilities/helpers/test_env_helper.py b/code/tests/utilities/helpers/test_env_helper.py index 10e1de308..8acd1e497 100644 --- a/code/tests/utilities/helpers/test_env_helper.py +++ b/code/tests/utilities/helpers/test_env_helper.py @@ -133,6 +133,7 @@ def test_azure_speech_recognizer_languages_default(monkeypatch: MonkeyPatch): ) def test_use_advanced_image_processing(monkeypatch: MonkeyPatch, value, expected): # given + monkeypatch.setenv("DATABASE_TYPE", "CosmosDB") if value is not None: monkeypatch.setenv("USE_ADVANCED_IMAGE_PROCESSING", value) From 5c7b01bbe6e0d3405eb0b69de82245cb691c2dc7 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Tue, 3 Dec 2024 21:33:51 +0530 Subject: [PATCH 092/107] Reverted changes and Index option changed --- infra/main.bicep | 5 +---- scripts/data_scripts/create_postgres_tables.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index eedbbf5b8..d4d8abea0 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,8 +1,5 @@ targetScope = 'subscription' -@description('Specify the name of the resource group to use. You can provide an existing resource group name, a new resource group name, or leave it blank to create a default resource group.') -param resourceGroupName string = '' - @minLength(1) @maxLength(20) @description('Name of the the environment which is used to generate a short unique hash used in all resources.') @@ -322,7 +319,7 @@ var queueName = 'doc-processing' var clientKey = '${uniqueString(guid(subscription().id, deployment().name))}${newGuidString}' var eventGridSystemTopicName = 'doc-processing' var tags = { 'azd-env-name': environmentName } -var rgName = resourceGroupName == '' ? 'rg-${environmentName}' : resourceGroupName +var rgName = 'rg-${environmentName}' var keyVaultName = 'kv-${resourceToken}' var baseUrl = 'https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/' var azureOpenAIModelInfo = string({ diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index a406c43a5..a4afbc338 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -135,7 +135,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): # PG_DISKANN is not available yet # cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") -cursor.execute("CREATE INDEX search_indexes_content_vector_idx ON search_indexes USING vector (content_vector vector_cosine_ops);") +cursor.execute("CREATE INDEX search_indexes_content_vector_idx ON search_indexes USING hnsw (content_vector vector_cosine_ops);") conn.commit() grant_permissions(cursor, dbname, "public", principal_name) From 921b7e61eb85eb4bbf4bc6b3de45b4c398bdde40 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Tue, 3 Dec 2024 22:17:36 +0530 Subject: [PATCH 093/107] Added Main.json file --- infra/main.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/infra/main.json b/infra/main.json index 409859048..c85ab819a 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,17 +5,10 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "362943193825732749" + "templateHash": "6080738341036036588" } }, "parameters": { - "resourceGroupName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Specify the name of the resource group to use. You can provide an existing resource group name, a new resource group name, or leave it blank to create a default resource group." - } - }, "environmentName": { "type": "string", "minLength": 1, @@ -653,7 +646,7 @@ "tags": { "azd-env-name": "[parameters('environmentName')]" }, - "rgName": "[if(equals(parameters('resourceGroupName'), ''), format('rg-{0}', parameters('environmentName')), parameters('resourceGroupName'))]", + "rgName": "[format('rg-{0}', parameters('environmentName'))]", "keyVaultName": "[format('kv-{0}', parameters('resourceToken'))]", "baseUrl": "https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/", "azureOpenAIModelInfo": "[string(createObject('model', parameters('azureOpenAIModel'), 'modelName', parameters('azureOpenAIModelName'), 'modelVersion', parameters('azureOpenAIModelVersion')))]", From 9c23afa714048e63179c439f1186b8917a692ab4 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 4 Dec 2024 18:57:30 +0530 Subject: [PATCH 094/107] fix env issue --- infra/app/web.bicep | 8 +-- infra/core/database/postgresdb.bicep | 32 ++++++----- infra/main.bicep | 85 +++++++++++++++------------- infra/main.json | 50 +++++++++++----- 4 files changed, 101 insertions(+), 74 deletions(-) diff --git a/infra/app/web.bicep b/infra/app/web.bicep index fd89d972b..176c1aabc 100644 --- a/infra/app/web.bicep +++ b/infra/app/web.bicep @@ -70,7 +70,6 @@ var azureBlobStorageInfoUpdated = useKeyVault // Database-specific settings var databaseSettings = databaseType == 'CosmosDB' ? { - DATABASE_TYPE: 'CosmosDB' AZURE_COSMOSDB_ACCOUNT_KEY: (useKeyVault || cosmosDBKeyName == '') ? cosmosDBKeyName : listKeys( @@ -83,10 +82,7 @@ var databaseSettings = databaseType == 'CosmosDB' '2022-08-15' ).primaryMasterKey } - : { - DATABASE_TYPE: 'PostgreSQL' - AZURE_POSTGRESQL_INFO: useKeyVault ? postgresInfoName : '' - } + : {} module web '../core/host/appservice.bicep' = { name: '${name}-app-module' @@ -224,7 +220,7 @@ resource cosmosRoleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefi name: '${json(appSettings.AZURE_COSMOSDB_INFO).accountName}/00000000-0000-0000-0000-000000000002' } -module cosmosUserRole '../core/database/cosmos-sql-role-assign.bicep' = if(databaseType == 'CosmosDB') { +module cosmosUserRole '../core/database/cosmos-sql-role-assign.bicep' = if (databaseType == 'CosmosDB') { name: 'cosmos-sql-user-role-${web.name}' params: { accountName: json(appSettings.AZURE_COSMOSDB_INFO).accountName diff --git a/infra/core/database/postgresdb.bicep b/infra/core/database/postgresdb.bicep index b2f342c47..9b28795fe 100644 --- a/infra/core/database/postgresdb.bicep +++ b/infra/core/database/postgresdb.bicep @@ -59,20 +59,20 @@ resource serverName_resource 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12- } } -// resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { -// name: 'waitForServerReady' -// location: resourceGroup().location -// kind: 'AzurePowerShell' -// properties: { -// azPowerShellVersion: '3.0' -// scriptContent: 'start-sleep -Seconds 300' -// cleanupPreference: 'Always' -// retentionInterval: 'PT1H' -// } -// dependsOn: [ -// serverName_resource -// ] -// } +resource delayScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: 'waitForServerReady' + location: resourceGroup().location + kind: 'AzurePowerShell' + properties: { + azPowerShellVersion: '3.0' + scriptContent: 'start-sleep -Seconds 300' + cleanupPreference: 'Always' + retentionInterval: 'PT1H' + } + dependsOn: [ + serverName_resource + ] +} resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-12-01-preview' = { name: 'azure.extensions' @@ -81,6 +81,9 @@ resource configurations 'Microsoft.DBforPostgreSQL/flexibleServers/configuration value: 'vector' source: 'user-override' } + dependsOn: [ + delayScript + ] } resource azureADAdministrator 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2022-12-01' = { @@ -105,7 +108,6 @@ resource azureADAdministrator 'Microsoft.DBforPostgreSQL/flexibleServers/adminis // } // }] - resource firewall_all 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-12-01-preview' = if (allowAllIPsFirewall) { parent: serverName_resource name: 'allow-all-IPs' diff --git a/infra/main.bicep b/infra/main.bicep index efdfb2fdb..67d1dfd41 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -369,6 +369,7 @@ module postgresDBModule './core/database/postgresdb.bicep' = if (databaseType == solutionLocation: 'eastus2' managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId managedIdentityObjectName: managedIdentityModule.outputs.managedIdentityOutput.name + allowAzureIPsFirewall: true } scope: rg } @@ -676,9 +677,7 @@ module web './app/web.bicep' = if (hostingModel == 'code') { ORCHESTRATION_STRATEGY: orchestrationStrategy CONVERSATION_FLOW: conversationFlow LOGLEVEL: logLevel - - // Add database type to settings - AZURE_DATABASE_TYPE: databaseType + DATABASE_TYPE: databaseType }, // Conditionally add database-specific settings databaseType == 'CosmosDB' @@ -688,11 +687,11 @@ module web './app/web.bicep' = if (hostingModel == 'code') { } : databaseType == 'PostgreSQL' ? { - AZURE_POSTGRESDB_INFO: string({ + AZURE_POSTGRESQL_INFO: string({ host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName user: websiteName - }) + }) } : {} ) @@ -783,9 +782,7 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { ORCHESTRATION_STRATEGY: orchestrationStrategy CONVERSATION_FLOW: conversationFlow LOGLEVEL: logLevel - - // Add database type to settings - AZURE_DATABASE_TYPE: databaseType + DATABASE_TYPE: databaseType }, // Conditionally add database-specific settings databaseType == 'CosmosDB' @@ -795,7 +792,7 @@ module web_docker './app/web.bicep' = if (hostingModel == 'container') { } : databaseType == 'PostgreSQL' ? { - AZURE_POSTGRESDB_INFO: string({ + AZURE_POSTGRESQL_INFO: string({ host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName user: '${websiteName}-docker' @@ -877,13 +874,14 @@ module adminweb './app/adminweb.bicep' = if (hostingModel == 'code') { FUNCTION_KEY: clientKey ORCHESTRATION_STRATEGY: orchestrationStrategy LOGLEVEL: logLevel - AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' - ? string({ - host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName - dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName - user: adminWebsiteName - }) - : {} + DATABASE_TYPE: databaseType + AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: adminWebsiteName + }) + : {} } } } @@ -958,13 +956,14 @@ module adminweb_docker './app/adminweb.bicep' = if (hostingModel == 'container') FUNCTION_KEY: clientKey ORCHESTRATION_STRATEGY: orchestrationStrategy LOGLEVEL: logLevel - AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' - ? string({ - host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName - dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName - user: '${adminWebsiteName}-docker' - }) - : {} + DATABASE_TYPE: databaseType + AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: '${adminWebsiteName}-docker' + }) + : {} } } } @@ -1061,13 +1060,14 @@ module function './app/function.bicep' = if (hostingModel == 'code') { LOGLEVEL: logLevel AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage AZURE_SEARCH_TOP_K: azureSearchTopK - AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' - ? string({ - host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName - dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName - user: functionName - }) - : {} + DATABASE_TYPE: databaseType + AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: functionName + }) + : {} } } } @@ -1129,13 +1129,14 @@ module function_docker './app/function.bicep' = if (hostingModel == 'container') LOGLEVEL: logLevel AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage AZURE_SEARCH_TOP_K: azureSearchTopK - AZURE_POSTGRESDB_INFO: databaseType == 'PostgreSQL' - ? string({ - host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName - dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName - user: '${functionName}-docker' - }) - : {} + DATABASE_TYPE: databaseType + AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' + ? string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: '${functionName}-docker' + }) + : {} } } } @@ -1292,8 +1293,12 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (data keyVaultName: keyvault.outputs.name postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_NAME : web_docker.outputs.FRONTEND_API_NAME - adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_NAME : adminweb_docker.outputs.WEBSITE_ADMIN_NAME - functionAppPrincipalName: hostingModel == 'code' ? function.outputs.functionName : function_docker.outputs.functionName + adminAppPrincipalName: hostingModel == 'code' + ? adminweb.outputs.WEBSITE_ADMIN_NAME + : adminweb_docker.outputs.WEBSITE_ADMIN_NAME + functionAppPrincipalName: hostingModel == 'code' + ? function.outputs.functionName + : function_docker.outputs.functionName managedIdentityName: managedIdentityModule.outputs.managedIdentityOutput.name } scope: rg @@ -1370,4 +1375,4 @@ output AZURE_ML_WORKSPACE_NAME string = orchestrationStrategy == 'prompt_flow' : '' output RESOURCE_TOKEN string = resourceToken output AZURE_COSMOSDB_INFO string = azureCosmosDBInfo -output AZURE_POSTGRESDB_INFO string = azurePostgresDBInfo +output AZURE_POSTGRESQL_INFO string = azurePostgresDBInfo diff --git a/infra/main.json b/infra/main.json index 0936af81b..0d0c003e8 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "14369776645478790054" + "templateHash": "7986340030505423959" } }, "parameters": { @@ -968,6 +968,9 @@ }, "managedIdentityObjectName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.name]" + }, + "allowAzureIPsFirewall": { + "value": true } }, "template": { @@ -977,7 +980,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "10751990458550607112" + "templateHash": "16024751692725526332" } }, "parameters": { @@ -1083,6 +1086,22 @@ "availabilityZone": "[parameters('availabilityZone')]" } }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "waitForServerReady", + "location": "[resourceGroup().location]", + "kind": "AzurePowerShell", + "properties": { + "azPowerShellVersion": "3.0", + "scriptContent": "start-sleep -Seconds 300", + "cleanupPreference": "Always", + "retentionInterval": "PT1H" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" + ] + }, { "type": "Microsoft.DBforPostgreSQL/flexibleServers/configurations", "apiVersion": "2023-12-01-preview", @@ -1092,6 +1111,7 @@ "source": "user-override" }, "dependsOn": [ + "[resourceId('Microsoft.Resources/deploymentScripts', 'waitForServerReady')]", "[resourceId('Microsoft.DBforPostgreSQL/flexibleServers', parameters('serverName'))]" ] }, @@ -2637,7 +2657,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('websiteName')))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESQL_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('websiteName')))), createObject())))]" } }, "template": { @@ -2647,7 +2667,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "13375017292788108601" + "templateHash": "2467145046105891366" } }, "parameters": { @@ -2811,7 +2831,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'CosmosDB'), createObject('DATABASE_TYPE', 'CosmosDB', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'PostgreSQL', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject()), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -3637,7 +3657,7 @@ "value": "[parameters('authType')]" }, "appSettings": { - "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESDB_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('websiteName'))))), createObject())))]" + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_CONVERSATIONS_LOG_INDEX', parameters('azureSearchConversationLogIndex'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SPEECH_SERVICE_NAME', parameters('speechServiceName'), 'AZURE_SPEECH_SERVICE_REGION', parameters('location'), 'AZURE_SPEECH_RECOGNIZER_LANGUAGES', parameters('recognizedLanguages'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'ADVANCED_IMAGE_PROCESSING_MAX_IMAGES', parameters('advancedImageProcessingMaxImages'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'CONVERSATION_FLOW', parameters('conversationFlow'), 'LOGLEVEL', parameters('logLevel'), 'DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_INFO', string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, ''))), 'AZURE_COSMOSDB_ENABLE_FEEDBACK', true()), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESQL_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('websiteName'))))), createObject())))]" } }, "template": { @@ -3647,7 +3667,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "13375017292788108601" + "templateHash": "2467145046105891366" } }, "parameters": { @@ -3811,7 +3831,7 @@ "value": "[parameters('appServicePlanId')]" }, "appSettings": { - "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'CosmosDB'), createObject('DATABASE_TYPE', 'CosmosDB', 'AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject('DATABASE_TYPE', 'PostgreSQL', 'AZURE_POSTGRESQL_INFO', if(parameters('useKeyVault'), parameters('postgresInfoName'), ''))), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" + "value": "[union(parameters('appSettings'), union(if(equals(parameters('databaseType'), 'CosmosDB'), createObject('AZURE_COSMOSDB_ACCOUNT_KEY', if(or(parameters('useKeyVault'), equals(parameters('cosmosDBKeyName'), '')), parameters('cosmosDBKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBKeyName')), '2022-08-15').primaryMasterKey)), createObject()), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1))))]" }, "keyVaultName": { "value": "[parameters('keyVaultName')]" @@ -4677,7 +4697,8 @@ "FUNCTION_KEY": "[variables('clientKey')]", "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('adminWebsiteName'))), createObject())]" + "DATABASE_TYPE": "[parameters('databaseType')]", + "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('adminWebsiteName'))), createObject())]" } } }, @@ -5637,7 +5658,8 @@ "FUNCTION_KEY": "[variables('clientKey')]", "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('adminWebsiteName')))), createObject())]" + "DATABASE_TYPE": "[parameters('databaseType')]", + "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('adminWebsiteName')))), createObject())]" } } }, @@ -8330,7 +8352,8 @@ "LOGLEVEL": "[parameters('logLevel')]", "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('functionName'))), createObject())]" + "DATABASE_TYPE": "[parameters('databaseType')]", + "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('functionName'))), createObject())]" } } }, @@ -9661,7 +9684,8 @@ "LOGLEVEL": "[parameters('logLevel')]", "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_POSTGRESDB_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('functionName')))), createObject())]" + "DATABASE_TYPE": "[parameters('databaseType')]", + "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('functionName')))), createObject())]" } } }, @@ -12352,7 +12376,7 @@ "type": "string", "value": "[string(createObject('accountName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosAccountName, ''), 'databaseName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosDatabaseName, ''), 'containerName', if(equals(parameters('databaseType'), 'CosmosDB'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_cosmos_db'), '2022-09-01').outputs.cosmosOutput.value.cosmosContainerName, '')))]" }, - "AZURE_POSTGRESDB_INFO": { + "AZURE_POSTGRESQL_INFO": { "type": "string", "value": "[string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', ''))]" } From 7d78aabef41d22f9be02d0cd5a26debc7d08f9ea Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 5 Dec 2024 11:28:41 +0530 Subject: [PATCH 095/107] Updated the base url based on fork branch --- infra/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index 67d1dfd41..480d89cbc 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -321,7 +321,7 @@ var eventGridSystemTopicName = 'doc-processing' var tags = { 'azd-env-name': environmentName } var rgName = 'rg-${environmentName}' var keyVaultName = 'kv-${resourceToken}' -var baseUrl = 'https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/' +var baseUrl = 'https://raw.githubusercontent.com/Fr4nc3/chat-with-your-data-solution-accelerator/main/' var azureOpenAIModelInfo = string({ model: azureOpenAIModel modelName: azureOpenAIModelName From fc602e2a069c906c7adc51ae365fd94386159142 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 5 Dec 2024 11:36:04 +0530 Subject: [PATCH 096/107] Updated base URL --- infra/main.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/main.json b/infra/main.json index 0d0c003e8..0cc7ead7a 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "7986340030505423959" + "templateHash": "11367626989637032545" } }, "parameters": { @@ -648,7 +648,7 @@ }, "rgName": "[format('rg-{0}', parameters('environmentName'))]", "keyVaultName": "[format('kv-{0}', parameters('resourceToken'))]", - "baseUrl": "https://raw.githubusercontent.com/Azure-Samples/chat-with-your-data-solution-accelerator/main/", + "baseUrl": "https://raw.githubusercontent.com/Fr4nc3/chat-with-your-data-solution-accelerator/main/", "azureOpenAIModelInfo": "[string(createObject('model', parameters('azureOpenAIModel'), 'modelName', parameters('azureOpenAIModelName'), 'modelVersion', parameters('azureOpenAIModelVersion')))]", "azureOpenAIEmbeddingModelInfo": "[string(createObject('model', parameters('azureOpenAIEmbeddingModel'), 'modelName', parameters('azureOpenAIEmbeddingModelName'), 'modelVersion', parameters('azureOpenAIEmbeddingModelVersion')))]", "appversion": "latest", From 23a821151bc2c7e39dc10b3eef85eb1a878ed41b Mon Sep 17 00:00:00 2001 From: Prajwal-Microsoft Date: Thu, 5 Dec 2024 11:49:46 +0530 Subject: [PATCH 097/107] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5df4d8c7b..8c9d4cc8d 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ There are two choices; the "Deploy to Azure" offers a one click deployment where The demo, which uses containers pre-built from the main branch is available by clicking this button: -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FFr4nc3%2Fchat-with-your-data-solution-accelerator%2Frefs%2Fheads%2FpostgresBicepChanges%2Finfra%2Fmain.json) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FFr4nc3%2Fchat-with-your-data-solution-accelerator%2Frefs%2Fheads%2Fmain%2Finfra%2Fmain.json) When Deployment is complete, follow steps in [Set Up Authentication in Azure App Service](./docs/azure_app_service_auth_setup.md) to add app authentication to your web app running on Azure App Service From 24da885ec28acdda45c4189b3eb5cf64d267a26e Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 5 Dec 2024 18:50:42 +0530 Subject: [PATCH 098/107] Updated script to change the table owner --- scripts/data_scripts/create_postgres_tables.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index a4afbc338..1149137c2 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -147,5 +147,10 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): grant_permissions(cursor, dbname, "public", function_app_principal_name) conn.commit() +cursor.execute("ALTER TABLE public.conversations OWNER TO azure_pg_admin;") +cursor.execute("ALTER TABLE public.messages OWNER TO azure_pg_admin;") +cursor.execute("ALTER TABLE public.search_indexes OWNER TO azure_pg_admin;") +conn.commit() + cursor.close() conn.close() From d741e7bac4511c0c98adb7f63e043283e624cc0b Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Thu, 5 Dec 2024 11:51:05 -0500 Subject: [PATCH 099/107] Create postgreSQL.md --- docs/postgreSQL.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/postgreSQL.md diff --git a/docs/postgreSQL.md b/docs/postgreSQL.md new file mode 100644 index 000000000..67bcfcb4e --- /dev/null +++ b/docs/postgreSQL.md @@ -0,0 +1,88 @@ +### PostgreSQL Integration in CWYD + +The CWYD has been enhanced with PostgreSQL as a core feature, enabling flexible, robust, and scalable database capabilities. This document outlines the features, configurations, and functionality introduced with PostgreSQL support. + +--- + +## Features and Enhancements + +### 1. **Default Database Configuration** +PostgreSQL is now the default database for CWYD deployments. If no database preference is specified (`DATABASE_TYPE` is unset or empty), the platform defaults to PostgreSQL. This ensures a streamlined deployment process while utilizing PostgreSQL’s advanced capabilities. + +--- + +### 2. **Unified Environment Configuration** +To simplify environment setup, PostgreSQL configurations are now grouped under a unified JSON environment variable: + +Example: +```json +{ + "type": "PostgreSQL", + "user": "DBUSER", + "database": "DBNAME", + "host": "DBHOST" +} +``` +This structure ensures easier management of environment variables and dynamic database selection during runtime. + +--- + +### 3. **PostgreSQL as the Search Index Provider** +The PostgreSQL `search_indexes` table is used for managing search-related indexing. It supports vector-based similarity searches, replacing Azure Search indexing in specific configurations. + +**Table Schema**: +```sql +CREATE TABLE IF NOT EXISTS search_indexes( + id TEXT, + title TEXT, + chunk INTEGER, + chunk_id TEXT, + offset INTEGER, + page_number INTEGER, + content TEXT, + source TEXT, + metadata TEXT, + content_vector VECTOR(1536) +); +``` + +**Similarity Query Example**: +```sql +SELECT content +FROM search_indexes +ORDER BY content_vector <=> $1 +LIMIT $2; +``` + + +--- + +### 4. **Automated Table Creation** +The PostgreSQL deployment process automatically creates the necessary tables for chat history and search indexes. The script `create_postgres_tables.py` is executed as part of the infrastructure deployment, ensuring the database is ready for use immediately after setup. + +--- + +### 8. **Secure PostgreSQL Connections** +All PostgreSQL connections use secure configurations: +- SSL is enabled with parameters such as `sslmode=verify-full`. +- Credentials are securely managed via environment variables and Key Vault integrations. + +--- + +### 9. **Backend Enhancements** +- PostgreSQL integration is limited to the Semantic Kernel orchestrator to ensure focused functionality. +- Database operations, including indexing and similarity searches, align with the CWYD workflow. + +--- + +## Benefits of PostgreSQL Integration +1. **Scalability**: PostgreSQL offers robust indexing capabilities suitable for large-scale deployments. +2. **Flexibility**: Dynamic database switching allows users to choose between PostgreSQL and CosmosDB based on their requirements. +3. **Ease of Use**: Automated table creation and environment configuration simplify deployment and management. +4. **Security**: SSL-enabled connections and secure credential handling ensure data protection. + + +--- + +## Conclusion +PostgreSQL integration transforms CWYD into a versatile, scalable platform capable of handling complex indexing and search scenarios. By leveraging PostgreSQL’s advanced features, CWYD ensures a seamless user experience, robust performance, and future-ready architecture. From 30c6acf093410a96d930a93ee4edb4bb2be3f40e Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 5 Dec 2024 22:41:46 +0530 Subject: [PATCH 100/107] Updated the container and version for 1 click deployment verification --- infra/main.bicep | 4 ++-- infra/main.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 73d79ffd0..9db5a74bb 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -333,8 +333,8 @@ var azureOpenAIEmbeddingModelInfo = string({ modelVersion: azureOpenAIEmbeddingModelVersion }) -var appversion = 'latest' // Update GIT deployment branch -var registryName = 'fruoccopublic' // Update Registry name +var appversion = 'devpostgre' // Update GIT deployment branch +var registryName = 'cwydcontainerregpk' // Update Registry name // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { diff --git a/infra/main.json b/infra/main.json index c6eeb7620..330188212 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "16182560991627619456" + "templateHash": "7275352967379033991" } }, "parameters": { @@ -651,8 +651,8 @@ "baseUrl": "https://raw.githubusercontent.com/Fr4nc3/chat-with-your-data-solution-accelerator/main/", "azureOpenAIModelInfo": "[string(createObject('model', parameters('azureOpenAIModel'), 'modelName', parameters('azureOpenAIModelName'), 'modelVersion', parameters('azureOpenAIModelVersion')))]", "azureOpenAIEmbeddingModelInfo": "[string(createObject('model', parameters('azureOpenAIEmbeddingModel'), 'modelName', parameters('azureOpenAIEmbeddingModelName'), 'modelVersion', parameters('azureOpenAIEmbeddingModelVersion')))]", - "appversion": "latest", - "registryName": "fruoccopublic", + "appversion": "devpostgre", + "registryName": "cwydcontainerregpk", "defaultOpenAiDeployments": [ { "name": "[parameters('azureOpenAIModel')]", From 404de0350a1dedf34c03b2a0d55850f97551733b Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Thu, 5 Dec 2024 23:54:36 +0530 Subject: [PATCH 101/107] feat: Updated the script to remove keyvault dependency --- scripts/data_scripts/create_postgres_tables.py | 12 ++---------- scripts/run_create_table_script.sh | 1 + 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index 1149137c2..ed6465efe 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -9,13 +9,8 @@ admin_principal_name = "adminAppPrincipalName" function_app_principal_name = "functionAppPrincipalName" user = "managedIdentityName" - -def get_secrets_from_kv(kv_name, secret_name): - credential = DefaultAzureCredential() - 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 +host = "serverName" +dbname = "postgres" def grant_permissions(cursor, dbname, schema_name, principal_name): @@ -57,9 +52,6 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): ) ) -postgres_details = json.loads(get_secrets_from_kv(key_vault_name, "AZURE-POSTGRESQL-INFO")) -host = postgres_details.get("host", "") -dbname = postgres_details.get("dbname", "") # Acquire the access token cred = DefaultAzureCredential() diff --git a/scripts/run_create_table_script.sh b/scripts/run_create_table_script.sh index 81210e4f5..8777ecbc5 100644 --- a/scripts/run_create_table_script.sh +++ b/scripts/run_create_table_script.sh @@ -35,6 +35,7 @@ sed -i "s/webAppPrincipalName/${webAppPrincipalName}/g" "create_postgres_tables. sed -i "s/adminAppPrincipalName/${adminAppPrincipalName}/g" "create_postgres_tables.py" sed -i "s/managedIdentityName/${managedIdentityName}/g" "create_postgres_tables.py" sed -i "s/functionAppPrincipalName/${functionAppPrincipalName}/g" "create_postgres_tables.py" +sed -i "s/serverName/${serverName}/g" "create_postgres_tables.py" pip install -r requirements.txt From 49314ba342c5721f6e5412bcc6356c7ae0d7d6c6 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 6 Dec 2024 00:01:50 +0530 Subject: [PATCH 102/107] fix: passing server deails to deployment script --- infra/main.bicep | 2 +- infra/main.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 9db5a74bb..6f7d65585 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1291,7 +1291,7 @@ module createIndex './core/database/deploy_create_table_script.bicep' = if (data identity: managedIdentityModule.outputs.managedIdentityOutput.id baseUrl: baseUrl keyVaultName: keyvault.outputs.name - postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgresSQLName + postgresSqlServerName: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName webAppPrincipalName: hostingModel == 'code' ? web.outputs.FRONTEND_API_NAME : web_docker.outputs.FRONTEND_API_NAME adminAppPrincipalName: hostingModel == 'code' ? adminweb.outputs.WEBSITE_ADMIN_NAME diff --git a/infra/main.json b/infra/main.json index 330188212..1d4a4652d 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "7275352967379033991" + "templateHash": "13919353598581755306" } }, "parameters": { @@ -12044,7 +12044,7 @@ "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'keyvault'), '2022-09-01').outputs.name.value]" }, "postgresSqlServerName": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgresSQLName]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName]" }, "webAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('websiteName')), '2022-09-01').outputs.FRONTEND_API_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('websiteName'))), '2022-09-01').outputs.FRONTEND_API_NAME.value))]", "adminAppPrincipalName": "[if(equals(parameters('hostingModel'), 'code'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('adminWebsiteName')), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', format('{0}-docker', parameters('adminWebsiteName'))), '2022-09-01').outputs.WEBSITE_ADMIN_NAME.value))]", From 396b68b0c0d4939476766d1be906fd5b52805530 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Fri, 6 Dec 2024 13:31:10 +0530 Subject: [PATCH 103/107] fix: For database type of Cosmos DB --- infra/app/adminweb.bicep | 2 +- infra/app/function.bicep | 2 +- infra/app/web.bicep | 2 +- infra/core/security/keyvault.bicep | 41 ++-- infra/main.bicep | 350 +++++++++++++++-------------- infra/main.json | 195 ++-------------- 6 files changed, 233 insertions(+), 359 deletions(-) diff --git a/infra/app/adminweb.bicep b/infra/app/adminweb.bicep index 3d494a39d..9347a8c33 100644 --- a/infra/app/adminweb.bicep +++ b/infra/app/adminweb.bicep @@ -69,7 +69,7 @@ module adminweb '../core/host/appservice.bicep' = { scmDoBuildDuringDeployment: useDocker ? false : true applicationInsightsName: applicationInsightsName appServicePlanId: appServicePlanId - managedIdentity: databaseType == 'PostgreSQL' + managedIdentity: databaseType == 'PostgreSQL' || !empty(keyVaultName) appSettings: union(appSettings, { AZURE_AUTH_TYPE: authType USE_KEY_VAULT: useKeyVault ? useKeyVault : '' diff --git a/infra/app/function.bicep b/infra/app/function.bicep index 6f265efca..2ec146344 100644 --- a/infra/app/function.bicep +++ b/infra/app/function.bicep @@ -67,7 +67,7 @@ module function '../core/host/functions.bicep' = { runtimeVersion: runtimeVersion dockerFullImageName: dockerFullImageName useKeyVault: useKeyVault - managedIdentity: databaseType == 'PostgreSQL' + managedIdentity: databaseType == 'PostgreSQL' || !empty(keyVaultName) appSettings: union(appSettings, { WEBSITES_ENABLE_APP_SERVICE_STORAGE: 'false' AZURE_AUTH_TYPE: authType diff --git a/infra/app/web.bicep b/infra/app/web.bicep index 176c1aabc..1efd7f6f4 100644 --- a/infra/app/web.bicep +++ b/infra/app/web.bicep @@ -164,7 +164,7 @@ module web '../core/host/appservice.bicep' = { dockerFullImageName: dockerFullImageName scmDoBuildDuringDeployment: useDocker ? false : true healthCheckPath: healthCheckPath - managedIdentity: databaseType == 'PostgreSQL' + managedIdentity: databaseType == 'PostgreSQL' || !empty(keyVaultName) } } diff --git a/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep index 491744904..120b3c074 100644 --- a/infra/core/security/keyvault.bicep +++ b/infra/core/security/keyvault.bicep @@ -2,7 +2,7 @@ metadata description = 'Creates an Azure Key Vault.' param name string param location string = resourceGroup().location param tags object = {} -param managedIdentityObjectId string +param managedIdentityObjectId string = '' param principalId string = '' @@ -13,25 +13,40 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { properties: { tenantId: subscription().tenantId sku: { family: 'A', name: 'standard' } - accessPolicies: !empty(principalId) - ? [ + accessPolicies: concat( + managedIdentityObjectId != '' ? [ { - objectId: principalId - permissions: { secrets: [ 'get', 'list' ] } - tenantId: subscription().tenantId - }, { objectId: managedIdentityObjectId - permissions: { secrets: [ 'get', 'list' ] } + permissions: { + keys: [ + 'get' + 'list' + ] + secrets: [ + 'get' + 'list' + ] + } tenantId: subscription().tenantId } - ] - : [ + ] : [], + principalId != '' ? [ { - objectId: managedIdentityObjectId - permissions: { secrets: [ 'get', 'list' ] } + objectId: principalId + permissions: { + keys: [ + 'get' + 'list' + ] + secrets: [ + 'get' + 'list' + ] + } tenantId: subscription().tenantId } - ] + ] : [] + ) } } diff --git a/infra/main.bicep b/infra/main.bicep index 6f7d65585..25edd43db 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -383,7 +383,7 @@ module keyvault './core/security/keyvault.bicep' = if (useKeyVault || authType = location: location tags: tags principalId: principalId - managedIdentityObjectId: managedIdentityModule.outputs.managedIdentityOutput.objectId + managedIdentityObjectId: databaseType == 'PostgreSQL' ? managedIdentityModule.outputs.managedIdentityOutput.objectId : '' } } @@ -587,8 +587,8 @@ var azureCosmosDBInfo = string({ }) var azurePostgresDBInfo = string({ - serverName: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName - databaseName: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + serverName: databaseType == 'PostgreSQL' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName : '' + databaseName: databaseType == 'PostgreSQL' ? postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName : '' userName: '' }) @@ -832,57 +832,59 @@ module adminweb './app/adminweb.bicep' = if (hostingModel == 'code') { keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType databaseType: databaseType - appSettings: { - AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion - AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_OPENAI_RESOURCE: azureOpenAIResourceName - AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo - AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature - AZURE_OPENAI_TOP_P: azureOpenAITopP - AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens - AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence - AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_OPENAI_STREAM: azureOpenAIStream - AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo - AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' - AZURE_SEARCH_INDEX: azureSearchIndex - AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch - AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig - AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked - AZURE_SEARCH_TOP_K: azureSearchTopK - AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain - AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn - AZURE_SEARCH_FILTER: azureSearchFilter - AZURE_SEARCH_FIELDS_ID: azureSearchFieldId - AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn - AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn - AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn - AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata - AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn - AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn - AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn - AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn - AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource - AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer - AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization - USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing - BACKEND_URL: 'https://${functionName}.azurewebsites.net' - DOCUMENT_PROCESSING_QUEUE_NAME: queueName - FUNCTION_KEY: clientKey - ORCHESTRATION_STRATEGY: orchestrationStrategy - LOGLEVEL: logLevel - DATABASE_TYPE: databaseType - AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' - ? string({ - host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName - dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName - user: adminWebsiteName - }) - : {} - } + appSettings: union( + { + AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion + AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo + AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature + AZURE_OPENAI_TOP_P: azureOpenAITopP + AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens + AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_OPENAI_STREAM: azureOpenAIStream + AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch + AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig + AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked + AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain + AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn + AZURE_SEARCH_FILTER: azureSearchFilter + AZURE_SEARCH_FIELDS_ID: azureSearchFieldId + AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn + AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata + AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn + AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn + AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn + AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn + AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource + AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer + AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization + USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing + BACKEND_URL: 'https://${functionName}.azurewebsites.net' + DOCUMENT_PROCESSING_QUEUE_NAME: queueName + FUNCTION_KEY: clientKey + ORCHESTRATION_STRATEGY: orchestrationStrategy + LOGLEVEL: logLevel + DATABASE_TYPE: databaseType + }, + databaseType == 'PostgreSQL' ? { + AZURE_POSTGRESQL_INFO: string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: adminWebsiteName + }) + } : {} + ) } } @@ -914,57 +916,59 @@ module adminweb_docker './app/adminweb.bicep' = if (hostingModel == 'container') keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType databaseType: databaseType - appSettings: { - AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion - AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_OPENAI_RESOURCE: azureOpenAIResourceName - AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo - AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature - AZURE_OPENAI_TOP_P: azureOpenAITopP - AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens - AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence - AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_OPENAI_STREAM: azureOpenAIStream - AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo - AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' - AZURE_SEARCH_INDEX: azureSearchIndex - AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch - AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig - AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked - AZURE_SEARCH_TOP_K: azureSearchTopK - AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain - AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn - AZURE_SEARCH_FILTER: azureSearchFilter - AZURE_SEARCH_FIELDS_ID: azureSearchFieldId - AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn - AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn - AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn - AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata - AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn - AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn - AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn - AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn - AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource - AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer - AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization - USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing - BACKEND_URL: 'https://${functionName}-docker.azurewebsites.net' - DOCUMENT_PROCESSING_QUEUE_NAME: queueName - FUNCTION_KEY: clientKey - ORCHESTRATION_STRATEGY: orchestrationStrategy - LOGLEVEL: logLevel - DATABASE_TYPE: databaseType - AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' - ? string({ - host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName - dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName - user: '${adminWebsiteName}-docker' - }) - : {} - } + appSettings: union( + { + AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion + AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo + AZURE_OPENAI_TEMPERATURE: azureOpenAITemperature + AZURE_OPENAI_TOP_P: azureOpenAITopP + AZURE_OPENAI_MAX_TOKENS: azureOpenAIMaxTokens + AZURE_OPENAI_STOP_SEQUENCE: azureOpenAIStopSequence + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_OPENAI_STREAM: azureOpenAIStream + AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_USE_SEMANTIC_SEARCH: azureSearchUseSemanticSearch + AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: azureSearchSemanticSearchConfig + AZURE_SEARCH_INDEX_IS_PRECHUNKED: azureSearchIndexIsPrechunked + AZURE_SEARCH_TOP_K: azureSearchTopK + AZURE_SEARCH_ENABLE_IN_DOMAIN: azureSearchEnableInDomain + AZURE_SEARCH_FILENAME_COLUMN: azureSearchFilenameColumn + AZURE_SEARCH_FILTER: azureSearchFilter + AZURE_SEARCH_FIELDS_ID: azureSearchFieldId + AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn + AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata + AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn + AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn + AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn + AZURE_SEARCH_URL_COLUMN: azureSearchUrlColumn + AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource + AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer + AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization + USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing + BACKEND_URL: 'https://${functionName}-docker.azurewebsites.net' + DOCUMENT_PROCESSING_QUEUE_NAME: queueName + FUNCTION_KEY: clientKey + ORCHESTRATION_STRATEGY: orchestrationStrategy + LOGLEVEL: logLevel + DATABASE_TYPE: databaseType + }, + databaseType == 'PostgreSQL' ? { + AZURE_POSTGRESQL_INFO: string({ + host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName + dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName + user: '${adminWebsiteName}-docker' + }) + } : {} + ) } } @@ -1032,43 +1036,45 @@ module function './app/function.bicep' = if (hostingModel == 'code') { keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType databaseType: databaseType - appSettings: { - AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion - AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo - AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo - AZURE_OPENAI_RESOURCE: azureOpenAIResourceName - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_SEARCH_INDEX: azureSearchIndex - AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' - AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource - AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer - AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization - AZURE_SEARCH_FIELDS_ID: azureSearchFieldId - AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn - AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn - AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn - AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata - AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn - AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn - AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn - USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing - DOCUMENT_PROCESSING_QUEUE_NAME: queueName - ORCHESTRATION_STRATEGY: orchestrationStrategy - LOGLEVEL: logLevel - AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage - AZURE_SEARCH_TOP_K: azureSearchTopK - DATABASE_TYPE: databaseType - AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' - ? string({ + appSettings: union( + { + AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion + AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint + AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo + AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource + AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer + AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization + AZURE_SEARCH_FIELDS_ID: azureSearchFieldId + AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn + AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata + AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn + AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn + AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn + USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing + DOCUMENT_PROCESSING_QUEUE_NAME: queueName + ORCHESTRATION_STRATEGY: orchestrationStrategy + LOGLEVEL: logLevel + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_SEARCH_TOP_K: azureSearchTopK + DATABASE_TYPE: databaseType + }, + databaseType == 'PostgreSQL' ? { + AZURE_POSTGRESQL_INFO: string({ host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName user: functionName }) - : {} - } + } : {} + ) } } @@ -1101,43 +1107,45 @@ module function_docker './app/function.bicep' = if (hostingModel == 'container') keyVaultName: useKeyVault || authType == 'rbac' ? keyvault.outputs.name : '' authType: authType databaseType: databaseType - appSettings: { - AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion - AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion - AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint - AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo - AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo - AZURE_OPENAI_RESOURCE: azureOpenAIResourceName - AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion - AZURE_SEARCH_INDEX: azureSearchIndex - AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' - AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource - AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer - AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization - AZURE_SEARCH_FIELDS_ID: azureSearchFieldId - AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn - AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn - AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn - AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata - AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn - AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn - AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn - USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing - DOCUMENT_PROCESSING_QUEUE_NAME: queueName - ORCHESTRATION_STRATEGY: orchestrationStrategy - LOGLEVEL: logLevel - AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage - AZURE_SEARCH_TOP_K: azureSearchTopK - DATABASE_TYPE: databaseType - AZURE_POSTGRESQL_INFO: databaseType == 'PostgreSQL' - ? string({ + appSettings: union( + { + AZURE_COMPUTER_VISION_ENDPOINT: useAdvancedImageProcessing ? computerVision.outputs.endpoint : '' + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION: computerVisionVectorizeImageApiVersion + AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION: computerVisionVectorizeImageModelVersion + AZURE_CONTENT_SAFETY_ENDPOINT: contentsafety.outputs.endpoint + AZURE_OPENAI_MODEL_INFO: azureOpenAIModelInfo + AZURE_OPENAI_EMBEDDING_MODEL_INFO: azureOpenAIEmbeddingModelInfo + AZURE_OPENAI_RESOURCE: azureOpenAIResourceName + AZURE_OPENAI_API_VERSION: azureOpenAIApiVersion + AZURE_SEARCH_INDEX: azureSearchIndex + AZURE_SEARCH_SERVICE: 'https://${azureAISearchName}.search.windows.net' + AZURE_SEARCH_DATASOURCE_NAME: azureSearchDatasource + AZURE_SEARCH_INDEXER_NAME: azureSearchIndexer + AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION: azureSearchUseIntegratedVectorization + AZURE_SEARCH_FIELDS_ID: azureSearchFieldId + AZURE_SEARCH_CONTENT_COLUMN: azureSearchContentColumn + AZURE_SEARCH_CONTENT_VECTOR_COLUMN: azureSearchVectorColumn + AZURE_SEARCH_TITLE_COLUMN: azureSearchTitleColumn + AZURE_SEARCH_FIELDS_METADATA: azureSearchFieldsMetadata + AZURE_SEARCH_SOURCE_COLUMN: azureSearchSourceColumn + AZURE_SEARCH_CHUNK_COLUMN: azureSearchChunkColumn + AZURE_SEARCH_OFFSET_COLUMN: azureSearchOffsetColumn + USE_ADVANCED_IMAGE_PROCESSING: useAdvancedImageProcessing + DOCUMENT_PROCESSING_QUEUE_NAME: queueName + ORCHESTRATION_STRATEGY: orchestrationStrategy + LOGLEVEL: logLevel + AZURE_OPENAI_SYSTEM_MESSAGE: azureOpenAISystemMessage + AZURE_SEARCH_TOP_K: azureSearchTopK + DATABASE_TYPE: databaseType + }, + databaseType == 'PostgreSQL' ? { + AZURE_POSTGRESQL_INFO: string({ host: postgresDBModule.outputs.postgresDbOutput.postgreSQLServerName dbname: postgresDBModule.outputs.postgresDbOutput.postgreSQLDatabaseName user: '${functionName}-docker' }) - : {} - } + } : {} + ) } } diff --git a/infra/main.json b/infra/main.json index 1d4a4652d..e85d319f5 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "13919353598581755306" + "templateHash": "8207330457159201967" } }, "parameters": { @@ -1201,9 +1201,7 @@ "principalId": { "value": "[parameters('principalId')]" }, - "managedIdentityObjectId": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.objectId]" - } + "managedIdentityObjectId": "[if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('value', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_managed_identity'), '2022-09-01').outputs.managedIdentityOutput.value.objectId), createObject('value', ''))]" }, "template": { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -1212,7 +1210,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12623823360057072578" + "templateHash": "6159059556257175429" }, "description": "Creates an Azure Key Vault." }, @@ -1229,7 +1227,8 @@ "defaultValue": {} }, "managedIdentityObjectId": { - "type": "string" + "type": "string", + "defaultValue": "" }, "principalId": { "type": "string", @@ -1249,7 +1248,7 @@ "family": "A", "name": "standard" }, - "accessPolicies": "[if(not(empty(parameters('principalId'))), createArray(createObject('objectId', parameters('principalId'), 'permissions', createObject('secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId), createObject('objectId', parameters('managedIdentityObjectId'), 'permissions', createObject('secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId)), createArray(createObject('objectId', parameters('managedIdentityObjectId'), 'permissions', createObject('secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId)))]" + "accessPolicies": "[concat(if(not(equals(parameters('managedIdentityObjectId'), '')), createArray(createObject('objectId', parameters('managedIdentityObjectId'), 'permissions', createObject('keys', createArray('get', 'list'), 'secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId)), createArray()), if(not(equals(parameters('principalId'), '')), createArray(createObject('objectId', parameters('principalId'), 'permissions', createObject('keys', createArray('get', 'list'), 'secrets', createArray('get', 'list')), 'tenantId', subscription().tenantId)), createArray()))]" } } ], @@ -2667,7 +2666,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "2467145046105891366" + "templateHash": "16156160398831650157" } }, "parameters": { @@ -2850,7 +2849,7 @@ "value": "[parameters('healthCheckPath')]" }, "managedIdentity": { - "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + "value": "[or(equals(parameters('databaseType'), 'PostgreSQL'), not(empty(parameters('keyVaultName'))))]" } }, "template": { @@ -3667,7 +3666,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "2467145046105891366" + "templateHash": "16156160398831650157" } }, "parameters": { @@ -3850,7 +3849,7 @@ "value": "[parameters('healthCheckPath')]" }, "managedIdentity": { - "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + "value": "[or(equals(parameters('databaseType'), 'PostgreSQL'), not(empty(parameters('keyVaultName'))))]" } }, "template": { @@ -4655,51 +4654,7 @@ "value": "[parameters('databaseType')]" }, "appSettings": { - "value": { - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", - "AZURE_OPENAI_TOP_P": "[parameters('azureOpenAITopP')]", - "AZURE_OPENAI_MAX_TOKENS": "[parameters('azureOpenAIMaxTokens')]", - "AZURE_OPENAI_STOP_SEQUENCE": "[parameters('azureOpenAIStopSequence')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_OPENAI_STREAM": "[parameters('azureOpenAIStream')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_USE_SEMANTIC_SEARCH": "[parameters('azureSearchUseSemanticSearch')]", - "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG": "[parameters('azureSearchSemanticSearchConfig')]", - "AZURE_SEARCH_INDEX_IS_PRECHUNKED": "[parameters('azureSearchIndexIsPrechunked')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_SEARCH_ENABLE_IN_DOMAIN": "[parameters('azureSearchEnableInDomain')]", - "AZURE_SEARCH_FILENAME_COLUMN": "[parameters('azureSearchFilenameColumn')]", - "AZURE_SEARCH_FILTER": "[parameters('azureSearchFilter')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "AZURE_SEARCH_URL_COLUMN": "[parameters('azureSearchUrlColumn')]", - "AZURE_SEARCH_DATASOURCE_NAME": "[parameters('azureSearchDatasource')]", - "AZURE_SEARCH_INDEXER_NAME": "[parameters('azureSearchIndexer')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "BACKEND_URL": "[format('https://{0}.azurewebsites.net', parameters('functionName'))]", - "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", - "FUNCTION_KEY": "[variables('clientKey')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]", - "DATABASE_TYPE": "[parameters('databaseType')]", - "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('adminWebsiteName'))), createObject())]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_DATASOURCE_NAME', parameters('azureSearchDatasource'), 'AZURE_SEARCH_INDEXER_NAME', parameters('azureSearchIndexer'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'BACKEND_URL', format('https://{0}.azurewebsites.net', parameters('functionName')), 'DOCUMENT_PROCESSING_QUEUE_NAME', variables('queueName'), 'FUNCTION_KEY', variables('clientKey'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'LOGLEVEL', parameters('logLevel'), 'DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESQL_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('adminWebsiteName')))), createObject()))]" } }, "template": { @@ -4709,7 +4664,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "1041527018081544410" + "templateHash": "12836754738443272381" } }, "parameters": { @@ -4874,7 +4829,7 @@ "value": "[parameters('appServicePlanId')]" }, "managedIdentity": { - "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + "value": "[or(equals(parameters('databaseType'), 'PostgreSQL'), not(empty(parameters('keyVaultName'))))]" }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" @@ -5616,51 +5571,7 @@ "value": "[parameters('databaseType')]" }, "appSettings": { - "value": { - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_TEMPERATURE": "[parameters('azureOpenAITemperature')]", - "AZURE_OPENAI_TOP_P": "[parameters('azureOpenAITopP')]", - "AZURE_OPENAI_MAX_TOKENS": "[parameters('azureOpenAIMaxTokens')]", - "AZURE_OPENAI_STOP_SEQUENCE": "[parameters('azureOpenAIStopSequence')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_OPENAI_STREAM": "[parameters('azureOpenAIStream')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_USE_SEMANTIC_SEARCH": "[parameters('azureSearchUseSemanticSearch')]", - "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG": "[parameters('azureSearchSemanticSearchConfig')]", - "AZURE_SEARCH_INDEX_IS_PRECHUNKED": "[parameters('azureSearchIndexIsPrechunked')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "AZURE_SEARCH_ENABLE_IN_DOMAIN": "[parameters('azureSearchEnableInDomain')]", - "AZURE_SEARCH_FILENAME_COLUMN": "[parameters('azureSearchFilenameColumn')]", - "AZURE_SEARCH_FILTER": "[parameters('azureSearchFilter')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "AZURE_SEARCH_URL_COLUMN": "[parameters('azureSearchUrlColumn')]", - "AZURE_SEARCH_DATASOURCE_NAME": "[parameters('azureSearchDatasource')]", - "AZURE_SEARCH_INDEXER_NAME": "[parameters('azureSearchIndexer')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "BACKEND_URL": "[format('https://{0}-docker.azurewebsites.net', parameters('functionName'))]", - "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", - "FUNCTION_KEY": "[variables('clientKey')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]", - "DATABASE_TYPE": "[parameters('databaseType')]", - "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('adminWebsiteName')))), createObject())]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_TEMPERATURE', parameters('azureOpenAITemperature'), 'AZURE_OPENAI_TOP_P', parameters('azureOpenAITopP'), 'AZURE_OPENAI_MAX_TOKENS', parameters('azureOpenAIMaxTokens'), 'AZURE_OPENAI_STOP_SEQUENCE', parameters('azureOpenAIStopSequence'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_OPENAI_STREAM', parameters('azureOpenAIStream'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_USE_SEMANTIC_SEARCH', parameters('azureSearchUseSemanticSearch'), 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG', parameters('azureSearchSemanticSearchConfig'), 'AZURE_SEARCH_INDEX_IS_PRECHUNKED', parameters('azureSearchIndexIsPrechunked'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'AZURE_SEARCH_ENABLE_IN_DOMAIN', parameters('azureSearchEnableInDomain'), 'AZURE_SEARCH_FILENAME_COLUMN', parameters('azureSearchFilenameColumn'), 'AZURE_SEARCH_FILTER', parameters('azureSearchFilter'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'AZURE_SEARCH_URL_COLUMN', parameters('azureSearchUrlColumn'), 'AZURE_SEARCH_DATASOURCE_NAME', parameters('azureSearchDatasource'), 'AZURE_SEARCH_INDEXER_NAME', parameters('azureSearchIndexer'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'BACKEND_URL', format('https://{0}-docker.azurewebsites.net', parameters('functionName')), 'DOCUMENT_PROCESSING_QUEUE_NAME', variables('queueName'), 'FUNCTION_KEY', variables('clientKey'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'LOGLEVEL', parameters('logLevel'), 'DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESQL_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('adminWebsiteName'))))), createObject()))]" } }, "template": { @@ -5670,7 +5581,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "1041527018081544410" + "templateHash": "12836754738443272381" } }, "parameters": { @@ -5835,7 +5746,7 @@ "value": "[parameters('appServicePlanId')]" }, "managedIdentity": { - "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + "value": "[or(equals(parameters('databaseType'), 'PostgreSQL'), not(empty(parameters('keyVaultName'))))]" }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" @@ -8324,37 +8235,7 @@ "value": "[parameters('databaseType')]" }, "appSettings": { - "value": { - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_DATASOURCE_NAME": "[parameters('azureSearchDatasource')]", - "AZURE_SEARCH_INDEXER_NAME": "[parameters('azureSearchIndexer')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "DATABASE_TYPE": "[parameters('databaseType')]", - "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('functionName'))), createObject())]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_DATASOURCE_NAME', parameters('azureSearchDatasource'), 'AZURE_SEARCH_INDEXER_NAME', parameters('azureSearchIndexer'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'DOCUMENT_PROCESSING_QUEUE_NAME', variables('queueName'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESQL_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', parameters('functionName')))), createObject()))]" } }, "template": { @@ -8364,7 +8245,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "11172260966600448386" + "templateHash": "5143341974057039705" } }, "parameters": { @@ -8549,7 +8430,7 @@ "value": "[parameters('useKeyVault')]" }, "managedIdentity": { - "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + "value": "[or(equals(parameters('databaseType'), 'PostgreSQL'), not(empty(parameters('keyVaultName'))))]" }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" @@ -9656,37 +9537,7 @@ "value": "[parameters('databaseType')]" }, "appSettings": { - "value": { - "AZURE_COMPUTER_VISION_ENDPOINT": "[if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, '')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION": "[parameters('computerVisionVectorizeImageApiVersion')]", - "AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION": "[parameters('computerVisionVectorizeImageModelVersion')]", - "AZURE_CONTENT_SAFETY_ENDPOINT": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value]", - "AZURE_OPENAI_MODEL_INFO": "[variables('azureOpenAIModelInfo')]", - "AZURE_OPENAI_EMBEDDING_MODEL_INFO": "[variables('azureOpenAIEmbeddingModelInfo')]", - "AZURE_OPENAI_RESOURCE": "[parameters('azureOpenAIResourceName')]", - "AZURE_OPENAI_API_VERSION": "[parameters('azureOpenAIApiVersion')]", - "AZURE_SEARCH_INDEX": "[parameters('azureSearchIndex')]", - "AZURE_SEARCH_SERVICE": "[format('https://{0}.search.windows.net', parameters('azureAISearchName'))]", - "AZURE_SEARCH_DATASOURCE_NAME": "[parameters('azureSearchDatasource')]", - "AZURE_SEARCH_INDEXER_NAME": "[parameters('azureSearchIndexer')]", - "AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION": "[parameters('azureSearchUseIntegratedVectorization')]", - "AZURE_SEARCH_FIELDS_ID": "[parameters('azureSearchFieldId')]", - "AZURE_SEARCH_CONTENT_COLUMN": "[parameters('azureSearchContentColumn')]", - "AZURE_SEARCH_CONTENT_VECTOR_COLUMN": "[parameters('azureSearchVectorColumn')]", - "AZURE_SEARCH_TITLE_COLUMN": "[parameters('azureSearchTitleColumn')]", - "AZURE_SEARCH_FIELDS_METADATA": "[parameters('azureSearchFieldsMetadata')]", - "AZURE_SEARCH_SOURCE_COLUMN": "[parameters('azureSearchSourceColumn')]", - "AZURE_SEARCH_CHUNK_COLUMN": "[parameters('azureSearchChunkColumn')]", - "AZURE_SEARCH_OFFSET_COLUMN": "[parameters('azureSearchOffsetColumn')]", - "USE_ADVANCED_IMAGE_PROCESSING": "[parameters('useAdvancedImageProcessing')]", - "DOCUMENT_PROCESSING_QUEUE_NAME": "[variables('queueName')]", - "ORCHESTRATION_STRATEGY": "[parameters('orchestrationStrategy')]", - "LOGLEVEL": "[parameters('logLevel')]", - "AZURE_OPENAI_SYSTEM_MESSAGE": "[parameters('azureOpenAISystemMessage')]", - "AZURE_SEARCH_TOP_K": "[parameters('azureSearchTopK')]", - "DATABASE_TYPE": "[parameters('databaseType')]", - "AZURE_POSTGRESQL_INFO": "[if(equals(parameters('databaseType'), 'PostgreSQL'), string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('functionName')))), createObject())]" - } + "value": "[union(createObject('AZURE_COMPUTER_VISION_ENDPOINT', if(parameters('useAdvancedImageProcessing'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'computerVision'), '2022-09-01').outputs.endpoint.value, ''), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_API_VERSION', parameters('computerVisionVectorizeImageApiVersion'), 'AZURE_COMPUTER_VISION_VECTORIZE_IMAGE_MODEL_VERSION', parameters('computerVisionVectorizeImageModelVersion'), 'AZURE_CONTENT_SAFETY_ENDPOINT', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', parameters('contentSafetyName')), '2022-09-01').outputs.endpoint.value, 'AZURE_OPENAI_MODEL_INFO', variables('azureOpenAIModelInfo'), 'AZURE_OPENAI_EMBEDDING_MODEL_INFO', variables('azureOpenAIEmbeddingModelInfo'), 'AZURE_OPENAI_RESOURCE', parameters('azureOpenAIResourceName'), 'AZURE_OPENAI_API_VERSION', parameters('azureOpenAIApiVersion'), 'AZURE_SEARCH_INDEX', parameters('azureSearchIndex'), 'AZURE_SEARCH_SERVICE', format('https://{0}.search.windows.net', parameters('azureAISearchName')), 'AZURE_SEARCH_DATASOURCE_NAME', parameters('azureSearchDatasource'), 'AZURE_SEARCH_INDEXER_NAME', parameters('azureSearchIndexer'), 'AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION', parameters('azureSearchUseIntegratedVectorization'), 'AZURE_SEARCH_FIELDS_ID', parameters('azureSearchFieldId'), 'AZURE_SEARCH_CONTENT_COLUMN', parameters('azureSearchContentColumn'), 'AZURE_SEARCH_CONTENT_VECTOR_COLUMN', parameters('azureSearchVectorColumn'), 'AZURE_SEARCH_TITLE_COLUMN', parameters('azureSearchTitleColumn'), 'AZURE_SEARCH_FIELDS_METADATA', parameters('azureSearchFieldsMetadata'), 'AZURE_SEARCH_SOURCE_COLUMN', parameters('azureSearchSourceColumn'), 'AZURE_SEARCH_CHUNK_COLUMN', parameters('azureSearchChunkColumn'), 'AZURE_SEARCH_OFFSET_COLUMN', parameters('azureSearchOffsetColumn'), 'USE_ADVANCED_IMAGE_PROCESSING', parameters('useAdvancedImageProcessing'), 'DOCUMENT_PROCESSING_QUEUE_NAME', variables('queueName'), 'ORCHESTRATION_STRATEGY', parameters('orchestrationStrategy'), 'LOGLEVEL', parameters('logLevel'), 'AZURE_OPENAI_SYSTEM_MESSAGE', parameters('azureOpenAISystemMessage'), 'AZURE_SEARCH_TOP_K', parameters('azureSearchTopK'), 'DATABASE_TYPE', parameters('databaseType')), if(equals(parameters('databaseType'), 'PostgreSQL'), createObject('AZURE_POSTGRESQL_INFO', string(createObject('host', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'dbname', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'user', format('{0}-docker', parameters('functionName'))))), createObject()))]" } }, "template": { @@ -9696,7 +9547,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "11172260966600448386" + "templateHash": "5143341974057039705" } }, "parameters": { @@ -9881,7 +9732,7 @@ "value": "[parameters('useKeyVault')]" }, "managedIdentity": { - "value": "[equals(parameters('databaseType'), 'PostgreSQL')]" + "value": "[or(equals(parameters('databaseType'), 'PostgreSQL'), not(empty(parameters('keyVaultName'))))]" }, "appSettings": { "value": "[union(parameters('appSettings'), createObject('WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'false', 'AZURE_AUTH_TYPE', parameters('authType'), 'USE_KEY_VAULT', if(parameters('useKeyVault'), parameters('useKeyVault'), ''), 'AZURE_OPENAI_API_KEY', if(parameters('useKeyVault'), parameters('openAIKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('azureOpenAIName')), '2023-05-01').key1), 'AZURE_SEARCH_KEY', if(parameters('useKeyVault'), parameters('searchKeyName'), listAdminKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Search/searchServices', parameters('azureAISearchName')), '2021-04-01-preview').primaryKey), 'AZURE_BLOB_STORAGE_INFO', if(parameters('useKeyVault'), parameters('azureBlobStorageInfo'), replace(parameters('azureBlobStorageInfo'), '$STORAGE_ACCOUNT_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2021-09-01').keys[0].value)), 'AZURE_FORM_RECOGNIZER_INFO', if(parameters('useKeyVault'), parameters('azureFormRecognizerInfo'), replace(parameters('azureFormRecognizerInfo'), '$FORM_RECOGNIZER_KEY', listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('formRecognizerName')), '2023-05-01').key1)), 'AZURE_CONTENT_SAFETY_KEY', if(parameters('useKeyVault'), parameters('contentSafetyKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('contentSafetyName')), '2023-05-01').key1), 'AZURE_SPEECH_SERVICE_KEY', if(parameters('useKeyVault'), parameters('speechKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('speechServiceName')), '2023-05-01').key1), 'AZURE_COMPUTER_VISION_KEY', if(or(parameters('useKeyVault'), equals(parameters('computerVisionName'), '')), parameters('computerVisionKeyName'), listKeys(resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.CognitiveServices/accounts', parameters('computerVisionName')), '2023-05-01').key1)))]" @@ -12378,7 +12229,7 @@ }, "AZURE_POSTGRESQL_INFO": { "type": "string", - "value": "[string(createObject('serverName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, 'databaseName', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, 'userName', ''))]" + "value": "[string(createObject('serverName', if(equals(parameters('databaseType'), 'PostgreSQL'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLServerName, ''), 'databaseName', if(equals(parameters('databaseType'), 'PostgreSQL'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, variables('rgName')), 'Microsoft.Resources/deployments', 'deploy_postgres_sql'), '2022-09-01').outputs.postgresDbOutput.value.postgreSQLDatabaseName, ''), 'userName', ''))]" } } } \ No newline at end of file From d4fa94c7f88fb81f0e341bdb6522993ba7e9ca20 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 6 Dec 2024 18:36:04 +0530 Subject: [PATCH 104/107] fix delete page issue --- code/backend/pages/03_Delete_Data.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/code/backend/pages/03_Delete_Data.py b/code/backend/pages/03_Delete_Data.py index 98e086c49..16676a1d2 100644 --- a/code/backend/pages/03_Delete_Data.py +++ b/code/backend/pages/03_Delete_Data.py @@ -47,10 +47,9 @@ def load_css(file_path): search_handler = Search.get_search_handler(env_helper) results = search_handler.get_files() if ( - results is None - or (hasattr(results, "get_count") and results.get_count() == 0) - or len(results) == 0 - ): + env_helper.DATABASE_TYPE == "CosmosDB" + and (results is None or results.get_count() == 0) + ) or (env_helper.DATABASE_TYPE == "PostgreSQL" and len(results) == 0): st.info("No files to delete") st.stop() else: From 9b91c96c2cef35b92c5facaad120713149d4d188 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Mon, 9 Dec 2024 14:10:39 -0500 Subject: [PATCH 105/107] update db to enum values --- code/backend/pages/02_Explore_Data.py | 5 +++-- code/backend/pages/03_Delete_Data.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/code/backend/pages/02_Explore_Data.py b/code/backend/pages/02_Explore_Data.py index 5ef55a774..0d71ed47b 100644 --- a/code/backend/pages/02_Explore_Data.py +++ b/code/backend/pages/02_Explore_Data.py @@ -4,6 +4,7 @@ import sys import pandas as pd from batch.utilities.helpers.env_helper import EnvHelper +from batch.utilities.helpers.config.database_type import DatabaseType from batch.utilities.search.search import Search sys.path.append(os.path.join(os.path.dirname(__file__), "..")) @@ -41,9 +42,9 @@ def load_css(file_path): search_handler = Search.get_search_handler(env_helper) # Determine unique files based on database type - if env_helper.DATABASE_TYPE == "PostgreSQL": + if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value: unique_files = search_handler.get_unique_files() - elif env_helper.DATABASE_TYPE == "CosmosDB": + elif env_helper.DATABASE_TYPE == DatabaseType.COSMOSDB.value: results = search_handler.search_with_facets("*", "title", facet_count=0) unique_files = search_handler.get_unique_files(results, "title") else: diff --git a/code/backend/pages/03_Delete_Data.py b/code/backend/pages/03_Delete_Data.py index 16676a1d2..c681ac411 100644 --- a/code/backend/pages/03_Delete_Data.py +++ b/code/backend/pages/03_Delete_Data.py @@ -5,6 +5,7 @@ import logging from batch.utilities.helpers.env_helper import EnvHelper from batch.utilities.search.search import Search +from batch.utilities.helpers.config.database_type import DatabaseType from batch.utilities.helpers.azure_blob_storage_client import AzureBlobStorageClient sys.path.append(os.path.join(os.path.dirname(__file__), "..")) @@ -47,9 +48,9 @@ def load_css(file_path): search_handler = Search.get_search_handler(env_helper) results = search_handler.get_files() if ( - env_helper.DATABASE_TYPE == "CosmosDB" + env_helper.DATABASE_TYPE == DatabaseType.COSMOSDB.value and (results is None or results.get_count() == 0) - ) or (env_helper.DATABASE_TYPE == "PostgreSQL" and len(results) == 0): + ) or (env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value and len(results) == 0): st.info("No files to delete") st.stop() else: From b2c199e1fcd9d933905c7bad9c9797bd8ece77b3 Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Mon, 9 Dec 2024 15:10:51 -0500 Subject: [PATCH 106/107] updte postgreSQL vector table name in the documents, and update document --- README.md | 22 +++++++++++++++++- .../helpers/azure_postgres_helper.py | 20 ++++++++-------- .../helpers/embedders/postgres_embedder.py | 2 +- .../search/postgres_search_handler.py | 6 ++--- .../backend_api/default/test_conversation.py | 2 +- .../test_iv_question_answer_tool.py | 2 +- .../test_advanced_image_processing.py | 2 +- .../test_postgres_search_handler.py | 4 ++-- .../helpers/test_azure_postgres_helper.py | 8 +++---- .../helpers/test_postgress_embedder.py | 2 +- docs/images/architecture.png | Bin 170616 -> 0 bytes docs/images/architecture_cdb.png | Bin 0 -> 137264 bytes docs/images/architrecture_pg.png | Bin 0 -> 141937 bytes docs/images/cwyd-solution-architecture.png | Bin 96156 -> 0 bytes docs/postgreSQL.md | 16 ++++++------- .../data_scripts/create_postgres_tables.py | 12 ++++------ 16 files changed, 57 insertions(+), 41 deletions(-) delete mode 100644 docs/images/architecture.png create mode 100644 docs/images/architecture_cdb.png create mode 100644 docs/images/architrecture_pg.png delete mode 100644 docs/images/cwyd-solution-architecture.png diff --git a/README.md b/README.md index 8c9d4cc8d..049cb41b1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ urlFragment: chat-with-your-data-solution-accelerator ## User story Welcome to the *Chat with your data* Solution accelerator repository! The *Chat with your data* Solution accelerator is a powerful tool that combines the capabilities of Azure AI Search and Large Language Models (LLMs) to create a conversational search experience. This solution accelerator uses an Azure OpenAI GPT model and an Azure AI Search index generated from your data, which is integrated into a web application to provide a natural language interface, including [speech-to-text](docs/speech_to_text.md) functionality, for search queries. Users can drag and drop files, point to storage, and take care of technical setup to transform documents. Everything can be deployed in your own subscription to accelerate your use of this technology. -![Solution Architecture - Chat with your data](/docs/images/cwyd-solution-architecture.png) + ### About this repo @@ -93,10 +93,25 @@ Here is a comparison table with a few features offered by Azure, an available Gi - **Easy access to source documentation when querying**: Review referenced documents in the same chat window for additional context. - **Data upload**: Batch upload documents of [various file types](docs/supported_file_types.md) - **Accessible orchestration**: Prompt and document configuration (prompt engineering, document processing, and data retrieval) +- **Database flexibility**: Dynamic database switching allows users to choose between PostgreSQL and Cosmos DB based on their requirements. If no preference is specified the platform defaults to PostgreSQL. **Note**: The current model allows users to ask questions about unstructured data, such as PDF, text, and docx files. See the [supported file types](docs/supported_file_types.md). + + + +### Cosmos DB vs PostgreSQL +This solution accelerator can be deployed with PostgreSQL or Cosmos DB. Both options enable chat history and have other similar features, see features table below. When choosing between which technology to deploy with, considering the following: scalability, data model flexibility, query language, security, and pricing + +One important consideration when deciding which option is best for you: Cosmos DB can be used to enable chat history, while PostgeSQL can be leveraged for data indexing as well as chat history. Review the relevant configuration documentation and architectures for both respective options below. + +To review Cosmos DB configuration overview and configuration steps [here](docs/employee_assistance.md). +![Solution Architecture - Chat with your data CosmosDB](/docs/images/architecture_cdb.png) + +To review PostgreSQL configuration overview and steps, follow the link [here](docs/postgreSQL.md). +![Solution Architecture - Chat with your data PostgreSQL](/docs/images/architrecture_pg.png) + ### Target end users Company personnel (employees, executives) looking to research against internal unstructured company data would leverage this accelerator using natural language to find what they need quickly. @@ -146,6 +161,7 @@ In this scenario, a newly hired employee is in the process of onboarding to thei - Azure Storage Account - Azure Speech Service - Azure CosmosDB +- Azure PostgreSQL - Teams (optional: Teams extension only) ### Required licenses @@ -197,7 +213,11 @@ switch to a lower version. To find out which versions are supported in different \ \ + + + ![Supporting documentation](/docs/images/supportingDocuments.png) + ## Supporting documentation ### Resource links diff --git a/code/backend/batch/utilities/helpers/azure_postgres_helper.py b/code/backend/batch/utilities/helpers/azure_postgres_helper.py index e3519110b..674ba166a 100644 --- a/code/backend/batch/utilities/helpers/azure_postgres_helper.py +++ b/code/backend/batch/utilities/helpers/azure_postgres_helper.py @@ -48,7 +48,7 @@ def get_search_client(self): self.conn = self._create_search_client() return self.conn - def get_search_indexes(self, embedding_array): + def get_vector_store(self, embedding_array): """ Fetches search indexes from PostgreSQL based on an embedding vector. """ @@ -58,7 +58,7 @@ def get_search_indexes(self, embedding_array): cur.execute( """ SELECT id, title, chunk, "offset", page_number, content, source - FROM search_indexes + FROM vector_store ORDER BY content_vector <=> %s::vector LIMIT %s """, @@ -76,9 +76,9 @@ def get_search_indexes(self, embedding_array): finally: conn.close() - def create_search_indexes(self, documents_to_upload): + def create_vector_store(self, documents_to_upload): """ - Inserts documents into the `search_indexes` table in batch mode. + Inserts documents into the `vector_store` table in batch mode. """ conn = self.get_search_client() try: @@ -101,7 +101,7 @@ def create_search_indexes(self, documents_to_upload): # Batch insert using execute_values for efficiency query = """ - INSERT INTO search_indexes ( + INSERT INTO vector_store ( id, title, chunk, chunk_id, "offset", page_number, content, source, metadata, content_vector ) VALUES %s @@ -133,7 +133,7 @@ def get_files(self): with conn.cursor(cursor_factory=RealDictCursor) as cursor: query = """ SELECT id, title - FROM search_indexes + FROM vector_store WHERE title IS NOT NULL ORDER BY title; """ @@ -171,7 +171,7 @@ def delete_documents(self, ids_to_delete): with conn.cursor() as cursor: # Construct the DELETE query with the list of ids_to_delete query = """ - DELETE FROM search_indexes + DELETE FROM vector_store WHERE id = ANY(%s) """ # Extract the 'id' values from the list of dictionaries (ids_to_delete) @@ -210,7 +210,7 @@ def perform_search(self, title): cur.execute( """ SELECT title, content, metadata - FROM search_indexes + FROM vector_store WHERE title = %s """, (title,), @@ -236,7 +236,7 @@ def get_unique_files(self): cur.execute( """ SELECT DISTINCT title - FROM search_indexes + FROM vector_store """ ) results = cur.fetchall() # Fetch all results as RealDictRow objects @@ -260,7 +260,7 @@ def search_by_blob_url(self, blob_url): cur.execute( """ SELECT id, title - FROM search_indexes + FROM vector_store WHERE source = %s """, (f"{blob_url}_SAS_TOKEN_PLACEHOLDER_",), diff --git a/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py index 7a6739484..d81c9727c 100644 --- a/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py +++ b/code/backend/batch/utilities/helpers/embedders/postgres_embedder.py @@ -69,7 +69,7 @@ def __embed( documents_to_upload.append(self.__convert_to_search_document(document)) if documents_to_upload: - self.azure_postgres_helper.create_search_indexes(documents_to_upload) + self.azure_postgres_helper.create_vector_store(documents_to_upload) else: logger.warning("No documents to upload.") diff --git a/code/backend/batch/utilities/search/postgres_search_handler.py b/code/backend/batch/utilities/search/postgres_search_handler.py index 5be9bb5ec..0671a16d2 100644 --- a/code/backend/batch/utilities/search/postgres_search_handler.py +++ b/code/backend/batch/utilities/search/postgres_search_handler.py @@ -21,7 +21,7 @@ def query_search(self, question) -> List[SourceDocument]: embedding_array = np.array(query_embedding).tolist() - search_results = self.azure_postgres_helper.get_search_indexes(embedding_array) + search_results = self.azure_postgres_helper.get_vector_store(embedding_array) return self._convert_to_source_documents(search_results) @@ -44,8 +44,8 @@ def _convert_to_source_documents(self, search_results) -> List[SourceDocument]: def create_search_client(self): return self.azure_postgres_helper.get_search_client() - def create_search_indexes(self, documents_to_upload): - return self.azure_postgres_helper.create_search_indexes(documents_to_upload) + def create_vector_store(self, documents_to_upload): + return self.azure_postgres_helper.create_vector_store(documents_to_upload) def perform_search(self, filename): return self.azure_postgres_helper.perform_search(filename) diff --git a/code/tests/functional/tests/backend_api/default/test_conversation.py b/code/tests/functional/tests/backend_api/default/test_conversation.py index 8d7106f8c..34e90cf7b 100644 --- a/code/tests/functional/tests/backend_api/default/test_conversation.py +++ b/code/tests/functional/tests/backend_api/default/test_conversation.py @@ -328,7 +328,7 @@ def test_post_makes_correct_call_to_openai_chat_completions_with_functions( ) -def test_post_makes_correct_call_to_list_search_indexes( +def test_post_makes_correct_call_to_list_vector_store( app_url: str, app_config: AppConfig, httpserver: HTTPServer ): # when diff --git a/code/tests/functional/tests/backend_api/integrated_vectorization_custom_conversation/test_iv_question_answer_tool.py b/code/tests/functional/tests/backend_api/integrated_vectorization_custom_conversation/test_iv_question_answer_tool.py index 9d1eb152b..875c8363c 100644 --- a/code/tests/functional/tests/backend_api/integrated_vectorization_custom_conversation/test_iv_question_answer_tool.py +++ b/code/tests/functional/tests/backend_api/integrated_vectorization_custom_conversation/test_iv_question_answer_tool.py @@ -136,7 +136,7 @@ def test_post_makes_correct_call_to_get_conversation_log_search_index( ) -def test_post_makes_correct_call_to_list_search_indexes( +def test_post_makes_correct_call_to_list_vector_store( app_url: str, app_config: AppConfig, httpserver: HTTPServer ): # when diff --git a/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py b/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py index 124806dad..d500077b4 100644 --- a/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py +++ b/code/tests/functional/tests/functions/advanced_image_processing/test_advanced_image_processing.py @@ -255,7 +255,7 @@ def test_metadata_is_updated_after_processing( ) -def test_makes_correct_call_to_list_search_indexes( +def test_makes_correct_call_to_list_vector_store( message: QueueMessage, httpserver: HTTPServer, app_config: AppConfig ): # when diff --git a/code/tests/search_utilities/test_postgres_search_handler.py b/code/tests/search_utilities/test_postgres_search_handler.py index 1c8117791..65811058d 100644 --- a/code/tests/search_utilities/test_postgres_search_handler.py +++ b/code/tests/search_utilities/test_postgres_search_handler.py @@ -39,7 +39,7 @@ def test_query_search(handler, mock_search_client): mock_llm_helper.generate_embeddings.return_value = [1, 2, 3] - mock_search_client.get_search_indexes.return_value = [ + mock_search_client.get_vector_store.return_value = [ { "id": "1", "title": "Title1", @@ -66,7 +66,7 @@ def test_query_search(handler, mock_search_client): result = handler.query_search("Sample question") mock_llm_helper.generate_embeddings.assert_called_once_with("Sample question") - mock_search_client.get_search_indexes.assert_called_once() + mock_search_client.get_vector_store.assert_called_once() assert len(result) == 2 assert isinstance(result[0], SourceDocument) assert result[0].id == "1" diff --git a/code/tests/utilities/helpers/test_azure_postgres_helper.py b/code/tests/utilities/helpers/test_azure_postgres_helper.py index e2cb9c438..7fc10fcec 100644 --- a/code/tests/utilities/helpers/test_azure_postgres_helper.py +++ b/code/tests/utilities/helpers/test_azure_postgres_helper.py @@ -57,7 +57,7 @@ def test_get_search_client_reuses_connection(self, mock_connect): ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") @patch("backend.batch.utilities.helpers.azure_postgres_helper.RealDictCursor") - def test_get_search_indexes_success( + def test_get_vector_store_success( self, mock_cursor, mock_connect, mock_credential ): # Arrange @@ -93,7 +93,7 @@ def test_get_search_indexes_success( embedding_vector = [1, 2, 3] # Act - results = helper.get_search_indexes(embedding_vector) + results = helper.get_vector_store(embedding_vector) # Assert self.assertEqual(results, mock_results) @@ -105,7 +105,7 @@ def test_get_search_indexes_success( "backend.batch.utilities.helpers.azure_postgres_helper.DefaultAzureCredential" ) @patch("backend.batch.utilities.helpers.azure_postgres_helper.psycopg2.connect") - def test_get_search_indexes_query_error(self, mock_connect, mock_credential): + def test_get_vector_store_query_error(self, mock_connect, mock_credential): # Arrange # Mock the EnvHelper and set required attributes mock_env_helper = MagicMock() @@ -138,7 +138,7 @@ def raise_exception(*args, **kwargs): # Act & Assert with self.assertRaises(Exception) as context: - helper.get_search_indexes(embedding_vector) + helper.get_vector_store(embedding_vector) self.assertEqual(str(context.exception), "Query execution error") diff --git a/code/tests/utilities/helpers/test_postgress_embedder.py b/code/tests/utilities/helpers/test_postgress_embedder.py index 1f76bae3d..8ed07f472 100644 --- a/code/tests/utilities/helpers/test_postgress_embedder.py +++ b/code/tests/utilities/helpers/test_postgress_embedder.py @@ -135,7 +135,7 @@ def test_embed_file( # Mock methods llm_helper_mock.generate_embeddings.return_value = [0.1, 0.2, 0.3] - azure_postgres_helper_mock.create_search_indexes.return_value = True + azure_postgres_helper_mock.create_vector_store.return_value = True # Execute postgres_embedder.embed_file(source_url, file_name) diff --git a/docs/images/architecture.png b/docs/images/architecture.png deleted file mode 100644 index 280ad8da595cc2f5da0e1d587d2f2d96da9a6e7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170616 zcmeFZc{r5q`#OFb)X_7`bKlo>Ug!BbU+3#OuRHjmhAQJp zj+1nBbc||um9^;TPUXU;ZwA&M1`#;2%3!XTGi;2 z8NbjW^YwjXx?oFGbjFpzlkXgZE3D#qhDxr5mZ$`$AGvZv;B@;Fe#INF;-Ap{b^1?5 zy34mFaMN7x&P!mW5uTJIDW-Lwp6MPUZjp zhw&Vv(ErQtbd*&5PyRn#?_B@G?zG$g_xqkxkGwzdxLIUr3S3uT!*gaA;>_WD*(QP-U9!{7h4|Mqf* zVwPcq)$dEDVY}0g{MY|@_m2O)@=I-dC__hgt0+Tp4@(YS@7^Ade)eDYRQ2COr-Rqu z7Z`>5L#}cDep`n;&U*L_aG9GNI-gh#z`s9v&dn(FU*ElW3d+$DE^+w%$*T0fPJ*t* z_F=Bxe;EV>AmBf*_uc=Q;QyaiFxhf*WfD4AVnzD$@@O|2aL0#?|7S;du|8aE8AR}>*_MO!C4d4eH>IKLD;;Xg|;+?W&FIY(Jq=-{660plHi9>Gz*&f_j`|E&Y z!)8bBE*pZy$U%0`lib7)J@Xbd_u_YOj;*NzmR&ll%5zhpP_5!;e_ z_pJYuF3F#F`)B^&!c+QwYx>yB2TmbazD*3-&C+t2MAhDShWDx=l14_90af)59}(I;1@5=-O?5 z&w#P|=PQPf{Gtm_t$7f_;rvmWa%(!C9TM2jcX@0-NY>4$m&%9x2(x{9_;{_V69FA4 zkGkzA%qC)ZZp6BD#wnV^suA&g&WJEn_C+PN$ZRv(ES{XO#p(6)%gbqiAOJ=4i0f;MI8;d^fuYska=f+6xO(IxN36woS6peQ zy!U`h;W+*ex*LSwivzk^@w4)p`ZX3~x(;^V^|*FbR`aV*bH|jzZ6vPG5*Ie^s2me2 zrkx2Ox=Y^w^oRJmVG2*U95XPGTb^mZf$5ghkJf-$!)fHEdX7LYDai&n24~Gcx#-p^n5~?uuWPFGb{cuT zn0T>p1Sa?4X#BbW52MgX=pNwB{)WV^zh%s?Bybs5 zI1F+n;NKr(U~7+mShm_>w6NiUNb4UG8?!glEMqQqnKoWHb&`oo>c(8TO2?7n;+O>H zCStjC4wIRJ!{3j*OtB_9jql{w!`_2(qIxlNJis{Kd^j9OZ|oZ!JmIw2wO7)%@?Cxg zVXmr(3tA3bt@On2bKA9GOD4(oNV|u2OjCqE=yG`XV{Oko(Y9xW!PhfeCUEIi-M7bZ zmCHx#GPzmm*VitukwY-un6z6R{_EvevOe5cF9<1sDxWgj?bFsw?v>KgKhMsvIo}X) zJlgfIAHZO`><)c?Wc8S+2l5Wt-W;P%OU| zI)1BI)8`jUseHD(q&LWtNq_kBaHe7wB+l;yU7GJfq?NGfrK{!RP_oaI;XuSl+Za{r z`Z&mTG5j9WCm+*pQ(#OOdFs4Dk%(Xzzd7HVOi)FipbvsumA24b`E4*&_+hx5Li7%F za3ME0=y-H<`TrP`%voyH*Cpb$ODCl7J6x|N-6^qdAA82>k+=>0KQC6{-FeRStl7$W z`x4#Cy!y?l#O!soP-Nm}fr+6*0k-!DR77^bFShRJTQ;#dc%EKCh`gyXT4>FkW^Wa? zwy3)Jy@6Lw01SQ_9>8W33x{6Vq0PFxA=e06?qlBFVrNX0s$;h2BwW?YEgP}*DVe^e zMhJ)7TMuj#ht|_oLQhh%K2;axzEePM8Ba^B608(B7f?{yWNqq@mK);?Ex@zoIdz!8 z!#(_9eeL_A=SG3cvzSww0GG4`#RM%%4Ff- zW!jo@?gN$y>;U|u^NFz^;F-0XRtni!5CH|7{vUI2U-`C9n6TNsWa2ti=SuHNk&(N` zNaqE;c<)}9(nW(^6N!wKmC?|O0@xH$G7rPve2%-x+C(xX`F?*am~}x@ct&oxbJ}Qh zC1x6R=fDT@d(wJ-5UcMN`S>QdkXEK?Gl(t9FNJVjWO9l=k%)S0x<#Y^_wdoW48H#T z)iH)C2hGqROG``8nM^e^_58r$Vo>s&mmk>t2sEwiL0^f^+Z7@EeEw3m_9iE-6*oKxZeK~9k(-U#F+eM!)4MGTRr;zm8l9dp z%y>n?3u-OXv3;9Ao||pKTiuJs*}Q#E2An8%jA4aJS~VECI9RytG|pAO?UT77F_*Bi z8s8%p;@I1}c$qsJw-~H}f?h))7wo4|Ry5zX5+QG2T9Z#iS- zqGMI?&G4!GPuG|Re6&WlKTrJt>3@81b$xq@smF?K*Zz7B=Bu4X!Bx!?3r{y>7=s=3 zq57Tms*7@kQ+y&z;f{R#bK5ft99wHwW!9Qs-oocQys{E)X$2L}m1Y)bjlHue~<68?-}_MjP>Y zu3W3$-=Rv>7}xse3)@HO68!gxX`14g@*A^NqX4#$AQtU}C->rOyOk}Y>7S@u$HdRG zhl$oaeoT!ikJE2FHb3O+U>xK8UN6$J*yXGSXscK{P=&=mh5y6?xI8Rsyl6o``C6}U zHYqh$dNbDZfmV@lvbagpSVi5EMC(=YnG^v$w3S%Sif-;f-O$OqO=TOd?{Bxi2Le}w zT}$}PRM}0B4a*{@u1KGxHO?uar57nr#0P1oy%&PUp2j!d6R4$5cf`{$(kR^Xx$m5Vd>3O>7*e!)5Yk!2{-{H8GBlF?S6bM4^RD|XRK{F!7Ja{z|5^(=euFL=7mZIV&?XkZ|E)}E!YFWptpe< zHR`U!OK77M)`zh5!|2tT>gnx3xBLaLn=j2$ms7_AU4`kqZIbG^YdX7a)D^r>8lD`~ zC}}bQFtcJ&uV4u~op|Q5P6{kUmrar=;=pDlG32`;+mR($v`lsyi^J8XOeHoM--TI? zq2h2BMBH*1eK@@Ph+{|9cVyOjoh{~(r51&c&M?nU9q z?%S)rAk~y;(brn!mQQYSgV_6aD>;{voCe@;_6XB9>wfA#~gm3G!=BmhAxG#;= zyBw0tthp#BoEJ>%=$`xD-2BbgiD<=8=d<$%U5Da+mDiQ#eCgZut%6GxIWkVSuYye@ zB@(9T8zPiLUrX3=C7v629l=vMu9T49GoiI%?5G(zUa_fcxn?-Fk&m^t@xLc!@G+o& z9cOn*0k6<+O`->x%%FDv?zi2kF&rcMu3zz4n z4Pv9@6~?}K4BYmijdpqK^oD+GE~8e{&rlwF`L6j|rn)~mAKRaV6&jNtv^OJ4)fQ?W zeHtuhMr>Cw%~H%_O$|uz^r+coa~pEsX?cC9d(NJhW}LEeB)JCH=vc$uz2W@U3JoS#N8q+mQ(r96 zcVJ28{C32TiR#pIDHQ9D4}s(k!PJ+R#uj-HOqS!d)e2bC%HK=NV#VQ`_*}tv*V5A6 zVHGvP+-)NaHcjnaIzQ+2t+^S8VBsd@lY8fgymVb$hx8}%O3(DSPyCLJj?Wsefc01) zW3S)UyIjfYzFhI6!uHp_NVzdsnUhLnzsDsZdk2(40w^r3%|amevePn5s(X6o@zluR z<%%3as|=<1*p5oa$+I;)TKvNn1XC;aw?QSy;0cMlGlmQT7uFWj?QbVYj3uH0y5VDL z1Pl=-qZe}fr87ZGUa|o7v5~-fQpIBYwxBu}z%?u^(0rK$wDf2(vCceFi>ZdGxdl^^$VtFziXrd8;~wEmyao5GV1d(M}PFm zeaFMRxb!|63-`#++N@0V>T*hXap7-*C7YP)P$F8V@$0kg=CrVjU2({R}=MOF?{fw=(K3`Qv=eH zF8as@oghfX8LBOIu z^|oTpkJdBLO_02Nc(${`T%3K+To4%sN8f9-v}f%w3StyXfQ5!dI_D#`XJs9v^abH$ z|x#YF)rU^1C@hf!+MmMej(Lk!Lm$*i3I{zEErDxhU%Bi3_aKG@lOpU={U3HG2;U5X8 zlLBeV3ZY>I>B)>j2@meuf!}==ysO2Y9u&GBTCXc4t8mg&OMrHHeH$lbHxYOB%jeGM zDEo}x)zl<;=#wQ2xOCd+J>bda=c6i}3aURgI(glz^S>A3VJvwOG1c_BzehexBQB_c zs@3Q(G;e=*eu(+#SQP)aB`5#JS2tGQ25`e0B#o90-rbQOF4#8uJK1^Pz1a@kJ9J7z z!saQ_DrAb*C<;@Hw=A89UCC zKPe$QErlz#B0gz?qia9%$zbx{X9Wyj1(M0zxn>3h7QvznnO`_!W_vJLsQpnnLB$hc zt1Ld{T+Ue2QF}bv8c|rYOxD)R?q~=K(}fQz#m!C7M%P+v3IE-+%8}B=E>^FH-y)16 z_+QM&;nIJe8_V;YmgWJ*_|WdVjUvaUgQP2W49)28g<29I;Ji40pn|x? z-Uw2QSLl(=K_D~nE_jAiBanhk09gYLyPFVMVkyyyo&H6`ZCeZ+*>ujIVrbf|%NLD& z#Kz|P@?E{LAXu-l*a{Q$dQ*3leHQ1UVo82I{5wPQJG{CZKOA*KysHWsmZvR6Nx|YD z1s&9WDvKX3q|QONx1;*)$ctM>mK0SgJEy>y&HWJh-7)v**!vwpWM(>BrhS3*#qog1 zEAU8VK~OOdR2)2Z-ZvwH|3Z_PAgIc+GIGt))ST~rKep|x#zTprKfCgevpQnsE88zy zIlR9C#xOX+O|33nm&gP>_U?JEEFY#uti$)$MF_dvlGF}qu(@;mb8N&CmyN_lwY);X zV?1LvT73?&?B_lL*5!MyIku?Sy?8w!zZk8RpwpOkanVzs4{+Q8_w&Bu`uU1I+NaIY z)`+7fgU6%weAqH0Iz5DU+3wa9dIPBZH^ZUd-j_nh-}KDhEM8(_fqPQ^8YHU4-|=5! z2r1(Qcs52dn5{C;;6rQd2eo{c%l2ciICZ=0q7HwxN<*djJ&Z?<<1sQrFE3e(1Zwzuh;b`n<`j3MQo$hi0sZDoEgD-W&H8J{~_`YJ1N9uI1 z^xbDb_R4XbcDsPfeHIfG)u#9Jn@zH|5+qOlTdD9=t^4w$2;A*aoU$Woy6(Xj^MJ@k z1D4LQ%eH`#Z}M{JG^!~ChmjkHZ(=Xbp#X=?oPV18x@a(j-i-QO%Ss{9-J$XOPQt_M zhEI4+!=Gz7Bw<_a@0n|!r-MJkcP*x*Q`mcfiid?%eEi%oPv5uvvw5K0*IyM&Z{MVH zym#jg^kca^XT;PrCn&6Q4G7SX&R3w7K_;`Jct3_r?tQBH4$vC*wfPagiZ_z(=Fm7# zTSdQF*aA7T{m4iOSz`=eo=xlb{!#6(mah{UwHVUe(s&aOSC)-4iXbdjv4bJhm3rJ*haS~^SDv3rbQ$cg5!7_F!^58;^J)?#j zJ@OCf+pHoD0C-!Frz-Fr7RVp#Fq|WjKc2baBf&I{JPKf&(v@xN%fTXlGNC6%NR=VZw;!-L7rPiVstwh)0QA4O z-M-xQk*h>P| zjZ^lGLR=YYf-)Z;fpXee>$C6JHi|iY_25G*gGS{QCSj-Rtw&hu#n#s{TYbS~+|@V+ zfgsj_9K~DMl){S=_g|;7KeAMQ6xY>i-`Xp0M+oK6UF9j|IGwL&CRrRzt96QMwb!Ee ziw#=5;#3n>Qp5+xe!v89=TUe~0cWg56ai|?{JwKTCkaRF*7ljixkJg*^GydmSjg#7 zA**~5;M787jdb|^j?&3Gn~udPiC2%!kE+E91epExw2~9!AK$f>(P*z<#L1~2 zSm;#K5%r)sZyYvKuvgqdb_;zj%Z6J4kTG1**qQ12-uXeC{WSRkviN@(I7%&DcC67_ zq3!*R;uLsTZ$@SYv{>(kX-0!+q=*=W9Now`R=ytr2Cb3Z*= zcfY)dpOXz3cVx+2mpfIyf>etJ^0eO`1cUb%K5p?)8Rc%oXI>v;$9;RYh!_R`jBD?J)hw&ekup7H0Qb zlN9piJz9~Kt)`OI8g8bI#LS#6D{rgU3~SK}&CUHH;Ot~+eeMkShO@Ws&Ez}s*pV$j ztuY`k8gvLgd)&8{P7xSmhX!nFwZg*@$R@I(N6r5L-{=-aQ7}A8!i7WQ@@&PIVi%Q0 zr>~t`Y(>r&-S?6lNi{y8@vO$uq=+w*07qM`o-q5Bpo<#f0^I;EXjzqjd^)Q6eaN_d zP{_B;2)3x1HcM4OB)&sH@21{ACN1b0`cBVO8(!A1buFMlUzzaZoHr6XJIBvQ(wj7IyO zItE4GZ{3FKj@d^;{PYGFqPU%+>!1!t-(a#tQ7vSbSrq?qyJ|_PtM~ju;?YH*t&6zYuSR*vH!!)vzH$|kQ%#ez@j_$ z%UWIfdowG&z@<_sGdIadkMF*&6}xOu?P1&*)rHOVF81vjzkUQF9n-CTu*tTZD=;Z}|{ z!n3Zc&n|t^_Nt5N;NU!C)NZyKU<_}B*yheX{&;}H;!}kFCCf^|T*mvlH}&boZ%qpC zIB9I~^n=*s6+mW~e-prBGBwTyG}3#vByxp}2vyH3t4l>(8-hAGIPA7w>$!e5@T^Bq z{u?$0VO()%r_nZ-Mx*;|`cw7e!^IvSJVrQvyLZx6$Mu2_-vMSkZj71w}Y zFE&Z0zb3;~U`eK^-JwbxN*afa_*cV9X&TsQJU$^)d2NV*Ls`?CnUPVtH_vxe1z{cI zA=Bq_e^X)B9cb?OWAqBD4_|X{N@$Gx(>LPISosC}yCLz+|EgS^6u7S?5S;&*9K=SO z8X*~m2Sh#)BzRa14m*ePXP5A!Ejq4QnTa~38xl}wp$b-KP)H@IT)kC1z$CQUuh{oU z%c46HHtYdH9Np66?5bxmK6|d-wAHub7w$D7Gqcn0TAaO(a8a(#SY-&SdQat6bubMo z#EiqHny6D0l)^Qkb}th_a}Og#(O@rTmy)`}Mx zHY5D-54?$kw~E;h3-mpJDdrr2p7}Ri(B^t?pEHck-io)E&}kEy!DVkqr%rR)3tnDY6wgz;!q#VdIznSjR;fQ zhY`4MX0sw`ZMG9}Z<>dR**;7b47-WTqxnl~TI;2{X#Q=Jv^0%BUKT=3WIoge7RdsN zPnlmX;ZN34#jLAoj%at5z(3x*E_iLxtQ#)exI_3Gm#>DPHxsqmwPaL};kiFZyvZok z^QrSTU+RM1t_V<Ub3d7t*4}(}==EM&e|7NBUg3!GYsiAKNpxEnxwiHN8C@ zCnS6_Ri8gx%k-?gLc4M6lu@1UzsV0J$}A}fH(JtF3KHI&YM5#1y^Zfv$xhe~R(E@5 zaa9=9t*n*beR-18r-V&-j`oc|z3eLF6r7VK$dyJ#MjM8L*!t;3wGHE2Wk-|>zw5^ zqk6M^43ehc62zRjjOm2G%rXv(KjF3NGaagdp(ENe@C2vAtpFuBNPxrCOpsV{!9ra& zebc^U7hUMR&@lDgaYO*W=3cFPA9&CEQWb!RP{kysR4S3iU3)VgBmOMZz77AIJb(wB!5xekBKOWNEPPQ(v`6f2UqDcFTts<$wR_ zkrZW+mMFcLS#??F4(vff=_;%;-B>V&90lQr!$MWacGB_&kjN;LCn96ib)TK5#LuO! z<>|Bv%dCG{tYtT-1uWWX8m9*dGH87J5SCT!QgD`}`F34Qm~SacF4K1^fmTKO2o#n> z#HI2!E%|d$ee^}HY|Hf2Ifl_F@oBCZ)N9N6hH}5L3i9jC0NuXJ@3Uh6b(<-w{720x z^;_VxZaFgZ?UD_41HhjY|1dy)B>i-R3PXDI3(2A zIN4e1HM*ZB=z6>-Xua$GVd2=4_+u|cNQuQu19Um+sog8^`gTdy#5 zP19MT>?8u%hV3C1w=Sw6VziFt0tq<9H%NDLged4-LU8-|w`kFf=pyB@!B~ZM;h9cJ z`Nvx$oSdB6zxN)NB+5o`s5p;p5!>HV>bHLu!A|2lrr1_qXc2kD>Kj|0jn(Zj6u>F9 ziOHY9?3qtxKRAz-XCG@XUe+5JNtTd2&uOMi6kkMk3tEJB6*&9+Q_;`5j|)bg>dc#Z zx3KMaZ#^wwLD1}R^d)?%5cb(f)Ys|9IGT_W)-YaF!eO0Qod_t`S)g;QEv@XWrbju7 zM3+hwgd>S>FuHm%S)%oC<+AUJ{+n#Qz966in)#%5UfpYQlMNQf}h~XH!?DO^XR|cW};YY<*!OVCyAHUKIUtxAsONQNpTX zwzTVqO^t~k!;t$vY+XF$n1#-!f>|#wMNgp{H>>gkssZDZor`5+zBLC=H8-lz3_!d| z&m|`x4zRCg){2T`BIPQ(r;c_k*j>L-v2ZORvbWagQLh%?KEu>Ak=(kOek16bG;abG zTh%99g-Dqm3X#ixyCmIRoRhbl#WbYQ;WCV9&#*6@B^Iy(nPp=;inZ-Vt5!41a7CfY zq_fIq9VXFg1v)7 zOm&f9w|6*AzNcYL`gt9W+-m#XJ@UJ$=1$y<-f})6s_Ck69YF}K*V#?Jvgo8FtiS*eur<8Lj)*OBpCJ5oAU;aRP8n@)M^8a5EIG`H zXC=ZTB~mjtxfR0ouUIaVi*sQr36x&nO*sFff$?yqcu48_j*LOeysDL(u*bf;+uoln zH3rGlDdJzziZe8-Y)6A>d{y6_&QyI5nSyF)V~wLdu;sv@cl31KNM#7Wd>_+~yZsH8sgOhyofczvd{ZE^ z33G9=?&=VbclyE)dTNKRoq7lw1NOpIBkMgllQ|mc;2YNf`a?JJ@~}U*Iy!-8zScgv zMl46H4XVUp*V+YZTCh{Fw0ya#9L6_hlYwl$Oh>16&`xd{{;o zrZXcoLVyO$G=qbdRVF8nu6{J=#5S~z8uCv6>B9zm|AKVuu&sGSsA8H1^s|K_8SRjI zmQ6&-5?j;G^5LyXvmL8SOEhx&@e}#(k+69iB?tqAo-VEbpNTa7{oNaAbdp~SGpjzT zd{+T0Qq@G9CY`AcRi1Z$p@IJI2J3LcMmh87nVrp)$-w)NJ77~#b`iq~4aj5^#Ir8O z$(NkijlIiyy>S;$lxOEY(Jmlbzwt|_G=}Do34Z?v-9v$cZs=(`c4OzS`SjyLt0H%| z*h+EhFQH$5-_7Tr<>}BXFl2Sq61>kSQoIoBvNe;fi71YkW_Y_%a_NcG^XyCVoOCZd z(tbB|zhEl%*<8te!oTil0S6kQDj$f_dTe5^|f0gt7%=fZ`7ZrK;s2VLSXm@gBVKc_bSb1Y@g6aI7V<{%*c*EI1)N=j+AT^k9ab_mC{)@rad^pZ6{C!69HX z?_?$eFH#1DgYc@?IbC_{-G+j!eU^F@DPQj&AK`7^`0_SVl*c)>;dvKgE$|P|p#^`^ z2?}F1124C$zMRAa$v|Pk6vs{29{^qTnL`@X*FxmFug6#u%FF4YkkA6zacFr*U1i;b z1a(UPfB++VD0x5%*{wLtpHN`gP`9qUp)I2rNyO${u=+;p?&l+E$7)h1qF6qt z3v=p>3bU`*MDo){-zuOo3AD9>qofdl6C)o-K>5HW{+0P<3bjKX40L}w&YB3@$RBTt zWkfYpl@m;AT%SBkwOrggfJQ!G051x`Akai$pela-s3j?7ErY}<8IJ0_#^aKjvMea} zHH^`5Xe4dLn|-y0V(jePS|fm1|Kv*{2OY=rB_1F7@?@K~f7Y!FdPtj2Tf&esSXLre zy<%2GQbytN=o`e14EwE-s_;|Q0+P%Tdq-s2wGR#xRrho)u@s+~ zciz(hN+bsT@Tor~OB;0a-JC6NCklP-$nWAmi~mtGvF#r@EHzz^Aik)XgCywg&7Ce8 zI70FJB%LfFYNatnjIvD%9*3aoIQj9!!t>Xm4|5Y>pv_Q}17`WBWd*TW6M<1w{N${%yVDp06DbY$G}FwXkLx%p#1} zw2z2EcztmklsPBIzdwqyvxi=Hm(UgGZSLfOSuPD~hH>ghZ)z+XK+tG5UN?5xdl;Y) zxWRk4=w5t$e9++?u}bsKijZHZLfiV=w|sEM(;S(pLVux0TKLjikrAr zRjK(LdkjJ98{Qg0bdnt{_ip`S{XnXvyWuBD^s?JU4S)7dzd?LlJo%2=>b~Z82CFmR zqGrh`6b(bh=ldrC9ert(gOHqlO!IFd<%(EB7m)Fvn?qiMd-?J1n&GN|@$Wga_~t#{ z{As;j86we>cBw&ToG)|>uM*CPl0*0mpWnNATKU0-23@-Xm5HfFpz>{v&AlnESOu|x zM`qvpr;~pL?A`E*?*Uova#<}f@RXPNwX+>4M#;v8RjHaL-8}{6|0pXqvk2w2&r!6( zREFsO1+}QG6J^2UoLCwB6A2K?mne!Uo_O8c;tTggV)Nd;=|pZH6e3S{y4amZ*4uNa zLx3AcaW><}jb6Rw6DPi#$saFry09=K`s>mj6iuriIG=QD6Q}A30e^G0-nl=HI(|U` z@kCY(1?f(6CBnu@;`&j{Pz!>P9zUw3`p^k#e@<1}Y=6x{K$jzr;?MH#4WY~J(D|o= zCg)VACb)?5#yg=XsJDlOsm~d92!KhqoId<66uQo&_}YC#*w4gK8+FIEKZG$tR|yn( zr^ZBETRwqToDloX$6C(XZo|~5pZq%o7L%_M^M^;X|CC%YB62zN8sUjhW2F~+4ViSd zhkJE&b&iuU^GV}v*taz&B;%)|ULR-OB#qcXRFloVNkqHrv_m-T9xem^m+gvFbOp}i zy|@mX_TIZjS^=2E;bAi>K2`6vwsDRZztJ@nUYN8V-qvoSll~#Boe9bVcWFdSR(C_b zs_WTmUqQ8O=@sAns(!6ZkQ0BkEP{|!LO?hy|3Ue|;NNYzch-;Y7T=*mtbWW-;%k?# z_tV@=8vn}9&!0+=I~=G@%QG#ZFu zfKCkQj#qt9@Bhh9Hu`?=Pf0jIXu#ILb@=I+FNN8Y$E}7Mn7+5Nz`TV1xpRyV1l{8< z?im_x&2>|fi29YgJjf^{G0ElbdnUG1dKRTEyT34=2gO#GE2jq%<@Z>6s@`pFBdYJ` z6aap zlpP+iITX=zU~b?(cQ|xP>UFOPqap}#ZnKUEDDmY+)t;C@_cw5Ke!naC1qo}5xu{>n z)oJ^%1p^_C+!&Mh)7?yJvf*~0;9A(ebbWu>Nx!?#>C&z=ICxSn=-z$Rh=XN#lFL z79wKa{3r=#yy9hw5tEtA5hYINi~f4=Y3B7}f71R3U}O6!AGk9IFy)X(l%~mXMO$qy z{11XCx5z0+4ti)SU|R@^ejo6cr=l3B_@L_w)AHVq4K))j=91E29yyzRJ0I>%(RC$n zotW!{V0@ok`ytiiirhs|r<0CHOT(BF-TPu-eH$vq4W7%vN#k1k7C-=+U6!kDz zPeSFLXA~(}b0Zcu8+^0`51V4M7i-lD2v?5EcnboaQUB- z!IgD>d+9Awp9xhoDm!2AO^B0OT&DNz-O7Ltezf!S;OA&ZP}0{!;z)kPbbxHd-dU zdMu)Q8ovJsfG>vxv5OmOBF@nvaofrjnV&xna70*}(3oL04>3dz?Ok63SQ_D}5V;ul823EycXO@NFmcsb)u$n6mZieb?ghCrwBxUObz z+qpAXg_}Hb4g2;z6nkG_z&Aki*Adr5i0`UhkxtjS3!-4g4c2&s&n%PBnq&@O(E8`S zpJm8jT|7sI=983WF%Sy_bjX4#{>Rl_Pj(*b6@(ioK zuU{cWe8>15f{v6yRzKReWyK*`dDonv9m1&-0_3CdJk=2dXKMgB+$r8;YNdOk>tn>v zDKhp{6|d|K64aNJQ$r1~)JC2FANvrd!w3byqxR<=p26pRVQ+bnDf_3xzic0BfobBo zkjd1=YlI}NzBDa<;zmz7AL#@8FLj~rw@MWcP^yyQHQG=vs2XPZ`&=O2fLR@7G?4lO zVr|9E-}@#vOVLW{IcB*JlL2)|;@>;XuXm8j9vTczofBjk$~-QanzHIy86M39_3Dp;pMwG6t}v$sBsbrqEUcv%LJ>pTxV!1H%?Ln(W1Y*aTNNRA%=0 zJ_HyZ=6EZ1IW05Ty?n(h5@ivZ>e7qdRb%R)vWWPfcYvHllK0egagIDYrvLs5)S2ix&nH7n=WOZ$)1a6WRF05MwFSm*B?I;|62;JL@E z%H>Ix-I11Ik~win<3&(T7j^}A+=zIj%~PNfXm6DUV+QVG&a_GM3_b5-JQc25w1;+w zTLQQ208UQ&*dOMTfx{n<@)d~hJcwK;f;Jky1LHG%c|er}pJ^DByEh_sQ-W=8 zu>gi?w|l5|%F)M}oZF^~#}@LsggeUXNTVW3NxUMgc%Y8{gHlk_j4$uxa-r03tVDT# zJaAA4_n$GB^)|YFdy2-?%eitpr)Fha-oD7-MKf(1vuCA&T@*FC#_WVK9S3!6#2M* zOkz9~)jGw+T<7+UF?`7{9I!Qr^B^f`4UpoTJ@aKIe0QE0`rk9zW5gvutW9L{;w3ql z9WI|8dh92`GCFZgk~hKbL6tkLc>JY0-v{;5{p2k4HB7d6{0%2JcbF-FEI<@&`XAJS zw`{yz+ctDC%aPPJzcaBN5Ql+?XjoL0cSr}s*T+u-bJ0lS^98iO=mXMXc~bxct;)_= z?N{%F7a6%BLyU`dZ=bkeU>LFQE${YX*X`&*so0V-3<0wa{kS3=;-8p56;<2CpQm_b zk|ZdP+MJkdA|>crX5Hp=+H9t$4CV!hcHI%Dk`wyOJhaFZDLrx3pED&@+cntMYVKod191zhIpKzzj0i7*G8-?y;0Z5Zo54IZnn@BI!`9G_jfHf;eL_*2gvFw7ItT-Ph{?ApSf7#vc_{%Z#!q+h7XKZqaAIWP= zanz}iCK9LTsVY&zji2>iv=`JSd+JCK8$}&X44{q-i{gl*9wbwWnQygC>#np-xcBq9 zC##10oM%=3Fo7(JvuaTk$KSfneU$<;WAw_fAN1P`6W*NW}%J;U~Z&18KjGCSK{}@c=jg zslLXik2dM3M1o=0Yba&|#2suWrD}Ia+^LA_H#uKNYUuE4d%L^UD~I0vIfWIdn`^9t z&eeaIme{W!ryc+CHYEr;q+>a0+#ZWwOxLb7c~Ap`4rKlAn`WJky(5az%vb zW#KyCTo;6A8P0Bak;e&RN_97;&YM>{e@6jam=6s@QcQ=u0Ewk-y=nxpU){MM;HIH$wHuq-7(dtl+MWcFPbO!s5hJ#);(W00U71TCR2$+EG4 z0J9Y$OutT2x><_6i*ecP4YHo(YJiy{An5#jOCU)%x>NU4+Os>q3yHsbL%q@XUMvg_dX5Sc72HWo4%Z~|WUJR6gYuUR zd4}5mlnmA9_1G=*X%|76chxPvm#0(!9%^KB_4D%kz1X{B{VH*HJ9301z`U+fwEvi^ zTZ;#ePoe92p2*n>SxM9ezzs-jb`J%AKzs(4RrQ%`yi+6o=8OmW&0<5sQMZqXOx54F;n+zhgBX^)goSI76`U9nVEEX%AR1+0I_QD zPHcY-F8=w^VNX5VjxF`9(KR}efj#{hM%1YErEQH{Saz9gLqI{iT>!}f(3lx2|9tQr zHPI-@CID>j$>S<>0yz-~9v$#k#UC{?36(I92AE)(BztsMD1FIstWoJ!L#&^jertc7JN)n zzAPgA!B+Rt3{tKm3n}a9&hWQduP2s{GcvO=PEcQ4Sa)O)K9wLnKkB`#NRc7(#J%Sn z0zptM42~1Ko6Atj_zBc8-;)wH*G#J2VoV)yk3T>8njf$oGg``B<2=@wo0{q|WOr?2 z@yNO{23_;3Z*8ZFR1=P=l-n)-Z!>Np0a_U>LEE~7DG|oiDGg8r;HhWk7#BRE2vvJ} z$3i3TZ?q=44AQv%DU@dtfjH`7O3<-oIWAdna!-btPzqglW!S!j%a2GOX3v{Lp5Cd; zfm{JMTYm7R?9GjpLuXGoPqV*;nma@H;GbI7XHU$Y90;~8JoD@-(bfWXPY<&kD;ZvG zgw-Sh1TZrx!_YrOrv-AkUAhoH^@bW$g9bKy7CSp2pRBnHFc5k8r2PIYj~~oZ3Q8U= zw!Le)`NevsI#`fh_9i#}au8Tw=al1RzB0jT?^cBo8L>6TCln`NDpFgHxW&!G`PO1MbDkeM}3fYtwUG-L&c*PT(>88ovqgO3w%D58{2JTyVF z>^x#^wqJXdP+XJutpY1DPrYmKWs%d6?b_on>4fVK*SFUbsz(*D2uk;61yKQfye~1r19+V2dGYC2^5nzRMbim)Yd*(EtBCpFY(o#VJK$R8C5fl-pcS7j&f}l3S5W*j&b3M>nZtjuOf(_sVs-Y>X*n z<+72>W*19~xy)v6!(4vvsm}NN{d|7?)uTVw_P)F>&)4($eA_!`Nc!&ma6WB#T8n`(AaUi>WB9~+mdQdi=$Hv{T zE@POp195ldLj#DNjE5^@4Cn=3x zkvC-^2BSRzI<^{`u>7xOU?#!lX~1^|zq!%wPqx7EnW*gJZXlaV)9&8^C5s%)0^q#9 zU@hGw--$`%+^FAZKEh?9cdq~sE@03)M12-~&_DcE?N&@r&1PIULyRNnDH zlV#6j`F4MzjrV7ZM*6ukuo0VAX=K{UlB=%qglEbBDN)nJ&!t| zZC5RGHu%UDbO>y}2g9CVbUZC|Im za^uTd`3>0=2aT@1*7)1nL7Kh?)o)kQ$=RmZYqu5Y4?KutESfETH#B!$B?k5&wrZ+L z#+*)^lKJ4h6$42L z&bL#rIdj7r=G|X-1Y(${Dp3NOK>%#3kUwqvt`N{lJIRYz$)rC`b5$98IH;gonwpAe zDo2NqFmQ6GXVs05MgpXqk|h(pZfyz}8tDU5=0)u>s#hJe566UeQ^W zf3DNJBzxU&Y3IsUs6A^=I~N_CZc7`FkTRaOOJk_zliG z5mBL27u`LNvF@hEJ8G`*va@|pVI?eGUEgk!t&5eZ^7;+#qv_kZ*U3P~;ZqT(S;oiv zjxP>UXQp@f+}XyHbGZ9_M^XOAZkm&iN!{DH-<)uIP#rUFN|EWnw(zmp}k{7Fy zcv;51;31^EXzW1k+B+yULk+hQ`BrrCSlUAf<&!qN4}(E%k)4l`;2RmJj2Y22gT|xM zGxPRhSX;ujL=}H@(%>)B{evJAkb&rFFph zhw1g3kI(rX1KGja4O~Wbxt~Kn*cwvTiLZWw0GuDx=W5xsgMp5Y%1uy%Sj5n~UD$q4 z9B&s1|1gI4!9F^X&!S9_Pn-jbT=}vF-Uh%~Xx2g|%*jVkO)7VMfCWgqk$fU<}lrHU5tR*rl zvY&s!q}gja;z4H2L>Q%NMh^MJgZO+9dxM2vN*}nzT4F~&K{4L3TNV{Jt-vcr`R77- zGpyOGF`VKSP6dc*OO?O{Pz_(NpO4X8sMTJ8uDuN=8$T?TFPew}Qc4>MO2X|2=;&3F z;GU@2{v}|#4_-_L#*iU@a~%mU^d01zB;iw)zSM=b6i*KVp{olDm&Nvt3it} zB@teR24xAm%P?8(*4LF`lCbVq-%_oe43Fs63bNWIL{j+uPA?(U^P&C>qs4Dj>yRmq zt`un#Y>1A13?P-Y$>Wcl!s}WNJGI-VIX#fsl-vi$ebuS@-I2TVv;CiiruayQ{(nur zlL+~U#>OTa;Xmo}>*&5smUcKb45j=sD;}a88k<%(Q43}aYWNQQu{4>DEX#iXZ4ka+ zJ$EV(J=P2_EkW!>rfDIZVXiUTQ5NIWBB2lQbdJbb=%H%bGS%oRE3b@vq7`P?fOl@FuLWtNlz9Oc+e2S;D zb`A5F6ZKz9GQ}yWh{bmGo&a>KV&;K1=v%D<(3@RLL3lEf2;4N+F2YHkpeZe9mvfWp z>e}Le<%Y8$H*}3PGn5>mbXhRQ!WLhU@UEr1FOEDt?fGmQ_4J<=pS>*R=f0BOSx1+6ao*RJas;dC3mXSj`RBHQvSAPRSS~ zZ|!$e2FF`g;I7^_6VHhgT$cSi+421$WQ0!^VOE{*PmaV#^d^&WKNYu9{gD zN82~6(t{|gzL)GG+gG6G6fYnr*ObT`;h0d{C-5}X1Y`tout%vYH74hRCQFmC zzR2?sVh)$zI{`n0(R{;2KJJgDvPSGUO>nE0WkoLvOBQeR8LtC7cBX%sHr^8yQR$4t zV>FW>@_6MfYo`D83r(D%g@T%EWV9Meyz6bD^}6Y3+|Eoy#9$2u{LA^?OZzyVyew{+ zk_7LQz(1Tr#V}h`h7{_kud>tF1F}PwtE+wB=Sj|}S$dHG&1&a(ILufZzS=ndzPSrF z)%ZUDyyr0pb-QkX!{SY(r*daU^S^aXv*HRS+{0HcVG&;+h9fBqI)m(TLZ1Iyw65+J z65V%)m8Wu47`uO1n;Of#+g&4PU4iTMwTgY@>Uv^87tBzpAkY5$+-L)1^(e=Wn|HD^ zc+&&KyvXuMM?M{W#)v;#I~wt^`ReRao*E8~i7hL1%3PlR@`P?j^k4_`Y9t zPi5!Cn&V>ChWr zb%>E-_kB{Ro+5B7yUtdR3@}tJg95N_qtE2YR7+>gZ-_ebp%>5@^St1l{vN}Xnjt0j+IfU9}9y3b_Meq2=O_8z~QvK{VV(F zkWc7%qr+~p6OSQEbSeC8eP5HH+9h1=$M=!vJ7@o#S)yv1(x+RD_XjyoQaJ@YkJnrs z?i`ivLfj53Z}Tr>zr>VYd+nSDHRU4T&8oWri#wuXsi@p~|4LY>DCsDuc48GN5L*{l zwYq$ih0l2)$4?Yq4z_yek=a#JuQ40uZF}!*i{JtZMkZ)UuK(5xr^}&=2Ju(_S;2&(w-d%v_9atX>;Z*P5MTV3MoIZ{hVKM z!LyMBa^6(+A@hR8DJG)cPEDq}4TA(oTF}Njao?kLV!vgz_Y~U8(ipRgn&P=%jnGl+ zt8wtmEBx3zCgi&EF2IY?`W&OH3T9F#Q!OOn%S<~1rkzmfp;K&K^I@}M*BimO-8ybr z#o~lG5o#JAbGbQ2Brh-hj_peC@t=<8K&(Wtnb6=r1+KZnr~-nLLB3~dwrR^O^h#4z z>i!4~Rnaahq`WHIsySpp^e|4q#kq3}VHTfj#!krK)AiA@+vCKGn)LFoz=BQc-BRaq}I zy;?G~rn{+EuPY}T22oKif(~oXW5q0#&Y@b>-+yF20yASptM7d1UqNc( zf3Nh+f<5*-?$dI@zI6CM7I%y6NpDSrp8G&UG*L;zYkL{}U>NHSpNLka<=>~P7y2B# zf4}s_yG%_tY{F4vwOO66vTGaR$Cj=r8mdjRM?(2q|Vd53CV~>(mXz!vKL(b zUz(et7t0@Rh&hQ0Qn!l8a+IQ%4`Z~FZk-n#j&jk*UAlL=Xo2?kTI$mSr;aUkdcRA)AN;R}ajMF0 zojys~oE=7rXLl*_e0|P;LX&B5uKjROk*kTuf_s=j)jubD8{9Td5=2)jRwUaw#q}O# z0h^Z=xUySB%TA35C5&bUgi?gsl4^ZfHjU27CD`~pd47~YB42*&hp#b+<@IN?Z}w>x z>nnnrq}6P^Uq=N*<>wUil4M!DZJ&D!qE`Gx;C)jpa3wM9)CHO*Eu+7n&LF{dI^(xj z&we3duVVtxr49zV?B5Y+`QGzQ|W*VDq&s_J-NXGcEdLwi99O}eORJsk|pr&bq60bd|TP22n;B7N#~BLg#F+ z<97>PLHzA1oJ=#j&(9n6PGXw1%q40%%^AD68XAQjw!*3SY1<+*$Ua%-K< z+M^AhzbL94Xo%p!ficx>-4QaS{ zP#@~S>{tqZ@p0O;)FbalXwE=cjE2GzQSEPoiDmTDp{1*l`3>h3h|By!N52y9s^P=H zt5DUT`Jfyqh2K16W@eAIqhURQviJh6-#z7Lp{vE(<^EV$WOwM3I92=w4EYu))*#^~ zRl~mSqtG(fQGLRj()1Rcsu7{N&*n9O|L_`T=K2ADmdMf=PfKHeI^>HhKx*yh;5gCA zOcM>I;QFXDDTtKy&##iQRGue&SLgxhPo2}j#!GHCEj&lH(33 zujL~X^MKsiPQDqV-Ctv*fww&dzvYa@U_H^a`2Oeu)w$k~YG-Uj>Y?0XkEje4q6sqvoPstjseJY=?UP z47b4+OewxAB`2cJ|6Fc31+L-tTj(ZHm}#t_@;V(IAGg&&W$@}_QeG6Ar?pkyN;e!f zD)%T(^;@QfDqq{J7Ggz#ZZ#;`CGsNPZ@*=iX+TD+i>}#CHjlBEF*U~L#D1qfpwy`P z7V>auvrM{%DgZ_>^fR|MpPA>07d$i&bM>z(OVx#Zs#_eD2XWz=@AYp%!uRLmqIG1A{i!JK`$em~;FapB0J4UBiE!BqB^i(2*`M&?K zeh(JoEeP}RIW2LkQGgVbY*(M%BHQA@tbQ-fak)=Q-+k>xOJTjSwnKVD7>R@_xswg} zf1mbAqiU)WFE@PTnFcdn->W-!dppF^vGqpdL*R}Peqk5uiMZ=<6us0{VVFRCML0L& zmC)jpTEPM~s;Go~8Mr?^6^0G)i)<96Hk#ZE6l`o+2t1c&cFU=AX%jnfz}s`$+uaFd+f@Tb$G_ zm*oL;r>AGR7x9vCVN7r_?}}voVomsPp6#J%9SVj(>chV~k6%TzBCh z=C+C%00F0~F9}Bq^Bcv(weKBfa=*i0?xKB)S`=7dh1dD4Wv5u~c!Skvy!RcMPc+{t zii@=fCzB{5PRb;Yagaqa%f#kKO3DLcA9J5j$m2L_%HQIEf-=b#=DP0qBIs%OmTlTN zP`y$9$e#{4UNqZKeAL5c>qad8Z7M1uPL<^B4I?lqwUp_0HDGSYp#(9t*Z_pGr4VG| zHnPqsOUzj{Uo~ZqEvxOols*q)jyiznbKbJIiVW|&o)W={p%}~?7kZbrB3dG?JVtNA2Ov+Rb+-~gz}loC7)_$8(86Xob_vH zzeFp4vBBhXB`|m~7MDTmUruxVHaylb^ElB`?1ucN05nzBA3}!aHU@~x z?U(5Y@mwbWwN@}D4I<8uLFoHh%_DkTf?tr~t4H(Gxf+fJ*yC=E%gH&%&l=QD(&Txa z#UokmMjEwT0!nxlmer2;i1>0}7xTAD^LpI?@`Fp*uM5kJ4wW~@^X_!y@A%NZVl+|H>W@p5hWHcAd_O~hcqco}b)@D;5pVsIfZrxvh;3dgcj@#q zH^DPDFR8Li@=2Y{mW#AAEWavu;i2fP_IGhahI9Wl1#e<&MgiWPjP021K&7%uWe4RT%%p}Q14vlw;LF?l}ug! z|GF%2-0Obb*$|`vllPH0P*t1gS}h*kV`e=DkgK=lnj5LmHV_`$B;!sT#>JGnDV2`Z zB-D@}0U&VPyJaV&-0xC^o{F1+s%ufUpnXnl$7fo8-Z(va4%CcJEu7;-bJv;rW$?sw}fM5RwiYGhQo5~f0oDuw_!g2ufIo$c0r;qGd_`(Ym z#KcT=Bh&Y}QPQ5MxH(vpr-_!z7&SRhxHEiZx$<2}j=O`dQ#$7qX!76Cy8*luO@X%{ zNBg7jb$(H5M%XrwH>Z0mi>F>AN;HvsA`8(mc@!*N*x+3Xs1$@d%7s~%+IOgK*$+k) zr^&a7EZK10%mOI?s|Jaq+Agk5@d;q<3&!>!i|@yI(%kz6-62s+9vGq-#RU{Z71IA? z5kNwS3|t8$Vagj3=pFP)1e)x8EHMpnil1@WinIq4(I^360Di`ow)M~7LCv{x;`-<3 zkivIy95(-3yIOLlU&|I{UbPW1`_7sDp)-{f=30BF7t%B}yYTL%fh%z_Iv9KbphRxa zog6UTz>Fh5Iki^)QM7gj0IN9ho2yw+EMN$(`*6WA>q5?l_S8sRqfy1J-C^mo`sz=6 zv#*r4>~$f-3p!yz2k~W&D-e0<$##jS?^5&)(HPC*?L5nJ;TcLCs@MXD<}RQ1+1vu0 ztKU2{Do9@ty}cqDjxQXpPwx5?ORC!iVG$#ToU$qOUgXZX(xoA zWqxSCD?e*idgcAwcc13IKeIGmRzQKrlQth=P1ud9T_J7wGSeWayx_(}ZjfIy1?+c! z3~fe-tVo(imRrPR-x!sr0cf@z@qDx9{;?y_n0Eb!G*_G6t)HQf0p8cV#}sPrb?5x7uzPQWyjf97n-T&N@=K z=-BBE82;A@zuJmBd}ca?)D+?J=&3zso(Cz z0rI@%hq&Nn*OR<)4xhXc_U*%k=NvMJ|h3exoA56R{$ZV+D6G_-2e7{%aBpU^<3$ zNqrmwCa{BvgEy$NBASYsqIYzIh0(+IO28%d1bsRln#GCb|p#W&7aC1>V`)9o(yRX&f5CD){%STWJRLJ+_F z`5OMAvK)au0)0| z^LTrNRS2QVphBF^YeR9gssozoHjjn+V7lFps_n05km2|Z$Lx%aNAfGBJCEe1)-FyS zdd3fWtqhR3@iUpYZ?EhZ7~BcK!3Juzp;La!LLUW$bKS_-G%?`3I0)46S;gkefn=5+ zfTA>>*3JS{YJ^E-?2>wdb6&Ysz5Y2Ml9Ix{{x$t_-3c)>=9)vP?=!4Kypeh?<*pNR z-`?4XHCq=NkLP zh%oce0Li^do|_E1SE*G%{hFW9(cD!Zwa{_#Lbbm5s%yT1H@W# z(q&F9!mGioB8DqxW0ThQ6@|W~fXRAu) zv8__aYkJqZi2476NZ7P^u(~ zIsA0tGeqFibc5iUnK24p-M;FS(uT>oF=A}o2L}b+HNdD&ykD<~OM@4>?wo&I>Y)1_ zt!PB+L@Piqw`6>1gzTa{yqk(uXYc{ocn{vUuvOQf;)OipCXk51cLlqeJ0nW#m&4B0 zPL6u>j!~wyr;A-S!Ipb+H|TCTRcbH(@V_|!jJDftOJY6bG?{+`5)S#(<#_#>t{vG#2Cp7Hu zOhA4@O~2F9ErrBCP+}gIqd8psgOTuV`ff2X8M=k&9T3*W)o!UqDb++Bt3G<%af;uyI$C=%D2F&yiJKVW%b2jTPH_~x71nNke1Bu1r|(el{9iC(Y}~I%&jtB6^@-0^PS6R=YAY zh2j4)2jQ`Bw5IdcfyN*&23WNO4Q#6 zoyCtmCknqf1xU^>wBLjM%+5c!MqsL(5x1Y04OD1FR2qOCK3qUO2fCm%wy54Q27c0rwuyUKp+EI)eu-R-{RBt^sL|ov#v0-P!gL95(Y6T^obd_gVoHCOZ!Q zVGn*-y22R=T>RcnY*`Bo@;qX%)dEo}&i~njZ!l*{% z9YFJW)U3IgITykY9nI_1(J272)5mYhtygAud^x$hA|5Qa8ajPmD z%kRF}n{#0^e0rs{vQ^sN>{@@?Wm`|>71S4E$uL5<7y@)d++hhl!7hloET5v7%tp*vuBf)!spkYHneN&zEA-x>$E)&QN| zodo61+AN491UkCh+6Sn1*L+qv;nkGRi_BN}T8`~Iv^>SS73$?}@#3@oWrQUCl?wtL?1Uo&lEL5j z@J3H9Z_k2KR)2CP0%)=;H}W$Ans<4eB0hJ+-jcUY(r`AOk(-@KI_#)FR&5ICu|_@C zuGkzrppc$RQhO0*iQ$BX8EuqDtdd4!J(VgnXpvlhfk=s9_MFWYR-%PDt{frD*W<(8 zbymTj?JzE;(@+~OY9wNQe8jMi{Y_X1jJUP8drgP^K2ph@ed|-$$-dg8lx&7ho$Yk? z4U3PIFGxqyx{*N+u4IXM*F4IUNvi?JZ4Opo%!;RA;*H?ZnUx1Q1wEO`7a{!L>VCG6 z!#6g;8~lcF=Z0wh*MX98I>Xd*GfC3N{e-*{RjU%3mk)?=tTQXFUqa)zR4bdK0~1pz zTauS;Fj4bM<2>bm>x#FfBj^{MJJFn^InLd%!OItH&tC~GBznZ z{vO>~AH*nB$kb=W%g~4!t~=8A8`!M$hL)L3EoyHKS5v7vU5_5yb5mC2q3RX+nyoi< zYZ~`RZm$dbq?e5<&C}BSbs2mP z;vW55%PxZj-OjYYiawyaK3pi!0cD+vM#X#-xqob#o{La~1ja~0Mi0dMD5|1_+wZOl z-i86c$oi1DH4}%JSzGKhzw_G{dS(KH|qa8_01@Gfs) z+5~ULmhQ*x3PYvqY9GBHe{1EP43q9}7dviB%xfdWT-X!jC}qQiMurcNd#F3;W_nel z{%u{K{GUxs9hjv?$L|;~fY=9;T*+jsq4_Wd7*BPIIz$53=@*k6^A&HUSR+>9m?7Q& z4Y8y|C|3B2ZL`)4-?018u5>eG@#Aq_Ev;?@0m**!K7U8M>X*}=()~tY?hwE})(BoZ z%+B%YH$fU!eD=Nw=n*TXeyjRN$49^Fn9%PEUB<}y^Q6xEL)%k>vB)#9 zM$)XQ!*_+5j&JVuDRvTt;#@(msCa1LaI<7x5Au9(Lsb2ogD6$E`7r5ln2D+ct+0%~ zndR&Agz`y{@Rm>P-*|$kfs{=LMW)4grjo38($*CmD@kb#`v%p0VqqfGO~{Nd9Si$i zzS4zmz4i{viEGpys$lip2u|}ga00KiT5BElH5tbp^Da+#$0ENCu`ElAea;Dq?IJdcx(-#eSWc*3Kz zfkRC>$VKk+#Z<`L6#0i<_;+agE~gOLyVRYf+;7X)$W*)7(ea&c*mKsQCf82;W=h-p z_|BV7XS+g-;n;n45>qFy%+{5R?THxA9v?+)Q!J>7cYS-5sLmi}lkw<#kj%$f1ly*lrB8(3nl`$={7+hT)z}P?ax?z>YoTsaW4!+6&6_JeR@E&< z-??xo)ygHSUGP=qcbH4CWoT<|c!tu;g_S6S&v!m#e|3j@Dl4hrR@ZEva~a2p=y+|tIbv=1y6JX(Sr@Fl zfzm8``<2u~qsHhrEehBP5+G&lp({KON?#z>>8Gl2k>$qcoLVqt@r8YV>Zn-mk8tA7 z9pwKuCG`VT?P$!P8gAd>Imv|{F_;_X^Z8!b;?Gh(`sGkJec(fP@W8|w z{DUtiFdAtSYT~6o!M5U^&NeWc6JQ>!TS-Z*&UhJg zxCyR{EP_d!^^pH!5;d>9V7m)1#YqDyz{KH$7|mIMepJjuCX&mrQzbTJj2(B@T;`s3DA7R!3-@fwiz?;CD0L^nCY2w8)ruG&v7Oe%lX`cbT3g(@lP`P5aJG}vp z^}JC{6<~-ze(A661V1$iq7?4%wpPi&*v;gzQ=@UxJEPJTLcu$F+_+UITeStAQzEo? zH$bMz?RNph$yrx^@vy)wu@BjT>EP=bm<1nKMG$30j6~tJ3d>ncw<~2h{Qcs|A3Eac zG7b_LjuGm7-p_zP_Z`DUH8s$W>w#c%zSM-tCWI0rb@lY9mrAaM?p77E`BzXx!haW( zg_EWZb%_WcCX<_%6CrNXBgBSG2Y`_fu2w_5sUHt*6C>02(sKNfS%`F2voALF-9!y{ zZpR^;^UQT8V#`myp&z_v==7ZF-aA(C1arAL8D(;<8&0&{3hy#bnk3Y|6Dht8GrC`D z@19xbu0XVYLo6N@fhx~q0MKT9PrZP}@HN5k)A6F=(s{~7pIXY9%;dM=?1FKEcw;&m zXQ#K|S@j{WD(Xa1xV0NAXIgLno|A2s5nd*n#RrRCAyy7CjL ziM~6Y@tNj7qBrxwKhfKMmc&nE&uWhZUjWVGYfvcc=FBzi0O330#^;P&vG3dK7{{+g z9~Uv2SE%FpYPA-SQk8!jRGsdxx-46f?1p_Yvj7HJi2^s4r5>ROz|Ns0opH7*zx5EF z@03G1IalL0+b4m&WW&|VKWhY!t0jx+PyG*%(j{50K2MT^n8OY?nP(0%BJF}AopM{i zw*K^)n#RQ+ejIa5lb`9o)Up=7vb+R=u?VPWV**Ya_&NfGZh?+c;L_3(rc?P8~H~7&m)@Y-qJ9ZC>U^D3q*}af+(`;%~jl#T8gA> z8nM~>#{XH0NYJKQnVieRd*7#5(7RG51Zr~LnSfRKmL2xHGq%k*d_ZSZ5B+93LPW!8 zZbNs~yRuy#XV^}MD7W|+M8pFQG#Xzb<)}Lwy3M;+0?Q(iV{K=Ru1EfaY{D3s4kjUn ze}4|@+z7Y`@5iYSsZ(;8V19p+U6nfkDOC2Az1c>y_OeL0FJ2LyiiAw#GLta;3Nqv- z`?ug__F7)s2FV(sLoFx7(+Yqq^4_zv#aa7qN^Aei)S%%;88&k3;Vs(>E}>VDN(-=0bwwfEa`UHac<(yn>9Dd z8AgsC6rNER>mbr83~_cR6`)L3cP^x!OkFuTf=WJ5pwtO`9lR?gY^N`*)8%}A{F`t* zT(U;Wfv{g!+|HrMLi7K|oSgf$cD8Fio=eM-FGoZ0v$$t36A|64w4>=_XDs8Y5KAqD zXH2G%p#f*Em$`u{(x5}yp$%Ymg7#{N9t%Q^5mt{Bcremmy-89D%ca&*djEf`Q=@#! z=lm{Y_>XIa^vR*zL)CiP+i_RVP>X`D0a&0QL++Fd5M#NKLLVQc9}eI05dc9Gb5lLs zt)OxM2k0Dl-ShT*%rAh;LmgPv^(PNR7vmWvj!e#5qELGp=Au&pEClrnp{9|jFK+Se zOJA(zfJ1a+fNe{yG)TY#@T3Vlq#Dw%Xa?m`I#K;x^U?)uW@oxM7oZ3ET#R zfeyd26|Va~fz%eH%F~nVAls+Zl%w3Sf?AEzHbOFhxqN&wxih67mj>9XKPrOQE(?S4 zIb64y7M=TwLu{S{l{D1y2G%h!c)#`&ke@V7AEP}ABAg}@t5yD|V}N(EPj+5(uUdZH zl{_8{fPU`5)GA&8zxsOAAS?ff5BuryUK^fM)LcaIuc6IKehl0@ZOc$ z#aE(+84;s?egG(JoinT3XQU)e|NGcfOG(v)RNa`dAYpE_mN4-7psOelW0ezPaWjxV zpOGC$8X%jeVr=FG_cpjQjrO%cftlNJ;ry682bhWgWaf!XBz;{wkRwlar(bb#J$Y@r zqP#CQ`Q3!RZv^hEzAvl6py7KR66c8Gxw2c{{iru=T0j-F-=Ui|tXW1s8|v(Y<6|_P z^r||?ZhU^aue|v?%yK$(W9q)2b(L5YDrX-U+2EQS6ue7J`gZFZ0&isfJz=4-P2M(l zI0V_k*=PB}`(IPw>4eqf#D+T;u74hKM47MdH*hbU2~dWbhTd@l#(voX;6rie!VS;z zve2vxMbNwh74tKhT>q6&z!G3+Yj5n46c*D{`TSbFz39ydcYS%gxG7yD*k1}PG)-QrW3=1ajgI&U)R7_aS*;-pQ?TGFPr5AgHKG*36=Z`DvJKy~qp zYEc!X$w9;veR4SqovVPa>0dh}`wK@e?$h7CvA4?v%Bu3J8ZIIA%TPAi}# zuS|A1@_Mfn1FsZX1>JGyEen7pF84@PJwtS#ihZ%-=29td|eU6a+hl!x^yx!aFL79vLIRo=XH2L zYW6fhiZDxM^fRH(8O=0KbMw_8XQ?pX$R|msxKI{PkyUP=ZzM4oJhUzyJIS=J(0LjusA#r89Z+Vj9k>N zHCk-XKW~gQs52L304o7IJr8h^{@0iweBr^+P0&t}KV7y3RFSmoOC|)Y9_OJOM2Dl7 z5EO_+0BPl7b3^ZtLaGyOM5a{XG$H))eQ1o_6&a%*Fh0DLQGfE(L8jfFBp4u7_L;YY z(0Bj+QNu~NayV?<4-^JJW|UDn-of0yD<`61@}!Do{DYEM9B^pjB$E*sH;~ z>{gH`J3X-TuyD9s!!+J9T?gJb1dqtC&+2d=cp}o3JXLC2@gQBzr@0AMuyH?--Tv1C z21%3@?&nL!i5>GQ;xQSjxSR9( zIq%x5kL{sb)?E9e#FPe1J5VL0U_jZ$Li*!Aws`Hm*mb+!{xLzLbuL@tI(WqOS0d7tk!PDF`ME9Ft#qc@_k4onwfyvlINf&SQt{Fbek|2B*< zZHRg>j!;i0CJGN;A&2hLPsU7TX98t(9{Y7nT~rAqp~iHF--;_$HAJD7y#vS2Ke?R8F_a>y@#uip{4m$d6Ga9C{L zlm`yyFK@e1Qmro%w84!BCYNJH%o(y`kd$%P87NUESoD%$2PJxxjAlL;PhD3fdO$Kl zEy{TofR?xMdE~x52p8VtE?g3c-VQ39Aax$Sc6FRHuYaqgbeE|639QOWnNKvP#Kw(n zN1BZ31=GwRpo*SwVbfF@^%jKLMpRtfnL%upyX2Cua{Gx$dmA4I=Aw6xK(XT=N#)ns z%6pwhR4D7pQG%Kq!9odQKBGF!HqfR1xelSM&k_4>B?PF?Fab?VeQ{j_7p`5mLLgem zG-+DM%E*2^W8@sRCW!1CkgC(eX%Ok+byq@SL=+F6-+pHTF@5GMU_6_-WW}XLDq-t< zb@3DRSEQ51bA_HPgwJMu3C`v#eoWA=$^C*1-r@&Fni1&Ie!ZjV{yzFcNyT1KB+Ni` zJFlhS8!&t;F`yFCPI|RL(H;;LPMMv#8fUH`PopOTvG|>*4*bES4)>p1l`#Txq&V5W zH!OpTI?{IMe+oaWr`KvHg;w&tn2_haz-W#WoLP3a$KNQ&&yHjPlgi-1+#L5>Yxw6J4V=)q0(P)W5DEKI zzce5Tm`1|tOcR>Y7N0-Cc#zME+8tJ$9mM??T+X`Vk)FrvyJPFu-0pq#OKZ%e$pxSh zim55j5sbw^Q2@G!T>rhq-bkTc?-Z1%s%zT{)gL*BYXOLC8atf>9Bwe&>?huekssCLknY%mxiP+GCNf>yIAB%P^ zXH*LwhF@>j!TPhClRB_&%&MWkw2HQ_)9E!~Xb(pUYq{_9+pv$GOTlKV)GGwbjk!Xn z3g*&RbUTkwkP%Ea5;{EHQAoj;^J(9WF`li~*QFom)X(07@SHF7@s`KP$Qmm+GZR8P z{Ri#S;2|lqQmYEI8D9g`5$)d!n{tOybjUC?=3>Z<)xGvPv?Aldx zxL;;fLyzhSLA&KznraOudQu;7?(6gd>L1=bqwiaWCC;8WMK*D!A)e74YrH7dJEbMO zo7U?PixvCbMNze0N)~lj+EUrGCuF0P{hdyHa$i&pM!UBBj)rgMEL&+*bb zPyjnKb+)zZ*6#CeW;A+#36OjQ{9<~GSzqCfcIuqDe$+``I)}WpP3-PC6uFTS*n24v zoDN_!LoAnkhDgIquM6%slFxw>Kg;?Ctq7%nA>X6#3iUxw)oHHnI;+gJoj(b#8MwfY zU(^0l>1iOUWZ!9bO#RoGIkT?NDd14%nH8BrwJ{Z=ny?)F(g+h|@hM+~Qsw0RDJAgA z#X}Q< z&%ngZ5%}gRwQYudpB61-19JG)3eN-s;Chz73~0rl;(_PNnQ4E4t9TeS##z8d@?>4a zqkJoRUp0bLPJ z*J2UCH~+TTPyBwWDW3^?IF;#=_n(tz@oqY4n!GlDddx8=H7H5|S{RkGghTn8@urqr@EYn+>hw_JoA>T26(dPQ ziA7>8<%Z|6L%0_kVc(tGLO37sOp}A}Hv+Pm;A9`dwS;ovQGo z8S7eCyrQAiX_o54RqS%ltnf=#_h-Q*urASbAk3)#h)L|M$;{P)P1rosm8v~^bA=9p zK8ETG@vz1gT82?oXq@D>3<`B;tD=Yc_xQLooka*6PS9wdd$gJzu@9}f@Fb5w@jKrc zB&$RGUZ(^r*g<*i20UM`n3xX;4sA289}Y4Fubclr_TD?H39Q=#Wpo_7j3T1a#zqwo zkzV3}4HSZaf>MIyqnC)Ghky-GstAbEGW6b*UJ@HbYNQ4UMOp%cVhAK8p}dob-@V_x zcfJ4LTF-yX%F>*3cKz*r_TES9XCc|o4%(V z5Oi^ zTqE>rG$ ziIUO=JpDPxMxriO7+aPeLk%}vUSRvHjd;lRR~UQkgGaPh7XP4(Rl!LzpFCuJjO-RK zM^NFUw;$mJ^V1@EtdoIfL2iEHViz@p^P2c)R=qt46{B|gCwX?xHW)PysJ9Q=6y3w? z*f1Bj?Jm_!MSw9X)|vqxt3h^)r99@SEUAaDd$b7X5N%L{*kA}7#(9kfnRmMAuh6y` z6m?G_qj5Ob3ZNHs;(;S%k?wF%&rrSMYu_2$odV_qXQX^`5Cvh`mtoVu&V!Ap@D{bV zz-ef4hpx_l+n8S24RFGip|ernjD6&!Hqof0TiqBFqL}|m3-zxDq@E8qB9EIE#VKe{ zcl!S;kMX|QQfsDYXDDFLtgQIJwXjeX`y^9?FqM}F2R~zP-2mQU(_t-Q5rO2wAoS)V zY`#N+J0~my5c|j8H`xo9XU0y>mi=})V)ECplD7<{hEq-47~!~Jc(Z+DNiZ%zi+Vuh zID9nsNCRj{V&G#0aDR^WRN3tvYKksAS!LLG+wbhB=I0HXF$3g7qE9TI#75)Lp#1-U z&}^>C&Swl|6(cwL#S!mYS;r_v3skd@>yfFdq5{Fip^JKR^iR~2S@`+YfurT{h97l3 z!6d?q4U$;TkH!l2@yfNMvVjm22XWJwY{zOa9Z@(FS-c*u{RiLsz{|f}ktb+Wku?{O zCJW`4+(r5osGs2hkQ>VRMNXhdy?N!7WJPd>2;$Kf@?V$b7}yH18N=J-IG!iVo`Bd3o^PdWQYL$Q@|niBSpf`D-d!qPV?tocIQGS(N4-e0is9FFoeEDCU&D_Nz3}LWz_C@OsF9ji3t% z4D{C&~ZR#GQ_j?-`$De@bi9*U1Ce9zjgXz|3-k;y@rzZOzFv8mW%^NdeQo+zP z$u#SUZZfDhU{=*`5|~wW6yN<=_d3IU@DEHz=*zeGx-K4Rt?^RcDUJl+R&QV< zbADA3<>6x%pl#|S-hX2(m*!%fLStvI^2eygBimg}6uE@Wq!j^-vU5000rIswCDSs= zG{YCFc^)XU)03$^g^fyd6V+(2Q#k<%;$L?%54cP3$x+eqOLq zoMYj);gz^+^YxKQl6%wf-EP~tO`hv2StUpZ(AlQZfhvkT57Kg_+@UI_9m+v&z=bhr z@e|#V?t|be_{@~I94h4!zF^k+F{rzS{wkU1oftEG0H{X#Duzo@R;eV;Fd{835ebS2RwyD-%G`-c1(PBYR1jHSJIjTMpaXR2o2&2D zQhngZF4(#}lfB#B#7Ga6$J2lGC+2^cFEY{YlO?A1~fk;(KuA(<`Lap8b z?}veCOzI%LVL`~&Z=h=^IZbo2IU2hVM2mo(MzH|7leFckidxy%>zvzw=qt!+ihlv7;QnnvNeV=~C$`+KDND z7ZYRplOs>V!mCV|{iFh+TjUNJ@skU!jxoPiEy-9ocF**AbbWBUYWQ@g6zBt7v0F@u z9sK>!MD+>ply^U9@$LpA-gmu|74548upVxa+#xqbpqKF&F%OJDi1uz>?S(ska~kasBrp9p$>Fy5xx-7m1PsoO{=gy;aCN+3A^%k+5MT-4K1)%fZ}fv_2vW6l=3CVV zhf^MCQJr)`~M%LHd@mXib4^#`Ij7BD;351tRTBxhhhwYgV ziT_5x)%&BZCdlW*X#*1RKRihd0Wp6%hHtYdoXgcq#`n9Sw7EzSnb{M~z ze_ARGhJW4YQlYvT?-o~(^@$1>HC+xmZ6DZ$YI(VW(&v=GkD)+tK?m(VKU<~PRvVW* zJJbJ&yYe8~H1Pltq-N$tx66THkFoY@!CbJe%ALd6P@~{a7H41R#|jRB&cFJ(mJ?tCkcbyTn{vrEGbaI^Sz4z$;+LsX zie=PssM*`Q-b*O+u!4cpDKLB$v3z&#vd=wa*JEmLNUzV0){_hO>&HYDKkJBjC|HbiqdXtyYX&;~2HNaD|4+FEWapJlzB4PQ%lahOZ4ce7#aV`ThHSoZce?p5 ze6yBuhC!EE^SQ(k6^1)UqK!bg2Wk0j8u5#Y-hUI|3QT4l=*;$vaDlc`kwh)UNvw}4hJ)D)y^vcW!$lAVoi~G#tzO)*?*m1rULgZH3!t5a%L?#n z3$K5J-usRJz$Gx3zBNj(L@H(X@I9*^lb0@J(d_O;J-r8p4vtj)cnYbRd#JhCmF^@J zC#hWaM#XDLJ?20i!OHtRdUiI*cXEoDId(nM2IJMzT7JOUC|{BrtF$J9mhgKDZ}%oe z9I?uk&#Ho(9PuPMY=0hA9ur0~Q_>rmxbu-IpaYGP+E7&1pLAD6XpT*Ai?8W^QbX0; zyH7BmF3kJ_Fwin?N-vHKQym#Yv(X2GiW*JB8%dY@Q*VJfyI3IaN+mrF_*R*BsXx5o z@Xui4fW?)tL2lMNFk&l>m+oGIuVq_S^Z4OBY8IQ;{hT+R8v9 zqZ)+?RYg)(q7Od?-^ljx_UfPAheH{|C5`%Z+lH5U8z@Ps? z$p}>FFhC5Rt^w3Y4nAWn(orwzHLeb=CY=`xrWZedPxiBbZ%eNJW5j#xH|D3E-QYn# zi&Id;wmohQZ4$#(LG7Sz#dTqY2tyQDVldMiU$20lWUfo2L!wbtu1=N&pc#>x-xtt_jcg19+kZWVz9KahZ#JpjLE>3JbEgyeA!M)q$ zlL6@U<)i<4H1%PCc0KxBlIIux&CR*>y%lGO*5a#ig3vE_+wcAk|LexfVBJHy*K9+t z{vq*N|F3_Y{P=mFk;J~17q+{;|0(%mm&5r-4f|A%k}6JiY$*xcJ`}DAvG=C_4WFz^_BGj=6xj^-eSxYXz|LOVKh^xzQCui(YyeL zE*xpFN}ZsUs#Py(fZ3|d5wbMmHt0d^q!|~(t2FR>mC-(_r%Ju|^{H=E!v;z1f5@+l#??acf>9{He8(zyEM8E=VfaC(Q1ra@L99wqWCE%>v;)2U&Cp&lO4CE;4rzfK-Q_4ticSD?!c^BWsW zUJlllB*gT2X9jxY1!Ez!oHst4!z|02x@(#+XnnAlTR0BSO5U(W9az58q?pXn+OIqc zQSLr^PY|*d_o?!R$P`7<|8w=R0<%f8>B=D%^f7zU*JA6B&2$ zX+$t0=+z0X1u4Ui1k%4nr12fuV9FW^?LFV<5j21tp>;jdHN09bA#zN! z-McujD7|*K-a3gRaPOrLMtX^^hzT>XJps9LaOlf+*>d#!2E2Iubq6{vG!UmZ>Jz$} zCcDG<_WCX>=@#EuXD)wO;w)#_V;TxzMrzaqY<3! zsaX+1jl;Tk^kOh;MxqqCuTP`7UwwfykQ=`8?3Q$&dUL4h(p|iWcXUI4I=Im&)7ZwX z9zZRKH~tuj*rN*Z>O)VB-*_qLY_KsUyV$np!uF7)`;+(JdsSd7N|N%cnvmePtbEl} zIU!fTgU~%HCjf=KeS#TuYH>s#hzbjj$&m8?w_OOd^?c!!1@(y~UTLXWlGWrz1Wlkr} zH+bw{^or3GTqkX$UIj+6xU4p%TYRbzX=O(J;v$l*an*+Ug(@!hvfuA*dN-{~@uPL7_lyR-cvP@dY zeF-T3l?t<^5KJaFz6172k-t;gtvve$OY!p)AqyKPby9Ym4a@ zY`$o)*zS$sP7ylVn-4LC4I?&&sJxVB< zcg4+|)DeJPN50&7)~;lUB_E}F<+!AmJFFxpxc%=D1%@Q~bhMrhbe^7vSnqlj^QKoU z^G~YGN~)b8-fe+?;d5052Y=LAb7GNJ*((NfjVYhS>_FqSKWmtb)^3wQVlr74dK>{4 zPBld-6;5+>L(US@G8sXiiA$d2uB-q)TTIjDySvQ_F5mddj+fRfv|sj)cErJ(yrY32 zBKy@h29=C1Y>pF+%zroict3TbiI|GeskHZw?i!0P`YF}x(M#rY;VhCWEb+5Se6zt47OfgUoV$MB zt0(viVs`PR$2fFzfrJJBw(9h73TD)8M9i@W%wnIv?!t+Av-q#M6pHF}bwaPjNp zs4^U`!t_7xQl)e$FUPeZX+DYl&UU)N^76$q?f%hZG?B+sQXq zZz%MJpURciLb|oN{HzFUbFQOLT{j;aTUd+?Y@;4+#m~aRw0-r{b4o2t zWjZS=Bb3oQlD9))tFBi83ubTSCQI)~o}9p}&69nDD84GQS7E^pA!G5Z2$lT%6Zv)# z!ddj{zfaRDYd&bMTt~BYx3efchTrB@Z=ODAlV97V$uGBWxJZ27Qvi)kvx*c{{gkL< z+3h_M+fm}SltpeQ&vP4w zn93J;=Ye4u?N=^>wA#b%5cZ&H2M#^Vvb!gtzavS0^v64J%+QYlG=IzKMbNw{^AFtdaN32^Qrw+IQj_lp`GlNo%P7*H^2ef8I}By2JAjB5DC^R`dvBP!T{Rv86qLlBm(k_4krofB$|*>Bjr*O&K?$#91r!%uTGfv!3?q20 zyM>&O^RKG5jZ4wkEGG+KRzKff4aI0;6UO3Ay2$k4wJLIBb*gk4>;~_5-2{G1aE`dE z!dEnMOVy|px%R_V$UEA8yL7tQ#IAV_yzx2vHyZeFJ9TV|)ZKKwQ{jK5EZr~8tnZ8^ zYM5}LomC$4scz~7bmMdJEEo@Y{ZDS&`}fE73);lBCGUwec}78)79$ZV6rtpvt!pSH z2r5(U4E+Mk%O$*o+#Yl>Vqk#?EQ414!lm4sMuyWN&_R|lkP;%+Q{hW zz2Eve!*I2nD5{WVdG;!omID&oWO|NNy75^w<{=xtH}=emj4~d7x=DGaqwF2M6LPl% zY`6gFhEhAThF49IN&_w3y$5-_J6T#!!;%wJ6C#wXZGt8wlS8A6!k@%DJgyn(Y}_b8_giktdS@RM-yb{+9xZ-r3syhy1(`8j!~jc0-Xh^D;! zkk;z=Czcb2)k8!rYLZe%@M@H42eWbfU-aOH@0aMxgt|gj21Y~0qT-Id+8Kqxz|P>h z%Gz1BX%qeV)NZZ|Azj*n1yrS~PMQEB;JYzV9l#?609WVg<_nNkNUwlYI6yss+8f)s zJ8j$ADQ(Xhtabt3bh6{J2mAA*#o(rIcCixi;TKwJ>wb(PhsZiZy=}}wi-$^3aOnyT^dbH@={jY)j;{y6x$eOSR87vgeuYon_ zINBH3V!CLBQk&8&Z7B4USBzg_^AbI?6w;GYzZi(zSo2w}WMXQwoO-72jDf@TjMVKa1&X$)?RWgn?-m;$K;j^n zBDCOwU2t4mi4>66<@i-P!Ou$Txn(;`*3 zR#%Uj3n|_9of=p3@{wzFm}ZxVrhkbacw72h^h555u2dTamJ<<@G(<05wYrVDPq1yQ$8LoIi_Xo7)a(TkF5g+#BQ-VPf z{?r+=GeUj1VQD=#g2H_OvtIv~u=!4*!l)tyH1TT0O4MXozq(PXG*LulESu==oI25y z(MfAgCLAV+^YJwcaP{J!o_sJ2-WzxRlnK}b?o zj_teq=Ja$;sH!6&ZFmm>Bqq2~!pzBMZwg-*<`$`^nxkujJLTmD8kSCst}ne407@_$p?vSg2Z_Lz-1TuyBNfNaly5bfi_$_a zFAYOYH|Tv#m0Uu=ls2&O*n?ZKj)N77gzk_Hm=)QaZgSM1ihQb!o-al=EY+d7zRS{i zYjtk44HB@)Ja#<^ns~y5N%dBsd*}!JRolU5>%sG#X8!Yr{5B>PwM*;b!Fhl6Wak`)N1g*@5P5io{o4M{fnkW=SGubTQO4OvOfO;#Q9s`CS#;e2JBU#{|w zxsC74r<#)0;uvi+!t~oz?`V_aQ>DnPyLfBK*2*yN02*SYSDnk;KGm1%R1rk|)D4=} zZ+y1uKX-;&F%+-QyHB4~h5RsnMD9M>VVv&ZR?f9h`t<v%_t0Cq zgy|nhP%k0(l3KHTi@5e%75;;k7$`5yZ7qJ?v<%=2& zdoCvfrq&vl?M7H)o#;=@Q=OtQ8s=IGU`GSuTRwTBM9ry+Aog4&N>>I z2|4TLUyF+mS8{J`O1iKA!6D$>8CC?u+Y#A^T5wvZfuo~Ge2Rck)LAoA96JnlQ}uj{ zRaN+%3t%_P?3kE6nNLm7r=5FYjORSBFg#nsBV<&xW7=SH&ELs;HfM zW$EDC6Q(n?)(ER9X7xbpGFhfA(4nq1e#{Z-I+alS4mfWwUpf>PkikkcPj%bC5mQ~# z)?h}(6;AYdg+oSMxM=cAY>blgkXBaI=@R5}pGH+}r)PRUO<-^NNa^Oc`XmJU1C8vd zP@l{CV2*JMT3%t6G<1%gumXPnCr0ibVW+?AAZiy{y_hkP={{X3)BhC2ZrdTOG76UV zsbeQ(^~1M@l{$VE*v4{^E#0_F;-mE5YE{^44lM-2dZFMyTTYh6cowme3H61nSFnb9 z)=mYjpzrmtAi@sFk5*Jt@Oo0*-cp@!YsMd#n}g5E7_8e^IWEwbK0@j|9K#)Qz%ktN zZ6w)Qk>L4bQvp{Jfyf;zL26l1U)Ohb-JuSY1Nhe;3#d;&^HG|QFZnN?z@II_O}No< zI=P(Zb|7kha5G&(%PjAhY0+)~oX^+V3cSte5L%xs-#)tfu3EJ;2){Eq*`v<+?P%Qw znY>#;{cd2Ah0MS{L3iw5bQZK>{xP9VONNO-xau$=qX8dIDPicCBUttu-wWam*So+Q z?GMjyfq%DZUr}pVD!lkZd|9~${=QmvVSkr7<%_FmHpuJhEWW3q+PV>2cqCU)GS_~rGN&uZZLv9mSYw00;DMW3tZ1wQW*hfU9R z-;e|0M686lX_>Q-D_Lsb=_afVj8GNb3k|R^|KKp~Y)6$ET$`pw3&orPi&ks}<==k* z1F2|1^{eUZG6$$t-)qV6{hJ zwW2%rA9AcKqy^muFDIAeK2Hc&vEGpkJV(8)l3B%bf);aLG6K)_Wd>U=_Wo)fvfYy6 zMAR_g`qs?SPTCGgkSl%Mbx}?Xy!hJ><-wbTI(Ci{P%qB7ogr{lTvH4p&^+k@E2;jc z$M&iSg{wddWCtoDROTOdh(HCXGJ{#U&(SkI9ta!cr~GkGP+a{`ev8v;DYtJ@c9~K| zg!(kVC#OCQaD@N$#vK}x@<<{YJLCB zpk2h-SrI+{G4=+mv0gpMfSVp7@#MSgh+}7(YtXXy&&-Cdp_7oSyv#sFxN@@Gz}avW zAwf4WnL#f>x8}p{z%`gvu--xZE$On0v;q`ZF6ZBpqXOUnASVzYym4`64wr+9>789A z-t9Ljt8|FBB|V(!uc^iNzKRz{*%ps@(ch4RIjo7MlORk6-p*Jc0Khk0z|5jM1-1fr zcO=)QNvKpg(Q%vOsU15PH7vE{!IH+;w+v4GJ(tu|z5VJRWu8b|S4MmHaLGNfx;6!6bqE00w-lV|ubR)>e-K4&jecfH+ z9ZwaBxQmwnws2M-c+DS1h_Ib}YWsNdJMfx08dSx?+4yA?09EXLTF&z{;JlYVI3xfG zk{t}BDhz*XU{)zLv33ISk^Y4WQ6-HlH|PUGy?yE*tB71A;gE-{a1{j-HA>5fN+8%;PyONvyg7^lfr_;=pQIpV5YZ}G0v z*1(Xf{4+8Gy+_A$A=}z^G>!+LX=64IWODU`#=fD0;l=@I(~wm3&31Td0mRcuZ#xfyzumx%1u@P0Dm}7#s7C~n%|+kWdE%yactOj*1VF?g5?8FW zVdW0_wyhaX@46p@JcXMW{kpG;0&1}lxPd)fH{cffYblbbBte8j1R>Amh(X>UDlpyN}Af@m$Q&NcJ`R>h0To>$phia*=X=A#6$PL zex@S$*jXC*n8t;)J0QQZ!?YPBSIBqp2WCYUwE}W#faJ{S@EpBQ_tj{OtIdxEkif#u z`O&AKklWV4wx7$ku27fi)#6yt8x?T%{>_$)&Liswn~sA+9N4)fbLX1F7Bom!v`!R1 zFgR1rq&t+=I*fMz8r@t30j@w>$ZXBdBTNtqt)JxStWC|>z&ElWz@$OrKHZm-fe7U%Lia{2~{wN_sHk+T5`-m8i=+tEmuB|^HxwUFpB-JChrjrJ>={BeM z)$}VmrcOiz2LL<&Ty> z89Y-$Mr^L7c9=ux7IeqpwtNL`$Yi^+Y37wtfS|VK)v+`L| zr<@f@SdAoy%a%x0m72QgxwXZfx*9&u$A{#h0WLv)QTn0v{fQ`5v7u?Pafb~PakWU3 ze@=h)x0yAif>EH2=lZQ4CE8n|SNh(~3-i7I^MBEEK!R5FOt!S)ZSC4po5E0ym>9DL zFWTUK!lO(9?E%{+?gu}_uaOz@o;{qd%&Q9QIqdIdOa6H`k3acjU-Ho4DXv?M z)_8k1E@CmW7q^I#8TowZQDSfPb#uNm*fjQMl~-(#Mf>S&^iIgX!iTWj{RA}t73P$- zB#ZRVIyt0fOxvS9MtCL$40&}UKyXpNC1~C6Ina9}p?UAj{1I!r5d-*K_^tLum2mhQ z2J+3r)&Uj}tj}us&}Tm^iX_=#YCGwS?xQrHSG{70T{0`b@x6Y+6U?xWRw0jSo2uM; z7+}@+zoc=X!e=#$>agl(Yqn7p(%>$8@$^>N@ENWUm32t-N~?TWQ*rxSDfmMYA(^kb-}Z4?|xYkSg{I z(IEQ z8cM!`g#RMma3oj((9Yr&k9KiW(8sL_Y}ik57(%u9Gr`U1 z(!`PFKVcDde>;_`qP2UmO&M~d(IzO&GWpGmDs6`1C~lyXL3z1)!_9Z@I##qCL#s_c zuJ61~qr8S+z^~?#{;PytRxVcSYMs^*!4Q|J)I3)-X_U0g~Q3a%R(tI7CKsxym=k zza^c?FJUmu@queg!b1}d>|HjKj~-QF|GHvji3k%iuv=qzudd~6oM2I0TrDk_84sjBR?c}cjn*4Qgq z$y~`#Xg>BXijNQeGaA1m^T(xwC&Gf%BbS%?;NO;8MZGSrl^idW+zXP z(jWVvo(<|+aTe>zn4P=oJ}Pf%a3j#H!93-1 z#TxMs0dF;>Q-POG)l>8sk=!@$s2}7O8tq#m@z0^9<5ths=QW`0yz1Zh3MF`G;xy~> zrrFn4T>nZ|fxPEI?O+$vXSsK?$D6+Hpic{-bqCW{m8zt1PU7%aI-@}*x82&lhO8fj z6u&d;%^S{1^6tAf5n_T$TtiwnJ^P>9DKMV2m_qa=O{`dx(k9Yx-*mkO_Zn^xCw@1@ zl(-JTvuWJ{oC>|8^XlRAZKUf&r z-E!nr8{yr#DXrF+>!WppE)XHgo%-k_cANMj#GAWfd>8lge2IjTF zZHAIy^M|Qff-@M5~G`cx))F=#zIvPg1FMyMv=wDza%V71gq%LGFp?d!RKA7)xou7P3Z7$ZDm=@Msgt+lAtJ!~0># zNEeDK$osS!Pl#dv{wyDXqjicg>xX4UpXj zN-IdNQ&j0n-xi2@63GW+q6zrI(fb(!yqBmbHBDqkDtcLKC64YI^`2LMe(vw(80wT~ zhfd3rqt?_m*CTpVV1f2#Gj!l&sY~vsR}}hW4VKVGh)5_|=Sn3#xzx9aQG=Y*)49^o zAdeE8!j!5n=wsL6I*Feh2memMs^*`q*Wr73f`{-TU*a^dUVlS^Pu{Zn6H!fZ!AJ-~ z#jmgH=AU~ZbX>RoueeD>@NVnN&(qB(!vG)VONVn_2=Ry7D2<+hne(KpB;)%3bTGT; zU5BlS4_)_L3lXMNKl&D{Vr>G-%=C%uSNo5|Pg3P7na7l_p1|amBweO~Fde-_g*ln5H*r_Z~tHM)iY9}Xt5ahf6eNCcr z28R_Uu3ksGZ0$9%M7vsCO3?e^>Wx5cHW2czO00TGDHtQ8o8L$P(!FO{D<+MsfHEYk#Y7dfxT z4)}tzc{}GHkDJe>j=7&^C|iF^L0DIf29EMV!5jn%XA z!FhK*p>{gWZhf9%H49d0c}o>}XV?WUXZmlQLhJCnmu#Z}ZuozIBYm0#-7Bh^?0!gp z$P2THDxr?B)NZ1_kRE_W-uaLqpo2t#_3P+n1FomQC?ex0@wrs@^ZH^>rL=FjmB7tr zl`h~|L0(bj@Dg-hDp6h6ilsvAZ3NGb+#V#B)EQc0H;iWm#yOCc=l9L>`F|B;=e>e` z5zq8G5i}PP@LR&Y89G2Z*<_?LusD1J{9AFHiZp+~(;EH8Me=fuQL4)}mNJz`QLh~i zcATZKd)~zgFeq%mKJtQ%6i>-49LlnRP+yLY4kI+U!+Pl(*DTiH>`?{Aqx z@VD$fko&Zx_UKEU(Um{mbbEHVBC`36pT5W-d{vqH5QkN*$JnCKdJHG@bvyARjYma_nxlNkv7cEQmAi>s{oqn z8s_pKdqeLPYwfjqYvpKkcdl&8q1!i;k1FBH#nI~|gOyCf6i1c@hnxWo!oC^_eM#&X ztYf@$;KZo~v&C#*1#j|q$nw26;~8qRmWY$tjF)S=zE)nqPVWf!iIpW{S8ZhWMM~eE zgeMHfeL7;?LXnO(&xewXpGoPXb^kNAu>4FC9KXUYId{pbKE|~_yZT%^dRWtATp{Gx zxa-Uxn-!^OYDDr9-Ic8mdyz?qPr7#AA=-c)ZD`fASeX0tR=5Bl{Q74OODX-PtCcYdNa(KpudK45_iQpUdMDyA>7{7ft-@wPXrJTGmeQMX6u~s z{Gf%FufPn(3+*2t_aAvRNi{MM>Y#u8@b&U)V37&x!N4||6ADK0x_{S7Ib>mJwV(`h zW$%BY7x%+N+C-9<$?UrWHzC(ZUzvYAJHgtB-=-3#uOmr%3o#{EH;SkCB1Fj=1a{V4 z1M^|ovr0nqERlEO`Jorg1h(;6g!00sCrgqt`?cQ`%5BainA`a4qcn{VLJJIp`sjR@ zH*#W}Tw_3m@;pbx_&MwP#M9TKubF{16QlEzA=#2oy<0f1{==0#98M%Q-4+DX`AfxD z8d-nv+5W6)+-D$Eq?g|a-S{KEyC=f{am_hm@09;Q=Hc&ONKEyA4%r#eze6$8<^me6 zkpHynd_FL--=(!{+)bClKDwLU|ATKpnwpOt2- zq9#c<_uQYHuly|mODqga&$ZfLjBHowIA*+?=CUrO;?P3km3PR0EvOnI+F~+oN*|UWTa}#eV+xQ;x)PwsIzfy&B zk85JJAMdvwH{2I?<{yw1fn7Wk<^9b*QIPcMYpnqOugCJ)1dncPilno+FKxdX-}`|7 zB?|B5uc?;vKmX@r)o8oIKj)zrrv}R5Ld~^Ht+Ekl-C3nc&luFu?H#umQ(Dh$4F(Bn zIb74M{J*5cRwzq&K##NksQ>p9-^jx39cJX+d6%TlL?M@_yX-J8*iRY$0_pJ$+C@u- zT9qUb?(Z2J4-))HhQQr!MO8jm4*&7dSn-~zNx7pCECxG|#6>RmmVt*84wC>vWJGO+ zO4cttYyqlyfqQNDJ9n+mqz879yWRkZR#9T?62tyDl?V&EiMy4m31^@&opD}g zC)Bt&)8*%drhv&oNPP4Bnd5E%7-PFQJdcQnOF?s3{%1F?!QYv-`6!&PS>bd zyCln4%kIx+oDM$KT!oFgM=W*ce{<@eL+LLsTxm$i>Xp<70A0IPmJ3Z7)N_L`Qx9>J zeHA?qaEw#*In@ye960sSEzN$fF-3Hz6j^5}>g8)cFVmf^E+$4X#|C{cd74@4n1JX> zynkL$EEoU1BqBpcTF>{rMvvywPh&Xl`(ORGbv5>niI(%(#=*K6;Q>`R*n*I-Zaq3- zT-wyR5e@ybt-2I?_+M4hd2jif-mGGcIMey}xFvX73Gy-{mNzL~_y_(PJk$hrh!}2) zOQ$w_Hfol9AR{_=&5YSr68{BzqF^!U2T= zMW@&y6{*e*fw1v0OZdoFF*J$aDgym%Q9n;&n!%s#A$N z$*u_e*UEVd$DqvMqRe5*&yJ=T`Gbd~&?*%%x7-}DBUL5yUoKmxFMm7TAIk)1%_86# zuLhC%2&qa*RvfkwPi*v-f(^MEvAL1tBg^=qxJGZsR(}ohs}qNHM&&?n*w& z_s=Enjlw^=L9tlsHjfRn3=`Dr2|I}W{I~Kng4IDGEwP*k&TN#Mo^z27%LmU$U{9Ki8oNN3c3!EZ%CG{P*U0YfEbJAzGoO7z z14rTQ%PW7<<746$9kCnvc*(&ftaGY0+*{kreSt2~!t}hQEcMH;nSD_hc{S)dCGTRQEUoMM2kuO)6p(|I02cm)Xz?3p- z{A)60YYr|A-VkZ?;q)HAc7A1bl5%63z43)oRXCbLN)T28HB)DJE@S(kkROPIYxC+g zSe}D94-IUlr3D{9_<5$^a&lguYw8_um){BQ6-xbEfxNyi+3F4CDXV&kx1@9XiGu@2 z>m+PW;p8prHc}uKPSbVnnisph-RmV%4lN09)<6E^1pk@%kB3-3`=SzXtjy)VvIBM4 zB}5AScuvYG#h{M5m)zZdJ>U>uC5u2*pYeCkxqxZ=-RzaR4ols1Pu%1Z%X-B>uC4l} z-0ys^b-9vhdvPN^+jzD|QhvEPWZ?yjFFK-ShMJB(q0yf_*xfYFh{M=^r%v^ ze5z*cAid0|erbIUbsm(^aIf)r&6H((JVh^*v{f~EfA!K<+OV08D@l%yZ7w=UWZ|s@ z!(vPT9Eyo$$T3QM>l>!J4-GTM9aEO)1?0ebdV+SpK=)?VvI2^~35J29Dzl46@A|?NrC^RTM^`GL+ETG*EdV>2r*F0kHUD+MMqshSp}>}875-(b~<4ILrg zArbGnmNfdz)K_7uo3tj~l?UN?pPXQRcVv(u)?t|C=|a5CW|Yg}s>CsEq$T(8yVpl< zpO>gGe!259;A9q7nN5b2ipS4d-^{!=Z%tW!jmA({CqWis<^lRdMr~E0Tyue_(7%nG zP5drvsJqN_Ns%TXOG5VJ84rLTfK7HNoTUgwXXgF6K0#m@F0n38I7JYe)K16`P=wyr z%Lh^5moTfo$4@=p=l}n(_xABj?{ED0s#Cf{w}*0vrIa{Sl)G5S?b2eo znK`GT5=yxpmP!)2Nv0WFQI?nqx!KIh*kWceHnZXP8ak(QzMseM_xtzv_#J=rc$^34 z^?qHi*L7X5>+X47QBSyACnM+r{BhR%kTUWu>J#xXx;hHxW>0F0lpOQP9LDSM>=QoS zjmR7p+hLstRIGT%T%GG;Pl$)Dv#~m2X>RTnN>Cx2zJhy`s0mFWs!zBs(+CJdf7IZF(mBx{V6klP9*UEWAm5 zXZSvtzL~UDg(vnNd3b1)Lig=$ze_iWwVQUH>MWrl?j?NgCgUPpCAE2J3Ex@_&csOt z7IZZ`CNv1z?tk31=Av6Zl{?SHAV*JwaI4%?s;NaAayyy&xS|y7T zL|KX753P8AJA1_edh4?8V{Rqebl&7fmb~C;UD|?DDz)f(sDYZm>FTyV&>2d>Rb5$W zs-dOr@_@R`-@yHPb44`mz8?AFQCHP`?a>^f>^#xZ@g$GmX}u@-{kupt9^zH>XhH{mxl{dg3DhD0oBs zk?%Vx9{ZH-R|%W3=2A#{k6KA6U`GjRgPG%D{)lh+mc8vaS+s!eGn=rFm3Isr5SR4` zCoYS4fzNo!GzgMCv6R)6XUn}>{u%pCXGpu3jtFZ~a!eQ!5o-~7Ns`a$( z08rDRPL$y#fGzw~|9uqqdOZAHA0F-Tb17PC^VIcwe93XtWgm?11lwlQjxxn{72h(; zMP{A_TdyWA4-ljV3=kd?{SR(cnmvW5!JMA1k61$`t9kL2HVzvev&U`B9AdJz8|@AB zU4}Y%Z}*j{2E0pM2MiR}6vzH;Orc)!(>abl4Yt12KDzAyw*6d1p*IKis;G_~5EN$w z&;NZrLG5{eI>EC&lS?P#r0avj0ja*~wuVm?Y!l)2O(8L@Mh|G@kA7+^9d8x&DW(n3 zb7!Q$nVc)k=%+Wz)CjgFowXFwG?R+j{k!u6N~~7h)~pW?%(b7hAB7Or`X5ayThI-? z==LOFkVLZs3j@|8?nL*x-^&hNoCl996zoLzIOjoN9CBttaeeL2SO}i5^;cRzKz*CVNI8Aql@ps7Vg35NE=@0C^KRQy<}I=iC|~ceI6Sz&FYM|nA@d|oY zZ+28OF0NBJV^?S6RnB?y4C73WSd5fJvo)U(Q5#R~dF3J&cSJ5bHr1O?G@81T7DL0! zf9f^}RL{CK#sP^Zo->)t6GU3TWJcrmzl}x>9)WFgX1)Fc8bUnwO}OCx2yaK#uKjgI zDk+6^?DiT-VQ7qSY=r3dAyn-N_YW(v%Kqdu7Lj+czUcRqwluhXyOYnMsp9;?lIUK# z3&Psm%`FdAuz&L!NhWp&igW4%C+J&c=eyx0p^)6sHzg+JJ5sO`d-cn5keL$k-Pc|p zz$hK8$ZB}?95W>#!pfHI@u`$<1nn7Q1j`~=d-ftNxCM--GW`W@YubOk)#wG`#b{b$ z#Ljp0sb=F8~q{*_CmSBz4Io#{#S68X3#fN}Hx7vtKDV7L~hzubkp zIY6GcgloIprxe8?KYJajzyY8*fBogwlOw??^A*L=5S~RGAo#dsr4`u#&QDJWG2U{* z%(9siP%U1N0OyGAZrj3a6t>i|X@$<+ z@QWva(sr4vYCWGh!hM_*_WqPl!`LTx zo?1H<^ExmCZZp-H?e{@!FlHOpazy?1&9QeABedgEGh*_edvnH?2UzOR?3v@vc{Wgu z5>yGp2Rd@VhRAb)$=7jkJN)lG0fa*jHv~WfZ%%F>qx6vt**j`)ncOs||B*BvHegB~ zuH)=%%WQ1F7;6r@BUKJTO*d4zXcp;yZu5aaXfv*$*8?3|h|3q3&(_nSOix{9j#3rt zPy|ruKgk2e4^yl9ShEBS0oy8b#uDx+8QTr(>F$^3(VsVdjyOqUv01HIWE9<`J*02M zq`*X0I!c&p8v&vy?UsIHRhf_0=v>UlZ&h)>R*CzRgL;gn81!~bwQ99}QjokO3An=+ z8}HmjLmN=L2qCV5F`ka;q>S;DImb{h=3MQ@UfY6m+A1^QJK4? zvB?d&9rE-~%QY^vN&1=K!Xx4Buf_NAhq~KxilzaxV+2w_lySivuMhqSA}`LbkI=Rh zKCTV!&o=VCzS!WwpYw;y)?7W>j+21bYOJp?AlAtDf%=Ej;qoRVovz>)=1`{6zMFba zp=)+Jgzqw?MY9du;7mu<$nFMPwar*8AT#vl#fFZc#$K|a51+g}-sa}ml}vpXj(^}< zdJ%Q5dxxhxRwpx5phwitSuM;Ro&&CsDE(M0;A%Z*e z!~Z-ppuc9-&9NM!g;9Hq)N8UM83c~S`v|o+m-UHPXIlOBAOTQj-jd79kHJ1WH`t&o zfL@dya}bNq+IK1IE5hQD(7=9UHc!vng-(Mi=4PzMXY;J=u(q;XWQW|`dvyN(5T298 z7C`xebj$rcD=GR}!*@JDBbsyXo*&wc8W~eG8^0YG+T{H$!1zb_WKr+in~R^nqf7)X z2wyeb2!FsQ_GgT>psvKNSaJD`!gyzFa^Oyy=x)N-s)BPOxI zcTIbokYzV99Js5AhkL_|M2#Cvo)4q7kMwT!H*}pXRI=*}d z`uL}&t1?A!VyH_sS7!_xO1FG?cIkdz|BhtwvCXf%~H<>U+%j ziN>$HMZiJ%(u9)*K7L@FijU`GkZx0+#{w-zw$3;V_j;aAzW$I2 z0s{scM-yvrf?}kd`Bg8nTZ@iQiSs0(sg&S5&4(wmS2TiXTU$cZ;XLZy;Ea3}jWfIz zr}?D#g!JqTBP#vHh-YM3kMnt+zAIsUV+q!+ZXw6Q9M7uO$WjlpG4f&$3po00P6Q(> z%)rI?CzFA?}(R%F#1oHa9OjNwUQ9bbWOvw6-=F&Yr zRoxRM8H;nC;Je7>OMpE0&MCAvTX`P9wT|M=;MO62D~eLwkhZl_~2L0hkKje3r%L?`%A4?p3)x6K3Qq7g{>FwCd&$*XU^ePqpW4d z+xWMh4z~vu`H#bReeilGVnU-~VXi_RrrRbTjTtn93(a20$RwqsM+b zckt4WzOv?J|BfOhf3zs8 z6*E+&{4c6HOhY?1FTY`5of>6zG3J!Iq=Wpc1RU-g_c;tD$9w1Z=Q9bg%B=Om916iRhgmC1C5%Tlht(|>H4*vH$$Uk`czjHbNA5X~t#}hwv z{D1QBpFDt;lK<(*Ih*G{dH7Er{*#BleSrVu;Xir!|Cv0zajh6@bHZ>7356YJ_82_8 zJkkr2)9`So5_#ZmDQuec#djZeCRcuqR)Hi71DL{q8NWPZCM+4wd5Q`2g$ne|1rHYs zu50bj?#Kio%>rEbqIrKO0u zzHKylqOW$*sgX-oR|jDlPJ=m1PcqXCFRO+!a-IWn#4dcg-Z=JZoo+Oth1T9iYD9?=v}5QL7BVz_8yYH%{Z{ z7Y7b+O=_lUK|*3@sqs5;G-vC>jTy2i33RR|0+z$Q1G1dE+m^Y0|C4P>WSK(R*s;WK zu{u)Gov5X6B4^R!MW3TjMbF|Xhv?QE~XB2IH z(yU~=Qr1!JM?B<@lIF2mS&F111e~yBrpX)x8nEq2^V+#ToQuVRKQti!`w!KdmF%<^ zo|@?;*I_y9kDo8N!qAgd7vn%fhc{C*@T{m44o7p;z=G~6YNjCt_<`s7pjaILv;*2! z24G9OT~|%(jByj8NLhij=^z0$u0D)7ZxRza#h%K%k~%0LmS!plsH@m>k&9ZZc6AFG z1b_~L%Y(C!yNlX4PK}_^pB+$1mzJ9HZrMci+z?1=Rp2{?dBI{(0j<0vB|!zvKW7tR zhT0l4O9PB9hquD`E3E1~@jkW;Ee2^;q=h-Z^(oE`2m@F|5 z`O2@>CU6Gwi=eMHU)2ME23}yZCt>mgXMB&*J2vb3xQGQhDa3+y{wH}Wy?heBdL8{% z{`Uu+!3Q7zzxd$3(Hp1!y!0_}yBaqUbhGvMhBsnZ(H$^yyvtsPVSZ}}IJ0GFc#LCn z`)VVLL!=*Phno83ln}w}ghu|_GS|MN){2Dw_pyEZwAOCr$xat>P`ru-qMxUMzxvq9 zC!@wcUd4m8OWy$TfON1AgzHmY5!S7m;MIt3Vi(k#3<`|M{3kWOEfRGSsbxZNNn4V~ z_P&MiBF%=`WJlNb@1tAv+tEH1(y)rI^M2I3P3oE%FKe2E6mo=DS0DXFdnA$&QZuM( z^sY88*5^*P$|KlGLZC+he%&fzCp02q+@v_828y1)9YYtPvch#v3rgRal)BA^jovu+ zBmK4kK%c+qytT5{in0jM+Ko>}S1I2?Y zR;%>cf)-d!3>vgM+e=UAv^h*fNKJ|AtQ4rjDlS3Ruw%U}nL+_QH56&N-OkGN6VyYjPXNz^V3f z)zWJU#eQ(Kw8}5?cyih`=g|N(^*xci|8V%76e1`eK36YQ&lE&7zVKs`rOvUFRuWow zIjDpt?Vw?;J??l$raWL1gvhh|bn3HGw$%GOq{8U6Q(%vruy$(VHw@IMFQapm@A9MF z8my1B>9|ZOg5ERdr)PrCkPE)crh{4l`qGF2N~1%WIzgLU`%8oLFG!BmCcf6(&1j-& z;mStEn*;AQNbcY@4M{;IHjy>%?UeX3Ufmu<#w&Qdztd-Hz6`F%TGT9EM8$P9Hpe~A zDe1Y&##+yIZ$JtHR-vr0lwigkLLCgBhuDy|2hQW)%NFGF$NLaW(d4huktZKCzR#nB z^2U&G>0mv%(8MyOWYA<^NMN&`#ssRVjL8${i`Ut&*D4=W-JCDYe9$l)eq~TE-$bsx zgzr8`ElY=Cda=hn2**)$UlOM!f`hfDVKLmw!9Br*YE`;!>~nt0M?7BL_~4#hhjJe+s(hR#Boy(kr?G^OzD}OhD&S z$zcNC&0#v0`rbM+4}KG40jq3M*P1kIvtOUT8U1Gn)YTseP_l4H{IET#BR--sUAV%{ z2KBTd=qCOs$|I|1(a_!_ca&JQxD+U%AVLGIW^#h8H68YimJm_UF*GVt-l3M#9J-2Q zH=yzeehzsP&EcVp?%>J|%&D{h9`y*e6*mMk8ABk__Q6X~{amrMmt!Lt51wrJ$T62y zFZaS~4PVg(ypQGRk0E`qB;1bL6TFhb5J|>}NqSg(2*r9dVHnmBf%PyOP8-*sG@W?B z-rYzijs<6K;vowB=ryW{6=x<*wd=}_C_%Jt8uRfdqp`EX2n8-p6yfBDF8oZC--I)m zc24CYk3pFwnf-h{BfTTiPUsoE$N{AQdulz@9=D^RB)~r2^FzBD8h|86SgTn$I?FGo zs&Bxpd_Fn!WFA^W2VByoX!cAi!rHI76`fj3GWGmrXVCD1oCEJ@D#dxw`TxmGY zv5DMtsh#>lD#49TCf6re__(sW3L^@HA@7H+0xUQaFTr?;;|Tk7zSKK>6|7v+N4np` z=Lu=B-vwsG=O&JQ{3j-n32lvEI|L>Lmzgczxl_(66Hsa&=g2lSku4%CiA~^3!V3kL zKXmYxA_AOzRl;{Vj3KK03iLQ92FhX>M)mCi4aIy<#!Du z=9Zw+i;*oZH!(JZz`e`GanDgkhuB8CLdKK!EohI>cKK^!`34u%AxS^S9=`lea4YC* zP|C8Es3J0o9$%>$e%{Ab3|ekAd5B0dFnUBbp%1krCz!mN76GYkN;BPvG@|L7|z0D9A^33PN2ZP%^cQ zR($a2WOY-dd?sBS(VpRl$V{=Q4V0<+WRv9liCMz+$>KVId6VTKWmX5Mq5Y@9w&A|g z6l*KtE_1hX&(<1Z?)ez$R;Kb}&`yKd)E;S3eK%WWksu%szob~mu7--`@1bDJ1H#=T z8Hfz#FREy41=D^x<)G7m3e9LJgEQi8LTMrM zm_;=EVuPwMlgjNDc>+~ZXpY4?+T__d5d(N7g5hX35QUeusX!D0>8-GJ^fai^D~bh~ z;58jn`@x!_g`4Y}0t^-6^8Z7Nqi#$~w~{XLfTi86RR`};$>%PrdVN+8ry2MmUIlVr zx`@;aWC8cvbVxWo6|;=ci}-AGgcx8!?+&EWYklGN4FO>vLstp*t@$mzrb+cxADK|4 z0m?8C#$!b$Mu#FJdj+e}8m4Dn%9AhKbKdL`{93CjxM+i-3)%uT*86?qyaPC%ge18| ze%(iRr$ngK`tn;+@pWYFGA$Gt)-Z7hN)|hcjbXuY_J~HsOh1_Hal5GS*la8S&aV)m zINYGFNo|d@$6Yn<`rbD4QN*au8gEtp`UlriLwn^^x<~U5mvNg5X}*=K|C6 zT{4qt?pPv#g_O&H?mfc{Ri06rS4|JAZ#BN$`(ewZgC;Y&BssL@cj(DHvwl-`SesHs zZMvVrNRWq1Ki4I?v26V?9v=z1VnE>@%oH*H3}Gq)>nGgN3oGYbX^7yU$lE8UzUg}r zs=MfheU5Ch*F{WBJd!fio~2*+bc!Ae+Plxn$q4h_zxd?5mtsaJxH!Wu=mz~43CYbbwsziR=Dq;cp`kgq?pld z2xKU6_PGgAp$P#x2-l!mei(bTB67Xyt)ip}-5qTyZNH~q!x35fAq7(7&lxJ3Zs@58 zf{20=L=s$Ns9K+g#dxHJE-QJ_FR&o*cXNm+EJ9r@$i}Xo5vJGKCV{Svq_(Msu8{yo zp=w-FdtmrX1C0!Ga9khBXT219{CqOfv8EOr+Fd+4L4o29+JEqP6!KXzk<%l9%}5xN zlRmjr+#%*C-leuQfwSXCCx2gs&1V$e6){`+IB5qc@NXK9FOrWl7|T#Aweg?pxMylGLfJ>)Ugn2JY3KN%GG7iPp_Y8`1if6kV?^JMtf2} z=^^gV;K7jlYYTLr5H+XQ>9?!)vI1}d&Pdi&gRV+SAoS8Z%WVEqJ=}@Om9_p39ns7Hv0)L5GvUnfXa3{S_4xOrt3T1I z#%$TEF|nG!Nwzf5FdM4{%#lv3o?Z(a(rl)zr6a0`w>^*g05j?f-`>F7+!VapjkwFj z=B}cc?q1!BStd|fCaL`(DtX1T;MF$<#bpOLo`FVoy;*NGkvwrj8ZB)OHS%=wqsCnt z`|RNM&FLK4AMKkUc*)3$RY3~Sa6yUlm`QO%Wn3o?=;Y&wjD*jkkeKxHlmcG@4fa0d zDK6c}ZYQh|aiyV4R$-ns%2<1@+_x)?-xm_dAF27;jmGm-fwhsw(p6`;DV}I=?`t0vDI)4%$RKb2)-4PG^M*>b^ zdDYorg_Pp#E7v^YB2eJDW<(jc5`ly3Xvs8WOYCUTt?Fd=kCap~^l6Zl(Om#01c)tm z{$?i05!vsoz(c&)>m7P2pSypVPo}utFmz{bB9PS z6rs|Xk6~aCBRmIy`iLUbR%U!5>Z4?N6>1Y-MSySMCyt-<<qE9;{VSjSuG&#a> zTiCl$I&G^ZYOupj(si&tE*u+stih&Rx7He#Gx?$Y^OW>yd|xYdOMTXY`Ua<0xkZXE zOnKfnsQ~$GzRUwN!n;_WLrSSlM-ZZGZ^M#l782#H_3F9BgC^^1=Zq#IoM+O|o}y8% zZ4RpjuGq-f`_!PWW??Vq9Fcf&N8D#EH=8?{@-h;s<|TP2&}xM|7cw!ImpXyi)ys7H zWFGJ5Zz(;yRHN5C_YrzRyfOfcT(l>43x|(Bf`f)g%sOX|Csv!jHkpANM@FCqN*btn z(%08RoF>9wm67v0r)dbUPLzk~j|a4w|L)(0rq}l`{m8wP81>W3uc~^XzkemsGRz5X zMmOTLrDXXldGM}FjsvT0mQvvzs;gjZY=X*!)#PO{1zg67w)iAwlw-xgk|PzvmoDDP z#;C>6YF;8TG{{grw3w49t^G~}PZb~5RhgFHu*#?+xps+x;B<53BNW!y(h9aY=&=vQ zF(=3{c&8Cc;d-9HkezB7KA?i#F?LJkj~Lvh#+K|7z{Ke#9vC}p$CPmj4aGJ-S)cXT zy`gKSJx>IahDa^aZ7h{~i4!|O*Rg6&>a}8EoR)Eqp>@>&-0`nt)s-N2^_#Zwh=@s@ zb^1Raoo7N{1-h_3Lm!ce^-um0J)34Y{>vu!`c5dHta*wT+YBZvLZ63zZ*_{55b8%% z#zB|V&}pJd80!d;h*`j5_Y`8j_eA$JQzD1UMqB&$3Y+Bb2O+*TIf~_P=74D1az=0J za>k%B(Boe5tWp=FQ?-;XpFpb-OciTus~r}agBqlzfCXz(#oKQU6WF_jrNSehQeyy| zs;D72r!FA z^77zJVIhnObF$(twlHr`EX2y8I3Iy;SKEMe8g&y^4!&#%x#5Wm=!A6!158FhA(mUl zX?Q7l_m_Y3KkT++a&Q~%h5HroHrW_P-2p+l%^HtUHQvNs;&z7@W-%6mChxSMw$PpTOR%v}AgWtnIQP>d zc;i8HEf0+|K^e&=mgR3%>1JfrURc41vr~Imts~hmZW4bcG2wTKID+}K5cynKVUfq& zy0bK(UcFu_$lu3IjBr8G@q^G3FltE#BRO9%I;}U;t?MWAX=gtYJ{e<;Xu)=9Pmi_n zw+-#MR3csS)gC^22cy@bjI!D6ZAuAJC=QEX4QZI`0dgm4%6#j;^9)<|UmZ{b3vmV? zn2$F(?B!6^bcb}m7r(AETYEZrN%G=ns;-F41qI2hUm$`xtUD}cI%Ubft>Go4a4rb- z$U(imrC;TUUp8E^r#XD_0I&A@g9rOS*i{$TzRK35lD$vPaTj?Buj3vUsyE?r<55ZE={_H-u33isYNmbc~MWn)UHLh(L8U%^~YdF350x{Lq4 ztw+qs4`5qy*iwCHFxPy?kd~}tKPZMY{BgWjsy6)lP?jikXW1W!a0XblJ=D*Dxd@y}u=QHqY1Q={uXC8|H%YR{$HG3x1#flW8`=vqsc# z2Cth}4%-+gW`rSw%t2ZS!1ZUE#Y=cQh&?+L0vjSgZZ3Sq8PI|FxDf)Ngy{?pbGr~U3D){MYQIUX(yVWS+PMz2TAs$?~5`AXZb- z5<(kb&kt?SLj$XEW5!6Cn*@Z-%}4Gf>=Z75q;8**^li}%J%*@}H(EnK{&dkPS%L4~ zne<)&#NTKJ5}@2KyOxJ!_t8A3>BB)9vhB}B{DpK`$DFV12HVwkb&?d?eE?D1&-1a_ zAzTPa9f*ni7Nam_%37Il^*6zvN|g^mCe#1GmMO`$DN}9Kk)sR0e4ovFt%QbARnpm$ z30+G_Xu)HQfHSmf2mrcdJr-x$EF_?%EZr{ubs!5F%CrmLcEdhv^oiD5$$ZF8Ryau% zt}7KC2{`I`;y{kt$t56dXy*pOrObF7cfN;fOt3vVSjFNR?(`MGn|^Mr1Pu0*b$pSv zc|V{m+j$c0hCRSDRDdZ=>AF)ry2S5HHG{cC<*I4Msexs0LS{PhL|FkX63X0!tR^S8 zo$ay&3n|YPBq!4DT6O0#wu1bme|5)QE9(P?jTfFTpRM1{VM@LjDb=XQQ21RF@Y@nu z64qdD2ZdXoB$jjBlt8-C(C5Ta&uy|CXL0y1r=oH)_8|*Hsb)w6xLKs!V1|f+sJz;} z03wmyNW0lclVlPzbp*l0AIH|pXeygqk#2i*3;ej-YOo^iV8GEF-I*0JV8vtIkv)xU z)1vU>f?&RMjP1u9KG$&;EpRDhV{r>3%V`U!!%5TP6thcrOwZd1Zm3I@gxy#4d!Nb9I?<$vYS0Okg$CGPJSW> zBXZaHZ8tYlYiMUA9MmiGWX$-6>^@>zK1Jvu0%m;UILr7CZM2^xPCLogIRkbVGP8~d ztn)e06!l#E6iLdy%7PBYqQ7?=8RRx*?ubkP>&+ZxEqxORAUw|}V=zU# zx(gH~Y$bRVe%x$6WH1X@{n9p$x@&vnLBS_h(XJs`RriTGbgggj4Ov<_Dxh5vm-|XIiYA)wQ2v#5cdxN6-+CF!wPl`=1IZ5;&m< zFSrm2+C>EW#+;&ooq+L&OKo|2B;dhOCW_&aqfbR^NS0ub=12*_^~`dV>aQfxALPBF zOjDOG(6L6=!tdj&!@E?DHfDge`a8tz&wNwnewMZ3Uh~m>#l6#`4(gTfu+v;-w-kw% zmZs~9Yx&x^ln839`o+lJBt=6Tah#k&^^SH%2+;(Es{|@Kzgd>6o!5byza7j|SnCLi z0HP+pOWMsVl%>YYr|7}xGW>MIr=Ca|_q?1Hnqtz)jEbUA$u15?sfs(rK30T&C?+bU z)NILLm7O#_N|RG)(|DWOY$($?--h2iF*Zi2z(ZrFhpvw7h&P;`#9nQ^ap_|Ubb_z( z3*!FtcbXhVi#YVh0-JEw0*Wqia*BProx!XeyT(zR!fcsRdHzZVf|DH<=-l!v2$7r@ z$y7Q68*W_1S(cuVbrlvw0_SrcAi`$Ux;cO1yO`Y%uOx|wl#;J)!dw>bP`~IZbP{QF z9X&@2ghWBLs9o)ql5b*m4t9!v3p)`fmyVuF1pkcmPuFe}<8r&3BEF`eV1oC|?=J6! znvav1Mv_9VrRNm~5k0ZuN82c~&|c5{C$1ggISMVclS!PAju1GW>rmRZhSGM$|I zL-*HYYVrMCr<^#7Sxa4)#`uGg8eBzkK(C z3sLl5(U2M;SRhAdk${p0^8){(zZ^B#n6mboM$B7&_jtg26i7}-nal>P-OB^GbUR^oLvEFteJL7U`>MA?=m4kzKx*@sfw#Vx9ywhh+dXuk zNqo(3n#`7_!T!0b(c#^FBMz$Z`c4qmm`1-1>eW3Tl8_-iz_5>9>#jBe$_lxBT;LG9C z7>ra@p~n&|E+f8EF`Y5MN^<1zA6xo~tF_q&=d( z{^Z_-Ft&>vaqu>ak`IU44-$N1#(3J;N}l&^N$H)%%Tbfd65Nb++ysBJc@|w!NYa%7kNYGdsXdc8o`0()>q4D6xONi0HgKMLL z!IKEEf^Yr=FKvTXwf4%0qnbB+ZJ?4u_Lg?6PNdgqU{H<3TdM7w9;-z=cVRt$ThXUG zEoG#rri`c0K!;{|+gRFtrA5q^b8}U=JXnz{tzAIL)T$TEhp0kVR-lFnR3qkCwOsnN zBn9a($kZhXrr3Z5_Ct1?&sz(ihf8;Yn|QOoqFTGI!eTpVy1Phs(E(hihO^`}MO;db zmFS;#Fy69KhSxbTYj(x7e`0YkT2+ze-Kou-CA#6oq3tndH&h`Cgz3v*C+1UOCQT*z zQ(|!4-IpGhSI?87rvBp8P>Zh*F1TnW0nJ=F`FqSq4I&GDQN5Cr`F=9ZcUwl{3<_y* zZhVvKNj@};7%Dyf@sBvv*q_KQW`>DFmM-LcIOCE;%ofAZ#rXcA1cT{5Huy)hOufU@ zTpTUI&}|-Y7R1KW|2!M zT7WSM-=X^E`A?kvR7LjDb#~%+YSfuyMNhsFtr2-XFZuf5HyI-9y!ot*e*JEY5)#=a zq)@b8>f4&g?|q7fM$EHW7LfC%8T(Og0;h7+=%L+*5A?a)pjnf00{e6$*_5}xSm=bl zK}KT1<|nIwg++BRq8MP27o~5@+}bnD!z(MzSD%p2-F!%vw*=K!Gsbg_MdjLF^GclR zhc9+aQI8x3iRxJWVcvNawJHe_%q|4<5Men{A1yKH(LW`k?u5#b2!!O;h;-iWxSwvb z(z8bU*!5G9yaj?Ar=UR+L9FK8fUy|niswpciYEhKvmyl7 zQm z^y)&vEMb|t2#%A{ECMvsq5P(s2dsnKwobIURplC!?PqyuE~f5Z&J8q0e>c2q2f4Pd zsA6cUr)xfhJN5{fN!k_#vf_h5x)r2Q0cC#q%$$@T!r6*3%}A`|ZSua&Rbk2OrxhP? z6RmQQ4_|B4-!y7|z(#JDf#SYQQ@gQI^+8D@BD#@+|GL#gWHVK_WM1`(!~;j>Yc5!~ zSYLU0^zQ4SyH~vVW%cFdntC1QRp?)iWy#gy?r_d#U5HEGb@t`Xv$&m|Z5JrbS>;b& zyiN9gk&lEYUo8_loz8!rPsq8E$)ZiSoQTx**){Gl;utb*Z<2F?TIBe1~^?47@T76ksz z=Zx{;y{dnHD<cu8tbKuHt&s?cdAkDB&J(t4*@OPmh4JQY)(`+~A~N zqpd*-%EveCKf=$tEP^AiqY!SqkWuJ`K+DG`@#XDZxpW@Za& zh6qNT2bZnU(o7;~c%BEZ>5D7Fl>uH~4y~}QivbF8FKGP`brIO^1quAPpdmp(>@tCKqL zCM3p6q0V$XUfy$#o)586RwdPC{tx#pijHRV>*FwQmp}oS*G^1c!jM` z*Yz}E2^a?nicA0IYiTumN>ik0PWM=}YW-<17yS{D+v_m@+**A{v%;Fa|byCcW@ zHVp~PEeKuGHz9%!V5?n4n(Y~!>Prm3@4vgkwq#&qXM16{VVGz=_ca6(`Nz*!^R3|0 z?W-Przv!>9+oIbwxv#v6|2txm{>O|>0NJo)?*1i!%L)80;~pq_9j)K3NHr({{xt<$ z!!}=gz9Dja?JuWj{dd5j&4Wni|9mkOF1a`CXG9ab&xpI_vw@^9kP}U(jKQ^LX@P(u z;29Q^I1|rfBwe1Sw)1i+Uv$9pvL?9smAh#8IIb^wjf@I9Du0l*0HSR1)2&1Ww)uQr zqZcMxvtx~&rVaFR27c)DfjQQilf#P%p4@W0t~0R)=#W$I6UjUn;O<0gmwkiKkRZJ? z;0+sDS*r6=#=a`buM$?vx;00s{@|^O(^}E?`@RloQ@f|#jrN0?6X?^kS_}dd#Oyr` zvrKUW>WS{(moZ;^y+d&;)Q0FrgO|t*y*&f1>oyH_5iGkB_+9WAseG++_2wl%# z5W38EAS+bVJNi$i>GeQG`h>}lbFafNp$)(WzRF>m4MM|$f{x8wo3GQO8hOLnyevTp za>ev#je7-iQa<+s!+tZwW1+=%_qKXQqqROIxM=&_Z62J}u}xv#jX-VCwM96Kpdd$;o_*)%2%s~xkOxe#I3;Dg^uM5Ev{0WlQoX?`pNv^nM-KN0z z$u6u#3CRJ1H#*441*Qz6qO$g!q#+j>v^;8ON`p#>Mp|p?`y&VX8v8bRxxAAMde#IY zu&6G`LkxImRsdDDTh<8Zq0^Pi#-5`?+K8#v<0n3{ z)JQWgNr=btE_qYe1#u_RHBKxE(CF+dJiB*Kg+g0JS+&hmS;TEa+;t5oqO(}SITm+p z=`+XRd(^ed20=R#2B?|;TY)kj-mlSq^bu(jIy7Wd!JG7>j16tR4jyKpCBs+FKH9HN zPJbzuG$+Y)GQ;(3E>%6qDdwE)yX!j)Dsn=nIq?S$2HFW=B_bJxGmKG`2JeYs9(HPPT{+Qm~$(AZ21R1E0on_|_1 zDFQ|K z1q|7s(1(QsD^9K>gn#q{M3d}IY;-5?;2VSIbWFt$^(EL};od*Gpc{hcvwin}}hMTs>nnZ$Gm;(8o#%Kxp8vbY!&E zoq^-P$BoukiI&jFBwkj!zu9g&_DRGADc2r&c1MmG-9~GQ1E&I zI6;;07)EPO&5%}0OqWuT@@0+6XX1vak1to22A+kYCly33NmTX4PcDc*S=ivw_y|nr z8Le?)oN#zil>fKf}I3{{o#Ekr*~c44f{nnen`7cXWz*TB3%t7xj#hK$JSk`zT@2`3Z!!*FOhYS zti;U60geIW^iq#^UAR3cX|sDdo(csa>Sx!82bbh5$3KRzyP}b`_Z#%;?|!?^nN9@; z!se|dL_Q{(z{&|%n-m^Bxo27s;nx;eN;z2^n2(aqdq@X{@(SxkY&fH0?YA&+Pmti> zb1!}T#<2Gyw($r8`fpL4+d;aswb$_a{4U>InH9|aa@wLJ)GKx)uYwt?F zZMinIK&dQYtt}qbdGl=f@662U>+7Jmw!BR)>Mm*VCkzChYp&q3Ahw<|P6Bv{JF?m6 z&#vveVgC+n^-k1LQ!^yhsSEd(!k!3OFv~)=zy+LnfBP3LieUsO$`guQ0_2qU)s@4$ zhGcTLcyI0Zc$c9JJb_VygnC9>uOGgaXM-28d6(&yBXXxVUlde1;V|@b$@*TM9AFe= zUYz=4H4}t8v z^^1M{>yRe3F!!>}3xU==j2eCEZWMG0yUy4I#zPDpw?EPqqpP;h)@^EP+^tCz7sHNhrbVd9et81EU0+r+#Wa+SO7d9Gt1oP zD~WI2k#89_Lz|Qo@ru?3G5JbBP9b~iLj@s()n5Ap7Fnf#ecf+ExFupM=IwMWZRuj~ ziSVB`*?|01(zqH7I|@Ybi%kRAcf+S|TuZt_T4{?%$vEN}Og-Y`^7IH!w^mp7YF_B| z6ZiMERWh?m$r-sH)#(?pn;=crK>Th3|3zj2LYndhluxd!3ja9TUV%bNPkIizNa5^? z5dAsc0Ig?cctg(Vbyx?Amo8Io6XO$QHGvm6Z?Bn`W`wk>>;Km5uhnq1HdD_VIW$&v zv0DK_um9e(PvQ&|*heq(J-twgzG;E&T8hH6i1B83{qq~M*X9%7FMXX+HXK?Sm}Kqb zh1Htd*wzICBY5}nD^*!p>H-sB*d%?{%h(8PUytj*(fT?~;0Sv3;?i@nFha%K?y7Kx zy^YyINC2$La31Br?7O-9X)d}WGZ-|m|MD)ImPYd8ync)IIhi*3WSzQC-a4D!rFqN! znLMaoXZO?*hR`;g=)DN?@Mc_9%}@tjjv>)eezsr#;mCJ|(!iCLC+G$`&XM?0eTlLP@{iN>8CZX^6Ihm2yT_8eY@evPrV7`W!?+gg3XkQ$ z$~wR>o0ba9r=O-D%C@E17qoAExNc5=M>C{JHAAru%khfNr_9aMD^Oi;sC&JcEm@|y zj*)xnUCEp8^XcwG*N$W&%Te9Hk}$AOUXD+2-Sb5O|4@IHdsRy>@L!*=Lk^*;3ou96 zOXzj1M^mVwRx&nt8Z~`?C?7d=g-`h4dQu<7@hWtf50Tj@7tT*TwZ@zayl~;V8}_$X zE`$*L3#amQj@?oYA6(4)szqCwueNc5~*Fo?! z2RKP$7-;66i;Gr2FX)_?ge{2ha(3*kFON$NZuzj^-862z|G78A5^dO2LD3ImUIbqm$MNx?y0GcOn@#7(%u|)0W?U z)si4AY8kN}*2EzAoqv-d?Mr=OefLaA{y(B3C*uDi8ok5A0o+N(y4tCf{2DxncL}gXE83 z*QA3(O&(^J;KfSZ7i0hKzG6f>kHE`G`YwHCjx=rW9ttVMoPn!&;lE}_3a+k^+2FR8 zznt2(#)=R-M*C$im}VjK2EQFyaII$OosY%Pq>@4#En_W94z<<9z1CHKgWWRz(ud z8pSoN#@R==v_p+(?ZwHR%kvHCoRyF(vNXk`vV9Nt?A<#m@HV`WNE+-cCGRf+k*%EI zCyz%4OR4iOJ^~DuMm4`;xFlCf`E161GkN6nZ1{xk<{5ps$(hO&lLOn+xfGV&Z~7qq zjsE0?G?xia>`L2g+ig6ZHow(P%V+h{V>n?rbH>Qi{BX)VN!;^8#)nV!4{0;L;nUE5>M4)X*V%=xouOseB>7PGD}rN@ z_5VfKn}zgIK-F`hH-C_5|F++tt6m6P1a$^~G) zpyS$UkuGGI?9Kbi$?1O~ei~?1uJj$;$-g73QdlZJ>DBGb43+|IQ2tKe_guuIAh3N`?v! zu5|d&0P+K=5e82WpsPD7`G@Z0bN{@7-6r$d!p)KoK!17T;jYVG#ET=v*A!D)-~1lJ zF4;cJ+}ju0M|tqCWKr{U_x=W7R+c|?52y8be-dA$@IuPuj7cZsP_Y)BuQQd393V^Y=!7g73t1JzG8 znQELtHK3(u*F?6BfhaJ_ekpG>85|G7!b_mF-2dkG^n`Bf2hn}6*8xZt2!%q68ym;c z@zPxfayQp==0l9D{PojFtNXD2E7zvqOYUdeTkZth6&c^uq{+D1NWYoQtGe(G1Ji$L zlKpQ3^!a0;;;q=2^6V&lOM|dB{?%=Q%irRe&T19^-=}m_=gZ{5k)X5Xl*O)4B}*mDQ|vyQz! zmvdSyr;zscfG?-i*w!kR6k$U@&8WoIccs5@II!^YPO9zRyh%6?%BULK>>BfyL9cg^ zzUCWusTAQmJ=?`>%fDPM?g`qG5JZDWI`#TtoJ1n#k!VsWyIS{_Whwj2bZ58^l{#im zdg!Ctr@PYlcAM-DHG2JTQyHG>l*65@H(Mc~Sp@f%@S{!{vLkg4ZS~B}r^Q}Pb--W! zE!@p3TMztcGIVagPYUr!4Sby!J|^FW z_c(gA_@F&V-$fT&VtjNvI~sBFGfB1GRB1)EkSz%@w&! z&fiXzSMk*AE^~@^!Wz!iUDr4#R_)1*O{$yx;NoAaB_&o@ggY5DmiHF)NTV$}DLY5- zQPKvao|0Pc0`k;N8;Ax!fU^T*aAmJD#zfkeS?`Yu=(|2@S$FwYAQ_$lLKj}!H1rmZ zla*AeCB-gj#_#f0Au|YLi0Kkh*kJ$iM2cb5{vZ<)BbS0Wqq?}rW6pe&pf3ODL&jB? z9=F*F-0u*`gOQEs5aW$MB$cxs5IjD;6y!@DkG>~MRmEY6-5+`LOc=+}o(~$+uK0g| z*2H|f9Mru7u=C^;$zX~0X|H!M_$wq$=F}L^@VhFp@ zhhZj3r-|QjDa!Ea&CT9r{W4;%x085Q>{xT?{)>l=QtDiLPCgmSlX_f1wPg>Vel)QDrDv%OBhyE6u4!na=F{i3CQ!(7xu`e&cb2-Z#t3O(X zX3Av1X1UhhLm?u+;6ISthK25kryGgF-s#xz(bX&3?qjaG;Qz)`zxFLa^nSg((XFG` zrO<)eCCal6hfNRU{#C!ffwFqrSJGnPOUu}`D4l|97ju?l;R-b?^;U&kka#e3f!{R! zQ{2>?Pv1GgEJN|%l?f^Juu=8nFVX(3zAWpx=>6QE57Nu&2dGy$Pi1@?z4n+R!O%wG zJ|){nc>~Wud9w@h7oLscJ#lRGCn}>V64+pU;#_cC*a(Ay?#n1!Uw>?O*zgUCoC&2^ z3&C=6=nmKKpGo5iHZMc_Rz^J_S>)5dTOf;=)21sj#+&sh|ElwSm(;coY9%mo(;k#CLLEY1c_3{*%^93d z8d(OzNAL8i`whHfes?=V5iUbkmvjNpb86U)0u{XGF^2Th(5|D^sUtrxJncP{fS~Ns zF#QEGzmC%f4MB);I~b@n-*4&8P}LEOV=a=s=QB)FMNU2PVaDU*OQXa&5AKCdVcYdT17vyYm?Sr=ZmGp+; zGJu{YQm&Bkp-1m1)B7~b+y_t#eGR?v0hKV0tni;LW@w+@>j93V&5~V*`R^<_f@H-# z?>+wkYD)?e&7auEy)fi+l6wyu7tz>aC)l7($24@b}T$X>;Un+cbC94D!cHmfuie*Z`L;Y+>MqH&Gx6ZlicWJiB_dYmQ#;YD2%AiyOYU@k2BA^8OF@Kz9rcA z24lF^T9Fewv;Obo^fsqAx_$9x*oY-=k?c3FVc0P;km1jraPbVMRI7oA0>jWWU}HHD z>r;S|=7iJ`6^qG84`?oSmEs@JTE(DB-o&OM$lr608UnRpjxzV$D%YkgUr|nt0X7im zwm4joEy!c3&BZXjp#96egcaS@W~()oUi&WZHp!WtIArYb;X3|XT6tevol*0;JyxzV zzPF(>P9g7HL0es9r|k_MMzHCF`FpQ>;cZE3ZURu9*cazG61>XK^P8LIFrQB0XcW^$ekrbh`>5Ull%B}*(=t6N|)e`2%8bvSt})BL&QnVsCU$V_KRK?NYK)#hBj`Mc}TlXK5GpjIw?s zeA!StQCnn}Q8;n(nSEcS&hT@RjAKS_O+)-ET*nRESRGDK^5608kML(EY`{?Be8>In zhGIcpdb!#tp&EIodhX>?W2L*$!Mx+lTzz#fy++H*Oe!Y_q|p;+A56h?Qx^B_#}9(} zn7eiQ+TdDHJ5xsJ3fU*NjC$e<&KZ)&uI>Rj;ib3HBU6n~Pks2cGg+rO=^t$i1-!1p z$am!(p>!EvzvXSu``Sh^@CzpwP6uCcIHa}U^Csa2k-=;|v3ZIpDtGD=RRUTvremhj zEEeUd5h^WrlC$O>Ihvs9_udDOrB>a$zC>O^FIs9lC$V{S}|LzeHt>7SJ z1WP|pPN!Gvi!v~aA+BK_I$Hz%0WrRt_E%3x`mJZW+O9pR;_KIcaw()!X7L%*AZHO~ zTWFvBH8bGY!H7(E6xV<@I_Xqwcrkxwt5<z`lerdCx#Uvc0557!-*dKul?Ha|-&ytc|}jC~G>IKKu%5Gv|3Ja8U9i@o#2y2or6! z$XY(@s}9|kpCsirbhIY$z6T+uY_3ars~CEu$fmx=absg6aI5ppon=8G4fl+@jz4z7 z++F#s)4{t7Pjl|0xA?ECo;)QkxK?z8wm-ek#^TE#T6`9I#O|EFhs^OUz0)hP`brPa zoLvm#)S#b=$%#eqw%NnCrYunV;yT_?5aw~>(cL6ZQC}Mn zo|rK~`qdU9M(b(98(mKs=CRF^HlPs5MjBSHzkc}C*{N~NO?c2%G|Z;-)*R*zMuV33aQB%|(auuVwX-4g8hOgIDEUKVJ%Z2s}4Rqkl|HiXS zOf#eP$G|-ETaFDQ5q=84(y~XX@Q2O->*27m`@^t$=u0h$l@w-f_$cf>fm9m{ntN~6 zYX9(SKN^eo=E+NBdCg90GRFASluz8?Npg%qY^(vMN9FPcP7?IfCW7qZ+d|~dFXT$u z1zcS;JkC?!Ws)avHewZjAHs@AFz4J?xbAo*=A^TE;BlKCT6@smf?2m7&T|f)zbPHk zKI`Ue^r#%gDo9`UPzuspeU6cCW=Uw51Rq%L3D%tSfGJ(HsuV3xF!3hh85Aoy5XDRVBE| zCGkKsvfKCOg#>qjsq(%C8>6(1GQBll4Z-?Ayz_C%jk3^pTrBb{nBp_c#n-cQx*n{r%QXKGDXzwh|NPm=u{XbV~?y5Vr!Oca2Ws)K={toa}5tC0#a+wNR zr%YE_`TkvQJS!eVM@pKMKVco*Q5F&O=z)wV=S8&b>=DIFL+TW0!01~MCsNH7PASk@ zAR^Yl7Z*v|EavCSXFg*oLvEERgct&&x@hx)SkS&5Ai`?Uvf>ZZn~S7c#thDV-?@gi z`_UCHgwz=mA@%KboYG91ZY3(nWzSAka_HSjrb$r{m-YCJzL(fXh z;oxj|VKrhX)nH(xbgF2rX|G;KQY%ZvB6y#J>ZW&$!tu{ik(DJ3yS^-L)X72)!9cM# zb-W#qwL|rI!`BW2DZe3nLrm)?J?H6Vzx!eDxL(d`p0j&IKN)VW1MQKVOQ-c0vA#vr zIWztATAPz@rzrrGQQoONUXp?hBx89`hJjrn+k)S8&ncvTueg{~iflUUl)P(dp6(f* zB$0FEnepUGpC2!na>j3;yr_-FzgO2<|4#m^T=R!fGtS4X7sJ=D-Z786poetsLB7@{ za@l&LE=G^%J}<;cfq#2Y+x%P|^M)A@^}&OLIye1Zj9w936YW$Es?=j+&vaFFaz0Vu z))HhjJW;X_ugQ&!U9}&Y$rpfG$1l~z$1*kt5HvO0o!)7Z7QdOyzZ^q2;DX*|`glG+ zx5jpB%2xe{C_R->=VYZjRd$zNjaI4^gdrC>iJuRymZw7*dUCss9LF$!c-)*Fk32Ko zkY9t$93vkqJfSwb$p!HaQ%&TpK`XgzGc^U*zjTVzP@8c{D`_j_5Bu^?dMyQ+E0&bp z8WTbh;?ItuJxiFa@P$=cbaXf(lT$;LU0oN6a2&06>1bG` zBfb(-=r^kZ{d!mSc8rIK1!hU7lEJ4bwy*k$TCfUlw1k)}gRE>8wv(0B6j@>;t6LS* zS07^4#sy@~voE<}y~{(CUXTU&gH7lc39n$}oP((Z8W|^U#sd?x)HXD9G2|x3;SIyA zODTA9ZS13i^Mt05LLA2I`OwyBZLxynm2h*udg=q11R1wzR_nL@8g;%?%1_8K9d6{E zQK*3UAhB32evaB8J)xHHJvX9s^>b zyP|&!FS1{YDC{-xeUYMrI8w$`I&Jy*|#Y`jV?xGBzLrt zo2%_v2uR+(9jqRVh)p6lcQvd`simGV)Ht_zyb-XAlHWHiqbt|L zZ2~i&o@cvfRC$6K-DkeW@6hd0R4(_fa|wm0xXNwR^Zaba>(LkNTbjDLgeY8$kG*Cc z&il3Pep)%=d{&VDBs9EvD7t%}&4BIde<}iidcR;X7QecFs`sDp5+C@Z}F<3^$mK)9J~qzP}X^ ztehidR@UsRQ3Lki2&7H3$MOT5^)bq)iqrHyG@V!wIw3kVVQ}U8DO1DUKT@9k~GJ;bpCGO`B z=Jg8F9kX6o^o;?b80*OBQ1?SUdf^wG){SDP8RuMTWR~~WHxx&&lyO;zAin-SP2-F7|8mnt z){h{7J`gx{H?XBE^;*Y*b3)6$eEAawOBP3se0`>CyYB31|DE6&?i^~;>=};y{#>Gl zP&a*TnkEPddbK%oPqL4jz*@=yakdcWU{pHa;cN7!6$DtA!lCZF<+lqaA4yob8}%YehreQ?`ZI-%N)yKlX(onI zLxig<_Kerk+Lx>N|JSR%OGw9lvGA zFr1-RHLC7^S%IqUY)W~F=^S+dwBFfS)m8Q|fZ)RHV)f}sXTYvdm|74GDC@@y7S+<3 zf@V>#d%zSM#*DbC%i2NTd5!S;D5#PB&tLP|jI#LgsY+qWU20pw8OvmrfzkXWf`!;s zyN~dE+UJdLpKP!Vt?E>ctbrXw7a4W${ewL{E6w{qvX@s$U_jeBbMORhlZK0GXe94n z!)FDE7`+cnFyRaM%kkj(z1*7;JP|g5!&%P?7jLoi=nPKEvHb8c=);TPR@0KFTEF&o z^fHZb!Z&d;=X}K0Rq#IgUs8)B4&3Vv?he_sg&D3c4B4x1_ax^V=ZbnGxp)dLSylBE zS}KSbNnQjQ^K?$>%=??2_xGG&Y%Z3RAe37A#S?No0n5DWJFHdz(Om$%D&ND1&}U`= z+_*3kC2B>V$vz!BoDfT2x0u#r7V;Mxn`>i*;0H?aXVOLc-^l3EuJt+HTQ299R?MGO zF?XBCwMKH8G|%~-1AV6fepUB*KBnTb-Z|>es`e#GFh#Wj(A`ZXV6!jBJm}Zn35y>0 z^iVJ0<`N2cbg=yZsopP2h%5gUh9rh$2&{*CyI{OetPd=;M6o^f4iW_@>uxg?^dhXx zALVI;sr8@YrJ?GDr0YG>szr zR%~3Gygjv$>tTh<7>Ag!XUsd~X?%$-W@8VuonQD)Z?MN%4S{3OAL34To|@7>oEB<3 z!@2$Xvf=i1w|ftgWcmZ3E~;_**^A`$GD_f){%0qn)L!@RlG<2OI|rOCK*UA$wZm;} zczTH+jJ(d17hjSwE?@|7Nd!$Je_WvCsCYsK*{NDWOyvvo{bWeHzaPc9>x%Axu99Qx zGFKDJ_6^qy{Znwg z<8KA^m^X(9R@$X%ZkyjQ`(hF0>31uw;cOQ{H_h%#zd_YV&MUG*MLtX{7pj=!a2b|v zgZF()jQ2A?_ohLum^>xv`^&$6&1Bn4i%}@%8+8WOdG&+m!dFeW#+#{0^MS^Qc7{8; zFLI%r0P}9D|2VBa_nTNl4w7!5x#7zJ$xE^nJK>F1wY#{=T%v1Ye=LaoUjPD7CC9Zj;|GUYE-Fo-!on#dHrae+@-jBEgVBaKpwkig$?xSf?v* z58dg-Knt4|Y@Q&s3uZU8Vf$MI$do9&v#2?@b#CD+V&|}gQ7K-lA*rX|9w6eynKEt` zqp`Uep4ECk^I0}`ddM8G`+&$>u zy2BLAwxy*Am!8-9hp%-WWr5o;X&r3}GJxcjZNrUAm}xwghn2h^lz{H>GXQVZk$rz_ zDpuJCpzZMXs-05`L>@r)&ERKt)m(j1Lw){u5j8n1Ugn-EroQ>L3W(F7nhGsHjb&5} zTps%Xyrq1nTQn=SniKV3xSB-<923e18n_ww6lfL?Jr6PU?xl?dG8bWJ`G(o2{Cek# z3>KChgPxWom1meeDrc!Nbts|}#R%P8yqSrLH~H7a z6u!c74oL&RCf`V@$}$67H{^luf960O=a)8 z>iH0HZJC>YGP|TVb>y&a-$7+0em;VwZ=k08y| z7?`D%MxjWhoKkGKtgCP_=7=%O$M$n#&dk5b7bnzP3+r^EdcSN@T)Dh=X=Kz-)QQ41VD0dxx|fep&sJEfT{T^1J{nV1 zI-jv1NzL#mSxoF>;B$%Q^4$Gaok#vJdph>u8tMl6J)z!zV=*Do! zk8510M;CD@TxH(C#+W>qpLILH9WU?2tM}ZzLBO_k*g!JS4}U5%=M}fwmj>VjFTm7D z?YdN#@k(uO-IXc6k#0m;C&ELK9gb@LKx$~C-+&4(CU@5D9ndthrf)>NbGR*ILHF6X zZ)nkr9J*z}$AW4u%qWT|y~Asvsp5zT&_K2AyV&AdAh6RwVWP@bZ+Bkx`0V>ODWw8x z^k6;-wROJ2`CsF>x93++5FV9nkZcVH!v(1Ic?37&VWeQf3aXa0)Q^z`Y+CPy^4yVN z;qPGOX%+f$x?$FPyNhl9uUn${Qc9Rb^x_C6Nidh2M6;A1DW`$=X9*lEK1QHE$5nWW{m-?Py4va_N@%aXR6j4Pq!U8EfY{vJBAD{z z$rrbe3q(chv!Me4aBYxsorGOd%jWH9rwbPJnNxPCW0%TiR|M^Na+505$9>)`DzZeL zIv-^T-Ek1HG=Ed3e>9spRE;;eO$XiTV7VXxMRHIx#z!e| zqn|SU`Z0%aFV-!|AE&*dCbPAAiMBYpC9S}<%bXgUdd|VJHm+>PHkSO!m$-fl{m{2!I1v~xB)sf5>!NW^i<0D>- zUab3B@BLYTA^(#;37B5H#S@mN4(7FmqHJ+|4Fqj2>$SP-T;#Q6uW}8&LQ=5rV#3Ee%hf4~aih|!w^KlP_(6f%akXF7WvP*A_3qC0J#(&$W zBfND>blH^>N;_QhO=#<9F|gGVITzv?uA}?f%Cy0MACRno4Cj(Kj!G^oYZI0*JlB^~ zwA5W355^J}zs!UeZpe~0)XtElI&Kqhuu^U&Iv#~tx6NeBhHt5Bp1o{|F7A&FA4M)B z!4N={fGDEM=iDQwc&lyBxRURMVBCsub9xwP9Jo1zD4BI(<~NI&N^hdx>q6RB!TL^i zw6nA&+*C!sj(mj^Bvci3xl56KS)4sXR& zmT^7NjnTZ~cmJ=XYww!(EVUoopJQ1-$o!158NV)}<5c0}S2cp}i4Jm!aKSD9VWl`4&Fcp-b^mJ(Br|r&UL;1r<j|Go{N|0ew zHNcvhv$U8N!%-2rB{v&g^i#L}zUeXE@c0l-SGOV-g2L6@2RzDBpX zCVji4Kpo;s|BzW4ap3b0!By>BKbi$$hQmQIg+>n5zMv)yW9I5gsx)G90b!%?2$xVUc9@{NP$$NuFiSh} zhg?}bxDzxqom+3Qr7&r8?_UC} zd+g4(d%_9K6yqOfZoDD`w2*#ob<=I~e+HhzMQzs#Z%#968`bRhpT1?IBZ7A ztOCJuJjN;3F;measoLOCgC_!0%|d*%2H>#z#T z8?D@AbHlV=Mm+0ul;zLdw$iL#=Ki;qwAgXN=z6_O71nCd1&h%py;8G6l&xTjzbe6+|wz5F9~Q&;G~sPh1g6^dp3P7<0Sj^1nv&i z<{koCGFeAJ=Pz>l2PdoO73Z0&+@$r zXB~S`RkvcgUBU4wR0VyW5ZxisKFt2YW`2?_wW24S8xiM$% z--carXZX8T+<^E6*zNjC!M*pp->@lS?5!cLa8{giM$0z>W5_kw(P$pLQS_a7=&Jai zYOl@R$UYqpEvG%oq0q*>n>^TpKZtJ#7s*xYepmk|BlJqgNf@g4%<%gL-dnUSnS1P^ zgxYV!28+xXl*bZXetiex51 zFp`+G?QK_Lu84nm_uvZ`Hi5TPd0QX~dIdE>kP+V=FQR=@N;VkBU81 z+3HB%1wQB(19tpEpG1x685+x@Ey34cnw0zx?=nX0cN(gp4#`3jLM=yZ(;I|%q$MTO zUv=43w~8pDN`#~zeL{~d<5M9}w#16yObJ$D4kMt`Kv|^6JWv+C?LYX4 zX-iC7DCr*g-aWd!Sp{jndXrJ3m~My5uAhiim-}=I>F|F3Zm&N-DCd4A)9{h}PjAy& zd-)bc2*uH=lWp&}hSM`BX9#@NhGR7(6nT_HDM#Q^S3=WeyhiVJNcPtya8AQHA*nl! z2B={3EGu2jJ4Sx=oR)h6`4k-#4w$Oqh8o(NlM=AUGJ#7GB)c#XCj%W>z_{0^fmOeN zv(d}#cN_XxUoe|`Zr8&?Ouj??XNX;3J&Ao{Sr)@AWjvpX0Px@ShGyIuq;uAt3mN~j zXHa`9RsIp(V&G+%XzIoE(}kMMTz?pJn`rR z(RiZ^bq9$?O*;`(@56vtQ^rIsmR?1`R94Ta`}F?+r^SwIjg6-Q*TW3Ogb2^t`fsS# zOZbuFxQUUlos7@%xOZEPGd0Rb`N=kx~;AE%-;tmyw*cBphXIF;Hod2ig30Zr7o3A2uHS9!YsTgBzPlbRT|HnbI2N zT<-ME!SuGJ!b5c!`~A`s=~b|??bK1%?S}ifszc3!xnM~%u5Rn^E~mBeM$v#i7I`5f zMO_sU=1*{BQ6l*(lNS|wc&LfOu7^lahYFMm9t?)hCy0(=;%C{50}dsf8s6PtXrk=y_e|3;A%(1(Wqd$~bA z%I%~qcNL;G!t%Jm`va80iA1;I#W!GG+q%>;`I5*QF}olAxh%#B{(k6!#trzz)r(xJyS&#M7_8{F?jkh+t6W%8F(`o@Vu0gE+j$ zQRGXd1v86S$9UB>4gN%rvbu`-^%JY)fRygRwGmCt-8|3h#t-mt53a-oEaTm{EBg0V z3wKza*FWBDV+M-p(PPL}Xs}b`_d4I;?Ea$l)E3@6-Fsh(ZNCzwDTr-#G`9BP{jsVk zUoYuRLaYRdyI547_%QT2jh$db*!ldjZNtcA>zZ=MV$0H=1EP?tXR{uFdH=n7zl!yf zn(O~W#6zcZWJk-4$0)1Tj8gqU|FPevRVw+}Jxbt9DU3amGNOFKL>W-Oj%Z1;>0^_D~L(&BP zZ^Y-%pdbvPD*{31W1qUOm<>L4YNppi*mmb%WTQEO|0F5bXH}xtx@myWRi|E$E znj)XU7LdQVAz%q;2c6D}16Ttj>uQL}XOk(4B>Mqp!S>AmYgb4<${n~97LhiQ%a_b~ zkvyGx?jz+k{{pIKvAeE-z+PTg`IuBPG{5y^o9tVL^7s-r(aa-O?>ktS_IaOKdd=!k zZ)=&A{5y}4y6@jLXSO!!UDM0qYi0ck)fWZ%(oo^|;*K4NthBGN_mcBny)9f{RpqJDXR&zyA_V|nG{&K42aSG-&U1%rqfK-(bO zjQZ@n&B_0-w*i~VolS0}$*S{V#y5P1qp*GLMPQ%Va83~`W*_Lu4Gw44VS8qJ`q_~> zbhTrekiipH$lCq3mxKc^T(s&O!O6qu&*ZRGXJ)7h#3#(xU__@qLce1V(D>o^!!lY0 zoKn;+^X;ms=VF(LE_jcyQ4S_BO-C?NJvIE154Ip0@S`cQdi5!@^Bcz@tzA-hP(qj% zn5FEj;ODt$Nolbt)s|xmtJcirNE7pCv+jg4%Dm zahw2CS}?%KUD+tv9D6&18lRs&DF$G8oZ^9Mr0u9}&BtQl6ZIXptJIbfhR#fbwb0U) z;A!}7Nc%5+oE;Sa$OS{iXC~|w&fPaG^qM7!TncGq!j7xx>S%=i-!&)$I-aIbO<;N3 z6V`jnr^Ad#JwMB=E@IMa9?M@UJ#7zh7IF{*vP^GAoz60Q&g~d`mYkJR2_XRG@ytVJ z#QvGUBfdE20X(5Wv&=Sz%)p{Z(MGhKFoOEQJG5n zGNDJiJoGMGl6B-_Rv)hiX2iMJRu$;;d;oT@G4u=a$p~R~o$-EAfIfJ^- z3Yst$hB*Is168G$dEV_RH+I0h03>fitI>(`(#NG%ilt=l(_=%mC>IHw(<%*26I4fD<(;!6W>~27)OrNUA*b z$>~XoWC?M5)a~-cNWdBn*m`bk`Z#ywnv%+_@ighmyM9;2vn%ca51)9yo3ZRB zKB$Zg0IbnYu;ZPzfnPaLP1te(;Ups9M~yhLH%LV3%S2tK;D{}zcEIY!F}W;R_@~0O z*6%B-pI21zbc#1xd|V(qaP~I8LL{D#n7lOTQxN=1t z*Pp9He;R<=1QIlzhth2sgHEL;VO!u>RyJ|evZ#TJotD6c{a?SOtD2NN&#*Igr$$cAGNrB(SJO>ic;KGqo-B)_Q3C!prTizr&m?QO#uH!rmE%cWI2 zfA2FpQ3VVnE2uHlz?|NJ#pO4wF6FyR0 zF|Y6{gCk)(;2RHPgD0(P<&RuU=fYKt7~~F6cm5{@fq@0>t8z}M^ZIKi&zz&|SOI=2 zdwy+t{)Ut$Bhf$N4ptDtJ=-yuhF&WLm5*n@8DwRnmgX-{ll|rDr@q@D@rg5C5Utd> zyReBZ_Xkl!PKEsdK@oI*k&LjfXTIv1*ibrh@BO^f`#+^U((kz2<^#(4(9DrrzT1Ed zx?#cr-mqw=+5^pF4Y%l%EXOyO8gOijX{fpX%-1HtZmKJ6hP!PEbRK??NQ!(rfiO(DD2)5&!ubx>`RM%~b}*GU~sa{AR}8IywZt)%hrr zPga|`!aE4c2My<{sh5F_R94B%;xblnr3!b8IQ}B=3H9NV$>s*Z8!U$|x$KwE8Mdmk zf!n#lUA_@OLd7;&`2jxCagRx{m6|KQ>ohIY4fTDw$cgmo#kE~hV>KR8=2%OsdLXD0 zglCgO*t^fzF*JtxVKd(e!trjC6$rCZYE{Fu?-;#p-c4Oc;(hAFJD>JN%kLCnY(0rz zx(ONcmWpnVOO$<4ivf$zmVz&JoE?vcL2q+$4tMA{zt0o~(M??QBM8L7%=);RV4*uH zSHaUEW?L*juON>V5$%3HA?qVp6RFV_sU6`37GB%(_Y^0)MBPaNQW+()y!#x ztVal@%-l%C2x$Aj&-?sO?6^0-*yig@Aa!!3VtGK= zZ=|goh!5RJ)CQK~HOvJpMXdy89}GG!h<75+x_XE;st5Ys+8_`It~|GfeFh$TrFdJu z&OV75jL}_d%4|Ko;;2sQ{bGD% zFx@-GVZHN>p9zGQE?cfhaVu2l^rJ+ zB9^^4tXAl$NBtoUCiWh(YY<18B`@fAqseS2M-A}nzSJ*& zTB!hwI_iq{VeG7&Ue^SzL^xQ%W{ZIW(^z^&gU49F>J8*}jmX3_>fywTUY{c06OV6Q z|9LJne9<&~*MIu0e^dtb#1j$$hMs!2+Pc8w8X?>ISFjBKr@vTu9~n%!CeiV&5+O2Y z4wslteqpRs-(5hvmDIBN?tmRh>akx9n>3Kp86BCh zLAGxM&v$G$BK=EzX9Sj>U#vW7(s+q2`{k~lGQ~p)U{zuZ$fgBR8e-lg_y9!1q zt;PR-vVUBXB6rtamb=!JB(~{YZ%L}{KY7UfnPZyHh%j#CzO?YlJ3m_VdOG{zMxWX- zT62q$jm!x|h0pM_C5{A~!I&NX^7r`Kjjq5L38Z$)@{b?0jd*YBq&drp=?LP>nAJOC zA@SfW4)PGF>qA~ae|+F{A8!uTAo$aJb#{bTlq2VQ!rUP;Dw<&!OE;sH|AHC}iHdTH; z65p@+7dMcvp10YWs1TW$Nb)xkDby4jX!98#-jm#rQ>Tz_}PZwxB@9ldPk9F&sS> z>@vs2S;KL9#DByM*~KN>W&7@fT39<<@+xSvoi{(=HPx5U*a*5ub5JtMSZ(XLkD1DN zyp;mV5=)%8WM<{wpRuC{z?wVAlZY$G&Lyg*Ts1|Dds|0_TzI2hLqd$V9{hQrusrGc z##%6@7+%l03J1441o*|CB}$Jo{O5RFT;|FT;$G}`4shD=0g6x#=)fn-PRQoYK*h&R zLCm37n#iWjrrvGzx7gd4$Hsg#9Fxqk-CJ8D2ie4h)aUE~7jf*e*ZyOd9_FVxcX?A) z$+?sYiA}*nQ#iwo2GxyuOv;S0xv1~t;hlKXmvGc*-C;}}TNP8AJA(dG=z?TF|4jqM zBUgQMdyrqhuxj7rH zngP5V(FPxu6GN40OC`5~)lxt9+gR`Hp(1?d7er~B+(AxkW;{FpQdMXUav=~0Wd=U2 z6aE&o)_~<(+#4MpVoVVOsGLalQJ^^IwZ-Ukyc@Io<_VsB_M8Jm>VIV3<{!Yv0M^q_ zNa6hUc3Ks?lB^kb8qc75dnmz!t51IIFP{QsIuLx}<0}&OE>Zgu$T#b~8>ltxRc<7> zi^$^8kcS{|LSE@@DD*?Z38`ROB<`-^!?&eR_aK_E7g9 zbCvmJt`hvy^s1zj+fSzI$*&@v2G7a>feu!+aHnHd=BScghA)K^GC$SmPAuI~2D1 zV)>a#``%o6CPk7xIRpRnGdyb`e<@}m`c$u|sRWIk7_>dNT+qu&M5erqyBA_y6BqKX z5Duzh887E5#as8Nfy}jviU+`HuDR8kV~ZOwY?V%YZ-JrBjG5asMe^6o59FxETE?iq zrBmM`Hu=A>WJi*WBG5yEoWg*wTL_H{^mYQJT?8Si%h4xxFnVXK4zB5E3d>7VOV>*v1lW|$)0tLJ3c+N-Bm=B98U6EDKm2nGMa1?dFMJLyW`d2 zanbF{!Gah8xEZmduD>{>)HII8({rjj_w5%kc5biYfmMW?=2=8atR}sjy{&SE%feX{R)mgkDeTYTP5f zBk;=B%i56(Cry?z{kk~3H4=2A=kWm9R6YO+ijHm5-<}o0Ae_rXVg9MhFw6hitX|E!wwX7bN<(QPr)9#M>0Y3v*N7o&4~#vdEaO#k#$b;mqn| zqm4joic&)^POQG)9{Ddf0)i_JJ8s0h=EaCF_Y4Q9(%yfp?fOqtqGkvb43q(hlB?W< z97S~GOfll3B46qZ=nt=UXVea`8evw8frxFUz9|UJQD7OKj2}P)eHC;)GS?hyy!koG ztok|4zms+Z@)jVTK@W>~^KMP@NT}pMxS#fg{ncQR+NWZU9;vj5(jpbIS4g>}Fo=sJMjM8h%2JZ0GGv$;g_O#gCCgB@$}lyy8q1V! z*P3O9nIhXoW=u?B3^RuJnW^sY{dMpCz3=xQX8V56^E~G{=RD_p&N8$0D+$W1~l(-YSGe0dE2B+|`nCHY3_V9q^ zrzW?8nMeU{jw&kN6}c3s5E3EY0TpEO&annR=5g4sw2z1-(PyQk?^V>{q1pC z-aXd0l^JQw)w0oXjyrKvGim(=0=@cAy| zSN@s&ChJ1$$G_Zp!>1>mFwbRny&uS_qLq9yzK9C}6=hmTxWiob$pfztQ128znM@}7 z0mWxJu>Uepd~7vg!M4*=yEXqE?a&H{Fhme> z5oo~p%tP1IJ20SLuS#0w+kk-{!%j+ft z8w`Y(9Rls1fab9Op*a#J7WD;_&zo4E2ns6Xowdtu&w_!E+Nak&6z-=}u~F;nGhV#| z3bVLRN3r&?7Y67*lW=~5&>pS|0@1;iX{%P!#LQzwd{EA-tF^8#?wnl{!4n#yJ)kAu zgm-JY*!L6nsfNZ|E4=4aTQ*ALyeiWQFM{)Y`8eeI&g0>shqQx8o^!K zQFe7kc6`3#3dF3tZvm7Msn=)q1>`)ZNbY%XC*mkad=J4mw1Pibvb|57&#$G;FN0+N zkfPsRHBaJ-E@1j6s{abP7JA-hF%URtL{d~(v|Y4y7Dm$Yy#{fTgJmmcAB8m$?qjb{ z8LmVwgBXG`-5nR5N2lrd5FO|hz$LR}LW;Jq4@60^NR6C!7bKs~Ye0_^O{M`dgCrVp z)}d>kW|-6)rQFxz(&@rLT!)JaOEz8XfgY?s1pm=dQfQci3)IaOY08WDBu9+?`g7gn zW(~ptiG@c-3m@ftKYgPbRF7_7RR}yGhwD>iTYLYCdwDLB-%>_&!G|%dxj(U?J0`yIP2Kc)w!<=JB-EAo-Yx@A1+dM*p3oOc z5IfM|r_fHE)DIj$-(^>}aR}_A(b#tH?V;cc!fy&&>xtSM_?EEbg31&pwhK11u8l3; zTp;N`I~*6Ypj)$GUb929ewXmu9RQ2~xm_7JWF9h>9kR?V0KnpeE`3P36%HKn^jUZP zuRj^MtN~8G!mt0m*rFE@q^O#f+=GC1GzrrCo(kXZMBOhc65N%uTAdktfD07R z6}3Y7{=4xw;m*%A2vCsEGDVJ`%JqGpb%?r7Qt~^Wg&Wb28NA)~2ylE|*FQL2G)j=5 z2JSpE)ua3zZdQ4V;Xr1q0}3)AiA5SkbdTIgFzXkc0|MBLL3FM^DCyC9O##fg zI}Px3=G2y|+JKTn0YY^uyngbv=mn%pUU-}WPZ%xegL>&@QObI<2*>_&nuTA@r39Da z0LWyqx^T?j-|qh!7x;F>(e1Tiu^a3|7fIy6-YOICHmb;c1Rwo#6-Bov2Ah~@?$086 z42yp@7CHfi@BYh(g>UciW!JDBm4!`WgfIVVa9&WXAX7@J=*tq;sQw>&Nc2nL8BDZ1 zr&6T=r%vDX4;yePa~GzI2tWUqMFa0fT-r7s(%!#_;kI^3SG#Ejl(EX!f1gkYXz(Ly zMkKm>1POo>g#Vc>|4;ULlDPY1;mf=@!|~U-hpx;G#Z=oQtG@!N_8+n&vVA5$y_%c@ z#l_BcKRfLW1$TM?q7tHe{}ctH7ouTYGOJ$`v*T*x;WwM^EXkSK1jY%}F_C;x}><*Dyy{+j=Qeg9A%ezuL4CcxIe}1hVIUs-> zI@f*YT`sRJS9~|M2&RZ`m2iyuq^*%9@&Enw%1hnP#KlLVPHWtclJneG+o2@!YGVxc z%o$|&>CX%1Jqb+Rcz0Q!07|~iL-84}?F(1KUz(e51q(NKqcT4`(B!E~JPT#Ms1x6? zTtDCb!!3MOe{cY(j^Vw%{*rZLIIu4D$95l09q#4!K2e;0l;V6fT>V&jV8^%kzL@*1 zn)&uBV{uvHi*t5z?_8T{*U#sm-cQqN^lzo4Mv=&Tg=eK{zq2MyH2lNtu%)M=_r|Xb<0=R@4RhkzU+jP zj_qBykoT7~ycLf&zRsvxRcu!)?KQyQ`HZ}rn@>_*ae3QArw3Oi8R#73r0u~!=oL40 zXrWIc)gJ}cZbR>le)Aw<`3?L$;C$a-REmgSAwv0Q7PB^TnNP^YRqy&aZ?-MygoJgp za5%>EWE@WKwY!A4XP;m|raa-m2m)?^KuCx!^r_{qQ)1=6)kK-t>6{#A92g1kML373 zce0eH8+rU-)xlf~iK&o-Sa-v&4W;q@ljUdQWN`*l*pCVu>@}~sAE&)iL89g7d!G$6 z4~))5QAhWix&=gjq33fb4Y`S9N$$AI#h=#wnPYv`-X=^p;2^NjMc8^F%|9)a@AM0m z))O)4PWQ%KUU{{XvnQk`XJFfFLetss+M0YXrkUjlsJ!G?&(|2|_GE_$yl;=;`14zm zv(iPjt6syZdJ-dIa&YB)QHqU=ur<*PK?XNHD#(=>4z-I3BbpyYtm$SLan3}{; zfu1S(;>af-N3(ZjvmRUBR~OwiZdV&(6-K9@?*SX;J)lKM)xT^>{hdl;=qf4LvgL4b zZmn_TIA^B-zb{H^gR}V8w`U{p*3KF_=ol$qzM#C^ETUhL7E~)uxv628kXTuxMUwYb zWIEj6!3`u8iW?4PJDZ<_6eNaPJ{*g~;gGls*#kqN>A}ammUyVEXFw7;&KC6w&IdeL zuK1slrt7vOg&sKu4Q(YQ-%h>YQ0w2jEte1-(z>dMCB}oliFGEuFOP`bJoA+}8#B

IK8EX@xN1!)(TfElU1ZxiAP=A-++ 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 z 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 diff --git a/docs/images/architecture_cdb.png b/docs/images/architecture_cdb.png new file mode 100644 index 0000000000000000000000000000000000000000..85aaa3c7d9ab9cd495eee95432c68e12d375bc7c GIT binary patch literal 137264 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

v}WTe5_I$zz~oIzW%tE0o0!R)(S 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 literal 0 HcmV?d00001 diff --git a/docs/images/architrecture_pg.png b/docs/images/architrecture_pg.png new file mode 100644 index 0000000000000000000000000000000000000000..bca060c2aaa6e473039236c55da69b61f4d7803b GIT binary patch literal 141937 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 kLG&qaQ7$H5uuYMzk+58{(=y7vB@4 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 literal 0 HcmV?d00001 diff --git a/docs/images/cwyd-solution-architecture.png b/docs/images/cwyd-solution-architecture.png deleted file mode 100644 index 0d41503fd2dbee9246a614a37ffae0f17ee75171..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96156 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 SuB?`Xc3J@sNaBD(AhO0HADQ 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 diff --git a/docs/postgreSQL.md b/docs/postgreSQL.md index 67bcfcb4e..c2c2ca947 100644 --- a/docs/postgreSQL.md +++ b/docs/postgreSQL.md @@ -27,12 +27,12 @@ This structure ensures easier management of environment variables and dynamic da --- -### 3. **PostgreSQL as the Search Index Provider** -The PostgreSQL `search_indexes` table is used for managing search-related indexing. It supports vector-based similarity searches, replacing Azure Search indexing in specific configurations. +### 3. **PostgreSQL as the Relational and Vector Store Database** +The PostgreSQL `vector_store` table is used for managing search-related indexing. It supports vector-based similarity searches. **Table Schema**: ```sql -CREATE TABLE IF NOT EXISTS search_indexes( +CREATE TABLE IF NOT EXISTS vector_store( id TEXT, title TEXT, chunk INTEGER, @@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS search_indexes( **Similarity Query Example**: ```sql SELECT content -FROM search_indexes +FROM vector_store ORDER BY content_vector <=> $1 LIMIT $2; ``` @@ -58,7 +58,7 @@ LIMIT $2; --- ### 4. **Automated Table Creation** -The PostgreSQL deployment process automatically creates the necessary tables for chat history and search indexes. The script `create_postgres_tables.py` is executed as part of the infrastructure deployment, ensuring the database is ready for use immediately after setup. +The PostgreSQL deployment process automatically creates the necessary tables for chat history and vector storage, including table indexes. The script `create_postgres_tables.py` is executed as part of the infrastructure deployment, ensuring the database is ready for use immediately after setup. --- @@ -70,13 +70,13 @@ All PostgreSQL connections use secure configurations: --- ### 9. **Backend Enhancements** -- PostgreSQL integration is limited to the Semantic Kernel orchestrator to ensure focused functionality. +- PostgreSQL database integration is included in the implementation of the Semantic Kernel orchestrator to ensure unified functionality. - Database operations, including indexing and similarity searches, align with the CWYD workflow. --- ## Benefits of PostgreSQL Integration -1. **Scalability**: PostgreSQL offers robust indexing capabilities suitable for large-scale deployments. +1 **Scalability**: PostgreSQL offers robust data storage and table indexing capabilities suitable for large-scale deployments 2. **Flexibility**: Dynamic database switching allows users to choose between PostgreSQL and CosmosDB based on their requirements. 3. **Ease of Use**: Automated table creation and environment configuration simplify deployment and management. 4. **Security**: SSL-enabled connections and secure credential handling ensure data protection. @@ -85,4 +85,4 @@ All PostgreSQL connections use secure configurations: --- ## Conclusion -PostgreSQL integration transforms CWYD into a versatile, scalable platform capable of handling complex indexing and search scenarios. By leveraging PostgreSQL’s advanced features, CWYD ensures a seamless user experience, robust performance, and future-ready architecture. +PostgreSQL integration transforms CWYD into a versatile, scalable platform capable of handling advanced database storage, table indexing, and query scenarios. By leveraging PostgreSQL’s cutting edge features, CWYD ensures a seamless user experience, robust performance, and future-ready architecture. diff --git a/scripts/data_scripts/create_postgres_tables.py b/scripts/data_scripts/create_postgres_tables.py index ed6465efe..605d7634c 100644 --- a/scripts/data_scripts/create_postgres_tables.py +++ b/scripts/data_scripts/create_postgres_tables.py @@ -98,17 +98,15 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): cursor.execute(create_ms_sql) conn.commit() -# Add pg_diskann extension and search_indexes table -# cursor.execute("CREATE EXTENSION IF NOT EXISTS pg_diskann CASCADE;") # Add Vector extension cursor.execute("CREATE EXTENSION IF NOT EXISTS vector CASCADE;") conn.commit() -cursor.execute("DROP TABLE IF EXISTS search_indexes;") +cursor.execute("DROP TABLE IF EXISTS vector_store;") conn.commit() -table_create_command = """CREATE TABLE IF NOT EXISTS search_indexes( +table_create_command = """CREATE TABLE IF NOT EXISTS vector_store( id text, title text, chunk integer, @@ -124,10 +122,8 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): cursor.execute(table_create_command) conn.commit() -# PG_DISKANN is not available yet -# cursor.execute("CREATE INDEX search_indexes_content_vector_diskann_idx ON search_indexes USING diskann (content_vector vector_cosine_ops);") -cursor.execute("CREATE INDEX search_indexes_content_vector_idx ON search_indexes USING hnsw (content_vector vector_cosine_ops);") +cursor.execute("CREATE INDEX vector_store_content_vector_idx ON vector_store USING hnsw (content_vector vector_cosine_ops);") conn.commit() grant_permissions(cursor, dbname, "public", principal_name) @@ -141,7 +137,7 @@ def grant_permissions(cursor, dbname, schema_name, principal_name): cursor.execute("ALTER TABLE public.conversations OWNER TO azure_pg_admin;") cursor.execute("ALTER TABLE public.messages OWNER TO azure_pg_admin;") -cursor.execute("ALTER TABLE public.search_indexes OWNER TO azure_pg_admin;") +cursor.execute("ALTER TABLE public.vector_store OWNER TO azure_pg_admin;") conn.commit() cursor.close() From 2a253ae6a6523b35b49e6fc43bcc12e39afcd94e Mon Sep 17 00:00:00 2001 From: Francia Riesco Date: Mon, 9 Dec 2024 15:23:39 -0500 Subject: [PATCH 107/107] Update postgreSQL.md --- docs/postgreSQL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/postgreSQL.md b/docs/postgreSQL.md index c2c2ca947..5e982f57c 100644 --- a/docs/postgreSQL.md +++ b/docs/postgreSQL.md @@ -76,7 +76,7 @@ All PostgreSQL connections use secure configurations: --- ## Benefits of PostgreSQL Integration -1 **Scalability**: PostgreSQL offers robust data storage and table indexing capabilities suitable for large-scale deployments +1. **Scalability**: PostgreSQL offers robust data storage and table indexing capabilities suitable for large-scale deployments 2. **Flexibility**: Dynamic database switching allows users to choose between PostgreSQL and CosmosDB based on their requirements. 3. **Ease of Use**: Automated table creation and environment configuration simplify deployment and management. 4. **Security**: SSL-enabled connections and secure credential handling ensure data protection.