From c61c355ee6645398f4f5e214bc15da0030013caa Mon Sep 17 00:00:00 2001 From: Tobias Quadfasel Date: Sat, 31 Aug 2024 17:03:58 +0200 Subject: [PATCH 1/5] feat(ai-chat): Add openai python SDK to project --- poetry.lock | 351 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 351 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index caa9cd1..25a3107 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,39 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "babel" version = "2.16.0" @@ -305,6 +338,17 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + [[package]] name = "docutils" version = "0.21.2" @@ -422,6 +466,63 @@ setproctitle = ["setproctitle"] testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "identify" version = "2.6.0" @@ -530,6 +631,76 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jiter" +version = "0.5.0" +description = "Fast iterable JSON parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f"}, + {file = "jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d"}, + {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87"}, + {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e"}, + {file = "jiter-0.5.0-cp310-none-win32.whl", hash = "sha256:f16ca8f10e62f25fd81d5310e852df6649af17824146ca74647a018424ddeccf"}, + {file = "jiter-0.5.0-cp310-none-win_amd64.whl", hash = "sha256:b2950e4798e82dd9176935ef6a55cf6a448b5c71515a556da3f6b811a7844f1e"}, + {file = "jiter-0.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4c8e1ed0ef31ad29cae5ea16b9e41529eb50a7fba70600008e9f8de6376d553"}, + {file = "jiter-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f16e21276074a12d8421692515b3fd6d2ea9c94fd0734c39a12960a20e85f3"}, + {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280e68e7740c8c128d3ae5ab63335ce6d1fb6603d3b809637b11713487af9e6"}, + {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:583c57fc30cc1fec360e66323aadd7fc3edeec01289bfafc35d3b9dcb29495e4"}, + {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26351cc14507bdf466b5f99aba3df3143a59da75799bf64a53a3ad3155ecded9"}, + {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829df14d656b3fb87e50ae8b48253a8851c707da9f30d45aacab2aa2ba2d614"}, + {file = "jiter-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42a4bdcf7307b86cb863b2fb9bb55029b422d8f86276a50487982d99eed7c6e"}, + {file = "jiter-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04d461ad0aebf696f8da13c99bc1b3e06f66ecf6cfd56254cc402f6385231c06"}, + {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6375923c5f19888c9226582a124b77b622f8fd0018b843c45eeb19d9701c403"}, + {file = "jiter-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cec323a853c24fd0472517113768c92ae0be8f8c384ef4441d3632da8baa646"}, + {file = "jiter-0.5.0-cp311-none-win32.whl", hash = "sha256:aa1db0967130b5cab63dfe4d6ff547c88b2a394c3410db64744d491df7f069bb"}, + {file = "jiter-0.5.0-cp311-none-win_amd64.whl", hash = "sha256:aa9d2b85b2ed7dc7697597dcfaac66e63c1b3028652f751c81c65a9f220899ae"}, + {file = "jiter-0.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9f664e7351604f91dcdd557603c57fc0d551bc65cc0a732fdacbf73ad335049a"}, + {file = "jiter-0.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:044f2f1148b5248ad2c8c3afb43430dccf676c5a5834d2f5089a4e6c5bbd64df"}, + {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:702e3520384c88b6e270c55c772d4bd6d7b150608dcc94dea87ceba1b6391248"}, + {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:528d742dcde73fad9d63e8242c036ab4a84389a56e04efd854062b660f559544"}, + {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf80e5fe6ab582c82f0c3331df27a7e1565e2dcf06265afd5173d809cdbf9ba"}, + {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44dfc9ddfb9b51a5626568ef4e55ada462b7328996294fe4d36de02fce42721f"}, + {file = "jiter-0.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c451f7922992751a936b96c5f5b9bb9312243d9b754c34b33d0cb72c84669f4e"}, + {file = "jiter-0.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:308fce789a2f093dca1ff91ac391f11a9f99c35369117ad5a5c6c4903e1b3e3a"}, + {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7f5ad4a7c6b0d90776fdefa294f662e8a86871e601309643de30bf94bb93a64e"}, + {file = "jiter-0.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ea189db75f8eca08807d02ae27929e890c7d47599ce3d0a6a5d41f2419ecf338"}, + {file = "jiter-0.5.0-cp312-none-win32.whl", hash = "sha256:e3bbe3910c724b877846186c25fe3c802e105a2c1fc2b57d6688b9f8772026e4"}, + {file = "jiter-0.5.0-cp312-none-win_amd64.whl", hash = "sha256:a586832f70c3f1481732919215f36d41c59ca080fa27a65cf23d9490e75b2ef5"}, + {file = "jiter-0.5.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f04bc2fc50dc77be9d10f73fcc4e39346402ffe21726ff41028f36e179b587e6"}, + {file = "jiter-0.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f433a4169ad22fcb550b11179bb2b4fd405de9b982601914ef448390b2954f3"}, + {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad4a6398c85d3a20067e6c69890ca01f68659da94d74c800298581724e426c7e"}, + {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6baa88334e7af3f4d7a5c66c3a63808e5efbc3698a1c57626541ddd22f8e4fbf"}, + {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ece0a115c05efca597c6d938f88c9357c843f8c245dbbb53361a1c01afd7148"}, + {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:335942557162ad372cc367ffaf93217117401bf930483b4b3ebdb1223dbddfa7"}, + {file = "jiter-0.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649b0ee97a6e6da174bffcb3c8c051a5935d7d4f2f52ea1583b5b3e7822fbf14"}, + {file = "jiter-0.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f4be354c5de82157886ca7f5925dbda369b77344b4b4adf2723079715f823989"}, + {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5206144578831a6de278a38896864ded4ed96af66e1e63ec5dd7f4a1fce38a3a"}, + {file = "jiter-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8120c60f8121ac3d6f072b97ef0e71770cc72b3c23084c72c4189428b1b1d3b6"}, + {file = "jiter-0.5.0-cp38-none-win32.whl", hash = "sha256:6f1223f88b6d76b519cb033a4d3687ca157c272ec5d6015c322fc5b3074d8a5e"}, + {file = "jiter-0.5.0-cp38-none-win_amd64.whl", hash = "sha256:c59614b225d9f434ea8fc0d0bec51ef5fa8c83679afedc0433905994fb36d631"}, + {file = "jiter-0.5.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0af3838cfb7e6afee3f00dc66fa24695199e20ba87df26e942820345b0afc566"}, + {file = "jiter-0.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:550b11d669600dbc342364fd4adbe987f14d0bbedaf06feb1b983383dcc4b961"}, + {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:489875bf1a0ffb3cb38a727b01e6673f0f2e395b2aad3c9387f94187cb214bbf"}, + {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b250ca2594f5599ca82ba7e68785a669b352156260c5362ea1b4e04a0f3e2389"}, + {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ea18e01f785c6667ca15407cd6dabbe029d77474d53595a189bdc813347218e"}, + {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462a52be85b53cd9bffd94e2d788a09984274fe6cebb893d6287e1c296d50653"}, + {file = "jiter-0.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92cc68b48d50fa472c79c93965e19bd48f40f207cb557a8346daa020d6ba973b"}, + {file = "jiter-0.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1c834133e59a8521bc87ebcad773608c6fa6ab5c7a022df24a45030826cf10bc"}, + {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab3a71ff31cf2d45cb216dc37af522d335211f3a972d2fe14ea99073de6cb104"}, + {file = "jiter-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cccd3af9c48ac500c95e1bcbc498020c87e1781ff0345dd371462d67b76643eb"}, + {file = "jiter-0.5.0-cp39-none-win32.whl", hash = "sha256:368084d8d5c4fc40ff7c3cc513c4f73e02c85f6009217922d0823a48ee7adf61"}, + {file = "jiter-0.5.0-cp39-none-win_amd64.whl", hash = "sha256:ce03f7b4129eb72f1687fa11300fbf677b02990618428934662406d2a76742a1"}, + {file = "jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a"}, +] + [[package]] name = "markdown" version = "3.7" @@ -901,6 +1072,30 @@ numpydoc = ">=1.0" [package.extras] test = ["pytest (>=2.7.3)", "pytest-cov"] +[[package]] +name = "openai" +version = "1.43.0" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "openai-1.43.0-py3-none-any.whl", hash = "sha256:1a748c2728edd3a738a72a0212ba866f4fdbe39c9ae03813508b267d45104abe"}, + {file = "openai-1.43.0.tar.gz", hash = "sha256:e607aff9fc3e28eade107e5edd8ca95a910a4b12589336d3cbb6bfe2ac306b3c"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +jiter = ">=0.4.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.11,<5" + +[package.extras] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] + [[package]] name = "packaging" version = "24.1" @@ -1086,6 +1281,129 @@ files = [ {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = [ + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pyflakes" version = "3.2.0" @@ -1460,6 +1778,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -1640,6 +1969,26 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tqdm" +version = "4.66.5" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, + {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + [[package]] name = "types-requests" version = "2.32.0.20240712" @@ -1794,4 +2143,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3f76ad143c2f6e35163d1bc8d123b97b9bf2a96f86914d240e0c7da2226f1d85" +content-hash = "a6af9cf3858b3e05a3ac81716c79d6a3ed8e797da13391b2fc3e8c383741d00b" diff --git a/pyproject.toml b/pyproject.toml index 05ab8e4..061a0aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dash = "^2.17.1" gunicorn = "^23.0.0" pyodbc = "^5.1.0" pandas = "^2.2.2" +openai = "^1.43.0" [tool.poetry.group.docs.dependencies] mkdocs = "^1.6.0" -- 2.49.1 From 923dc3b439c03743d99e39bae3edad0763f80fca Mon Sep 17 00:00:00 2001 From: Tobias Quadfasel Date: Sat, 31 Aug 2024 23:38:14 +0200 Subject: [PATCH 2/5] feat(ai-chat): Add first version of ai chat as well as frontend Includes the first version of a rudimentary chat app, still without the SQL capabilities that we want later. For now, we can connect to the Azure OpenAI source and then have the response displayed in a plotly dash webapp. Some styling and UI elements were also added, such as logos. UI components are designed that the user cannot enter the same query twice and cannot click the submit button as long as the query is running. --- app/app.py | 140 +++++++++++++++++++++++++++++++++++++++++++- app/app_styles.py | 37 ++++++++++++ app/assets/logo.png | Bin 0 -> 9441 bytes app/data_chat.py | 36 ++++++++++++ 4 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 app/app_styles.py create mode 100644 app/assets/logo.png create mode 100644 app/data_chat.py diff --git a/app/app.py b/app/app.py index a83c9fc..797969d 100644 --- a/app/app.py +++ b/app/app.py @@ -1,8 +1,142 @@ -from dash import Dash, html +from typing import Any, Dict, Tuple -app = Dash() +from dash import ( + Dash, + Input, + Output, + State, + callback, + dcc, + get_asset_url, + html, + no_update, +) +from dash.exceptions import PreventUpdate + +from .app_styles import header_style +from .data_chat import send_message + +external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"] + +app = Dash(__name__, external_stylesheets=external_stylesheets) +app.index_string = header_style + +err_style = { + "height": "0px", + "overflow": "hidden", + "transition": "height 0.5s ease-in-out", + "border-radius": "15px", + "background-color": "#FFCCCB", + "text-align": "center", + "color": "#FF6B6B", + "margin-top": "20px", + "margin-left": "20px", + "margin-right": "20px", + "font-weight": "bold", + "display": "flex", + "justify-content": "center", + "align-items": "center", +} + +start_value = "Stelle deine Frage an die Datenbank..." +app.layout = html.Div( + [ + html.Div( + [ + html.H1("Datenbank-Chat", className="heading"), + html.Img(src=get_asset_url("logo.png"), className="logo"), + ], + className="header-container", + ), # Header + dcc.Store( + id="tmp-value", data=start_value, storage_type="session" + ), # Store previous prompt + dcc.Textarea( + id="input-field", + value=start_value, + style={"width": "96%", "height": 200, "margin-left": "20px"}, + ), # Input field + html.Div([]), # Needed for keeping the layout clean + html.Button( + "Abschicken", + id="submit-button", + n_clicks=0, + disabled=False, + style={"margin-left": "20px"}, + ), # Submit button + html.Div( + [html.P("Bitte eine neue Anfrage eingeben.")], id="error", style=err_style + ), # Error message (only visible if input is not updated but submit button is clicked) + dcc.Loading( + id="loading", + type="default", + children=[ + html.Div( + "Hier erscheint die Antwort der Datenbank.", + id="text-output", + style={ + "whiteSpace": "pre-line", + "margin-top": "30px", + "margin-left": "20px", + "margin-right": "20px", + "border": "2px solid #86bc25", + "border-radius": "15px", + "padding": "20px", + }, + ) + ], + ), + ], # Output field + className="container", +) + + +@callback( + Output("text-output", "children"), + Output("tmp-value", "data"), + Output("error", "style"), + Input("submit-button", "n_clicks"), + State("input-field", "value"), + State("tmp-value", "data"), + prevent_initial_call=True, + running=[ + (Output("submit-button", "disabled"), True, False), + ( + Output("submit-button", "style"), + {"opacity": 0.5, "margin-left": "20px"}, + {"opacity": 1.0, "margin-left": "20px"}, + ), + ], +) +def update_output(n_clicks: int, value: str, data: str) -> Tuple[str, str, Dict[str, Any]]: + """Update the output based on user input and button clicks. + + Parameters + ---------- + n_clicks : int + Number of times the submit button has been clicked. + value : str + Current value of the input field. + data : str + Previously stored value. + + Returns + ------- + Tuple[str, str, Dict[str, Any]] + Updated output text, new stored value, and error style. + """ + global err_style + if n_clicks > 0 and value != data: + result = send_message(value) + err_style["height"] = "0px" + return result, value, err_style + elif value == data: + + err_style["height"] = "50px" + return no_update, no_update, err_style + + raise PreventUpdate -app.layout = [html.Div(children="Hello World")] server = app.server diff --git a/app/app_styles.py b/app/app_styles.py new file mode 100644 index 0000000..9d46c7f --- /dev/null +++ b/app/app_styles.py @@ -0,0 +1,37 @@ +header_style = """ + + + + {%metas%} + {%title%} + {%favicon%} + {%css%} + + + + {%app_entry%} +
+ {%config%} + {%scripts%} + {%renderer%} +
+ + +""" diff --git a/app/assets/logo.png b/app/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9399c006b5addefbfeff15c328023abc074fa4fb GIT binary patch literal 9441 zcmbW7WmHsO*!IsbFobjt-5}ipf20JI2I+2)2I*!92`T9kK}ryiE~%j#=@teAB&5@! zp7H(uet4d<)`@l2S$m(e_dff+uHU|6HPv6?;ZWfK0Dz~YD6a(oAYat=8!QOwcx#C^ zg*suoD;jzN059{u9pqOc?Sr~W>7`)crR`?tbxiigzwNbInUw_M-xXsL#j51< z=g&t7`C0<~{QS}w&D^g~PEMZ6wIh!HJsIWK{j;~Xw+Qz%ye&F9`W59ShA#mDL6TtK zlOsCRW*hVWD_(0~UhNz;3U7-NFBvoW>jzFSJ5%$L!O@9D5r-NNX;dI&? z-f{{Q?cT#{^g3)nTV2;P+Z_+`Q4 zK}NsQZuGYQ0avTiK2K!%!Z%b^PAzIjNj|7>UYJ~73Qb?|rhtjYqVyj3zXFRR%y=C; zImb8L1K$@P2wnyqj$z9(3oYr0MHu9jD<)0FzlCX`OGbTO!}K)c2uaGm68eQ_fA7ev zSzG^cXOSaTWJ6mFE<_g3ub8wD|F%yDTR{@NqwCQvyue(y()=wkfzk`Iv!=xTR3Wx~ zvYQunV}Rvo+{veB(V4o=nX5UFXjhTNkqIZ{aO2^aHf}TA~8eP)V( zvW+{2>Hb}fgfF5NA99p1U;3%mBccdFmz0WQLi#<8Y==Y#4F6p?Y5Y9WH|wm@SSUUvbCzldWrpC*UD_~R)cS-U~#XrLX#o@I;P2LA!@C|-pd*% zSG;i+g>}$WIxz7Rq4^`i-xmHQmM@(3fct`$cI}wGZ4zXG9@q@1u zj1gU!rPXNAL$7vbKhS&YspSji2vZxb7Ct9DH7$rK-sGJ7DFL$#wZQtULHhlDW> zRX9EAFLu$nFHPjb(Y@SmxzE&7j|Xud=s@fQ3oW?L80YU(2a|5c8|y{4aRgp^8v98R zxuTVQBfP@gAdCrBdGF>@xwp@`CF2?)LDacokcnI$yuClW+^BCH(1J-}`nuIczzG=R zoQj@IAJZH(XXsOo4}Zl>`|JadzIqFm0A_`zO6BYJ6D1ZRBYfN@F1@ND4BwBxDDr%0 zSkRc%IA?$^g5a7X(-Ay$?CFYnk^`l!w207`r)UxYAvj+EJ2y9VW5@56OZP-vRH3-x zJ(=2o9_-Oy3{^BCe?&}3tM6kQp9p)!j?%L09@abJ;r-j#-C8DeAD|3Wk^A}7q`h^x@w7{2UjP0UxwvC9$BDKMz6~rb zqrF7i!#R?}pKNVB;K@Hbjc>3+H)4TKlWC@vyHy;Wlyh~sEiAjeswlufZ$f3IcV>+4 zo)nZ%pLkkn`Fv}>=l#545y|k^z^uRO7rY2%tena$dN+q^WVBVF2X5&l=4?9Gu)S+V z=R4A7G~rgPPE|(e;~X`g6x_pIe==$0mIVy9?7zG98%)EgxQ-v*7Xesicw&R>A_^ph ztDpQpvpD}OBBs1|duUQE%PMgU*x9O?>F)C6{A*#z%r9VqcT4POkyr4110TD$ z_g4!ti%iF9(BF`TNAoJPpr7GhQE-CM?*+Eanxw<7mxN8+-_*LCe`j>Y!xj|oKWuAt z{T=8c9l5Ey+K+|I{0B5OJ>cL8nN%(Rq7@q4ikEW&W{3QA44^kkp2n9v{a4luW605* zc=oB>Wdk%#?oJQ8g2%2uk`7STzwbk!1JVsF6+fDTTKgju4$2_Ix-1d41P&rJ!Df*8 z7$hTZSV&z^utAV+@U^Gs9poGglq#Grf zWFA(_SYULr=6#GOWU*WEoE(8XE*n&kdxMcr+;@bC%7*q<$sjr?wnI`>GJ})%BWj3g zVaRU3bO}Uz-RQ!8pF;Ul49;0rSj3E}o$m7O z5deG_q+?gfcUfMW4FDc_#BR869kS_5fA~(34n0TMtL%VVim!iyU}1d9i>KyP4-j|> za=+_w3)`CB*TdS=cy1w3Tk(~Yd`oQ!Gbv{iJ!|Z|CxF?V=MjJhnbD+ zImPsh@1P^ZIs=$ZS&QYgH{R z?4?1YIqD$Vcn#r&Zl#EVLLl*P49dsf*HT9XuzNo3qTt*Wy30OQp7ByJ8P!~e__Y@i z2(%>qHtcBn9eWW4XWcL*n3)au)&K9uh)~KVAPi2_|}#7=A@`>cV!` zCHblEux@^mrI(+a|K@m#i!z8MI_UeEz^fvXpNOqaM!#;0Aa5+j<^QR z_pKi{&nc17z&DBp4h1?H%q)F}XPZ3vG>?z&Wf#Btt~Q7EwrY(8rBm4~?DC_WOO6A{itzKUidw7b z>pWA9b%$h!fFO!Gd+|?3*3w-1Damy7528$pqg*yc$vTs-Tb->BuYQBtu@NY<057JT zQr!P&AM9BE)2nUqjwev)x$<+StM>E3VdX~Gi-3ygz}$CYFC2m~w!pV5AFp|8 zNESf_0@IfUSDM$Y7RYf-{QoMHMTSW!z}6>oKB$1-=h)O<-H0Ryx+%2d$7w3LW9;+6 zdQKzyubB6!qj3Jn9Q&J3h@SqZI4H$HcwBZVKKsH#PgUPG0Zy5aJbUSy`?Sx|=R#&m zy0!K#?n~DBNbWdgp7P@W2wc5(3On%uv3j&XA2!e8Yd~Y%z^XEqUjg;D*Ah-KT7~Z2 z#c>6S7_TkhI_$w_J`Tx%ff(oN_tD|mMQZ2f{xicJ|H6OKWw{&LjW|F;=~HZ@jJ|}J zuuph7ts&m(J>2e+i&d9?RJr^0b2aDFpSULU3B}blE4rL|^-@0&DQxvPPOSW-BgeNfmlfHdm@1F%tk%lluT{D><~YS4Uy5$TvU=yW_5A_a zS8Dh!24m30oBZgGW(Zs?T@$&{X%oF9>yL3I0{Q*gI{g(QbYUCOO_NX@qF7LEXoyEQ zJbgJ+(f#3I69X3WP@Q{AdFRBakHkHD;~MIyRZ^yAJo&y5TQ~*uR%pP-~L+fV>6?0zV&h(eR+Qh#I;A7CGMps;$dV36pB?U*X9gBVo zY&wj0OFy4!|MNoVEy}u5J54y~$w(Git~Z37FNjErhVtV(j^X$Pc4Oh3n$#vufp*?UXQl zfkI=fV#{0IgyC$DO@Tqog_Wg8R>R@2OSR-KMZGn0T%PC0BePe(X{JU{WZ(uh9WLsWlr{J`! z1MBlJE1`x_5L+$zvwNhSdL$MT0A>l}>smU=9XHF_Bz+QKqS*E&;tMWf9|-@#*DXbk zLOABOO9r`uKAp4U?lKx&PcE+8A`b8Q8R+rp(Dzn=9B8Jt|_hHg1 z%bwvs)|mr7-~hNh#H)?u0pJ^$K&Q$VRpKV`_zx|`DyVP9o1j>UHZ$OLimEJ^e>U&@ zlVfi0J=|q#NKZBo`9gcuzq(A!k}C3i1)IAw+K~c7#6YB$IcrfD=_K73wW~)1e0bJJ zZ^xXSf^$?7acm}_9lwXtkmQZZL$B%1r{t7Jp_!sob_UCAL~9vM4%8c@ih#vgEH@SI z^qH}qyqP<_Ot)9wafoZ;shBV016rC}rMW+WT*t9Xnh;w@(ym4;Cg=B)jAK!etx zlE@YleH4cj3Ab_9NWmti?}^P`_zHuqe9lNJUo%0edC#m&h1Iyx2q+ApKD+wgq4Uiw zO}|)lnBlJO+-;d99I%&RdJ0yJh&p2?e-Wgfp9V$RcLm;U(JBU|jzb&Y^3*7(}KZ9fcu85=@0t zxNTr|AUNuCE@RDwWdZL$&$-xR8e#X2+o;cu$?y^it8Rr>SQra(8NErEp!%85ZefX- zcF&s+!jZXuq^{-PwQ+b9F-|+-;r76I+i_vi%)q0i_HUlcRn+SsfcvQRoH8_-XPZnc z;vDhNe*Q2~L5;Tch=av`_0aI~1rl?0wZ`QYsv>ScMNK-YK+wR@pAyEnZ4`U*wEh=j zEp=3UhMqEh`=IxYQ>rUUNf9BxN{>1%_v;^}i7L!ULcHoVAB=$4`*WtBN#)1SahVT=Evkp=y)=sJRE_WoxE{2c$bs^84scjk0?oZAe zYMehX%iUW>_TBo)5ruzD2G~$y2iJVaP<^h>Ic4Tu)P2EpdBw`J0>=j|Zqvu)QX(#* z>UcJ3>7Tp^SyTS61uWK`IOf~xxjNkCsV0_XUx5r|K>7mgxJ4SQy=o$RqRC|0>i?X>4PUtt#=s9C-owc)l7R~ffd!e&A`<1KPJf`26AVlf zJo)aarug8Qq?t&$s^2g{70h3-EPHpk;ZruN8E$)rJEPUeDem}kysN9$vc7_NCj&2a z5OeGg2|D?L2;bX2@C$x7a&Kcc_m5uIqJ-U~jqF_>gKKC(N690vyYG*F@7$cfydFWr z;z&>wD>{ytq-O+Wga-#Q7doCZ~Nl&sXVtvONGH7L*zR$H&RQ~aq6>H7^ zbTqO8G9VV`lE85{`13=b^fGAV%lSK~xz{#+hKnW$SVrJD~;Ol+}IFJ4VWBUJS5`}AKx>j3KWm>xmXgb_%w53{b6Bt zei{^Y1GX+c+aOapW?Wv^c^S_dF3APT{)1WJrjRY!jStTm5NfkOqD@M*nsk9Kc`JUi z7@`lMdRbc5ZHh7ef@b&qNO~FCQ2tXr1^o;jGJMSIou-o@LeuA}zw83q)@D3$oq@># zcm(i?w)E6x%6LNKkhlrUnS)iZ$zFv#(wm(3r!?EB-Zu%hZk4a(VFELI_uFB_1d-w; z@2OUWZqG3cTI=okdHY5rouI6b1A-Z5@I+LJH0fltYLL|c{hEs|GLv|RIe_t~pdk_>GvZAaTRCo71yi0#s zfh2Gm)2@!$UmwCZf$^}vu6ChPB*rZj5-3kcr@BdtsCpsu@;l;LL3tU6{~KDQQ)z{k z(VvuJ{pHQ>`-@sKGxy_OOaNurQh_l(*!?rD@3pl}6*q?iCMlyQUu9qqVAugTx6^Ts zF(OdreE*{?Rm3cF%TkKryp(rccr~G$Qy}(@Isb)Zf{1BTUC4XTXy#l*!Y1TeDz7jC zc=;qJ9sg+hO0_&{Yg@)`lHnW&OIjhDgD#<%dW5kC9IO^>Sh^vQ+-O!V`pl~X57F`% zg8N6x|4wXhMWHx>PbQ=~n6~bDhd>H)PZ8@601QZMvZ+a$Eti6e(>dNn)ZT#I(M8HQ zo_BQT*;e)dqa(5^! ztv#HXL>(2ND8l`E2?dg?*FwHH?@3tD@`AFrr)5E#dF%Bd%#Rx3SISYaZ$VI)JhO2Z zn|W0U*WL|h(unN_F(~Eyn_9`Y@>46y+${Vm9S+AhC5+%*t<)RBaY)p<9taDQj z-!h}BR~)g}V=8J_gny~xSQHStDZ|7Os*0bh^fkf%Mu5RC(Q$3Se#@+oQ5-EL$%qR7i*TxkOgQk;peQ}2tv(uKo99!FF{Ep4#V3moY z=BvXt>a8P2IVq``@%vXx+IQi_9UJn0>)wgoaLvJ;t?GeUaxMVRQxuCLys#0PTxy*B{STk8<_)(9xdtu-8-2#n%vWM< znS;-NHmtk)Qv}`gw1RTiLv6_2?5c)Qq2u)!cjc0#(I&jM;cctq(co87*<&+lZyGwf zmPZ2x3m;9*l=F$&oXl4j?Ib64HN&YHjrtm-O%8EmQM2kpEC8-d9J(!&j>;lsjK=R* zf6$5upK8?+XQvXh8(b~pFa7Ku1paut#JqjmZaosK$IFT=2#e0`DG3p--)i^GlwCd$ znud_=Eloc=B#f(tY&a<8Rw}VQ6;h+9JqLvvnsnX6YQ~8YtkenxKe;7f$HsxuscIFB z>(HMwS&UH3cKYk_vg94pA79!Rk6u#fCAXzN62)J@@$rc9!U4G5X-PfyU1XVL3f7T4 zkbk%~0mQ-5xP6xjNEvc6+nWXX)|2X3rUZ?JmW==dRAnLnU&Pz|KF)00-@$!9S_RYm z+%Y#)P7^r_k(*bCl!e`S-JY9HJ>MprP|c7>uwgf^YUVCYjL~0+sq|8vti#V*YzUgV z%*;o+%Mr6JU+bN zcIW@-?Q`@*k1hzO?)E6mVloe(tX9NUMCD)MlI@|P?BzyIipWiB(Ff}wW9d$r{MG^Wisel^`a z*3iZo3U$0N5yZ?C*Y;UcOZh3s1Hrv?2gQFXM3~#^s_ZJ}tW;3Av)wSkkWzse1RJ-E zs=pO`r9^2n-=s&*oIOU8M+-dc&*M8e zX`!cl{Lk`E9jhtR`a?;?h8A0c=p{=c>m`C1Vs4Xis-t4uGO$c5&ojJ_M8R$WE7Y_q z1i7205C%6jDF{kpF33DflD3EL^H!|H-v6WU_*Bvq7hu4mj$;l|U%5+iWZA0xtT2vv z_{5~GnFsYiViqsaxivJV-yp(2-APWK;>uy4fO=tKEzMe{QZNEsemp>}%@wmA@36-do3hJ-Bnp7Ac)^uSqY^-VJxhq1w zBB(?%h7`?6Oc;*1V> zRPJvVip->^n4|#V3A=1A`XnE*Q%?rI9?3AJ?mucPLRk&E^9>KdeWG0I-z01E3~RCW z-AQ}Wcn;J&W<9|=K`cL(^zG5QIathC97(3x)$fLD)sf(?igP4Ru+v+Q?dmTd&z zND|$1!BB2{w}3oG+FCy^IcR_upm8Fi9o(0OjG1b%%|}YQb$($AX)c%?qeK$M9##>nI>uvJCL8kf0w^_lSh@%&eWFg@N z?;A*oytjzAVsI|dfHc>r8Jk+)c%?c_XKMERy)B;mV#(YBhAA0X z@k?Qb=_Lq@O~~uDqTm(;>Pxng)XflnXma|G zX=cAQX1t?xh5EH~q@3YvVQ!qfPf&`ZYq{g8E&g_{VR0Ru3h^QunOn-ewl1*X#i*%Y z48iSTcRj?)7r3ycXDi?$0bT~E`(?n_dBBoI;T~cweB}p5c!+c+q+s3a3z#v9UI`Y9 zxWj5oASkU-wVs50!A%;ddSC}$AF|302@m@$pC;ht5(v&x05nu*o5b%ZMPD*I@af@o z!#ecr_($rl_uS#7FkJqPJF+Uq*W(GFTYRocX@mx&J;#z?c&#-hSgdL}ODAG>n zUZ@5=7g_z4h+cSEUIGi+NI=J(9n{zGDjr>j?zK}#mP|{L5ksEccyEiKRwIhtbuuD< z44L7{v|HAIak8CE2(cQb8a>U{h463eMxS29rt4w@d0!6lMtf2y>}nbZ)oBh05_bia*~}D z9?fLLPLzuT^e*1?1vUVnTm8!gu*Fi5Sqa}0eV-uyR3P|9A#O4L<+QnNshIMZs0YV2 zQ9^Mhzq_RJZQGR$yt4hSG_D!D`31LIV)i-;P31G~{CH7H)If_b@T>F>$zD_^I5p9d zZk=o@YQhER+L~4k4I8PMAyulf9-shu5gZR)kdub;8m<3L=)W{JN2_u08*E1N)DHg5 zgZlu9dJGi6+JDhZp2JiwLHe1lupb9?3x3lrJ|gcPlU){GM7g&i)5#Vz0$O*?v_i&Y+KLW-fVi~gc=r% zPUupg0UXjDZ&RU@0EMsfjUPy?^-`s`$*?ad$`eGJb$NVr<^ z!0Tp=9_`MTcrFV4`8Z;wj{3VEp5BX(ic!ZHIzv;?N;hXzqlx)UGsfCn-UBAWJX0UX znTU-)@FEW$&zcU;P{tt80(_xrZ|++XV*$1=1;RPYu+zJ9}GPyy^03&U4yc-B0aAJ4O_ zRjkTXm|-)A@@{Bw6aF2*eKx2#Ne3ff=s9V}aFuAQ<()H8?g SexlNo03`)=`EM^Q!v6=I+4PM7 literal 0 HcmV?d00001 diff --git a/app/data_chat.py b/app/data_chat.py new file mode 100644 index 0000000..f7c55f2 --- /dev/null +++ b/app/data_chat.py @@ -0,0 +1,36 @@ +import os + +from openai import AzureOpenAI + +# Set up credentials +# NOTE: When running locally, these have to be set in the environment +client = AzureOpenAI( + azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), + api_key=os.getenv("AZURE_OPENAI_KEY"), + api_version="2024-02-01", +) + +deployment_name = "sqlai" + + +def send_message(message: str) -> str: + """Send a message to the openai chat completion API and return the response. + + Parameters + ---------- + message : str + The user's message to be sent to the chat completion API. + + Returns + ------- + str + The content of the assistant's response message. + """ + response = client.chat.completions.create( + model=deployment_name, + messages=[ + {"role": "system", "content": "Du bist ein hilfreicher Assistent."}, + {"role": "user", "content": message}, + ], + ) + return response.choices[0].message.content -- 2.49.1 From 4b9fa0579e42df30990334d1f3c5d393cb774e2a Mon Sep 17 00:00:00 2001 From: Tobias Quadfasel Date: Mon, 2 Sep 2024 20:43:48 +0200 Subject: [PATCH 3/5] feat(ai-chat): Add SQL query field for comparison In order to compare the (not yet implemented) SQL query generated by the LLM with an actual query, another text field was added that parses the query to `pyodbc`, which connects to our database, stores the resulting rows in a `pandas` dataframe and then visualizes it as a table in plotly dash. The SQL functionalities are implemented in the `sql_utils.py` module. Additionally, some minor updates to the overall behavior and layout of the app were implemented. --- app/app.py | 237 +++++++++++++++++++++++++++++++++++----------- app/app_styles.py | 2 +- app/data_chat.py | 25 ++++- app/sql_utils.py | 65 +++++++++++++ 4 files changed, 272 insertions(+), 57 deletions(-) create mode 100644 app/sql_utils.py diff --git a/app/app.py b/app/app.py index 797969d..eaf6120 100644 --- a/app/app.py +++ b/app/app.py @@ -6,6 +6,7 @@ from dash import ( Output, State, callback, + dash_table, dcc, get_asset_url, html, @@ -15,12 +16,14 @@ from dash.exceptions import PreventUpdate from .app_styles import header_style from .data_chat import send_message +from .sql_utils import execute_query, test_db_connection external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"] app = Dash(__name__, external_stylesheets=external_stylesheets) app.index_string = header_style + err_style = { "height": "0px", "overflow": "hidden", @@ -38,63 +41,114 @@ err_style = { "align-items": "center", } -start_value = "Stelle deine Frage an die Datenbank..." -app.layout = html.Div( - [ - html.Div( - [ - html.H1("Datenbank-Chat", className="heading"), - html.Img(src=get_asset_url("logo.png"), className="logo"), - ], - className="header-container", - ), # Header - dcc.Store( - id="tmp-value", data=start_value, storage_type="session" - ), # Store previous prompt - dcc.Textarea( - id="input-field", - value=start_value, - style={"width": "96%", "height": 200, "margin-left": "20px"}, - ), # Input field - html.Div([]), # Needed for keeping the layout clean - html.Button( - "Abschicken", - id="submit-button", - n_clicks=0, - disabled=False, - style={"margin-left": "20px"}, - ), # Submit button - html.Div( - [html.P("Bitte eine neue Anfrage eingeben.")], id="error", style=err_style - ), # Error message (only visible if input is not updated but submit button is clicked) - dcc.Loading( - id="loading", - type="default", - children=[ - html.Div( - "Hier erscheint die Antwort der Datenbank.", - id="text-output", - style={ - "whiteSpace": "pre-line", - "margin-top": "30px", - "margin-left": "20px", - "margin-right": "20px", - "border": "2px solid #86bc25", - "border-radius": "15px", - "padding": "20px", - }, - ) - ], - ), - ], # Output field - className="container", -) + +def get_layout() -> html.Div: + """Generate the layout for a Dash application. + + This function creates a complex layout for a database chat interface + and a direct SQL query interface. It includes various Dash components + such as text areas, buttons, and a loading spinner. + + The layout consists of: + - A header with title and logo + - A textarea for user input (database chat) + - A submit button for the database chat + - An error message area + - A loading spinner and output area for database responses + - A section for direct SQL queries, including a textarea and submit button + - An output area for SQL query results + + Returns + ------- + html.Div + A Dash html.Div component containing the entire layout of the application. + """ + global err_style + tmp_style = err_style.copy() + tmp_style["height"] = "0px" + + start_value = "Stelle deine Frage an die Datenbank..." + + layout = html.Div( + [ + html.Div( + [ + html.H1("Datenbank-Chat", className="heading"), + html.Img(src=get_asset_url("logo.png"), className="logo"), + ], + className="header-container", + ), # Header + dcc.Store( + id="tmp-value", data=start_value, storage_type="memory" + ), # Store previous prompt + dcc.Textarea( + id="input-field", + value=start_value, + style={"width": "96%", "height": 200, "margin-left": "20px"}, + ), # Input field + html.Div([]), # Needed for keeping the layout clean + html.Button( + "Abschicken", + id="submit-button", + n_clicks=0, + disabled=False, + style={"margin-left": "20px"}, + ), # Submit button + html.Div( + [html.P("Bitte eine neue Anfrage eingeben.")], id="error", style=tmp_style + ), # Error message (only visible if input is not updated but submit button is clicked) + dcc.Loading( + id="loading", + type="default", + children=[ + html.Div( + "Hier erscheint die Antwort der Datenbank.", + id="text-output", + style={ + "whiteSpace": "pre-line", + "margin-top": "30px", + "margin-left": "20px", + "margin-right": "20px", + "border": "2px solid #86bc25", + "border-radius": "15px", + "padding": "20px", + }, + ) + ], + ), + html.H2( + "Direkte SQL-Abfrage", + style={"margin-left": "20px", "margin-top": "50px", "font-size": 24}, + ), # SQL Header + dcc.Textarea( + id="sql-input-field", + value="(Microsoft) SQL-Abfrage eingeben...", + style={"width": "96%", "height": 200, "margin-left": "20px"}, + ), # SQL Input field + html.Div([]), # Needed for keeping the layout clean + html.Button( + "Abschicken", + id="sql-submit-button", + n_clicks=0, + disabled=False, + style={"margin-left": "20px"}, + ), # Submit button + html.Div(id="sql-output", style={"margin-top": "10px"}), # SQL Output + ], + className="container", + ) + + return layout + + +app.layout = html.Div([get_layout()]) @callback( Output("text-output", "children"), Output("tmp-value", "data"), Output("error", "style"), + Output("error", "children"), Input("submit-button", "n_clicks"), State("input-field", "value"), State("tmp-value", "data"), @@ -108,7 +162,7 @@ app.layout = html.Div( ), ], ) -def update_output(n_clicks: int, value: str, data: str) -> Tuple[str, str, Dict[str, Any]]: +def update_output(n_clicks: int, value: str, data: str) -> Tuple[Any, Any, Dict[str, str], Any]: """Update the output based on user input and button clicks. Parameters @@ -126,18 +180,91 @@ def update_output(n_clicks: int, value: str, data: str) -> Tuple[str, str, Dict[ Updated output text, new stored value, and error style. """ global err_style - if n_clicks > 0 and value != data: + print(f"Value: {value}") + print(f"Data: {data}") + db_connected = test_db_connection() + if n_clicks > 0 and value != data and db_connected: result = send_message(value) err_style["height"] = "0px" - return result, value, err_style + return result, value, err_style, html.P("") elif value == data: err_style["height"] = "50px" - return no_update, no_update, err_style + err_child = html.P("Bitte eine neue Anfrage eingeben.") + return no_update, no_update, err_style, err_child + elif not db_connected: + err_style["height"] = "50px" + err_child = html.P( + ( + "Fehler beim Herstellen der Verbindung zur " + "Datenbank. Bitte versuche es später erneut." + ) + ) + return no_update, no_update, err_style, err_child raise PreventUpdate +@callback( + Output("sql-output", "children"), + Input("sql-submit-button", "n_clicks"), + State("sql-input-field", "value"), + prevent_initial_call=True, +) +def run_sql_query(n_clicks: int, value: str) -> str: + """Run a SQL query and return the results. + + Parameters + ---------- + n_clicks : int + Number of times the submit button has been clicked. + value : str + Current value of the input field. + + Returns + ------- + str + The results of the SQL query. + """ + if n_clicks > 0: + result = execute_query(value) + if isinstance(result, str): + global err_style + tmp_style = err_style.copy() + tmp_style["height"] = "80px" + tmp_style["padding"] = "20px" + err_child = html.Div( + [html.P(f"Fehler bei der Ausführung der Abfrage: {result}")], style=tmp_style + ) + return err_child + else: + table_child = dash_table.DataTable( + id="table", + columns=[{"name": i, "id": i} for i in result.columns], + data=result.to_dict("records"), + page_size=10, + style_table={ + "overflowX": "auto", + "margin": "auto", + "width": "96%", + }, + style_cell={ + "minWidth": "100px", + "width": "150px", + "maxWidth": "300px", + "overflow": "hidden", + "textOverflow": "ellipsis", + }, + style_header={ + "backgroundColor": "lightblue", + "fontWeight": "bold", + "color": "black", + }, + ) + return table_child + raise PreventUpdate + + server = app.server if __name__ == "__main__": diff --git a/app/app_styles.py b/app/app_styles.py index 9d46c7f..9e893b3 100644 --- a/app/app_styles.py +++ b/app/app_styles.py @@ -12,7 +12,7 @@ header_style = """ justify-content: space-between; align-items: center; padding: 20px; - background-color: #f8f9fa; + background-color: #ffffff; } .heading { font-size: 2.5em; diff --git a/app/data_chat.py b/app/data_chat.py index f7c55f2..369f7c7 100644 --- a/app/data_chat.py +++ b/app/data_chat.py @@ -26,10 +26,33 @@ def send_message(message: str) -> str: str The content of the assistant's response message. """ + system_message = """ + Du bist ein hilfsbereiter, fröhlicher Datenbankassistent. + Verwende beim Erstellen Ihrer Antworten das folgende Datenbankschema: + + MEIN_DATENBANKSCHEMA + + Füge Spaltenüberschriften in die Abfrageergebnisse ein. + + Gib deine Antwort immer im folgenden JSON-Format an: + + JSON FORMAT + + Gib NUR JSON aus. + Ersetze in der vorangehenden JSON-Antwort "your-query" durch die Microsoft SQL Server Query, + um die angeforderten Daten abzurufen. + Ersetze in der vorangehenden JSON-Antwort "your-summary" durch eine Zusammenfassung der Abfrage. + Gib immer alle Spalten der Tabelle an. + Wenn die resultierende Abfrage nicht ausführbar ist, ersetze "your-query“ durch NA, aber ersetze + trotzdem "your-query" durch eine Zusammenfassung der Abfrage. + Verwende KEINE MySQL-Syntax. + Begrenze die SQL-Abfrage immer auf 100 Zeilen. + """ + system_message = "Du bist ein hilfreicher Assistent." response = client.chat.completions.create( model=deployment_name, messages=[ - {"role": "system", "content": "Du bist ein hilfreicher Assistent."}, + {"role": "system", "content": system_message}, {"role": "user", "content": message}, ], ) diff --git a/app/sql_utils.py b/app/sql_utils.py new file mode 100644 index 0000000..079f6fb --- /dev/null +++ b/app/sql_utils.py @@ -0,0 +1,65 @@ +import os +from typing import Union + +import pandas as pd +import pyodbc + + +def test_db_connection() -> bool: + """Test the connection to Azure SQL Database. + + This function attempts to establish a connection to an Azure SQL Database + using the connection string stored in the environment variable + 'AZURE_SQL_CONNECTION_STRING'. It makes up to 5 attempts to connect, + with a timeout of 240 seconds for each attempt. + + Returns + ------- + bool + True if the connection was successful, False otherwise. + """ + connection_string = os.environ.get("AZURE_SQL_CONNECTION_STRING") + for i in range(5): + print(f"Trying to connect to Azure SQL Database... Attempt {i + 1}") + try: + pyodbc.connect(connection_string, timeout=240) + print("Connected to Azure SQL Database successfully!") + connected = True + break + + except pyodbc.Error as e: + print(f"Error connecting to Azure SQL Database: {e}") + connected = False + + return connected + + +def execute_query(query: str) -> Union[pd.DataFrame, str]: + """Execute a SQL query on an Azure SQL Database and return the results. + + This function connects to an Azure SQL Database using the connection string + stored in the environment variable 'AZURE_SQL_CONNECTION_STRING', executes + the provided SQL query, and returns the results as a pandas DataFrame. + + Parameters + ---------- + query : str + The SQL query to execute. + + Returns + ------- + Union[pd.DataFrame, str] + A pandas DataFrame containing the query results if successful, + or a string containing the error message if an exception occurs. + """ + try: + connection_string = os.environ.get("AZURE_SQL_CONNECTION_STRING") + conn = pyodbc.connect(connection_string, timeout=240) + df = pd.read_sql(query, conn) + conn.close() + return df + except Exception as e: + return str(e) + finally: + if conn in locals(): + conn.close() -- 2.49.1 From 94b5545173bc5734457b0e5ec0cd40558cf723ca Mon Sep 17 00:00:00 2001 From: Tobias Quadfasel Date: Tue, 3 Sep 2024 14:48:38 +0200 Subject: [PATCH 4/5] feat(ai-chat): Add code logic for AI-based data chat Add the first working code logic both in terms of backend and frontend-related tasks. Add a detailled system message for improved results. Add several UI improvements for result display and user information. Add text input field for direct SQL code comparison. The implementation of the openAI backend had to be changed due to strict rate limits of azure OpenAI free tier and was replaced with a regular openai API key. --- app/app.py | 150 +++++++++++++++++++++++++++++++++++------------ app/data_chat.py | 86 ++++++++++++++++++++++----- app/sql_utils.py | 7 ++- 3 files changed, 189 insertions(+), 54 deletions(-) diff --git a/app/app.py b/app/app.py index eaf6120..32cdacb 100644 --- a/app/app.py +++ b/app/app.py @@ -1,5 +1,8 @@ +import json from typing import Any, Dict, Tuple +import pandas as pd +from app_styles import header_style from dash import ( Dash, Input, @@ -13,16 +16,40 @@ from dash import ( no_update, ) from dash.exceptions import PreventUpdate - -from .app_styles import header_style -from .data_chat import send_message -from .sql_utils import execute_query, test_db_connection +from data_chat import send_message +from sql_utils import execute_query, test_db_connection external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"] app = Dash(__name__, external_stylesheets=external_stylesheets) app.index_string = header_style +notification_md = """ +**Hinweise:** + +Aufgrund des sparsamen pricing Tiers kann es einige Sekunden dauern, bis die +Verbindung zur Datenbank hergestellt wird. Im Falle eines Fehlers gern ein-zwei mal erneut +versuchen. + +GPT-4o kann einige Fehler machen. Sollte dies passieren wird eine Fehlermeldung angezeigt. +In diesem Fall lohnt es sich oft, die Anfrage leicht verändert erneut zu stellen und evtl +zusätzliche Informationen zu geben. + +Das Modell ist dazu aufgefordert, den Output stets auf 100 Zeilen zu begrenzen. + +Alle Daten sind komplett zufällig generiert und haben keine Beziehung zu realen Personen. + +**Beispielfragen**: +- Wie viele Kunden haben wir in Hannover? +- Zeige alle Kunden in Bremen. +- Berechne den gesamten Stromverbrauch aller Kunden in Magdeburg. +- Zeige alle Kunden, die zwischen 2021 und 2022 mindestens 200 Kubikmeter Gas verbraucht haben. +- Wie viele Kunden haben zwischen 2021 und 2022 weniger Strom verbraucht als zwischen 2022 +und 2023? + +Weitere Informationen zu den Daten, dem Code sowie zur Nutzung befinden sich in der README im +[GiTea Repository](https://gitea.captain.particlephysics.de/quadfaselt/grid_application). +""" err_style = { "height": "0px", @@ -42,6 +69,46 @@ err_style = { } +def render_table(df: pd.DataFrame) -> dash_table.DataTable: + """Create a Dash DataTable from a pandas DataFrame. + + Parameters + ---------- + df : pd.DataFrame + The input DataFrame to be rendered as a table. + + Returns + ------- + dash_table.DataTable + A Dash DataTable component with styled layout and pagination. + """ + tab = dash_table.DataTable( + id="table", + columns=[{"name": i, "id": i} for i in df.columns], + data=df.to_dict("records"), + page_size=10, + style_table={ + "overflowX": "auto", + "margin": "auto", + "width": "96%", + "margin-top": "20px", + }, + style_cell={ + "minWidth": "100px", + "width": "150px", + "maxWidth": "300px", + "overflow": "hidden", + "textOverflow": "ellipsis", + }, + style_header={ + "backgroundColor": "lightblue", + "fontWeight": "bold", + "color": "black", + }, + ) + return tab + + def get_layout() -> html.Div: """Generate the layout for a Dash application. @@ -53,6 +120,7 @@ def get_layout() -> html.Div: - A header with title and logo - A textarea for user input (database chat) - A submit button for the database chat + - A Notification about the connection time and LLM performance - An error message area - A loading spinner and output area for database responses - A section for direct SQL queries, including a textarea and submit button @@ -78,6 +146,10 @@ def get_layout() -> html.Div: ], className="header-container", ), # Header + html.Div( + "Ganz ohne SQL-Kenntnisse Daten zu Zählerstandmessungen unserer Kunden abrufen!", + style={"margin-left": "20px", "font-weight": "bold", "font-size": "20px"}, + ), dcc.Store( id="tmp-value", data=start_value, storage_type="memory" ), # Store previous prompt @@ -94,6 +166,17 @@ def get_layout() -> html.Div: disabled=False, style={"margin-left": "20px"}, ), # Submit button + dcc.Markdown( + notification_md, + style={ + "margin-left": "20px", + "margin-top": "20px", + "margin-right": "10px", + "background-color": "#C7E6F5", + "border-radius": "5px", + "padding": "10px", + }, + ), html.Div( [html.P("Bitte eine neue Anfrage eingeben.")], id="error", style=tmp_style ), # Error message (only visible if input is not updated but submit button is clicked) @@ -102,7 +185,7 @@ def get_layout() -> html.Div: type="default", children=[ html.Div( - "Hier erscheint die Antwort der Datenbank.", + "Hier erscheint die Antwort des KI-Modells.", id="text-output", style={ "whiteSpace": "pre-line", @@ -180,18 +263,28 @@ def update_output(n_clicks: int, value: str, data: str) -> Tuple[Any, Any, Dict[ Updated output text, new stored value, and error style. """ global err_style - print(f"Value: {value}") - print(f"Data: {data}") db_connected = test_db_connection() if n_clicks > 0 and value != data and db_connected: result = send_message(value) err_style["height"] = "0px" - return result, value, err_style, html.P("") - elif value == data: - err_style["height"] = "50px" - err_child = html.P("Bitte eine neue Anfrage eingeben.") - return no_update, no_update, err_style, err_child + # parse LLM response to dict, then try to execute the query + try: + parsed_result = json.loads(result, strict=False) + + result_table = execute_query(parsed_result["query"]) + children = [ + html.P([html.B("Zusammenfassung: "), f"{parsed_result['summary']}"]), + html.P([html.B("SQL Abfrage: "), f"{parsed_result['query']}"]), + render_table(result_table), + ] + return children, value, err_style, html.P("") + except Exception as e: + err_style["height"] = "400px" + err_child = html.Div(f"Folgender Fehler ist aufgetreten: {e}.LLM Output: {result}.") + + return no_update, value, err_style, err_child + elif not db_connected: err_style["height"] = "50px" err_child = html.P( @@ -202,6 +295,12 @@ def update_output(n_clicks: int, value: str, data: str) -> Tuple[Any, Any, Dict[ ) return no_update, no_update, err_style, err_child + elif value == data: + + err_style["height"] = "50px" + err_child = html.P("Bitte eine neue Anfrage eingeben.") + return no_update, no_update, err_style, err_child + raise PreventUpdate @@ -231,37 +330,14 @@ def run_sql_query(n_clicks: int, value: str) -> str: if isinstance(result, str): global err_style tmp_style = err_style.copy() - tmp_style["height"] = "80px" + tmp_style["height"] = "100px" tmp_style["padding"] = "20px" err_child = html.Div( [html.P(f"Fehler bei der Ausführung der Abfrage: {result}")], style=tmp_style ) return err_child else: - table_child = dash_table.DataTable( - id="table", - columns=[{"name": i, "id": i} for i in result.columns], - data=result.to_dict("records"), - page_size=10, - style_table={ - "overflowX": "auto", - "margin": "auto", - "width": "96%", - }, - style_cell={ - "minWidth": "100px", - "width": "150px", - "maxWidth": "300px", - "overflow": "hidden", - "textOverflow": "ellipsis", - }, - style_header={ - "backgroundColor": "lightblue", - "fontWeight": "bold", - "color": "black", - }, - ) - return table_child + return render_table(result) raise PreventUpdate diff --git a/app/data_chat.py b/app/data_chat.py index 369f7c7..4cc3106 100644 --- a/app/data_chat.py +++ b/app/data_chat.py @@ -1,16 +1,24 @@ import os -from openai import AzureOpenAI +from openai import OpenAI + +# from openai import AzureOpenAI # Set up credentials -# NOTE: When running locally, these have to be set in the environment -client = AzureOpenAI( - azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), - api_key=os.getenv("AZURE_OPENAI_KEY"), - api_version="2024-02-01", -) +# NOTE: Usually I would use AzureOpenAI, but due to heavy rate +# limitations on azure trial accounts, I am using OpenAI directly +# for this project. However, this is how it would look like for +# AzureOpenAI (credentials must be provided to environment): +# client = AzureOpenAI( +# azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), +# api_key=os.getenv("AZURE_OPENAI_KEY"), +# api_version="2024-02-01", +# ) +# MODEL = "sqlai" # deployment name -deployment_name = "sqlai" +# Set up the OpenAI client +MODEL = "gpt-4o" +client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) def send_message(message: str) -> str: @@ -28,15 +36,60 @@ def send_message(message: str) -> str: """ system_message = """ Du bist ein hilfsbereiter, fröhlicher Datenbankassistent. + Du hilfst Benutzern bei der Erstellung von SQL-Abfragen für eine Datenbank eines + großen Energieversorgungsunternehmens. Die Datenbank enthält Tabellen für Adressen, + Zähler, Kunden und Ablesungen. Es werden Gaszähler (MeterType 'GAS') und Stromzähler + (MeterType 'ELT')unterschieden. + + Besonders wichtig ist, dass die Ablesungen der Werte kumulativ sind. Wenn nach dem Verbrauch + gefragt wird, sollte der Unterschied zwischen zwei aufeinanderfolgenden Ablesungen berechnet + werden. + Verwende beim Erstellen Ihrer Antworten das folgende Datenbankschema: - MEIN_DATENBANKSCHEMA + CREATE TABLE Addresses ( + ID INT PRIMARY KEY IDENTITY(1,1), + StreetName NVARCHAR(100), + HouseNumber NVARCHAR(10), + City NVARCHAR(50), + PostalCode NVARCHAR(10), + Longitude FLOAT, + Latitude FLOAT + ); + + CREATE TABLE Meters ( + ID INT PRIMARY KEY IDENTITY(1,1), + Signature NVARCHAR(11), + MeterType NVARCHAR(3), + AddressID INT, + FOREIGN KEY (AddressID) REFERENCES Addresses(ID) + ); + + CREATE TABLE Customers ( + ID INT PRIMARY KEY IDENTITY(1,1), + FirstName NVARCHAR(100), + LastName NVARCHAR(100), + GasMeterID INT, + EltMeterID INT, + FOREIGN KEY (GasMeterID) REFERENCES Meters(ID), + FOREIGN KEY (EltMeterID) REFERENCES Meters(ID) + ); + + CREATE TABLE Readings ( + ID INT PRIMARY KEY IDENTITY(1,1), + CustomerID INT, + MeterID INT, + ReadingDate DATE, + ReadingValue INT, + FOREIGN KEY (CustomerID) REFERENCES Customers(ID), + FOREIGN KEY (MeterID) REFERENCES Meters(ID) + ); Füge Spaltenüberschriften in die Abfrageergebnisse ein. Gib deine Antwort immer im folgenden JSON-Format an: - JSON FORMAT + { "summary": "your-summary", "query": "your-query" } Gib NUR JSON aus. Ersetze in der vorangehenden JSON-Antwort "your-query" durch die Microsoft SQL Server Query, @@ -45,15 +98,20 @@ def send_message(message: str) -> str: Gib immer alle Spalten der Tabelle an. Wenn die resultierende Abfrage nicht ausführbar ist, ersetze "your-query“ durch NA, aber ersetze trotzdem "your-query" durch eine Zusammenfassung der Abfrage. - Verwende KEINE MySQL-Syntax. + Verwende KEINE MySQL-Syntax, sondern AUSSCHLIESSLICH Microsoft SQL. Begrenze die SQL-Abfrage immer auf 100 Zeilen. + Formatiere den Output bestmöglich. """ - system_message = "Du bist ein hilfreicher Assistent." + response = client.chat.completions.create( - model=deployment_name, + model=MODEL, messages=[ {"role": "system", "content": system_message}, {"role": "user", "content": message}, ], ) - return response.choices[0].message.content + + result_str = response.choices[0].message.content.replace("```json\n", "").replace("```", "") + if ("\n") not in result_str: + result_str = result_str.replace("\\", "\n") + return result_str diff --git a/app/sql_utils.py b/app/sql_utils.py index 079f6fb..c45f05d 100644 --- a/app/sql_utils.py +++ b/app/sql_utils.py @@ -11,7 +11,7 @@ def test_db_connection() -> bool: This function attempts to establish a connection to an Azure SQL Database using the connection string stored in the environment variable 'AZURE_SQL_CONNECTION_STRING'. It makes up to 5 attempts to connect, - with a timeout of 240 seconds for each attempt. + with a timeout of 480 seconds for each attempt. Returns ------- @@ -22,14 +22,15 @@ def test_db_connection() -> bool: for i in range(5): print(f"Trying to connect to Azure SQL Database... Attempt {i + 1}") try: - pyodbc.connect(connection_string, timeout=240) + pyodbc.connect(connection_string, timeout=480) print("Connected to Azure SQL Database successfully!") connected = True break - except pyodbc.Error as e: + except Exception as e: print(f"Error connecting to Azure SQL Database: {e}") connected = False + continue return connected -- 2.49.1 From 2c83ad2ee552c1b65515fe739798b8196fc636b5 Mon Sep 17 00:00:00 2001 From: Tobias Quadfasel Date: Tue, 3 Sep 2024 15:06:32 +0200 Subject: [PATCH 5/5] feat(ai-chat): Add text field to describe manual SQL input field Before, there was no direct description about the usage of the manual SQL query input field. A plotly dash component was added with a more precise description for the user. --- app/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/app.py b/app/app.py index 32cdacb..9b97855 100644 --- a/app/app.py +++ b/app/app.py @@ -203,6 +203,13 @@ def get_layout() -> html.Div: "Direkte SQL-Abfrage", style={"margin-left": "20px", "margin-top": "50px", "font-size": 24}, ), # SQL Header + html.Div( + ( + "Hier kann der ausgegebene SQL-Code getestet oder mit selbst" + "geschriebenen Code verglichen werden." + ), + style={"margin-left": "20px", "font-weight": "bold", "font-size": "16px"}, + ), dcc.Textarea( id="sql-input-field", value="(Microsoft) SQL-Abfrage eingeben...", -- 2.49.1