diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f6e42cc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +# Ignore git objects +.git/ +.gitignore +.gitlab-ci.yml +.gitmodules + +# Ignore temperory volumes +**/volumes + +# creating a docker image +.dockerignore + +# Ignore any virtual environment configuration files +.env* +.venv/ +env/ +# Ignore python bytecode files +*.pyc +__pycache__/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa4bd29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +deploy/compose/volumes/ +notebooks/.ipynb_checkpoints \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4f1d4e1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## Changelog +All notable changes to this project will be documented in this file. +The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. + +## [1.0.0] - 2024-10-23 + +### Added + +- First release. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..373ff92 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +### Pull Requests +Developer workflow for code contributions is as follows: + +1. Developers must first [fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) the upstream this repository. +2. Git clone the forked repository and push changes to the personal fork. +3. Once the code changes are staged on the fork and ready for review, a Pull Request (PR) can be requested to merge the changes from a branch of the fork into a selected branch of upstream. +4. Since there is no CI/CD process in place yet, the PR will be accepted and the corresponding issue closed only after adequate testing has been completed, manually, by the developer and/or repository owners reviewing the code. + + +#### Signing Your Work +We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. + +Any contribution which contains commits that are not Signed-Off will not be accepted. +To sign off on a commit, use the `--signoff` (or `-s`) option when committing your changes: + +`$ git commit -s -m "Add cool feature."` +This will append the following to your commit message: + +Signed-off-by: Your Name + + +#### Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. \ No newline at end of file diff --git a/LICENSE-3rd-party.txt b/LICENSE-3rd-party.txt new file mode 100644 index 0000000..4a6fa6f --- /dev/null +++ b/LICENSE-3rd-party.txt @@ -0,0 +1,3686 @@ + + +--- LICENSE FOR fastapi --- +https://raw.githubusercontent.com/tiangolo/fastapi/master/LICENSE + + +The MIT License (MIT) + +Copyright (c) 2018 Sebastián Ramírez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +--- LICENSE FOR uvicorn --- +https://raw.githubusercontent.com/encode/uvicorn/master/LICENSE.md + + +Copyright © 2017-present, [Encode OSS Ltd](https://www.encode.io/). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--- LICENSE FOR langchain --- +https://raw.githubusercontent.com/langchain-ai/langchain/master/LICENSE + + +MIT License + +Copyright (c) LangChain, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR dataclass-wizard --- +https://raw.githubusercontent.com/rnag/dataclass-wizard/main/LICENSE + + +Apache Software License 2.0 + +Copyright (c) 2021, Ritvik Nag + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + +--- LICENSE FOR langchain --- +https://raw.githubusercontent.com/langchain-ai/langchain/master/LICENSE + + +MIT License + +Copyright (c) LangChain, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR langgraph --- +https://raw.githubusercontent.com/langchain-ai/langgraph/main/LICENSE + + +MIT License + +Copyright (c) 2024 LangChain, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR redis --- +https://raw.githubusercontent.com/redis/redis/unstable/LICENSE.txt + + +Starting on March 20th, 2024, Redis follows a dual-licensing model with all Redis project code +contributions under version 7.4 and subsequent releases governed by the Redis Software Grant and +Contributor License Agreement. After this date, contributions are subject to the user's choice of +the Redis Source Available License v2 (RSALv2) or the Server Side Public License v1 (SSPLv1), as +follows: + + +1. Redis Source Available License 2.0 (RSALv2) Agreement +======================================================== + +Last Update: December 30, 2023 + +Acceptance +---------- + +This Agreement sets forth the terms and conditions on which the Licensor +makes available the Software. By installing, downloading, accessing, +Using, or distributing any of the Software, You agree to all of the +terms and conditions of this Agreement. + +If You are receiving the Software on behalf of Your Company, You +represent and warrant that You have the authority to agree to this +Agreement on behalf of such entity. + +The Licensor reserves the right to update this Agreement from time to +time. + +The terms below have the meanings set forth below for purposes of this +Agreement: + +Definitions +----------- + +Agreement: this Redis Source Available License 2.0 Agreement. + +Control: ownership, directly or indirectly, of substantially all the +assets of an entity, or the power to direct its management and policies +by vote, contract, or otherwise. + +License: the License as described in the License paragraph below. + +Licensor: the entity offering these terms, which includes Redis Ltd. on +behalf of itself and its subsidiaries and affiliates worldwide. + +Modify, Modified, or Modification: copy from or adapt all or part of the +work in a fashion requiring copyright permission other than making an +exact copy. The resulting work is called a Modified version of the +earlier work. + +Redis: the Redis software as described in redis.com redis.io. + +Software: certain Software components designed to work with Redis and +provided to You under this Agreement. + +Trademark: the trademarks, service marks, and any other similar rights. + +Use: anything You do with the Software requiring one of Your Licenses. + +You: the recipient of the Software, the individual or entity on whose +behalf You are agreeing to this Agreement. + +Your Company: any legal entity, sole proprietorship, or other kind of +organization that You work for, plus all organizations that have control +over, are under the control of, or are under common control with that +organization. + +Your Licenses: means all the Licenses granted to You for the Software +under this Agreement. + +License +------- + +The Licensor grants You a non-exclusive, royalty-free, worldwide, +non-sublicensable, non-transferable license to use, copy, distribute, +make available, and prepare derivative works of the Software, in each +case subject to the limitations and conditions below. + +Limitations +----------- + +You may not make the functionality of the Software or a Modified version +available to third parties as a service or distribute the Software or a +Modified version in a manner that makes the functionality of the +Software available to third parties. + +Making the functionality of the Software or Modified version available +to third parties includes, without limitation, enabling third parties to +interact with the functionality of the Software or Modified version in +distributed form or remotely through a computer network, offering a +product or service, the value of which entirely or primarily derives +from the value of the Software or Modified version, or offering a +product or service that accomplishes for users the primary purpose of +the Software or Modified version. + +You may not alter, remove, or obscure any licensing, copyright, or other +notices of the Licensor in the Software. Any use of the Licensor's +Trademarks is subject to applicable law. + +Patents +------- + +The Licensor grants You a License, under any patent claims the Licensor +can License, or becomes able to License, to make, have made, use, sell, +offer for sale, import and have imported the Software, in each case +subject to the limitations and conditions in this License. This License +does not cover any patent claims that You cause to be infringed by +Modifications or additions to the Software. If You or Your Company make +any written claim that the Software infringes or contributes to +infringement of any patent, your patent License for the Software granted +under this Agreement ends immediately. If Your Company makes such a +claim, your patent License ends immediately for work on behalf of Your +Company. + +Notices +------- + +You must ensure that anyone who gets a copy of any part of the Software +from You also gets a copy of the terms and conditions in this Agreement. + +If You modify the Software, You must include in any Modified copies of +the Software prominent notices stating that You have Modified the +Software. + +No Other Rights +--------------- + +The terms and conditions of this Agreement do not imply any Licenses +other than those expressly granted in this Agreement. + +Termination +----------- + +If You Use the Software in violation of this Agreement, such Use is not +Licensed, and Your Licenses will automatically terminate. If the +Licensor provides You with a notice of your violation, and You cease all +violations of this License no later than 30 days after You receive that +notice, Your Licenses will be reinstated retroactively. However, if You +violate this Agreement after such reinstatement, any additional +violation of this Agreement will cause your Licenses to terminate +automatically and permanently. + +No Liability +------------ + +As far as the law allows, the Software comes as is, without any +warranty or condition, and the Licensor will not be liable to You for +any damages arising out of this Agreement or the Use or nature of the +Software, under any kind of legal claim. + +Governing Law and Jurisdiction +------------------------------ + +If You are located in Asia, Pacific, Americas, or other jurisdictions +not listed below, the Agreement will be construed and enforced in all +respects in accordance with the laws of the State of California, U.S.A., +without reference to its choice of law rules. The courts located in the +County of Santa Clara, California, have exclusive jurisdiction for all +purposes relating to this Agreement. + +If You are located in Israel, the Agreement will be construed and +enforced in all respects in accordance with the laws of the State of +Israel without reference to its choice of law rules. The courts located +in the Central District of the State of Israel have exclusive +jurisdiction for all purposes relating to this Agreement. + +If You are located in Europe, United Kingdom, Middle East or Africa, the +Agreement will be construed and enforced in all respects in accordance +with the laws of England and Wales without reference to its choice of +law rules. The competent courts located in London, England, have +exclusive jurisdiction for all purposes relating to this Agreement. + + + +2. Server Side Public License (SSPL) +==================================== + + Server Side Public License + VERSION 1, OCTOBER 16, 2018 + + Copyright (c) 2018 MongoDB, Inc. + + Everyone is permitted to copy and distribute verbatim copies of this + license document, but changing it is not allowed. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to Server Side Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of + works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this + License. Each licensee is addressed as "you". "Licensees" and + "recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work in + a fashion requiring copyright permission, other than the making of an + exact copy. The resulting work is called a "modified version" of the + earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based on + the Program. + + To "propagate" a work means to do anything with it that, without + permission, would make you directly or secondarily liable for + infringement under applicable copyright law, except executing it on a + computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the + public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other + parties to make or receive copies. Mere interaction with a user through a + computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" to the + extent that it includes a convenient and prominently visible feature that + (1) displays an appropriate copyright notice, and (2) tells the user that + there is no warranty for the work (except to the extent that warranties + are provided), that licensees may convey the work under this License, and + how to view a copy of this License. If the interface presents a list of + user commands or options, such as a menu, a prominent item in the list + meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work for + making modifications to it. "Object code" means any non-source form of a + work. + + A "Standard Interface" means an interface that either is an official + standard defined by a recognized standards body, or, in the case of + interfaces specified for a particular programming language, one that is + widely used among developers working in that language. The "System + Libraries" of an executable work include anything, other than the work as + a whole, that (a) is included in the normal form of packaging a Major + Component, but which is not part of that Major Component, and (b) serves + only to enable use of the work with that Major Component, or to implement + a Standard Interface for which an implementation is available to the + public in source code form. A "Major Component", in this context, means a + major essential component (kernel, window system, and so on) of the + specific operating system (if any) on which the executable work runs, or + a compiler used to produce the work, or an object code interpreter used + to run it. + + The "Corresponding Source" for a work in object code form means all the + source code needed to generate, install, and (for an executable work) run + the object code and to modify the work, including scripts to control + those activities. However, it does not include the work's System + Libraries, or general-purpose tools or generally available free programs + which are used unmodified in performing those activities but which are + not part of the work. For example, Corresponding Source includes + interface definition files associated with source files for the work, and + the source code for shared libraries and dynamically linked subprograms + that the work is specifically designed to require, such as by intimate + data communication or control flow between those subprograms and other + parts of the work. + + The Corresponding Source need not include anything that users can + regenerate automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of + copyright on the Program, and are irrevocable provided the stated + conditions are met. This License explicitly affirms your unlimited + permission to run the unmodified Program, subject to section 13. The + output from running a covered work is covered by this License only if the + output, given its content, constitutes a covered work. This License + acknowledges your rights of fair use or other equivalent, as provided by + copyright law. Subject to section 13, you may make, run and propagate + covered works that you do not convey, without conditions so long as your + license otherwise remains in force. You may convey covered works to + others for the sole purpose of having them make modifications exclusively + for you, or provide you with facilities for running those works, provided + that you comply with the terms of this License in conveying all + material for which you do not control copyright. Those thus making or + running the covered works for you must do so exclusively on your + behalf, under your direction and control, on terms that prohibit them + from making any copies of your copyrighted material outside their + relationship with you. + + Conveying under any other circumstances is permitted solely under the + conditions stated below. Sublicensing is not allowed; section 10 makes it + unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological + measure under any applicable law fulfilling obligations under article 11 + of the WIPO copyright treaty adopted on 20 December 1996, or similar laws + prohibiting or restricting circumvention of such measures. + + When you convey a covered work, you waive any legal power to forbid + circumvention of technological measures to the extent such circumvention is + effected by exercising rights under this License with respect to the + covered work, and you disclaim any intention to limit operation or + modification of the work as a means of enforcing, against the work's users, + your or third parties' legal rights to forbid circumvention of + technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you + receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice; keep + intact all notices stating that this License and any non-permissive terms + added in accord with section 7 apply to the code; keep intact all notices + of the absence of any warranty; and give all recipients a copy of this + License along with the Program. You may charge any price or no price for + each copy that you convey, and you may offer support or warranty + protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to + produce it from the Program, in the form of source code under the terms + of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, + and giving a relevant date. + + b) The work must carry prominent notices stating that it is released + under this License and any conditions added under section 7. This + requirement modifies the requirement in section 4 to "keep intact all + notices". + + c) You must license the entire work, as a whole, under this License to + anyone who comes into possession of a copy. This License will therefore + apply, along with any applicable section 7 additional terms, to the + whole of the work, and all its parts, regardless of how they are + packaged. This License gives no permission to license the work in any + other way, but it does not invalidate such permission if you have + separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your work + need not make them do so. + + A compilation of a covered work with other separate and independent + works, which are not by their nature extensions of the covered work, and + which are not combined with it such as to form a larger program, in or on + a volume of a storage or distribution medium, is called an "aggregate" if + the compilation and its resulting copyright are not used to limit the + access or legal rights of the compilation's users beyond what the + individual works permit. Inclusion of a covered work in an aggregate does + not cause this License to apply to the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms of + sections 4 and 5, provided that you also convey the machine-readable + Corresponding Source under the terms of this License, in one of these + ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium customarily + used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a written + offer, valid for at least three years and valid for as long as you + offer spare parts or customer support for that product model, to give + anyone who possesses the object code either (1) a copy of the + Corresponding Source for all the software in the product that is + covered by this License, on a durable physical medium customarily used + for software interchange, for a price no more than your reasonable cost + of physically performing this conveying of source, or (2) access to + copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This alternative is + allowed only occasionally and noncommercially, and only if you received + the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place + (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to copy + the object code is a network server, the Corresponding Source may be on + a different server (operated by you or a third party) that supports + equivalent copying facilities, provided you maintain clear directions + next to the object code saying where to find the Corresponding Source. + Regardless of what server hosts the Corresponding Source, you remain + obligated to ensure that it is available for as long as needed to + satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you + inform other peers where the object code and Corresponding Source of + the work are being offered to the general public at no charge under + subsection 6d. + + A separable portion of the object code, whose source code is excluded + from the Corresponding Source as a System Library, need not be included + in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any + tangible personal property which is normally used for personal, family, + or household purposes, or (2) anything designed or sold for incorporation + into a dwelling. In determining whether a product is a consumer product, + doubtful cases shall be resolved in favor of coverage. For a particular + product received by a particular user, "normally used" refers to a + typical or common use of that class of product, regardless of the status + of the particular user or of the way in which the particular user + actually uses, or expects or is expected to use, the product. A product + is a consumer product regardless of whether the product has substantial + commercial, industrial or non-consumer uses, unless such uses represent + the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, + procedures, authorization keys, or other information required to install + and execute modified versions of a covered work in that User Product from + a modified version of its Corresponding Source. The information must + suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because + modification has been made. + + If you convey an object code work under this section in, or with, or + specifically for use in, a User Product, and the conveying occurs as part + of a transaction in which the right of possession and use of the User + Product is transferred to the recipient in perpetuity or for a fixed term + (regardless of how the transaction is characterized), the Corresponding + Source conveyed under this section must be accompanied by the + Installation Information. But this requirement does not apply if neither + you nor any third party retains the ability to install modified object + code on the User Product (for example, the work has been installed in + ROM). + + The requirement to provide Installation Information does not include a + requirement to continue to provide support service, warranty, or updates + for a work that has been modified or installed by the recipient, or for + the User Product in which it has been modified or installed. Access + to a network may be denied when the modification itself materially + and adversely affects the operation of the network or violates the + rules and protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, in + accord with this section must be in a format that is publicly documented + (and with an implementation available to the public in source code form), + and must require no special password or key for unpacking, reading or + copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this + License by making exceptions from one or more of its conditions. + Additional permissions that are applicable to the entire Program shall be + treated as though they were included in this License, to the extent that + they are valid under applicable law. If additional permissions apply only + to part of the Program, that part may be used separately under those + permissions, but the entire Program remains governed by this License + without regard to the additional permissions. When you convey a copy of + a covered work, you may at your option remove any additional permissions + from that copy, or from any part of it. (Additional permissions may be + written to require their own removal in certain cases when you modify the + work.) You may place additional permissions on material, added by you to + a covered work, for which you have or can give appropriate copyright + permission. + + Notwithstanding any other provision of this License, for material you add + to a covered work, you may (if authorized by the copyright holders of + that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade + names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material + by anyone who conveys the material (or modified versions of it) with + contractual assumptions of liability to the recipient, for any + liability that these contractual assumptions directly impose on those + licensors and authors. + + All other non-permissive additional terms are considered "further + restrictions" within the meaning of section 10. If the Program as you + received it, or any part of it, contains a notice stating that it is + governed by this License along with a term that is a further restriction, + you may remove that term. If a license document contains a further + restriction but permits relicensing or conveying under this License, you + may add to a covered work material governed by the terms of that license + document, provided that the further restriction does not survive such + relicensing or conveying. + + If you add terms to a covered work in accord with this section, you must + place, in the relevant source files, a statement of the additional terms + that apply to those files, or a notice indicating where to find the + applicable terms. Additional terms, permissive or non-permissive, may be + stated in the form of a separately written license, or stated as + exceptions; the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly + provided under this License. Any attempt otherwise to propagate or modify + it is void, and will automatically terminate your rights under this + License (including any patent licenses granted under the third paragraph + of section 11). + + However, if you cease all violation of this License, then your license + from a particular copyright holder is reinstated (a) provisionally, + unless and until the copyright holder explicitly and finally terminates + your license, and (b) permanently, if the copyright holder fails to + notify you of the violation by some reasonable means prior to 60 days + after the cessation. + + Moreover, your license from a particular copyright holder is reinstated + permanently if the copyright holder notifies you of the violation by some + reasonable means, this is the first time you have received notice of + violation of this License (for any work) from that copyright holder, and + you cure the violation prior to 30 days after your receipt of the notice. + + Termination of your rights under this section does not terminate the + licenses of parties who have received copies or rights from you under + this License. If your rights have been terminated and not permanently + reinstated, you do not qualify to receive new licenses for the same + material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or run a + copy of the Program. Ancillary propagation of a covered work occurring + solely as a consequence of using peer-to-peer transmission to receive a + copy likewise does not require acceptance. However, nothing other than + this License grants you permission to propagate or modify any covered + work. These actions infringe copyright if you do not accept this License. + Therefore, by modifying or propagating a covered work, you indicate your + acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically receives + a license from the original licensors, to run, modify and propagate that + work, subject to this License. You are not responsible for enforcing + compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an + organization, or substantially all assets of one, or subdividing an + organization, or merging organizations. If propagation of a covered work + results from an entity transaction, each party to that transaction who + receives a copy of the work also receives whatever licenses to the work + the party's predecessor in interest had or could give under the previous + paragraph, plus a right to possession of the Corresponding Source of the + work from the predecessor in interest, if the predecessor has it or can + get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the rights + granted or affirmed under this License. For example, you may not impose a + license fee, royalty, or other charge for exercise of rights granted + under this License, and you may not initiate litigation (including a + cross-claim or counterclaim in a lawsuit) alleging that any patent claim + is infringed by making, using, selling, offering for sale, or importing + the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this + License of the Program or a work on which the Program is based. The work + thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims owned or + controlled by the contributor, whether already acquired or hereafter + acquired, that would be infringed by some manner, permitted by this + License, of making, using, or selling its contributor version, but do not + include claims that would be infringed only as a consequence of further + modification of the contributor version. For purposes of this definition, + "control" includes the right to grant patent sublicenses in a manner + consistent with the requirements of this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free + patent license under the contributor's essential patent claims, to make, + use, sell, offer for sale, import and otherwise run, modify and propagate + the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express + agreement or commitment, however denominated, not to enforce a patent + (such as an express permission to practice a patent or covenant not to + sue for patent infringement). To "grant" such a patent license to a party + means to make such an agreement or commitment not to enforce a patent + against the party. + + If you convey a covered work, knowingly relying on a patent license, and + the Corresponding Source of the work is not available for anyone to copy, + free of charge and under the terms of this License, through a publicly + available network server or other readily accessible means, then you must + either (1) cause the Corresponding Source to be so available, or (2) + arrange to deprive yourself of the benefit of the patent license for this + particular work, or (3) arrange, in a manner consistent with the + requirements of this License, to extend the patent license to downstream + recipients. "Knowingly relying" means you have actual knowledge that, but + for the patent license, your conveying the covered work in a country, or + your recipient's use of the covered work in a country, would infringe + one or more identifiable patents in that country that you have reason + to believe are valid. + + If, pursuant to or in connection with a single transaction or + arrangement, you convey, or propagate by procuring conveyance of, a + covered work, and grant a patent license to some of the parties receiving + the covered work authorizing them to use, propagate, modify or convey a + specific copy of the covered work, then the patent license you grant is + automatically extended to all recipients of the covered work and works + based on it. + + A patent license is "discriminatory" if it does not include within the + scope of its coverage, prohibits the exercise of, or is conditioned on + the non-exercise of one or more of the rights that are specifically + granted under this License. You may not convey a covered work if you are + a party to an arrangement with a third party that is in the business of + distributing software, under which you make payment to the third party + based on the extent of your activity of conveying the work, and under + which the third party grants, to any of the parties who would receive the + covered work from you, a discriminatory patent license (a) in connection + with copies of the covered work conveyed by you (or copies made from + those copies), or (b) primarily for and in connection with specific + products or compilations that contain the covered work, unless you + entered into that arrangement, or that patent license was granted, prior + to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting any + implied license or other defenses to infringement that may otherwise be + available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot use, + propagate or convey a covered work so as to satisfy simultaneously your + obligations under this License and any other pertinent obligations, then + as a consequence you may not use, propagate or convey it at all. For + example, if you agree to terms that obligate you to collect a royalty for + further conveying from those to whom you convey the Program, the only way + you could satisfy both those terms and this License would be to refrain + entirely from conveying the Program. + + 13. Offering the Program as a Service. + + If you make the functionality of the Program or a modified version + available to third parties as a service, you must make the Service Source + Code available via network download to everyone at no charge, under the + terms of this License. Making the functionality of the Program or + modified version available to third parties as a service includes, + without limitation, enabling third parties to interact with the + functionality of the Program or modified version remotely through a + computer network, offering a service the value of which entirely or + primarily derives from the value of the Program or modified version, or + offering a service that accomplishes for users the primary purpose of the + Program or modified version. + + "Service Source Code" means the Corresponding Source for the Program or + the modified version, and the Corresponding Source for all programs that + you use to make the Program or modified version available as a service, + including, without limitation, management software, user interfaces, + application program interfaces, automation software, monitoring software, + backup software, storage software and hosting software, all such that a + user could run an instance of the service using the Service Source Code + you make available. + + 14. Revised Versions of this License. + + MongoDB, Inc. may publish revised and/or new versions of the Server Side + Public License from time to time. Such new versions will be similar in + spirit to the present version, but may differ in detail to address new + problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies that a certain numbered version of the Server Side Public + License "or any later version" applies to it, you have the option of + following the terms and conditions either of that numbered version or of + any later version published by MongoDB, Inc. If the Program does not + specify a version number of the Server Side Public License, you may + choose any version ever published by MongoDB, Inc. + + If the Program specifies that a proxy can decide which future versions of + the Server Side Public License can be used, that proxy's public statement + of acceptance of a version permanently authorizes you to choose that + version for the Program. + + Later license versions may give you additional or different permissions. + However, no additional obligations are imposed on any author or copyright + holder as a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING + ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF + THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO + LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU + OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided above + cannot be given local legal effect according to their terms, reviewing + courts shall apply local law that most closely approximates an absolute + waiver of all civil liability in connection with the Program, unless a + warranty or assumption of liability accompanies a copy of the Program in + return for a fee. + + END OF TERMS AND CONDITIONS + + +--- LICENSE FOR psycopg2 --- +https://raw.githubusercontent.com/psycopg/psycopg2/master/LICENSE + + +psycopg2 and the LGPL +--------------------- + +psycopg2 is free software: you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +psycopg2 is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +License for more details. + +In addition, as a special exception, the copyright holders give +permission to link this program with the OpenSSL library (or with +modified versions of OpenSSL that use the same license as OpenSSL), +and distribute linked combinations including the two. + +You must obey the GNU Lesser General Public License in all respects for +all of the code used other than OpenSSL. If you modify file(s) with this +exception, you may extend this exception to your version of the file(s), +but you are not obligated to do so. If you do not wish to do so, delete +this exception statement from your version. If you delete this exception +statement from all source files in the program, then also delete it here. + +You should have received a copy of the GNU Lesser General Public License +along with psycopg2 (see the doc/ directory.) +If not, see . + + +Alternative licenses +-------------------- + +The following BSD-like license applies (at your option) to the files following +the pattern ``psycopg/adapter*.{h,c}`` and ``psycopg/microprotocol*.{h,c}``: + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product documentation + would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + + +--- LICENSE FOR sqlalchemy --- +https://raw.githubusercontent.com/sqlalchemy/sqlalchemy/main/LICENSE + + +Copyright 2005-2024 SQLAlchemy authors and contributors . + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR client_python --- +https://raw.githubusercontent.com/prometheus/client_python/master/LICENSE + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LICENSE FOR bleach --- +https://raw.githubusercontent.com/mozilla/bleach/main/LICENSE + + +Copyright (c) 2014-2017, Mozilla Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +--- LICENSE FOR nltk --- +https://raw.githubusercontent.com/nltk/nltk/develop/LICENSE.txt + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LICENSE FOR python-multipart --- +https://raw.githubusercontent.com/andrew-d/python-multipart/master/LICENSE.txt + + +Copyright 2012, Andrew Dunham + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + +--- LICENSE FOR langchain --- +https://raw.githubusercontent.com/langchain-ai/langchain/master/LICENSE + + +MIT License + +Copyright (c) LangChain, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR pymilvus --- +https://raw.githubusercontent.com/milvus-io/pymilvus/master/LICENSE + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Zilliz + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LICENSE FOR sentence-transformers --- +https://raw.githubusercontent.com/UKPLab/sentence-transformers/master/LICENSE + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Nils Reimers + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and +limitations under the License. + + +--- LICENSE FOR pandas --- +https://raw.githubusercontent.com/pandas-dev/pandas/main/LICENSE + + +BSD 3-Clause License + +Copyright (c) 2008-2011, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team +All rights reserved. + +Copyright (c) 2011-2024, Open source contributors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--- LICENSE FOR pandas-ai --- +https://raw.githubusercontent.com/Sinaptik-AI/pandas-ai/main/LICENSE + + +Copyright (c) 2023 Sinaptik GmbH + +Portions of this software are licensed as follows: + +- All content that resides under any "pandasai/ee/" directory of this repository, if such directories exists, are licensed under the license defined in "pandasai/ee/LICENSE". +- All third party components incorporated into the PandasAI Software are licensed under the original license provided by the owner of the applicable component. +- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR numexpr --- +https://raw.githubusercontent.com/pydata/numexpr/master/LICENSE.txt + + +Copyright (c) 2007,2008 David M. Cooke +Copyright (c) 2009,2010 Francesc Alted +Copyright (c) 2011- See AUTHORS.txt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +--- LICENSE FOR httpx --- +https://raw.githubusercontent.com/projectdiscovery/httpx/main/LICENSE.md + + +MIT License + +Copyright (c) 2021 ProjectDiscovery, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR psycopg --- +https://raw.githubusercontent.com/psycopg/psycopg/master/LICENSE.txt + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + + +--- LICENSE FOR nest_asyncio --- +https://raw.githubusercontent.com/erdewit/nest_asyncio/master/LICENSE + + +BSD 2-Clause License + +Copyright (c) 2018-2020, Ewald de Wit +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--- LICENSE FOR unstructured --- +https://raw.githubusercontent.com/Unstructured-IO/unstructured/main/LICENSE.md + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Unstructured Technologies, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +--- LICENSE FOR langchain-postgres --- +https://raw.githubusercontent.com/langchain-ai/langchain-postgres/main/LICENSE + + +MIT License + +Copyright (c) 2024 LangChain, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- LICENSE FOR pip --- +https://raw.githubusercontent.com/pypa/pip/master/LICENSE.txt + +Copyright (c) 2008-present The pip developers (see AUTHORS.txt file) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--- LICENSE FOR python 3.10 --- +This package was put together by Klee Dienes from +sources from ftp.python.org:/pub/python, based on the Debianization by +the previous maintainers Bernd S. Brentrup and +Bruce Perens. Current maintainer is Matthias Klose . + +It was downloaded from http://python.org/ + +Copyright: + +Upstream Author: Guido van Rossum and others. + +License: + +The following text includes the Python license and licenses and +acknowledgements for incorporated software. The licenses can be read +in the HTML and texinfo versions of the documentation as well, after +installing the pythonx.y-doc package. Licenses for files not licensed +under the Python Licenses are found at the end of this file. + + +Python License +============== + +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations (now Zope +Corporation, see http://www.zope.com). In 2001, the Python Software +Foundation (PSF, see http://www.python.org/psf/) was formed, a +non-profit organization created specifically to own Python-related +Intellectual Property. Zope Corporation is a sponsoring member of +the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.2 2.1.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python alone +or in any derivative version, provided, however, that PSF's License +Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, +2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, +2013, 2014 Python Software Foundation; All Rights Reserved" are +retained in Python alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +Licenses and Acknowledgements for Incorporated Software +======================================================= + +Mersenne Twister +---------------- + +The `_random' module includes code based on a download from +`http://www.math.keio.ac.jp/~matumoto/MT2002/emt19937ar.html'. The +following are the verbatim comments from the original code: + + A C-program for MT19937, with initialization improved 2002/1/26. + Coded by Takuji Nishimura and Makoto Matsumoto. + + Before using, initialize the state by using init_genrand(seed) + or init_by_array(init_key, key_length). + + Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Any feedback is very welcome. + http://www.math.keio.ac.jp/matumoto/emt.html + email: matumoto@math.keio.ac.jp + + +Sockets +------- + +The `socket' module uses the functions, `getaddrinfo', and +`getnameinfo', which are coded in separate source files from the WIDE +Project, `http://www.wide.ad.jp/about/index.html'. + + Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the project nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND + GAI_ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE + FOR GAI_ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON GAI_ANY THEORY OF LIABILITY, WHETHER + IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN GAI_ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + + +Floating point exception control +-------------------------------- + +The source for the `fpectl' module includes the following notice: + + --------------------------------------------------------------------- + / Copyright (c) 1996. \ + | The Regents of the University of California. | + | All rights reserved. | + | | + | Permission to use, copy, modify, and distribute this software for | + | any purpose without fee is hereby granted, provided that this en- | + | tire notice is included in all copies of any software which is or | + | includes a copy or modification of this software and in all | + | copies of the supporting documentation for such software. | + | | + | This work was produced at the University of California, Lawrence | + | Livermore National Laboratory under contract no. W-7405-ENG-48 | + | between the U.S. Department of Energy and The Regents of the | + | University of California for the operation of UC LLNL. | + | | + | DISCLAIMER | + | | + | This software was prepared as an account of work sponsored by an | + | agency of the United States Government. Neither the United States | + | Government nor the University of California nor any of their em- | + | ployees, makes any warranty, express or implied, or assumes any | + | liability or responsibility for the accuracy, completeness, or | + | usefulness of any information, apparatus, product, or process | + | disclosed, or represents that its use would not infringe | + | privately-owned rights. Reference herein to any specific commer- | + | cial products, process, or service by trade name, trademark, | + | manufacturer, or otherwise, does not necessarily constitute or | + | imply its endorsement, recommendation, or favoring by the United | + | States Government or the University of California. The views and | + | opinions of authors expressed herein do not necessarily state or | + | reflect those of the United States Government or the University | + | of California, and shall not be used for advertising or product | + \ endorsement purposes. / + --------------------------------------------------------------------- + + +Cookie management +----------------- + +The `Cookie' module contains the following notice: + + Copyright 2000 by Timothy O'Malley + + All Rights Reserved + + Permission to use, copy, modify, and distribute this software + and its documentation for any purpose and without fee is hereby + granted, provided that the above copyright notice appear in all + copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + Timothy O'Malley not be used in advertising or publicity + pertaining to distribution of the software without specific, written + prior permission. + + Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR + ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + + +Execution tracing +----------------- + +The `trace' module contains the following notice: + + portions copyright 2001, Autonomous Zones Industries, Inc., all rights... + err... reserved and offered to the public under the terms of the + Python 2.2 license. + Author: Zooko O'Whielacronx + http://zooko.com/ + mailto:zooko@zooko.com + + Copyright 2000, Mojam Media, Inc., all rights reserved. + Author: Skip Montanaro + + Copyright 1999, Bioreason, Inc., all rights reserved. + Author: Andrew Dalke + + Copyright 1995-1997, Automatrix, Inc., all rights reserved. + Author: Skip Montanaro + + Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved. + + Permission to use, copy, modify, and distribute this Python software and + its associated documentation for any purpose without fee is hereby + granted, provided that the above copyright notice appears in all copies, + and that both that copyright notice and this permission notice appear in + supporting documentation, and that the name of neither Automatrix, + Bioreason or Mojam Media be used in advertising or publicity pertaining + to distribution of the software without specific, written prior + permission. + + +UUencode and UUdecode functions +------------------------------- + +The `uu' module contains the following notice: + + Copyright 1994 by Lance Ellinghouse + Cathedral City, California Republic, United States of America. + All Rights Reserved + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of Lance Ellinghouse + not be used in advertising or publicity pertaining to distribution + of the software without specific, written prior permission. + LANCE ELLINGHOUSE DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL LANCE ELLINGHOUSE CENTRUM BE LIABLE + FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + Modified by Jack Jansen, CWI, July 1995: + - Use binascii module to do the actual line-by-line conversion + between ascii and binary. This results in a 1000-fold speedup. The C + version is still 5 times faster, though. + - Arguments more compliant with python standard + + +XML Remote Procedure Calls +-------------------------- + +The `xmlrpclib' module contains the following notice: + + The XML-RPC client interface is + + Copyright (c) 1999-2002 by Secret Labs AB + Copyright (c) 1999-2002 by Fredrik Lundh + + By obtaining, using, and/or copying this software and/or its + associated documentation, you agree that you have read, understood, + and will comply with the following terms and conditions: + + Permission to use, copy, modify, and distribute this software and + its associated documentation for any purpose and without fee is + hereby granted, provided that the above copyright notice appears in + all copies, and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + Secret Labs AB or the author not be used in advertising or publicity + pertaining to distribution of the software without specific, written + prior permission. + + SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- + ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR + BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY + DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + OF THIS SOFTWARE. + +Licenses for Software linked to +=============================== + +Note that the choice of GPL compatibility outlined above doesn't extend +to modules linked to particular libraries, since they change the +effective License of the module binary. + + +GNU Readline +------------ + +The 'readline' module makes use of GNU Readline. + + The GNU Readline Library is free software; you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2, or (at + your option) any later version. + + On Debian systems, you can find the complete statement in + /usr/share/doc/readline-common/copyright'. A copy of the GNU General + Public License is available in /usr/share/common-licenses/GPL-2'. + + +OpenSSL +------- + +The '_ssl' module makes use of OpenSSL. + + The OpenSSL toolkit stays under a dual license, i.e. both the + conditions of the OpenSSL License and the original SSLeay license + apply to the toolkit. Actually both licenses are BSD-style Open + Source licenses. Note that both licenses are incompatible with + the GPL. + + On Debian systems, you can find the complete license text in + /usr/share/doc/openssl/copyright'. + + +Files with other licenses than the Python License +------------------------------------------------- + +Files: Include/dynamic_annotations.h +Files: Python/dynamic_annotations.c +Copyright: (c) 2008-2009, Google Inc. +License: Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Files: Include/unicodeobject.h +Copyright: (c) Corporation for National Research Initiatives. +Copyright: (c) 1999 by Secret Labs AB. +Copyright: (c) 1999 by Fredrik Lundh. +License: By obtaining, using, and/or copying this software and/or its + associated documentation, you agree that you have read, understood, + and will comply with the following terms and conditions: + + Permission to use, copy, modify, and distribute this software and its + associated documentation for any purpose and without fee is hereby + granted, provided that the above copyright notice appears in all + copies, and that both that copyright notice and this permission notice + appear in supporting documentation, and that the name of Secret Labs + AB or the author not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior + permission. + + SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR + ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Files: Lib/logging/* +Copyright: 2001-2010 by Vinay Sajip. All Rights Reserved. +License: Permission to use, copy, modify, and distribute this software and + its documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of Vinay Sajip + not be used in advertising or publicity pertaining to distribution + of the software without specific, written prior permission. + VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING + ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL + VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR + ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Files: Lib/multiprocessing/* +Files: Modules/_multiprocessing/* +Copyright: (c) 2006-2008, R Oudkerk. All rights reserved. +License: Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of author nor the names of any contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Files: Lib/sqlite3/* +Files: Modules/_sqlite/* +Copyright: (C) 2004-2005 Gerhard Häring +License: This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +Files: Lib/async* +Copyright: Copyright 1996 by Sam Rushing +License: Permission to use, copy, modify, and distribute this software and + its documentation for any purpose and without fee is hereby + granted, provided that the above copyright notice appear in all + copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of Sam + Rushing not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior + permission. + + SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN + NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR + CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Files: Lib/tarfile.py +Copyright: (C) 2002 Lars Gustaebel +License: Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +Files: Lib/turtle.py +Copyright: (C) 2006 - 2010 Gregor Lingl +License: This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + is copyright Gregor Lingl and licensed under a BSD-like license + +Files: Modules/_ctypes/libffi/* +Copyright: Copyright (C) 1996-2011 Red Hat, Inc and others. + Copyright (C) 1996-2011 Anthony Green + Copyright (C) 1996-2010 Free Software Foundation, Inc + Copyright (c) 2003, 2004, 2006, 2007, 2008 Kaz Kojima + Copyright (c) 2010, 2011, Plausible Labs Cooperative , Inc. + Copyright (c) 2010 CodeSourcery + Copyright (c) 1998 Andreas Schwab + Copyright (c) 2000 Hewlett Packard Company + Copyright (c) 2009 Bradley Smith + Copyright (c) 2008 David Daney + Copyright (c) 2004 Simon Posnjak + Copyright (c) 2005 Axis Communications AB + Copyright (c) 1998 Cygnus Solutions + Copyright (c) 2004 Renesas Technology + Copyright (c) 2002, 2007 Bo Thorsen + Copyright (c) 2002 Ranjit Mathew + Copyright (c) 2002 Roger Sayle + Copyright (c) 2000, 2007 Software AG + Copyright (c) 2003 Jakub Jelinek + Copyright (c) 2000, 2001 John Hornkvist + Copyright (c) 1998 Geoffrey Keating + Copyright (c) 2008 Björn König + +License: Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + ``Software''), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + Documentation: + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU General Public License as published by the + Free Software Foundation; either version 2, or (at your option) any + later version. A copy of the license is included in the + section entitled ``GNU General Public License''. + +Files: Modules/_gestalt.c +Copyright: 1991-1997 by Stichting Mathematisch Centrum, Amsterdam. +License: Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the names of Stichting Mathematisch + Centrum or CWI not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior permission. + + STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE + FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Files: Modules/syslogmodule.c +Copyright: 1994 by Lance Ellinghouse +License: Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of Lance Ellinghouse + not be used in advertising or publicity pertaining to distribution + of the software without specific, written prior permission. + + LANCE ELLINGHOUSE DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL LANCE ELLINGHOUSE BE LIABLE FOR ANY SPECIAL, + INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING + FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION + WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Files: Modules/zlib/* +Copyright: (C) 1995-2010 Jean-loup Gailly and Mark Adler +License: This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + + If you use the zlib library in a product, we would appreciate *not* receiving + lengthy legal documents to sign. The sources are provided for free but without + warranty of any kind. The library has been entirely written by Jean-loup + Gailly and Mark Adler; it does not include third-party code. + +Files: Modules/expat/* +Copyright: Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd + and Clark Cooper + Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers +License: Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Files: Modules/_decimal/libmpdec/* +Copyright: Copyright (c) 2008-2012 Stefan Krah. All rights reserved. +License: Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + . + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + , + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Files: Misc/python-mode.el +Copyright: Copyright (C) 1992,1993,1994 Tim Peters +License: This software is provided as-is, without express or implied + warranty. Permission to use, copy, modify, distribute or sell this + software, without fee, for any purpose and by any individual or + organization, is hereby granted, provided that the above copyright + notice and this paragraph appear in all copies. + +Files: Python/dtoa.c +Copyright: (c) 1991, 2000, 2001 by Lucent Technologies. +License: Permission to use, copy, modify, and distribute this software for any + purpose without fee is hereby granted, provided that this entire notice + is included in all copies of any software which is or includes a copy + or modification of this software and in all copies of the supporting + documentation for such software. + + THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED + WARRANTY. IN PARTICULAR, NEITHER THE AUTHOR NOR LUCENT MAKES ANY + REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY + OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + +Files: Python/getopt.c +Copyright: 1992-1994, David Gottner +License: Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice, this permission notice and + the following disclaimer notice appear unmodified in all copies. + + I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL I + BE LIABLE FOR ANY SPECIAL, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY + DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER + IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Files: PC/_subprocess.c +Copyright: Copyright (c) 2004 by Fredrik Lundh + Copyright (c) 2004 by Secret Labs AB, http://www.pythonware.com + Copyright (c) 2004 by Peter Astrand +License: + * Permission to use, copy, modify, and distribute this software and + * its associated documentation for any purpose and without fee is + * hereby granted, provided that the above copyright notice appears in + * all copies, and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of the + * authors not be used in advertising or publicity pertaining to + * distribution of the software without specific, written prior + * permission. + * + * THE AUTHORS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION + * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Files: PC/winsound.c +Copyright: Copyright (c) 1999 Toby Dickenson +License: * Permission to use this software in any way is granted without + * fee, provided that the copyright notice above appears in all + * copies. This software is provided "as is" without any warranty. + */ + +/* Modified by Guido van Rossum */ +/* Beep added by Mark Hammond */ +/* Win9X Beep and platform identification added by Uncle Timmy */ + +Files: Tools/pybench/* +Copyright: (c), 1997-2006, Marc-Andre Lemburg (mal@lemburg.com) + (c), 2000-2006, eGenix.com Software GmbH (info@egenix.com) +License: Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee or royalty is hereby + granted, provided that the above copyright notice appear in all copies + and that both that copyright notice and this permission notice appear + in supporting documentation or portions thereof, including + modifications, that you make. + + THE AUTHOR MARC-ANDRE LEMBURG DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, + INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING + FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION + WITH THE USE OR PERFORMANCE OF THIS SOFTWARE ! + + --- LICENSE FOR libgl --- +https://changelogs.ubuntu.com/changelogs/pool/main/libg/libglvnd/libglvnd_1.3.2-1~ubuntu0.20.04.2/copyright + +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: libglvnd +Source: https://gitlab.freedesktop.org/glvnd/libglvnd + +Files: * +Copyright: 2013-2017 NVIDIA Corporation + 2007-2013 VMware, Inc + 2010 Intel Corporation + 2010 Francisco Jerez + 2007-2012 The Khronos Group Inc + 1999-2006 Brian Paul + 2010 LunarG Inc + 2009 Dave Gamble +License: MIT + +Files: include/GLES/egl.h + include/GLES/glplatform.h + include/GLES2/gl2platform.h + include/GLES3/gl3platform.h + src/generate/xml/gl.xml + src/generate/xml/glx.xml +Copyright: 2008-2018 The Khronos Group Inc. +License: Apache-2.0 + +Files: m4/ax_check_enable_debug.m4 +Copyright: 2011 Rhys Ulerich + 2014-2015 Philip Withnall +License: public-domain + Public Domain. + +Files: m4/ax_check_link_flag.m4 +Copyright: 2008 Guido U. Draheim + 2011 Maarten Bosmans +License: GPL-3+ + +Files: m4/ax_pthread.m4 +Copyright: 2008 Steven G. Johnson + 2011 Daniel Richard G. +License: GPL-3+ + +Files: src/util/uthash/* +Copyright: 2005-2013 Troy D. Hanson +License: BSD-1-clause + +Files: src/util/uthash/doc/userguide.html +Copyright: 2006 Troy D. Hanson + 2006-2009 Stuart Rackham +License: GPL + +Files: debian/* +Copyright: 2013 Timo Aaltonen +License: MIT + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sub license, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + . + The above copyright notice and this permission notice (including the + next paragraph) shall be included in all copies or substantial portions + of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + IN NO EVENT SHALL TUNGSTEN GRAPHICS AND/OR ITS SUPPLIERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License: BSD-1-clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER + OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: GPL + On Debian machines the full text of the GNU General Public License + can be found in the file /usr/share/common-licenses/GPL. + +License: GPL-3+ + On Debian machines the full text of the GNU General Public License + version 3 can be found in the file /usr/share/common-licenses/GPL-3. + + --- LICENSE FOR glib --- +https://changelogs.ubuntu.com/changelogs/pool/main/g/glib2.0/glib2.0_2.64.6-1~ubuntu20.04.6/copyright + +This package was debianized by Akira TAGOH on +Thu, 7 Mar 2002 01:05:25 +0900. + +It was downloaded from . + +Original Authors +---------------- +Peter Mattis +Spencer Kimball +Josh MacDonald + +Please do not mail the original authors asking questions about this +version of GLib. + +GLib Team +--------- +Shawn T. Amundson +Jeff Garzik +Raja R Harinath +Tim Janik +Elliot Lee +Tor Lillqvist +Paolo Molaro +Havoc Pennington +Manish Singh +Owen Taylor +Sebastian Wilhelmi + +The random number generator "Mersenne Twister", which is used by GLib, +was developed and originally coded by: +Makoto Matsumoto +Takuji Nishimura + +Major copyright holders: + + Copyright © 1995-2018 Red Hat, Inc. + Copyright © 2008-2010 Novell, Inc. + Copyright © 2008-2010 Codethink Limited. + Copyright © 2008-2018 Collabora, Ltd. + Copyright © 2018 Endless Mobile, Inc. + Copyright © 2018 Emmanuele Bassi + +License: + + This package is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +On Debian systems, the complete text of the GNU Lesser General +Public License can be found in `/usr/share/common-licenses/LGPL'. + +Files: + gobject/tests/taptestrunner.py +Copyright: + 2015 Remko Tronçon +License: Expat + +Files: + tests/gen-casefold-txt.py + tests/gen-casemap-txt.py +Copyright: + 1998-1999 Tom Tromey + 2001 Red Hat Software +License: GPL-2+ + +License: Expat + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +License: GPL-2+ + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program; if not, see . \ No newline at end of file diff --git a/LICENSE b/LICENSE.md similarity index 99% rename from LICENSE rename to LICENSE.md index 261eeb9..f49a4e1 100644 --- a/LICENSE +++ b/LICENSE.md @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index bb35251..3713e60 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# ai-virtual-assistant \ No newline at end of file +# NIM Agent Blueprint: AI Virtual Assistant for Customer Service + +## Overview +With the rise of generative AI, companies are eager to enhance their customer service operations by integrating knowledge bases that are close to sensitive customer data. Traditional solutions often fall short in delivering context-aware, secure, and real-time responses to complex customer queries. This leads to longer resolution times, limited customer satisfaction, and potential data exposure risks. A centralized knowledge base that integrates seamlessly with internal applications and call center tools is vital to improving customer experience while ensuring data governance. +The AI virtual assistant for customer service NIM Agent Blueprint, powered by NVIDIA NeMo Retriever™ and NVIDIA NIM™ microservices, along with retrieval-augmented generation (RAG), offers a streamlined solution for enhancing customer support. It enables context-aware, multi-turn conversations, providing general and personalized Q&A responses based on structured and unstructured data, such as order history and product details. + +## Architecture + +![Key Generated window.](./docs/imgs/IVA-blueprint-diagram-r5.png) + +## Software Components +The RAG-based AI virtual assistant provides a reference to build an enterprise-ready generative AI solution with minimal effort. It contains the following software components: + +* NVIDIA NIM microservices + * Response Generation (Inference) + * LLM NIM - llama-3.1-70B-instruct + * NeMo Retriever embedding NIM - NV-Embed-QA-v5 + * NeMo Retriever reranking NIM - Rerank-Mistral-4b-v3 + * [Synthetic Data Generation](./notebooks/synthetic_data_generation.ipynb) for customization + * Nemotron4-340B +* Orchestrator Agent - Langgraph based +* Text Retrievers - LangChain +* Structured Data (CSV) Ingestion - Postgres Database +* Unstructured Data (PDF) Ingestion - Milvus Database (Vector GPU-optimized) + + +Docker Compose scripts are provided which spin up the microservices on a single node. When ready for a larger-scale deployment, you can use the included Helm charts to spin up the necessary microservices. You will use sample Jupyter notebooks with the JupyterLab service to interact with the code directly. + +The Blueprint contains sample use-case data pertaining to retail product catalog and customer data with purchase history but Developers can build upon this blueprint, by customizing the RAG application to their specific use case. A sample customer service agent user interface and API-based analytic server for conversation summary and sentiment are also included. + +## Key Functionalities +* Personalized Responses: Handles structured and unstructured customer queries (e.g., order details, spending history). +* Multi-Turn Dialogue: Offers context-aware, seamless interactions across multiple questions. +* Custom Conversation Style: Adapts text responses to reflect corporate branding and tone. +* Sentiment Analysis: Analyzes real-time customer interactions to gauge sentiment and adjust responses. +* Multi-Session Support: Allows for multiple user sessions with conversation history and summaries. +* Data Privacy: Integrates with on-premises or cloud-hosted knowledge bases to protect sensitive data. + +By integrating NVIDIA NIM and RAG, the system empowers developers to build customer support solutions that can provide faster and more accurate support while maintaining data privacy. + +## Target Audience +* Developers +* Data scientists +* Customer support teams + +## Get Started + +To get started with deployment follow + +* [Prerequisites](#basic-prerequisites) +* Deployment + * [Docker compose](./deploy/compose/README.md) + + +## Basic Prerequisites + +This section lists down the bare mininum requirements to deploy this blueprint. Follow the required [deployment guide](./deploy/) based on your requirement to understand deployment method specific prerequisites. + +#### Hardware requirements + +##### Option 1: Deploy with NVIDIA hosted endpoints +By default, the blueprint uses the NVIDIA API Catalog hosted endpoints for LLM, embedding and reranking models. Therefore all that is required is an instance with at least 8 cores and 64GB memory. + + +##### Option-2: Deploy with NIMs hosted locally +Once you familiarize yourself with the blueprint, you may want to further customize based upon your own use case which requires you to host your own LLM, embedding and reranking models. In this case you will need access to a GPU accelerated A instance with 8 cores, 64GB memory and 8XA100 or 8XH100. + +#### System requirements +Ubuntu 20.04 or 22.04 based machine, with sudo privileges + +#### Software requirements +* **NVIDIA AI Enterprise or Developer License**: NVIDIA NIM for LLMs are available for self-hosting under the NVIDIA AI Enterprise (NVAIE) License. [Sign up](https://build.nvidia.com/meta/llama-3-8b-instruct?snippet_tab=Docker&signin=true&integrate_nim=true&self_hosted_api=true) for NVAIE license. +* An **NGC API key** is required to access NGC resources. To obtain a key, navigate to Blueprint experience on NVIDIA API Catalog. Login / Sign up if needed and "Generate your API Key". + +## Sample Data +The blueprint comes with [synthetic sample data](./data/) representing a typical customer service function, including customer profiles, order histories (structured data), and technical product manuals (unstructured data). A notebook is provided to guide users on how to ingest both structured and unstructured data efficiently. +Structured Data: Includes customer profiles and order history +Unstructured Data: Ingests product manuals, product catalogs, and FAQ + +## AI Agent +This reference solution implements [different sub-agents using the open-source LangGraph framework and a supervisor agent to orchestrate the entire flow.](./src/agent/) These sub-agents address common customer service tasks for the included sample dataset. They rely on the Llama 3.1 models and NVIDIA NIM microservices for generating responses, converting natural language into SQL queries, and assessing the sentiment of the conversation. + +## Key Components +* [**Structured Data Retriever**](./src/retrievers/structured_data/): Works in tandem with a Postgres database and PandasAI to fetch relevant data based on user queries. +* [**Unstructured Data Retriever**](./src/retrievers/unstructured_data/): Processes unstructured data (e.g., PDFs, FAQs) by chunking it, creating embeddings using the NeMo Retriever embedding NIM, and storing it in Milvus for fast retrieval. +* [**Analytics and Admin Operations**](./src/analytics/): To support operational requirements, the blueprint includes reference code and APIs for managing key administrative tasks + * Storing conversation histories + * Generating conversation summaries + * Conducting sentiment analysis on customer interactions +These features ensure that customer service teams can efficiently monitor and evaluate interactions for quality and performance. + +## Data Flywheel +The blueprint comes with [pre-built APIs](./docs/api_references/analytics_server.json) that support continuous model improvement. The feedback loop, or “data flywheel,” allows LLM models to be fine-tuned over time to enhance both accuracy and cost-effectiveness. Feedback is collected at multiple points in the process to refine the models’ performance further. + +## Known issues +- The Blueprint responses can have significant latency when using [NVIDIA API Catalog cloud hosted models.](#option-1-deploy-with-nvidia-hosted-endpoints) + +## Inviting the community to contribute +We're posting these examples on GitHub to support the NVIDIA LLM community and facilitate feedback. We invite contributions! Open a GitHub issue or pull request! See contributing [guidelines here.](./CONTRIBUTING.md) + +## License +This NVIDIA NIM-AGENT BLUEPRINT is licensed under the [Apache License, Version 2.0.](./LICENSE.md) +Use of the sample data provided as part of this blueprint is governed by [the NVIDIA asset license.](./data/LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3518096 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ + ## Security + +NVIDIA is dedicated to the security and trust of our software products and services, including all source code repositories managed through our organization. + +If you need to report a security issue, please use the appropriate contact points outlined below. **Please do not report security vulnerabilities through GitHub.** + +## Reporting Potential Security Vulnerability in an NVIDIA Product + +To report a potential security vulnerability in any NVIDIA product: +- Web: [Security Vulnerability Submission Form](https://www.nvidia.com/object/submit-security-vulnerability.html) +- E-Mail: psirt@nvidia.com + - We encourage you to use the following PGP key for secure email communication: [NVIDIA public PGP Key for communication](https://www.nvidia.com/en-us/security/pgp-key) + - Please include the following information: + - Product/Driver name and version/branch that contains the vulnerability + - Type of vulnerability (code execution, denial of service, buffer overflow, etc.) + - Instructions to reproduce the vulnerability + - Proof-of-concept or exploit code + - Potential impact of the vulnerability, including how an attacker could exploit the vulnerability + +While NVIDIA currently does not have a bug bounty program, we do offer acknowledgement when an externally reported security issue is addressed under our coordinated vulnerability disclosure policy. Please visit our [Product Security Incident Response Team (PSIRT)](https://www.nvidia.com/en-us/security/psirt-policies/) policies page for more information. + +## NVIDIA Product Security + +For all security-related concerns, please visit NVIDIA's Product Security portal at https://www.nvidia.com/en-us/security \ No newline at end of file diff --git a/data/FAQ.pdf b/data/FAQ.pdf new file mode 100644 index 0000000..4e5048b Binary files /dev/null and b/data/FAQ.pdf differ diff --git a/data/LICENSE b/data/LICENSE new file mode 100644 index 0000000..6e6d0f6 --- /dev/null +++ b/data/LICENSE @@ -0,0 +1,58 @@ +NVIDIA ASSET LICENSE + +IMPORTANT NOTICE - PLEASE READ AND AGREE BEFORE USING THE ASSETS. + +This license agreement (“Agreement”) is a legal agreement between you, whether an individual or entity ("you”) and NVIDIA Corporation ("NVIDIA") and governs your use of the NVIDIA data provided hereunder (the “ASSETS”). + +This Agreement can be accepted only by an adult of legal age of majority in the country in which the ASSETS is used. If you are under the legal age of majority, you must ask your parent or legal guardian to consent to this Agreement. + +If you don’t have the required age or authority to accept this Agreement or if you don’t accept all the terms and conditions of this Agreement, do not use the ASSETS. + +You agree to use the ASSETS only for purposes that are permitted by this Agreement and any applicable law or regulation in the relevant jurisdictions. + +1. License. + +Subject to the terms of this Agreement, NVIDIA grants you a non-exclusive, revocable, non-transferable, non-sublicensable license to use the ASSETS, reproduce the ASSETS and prepare derivative works based on the ASSETS (“Derivative Works”), in each case solely for you to perform a trial or demonstration. The ASSETS include images and other information. The information provided is for example purposes, and may not correspond to actual information regarding the corresponding images. + +2. Limitations. + +Your license to use the ASSETS and Derivative Works is restricted as follows: (i) you may not change or remove copyright or other proprietary notices in the ASSETS and Derivative Works; (ii) you may not sell, rent, sublicense, transfer, distribute, or otherwise make the ASSETS and Derivative Works available to others; and (iii) you may not deploy the ASSETS as part of a commercial product or service or train or test AI models using the ASSETS. + +3. Ownership. + +The ASSETS, including all intellectual property rights, is and will remain the sole and exclusive property of NVIDIA or its licensors. Except as expressly granted in this Agreement, (i) NVIDIA reserves all rights, interests, and remedies in connection with the ASSETS and Derivative Works, and (ii) no other license or right is granted to you by implication, estoppel or otherwise. + +4. Feedback. + +You may, but you are not obligated to, provide suggestions, requests, fixes, modifications, enhancements, or other feedback regarding the ASSETS (collectively, “Feedback”). Feedback, even if designated as confidential by you, will not create any confidentiality obligation for NVIDIA or its affiliates. If you provide Feedback, you hereby grant NVIDIA, its affiliates and its designees a non-exclusive, perpetual, irrevocable, sublicensable, worldwide, royalty-free, fully paid-up and transferable license, under your intellectual property rights, to publicly perform, publicly display, reproduce, use, make, have made, sell, offer for sale, distribute (through multiple tiers of distribution), import, create derivative works of and otherwise commercialize and exploit the Feedback at NVIDIA’s discretion. + +5. Term and Termination. + +This Agreement expires twelve (12) months after the date of initial delivery or download of the ASSET. This Agreement will automatically terminate without notice from NVIDIA if you fail to comply with any of the terms in this Agreement or if you commence or participate in any legal proceeding against NVIDIA with respect to the ASSETS. Additionally, either party may terminate this Agreement at any time with prior written notice to the other party. Upon any termination, you must stop using and destroy all copies of the ASSETS and Derivative Works. Upon written request, you will certify in writing that you have complied with your commitments under this section. All provisions will survive termination, except for the licenses granted to you. + +6. Disclaimer of Warranties. + +THE ASSETS ARE PROVIDED BY NVIDIA AS-IS AND WITH ALL FAULTS. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NVIDIA DISCLAIMS ALL WARRANTIES AND REPRESENTATIONS OF ANY KIND, WHETHER EXPRESS, IMPLIED OR STATUTORY, RELATING TO OR ARISING UNDER THIS AGREEMENT, INCLUDING, WITHOUT LIMITATION, THE WARRANTIES OF TITLE, NONINFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, USAGE OF TRADE AND COURSE OF DEALING. + +7. Limitations of Liability. + +7.1 DISCLAIMER. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL NVIDIA BE LIABLE FOR ANY (I) INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, OR (II) DAMAGES FOR THE (A) COST OF PROCURING SUBSTITUTE GOODS OR (B) LOSS OF PROFITS, REVENUES, USE, DATA OR GOODWILL ARISING OUT OF OR RELATED TO THIS AGREEMENT, WHETHER BASED ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, OR OTHERWISE, AND EVEN IF NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND EVEN IF A PARTY’S REMEDIES FAIL THEIR ESSENTIAL PURPOSE. +7.2 DAMAGES CAP. ADDITIONALLY, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, NVIDIA’S TOTAL CUMULATIVE AGGREGATE LIABILITY FOR ANY AND ALL LIABILITIES, OBLIGATIONS OR CLAIMS ARISING OUT OF OR RELATED TO THIS AGREEMENT WILL NOT EXCEED FIVE U.S. DOLLARS (US$5). + +8. Governing Law and Jurisdiction. + +This Agreement will be governed in all respects by the laws of the United States and the laws of the State of Delaware, without regard to conflict of laws principles or the United Nations Convention on Contracts for the International Sale of Goods. The state and federal courts residing in Santa Clara County, California will have exclusive jurisdiction over any dispute or claim arising out of or related to this Agreement, and the parties irrevocably consent to personal jurisdiction and venue in those courts; except that either party may apply for injunctive remedies or an equivalent type of urgent legal relief in any jurisdiction. + +9. No Assignment. + +NVIDIA may assign, delegate or transfer its rights or obligations under this Agreement by any means or operation of law. You may not, without NVIDIA’s prior written consent, assign, delegate or transfer any of your rights or obligations under this Agreement by any means or operation of law, and any attempt to do so is null and void. + +10. Export. + +The ASSETS are subject to United States export laws and regulations. You agree to comply with all applicable export, import, trade and economic sanctions laws and regulations, including the Export Administration Regulations and Office of Foreign Assets Control regulations. These laws include restrictions on destinations, end-users and end-use. + +11. Entire Agreement. + +Regarding the subject matter of this Agreement, the parties agree that this Agreement constitutes the entire and exclusive agreement between the parties and supersedes all prior and contemporaneous communications. If a court of competent jurisdiction rules that a provision of this Agreement is unenforceable, that provision will be deemed modified to the extent necessary to make it enforceable and the remainder of this Agreement will continue in full force and effect. Any amendment to this Agreement must be in writing and signed by authorized representatives of both parties. + +(v. May 1, 2024) diff --git a/data/download.sh b/data/download.sh new file mode 100755 index 0000000..e6f9537 --- /dev/null +++ b/data/download.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Check if the file with URLs is provided as an argument +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Create the pdfs directory if it doesn't exist +mkdir -p ./data/manuals_pdf + +# Read the file line by line +while IFS= read -r url; do + # Download the file to the pdfs folder + echo "Downloading $url..." + wget -P ./data/manuals_pdf "$url" +done < "$1" + +echo "Download completed." diff --git a/data/gear-store.csv b/data/gear-store.csv new file mode 100644 index 0000000..b7182e4 --- /dev/null +++ b/data/gear-store.csv @@ -0,0 +1,687 @@ +category,subcategory,name,description,price +NVIDIA Electronics,Geforce,GEFORCE NOW $50 MEMBERSHIP GIFT CARD,"GeForce NOW gift cards can be redeemed for either a Priority or Ultimate membership, making it easy for any gamer to upgrade to the membership level of their preference. Play the most anticipated new releases, the world’s largest free-to-play titles, and thousands of games you already own with GeForce performance, streaming from the cloud. + +Please note that gift cards cannot be redeemed with GFN Alliance partners. + +Gift Card codes will work in the following countries (GFN supported regions): + +Åland (Finland), Austria, Canada, Cyprus, Czechia (Czech Republic), Denmark, Finland, France, Germany, Guernsey, Ireland, Isle of Man, Italy, Jersey, Netherlands, Norway, Poland, Saint Pierre and Miquelon, Spain, Sweden, Switzerland, United Kingdom, United States",50 +NVIDIA Electronics,Geforce,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",1599 +NVIDIA Electronics,Geforce,NVIDIA® GEFORCE RTX™ 4080,"The NVIDIA® GeForce RTX™ 4080 delivers the ultra performance and features that enthusiast gamers and creators demand. Bring your games and creative projects to life with ray tracing and AI-powered graphics. It's powered by the ultra-efficient NVIDIA Ada Lovelace architecture and 16GB of superfast G6X memory. + +CUDA Cores: 9728 +Boost Clock: 2.51 GHz +Memory Configuration: 16GB GDDR6X +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",1199 +NVIDIA Electronics,Geforce,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",599 +NVIDIA Electronics,Geforce,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",299 +NVIDIA Electronics,Geforce,NVIDIA® GEFORCE RTX™ 4070 SUPER,"Product Description: Get equipped for supercharged gaming and creating with the NVIDIA® GeForce RTX™ 4070 SUPER. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience super-fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 7168 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X",799 +NVIDIA Electronics,Shield,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",54.99 +NVIDIA Electronics,Shield,NVIDIA® SHIELD™ TV 2019,"NVIDIA® SHIELD® TV is the ultimate streaming media player for the modern living room. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. The new SHIELD TV is compact, stealth, and designed to disappear behind your entertainment center, right along with your cables. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up. Experience more.",199 +NVIDIA Electronics,Shield,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",199.99 +NVIDIA Electronics,Jetson,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",149 +NVIDIA Electronics,Jetson,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",499.95 +Apparel,Mens,UNISEX TMYBTMYS CUDA TEE,"Product Details: + +4.2 oz., 100% airlume combed and ringspun cotton +Retail fit +Unisex sizing +Shoulder taping +Sideseamed +Tear away label +Pre-shrunk",22 +Apparel,Mens,MEN'S BEYOND YOGA CREW T-SHIRT - DARKEST NIGHT,"Beyond Yoga Featherweight Always Beyond Crew T-shirt is a classic top you can wear anytime, anywhere. Darkest Night + +Product Details: + +Lightweight, ultrasoft +4-way stretch fabric +Crew neckline +Classic fit +94% Polyester 6% Elastane",66 +Apparel,Mens,MEN'S BEYOND YOGA TAKE IT EASY SHORTS,"Beyond Yoga Take It Easy shorts are total comfort whether you're lounging on the couch or out and about for the day. Available in Darkest Night or Olive Heather. + +Product Details: + +Elasticized waistband, relaxed fit +Two side hand pockets +One zippered back pocket +87% Polyester 13% Elastane +4-way stretch +7"" inseam",88 +Apparel,Mens,MEN'S BEYOND YOGA TAKE IT EASY PANT - DARKEST NIGHT,"Whether you are lounging at your home or out on the town on those casual weekends, the Beyond Yoga Take It Easy Pants are perfect. + +Product Details: + +Slim fit with slightly tapered legs +Elasticized waistband with internal drawcord closure +Two side hip pockets +One zippered back pocket +87% Polyester 13% Elastane",99 +Apparel,Mens,MEN'S LOGO TEE,"4.2 oz., 100% airlume combed and ringspun cotton +Retail fit +Unisex sizing +Shoulder taping +Sideseamed +Tear away label +Pre-shrunk",22 +Apparel,Mens,NEXT LEVEL ECO UNISEX TEE UNISEX,"5.6 oz./yd², 60/40 organic cotton/recycled poly RPET, 20 singles +Made of approximately 4 recycled RPET bottles per shirt +Melange color effect +1x1 baby rib-knit set-in collar +Unisex fit +Side seams +Tear away label",22 +Apparel,Mens,NVIDIA DUO-TONE LOGO UNISEX TEE,"4.2 oz., 100% airlume combed and ringspun cotton +Retail fit +Unisex sizing +Shoulder taping +Sideseamed +Tear away label +Pre-shrunk",18 +Apparel,Mens,UNISEX INSPIRE 365 TEE,"4.2 oz., 100% airlume combed and ringspun cotton +Retail fit +Unisex sizing +Shoulder taping +Sideseamed +Tear away label +Pre-shrunk",18 +Apparel,Mens,NORTH FACE MEN'S TREKKER JACKET,"Designed with baffles contoured to fit your body, this streamlined jacket offers lightweight, highly compressible synthetic insulation. +The Trekker jacket will keep you warmer regardless of the conditions. + +Product Details: + +15D 33 g/m2 100% nylon with durable water-repellent (DWR) finish +Partially constructed from recycled fabrications +Interior left chest zippered pocket +Interior elastic cuffs +Secure-zip hand pockets +Stows in left hand pocket +Hem cinch-cord",229 +Apparel,Mens,MARINE LAYER CORBET FULL ZIP MEN'S,"We took the (dare we say) famous Corbet fabric and put it in a full zip jacket silhouette! + +Product Details: + +Black +68% Polyester, 16% Cotton, 16% Rayon +Signature Corbet Quilted Fabric +Standard fit +Wash cold, dry low +Made responsibly in China",139 +Apparel,Mens,MARINE LAYER CORBET FULL ZIP VEST MEN'S,"We took the (dare we say) famous Corbet fabric and put it in a full zip vest silhouette! + +68% Polyester, 16% Cotton, 16% Rayon +Signature Corbet Quilted Fabric +Standard fit +Wash cold, dry low +Made responsibly in China",119 +Apparel,Mens,CHAMPION REVERSE WEAVE CREWNECK UNISEX,"12-ounce, 82/18 cotton/poly fleece +Reverse weave cross-grain cut resists shrinkage +1x1 rib knit neck, side panels, sleeve cuffs and hem +Embroidered Champion “C” logo at left cuff +Woven back neck label +Loose fit, runs large",55 +Apparel,Mens,FLUX BONDED JACKET 2.0 MEN'S LIGHT HEATHER,"Take performance to the next level with this 2.0 Fleece Jacket. The modern fit and sleek look make this the ideal jacket for all. + +Product Details: + +100% polyester microfleece +Coverstitch details throughout +Center front coil zipper with contrast teeth, semi-autolock slider and rubber pull +Active fit +Easy care",65 +Apparel,Mens,FULL ZIP NEURAL HOODIE UNISEX,"Stay comfortable and warm in this stylish hoodie. + +Product Details: + +50/50 cotton/poly fleece +NVIDIA pattern hood liner",55 +Apparel,Mens,LONG SLEEVE PERFORMANCE SHIRT MEN'S,"This crew neck is the perfect shirt whether you are exercising or just lounging around. + +Product Details: + +Stretchy and comfortable 90% polyester 10% spandex fabric +Decorative and functional reflective patterns on front & back +Moisture wicking, UPF 30",35 +Apparel,Mens,NIKE 2.0 POLO MEN'S,"100% polyester Dri-FIT fabric +Flat knit collar and three-button placket +Rolled-forward shoulder seams +Open hem sleeves and open hem +Contrast Swoosh logo is embroidered on the left sleeve",40 +Apparel,Mens,COTTON TOUCH POLO MEN'S,"This polyester polo performs with the comfortable feel of cotton. Just right for the workday or after hours, this sophisticated style has subtle texture, manages moisture, keeps you fresher with odor-controlling technology and feels great against your skin. + +Product Details: + +Made from 5.8 oz. 95/5 poly/spandex jersey +Self-fabric collar +Tag-free label +3-button placket",25 +Apparel,Mens,AUTONOMOUS TIME TRAVELER TEE UNISEX,"Retail fit +90% cotton/10% poly +Machine wash +Imported",22 +Apparel,Mens,GEFORCE LOGO HIGH DENSITY TEE UNISEX,"Heather Black & Athletic Heather: 90% airlume combed and ring-spun cotton, 10% polyester +Stone: 52% airlume combed and ringspun cotton, 48% polyester +Retail fit +Tear-away label +Side seamed +Shoulder taping",18 +Apparel,Mens,GEFORCE TRIANGULATION TEE UNISEX,"90/10 airlume combed and ring spun cotton/polyester +Retail fit +Tear-away label +Side seamed +Shoulder taping",16 +Apparel,Mens,GEFORCE ABSTRACTION TEE UNISEX,"Retail fit +90% cotton/10% poly +Machine wash +Imported",16 +Apparel,Mens,WIREFRAME EYE GRAPHIC TEE UNISEX,"100% airlume combed and ring spun cotton +Retail fit +Tear-away label +Side seamed +Shoulder taping",16 +Apparel,Mens,HEROES OF NVIDIA 3.0 UNISEX TEE,"Represent the bright minds known as the Heroes of NVIDIA with this tee. + +Product Details: + +100% cotton jersey +Fine rib trim at neck +Straight fit, hits at the hip +Machine wash +Imported",20 +Apparel,Mens,I AM AI TEE MEN'S,"Express your love for AI with this signature style, premium fitted, short sleeve, super soft crew tee. + +Product Details: + +100% cotton jersey +Fine rib trim at neck +Straight fit, hits at the hip +Machine wash +Imported",16 +Apparel,Mens,MEN'S LULULEMON ABC JOGGER,"Everyday performance featuring shape retention, quick-drying, four-way stretch, breathable and wrinkle-resistant. + +Product Details: + +Intended to sit just above ankle for 30"" inseam +Designed to sit 2-3 inches below your natural waist, it is recommend to measure your waist and adding an inch +Streamlined fit that gives glutes and thighs breathing room, then tapers to hem +Feels smooth, falls softly away from body +Wear the drawcord out or hide it inside for a flat waistband +Hidden media and coin pockets +Secure back pocket",115 +Apparel,Mens,NVIDIA TWILL HAT - SAGE,"Low profile six panel unstructured cap +Standard pre-curved visor +Washed chino twill +Fabric strap with antique brass sliding buckle",18.5 +Apparel,Mens,"12"" MARLED KNIT CUFF BEANIE","12"" Marled Beanie with Cuff or Slouch, Regular Gauge Knit (100% Acrylic), Stretch to Fit",10 +Apparel,Mens,NVIDIA GREEN LABEL BEANIE,"Premium tri-blend (rayong/polyester/nylon) knit cap with cuff +Ultra-soft ribbing throughout",25 +Apparel,Mens,NVIDIA GEFORCE HAT,"Light weight brushed cotton twill +Polysnap closure +Adjustable +Structured",9.5 +Apparel,Mens,NVIDIA CORDUROY HAT,"100% cotton corduroy +6-panel +Unstructured +Low-profile",18 +Apparel,Mens,ANKLE SOCK UNISEX,"80% cotton/15% nylon/5% spandex +Sizing: Men's 9 - 11",12 +Apparel,Mens,NVIDIA ATHLETIC CREW SOCKS,"Cotton athletic socks with compression knitting and cushion, ribbed upper. Black socks with NVIDIA logo vertically on both sides. + +75% Cotton/21% Nylon/4% Lycra +One size fits most (Womens shoe size 6-11 / Mens show size 7-12)",16 +Apparel,Mens,NVIDIA LIGHTWEIGHT HOODIE,"78 oz., 52% Airlume combed and ring spun cotton, 48% polyester fleece +White cord drawstring +Retail fit +Kangaroo pockets +Ribbed cuffs and waistband",40 +Apparel,Kids,YOUTH LOGO TEE,"4.2 oz., 100% airlume combed, ringspun cotton +Retail fit +Unisex youth sizing +Sideseamed +Tear away label +Pre-shrunk",22 +Lifestyle,Lifestyle,PORTABLE FOLDING CHAIR,"Outdoor portable folding chair is great for amphitheaters, beaches, and concerts. Strong and durable construction with cradle-style seat. + +Product Details: + +Steel frame +Includes mesh gear pouch +Capacity: 300 lbs +Chair Dimensions: 26.5""h x 26""w x 18""d +Seat is 12.5"" off the ground",60 +Lifestyle,Lifestyle,TITLEIST® PRO V1® HALF DOZEN GOLF BALLS,"Go for the green! The top selling golf ball in the industry combines soft, fast core; spin control ionomer casing; and a soft, thin urethane elastomer cover with 392 dimples in staggered wave parting line; AIM sidestamp. Assembled in USA. Three balls per sleeve; two sleeves per box.",45 +Lifestyle,Lifestyle,LAPEL PIN,"Stylish and sleek custom NVIDIA lapel pin. + +Product Details: + +Actual size approx. 3/4"" wide +Includes military clutch attachment",2 +Lifestyle,Lifestyle,GEFORCE 40-SERIES ALUMINUM COASTER,"A custom-engraved drink coaster made from a solid piece of aluminum with a cork backing. Durable and long-lasting. Laser engraved design. + +Product Details: + +Material: Black Aluminum",15 +Lifestyle,Lifestyle,S'WELL 16OZ PET BOWL,"This 16 oz S’well Dog Bowl is perfect for keeping food-loving, water-gulping four-legged friends fed and hydrated in style! Engraved logo. + +S'well's timeless designs and durable, double-walled construction making it ideal for food and water. +Non-slip bottom and wide stance for improved stability. +BPA/BPS-free and reusable. +Dishwasher safe. +Approx. 5.35""D x 2.40""H x 5.35""W",35 +Lifestyle,Lifestyle,NVIDIA ICONOGRAPHY BANDANA,"2-ply folded triangular bandana +31.0"" W x 18.5"" H +100% spun polyester",11 +Lifestyle,Lifestyle,CLUBMAN SUNGLASSES,"Vintage design sunglasses fully customized for NVIDIA +Custom pouch +Metallic lenses +UV400 +Glossy finish",12 +Lifestyle,Lifestyle,NVIDIA NIMBLE CHAMP PRO CHARGER,"Keep your devices fully charged while on the go with the Eco-Friendly CHAMP Pro 60W USB-C Portable Charger. Made with REPLAY 72.5% post-consumer plastic, this portable battery is both eco-friendly and supports most USB devices including laptops, smartphones, tablets and cameras. The included USB-C cable is both BPA and PVC-free and made with recyclable materials. Charge up to 2 devices at once. And, it holds up to 8 full phone charges or 3 full tablet charges. + +Product Details: + +Extra compact +20,000 mAh battery +60W Max Output +Works with MacBook, iPhone, iPad, Android devices and more +Up to 80% charge in 30 minutes +Charges two devices simultaneously",124 +Lifestyle,Lifestyle,NVIDIA LICENSE PLATE FRAME,"Sleek and stylish NVIDIA license plate cover. + +Product Details: + +Choose from Matte black or Chrome coated zinc frame +Includes matching screw covers and packaged for protection",30 +Drinkware,Drinkware,40 OZ. STANLEY QUENCHER TUMBLER,"18/8 Recycled stainless steel; naturally BPA-free +Double-wall vacuum insulation +FlowState™ 3-position lid with rotating cover for versatility: no splash straw quencher, open drink spout, or fully covered & close +Silicone sealed cover reduces spills +Reusable straw included +Lid disassembles for deep cleaning +Hand Wash Only +Built for Life™ lifetime warranty +7 HRS HOT / 11 HRS COLD / 2 DAYS ICED",50 +Drinkware,Drinkware,20 OZ. ELEMENTAL WATER BOTTLE,"Featuring unique stainless steel lid with bamboo and convenient partially removable strap, this water bottle is the last water bottle you'll need. Attach it to your purse or bag to make transportation a breeze. The internal strainer is perfect for loose leaf tea, fruit, or drinks that need shaking. + +Product Details: + +2.75"" D x 9.7"" H +Complies with Food Grade +Keeps your beverages cold for up to 24 hours or hot for up to 12 hours +Prevents condensation",30 +Drinkware,Drinkware,25 OZ. NVIDIA ICONOGRAPHY BOTTLE,"The CamelBak® eddy®+ water bottle is made from Eastman Tritan™ RENEW, a durable and sustainable material containing 50% recycled plastic. The bottle is shatter, stain and odor-resistant, and its wide mouth makes it easy to fill or clean. Enjoy spill-proof sipping at work or on the trail thanks to the screw-on lid with bite valve and one-finger carry handle. BPA-free + +Product Details: + +Dimensions: 10.0"" H x 3.0""W x 3.0"" D +Single-wall construction +Handwash only +Straw Top +25 oz.",24 +Drinkware,Drinkware,32 OZ. NVIDIA ICONOGRAPHY BOTTLE,"The CamelBak Mag bottle is made from Eastman Tritan RENEW, a durable and sustainable material containing 50% recycled plastic. The bottle is shatter-, stain- and odor-resistant, and it's wide mouth makes it easy to fill or clean. The two-finger magnetic carry handle keeps the cap stowed while drinking, and the angled spout provides a high flow of water without sloshing or spilling. BPA-free. + +Product Details: + +Dimensions: 9.50"" H x 3.70""W x 3.70"" D +Single-wall construction +Handwash only +Spill-proof with included lid +32 oz.",28 +Drinkware,Drinkware,14 OZ. VISUAL PURR-CEPTION MUG,"Everyone loves cats. Keep an eye on those felines with this 14 oz. deep learning mug inspired by NVIDIA Engineer Robert Bond. + +Product Details: + +3-5/8"" H x 3-5/8 (5 w/handle)"" +Hand wash recommended +Microwave safe +Cannot be shipped to APAC",12 +Drinkware,Drinkware,14 OZ. A NEW BREED OF INNOVATION MUG,"14 oz. ceramic mug features a barrel design, large handle, matte exterior finish and gloss colored interior. + +Product Details: + +3-5/8"" H x 3-5/8 (5 w/handle)"" +Hand wash recommended +Microwave safe",9.5 +Drinkware,Drinkware,14 OZ. I AM AI MUG,"14 oz. ceramic mug features a barrel design, large handle, matte exterior finish and gloss colored interior. + +Product Details: + +3-5/8"" H x 3-5/8 (5 w/handle)"" +Hand wash recommended +Microwave safe",9.5 +Drinkware,Drinkware,14 OZ. NVIDIA LOGO MUG,"14 oz. ceramic mug features a barrel design, large handle, matte exterior finish and gloss colored interior. + +Product Details: + +3-5/8"" H x 3-5/8 (5 w/handle)"" +Hand wash recommended +Microwave safe",7.5 +Office,Office,TIMBUK2 LAPTOP SLEEVE - 2 SIZES,"A protective case with contemporary styling, this accessory begs to be carried in your hand, but will agree to sit inside a bag, for a moment. Sized to fit a laptop, cords, phone, it carries and protects everything you really need. Easily stows inside a bag or can be used independently with the wrist strap. Eco Black + +Product Details: + +Lots of stretchy pockets and internal webbing to maximize organization +Extra padding inside to protect all your contents +100% recycled nylon from pre-consumer material + + +Available in 2 Sizes: + +Fits up to most 13"" laptops: 14.2"" W x 10.6"" H x .984"" D (L) + +Fits up to most 16"" laptops: 15.2"" W x 11.6"" H x .984"" D (XL)",55 +Office,Office,NVIDIA SMALL INFINITY MOUSEPAD,"Dimensions: 10""x8"" +Anti-slip base keeps the mousepad from sliding around +Smooth surface for maximized speed, accuracy, and control +Crafted so edges remain flat +For use on any flat, hard surface",10 +Office,Office,NVIDIA FLOW LARGE MOUSEPAD,"Dimensions: 27.20"" x 11.75"" +2.22 sq. ft. of full-color space +Anti-slip base keeps the mousepad from sliding around +Smooth surface for maximized speed, accuracy, and control +Crafted so edges remain flat +For use on any flat, hard surface",20 +Office,Office,NVIDIA ICONOGRAPHY KRAFT POCKET JOURNAL -3 PACK,"Classic saddle-stitch notebook +Softcover with kraft paper +Round corners +48 pages (24 sheets) +70# (100gsm) paper",16 +Office,Office,RULER 2.0,"The new Ruler 2.0 highlights the Geforce RTX. + + Features include: + +35cm/12in ruler (1mm edge to first marker) +Image from Geforce RTX 2080Ti PCB +RLCs (Resistors, Inductors and Capacitors), Mosfets and Crystals +Gold color is ENIG (Electroless Nickel Immersion Gold) +Endeavor symbol +Simulated break-off tab of a PCB +Holes to facilitate hanging +Unique Serial Numbers",13.5 +Office,Office,RULER,"Made from standard FR4 dielectric with an ENIG plating finish, this ruler is designed to be an engineer's best friend + +Some features include: + +RLCs (Resistors, Inductors and Capacitors), Mosfets and Crystals +Formula quick guide +Footprints for some advanced packages, including memories and some ICs + +All bulk orders must be charged through a cost center and submitted via a Purchase Requisition (PR) through Coupa or NVIDIA IBR. ",4 +Office,Office,TROIKA CONSTRUCTION PEN,"Multifunction black gold brass ballpoint pen. + +Product Details: + +4 different metric ratios (1:20, 1:50, 1:100, 1:1 inch) +Phillips and slotted screwdriver +Stylus",25 +Office,Office,PRODIR® PATTERN PEN,"A fascinating triangular structure characterizes the casing surface of this pen. The pattern is introduced in the injection moulding into the surface of the casing and is an authentic part of the writing instrument. The smooth, minimalistic clip is located above the casing and, in combination with the three-dimensional surface, strongly draws attention to the logo.",4.5 +Office,Office,INTENSITY CLIC GEL PEN,With an automatic clip-retracting mechanism and an ultra-smooth gel roller the BIC® Intensity® Clic™ Gel is a modern take on the retractable pen.,2.5 +Office,Office,COMPUTER CARE KIT,"Whether you want to show off some cool laptop stickers, clean your electronics in style or cover your webcam, this is the kit for you! + +This Computer Care Kit contains: + +Razor Webcam Cover +Screen cleaner +5-piece sticker set (Features 2 new sheet designs and 3 NVIDIA stickers) +Size: 7"" x 9""",12 +Bags,Bags,TIMBUK2 VAPOR BACKPACK TOTE - GRAPHITE,"New Color - Graphite +Timbuk2 Vapor Backpack Tote. + +Product Details: +Constructed with 100% recycled nylon and polyester made from pre- and post-consumer materials waste. +Padded laptop sleeve fits most 15"" laptops +Tuck the tote straps when wearing as a backpack or tuck the shoulder straps when carrying as a tote +Napoleon pocket with key keeper",129 +Bags,Bags,NVIDIA HEX SLING BAG - GRAY,"Product Details: + +Dimensions: (H x W x D): 5.5"" x 10"" x 2.5"" +Recycled ECco Cordura exterior fabric +Anti-microbial technology to repel bacteria, mold and fungus +Durable water-resistant shell +Front zippered pocket +Back panel zippered pocket +Interior mesh pocket +Padded back panel +Side compression straps +Woven cord zipper pulls",55 +Bags,Bags,IGLOO COAST COOLER,"The Igloo Seadrift Cooler collection features MaxCold® insulation more foam to keep drinks and food cooler longer and a classic colorblock design. + +Features MaxCold® insulation with 25% more foam to keep drinks and food cooler longer +Dual zippered opening to large main compartment +Dual side pockets for water bottles and other belongings +Top hatch for easy, quick access to main compartment +Deep, gusseted front zippered pocket for additional storage +Comfortable, rubberized dual carry handles secure together effortlessly +Adjustable, removable shoulder strap with shoulder pad +PEVA heat-sealed lining +PVC and Phthalate free +36 can capacity",95 +Bags,Bags,OGIO CATALYST DUFFEL - BLACK,"Made for the gym or a weekend trip, this modern duffel has ample room to store and organize everything you need. Easy to carry using the padded carrying handles or the detachable, adjustable shoulder strap. + +Product Details: + +600D rhombus poly/600D poly +Large main compartment with U-shaped opening +Side compartment with large mesh window for ventilation +Front zippered pocket +Durable, water-resistant bottom +Dimensions: 10.5""h x 20""w x 10.5""d",50 +Bags,Bags,TIMBUK2 LAPTOP SLEEVE - 2 SIZES,"A protective case with contemporary styling, this accessory begs to be carried in your hand, but will agree to sit inside a bag, for a moment. Sized to fit a laptop, cords, phone, it carries and protects everything you really need. Easily stows inside a bag or can be used independently with the wrist strap. Eco Black + +Product Details: + +Lots of stretchy pockets and internal webbing to maximize organization +Extra padding inside to protect all your contents +100% recycled nylon from pre-consumer material + + +Available in 2 Sizes: + +Fits up to most 13"" laptops: 14.2"" W x 10.6"" H x .984"" D (L) + +Fits up to most 16"" laptops: 15.2"" W x 11.6"" H x .984"" D (XL)",55 +Bags,Bags,TIMBUK2 PARKSIDE BACKPACK 2.0,"The Parkside is loaded with useful organization and well-suited for anyone, from campus to the boardroom. Multiple front pockets are great for storing a tablet and headphones, to charging cables, keys and a phone. Perfect for stowing away your lunch and a light jacket, the interior compartment is roomy and includes a slip pocket for a laptop. Designed to be a workhorse, pack it full with everything you need to get through your day. + +Product Details: + +Front zippered organization zone with iPad slip pocket +Large main compartment fits books, lunch, and a light jacket +Elasticized external side pocket for water bottle or U-lock +Padded back panel and straps for maximum comfort +100% recycled nylon from pre-consumer materials +Fits 15"" laptop",99 +Bags,Bags,VICTORINOX LAPTOP WOMEN'S TOTE,"16"" Women's Laptop Tote +Padded 15.6"" laptop compartment and dedicated 10"" tablet pocket +Essentials organizer with anti-scratch lining +Feet protect the bottom of the bag +Interior zippered pocket +Removable adjustable shoulder strap +Rear pocket unzips to become a Pass-Thru trolley sleeve to slide over the handle of wheeled luggage for easy travel",140 +Bags,Bags,TOPO ROVER TECH PACK,"An exterior front pocket and top flap pocket give quick access to smaller items +External padded laptop sleeve fits most 15"" laptops +Large internal sleeve can be used for notebooks, a laptop or other gear +Expandable side pockets hold your water bottle in place",129 +Bags,Bags,TIMBUK2 COPILOT ROLLER LUGGAGE,"The Copilot is lightweight and easy to pack. Its sleek new styling works downtown or way way out of town and its bike-inspired handle system and broad-base skateboard wheels yield a smooth roll. Its clamshell structure makes it easy to pack and repack, while always providing the opportunity to quarantine things as needed. The Copilot features multiple reinforced grab straps for an easy heave-ho into overhead bins, and its expandable top stash pocket is designed for easy access in said bins. Light, organized, and subtly chic, the Copilot is ready to board. + + Product Details: +52 L +Expandable top compartment for quarantining shoes and toiletries +Padded front zippered pocket fits up to 13"" MacBook or iPad +Internal mesh divider keeps items separate and organized",225 +Bags,Bags,TOTE BAG,"Cotton canvas tote bag. + +Product Details: + +15"" W x 16"" H",9 +Bags,Bags,OGIO® STRATAGEM BACKPACK,"Materials: 600D Oxford polyester, 420D dobby polyester +Padded fleece-lined interior laptop compartment fits most 17"" laptops +Integrated foam panels keep your electronics and other valuables protected +Padded tablet/e-reader sleeve +Large main compartment for books, binders and files +Padded back panel with moisture-wicking air mesh +Dual side water bottle/accessory holders +Easy access front stash pocket",100 +Bags,Bags,GIFT BAG,"Premium weight matte laminated medium gift bag with green handles and cardboard bottom insert. Green tissue paper included. + +Product Details: +Medium: 13x5x10 +Large: 16x6x12",2.1 +Apparel,Womens,WOMEN'S BEYOND YOGA REFOCUS TANK,"Beyond Yoga Spacedye Refocus tank is a flattering fit for all shapes. Cropped active tank with built-in bra. The right length to pair over your high waisted leggings. Reflective NVIDIA logo on right side hem. + +Product Details: + +High scooped neckline +Slim racerback design +87% polyester/13% elastane",70 +Apparel,Womens,WOMEN'S BEYOND YOGA MOVEMENT SKIRT,"Beyond Yoga Spacedye lined Movement skirt has comfortable built-in shorts. Made with the classic A-line flare and curved hem at bottom. Super comfortable and flattering active skirt. Reflective NVIDIA logo on hem. + +Product Details: + +3"" waistband sits at natural waist +15 in. length +87% polyester/13% elastane",88 +Apparel,Womens,WOMEN'S BEYOND YOGA IN-STRIDE PULLOVER - BLACK,"Ultra-light, breezy workout zip front pullover made of our new airy performance fabric. Featuring a front kangaroo pocket and elastic waist for the perfect fit. This woven active fabric features an ultra light feel, enabling the versatility for both being active or kicking back. Reflective NVIDIA logo on back of collar. + +Product Details: + +Half zip front +Elasticated cuffs and hemband +Classic length +86% Recycled Polyester/14% Spandex",128 +Apparel,Womens,WOMEN'S V-NECK TEE,"Bella + Canvas women's v-neck t-shirt with logo embroidered. + +Product Details: + +4.2 oz, 100& combed and ringspun cotton +Relaxed fit +Sideseamed +Pre-shrunk",22 +Apparel,Womens,MARINE LAYER CORBET FULL ZIP VEST WOMEN'S,"We took the (dare we say) famous Corbet fabric and put it in a full zip vest silhouette! + +Product Details: + +68% Polyester, 16% Cotton, 16% Rayon +Signature Corbet Quilted Fabric +Standard fit +Wash cold, dry low +Made responsibly in China",119 +Apparel,Womens,WOMEN'S BEYOND YOGA SPACEDYE RACERBACK CROPPED TANK,"This Beyond Yoga tank is made from performance fabric featuring UPF 50+ protection, built-in bra offering medium support, soft straps that never dig in, and racerback design to balance athleticism with effortlessness. Reflective NVIDIA logo on back + +Color: Darkest Night +Skinny racerback with keyhole +Self shelf bra +Cropped length +Reflective NVIDIA logo on back",60 +Apparel,Womens,WOMEN'S LOGO TEE,"Women's relaxed fit t-shirt. + +Product details: + +Relaxed fit +Tear-away label +Side seamed +4.2-ounce, 100% Airlume combed and ring spun cotton",22 +Apparel,Womens,WOMEN'S FLEECE CROPPED CREW,"District women's perfect weight fleece. Cropped with crewneck in premium 8.26-ounce weight. Heathered Loganberry with NVIDIA logo embroidered on front and woven patch along bottom hem. + +Product Details: + +65/35 combed ring spun cotton/poly blend +Drop shoulder +Twill back neck tape +2x1 rib knit neck, cuffs and hem",35 +Apparel,Womens,NORTH FACE WOMEN'S TREKKER JACKET,"Designed with baffles contoured to fit your body, this streamlined jacket offers lightweight, highly compressible synthetic insulation. +The Trekker jacket will keep you warmer regardless of the conditions. + + Product Details: +15D 33 g/m2 100% nylon with durable water-repellent (DWR) finish +Partially constructed from recycled fabrications +Interior left chest zippered pocket +Interior elastic cuffs +Secure-zip hand pockets +Stows in left hand pocket +Hem cinch-cord",229 +Apparel,Womens,FLUX BONDED JACKET WOMEN'S LIGHT HEATHER,"Take performance to the next level with this 2.0 Fleece Jacket. The modern fit and sleek look make this the ideal jacket for all. + +Product Details: + +100% polyester microfleece +Coverstitch details throughout +Center front coil zipper with contrast teeth, semi-autolock slider and rubber pull +Lower pockets for flattering feminine fit +Lower pockets with invisible zippers +Active fit +Easy care",65 +Apparel,Womens,NIKE 2.0 POLO WOMEN'S,"100% polyester Dri-FIT fabric +Tailored for a feminine fit with a self-fabric collar +Open neckline and side vents +Rolled-forward shoulder seams +Open hem sleeves and open hem +Contrast Swoosh logo is embroidered on the left sleeve",40 +Apparel,Womens,COTTON TOUCH POLO WOMEN'S,"This polyester polo performs with the comfortable feel of cotton. Just right for the workday or after hours, this sophisticated style has subtle texture, manages moisture, keeps you fresher with odor-controlling technology and feels great against your skin. + +Product Details: + +Made from 5.8 oz. 95/5 poly/spandex jersey +Self-fabric collar +Tag-free label +Y-neck placket",25 +Apparel,Womens,LONG SLEEVE PERFORMANCE SHIRT WOMEN'S,"This crew neck is the perfect shirt whether you are exercising or just lounging around. + +Product Details: + +Stretchy and comfortable 90% polyester 10% spandex fabric +Decorative and functional reflective patterns on front & back +Moisture wicking, UPF 30 +Sleeve thumbholes",35 +Apparel,Womens,I AM AI TEE WOMEN'S,"This short sleeve, ladies cut, super soft boyfriend crew will instantly be loved by all who wear it. + +Product Details: + +100% cotton jersey +Laundered for reduced shrinkage +Ladies' cut +Machine wash",16 +Apparel,Womens,WOMEN'S POCKET LEGGINGS,"Beyond Yoga Pocket Leggings + +Product Details: + +Ultra-soft feel +4-way stretch to move with you +Moisture-wicking to keep dry +Easy care- wash + dry +Enjoy the sun with UV protection +87% Polyester, 13% LYCRA®",99 diff --git a/data/list_manuals.txt b/data/list_manuals.txt new file mode 100755 index 0000000..a3690bd --- /dev/null +++ b/data/list_manuals.txt @@ -0,0 +1,6 @@ +https://www.nvidia.com/content/geforce-gtx/GeForce_RTX_4090_QSG_Rev1.pdf +https://www.nvidia.com/content/geforce-gtx/GEFORCE_RTX_4080_SUPER_User_Guide_Rev1.pdf +https://www.nvidia.com/content/geforce-gtx/GeForce_RTX_4080_QSG_Rev1.pdf +https://www.nvidia.com/content/geforce-gtx/GEFORCE_RTX_4070_SUPER_User_Guide_Rev1.pdf +https://www.nvidia.com/content/geforce-gtx/GeForce_RTX_4070_QSG_Rev1.pdf +https://www.nvidia.com/content/geforce-gtx/GEFORCE_RTX_4060_Ti_User_Guide_Rev1.pdf diff --git a/data/orders.csv b/data/orders.csv new file mode 100644 index 0000000..58e3276 --- /dev/null +++ b/data/orders.csv @@ -0,0 +1,574 @@ +CID,OrderID,product_name,product_description,OrderDate,Quantity,OrderAmount,OrderStatus,ReturnStatus,ReturnStartDate,ReturnReceivedDate,ReturnCompletedDate,ReturnReason,Notes +4165,52768,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-05T10:00:00,2,298.0,Delivered,"",,,,, +4165,4065,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-10T14:00:00,1,1599.0,Return Requested,Requested,2024-10-12T14:00:00,,,Received a faulty unit that doesn't display correctly., +4165,69268,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",2024-10-01T09:30:00,1,599.0,Delivered,"",,,,, +4165,71110,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-15T16:00:00,3,164.97,Returned,Approved,,2024-10-18T16:00:00,2024-11-02T16:00:00,Item stopped functioning correctly within a week., +4165,15620,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-03T11:30:00,1,199.99,Delivered,"",,,,, +4165,15848,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",2024-10-07T08:45:00,4,1196.0,Canceled,"",,,,,Order canceled due to payment processing issues. +4165,13955,NVIDIA® GEFORCE RTX™ 4070 SUPER,"Product Description: Get equipped for supercharged gaming and creating with the NVIDIA® GeForce RTX™ 4070 SUPER. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience super-fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 7168 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X",2024-10-16T15:00:00,2,1598.0,Return Requested,Pending Approval,2024-10-18T15:00:00,,,The performance did not meet expectations., +4165,51115,NVIDIA SMALL INFINITY MOUSEPAD,"Dimensions: 10""x8"" +Anti-slip base keeps the mousepad from sliding around +Smooth surface for maximized speed, accuracy, and control +Crafted so edges remain flat +For use on any flat, hard surface",2024-10-05T12:00:00,5,50.0,Delivered,"",,,,, +4165,67932,COMPUTER CARE KIT,"Whether you want to show off some cool laptop stickers, clean your electronics in style or cover your webcam, this is the kit for you! + +This Computer Care Kit contains: + +Razor Webcam Cover +Screen cleaner +5-piece sticker set (Features 2 new sheet designs and 3 NVIDIA stickers) +Size: 7"" x 9""",2024-10-09T13:00:00,6,72.0,Returned,Rejected,,2024-10-18T13:00:00,,Item was opened and used.,Return rejected as the product was not in original condition. +4165,62025,GEFORCE ABSTRACTION TEE UNISEX,"Retail fit +90% cotton/10% poly +Machine wash +Imported",2024-10-11T17:00:00,3,48.0,In Transit,"",,,,,Order has been delayed due to shipping issues. +125,88153,NVIDIA® GEFORCE RTX™ 4080,"The NVIDIA® GeForce RTX™ 4080 delivers the ultra performance and features that enthusiast gamers and creators demand. Bring your games and creative projects to life with ray tracing and AI-powered graphics. It's powered by the ultra-efficient NVIDIA Ada Lovelace architecture and 16GB of superfast G6X memory. + +CUDA Cores: 9728 +Boost Clock: 2.51 GHz +Memory Configuration: 16GB GDDR6X +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-10T14:30:00,2,2398.0,Delivered,"",,,,, +125,7035,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-15T10:00:00,1,1599.0,Return Requested,Requested,2024-10-16T10:00:00,,,Product did not perform as expected., +125,17774,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-12T09:00:00,4,596.0,Returned,Approved,,2024-10-17T09:00:00,2024-11-02T09:00:00,Compatibility issues with existing hardware., +125,45309,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",2024-10-14T16:00:00,3,897.0,Shipped,"",,,,, +125,46869,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-05T12:30:00,5,274.95,Delivered,"",,,,, +125,74210,NVIDIA® SHIELD™ TV 2019,"NVIDIA® SHIELD® TV is the ultimate streaming media player for the modern living room. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. The new SHIELD TV is compact, stealth, and designed to disappear behind your entertainment center, right along with your cables. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up. Experience more.",2024-10-18T08:15:00,1,199.0,Pending,"",,,,, +125,39550,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-02T11:45:00,2,399.98,Returned,Rejected,,2024-10-10T11:45:00,,The device did not integrate properly with existing equipment.,Return has been rejected due to insufficient compatibility information provided. +125,19498,WOMEN'S BEYOND YOGA MOVEMENT SKIRT,"Beyond Yoga Spacedye lined Movement skirt has comfortable built-in shorts. Made with the classic A-line flare and curved hem at bottom. Super comfortable and flattering active skirt. Reflective NVIDIA logo on hem. + +Product Details: + +3"" waistband sits at natural waist +15 in. length +87% polyester/13% elastane",2024-10-06T14:20:00,3,264.0,In Transit,"",,,,, +125,85593,GEFORCE TRIANGULATION TEE UNISEX,"90/10 airlume combed and ring spun cotton/polyester +Retail fit +Tear-away label +Side seamed +Shoulder taping",2024-10-08T17:30:00,8,128.0,Cancelled,"",,,,,Order cancelled due to processing delays. +125,89350,14 OZ. I AM AI MUG,"14 oz. ceramic mug features a barrel design, large handle, matte exterior finish and gloss colored interior. + +Product Details: + +3-5/8"" H x 3-5/8 (5 w/handle)"" +Hand wash recommended +Microwave safe",2024-10-17T15:15:00,6,57.0,Processing,"",,,,, +5603,36121,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",2024-10-05T10:00:00,3,1498.85,Delivered,"",,,,, +5603,47597,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-12T11:30:00,2,298.0,Return Requested,Requested,2024-10-17T11:30:00,,,Incompatibility with existing hardware., +5603,58681,NVIDIA® GEFORCE RTX™ 4080,"The NVIDIA® GeForce RTX™ 4080 delivers the ultra performance and features that enthusiast gamers and creators demand. Bring your games and creative projects to life with ray tracing and AI-powered graphics. It's powered by the ultra-efficient NVIDIA Ada Lovelace architecture and 16GB of superfast G6X memory. + +CUDA Cores: 9728 +Boost Clock: 2.51 GHz +Memory Configuration: 16GB GDDR6X +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-01T14:45:00,1,1199.0,Returned,Approved,,2024-10-03T14:45:00,2024-10-20T14:45:00,Product did not meet performance expectations., +5603,54706,NVIDIA® SHIELD™ TV 2019,"NVIDIA® SHIELD® TV is the ultimate streaming media player for the modern living room. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. The new SHIELD TV is compact, stealth, and designed to disappear behind your entertainment center, right along with your cables. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up. Experience more.",2024-10-08T09:00:00,1,199.0,Shipped,"",,,,, +5603,1643,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-10T16:00:00,2,3198.0,Cancelled,"",,,,,Order cancelled due to payment issues. +5603,57160,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",2024-10-03T13:15:00,1,599.0,Returned,Rejected,,2024-10-05T13:15:00,,Not suitable for VR applications.,Performance issue not covered under warranty. +5603,6394,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-04T15:00:00,4,219.96,In Transit,"",,,,, +5603,46575,NORTH FACE WOMEN'S TREKKER JACKET,"Designed with baffles contoured to fit your body, this streamlined jacket offers lightweight, highly compressible synthetic insulation. +The Trekker jacket will keep you warmer regardless of the conditions. + + Product Details: +15D 33 g/m2 100% nylon with durable water-repellent (DWR) finish +Partially constructed from recycled fabrications +Interior left chest zippered pocket +Interior elastic cuffs +Secure-zip hand pockets +Stows in left hand pocket +Hem cinch-cord",2024-10-14T12:00:00,2,458.0,Processing,"",,,,,Processing delay due to high order volume. +5603,9145,TIMBUK2 LAPTOP SLEEVE - 2 SIZES,"A protective case with contemporary styling, this accessory begs to be carried in your hand, but will agree to sit inside a bag, for a moment. Sized to fit a laptop, cords, phone, it carries and protects everything you really need. Easily stows inside a bag or can be used independently with the wrist strap. Eco Black + +Product Details: + +Lots of stretchy pockets and internal webbing to maximize organization +Extra padding inside to protect all your contents +100% recycled nylon from pre-consumer material + + +Available in 2 Sizes: + +Fits up to most 13"" laptops: 14.2"" W x 10.6"" H x .984"" D (L) + +Fits up to most 16"" laptops: 15.2"" W x 11.6"" H x .984"" D (XL)",2024-10-11T09:30:00,1,55.0,Returned,Approved,,2024-10-13T09:30:00,2024-10-28T09:30:00,Size too small for my laptop., +5603,65042,MEN'S LULULEMON ABC JOGGER,"Everyday performance featuring shape retention, quick-drying, four-way stretch, breathable and wrinkle-resistant. + +Product Details: + +Intended to sit just above ankle for 30"" inseam +Designed to sit 2-3 inches below your natural waist, it is recommend to measure your waist and adding an inch +Streamlined fit that gives glutes and thighs breathing room, then tapers to hem +Feels smooth, falls softly away from body +Wear the drawcord out or hide it inside for a flat waistband +Hidden media and coin pockets +Secure back pocket",2024-10-15T18:00:00,5,575.0,Delivered,"",,,,, +9522,50433,NVIDIA® GEFORCE RTX™ 4070 SUPER,"Product Description: Get equipped for supercharged gaming and creating with the NVIDIA® GeForce RTX™ 4070 SUPER. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience super-fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 7168 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X",2024-10-10T10:00:00,2,1598.0,Delivered,"",,,,, +9522,68658,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-15T14:30:00,1,1599.0,Returned,Approved,2024-10-16T14:30:00,2024-10-18T14:30:00,2024-11-02T14:30:00,Product was too advanced for my current needs., +9522,3480,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-12T09:00:00,1,199.99,Delivered,"",,,,, +9522,36447,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",2024-10-05T11:15:00,4,1196.0,Return Requested,Pending Approval,2024-10-07T11:15:00,,,Incompatibility with existing hardware., +9522,51111,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",2024-10-09T16:45:00,2,999.9,Shipped,"",,,,, +9522,99510,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",2024-10-14T10:30:00,3,1797.0,Delivered,"",,,,, +9522,20539,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-01T08:00:00,5,745.0,Returned,Rejected,2024-10-03T08:00:00,2024-10-06T08:00:00,,Product did not meet expectations for AI projects.,Return request rejected due to misuse claims. +9522,93281,IGLOO COAST COOLER,"The Igloo Seadrift Cooler collection features MaxCold® insulation more foam to keep drinks and food cooler longer and a classic colorblock design. + +Features MaxCold® insulation with 25% more foam to keep drinks and food cooler longer +Dual zippered opening to large main compartment +Dual side pockets for water bottles and other belongings +Top hatch for easy, quick access to main compartment +Deep, gusseted front zippered pocket for additional storage +Comfortable, rubberized dual carry handles secure together effortlessly +Adjustable, removable shoulder strap with shoulder pad +PEVA heat-sealed lining +PVC and Phthalate free +36 can capacity",2024-10-04T12:00:00,1,95.0,In Transit,"",,,,, +9522,47589,NVIDIA LIGHTWEIGHT HOODIE,"78 oz., 52% Airlume combed and ring spun cotton, 48% polyester fleece +White cord drawstring +Retail fit +Kangaroo pockets +Ribbed cuffs and waistband",2024-10-20T15:00:00,3,120.0,Processing,"",,,,,Order has been in processing longer than expected. +9522,31197,MARINE LAYER CORBET FULL ZIP MEN'S,"We took the (dare we say) famous Corbet fabric and put it in a full zip jacket silhouette! + +Product Details: + +Black +68% Polyester, 16% Cotton, 16% Rayon +Signature Corbet Quilted Fabric +Standard fit +Wash cold, dry low +Made responsibly in China",2024-10-13T13:00:00,2,278.0,Cancelled,"",,,,,Order was cancelled due to stock issues. +6229,94036,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-15T10:30:00,2,3198.0,Delivered,"",,,,, +6229,52557,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-07T14:45:00,1,199.99,Return Requested,Requested,2024-10-10T14:45:00,,,Product did not meet expectations regarding streaming quality., +6229,96577,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-12T09:00:00,4,596.0,Returned,Approved,,2024-10-14T09:00:00,2024-10-30T09:00:00,"Device was too complicated for intended use, not suitable for beginners.", +6229,29933,NVIDIA® SHIELD™ TV 2019,"NVIDIA® SHIELD® TV is the ultimate streaming media player for the modern living room. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. The new SHIELD TV is compact, stealth, and designed to disappear behind your entertainment center, right along with your cables. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up. Experience more.",2024-10-16T11:00:00,1,199.0,Cancelled,"",,,,,Order was cancelled due to a last-minute decision. +6229,24251,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",2024-10-18T13:15:00,2,999.9,In Transit,"",,,,, +6229,77196,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-05T15:25:00,3,164.97,Returned,Approved,,2024-10-10T15:25:00,2024-10-25T15:25:00,Remote had issues with connectivity and did not sync with devices., +6229,63802,NVIDIA® GEFORCE RTX™ 4070 SUPER,"Product Description: Get equipped for supercharged gaming and creating with the NVIDIA® GeForce RTX™ 4070 SUPER. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience super-fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 7168 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X",2024-10-08T16:00:00,1,799.0,Shipped,"",,,,, +6229,51961,RULER 2.0,"The new Ruler 2.0 highlights the Geforce RTX. + + Features include: + +35cm/12in ruler (1mm edge to first marker) +Image from Geforce RTX 2080Ti PCB +RLCs (Resistors, Inductors and Capacitors), Mosfets and Crystals +Gold color is ENIG (Electroless Nickel Immersion Gold) +Endeavor symbol +Simulated break-off tab of a PCB +Holes to facilitate hanging +Unique Serial Numbers",2024-10-11T14:00:00,5,67.5,Return Requested,Requested,2024-10-14T14:00:00,,,Item arrived with scratches that affect the display quality., +6229,44097,"12"" MARLED KNIT CUFF BEANIE","12"" Marled Beanie with Cuff or Slouch, Regular Gauge Knit (100% Acrylic), Stretch to Fit",2024-10-20T12:00:00,6,60.0,Delivered,"",,,,, +6229,29599,RULER,"Made from standard FR4 dielectric with an ENIG plating finish, this ruler is designed to be an engineer's best friend + +Some features include: + +RLCs (Resistors, Inductors and Capacitors), Mosfets and Crystals +Formula quick guide +Footprints for some advanced packages, including memories and some ICs + +All bulk orders must be charged through a cost center and submitted via a Purchase Requisition (PR) through Coupa or NVIDIA IBR. ",2024-10-06T17:50:00,2,8.0,Delayed,"",,,,,Order has been delayed due to unforeseen shipping issues. +8301,37702,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-05T14:30:00,2,109.98,Delivered,"",,,,, +8301,71718,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-10T10:15:00,1,149.0,Return Requested,Requested,2024-10-12T10:00:00,,,Product did not meet the intended use for AI training., +8301,24497,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",2024-10-15T12:00:00,3,897.0,Returned,Approved,,2024-10-16T12:00:00,2024-11-02T12:00:00,Product was not compatible with existing setup., +8301,53810,NVIDIA® GEFORCE RTX™ 4070 SUPER,"Product Description: Get equipped for supercharged gaming and creating with the NVIDIA® GeForce RTX™ 4070 SUPER. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience super-fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 7168 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X",2024-10-01T09:00:00,1,799.0,Cancelled,"",,,,,Order was cancelled due to payment processing delay. +8301,67088,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",2024-10-08T16:45:00,2,999.9,In Transit,"",,,,,Order is delayed due to shipping issues. +8301,90567,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",2024-10-14T11:15:00,1,599.0,Returned,Approved,,2024-10-17T11:00:00,2024-11-01T11:00:00,Performance did not match expectations., +8301,81103,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-18T15:30:00,1,199.99,Processing,"",,,,,Order is being processed. +8301,54212,MARINE LAYER CORBET FULL ZIP VEST WOMEN'S,"We took the (dare we say) famous Corbet fabric and put it in a full zip vest silhouette! + +Product Details: + +68% Polyester, 16% Cotton, 16% Rayon +Signature Corbet Quilted Fabric +Standard fit +Wash cold, dry low +Made responsibly in China",2024-10-06T09:45:00,4,476.0,Delivered,"",,,,, +8301,48309,COTTON TOUCH POLO WOMEN'S,"This polyester polo performs with the comfortable feel of cotton. Just right for the workday or after hours, this sophisticated style has subtle texture, manages moisture, keeps you fresher with odor-controlling technology and feels great against your skin. + +Product Details: + +Made from 5.8 oz. 95/5 poly/spandex jersey +Self-fabric collar +Tag-free label +Y-neck placket",2024-10-11T13:20:00,3,75.0,Returned,Rejected,,2024-10-13T13:00:00,,Product size was not as described.,Return was rejected due to incorrect size selection. +8301,43853,NIKE 2.0 POLO MEN'S,"100% polyester Dri-FIT fabric +Flat knit collar and three-button placket +Rolled-forward shoulder seams +Open hem sleeves and open hem +Contrast Swoosh logo is embroidered on the left sleeve",2024-10-09T08:30:00,2,80.0,Delivered,"",,,,, +7154,22723,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-05T12:30:00,2,109.98,Delivered,"",,,,, +7154,85009,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",2024-10-09T09:00:00,1,299.0,Returned,Approved,2024-10-10T09:00:00,2024-10-12T09:00:00,2024-10-27T09:00:00,"Too complex for my needs, found it overwhelming.", +7154,34078,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-15T15:00:00,1,199.99,Returned,Rejected,2024-10-16T15:00:00,2024-10-18T15:00:00,,Device stopped working after one week.,Device malfunctioned and is not covered under warranty. +7154,42084,NVIDIA® GEFORCE RTX™ 4080,"The NVIDIA® GeForce RTX™ 4080 delivers the ultra performance and features that enthusiast gamers and creators demand. Bring your games and creative projects to life with ray tracing and AI-powered graphics. It's powered by the ultra-efficient NVIDIA Ada Lovelace architecture and 16GB of superfast G6X memory. + +CUDA Cores: 9728 +Boost Clock: 2.51 GHz +Memory Configuration: 16GB GDDR6X +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-11T10:30:00,1,1199.0,In Transit,"",,,,, +7154,89884,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",2024-10-20T11:00:00,1,499.95,Delivered,"",,,,, +7154,46993,NVIDIA® SHIELD™ TV 2019,"NVIDIA® SHIELD® TV is the ultimate streaming media player for the modern living room. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. The new SHIELD TV is compact, stealth, and designed to disappear behind your entertainment center, right along with your cables. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up. Experience more.",2024-10-03T14:00:00,3,597.0,Cancelled,"",,,,,Order cancelled upon request due to a change in need. +7154,9205,NVIDIA® GEFORCE RTX™ 4070 SUPER,"Product Description: Get equipped for supercharged gaming and creating with the NVIDIA® GeForce RTX™ 4070 SUPER. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience super-fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 7168 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X",2024-10-08T13:00:00,4,3196.0,Returned,Approved,2024-10-09T13:00:00,2024-10-11T13:00:00,2024-10-26T13:00:00,Product did not meet performance expectations., +7154,22931,VICTORINOX LAPTOP WOMEN'S TOTE,"16"" Women's Laptop Tote +Padded 15.6"" laptop compartment and dedicated 10"" tablet pocket +Essentials organizer with anti-scratch lining +Feet protect the bottom of the bag +Interior zippered pocket +Removable adjustable shoulder strap +Rear pocket unzips to become a Pass-Thru trolley sleeve to slide over the handle of wheeled luggage for easy travel",2024-10-06T08:00:00,2,280.0,Return Requested,Requested,2024-10-07T08:00:00,,,Color mismatch; ordered thinking it was black., +7154,93937,MARINE LAYER CORBET FULL ZIP VEST MEN'S,"We took the (dare we say) famous Corbet fabric and put it in a full zip vest silhouette! + +68% Polyester, 16% Cotton, 16% Rayon +Signature Corbet Quilted Fabric +Standard fit +Wash cold, dry low +Made responsibly in China",2024-10-04T14:45:00,1,119.0,Shipped,"",,,,, +7154,28089,NIKE 2.0 POLO MEN'S,"100% polyester Dri-FIT fabric +Flat knit collar and three-button placket +Rolled-forward shoulder seams +Open hem sleeves and open hem +Contrast Swoosh logo is embroidered on the left sleeve",2024-10-14T17:30:00,4,160.0,Out for Delivery,"",,,,, +4706,83007,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-05T10:00:00,2,3198.0,Returned,Approved,2024-10-06T10:00:00,2024-10-09T10:00:00,2024-10-25T10:00:00,Device did not meet performance expectations for high-end gaming., +4706,18855,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",2024-10-10T10:00:00,1,499.95,Delivered,"",,,,, +4706,76125,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-15T10:00:00,3,164.97,Return Requested,Requested,2024-10-16T10:00:00,,,Remote control frequently disconnects during use., +4706,70214,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-05T10:00:00,1,199.99,Shipped,"",,,,, +4706,50036,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",2024-10-02T10:00:00,1,599.0,Return Requested,Pending Approval,2024-10-03T10:00:00,,,Graphics card did not meet compatibility requirements., +4706,41445,NVIDIA® GEFORCE RTX™ 4080,"The NVIDIA® GeForce RTX™ 4080 delivers the ultra performance and features that enthusiast gamers and creators demand. Bring your games and creative projects to life with ray tracing and AI-powered graphics. It's powered by the ultra-efficient NVIDIA Ada Lovelace architecture and 16GB of superfast G6X memory. + +CUDA Cores: 9728 +Boost Clock: 2.51 GHz +Memory Configuration: 16GB GDDR6X +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-12T10:00:00,2,2398.0,Cancelled,"",,,,,Order cancelled due to pricing error. +4706,34172,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-07T10:00:00,4,596.0,Delivered,"",,,,, +4706,37929,32 OZ. NVIDIA ICONOGRAPHY BOTTLE,"The CamelBak Mag bottle is made from Eastman Tritan RENEW, a durable and sustainable material containing 50% recycled plastic. The bottle is shatter-, stain- and odor-resistant, and it's wide mouth makes it easy to fill or clean. The two-finger magnetic carry handle keeps the cap stowed while drinking, and the angled spout provides a high flow of water without sloshing or spilling. BPA-free. + +Product Details: + +Dimensions: 9.50"" H x 3.70""W x 3.70"" D +Single-wall construction +Handwash only +Spill-proof with included lid +32 oz.",2024-10-08T10:00:00,5,140.0,Returned,Rejected,2024-10-09T10:00:00,2024-10-11T10:00:00,,Item arrived with a dent and scratches.,Return request rejected due to signs of use. +4706,63422,COMPUTER CARE KIT,"Whether you want to show off some cool laptop stickers, clean your electronics in style or cover your webcam, this is the kit for you! + +This Computer Care Kit contains: + +Razor Webcam Cover +Screen cleaner +5-piece sticker set (Features 2 new sheet designs and 3 NVIDIA stickers) +Size: 7"" x 9""",2024-10-01T10:00:00,2,24.0,In Transit,"",,,,, +4706,44400,14 OZ. NVIDIA LOGO MUG,"14 oz. ceramic mug features a barrel design, large handle, matte exterior finish and gloss colored interior. + +Product Details: + +3-5/8"" H x 3-5/8 (5 w/handle)"" +Hand wash recommended +Microwave safe",2024-10-20T10:00:00,6,45.0,Delivered,"",,,,, +4402,64056,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",2024-10-10T10:00:00,3,1797.0,Returned,Approved,2024-10-13T10:00:00,2024-10-15T10:00:00,2024-10-30T10:00:00,Item did not meet performance expectations for gaming., +4402,33037,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-05T10:00:00,1,199.99,Delivered,"",,,,, +4402,7161,NVIDIA JETSON ORIN NANO DEVELOPER KIT,"The NVIDIA® Jetson Orin™ Nano Developer Kit sets a new standard for creating entry-level AI-powered robots, smart drones, and intelligent cameras. Compact design, lots of connectors, and up to 40 TOPS of AI performance deliver everything you need to transform your visionary concepts into reality. + +With up to 80X the performance of Jetson Nano, the developer kit can run all modern AI models, including those for transformer and advanced robotics. It features a Jetson Orin Nano 8GB module, a reference carrier board that can accommodate all Orin Nano and Orin NX modules, and the power to run the entire NVIDIA AI software stack. This gives you the ideal platform for prototyping your next-gen edge AI solution. + +Get started with your AI journey!",2024-10-01T10:00:00,2,999.9,Return Requested,Requested,2024-10-03T10:00:00,,,Product did not work as expected for AI development., +4402,70054,NVIDIA® SHIELD™ TV 2019,"NVIDIA® SHIELD® TV is the ultimate streaming media player for the modern living room. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. The new SHIELD TV is compact, stealth, and designed to disappear behind your entertainment center, right along with your cables. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up. Experience more.",2024-10-12T10:00:00,1,199.0,Shipped,"",,,,, +4402,8542,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-15T10:00:00,2,298.0,In Transit,"",,,,, +4402,99659,NVIDIA® GEFORCE RTX™ 4070 SUPER,"Product Description: Get equipped for supercharged gaming and creating with the NVIDIA® GeForce RTX™ 4070 SUPER. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience super-fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 7168 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X",2024-10-18T10:00:00,1,799.0,Cancelled,"",,,,,Order was cancelled before shipping due to payment issue. +4402,65954,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",2024-10-02T10:00:00,4,1196.0,Delivered,"",,,,, +4402,73966,25 OZ. NVIDIA ICONOGRAPHY BOTTLE,"The CamelBak® eddy®+ water bottle is made from Eastman Tritan™ RENEW, a durable and sustainable material containing 50% recycled plastic. The bottle is shatter, stain and odor-resistant, and its wide mouth makes it easy to fill or clean. Enjoy spill-proof sipping at work or on the trail thanks to the screw-on lid with bite valve and one-finger carry handle. BPA-free + +Product Details: + +Dimensions: 10.0"" H x 3.0""W x 3.0"" D +Single-wall construction +Handwash only +Straw Top +25 oz.",2024-10-07T10:00:00,8,192.0,Returned,Approved,2024-10-10T10:00:00,2024-10-12T10:00:00,2024-10-27T10:00:00,"Quality issue with material, leak during use.", +4402,52719,GEFORCE 40-SERIES ALUMINUM COASTER,"A custom-engraved drink coaster made from a solid piece of aluminum with a cork backing. Durable and long-lasting. Laser engraved design. + +Product Details: + +Material: Black Aluminum",2024-10-04T10:00:00,5,75.0,Processing,"",,,,,Order processing longer than expected due to high demand. +4402,41544,NVIDIA CORDUROY HAT,"100% cotton corduroy +6-panel +Unstructured +Low-profile",2024-10-06T10:00:00,6,108.0,Out for Delivery,"",,,,, +6301,85763,NVIDIA® GEFORCE RTX™ 4080,"The NVIDIA® GeForce RTX™ 4080 delivers the ultra performance and features that enthusiast gamers and creators demand. Bring your games and creative projects to life with ray tracing and AI-powered graphics. It's powered by the ultra-efficient NVIDIA Ada Lovelace architecture and 16GB of superfast G6X memory. + +CUDA Cores: 9728 +Boost Clock: 2.51 GHz +Memory Configuration: 16GB GDDR6X +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-05T10:30:00,2,2398.0,Delivered,"",,,,, +6301,91283,NVIDIA® SHIELD™ TV PRO 2019,"NVIDIA® SHIELD® TV Pro is the supreme streaming media player - packed with features to make even the most demanding users proud. Beautifully designed to be the perfect centerpiece of your entertainment center. Enjoy a cinematic experience with stunning visuals brought to you by Dolby Vision HDR, and immersive audio with Dolby Atmos surround sound. Now powered by the latest NVIDIA Tegra X1+ processor, SHIELD TV is faster, and smarter. Level up to SHIELD TV Pro for more storage space, two USB 3.0 ports for expandability (storage expansion, USB cameras, USB keyboards and controllers, TV tuners, and more), Plex Media Server, SmartThings hub-ready (just add a SmartThings Link), AAA Android gaming, Twitch broadcasting, and 3GB RAM.",2024-10-12T14:00:00,1,199.99,Delivered,"",,,,, +6301,23540,NVIDIA® SHIELD™ REMOTE (2020),"Motion-activated, backlit buttons +Mic for voice search and control +Bluetooth connectivity +IR for TV control +Built-in lost remote locator +930-13700-2500-100",2024-10-15T09:25:00,3,164.97,Returned,Approved,2024-10-17T09:25:00,2024-10-18T09:25:00,2024-10-22T09:25:00,The remote was non-responsive and not connecting as expected., +6301,49362,JETSON NANO DEVELOPER KIT,"The power of modern AI is now available for makers, learners, and embedded developers everywhere. + +NVIDIA® Jetson Nano Developer Kit is a small, powerful computer that lets you run multiple neural networks in parallel for applications like image classification, object detection, segmentation, and speech processing. All in an easy-to-use platform that runs in as little as 5 watts. + +It’s simpler than ever to get started! Just insert a microSD card with the system image, boot the developer kit, and enjoy using the same NVIDIA JetPack SDK used across the NVIDIA Jetson™ family of products. + +This version of Jetson Nano Developer Kit has two camera connectors.",2024-10-02T11:15:00,1,149.0,Cancelled,"",,,,,Order was cancelled due to shipping delays. +6301,49513,NVIDIA® GEFORCE RTX™ 4070,"Get equipped for stellar gaming and creating with the NVIDIA® GeForce RTX™ 4070. It’s built with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience fast ray tracing, AI-accelerated performance with DLSS 3, new ways to create, and much more. + +CUDA Cores: 5888 +Boost Clock: 2.48 GHz +Memory Configuration: 12GB GDDR6X +Model 900-1G141-2544-000",2024-10-09T13:30:00,4,2396.0,Returned,Requested,2024-10-14T13:30:00,2024-10-16T13:30:00,,The graphics card was incompatible with current system requirements., +6301,26879,NVIDIA® GEFORCE™ RTX 4060TI 8GB,"Game, stream, create. The GeForce RTX™ 4060 Ti lets you take on the latest games and apps with the ultra-efficient NVIDIA Ada Lovelace architecture. Experience immersive, AI-accelerated gaming with ray tracing and DLSS 3, and supercharge your creative process and productivity with NVIDIA Studio + +CUDA Cores: 4352 +Boost Clock: 2.54 GHz +Memory Configuration: 8GB GDDR6",2024-10-20T15:00:00,5,1495.0,Shipped,"",,,,, +6301,95899,NVIDIA® GEFORCE RTX™ 4090,"The NVIDIA® GeForce RTX® 4090 is the ultimate GeForce GPU. It brings an enormous leap in performance, efficiency, and AI-powered graphics. Experience ultra-high performance gaming, incredibly detailed virtual worlds, unprecedented productivity, and new ways to create. It’s powered by the NVIDIA Ada Lovelace architecture and comes with 24 GB of G6X memory to deliver the ultimate experience for gamers and creators. + +CUDA Cores: 16384 +Boost Clock: 2.52 GHz +Memory Configuration: 24GB GDDR6X + +Please refer to the graphics card comparison page for a full list of specifications: https://www.nvidia.com/en-us/geforce/graphics-cards/compare/",2024-10-08T16:45:00,1,1599.0,In Transit,"",,,,, +6301,8395,NVIDIA FLOW LARGE MOUSEPAD,"Dimensions: 27.20"" x 11.75"" +2.22 sq. ft. of full-color space +Anti-slip base keeps the mousepad from sliding around +Smooth surface for maximized speed, accuracy, and control +Crafted so edges remain flat +For use on any flat, hard surface",2024-10-04T08:10:00,5,100.0,Processing,"",,,,,Order is processing longer than expected. +6301,64566,MEN'S BEYOND YOGA CREW T-SHIRT - DARKEST NIGHT,"Beyond Yoga Featherweight Always Beyond Crew T-shirt is a classic top you can wear anytime, anywhere. Darkest Night + +Product Details: + +Lightweight, ultrasoft +4-way stretch fabric +Crew neckline +Classic fit +94% Polyester 6% Elastane",2024-10-19T12:15:00,1,66.0,Delivered,"",,,,, +6301,55675,OGIO® STRATAGEM BACKPACK,"Materials: 600D Oxford polyester, 420D dobby polyester +Padded fleece-lined interior laptop compartment fits most 17"" laptops +Integrated foam panels keep your electronics and other valuables protected +Padded tablet/e-reader sleeve +Large main compartment for books, binders and files +Padded back panel with moisture-wicking air mesh +Dual side water bottle/accessory holders +Easy access front stash pocket",2024-10-11T19:45:00,2,200.0,Returned,Rejected,2024-10-16T19:45:00,2024-10-17T19:45:00,,The bag arrived with a manufacturing defect.,Returns not accepted for items with visible defects. diff --git a/deploy/compose/README.md b/deploy/compose/README.md new file mode 100644 index 0000000..66ea5e3 --- /dev/null +++ b/deploy/compose/README.md @@ -0,0 +1,221 @@ +# Docker deployment guide +- [Docker deployment guide](#docker-deployment-guide) + - [1. Prerequisites](#1-prerequisites) + - [Software requirements](#software-requirements) + - [Get an NVIDIA API Catalog key](#get-an-nvidia-api-catalog-key) + - [Get an NVIDIA NGC API Key](#get-an-nvidia-ngc-api-key) + - [2. Start the required microservices](#2-start-the-required-microservices) + - [Deploy with cloud hosted models](#deploy-with-cloud-hosted-models) + - [Deploy with on-prem models](#deploy-with-on-prem-models) + - [3. Data Ingestion](#3-data-ingestion) + - [4. Testing](#4-testing) + - [Using sample frontend](#using-sample-frontend) + - [Using standalone API's](#using-standalone-apis) + - [5. API References](#5-api-references) + - [6. Stopping Services](#6-stopping-services) + - [7. Customizing GPUs for on-prem NIM Deployment](#7-customizing-gpus-for-on-prem-nim-deployment) + +### 1. Prerequisites + +#### Software requirements +- Install Docker Engine and Docker Compose. + Refer to the instructions for [Ubuntu](https://docs.docker.com/engine/install/ubuntu/). + + Ensure the Docker Compose plugin version is 2.29.1 or higher. + Run `docker compose version` to confirm. + Refer to [Install the Compose plugin](https://docs.docker.com/compose/install/linux/) + in the Docker documentation for more information. + +- Optional: You can run some containers with GPU acceleration, such as Milvus and NVIDIA NIM for LLMs. + To configure Docker for GPU-accelerated containers, [install](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) the NVIDIA Container Toolkit. + + +#### Get an NVIDIA API Catalog key + +This step can be skipped if you are interested in [deploying the models on-prem.](#deploy-with-on-prem-models) +This key will be used to access cloud hosted models in [API Catalog](https://build.nvidia.com/explore/discover). +You can use different model API endpoints with the same API key. + +1. Navigate to . + +2. Find the **Llama 3.1 70B Instruct** card and click the card. + ![Llama 3.1 70B Instruct model card](../../docs/imgs/llama3.1-70b-instruct-model-card.png) + +3. Click **Get API Key**. + ![API section of the model page.](../../docs/imgs/llama3.1-70b-instruct-get-api-key.png) + +4. Click **Generate Key**. + + ![Generate key window.](../../docs/imgs/api-catalog-generate-api-key.png) + +5. Click **Copy Key** and then save the API key. + The key begins with the letters nvapi-. + + ![Key Generated window.](../../docs/imgs/key-generated.png) + +6. Export NVIDIA_API_KEY + ``` + export NVIDIA_API_KEY="give your NVIDIA api key" + ``` + + +#### Get an NVIDIA NGC API Key + +The NVIDIA NGC API Key is a mandatory key that is required to use this blueprint. This is needed to log into the NVIDIA container registry, `nvcr.io`, and to pull secure container images used in this NVIDIA NIM Blueprint. + +Refer to [Generating NGC API Keys](https://docs.nvidia.com/ngc/gpu-cloud/ngc-user-guide/index.html#generating-api-key) +in the _NVIDIA NGC User Guide_ for more information. + +After you get your NGC API key, run `docker login nvcr.io` and provide the credentials. + + +### 2. Start the required microservices + +#### Deploy with cloud hosted models + + + `docker compose -f deploy/compose/docker-compose.yaml up -d` + + +On deployment following containers should be up and running + + ``` + $ docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}" + CONTAINER ID NAMES STATUS + b6a1853c4e81 agent-chain-server Up 3 hours + 91487a937be1 analytics-server Up 3 hours + 0112183489fe unstructured-retriever Up 3 hours + 9970bb569dbd structured-retriever Up 3 hours + 4ea1a3267a17 milvus-standalone Up 3 hours + c988dcdd67c3 postgres_container Up 3 hours (healthy) + 3dc1c2262903 milvus-minio Up 3 hours (healthy) + eee52b7302fb milvus-etcd Up 3 hours (healthy) + 907f5702f82b compose-redis-1 Up 3 hours + fcde431d44de pgadmin_container Up 3 hours + f2ce39cf3027 compose-redis-commander-1 Up 3 hours (healthy) + ``` +#### Deploy with on-prem models + + ``` + # Create model directory to download model from NGC + mkdir -p ~/.cache/models + export MODEL_DIRECTORY=~/.cache/models/ + + # export your ngc api key, note it's not nvidia_api_key from build.nvidia.com + export NGC_API_KEY= + export USERID="$(id -u):$(id -g)" + + # Export path where NIMs are hosted + # LLM server path + export APP_LLM_SERVERURL=nemollm-inference:8000 + # Embedding server path + export APP_EMBEDDINGS_SERVERURL=nemollm-embedding:8000 + # Re-ranking model path + export APP_RANKING_SERVERURL=ranking-ms:8000 + + docker compose -f deploy/compose/docker-compose.yaml --profile local-nim up -d + ``` + + +On deployment following containers should be up and running + + ``` + $ docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}" + CONTAINER ID NAMES STATUS + 1dd42caad60e agent-chain-server Up 55 minutes + 4c4d1136cd7a structured-retriever Up 3 hours + ff2f71eb9d75 unstructured-retriever Up 3 hours + fd70635efcac analytics-server Up 3 hours + 8fc99cf27945 nemo-retriever-ranking-microservice Up 3 hours (healthy) + d3853cc6b622 nemo-retriever-embedding-microservice Up 3 hours (healthy) + dcc22f20df1f nemollm-inference-microservice Up 3 hours (healthy) + b4cfafffa57b milvus-standalone Up 3 hours + dfdaa5ff59be postgres_container Up 3 hours (healthy) + 8787645d8b4f milvus-minio Up 3 hours (healthy) + caa2e19b030b pgadmin_container Up 3 hours + 77b4fb45d600 milvus-etcd Up 3 hours (healthy) + 5be79d19281e compose-redis-1 Up 3 hours + 6a5353baa2d1 compose-redis-commander-1 Up 3 hours (healthy) + ``` + +Note: + + - By default, GPU IDs 0-3 are for LLM, 4 for the embedding model, and 5 for the reranking model; see [Customizing GPU](#customizing-gpus-for-nim-deployment) for changes. + + - The above command pulls in the prebuilt containers from NGC and deploys it locally. If you have made changes locally or you are interested in building the container from source code, append `--build` argument to the above commands. For example to deploy the pipeline locally using cloud hosted models and containers build from source use: + + `docker compose -f deploy/compose/docker-compose.yaml up -d --build` + +### 3. Data Ingestion + +Download the manuals into `data/manuals_pdf` folder +``` +# Run this script to download the manuals listed in the specified txt file +./data/download.sh ./data/list_manuals.txt +``` + +Install jupyterlab +``` +pip install jupyterlab +``` + +Run the jupyterlab server +``` +# Use this command to run Jupyter Lab so that you can execute this IPython notebook. +jupyter lab --allow-root --ip=0.0.0.0 --NotebookApp.token='' --port=8889 +``` + +Follow the cells in the notebook ingest_data.ipynb to do the following + +1. Unstructured Data (PDF) Ingestion to Milvus DB +2. Structured Data (CSV) Ingestion to Postgres DB + +### 4. Testing + +#### Using sample frontend +A sample frontend is available to showcase key functionalities of this blueprint. +You can deploy it using the following docker run command: +``` + docker run -d -it -e INFERENCE_ORIGIN="http://:9000" --name agent-frontend --rm -p 3001:3001 nvcr.io/nvidia/blueprint/aiva-customer-service-ui:1.0.0 + ``` + +- After deploying it visit `http://:3001/` to access the frontend and try out queries. +You can choose any of the preset customer names and ask queries relevant to order history displayed or relevant to [any ingested unstructured data.](../../data/) + +- Click `End Chat Session` button once you have finished your conversation. +- The sample UI showcases limited number of functionalities and interacts with a [single API endpoint exposed by the API Gateway container.](../../docs/api_references/api_gateway_server.json) + +#### Using standalone API's +You can also try out the pipeline using REST based API's exposed to better understand the underlying flow. + +1. Create a new session using the `create_session` API exposed by the Langgraph based agent at `http://:8081/docs#/default/create_session_create_session_get`. Note down the session id returned for this request. + +2. Carry out conversation with the agent using `generate` API at `http://:8081/docs#/Inference/generate_answer_generate_post`. The `session_id` must match the one returned by `create_session` API call and `user_id` should match one of the `CID` mentioned in the [sample structured purchase history dataset which was ingested.](../../data/orders.csv) + +3. After testing queries, end the session at `http://:8081/docs#/default/end_session_end_session_get` + +4. Explore the analytics server APIs at `http://:8082/docs#/` + This server offers different APIs for fetching session specific info like conversation history, summary and sentiment as well as for submitting feedback related to a session. Please note these API's will be functional only after `end_session` is invoked. + + +### 5. API References +For detailed API references of all the key deployed microservices, please refer to the following: + +- [Analytics Microservice](../../docs/api_references/analytics_server.json) +- [Agent Microservice](../../docs/api_references/agent_server.json) +- [Retriever Microservices](../../docs/api_references/retriever_server.json) + + +### 6. Stopping Services +``` +docker compose -f deploy/compose/docker-compose.yaml down +``` + +### 7. Customizing GPUs for on-prem NIM Deployment +To change the GPUs used for NIM deployment, set the following environment variables before triggering the docker compose: + +`LLM_MS_GPU_ID`: Update this to specify the LLM GPU IDs (e.g., 0,1,2,3). +`EMBEDDING_MS_GPU_ID`: Change this to set the embedding GPU ID. +`RANKING_MS_GPU_ID`: Modify this to adjust the reranking LLM GPU ID. +`RANKING_MS_GPU_ID`: Modify this to adjust the reranking LLM GPU ID. +`VECTORSTORE_GPU_DEVICE_ID` : Modify to adjust the Milvus vector database GPU ID. \ No newline at end of file diff --git a/deploy/compose/docker-compose.yaml b/deploy/compose/docker-compose.yaml new file mode 100644 index 0000000..ea0a79f --- /dev/null +++ b/deploy/compose/docker-compose.yaml @@ -0,0 +1,350 @@ +include: + - path: + - nims.yaml + +services: + # ======================= + # Agent Services + # ======================= + agent-chain-server: + container_name: agent-chain-server + image: aiva-customer-service-agent:1.0.0 + build: + # Set context to repo's root directory + context: ../../ + dockerfile: src/agent/Dockerfile + command: --port 8081 --host 0.0.0.0 --workers 1 --loop asyncio + environment: + EXAMPLE_PATH: './src/agent' + APP_LLM_MODELNAME: ${APP_LLM_MODELNAME:-"meta/llama-3.1-70b-instruct"} + APP_LLM_MODELENGINE: nvidia-ai-endpoints + APP_LLM_SERVERURL: ${APP_LLM_SERVERURL:-""} + # Cache name to store user conversation + # supported type inmemory, redis + APP_CACHE_NAME: ${APP_CACHE_NAME:-"redis"} + APP_CACHE_URL: ${APP_CACHE_URL:-"redis:6379"} + # Database name to store user conversation + # supported type postgres + APP_DATABASE_NAME: ${APP_DATABASE_NAME:-"postgres"} + APP_DATABASE_URL: ${APP_DATABASE_URL:-"postgres:5432"} + # Checkpointer name to store intermediate state of conversation + # supported type postgres, inmemory + APP_CHECKPOINTER_NAME: ${APP_CHECKPOINTER_NAME:-"postgres"} + APP_CHECKPOINTER_URL: ${APP_CHECKPOINTER_URL:-"postgres:5432"} + # Postgres config + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + CANONICAL_RAG_URL: http://unstructured-retriever:8081 + STRUCTURED_RAG_URI: http://structured-retriever:8081 + NVIDIA_API_KEY: ${NVIDIA_API_KEY} + GRAPH_RECURSION_LIMIT: 20 + RETURN_WINDOW_CURRENT_DATE: '2024-10-23' # Leave it empty to get the current date + RETURN_WINDOW_THRESHOLD_DAYS: 30 + # Log level for server, supported level NOTSET, DEBUG, INFO, WARN, ERROR, CRITICAL + LOGLEVEL: ${LOGLEVEL:-INFO} + # Set the default expiration time (TTL) for Redis keys (in seconds) + REDIS_SESSION_EXPIRY: 12 # in hours + ports: + - "8081:8081" + expose: + - "8081" + shm_size: 5gb + depends_on: + - unstructured-retriever + - structured-retriever + - postgres + - redis + + + # ======================= + # UI and related services for Agent Interface + # ======================= + api-gateway-server: + build: + context: ../../ + dockerfile: ./src/api_gateway/Dockerfile + image: aiva-customer-service-api-gateway:1.0.0 + command: --port 9000 --host 0.0.0.0 --workers 1 + ports: + - "9000:9000" + environment: + AGENT_SERVER_URL: ${AGENT_SERVER_URL:-http://agent-chain-server:8081} + ANALYTICS_SERVER_URL: ${ANALYTICS_SERVER_URL:-http://analytics-server:8081} + REQUEST_TIMEOUT: 180 + restart: unless-stopped # Optional: Automatically restart the container unless it is stopped + depends_on: + - agent-chain-server + + + # ======================= + # Analytics Services - summary/sentiment and similar APIs are exposed as part of analytics MS + # ======================= + analytics-server: + container_name: analytics-server + image: aiva-customer-service-analytics:1.0.0 + build: + # Set context to repo's root directory + context: ../../ + dockerfile: src/analytics/Dockerfile + command: --port 8081 --host 0.0.0.0 --workers 1 + environment: + EXAMPLE_PATH: './src/analytics' + APP_LLM_MODELNAME: ${APP_LLM_MODELNAME:-"meta/llama-3.1-70b-instruct"} + APP_LLM_MODELENGINE: nvidia-ai-endpoints + APP_LLM_SERVERURL: ${APP_LLM_SERVERURL:-""} + # Database name to store user conversation/summary + # supported type inmemory, redis + APP_DATABASE_NAME: ${APP_DATABASE_NAME:-"postgres"} + APP_DATABASE_URL: ${APP_DATABASE_URL:-"postgres:5432"} + # Postgres config + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + # Postgres database name for customer data + CUSTOMER_DATA_DB: ${CUSTOMER_DATA_DB:-customer_data} + # Store summary/sentiment in database + PERSIST_DATA: ${PERSIST_DATA:-true} + NVIDIA_API_KEY: ${NVIDIA_API_KEY} + # Log level for server, supported level NOTSET, DEBUG, INFO, WARN, ERROR, CRITICAL + LOGLEVEL: ${LOGLEVEL:-INFO} + ports: + - "8082:8081" + expose: + - "8081" + shm_size: 5gb + depends_on: + - postgres + + + # ======================= + # Retriever Microservices + # ======================= + # Fetch relevant document from vectorstore + unstructured-retriever: + container_name: unstructured-retriever + image: aiva-customer-service-unstructured-retriever:1.0.0 + build: + # Set context to repo's root directory + context: ../../ + dockerfile: src/retrievers/Dockerfile + args: + # Build args, used to copy relevant directory inside the container + EXAMPLE_PATH: 'src/retrievers/unstructured_data' + # start the server on port 8081 + command: --port 8081 --host 0.0.0.0 --workers 1 + environment: + # Path to example directory relative to GenerativeAIExamples/RAG/examples + EXAMPLE_PATH: 'src/retrievers/unstructured_data' + # URL on which vectorstore is hosted + APP_VECTORSTORE_URL: "http://milvus:19530" + # Type of vectordb used to store embedding supported type milvus, pgvector + APP_VECTORSTORE_NAME: "milvus" + # url on which llm model is hosted. If "", Nvidia hosted API is used + APP_LLM_MODELNAME: ${APP_LLM_MODELNAME:-"meta/llama-3.1-70b-instruct"} + # embedding model engine used for inference, supported type nvidia-ai-endpoints, huggingface + APP_LLM_MODELENGINE: nvidia-ai-endpoints + # url on which llm model is hosted. If "", Nvidia hosted API is used + APP_LLM_SERVERURL: ${APP_LLM_SERVERURL:-""} + APP_EMBEDDINGS_MODELNAME: ${APP_EMBEDDINGS_MODELNAME:-nvidia/nv-embedqa-e5-v5} + # embedding model engine used for inference, supported type nvidia-ai-endpoints + APP_EMBEDDINGS_MODELENGINE: ${APP_EMBEDDINGS_MODELENGINE:-nvidia-ai-endpoints} + # url on which embedding model is hosted. If "", Nvidia hosted API is used + APP_EMBEDDINGS_SERVERURL: ${APP_EMBEDDINGS_SERVERURL:-""} + APP_RANKING_MODELNAME: ${APP_RANKING_MODELNAME:-"nvidia/nv-rerankqa-mistral-4b-v3"} # Leave it blank to avoid using ranking + # ranking engine used for inference, supported type nvidia-ai-endpoints + APP_RANKING_MODELENGINE: ${APP_RANKING_MODELENGINE:-nvidia-ai-endpoints} + # url on which re-ranking model is hosted. If "", Nvidia hosted API is used + APP_RANKING_SERVERURL: ${APP_RANKING_SERVERURL:-""} + # text splitter model name, it's fetched from huggingface + APP_TEXTSPLITTER_MODELNAME: Snowflake/snowflake-arctic-embed-l + APP_TEXTSPLITTER_CHUNKSIZE: 506 + APP_TEXTSPLITTER_CHUNKOVERLAP: 200 + NVIDIA_API_KEY: ${NVIDIA_API_KEY} + # vectorstore collection name to store embeddings + COLLECTION_NAME: ${COLLECTION_NAME:-unstructured_data} + APP_RETRIEVER_TOPK: 4 + APP_RETRIEVER_SCORETHRESHOLD: 0.25 + # Number of documents to be retrieved from retriever when reranking model is enabled + # This will be then send to re ranker to get `APP_RETRIEVER_TOPK` documents + VECTOR_DB_TOPK: 20 + # Log level for server, supported level NOTSET, DEBUG, INFO, WARN, ERROR, CRITICAL + LOGLEVEL: ${LOGLEVEL:-INFO} + ports: + - "8086:8081" + expose: + - "8081" + shm_size: 5gb + depends_on: + - milvus + + + # Fetch user information form database + structured-retriever: + container_name: structured-retriever + image: aiva-customer-service-structured-retriever:1.0.0 + build: + context: ../../ + dockerfile: src/retrievers/Dockerfile + args: + EXAMPLE_PATH: 'src/retrievers/structured_data' + command: --port 8081 --host 0.0.0.0 --workers 1 + environment: + EXAMPLE_PATH: 'src/retrievers/structured_data' + APP_LLM_MODELNAME: ${APP_LLM_MODELNAME:-meta/llama-3.1-70b-instruct} + APP_LLM_MODELENGINE: nvidia-ai-endpoints + APP_LLM_SERVERURL: ${APP_LLM_SERVERURL:-""} + APP_LLM_MODELNAMEPANDASAI: ${APP_LLM_MODELNAME:-meta/llama-3.1-70b-instruct} + APP_PROMPTS_CHATTEMPLATE: "You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Please ensure that your responses are positive in nature." + APP_PROMPTS_RAGTEMPLATE: "You are a helpful AI assistant named Envie. You will reply to questions only based on the context that you are provided. If something is out of context, you will refrain from replying and politely decline to respond to the user." + NVIDIA_API_KEY: ${NVIDIA_API_KEY} + COLLECTION_NAME: ${COLLECTION_NAME:-structured_data} + # Database name to store user purchase history, only postgres is supported + APP_DATABASE_NAME: ${APP_DATABASE_NAME:-"postgres"} + APP_DATABASE_URL: ${APP_DATABASE_URL:-"postgres:5432"} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_DB:-customer_data} + CSV_NAME: PdM_machines + LOGLEVEL: ${LOGLEVEL:-INFO} + ports: + - "8087:8081" + expose: + - "8081" + shm_size: 5gb + depends_on: + postgres: + condition: service_healthy + required: false + + + # ======================= + # Database Services - User purchase history and permanently store conversation details + # ======================= + postgres: + container_name: postgres_container + image: postgres:17.0 + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_DB:-customer_data} + command: + - "postgres" + - "-c" + - "shared_buffers=256MB" + - "-c" + - "max_connections=200" + volumes: + - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "sh -c 'pg_isready -U postgres -d postgres'"] + interval: 10s + timeout: 3s + retries: 3 + restart: unless-stopped + + # For visualization purpose + pgadmin: + container_name: pgadmin_container + image: dpage/pgadmin4:8.12.0 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: 'False' + user: '$UID:$GID' + volumes: + - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/pgadmin:/var/lib/pgadmin + ports: + - "${PGADMIN_PORT:-5050}:80" + restart: unless-stopped + + + # ======================= + # Cache Services - To store conversation of user to share among multiple workers + # ======================= + redis: + image: redis:7.0.13 + restart: always + ports: + - "6379:6379" + volumes: + - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/redis-data:/data + + redis-commander: + # Visualization tool for redis + image: rediscommander/redis-commander:latest + restart: always + ports: + - "9092:8081" + environment: + - REDIS_HOSTS=local:redis:6379 + + + # ======================= + # Vector Store Services + # ======================= + etcd: + container_name: milvus-etcd + image: quay.io/coreos/etcd:v3.5.5 + environment: + - ETCD_AUTO_COMPACTION_MODE=revision + - ETCD_AUTO_COMPACTION_RETENTION=1000 + - ETCD_QUOTA_BACKEND_BYTES=4294967296 + - ETCD_SNAPSHOT_COUNT=50000 + volumes: + - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd + command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd + healthcheck: + test: ["CMD", "etcdctl", "endpoint", "health"] + interval: 30s + timeout: 20s + retries: 3 + + minio: + container_name: milvus-minio + image: minio/minio:RELEASE.2024-05-01T01-11-10Z + environment: + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin + ports: + - "9011:9011" + - "9010:9010" + volumes: + - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data + command: minio server /minio_data --console-address ":9011" --address ":9010" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9010/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + + milvus: + container_name: milvus-standalone + image: milvusdb/milvus:v2.4.4-gpu + command: ["milvus", "run", "standalone"] + environment: + ETCD_ENDPOINTS: etcd:2379 + MINIO_ADDRESS: minio:9010 + KNOWHERE_GPU_MEM_POOL_SIZE: 2048;4096 + volumes: + - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus + ports: + - "19530:19530" + - "9091:9091" + depends_on: + - "etcd" + - "minio" + deploy: + resources: + reservations: + devices: + - driver: nvidia + capabilities: ["gpu"] + device_ids: ['${VECTORSTORE_GPU_DEVICE_ID:-0}'] + + +networks: + default: + name: nvidia-rag diff --git a/deploy/compose/nims.yaml b/deploy/compose/nims.yaml new file mode 100644 index 0000000..212a928 --- /dev/null +++ b/deploy/compose/nims.yaml @@ -0,0 +1,87 @@ +services: + nemollm-inference: + container_name: nemollm-inference-microservice + image: nvcr.io/nim/meta/llama-3.1-70b-instruct:1.1 + volumes: + # Use current path for model directory if nothing is specified + - ${MODEL_DIRECTORY:-.}:/opt/nim/.cache + user: "${USERID:-1000:1000}" + ports: + - "8000:8000" + expose: + - "8000" + environment: + NGC_API_KEY: ${NGC_API_KEY} + shm_size: 20gb + deploy: + resources: + reservations: + devices: + - driver: nvidia + # count: ${INFERENCE_GPU_COUNT:-all} + device_ids: ['${LLM_MS_GPU_ID:-0,1,2,3}'] + capabilities: [gpu] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/v1/health/ready"] + interval: 10s + timeout: 20s + retries: 100 + profiles: ["local-nim"] + + nemollm-embedding: + container_name: nemo-retriever-embedding-microservice + image: nvcr.io/nim/nvidia/nv-embedqa-e5-v5:1.0.0 + volumes: + - ${MODEL_DIRECTORY:-.}:/opt/nim/.cache + ports: + - "9080:8000" + expose: + - "8000" + environment: + NGC_API_KEY: ${NGC_API_KEY} + user: "${USERID:-1000:1000}" + shm_size: 16GB + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['${EMBEDDING_MS_GPU_ID:-4}'] + capabilities: [gpu] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/v1/health/ready"] + interval: 30s + timeout: 20s + retries: 3 + start_period: 10m + profiles: ["local-nim"] + + ranking-ms: + container_name: nemo-retriever-ranking-microservice + image: nvcr.io/nim/nvidia/nv-rerankqa-mistral-4b-v3:1.0.0 + volumes: + - ${MODEL_DIRECTORY:-.}:/opt/nim/.cache + ports: + - "1976:8000" + expose: + - "8000" + environment: + NGC_API_KEY: ${NGC_API_KEY} + user: "${USERID:-1000:1000}" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 10s + timeout: 20s + retries: 100 + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['${RANKING_MS_GPU_ID:-5}'] + capabilities: [gpu] + profiles: ["local-nim"] + +networks: + default: + name: nvidia-rag diff --git a/docs/api_references/agent_server.json b/docs/api_references/agent_server.json new file mode 100644 index 0000000..aafe085 --- /dev/null +++ b/docs/api_references/agent_server.json @@ -0,0 +1,597 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Agent API's for AI Virtual Assistant for Customer Service", + "description": "This API schema describes all the core agentic endpoints exposed by the AI Virtual Assistant for Customer Service NIM Blueprint", + "version": "1.0.0" + }, + "paths": { + "/health": { + "get": { + "tags": [ + "Health" + ], + "summary": "Health Check", + "description": "Perform a Health Check\n\nReturns 200 when service is up. This does not check the health of downstream services.", + "operationId": "health_check_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/metrics": { + "get": { + "tags": [ + "Health" + ], + "summary": "Get Metrics", + "operationId": "get_metrics_metrics_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/create_session": { + "get": { + "tags": [ + "Session Management" + ], + "summary": "Create Session", + "operationId": "create_session_create_session_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSessionResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/end_session": { + "get": { + "tags": [ + "Session Management" + ], + "summary": "End Session", + "operationId": "end_session_end_session_get", + "parameters": [ + { + "name": "session_id", + "in": "query", + "required": true, + "schema": { + "title": "Session Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EndSessionResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/delete_session": { + "delete": { + "tags": [ + "Session Management" + ], + "summary": "Delete Session", + "operationId": "delete_session_delete_session_delete", + "parameters": [ + { + "name": "session_id", + "in": "query", + "required": true, + "schema": { + "title": "Session Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteSessionResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/generate": { + "post": { + "tags": [ + "Inference" + ], + "summary": "Generate Answer", + "description": "Generate and stream the response to the provided prompt.", + "operationId": "generate_answer_generate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Prompt" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChainResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/feedback/response": { + "post": { + "tags": [ + "Feedback" + ], + "summary": "Store Last Response Feedback", + "description": "Store user feedback for the last response in a conversation session.", + "operationId": "store_last_response_feedback_feedback_response_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackResponse" + } + } + } + }, + "404": { + "description": "Session Not Found", + "content": { + "application/json": { + "example": { + "detail": "Session not found" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ChainResponse": { + "properties": { + "id": { + "type": "string", + "maxLength": 100000, + "pattern": "[\\s\\S]*", + "title": "Id", + "default": "" + }, + "choices": { + "items": { + "$ref": "#/components/schemas/ChainResponseChoices" + }, + "type": "array", + "maxItems": 256, + "title": "Choices", + "default": [] + }, + "session_id": { + "type": "string", + "title": "Session Id", + "description": "A unique identifier representing the session associated with the response." + } + }, + "type": "object", + "title": "ChainResponse", + "description": "Definition of Chain APIs resopnse data type" + }, + "ChainResponseChoices": { + "properties": { + "index": { + "type": "integer", + "maximum": 256.0, + "minimum": 0.0, + "format": "int64", + "title": "Index", + "default": 0 + }, + "message": { + "$ref": "#/components/schemas/Message", + "default": { + "role": "assistant", + "content": "" + } + }, + "finish_reason": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Finish Reason", + "default": "" + } + }, + "type": "object", + "title": "ChainResponseChoices", + "description": "Definition of Chain response choices" + }, + "CreateSessionResponse": { + "properties": { + "session_id": { + "type": "string", + "maxLength": 4096, + "title": "Session Id" + } + }, + "type": "object", + "required": [ + "session_id" + ], + "title": "CreateSessionResponse" + }, + "DeleteSessionResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "DeleteSessionResponse" + }, + "EndSessionResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "EndSessionResponse" + }, + "FeedbackRequest": { + "properties": { + "feedback": { + "type": "number", + "maximum": 1.0, + "minimum": -1.0, + "title": "Feedback", + "description": "A unique identifier representing your end-user." + }, + "session_id": { + "type": "string", + "title": "Session Id", + "description": "A unique identifier representing the session associated with the response." + } + }, + "type": "object", + "required": [ + "feedback", + "session_id" + ], + "title": "FeedbackRequest", + "description": "Definition of the Feedback Request data type." + }, + "FeedbackResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "FeedbackResponse", + "description": "Definition of the Feedback Request data type." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HealthResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "HealthResponse" + }, + "Message": { + "properties": { + "role": { + "type": "string", + "maxLength": 256, + "pattern": "[\\s\\S]*", + "title": "Role", + "description": "Role for a message AI, User and System", + "default": "user" + }, + "content": { + "type": "string", + "maxLength": 131072, + "pattern": "[\\s\\S]*", + "title": "Content", + "description": "The input query/prompt to the pipeline.", + "default": "I am going to Paris, what should I see?" + } + }, + "type": "object", + "title": "Message", + "description": "Definition of the Chat Message type." + }, + "Prompt": { + "properties": { + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array", + "maxItems": 50000, + "title": "Messages", + "description": "A list of messages comprising the conversation so far. The roles of the messages must be alternating between user and assistant. The last input message should have role user. A message with the the system role is optional, and must be the very first message if it is present." + }, + "max_tokens": { + "type": "integer", + "maximum": 1024.0, + "minimum": 0.0, + "format": "int64", + "title": "Max Tokens", + "description": "The maximum number of tokens to generate in any given call. Note that the model is not aware of this value, and generation will simply stop at the number of tokens specified.", + "default": 1024 + }, + "stop": { + "items": { + "type": "string", + "maxLength": 256, + "pattern": "[\\s\\S]*" + }, + "type": "array", + "maxItems": 256, + "title": "Stop", + "description": "A string or a list of strings where the API will stop generating further tokens. The returned text will not contain the stop sequence.", + "default": [] + }, + "user_id": { + "type": "string", + "title": "User Id", + "description": "A unique identifier representing your end-user." + }, + "session_id": { + "type": "string", + "title": "Session Id", + "description": "A unique identifier representing the session associated with the response." + } + }, + "type": "object", + "required": [ + "messages", + "session_id" + ], + "title": "Prompt", + "description": "Definition of the Prompt API data type." + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + }, + "tags": [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness." + }, + { + "name": "Feedback", + "description": "APIs for storing useful information for data flywheel." + }, + { + "name": "Session Management", + "description": "APIs for managing sessions." + }, + { + "name": "Inference", + "description": "Core APIs for interacting with the agent." + } + ] + } \ No newline at end of file diff --git a/docs/api_references/analytics_server.json b/docs/api_references/analytics_server.json new file mode 100644 index 0000000..1bc125c --- /dev/null +++ b/docs/api_references/analytics_server.json @@ -0,0 +1,927 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Analytics API's for AI Virtual Assistant for Customer Service", + "description": "This API schema describes all the analytics endpoints exposed for the AI Virtual Assistant for Customer Service NIM Blueprint", + "version": "1.0.0" + }, + "paths": { + "/health": { + "get": { + "tags": [ + "Health" + ], + "summary": "Health Check", + "description": "Perform a Health Check\n\nReturns 200 when service is up. This does not check the health of downstream services.", + "operationId": "health_check_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/sessions": { + "get": { + "tags": [ + "Session" + ], + "summary": "Get Sessions", + "description": "Retrieve session information in last k hours", + "operationId": "get_sessions_sessions_get", + "parameters": [ + { + "name": "hours", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "description": "Last K hours, for which sessions info is extracted", + "title": "Hours" + }, + "description": "Last K hours, for which sessions info is extracted" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionsResponse" + }, + "title": "Response Get Sessions Sessions Get" + } + } + } + }, + "404": { + "description": "No Sessions Found", + "content": { + "application/json": { + "example": { + "detail": "No sessions found for the specified time range" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/session/summary": { + "get": { + "tags": [ + "Session" + ], + "summary": "Generate Session Summary", + "description": "Generate a summary and sentiment analysis for the specified session.", + "operationId": "generate_session_summary_session_summary_get", + "parameters": [ + { + "name": "session_id", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the session", + "title": "Session Id" + }, + "description": "The ID of the session" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionSummaryResponse" + } + } + } + }, + "404": { + "description": "Session Not Found", + "content": { + "application/json": { + "example": { + "detail": "Session not found" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/session/conversation": { + "get": { + "tags": [ + "Session" + ], + "summary": "Get Session Conversation", + "description": "Retrieve the conversation and sentiment for the specified session.", + "operationId": "get_session_conversation_session_conversation_get", + "parameters": [ + { + "name": "session_id", + "in": "query", + "required": true, + "schema": { + "type": "string", + "description": "The ID of the session", + "title": "Session Id" + }, + "description": "The ID of the session" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionConversationResponse" + } + } + } + }, + "404": { + "description": "Session Not Found", + "content": { + "application/json": { + "example": { + "detail": "Session not found" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/get_user_purchase_history": { + "post": { + "tags": [ + "User Data" + ], + "summary": "Get User Purchase History", + "description": "Get purchase history for user", + "operationId": "get_user_purchase_history_get_user_purchase_history_post", + "parameters": [ + { + "name": "user_id", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "User Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PurchaseInfo" + }, + "title": "Response Get User Purchase History Get User Purchase History Post" + } + } + } + }, + "404": { + "description": "Session Not Found", + "content": { + "application/json": { + "example": { + "detail": "Session not found" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/feedback/sentiment": { + "post": { + "tags": [ + "Feedback" + ], + "summary": "Store Sentiment Feedback", + "description": "Store user feedback for the sentiment analysis of a conversation session.", + "operationId": "store_sentiment_feedback_feedback_sentiment_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackResponse" + } + } + } + }, + "404": { + "description": "Session Not Found", + "content": { + "application/json": { + "example": { + "detail": "Session not found" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/feedback/summary": { + "post": { + "tags": [ + "Feedback" + ], + "summary": "Store Summary Feedback", + "description": "Store user feedback for the summary of a conversation session.", + "operationId": "store_summary_feedback_feedback_summary_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackResponse" + } + } + } + }, + "404": { + "description": "Session Not Found", + "content": { + "application/json": { + "example": { + "detail": "Session not found" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/feedback/session": { + "post": { + "tags": [ + "Feedback" + ], + "summary": "Store Conversation Session Feedback", + "description": "Store user feedback for the overall conversation session.", + "operationId": "store_conversation_session_feedback_feedback_session_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FeedbackResponse" + } + } + } + }, + "404": { + "description": "Session Not Found", + "content": { + "application/json": { + "example": { + "detail": "Session not found" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "FeedbackRequest": { + "properties": { + "feedback": { + "type": "number", + "maximum": 1.0, + "minimum": -1.0, + "title": "Feedback", + "description": "A unique identifier representing your end-user." + }, + "session_id": { + "type": "string", + "title": "Session Id", + "description": "A unique identifier representing the session associated with the response." + } + }, + "type": "object", + "required": [ + "feedback", + "session_id" + ], + "title": "FeedbackRequest", + "description": "Definition of the Feedback Request data type." + }, + "FeedbackResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "FeedbackResponse", + "description": "Definition of the Feedback Request data type." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HealthResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "HealthResponse" + }, + "PurchaseInfo": { + "properties": { + "customer_id": { + "type": "string", + "title": "Customer Id" + }, + "order_id": { + "type": "string", + "title": "Order Id" + }, + "product_name": { + "type": "string", + "title": "Product Name" + }, + "order_date": { + "type": "string", + "title": "Order Date" + }, + "quantity": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Quantity" + }, + "order_amount": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Order Amount" + }, + "order_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Order Status" + }, + "return_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Return Status" + }, + "return_start_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Return Start Date" + }, + "return_received_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Return Received Date" + }, + "return_completed_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Return Completed Date" + }, + "return_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Return Reason" + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Notes" + } + }, + "type": "object", + "required": [ + "customer_id", + "order_id", + "product_name", + "order_date", + "quantity", + "order_amount", + "order_status", + "return_status", + "return_start_date", + "return_received_date", + "return_completed_date", + "return_reason", + "notes" + ], + "title": "PurchaseInfo" + }, + "SessionConversationMessage": { + "properties": { + "role": { + "type": "string", + "maxLength": 256, + "pattern": "[\\s\\S]*", + "title": "Role", + "description": "Role for a message AI, User and System", + "default": "user" + }, + "content": { + "type": "string", + "maxLength": 131072, + "pattern": "[\\s\\S]*", + "title": "Content", + "description": "The input query/prompt to the pipeline.", + "default": "I am going to Paris, what should I see?" + }, + "sentiment": { + "type": "string", + "enum": [ + "positive", + "negative", + "neutral" + ], + "title": "Sentiment", + "description": "The sentiment of the text, which can be positive, negative, or neutral." + } + }, + "type": "object", + "required": [ + "sentiment" + ], + "title": "SessionConversationMessage", + "description": "Definition of the Chat Message type." + }, + "SessionConversationResponse": { + "properties": { + "session_info": { + "$ref": "#/components/schemas/SessionInfo" + }, + "messages": { + "items": { + "$ref": "#/components/schemas/SessionConversationMessage" + }, + "type": "array", + "title": "Messages", + "description": "The list of messages in the conversation" + } + }, + "type": "object", + "required": [ + "session_info", + "messages" + ], + "title": "SessionConversationResponse" + }, + "SessionInfo": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id", + "description": "The ID of the session" + }, + "start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time", + "description": "The start time of the session" + }, + "end_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Time", + "description": "The end time of the session" + } + }, + "type": "object", + "required": [ + "session_id" + ], + "title": "SessionInfo" + }, + "SessionSummaryResponse": { + "properties": { + "session_info": { + "$ref": "#/components/schemas/SessionInfo" + }, + "summary": { + "type": "string", + "title": "Summary", + "description": "The generated summary of the session" + }, + "sentiment": { + "type": "string", + "enum": [ + "positive", + "negative", + "neutral" + ], + "title": "Sentiment", + "description": "The sentiment of the text, which can be positive, negative, or neutral." + } + }, + "type": "object", + "required": [ + "session_info", + "summary", + "sentiment" + ], + "title": "SessionSummaryResponse" + }, + "SessionsResponse": { + "properties": { + "session_id": { + "type": "string", + "title": "Session Id", + "description": "The ID of the session" + }, + "user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id", + "description": "The ID of the user" + }, + "start_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time", + "description": "The start time of the session" + }, + "end_time": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Time", + "description": "The end time of the session" + } + }, + "type": "object", + "required": [ + "session_id", + "user_id" + ], + "title": "SessionsResponse" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + }, + "tags": [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness." + }, + { + "name": "Feedback", + "description": "APIs for storing useful information for data flywheel." + }, + { + "name": "Session", + "description": "APIs for fetching useful information for different sessions." + }, + { + "name": "User Data", + "description": "APIs for fetching user specific information." + } + ] + } \ No newline at end of file diff --git a/docs/api_references/api_gateway_server.json b/docs/api_references/api_gateway_server.json new file mode 100644 index 0000000..6636b20 --- /dev/null +++ b/docs/api_references/api_gateway_server.json @@ -0,0 +1,330 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "AI Virtual Assistant for Customer Service", + "description": "This API schema describes all the endpoints exposed by the AI Virtual Assistant for Customer Service NIM Blueprint", + "version": "1.0.0" + }, + "paths": { + "/agent/metrics": { + "get": { + "tags": [ + "Health" + ], + "summary": "Get Metrics", + "operationId": "get_metrics_agent_metrics_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/agent/health": { + "get": { + "tags": [ + "Health" + ], + "summary": "Health Check", + "description": "Perform a Health Check\n\nReturns 200 when service is up. This does not check the health of downstream services.", + "operationId": "health_check_agent_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/agent/generate": { + "post": { + "tags": [ + "Agent" + ], + "summary": "Generate Response", + "description": "Generate and stream the response to the provided prompt.", + "operationId": "generate_response_agent_generate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AgentRequest": { + "properties": { + "messages": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array", + "maxItems": 50000 + }, + { + "type": "null" + } + ], + "title": "Messages", + "description": "A list of messages comprising the conversation so far. The roles of the messages must be alternating between user and assistant. The last input message should have role user. A message with the the system role is optional, and must be the very first message if it is present.", + "default": [] + }, + "user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id", + "description": "A unique identifier representing your end-user.", + "default": "" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Session Id", + "description": "A unique identifier representing the session associated with the response.", + "default": "" + }, + "api_type": { + "type": "string", + "title": "Api Type", + "description": "The type of API action: 'create_session', 'end_session', 'generate', or 'summary'." + } + }, + "type": "object", + "required": [ + "api_type" + ], + "title": "AgentRequest", + "description": "Definition of the Prompt API data type." + }, + "AgentResponse": { + "properties": { + "id": { + "type": "string", + "maxLength": 100000, + "pattern": "[\\s\\S]*", + "title": "Id", + "default": "" + }, + "choices": { + "items": { + "$ref": "#/components/schemas/AgentResponseChoices" + }, + "type": "array", + "maxItems": 256, + "title": "Choices", + "default": [] + }, + "session_id": { + "type": "string", + "title": "Session Id", + "description": "A unique identifier representing the session associated with the response." + } + }, + "type": "object", + "title": "AgentResponse", + "description": "Definition of Chain APIs resopnse data type" + }, + "AgentResponseChoices": { + "properties": { + "index": { + "type": "integer", + "maximum": 256, + "minimum": 0, + "format": "int64", + "title": "Index", + "default": 0 + }, + "message": { + "$ref": "#/components/schemas/Message", + "default": { + "role": "assistant", + "content": "" + } + }, + "finish_reason": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Finish Reason", + "default": "" + }, + "sentiment": { + "type": "string", + "title": "Sentiment", + "description": "Any sentiment associated with this message", + "default": "" + } + }, + "type": "object", + "title": "AgentResponseChoices", + "description": "Definition of Chain response choices" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HealthResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "HealthResponse" + }, + "Message": { + "properties": { + "role": { + "type": "string", + "maxLength": 256, + "pattern": "[\\s\\S]*", + "title": "Role", + "description": "Role for a message AI, User and System", + "default": "user" + }, + "content": { + "type": "string", + "maxLength": 131072, + "pattern": "[\\s\\S]*", + "title": "Content", + "description": "The input query/prompt to the pipeline.", + "default": "I am going to Paris, what should I see?" + } + }, + "type": "object", + "title": "Message", + "description": "Definition of the Chat Message type." + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + }, + "tags": [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness." + }, + { + "name": "Agent", + "description": "Core APIs for interacting with the agent." + } + ] +} \ No newline at end of file diff --git a/docs/api_references/retriever_server.json b/docs/api_references/retriever_server.json new file mode 100644 index 0000000..ace53c3 --- /dev/null +++ b/docs/api_references/retriever_server.json @@ -0,0 +1,419 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Retriever API's for AI Virtual Assistant for Customer Service", + "description": "This API schema describes all the retriever endpoints exposed for the AI Virtual Assistant for Customer Service NIM Blueprint", + "version": "1.0.0" + }, + "paths": { + "/health": { + "get": { + "tags": [ + "Health" + ], + "summary": "Health Check", + "description": "Perform a Health Check\n\nReturns 200 when service is up. This does not check the health of downstream services.", + "operationId": "health_check_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/documents": { + "post": { + "tags": [ + "Core" + ], + "summary": "Upload Document", + "description": "Upload a document to the vector store.", + "operationId": "upload_document_documents_post", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_document_documents_post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + }, + "get": { + "tags": [ + "Management" + ], + "summary": "Get Documents", + "description": "List available documents.", + "operationId": "get_documents_documents_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Management" + ], + "summary": "Delete Document", + "description": "Delete a document.", + "operationId": "delete_document_documents_delete", + "parameters": [ + { + "name": "filename", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Filename" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + }, + "/search": { + "post": { + "tags": [ + "Core" + ], + "summary": "Document Search", + "description": "Search for the most relevant documents for the given search parameters.", + "operationId": "document_search_search_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentSearch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentSearchResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": { + "detail": "Internal server error occurred" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Body_upload_document_documents_post": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_document_documents_post" + }, + "DocumentChunk": { + "properties": { + "content": { + "type": "string", + "maxLength": 131072, + "pattern": "[\\s\\S]*", + "title": "Content", + "description": "The content of the document chunk.", + "default": "" + }, + "filename": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Filename", + "description": "The name of the file the chunk belongs to.", + "default": "" + }, + "score": { + "type": "number", + "title": "Score", + "description": "The relevance score of the chunk." + } + }, + "type": "object", + "required": [ + "score" + ], + "title": "DocumentChunk", + "description": "Represents a chunk of a document." + }, + "DocumentSearch": { + "properties": { + "query": { + "type": "string", + "maxLength": 131072, + "pattern": "[\\s\\S]*", + "title": "Query", + "description": "The content or keywords to search for within documents.", + "default": "" + }, + "top_k": { + "type": "integer", + "maximum": 25.0, + "minimum": 0.0, + "format": "int64", + "title": "Top K", + "description": "The maximum number of documents to return in the response.", + "default": 4 + }, + "user_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id", + "description": "An optional unique identifier for the customer." + } + }, + "type": "object", + "title": "DocumentSearch", + "description": "Definition of the DocumentSearch API data type." + }, + "DocumentSearchResponse": { + "properties": { + "chunks": { + "items": { + "$ref": "#/components/schemas/DocumentChunk" + }, + "type": "array", + "maxItems": 256, + "title": "Chunks", + "description": "List of document chunks." + } + }, + "type": "object", + "required": [ + "chunks" + ], + "title": "DocumentSearchResponse", + "description": "Represents a response from a document search." + }, + "DocumentsResponse": { + "properties": { + "documents": { + "items": { + "type": "string", + "maxLength": 131072, + "pattern": "[\\s\\S]*" + }, + "type": "array", + "maxItems": 1000000, + "title": "Documents", + "description": "List of filenames.", + "default": [] + } + }, + "type": "object", + "title": "DocumentsResponse", + "description": "Represents the response containing a list of documents." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HealthResponse": { + "properties": { + "message": { + "type": "string", + "maxLength": 4096, + "pattern": "[\\s\\S]*", + "title": "Message", + "default": "" + } + }, + "type": "object", + "title": "HealthResponse" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + }, + "tags": [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness." + }, + { + "name": "Core", + "description": "Core APIs for ingestion and searching." + }, + { + "name": "Management", + "description": "APIs for deleting and listing ingested files." + } + ] + } \ No newline at end of file diff --git a/docs/imgs/IVA-blueprint-diagram-r5.png b/docs/imgs/IVA-blueprint-diagram-r5.png new file mode 100644 index 0000000..2cacd93 Binary files /dev/null and b/docs/imgs/IVA-blueprint-diagram-r5.png differ diff --git a/docs/imgs/api-catalog-generate-api-key.png b/docs/imgs/api-catalog-generate-api-key.png new file mode 100644 index 0000000..c4332dc Binary files /dev/null and b/docs/imgs/api-catalog-generate-api-key.png differ diff --git a/docs/imgs/key-generated.png b/docs/imgs/key-generated.png new file mode 100644 index 0000000..e1f204d Binary files /dev/null and b/docs/imgs/key-generated.png differ diff --git a/docs/imgs/llama3.1-70b-instruct-get-api-key.png b/docs/imgs/llama3.1-70b-instruct-get-api-key.png new file mode 100644 index 0000000..586c661 Binary files /dev/null and b/docs/imgs/llama3.1-70b-instruct-get-api-key.png differ diff --git a/docs/imgs/llama3.1-70b-instruct-model-card.png b/docs/imgs/llama3.1-70b-instruct-model-card.png new file mode 100644 index 0000000..507923b Binary files /dev/null and b/docs/imgs/llama3.1-70b-instruct-model-card.png differ diff --git a/notebooks/ingest_data.ipynb b/notebooks/ingest_data.ipynb new file mode 100644 index 0000000..ea67111 --- /dev/null +++ b/notebooks/ingest_data.ipynb @@ -0,0 +1,630 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "757a1e05-6aaf-44f7-9a8e-63b9bdf5917c", + "metadata": {}, + "source": [ + "## Description\n", + "This notebook demonstrates the usage of Unstructured Data Ingestion APIs. \n", + "\n", + "## Usage Instructions\n", + "Run each cell sequentially to execute the notebook.\n", + "Note some cells are for reference and in order to not accidently excute them, they are marked as \"Markdown\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c128728d-dfac-44ee-80dd-d2bcbc9e9963", + "metadata": {}, + "outputs": [], + "source": [ + "IPADDRESS = \"localhost\" #Replace this with the correct IP address\n", + "UNSTRUCTURED_DATA_PORT = \"8086\"" + ] + }, + { + "cell_type": "markdown", + "id": "aef68f73-60c3-4be5-98e1-05cd8841ca26", + "metadata": {}, + "source": [ + "## Document Ingestion\n", + "#### Get health of the document ingest service" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6e206ac-97e8-4f72-9045-d9bcb84642ae", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "url = f'http://{IPADDRESS}:{UNSTRUCTURED_DATA_PORT}/health'\n", + "print(url)\n", + "headers = {\n", + " 'accept': 'application/json'\n", + "}\n", + "\n", + "response = requests.get(url, headers=headers)\n", + "\n", + "# Print the response\n", + "print(response.status_code)\n", + "print(response.json())" + ] + }, + { + "cell_type": "markdown", + "id": "8768d51b-1c7a-443e-b600-b29235cae1a3", + "metadata": {}, + "source": [ + "#### Ingest Manuals (pdf)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0420fee4-b652-4c5b-b82f-9e113f484e96", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import os\n", + "# URL of the API endpoint\n", + "url = f'http://{IPADDRESS}:{UNSTRUCTURED_DATA_PORT}/documents'\n", + "# Path to the PDF file you want to upload\n", + "directory_path = '../data/manuals_pdf'\n", + "\n", + "# Loop through all files in the directory\n", + "for filename in os.listdir(directory_path):\n", + " # Check if the file is a PDF\n", + " if filename.endswith('.pdf'):\n", + " file_path = os.path.join(directory_path, filename)\n", + "\n", + " # Open the file in binary mode and send it in a POST request\n", + " with open(file_path, 'rb') as file:\n", + " files = {'file': file}\n", + " response = requests.post(url, files=files)\n", + "\n", + " # Print the response from the server\n", + " print(f'Uploaded {filename}: {response.status_code}')\n", + " print(response.json())" + ] + }, + { + "cell_type": "markdown", + "id": "960c93b5-f2d1-4e06-838d-1b37bc50eb86", + "metadata": {}, + "source": [ + "#### Ingest FAQs (pdf)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46ad44c7-1f4b-4b1c-8d42-b629b81de394", + "metadata": {}, + "outputs": [], + "source": [ + "# URL of the API endpoint\n", + "import requests\n", + "url = f'http://{IPADDRESS}:{UNSTRUCTURED_DATA_PORT}/documents'\n", + "# Open the file in binary mode and send it in a POST request\n", + "filename = \"../data/FAQ.pdf\"\n", + "with open(filename, 'rb') as file:\n", + " files = {'file': file}\n", + " response = requests.post(url, files=files)\n", + "\n", + "# Print the response from the server\n", + "print(f'Uploaded {filename}: {response.status_code}')\n", + "print(response.json())" + ] + }, + { + "cell_type": "markdown", + "id": "2a14e119-26a3-434b-9ff1-c0427f371d09", + "metadata": {}, + "source": [ + "#### Get the list of documents" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "072cfc02-ed4c-4a20-8755-7abbd4a14ff4", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "\n", + "# URL of the API endpoint\n", + "url = f'http://{IPADDRESS}:{UNSTRUCTURED_DATA_PORT}/documents'\n", + "\n", + "# Send the GET request\n", + "response = requests.get(url)\n", + "\n", + "# Print the response from the server\n", + "print(f'Response Status Code: {response.status_code}')\n", + "#print(response.json())\n", + "\n", + "# Check if the request was successful\n", + "if response.status_code == 200:\n", + " data = response.json()\n", + " documents = data.get('documents', [])\n", + "\n", + " # Format and print the list of documents\n", + " print(\"Available Documents:\")\n", + " for idx, document in enumerate(documents, start=1):\n", + " print(f\"{idx}. {document}\")\n", + "else:\n", + " print(f\"Failed to retrieve documents. Status Code: {response.status_code}\")" + ] + }, + { + "cell_type": "markdown", + "id": "491db31b-9496-4dd6-b8bf-550f6d05bc48", + "metadata": {}, + "source": [ + "## Ingesting Product information from gear-store.csv\n", + "\n", + "Since the data is in csv file, but we support txt file for unstructured data ingestion. We will convert data into multiple text files and ingest them." + ] + }, + { + "cell_type": "markdown", + "id": "e0633d53-579f-4c39-8c53-5978c2a4e9fa", + "metadata": {}, + "source": [ + "### Display Data in csv file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57188764-3fe5-4ee5-b9ec-b65c10f295cd", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture output\n", + "! pip install pandas\n", + "! pip install psycopg2-binary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bea9c0cc-a836-4103-b6b8-4b641133d298", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "# Read the CSV file\n", + "df = pd.read_csv('../data/gear-store.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f23cf29-e73e-4070-bfd0-93c451b9a63e", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "display(df.head())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2796fc82-1621-4220-8cf1-62243007027b", + "metadata": {}, + "outputs": [], + "source": [ + "len(df)" + ] + }, + { + "cell_type": "markdown", + "id": "c1e88f91-a7e7-4975-83bb-c15cf1fd73df", + "metadata": {}, + "source": [ + "### Create *.txt file from csv data to ingest in canonical RAG" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51029daa-1402-483c-b5cb-8c1d8a53a391", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "\n", + "# Function to create a valid filename\n", + "def create_valid_filename(s):\n", + " # Remove invalid characters and replace spaces with underscores\n", + " s = re.sub(r'[^\\w\\-_\\. ]', '', s)\n", + " return s.replace(' ', '_')\n", + "\n", + "# Create the directory if it doesn't exist\n", + "os.makedirs('../data/product', exist_ok=True)\n", + "\n", + "# Iterate through each row in the DataFrame\n", + "for index, row in df.iterrows():\n", + " # Create filename using name, category, and subcategory\n", + " filename = f\"{create_valid_filename(row['name'])}_{create_valid_filename(row['category'])}_{create_valid_filename(row['subcategory'])}.txt\"\n", + "\n", + " print(f\"Creating file {filename}, current index {index}\")\n", + " # Full path for the file\n", + " filepath = os.path.join('../data/product', filename)\n", + "\n", + " # Create the content for the file\n", + " content = f\"Name: {row['name']}\\n\"\n", + " content += f\"Category: {row['category']}\\n\"\n", + " content += f\"Subcategory: {row['subcategory']}\\n\"\n", + " content += f\"Price: ${row['price']}\\n\"\n", + " content += f\"Description: {row['description']}\\n\"\n", + "\n", + " # Write the content to the file\n", + " with open(filepath, 'w', encoding='utf-8') as file:\n", + " file.write(content)\n", + "\n", + "print(f\"Created {len(df)} files in ../data/product\")" + ] + }, + { + "cell_type": "markdown", + "id": "49107597-2c46-4414-ab2d-5ea4971757cb", + "metadata": {}, + "source": [ + "### Ingest data from newly created text file in canonical RAG" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32129b1f-a4ee-4a1c-b3a4-f00a233de1bc", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import os\n", + "\n", + "# Helper function to ingest document in canonical RAG retriever\n", + "def ingest_file(filepath: str) -> bool:\n", + " \"\"\"\n", + " Ingest file in canonical RAG retriever\n", + "\n", + " Args:\n", + " filepath: Path to the file to be ingested in retreiver\n", + "\n", + " Returns:\n", + " bool: Status of file ingestion\n", + " \"\"\"\n", + " # URL of the API endpoint\n", + " url = f'http://{IPADDRESS}:{UNSTRUCTURED_DATA_PORT}/documents'\n", + "\n", + " # Open the file in binary mode and send it in a POST request\n", + " with open(filepath, 'rb') as file:\n", + " files = {'file': file}\n", + " try:\n", + " response = requests.post(url, files=files)\n", + " return response.status_code == 200\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Request failed for {filepath}: {e}\")\n", + " return False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "490681fa-fab9-488a-aedc-520c89cacd0f", + "metadata": {}, + "outputs": [], + "source": [ + "from concurrent.futures import ThreadPoolExecutor, as_completed\n", + "\n", + "directory_path = '../data/product'\n", + "max_workers = 5 # Adjust this based on your system's capabilities and API limits\n", + "\n", + "filepaths = [os.path.join(directory_path, filename) for filename in os.listdir(directory_path) if filename.endswith(\".txt\")]\n", + "filepaths\n", + "\n", + "successfully_ingested = []\n", + "failed_ingestion = []\n", + "\n", + "with ThreadPoolExecutor(max_workers=max_workers) as executor:\n", + " future_to_file = {executor.submit(ingest_file, filepath): filepath for filepath in filepaths}\n", + "\n", + " for future in as_completed(future_to_file):\n", + " filepath = future_to_file[future]\n", + " try:\n", + " if future.result():\n", + " print(f\"Successfully Ingested {os.path.basename(filepath)}\")\n", + " successfully_ingested.append(filepath)\n", + " else:\n", + " print(f\"Failed to Ingest {os.path.basename(filepath)}\")\n", + " failed_ingestion.append(filepath)\n", + " except Exception as e:\n", + " print(f\"Exception occurred while ingesting {os.path.basename(filepath)}: {e}\")\n", + " # traceback.print_exc()\n", + " failed_ingestion.append(filepath)\n", + "\n", + "print(f\"Total files successfully ingested: {len(successfully_ingested)}\")\n", + "print(f\"Total files failed ingestion: {len(failed_ingestion)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4bad4d7a-59f6-4a37-ad5d-bcc40e36edee", + "metadata": {}, + "source": [ + "#### (For reference) Delete a document\n", + "\n", + "The cell is in \"raw\" and does not execute. This code is for reference alone." + ] + }, + { + "cell_type": "raw", + "id": "925c5709-7ef9-4b88-b7c4-d63e7feb18e1", + "metadata": {}, + "source": [ + "import requests\n", + "\n", + "# URL of the API endpoint\n", + "url = f'http://{IPADDRESS}:{UNSTRUCTURED_DATA_PORT}/documents'\n", + "\n", + "# Filename of the document to delete\n", + "filename = 'GEFORCE_RTX_4070_SUPER_User_Guide_Rev1'\n", + "\n", + "# Parameters to be sent with the DELETE request\n", + "params = {'filename': filename}\n", + "\n", + "# Send the DELETE request\n", + "response = requests.delete(url, params=params)\n", + "\n", + "# Print the response from the server\n", + "print(f'Response Status Code: {response.status_code}')\n", + "print(response.json())" + ] + }, + { + "cell_type": "markdown", + "id": "ccf25005-13c8-4cf5-9d80-f13f0f7b51d7", + "metadata": {}, + "source": [ + "### Ingest the customer order history data into a postgres db" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbe6a2ac-c013-4ab9-b7b0-47a11d5bafe9", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import csv\n", + "import re\n", + "import psycopg2\n", + "from datetime import datetime\n", + "\n", + "# Database connection parameters\n", + "db_params = {\n", + " 'dbname': 'customer_data',\n", + " 'user': 'postgres',\n", + " 'password': 'password',\n", + " 'host': IPADDRESS, # e.g., 'localhost' or the IP address\n", + " 'port': '5432' # e.g., '5432'\n", + "}\n", + "\n", + "# CSV file path\n", + "csv_file_path = '../data/orders.csv'\n", + "\n", + "# Connect to the database\n", + "conn = psycopg2.connect(**db_params)\n", + "cur = conn.cursor()\n", + "\n", + "# Create the table if it doesn't exist\n", + "create_table_query = '''\n", + "CREATE TABLE IF NOT EXISTS customer_data (\n", + " customer_id INTEGER NOT NULL,\n", + " order_id INTEGER NOT NULL,\n", + " product_name VARCHAR(255) NOT NULL,\n", + " product_description VARCHAR NOT NULL,\n", + " order_date DATE NOT NULL,\n", + " quantity INTEGER NOT NULL,\n", + " order_amount DECIMAL(10, 2) NOT NULL,\n", + " order_status VARCHAR(50),\n", + " return_status VARCHAR(50),\n", + " return_start_date DATE,\n", + " return_received_date DATE,\n", + " return_completed_date DATE,\n", + " return_reason VARCHAR(255),\n", + " notes TEXT,\n", + " PRIMARY KEY (customer_id, order_id)\n", + ");\n", + "'''\n", + "cur.execute(create_table_query)\n", + "\n", + "# Open the CSV file and insert data\n", + "with open(csv_file_path, 'r') as f:\n", + " reader = csv.reader(f)\n", + " next(reader) # Skip the header row\n", + "\n", + " for row in reader:\n", + " # Access columns by index as per the provided structure\n", + " order_id = int(row[1]) # OrderID\n", + " customer_id = int(row[0]) # CID (Customer ID)\n", + "\n", + " # Correcting the order date to include time\n", + " order_date = datetime.strptime(row[4], \"%Y-%m-%dT%H:%M:%S\") # OrderDate with time\n", + "\n", + " quantity = int(row[5]) # Quantity\n", + "\n", + " # Handle optional date fields with time parsing\n", + " return_start_date = datetime.strptime(row[9], \"%Y-%m-%dT%H:%M:%S\") if row[9] else None # ReturnStartDate\n", + " return_received_date = datetime.strptime(row[10],\"%Y-%m-%dT%H:%M:%S\") if row[10] else None # ReturnReceivedDate\n", + " return_completed_date = datetime.strptime(row[11], \"%Y-%m-%dT%H:%M:%S\") if row[11] else None # ReturnCompletedDate\n", + "\n", + " # Clean product name\n", + " product_name = re.sub(r'[®™]', '', row[2]) # ProductName\n", + "\n", + " product_description = re.sub(r'[®™]', '', row[3])\n", + " # OrderAmount as float\n", + " order_amount = float(row[6].replace(',', ''))\n", + "\n", + " # Insert data into the database\n", + " cur.execute(\n", + " '''\n", + " INSERT INTO customer_data (\n", + " customer_id, order_id, product_name, product_description, order_date, quantity, order_amount,\n", + " order_status, return_status, return_start_date, return_received_date,\n", + " return_completed_date, return_reason, notes\n", + " ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n", + " ''',\n", + " (customer_id, order_id, product_name, product_description, order_date, quantity, order_amount,\n", + " row[7], # OrderStatus\n", + " row[8], # ReturnStatus\n", + " return_start_date, return_received_date, return_completed_date,\n", + " row[12], # ReturnReason\n", + " row[13]) # Notes\n", + " )\n", + "\n", + "# Commit the changes and close the connection\n", + "conn.commit()\n", + "cur.close()\n", + "conn.close()\n", + "\n", + "print(\"CSV Data imported successfully!\")" + ] + }, + { + "cell_type": "markdown", + "id": "83f6c850-4083-48d3-bc3a-0b517a440027", + "metadata": {}, + "source": [ + "#### Read the data to ensure it was written " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ab0d010-2250-4489-af04-88e24c1241fa", + "metadata": {}, + "outputs": [], + "source": [ + "import psycopg2\n", + "\n", + "# Database connection parameters\n", + "db_params = {\n", + " 'dbname': 'customer_data',\n", + " 'user': 'postgres',\n", + " 'password': 'password',\n", + " 'host': IPADDRESS, # e.g., 'localhost' or the IP address\n", + " 'port': '5432' # e.g., '5432'\n", + "}\n", + "\n", + "# Connect to the database\n", + "conn = psycopg2.connect(**db_params)\n", + "cur = conn.cursor()\n", + "\n", + "# Query to select the first 5 rows from the customer_data table\n", + "query = 'SELECT * FROM customer_data LIMIT 5;'\n", + "\n", + "# Execute the query\n", + "cur.execute(query)\n", + "\n", + "# Fetch the column headers\n", + "colnames = [desc[0] for desc in cur.description]\n", + "\n", + "# Fetch the first 5 rows\n", + "rows = cur.fetchall()\n", + "\n", + "# Print the headers and the corresponding rows\n", + "for i, row in enumerate(rows, start=1):\n", + " print(f\"\\nRow {i}:\")\n", + " for header, value in zip(colnames, row):\n", + " print(f\"{header}: {value}\")\n", + "\n", + "# Close the connection\n", + "cur.close()\n", + "conn.close()" + ] + }, + { + "cell_type": "markdown", + "id": "95ce05ca-d926-44a5-970d-9a5d95cffd1f", + "metadata": {}, + "source": [ + "#### (For reference)Drop the postgres table\n", + "\n", + "The cell is in raw format and does not execute. This code is for reference alone." + ] + }, + { + "cell_type": "raw", + "id": "08c07dfd-c056-44b3-aa3b-4e12114b1b10", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "# pip install psycopg2-binary\n", + "import psycopg2\n", + "\n", + "# Database connection parameters\n", + "db_params = {\n", + " 'dbname': 'customer_data',\n", + " 'user': 'postgres',\n", + " 'password': 'password',\n", + " 'host': IPADDRESS, # e.g., 'localhost' or the IP address\n", + " 'port': '5432' # e.g., '5432'\n", + "}\n", + "\n", + "# Connect to the database\n", + "conn = psycopg2.connect(**db_params)\n", + "cur = conn.cursor()\n", + "\n", + "# Drop the table if it exists\n", + "drop_table_query = 'DROP TABLE IF EXISTS customer_data;'\n", + "\n", + "# Execute the drop query\n", + "cur.execute(drop_table_query)\n", + "\n", + "# Commit the changes and close the connection\n", + "conn.commit()\n", + "cur.close()\n", + "conn.close()\n", + "\n", + "print(\"Table 'customer_data' dropped successfully!\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/synthetic_data_generation.ipynb b/notebooks/synthetic_data_generation.ipynb new file mode 100644 index 0000000..2da3763 --- /dev/null +++ b/notebooks/synthetic_data_generation.ipynb @@ -0,0 +1,451 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "703125c0-7f27-4ef6-ada2-b433879b0c97", + "metadata": {}, + "source": [ + "# Synthetic Data Generation\n", + "## Description\n", + "\n", + "This notebook demonstrates how to use the nemotron-4-340b-instruct model for synthetic data generation that is used in this blueprint.\n", + "\n", + "This uses the nvidia gear store data as a source of product data.\n", + "\n", + "It then creates a sample customer set and then creates a realistic order history based on the nvidia gear store data.\n", + "\n", + "## Usage Instructions\n", + "1. Install the required libraries using pip.\n", + "2. Run each cell sequentially to execute the notebook." + ] + }, + { + "cell_type": "markdown", + "id": "7edd1357-a874-4348-a34d-614fa344a2bf", + "metadata": {}, + "source": [ + "### Install requirements" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f93e604a-04e6-4800-8a80-4b68c1491575", + "metadata": {}, + "outputs": [], + "source": [ + "## Install requirements\n", + "!pip install pandas\n", + "!pip install --upgrade --quiet langchain-nvidia-ai-endpoints\n", + "!pip install langchain" + ] + }, + { + "cell_type": "markdown", + "id": "5b899a94-f4d5-414d-9b5d-3422c85d5a5f", + "metadata": {}, + "source": [ + "### Set the NVIDIA API Key \n", + "\n", + "The nemotron-4-340b-instruct model is accessed from the NVIDIA API Catalog. In order to access it, you need to set a valid API Key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6db9484-2b47-45c8-92b3-235ebb2ed1d6", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "if not os.environ.get(\"NVIDIA_API_KEY\", \"\").startswith(\"nvapi-\"):\n", + " nvidia_api_key = getpass.getpass(\"Enter your NVIDIA API key: \")\n", + " assert nvidia_api_key.startswith(\"nvapi-\"), f\"{nvidia_api_key[:5]}... is not a valid key\"\n", + " os.environ[\"NVIDIA_API_KEY\"] = nvidia_api_key" + ] + }, + { + "cell_type": "markdown", + "id": "cedc2be6-4a0f-4851-9bf5-14a100372203", + "metadata": {}, + "source": [ + "## Re-usable functions\n", + "\n", + "This function is used to generated the data. Data returned is in the form of a json object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36854645-32f9-438b-b499-5c79461694e6", + "metadata": {}, + "outputs": [], + "source": [ + "def get_do_task(llm, template, input_variables=[], input_values={}):\n", + " from langchain_core.output_parsers import JsonOutputParser \n", + " parser = JsonOutputParser()\n", + " task_template = PromptTemplate(\n", + " input_variables=input_variables,\n", + " template=template\n", + " )\n", + " synthetic_data_chain = task_template | llm | parser \n", + " response = synthetic_data_chain.invoke(input_values)\n", + " return response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05310e58-a60b-495d-b803-59ceefee1a83", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "unique_numbers = set()\n", + "def get_random_number(low, high):\n", + " # Loop until we have a certain number of unique random numbers\n", + " while True:\n", + " random_number = random.randint(low, high)\n", + " if random_number not in unique_numbers:\n", + " unique_numbers.add(random_number)\n", + " break\n", + " return random_number" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c02965cf-60d0-4f22-aed3-79d593b1598a", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import random\n", + "import getpass\n", + "import os\n", + "import langchain\n", + "from langchain.prompts import PromptTemplate\n", + "\n", + "from langchain_nvidia_ai_endpoints import ChatNVIDIA\n", + "llm = ChatNVIDIA(model=\"nvidia/nemotron-4-340b-instruct\", temperature=1.0, max_tokens=2048)" + ] + }, + { + "cell_type": "markdown", + "id": "a1e7e41c-0f55-49b6-a65e-e928cd7c4e14", + "metadata": {}, + "source": [ + "## Variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6504ee7f-8408-475c-aad2-f264efe8a1bf", + "metadata": {}, + "outputs": [], + "source": [ + "CUSTOMER_FILENAME=\"./customers.csv\"\n", + "PRODUCT_FILENAME=\"../data/gear-store.csv\"\n", + "ORDERHISTORY_FILENAME=\"./orders.csv\"" + ] + }, + { + "cell_type": "markdown", + "id": "13b529b2-cee0-4339-a09d-a97b50639a6c", + "metadata": {}, + "source": [ + "## Customer Profile Generation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5e779ba-a00f-4c48-aa57-610a84c2b2a0", + "metadata": {}, + "outputs": [], + "source": [ + "customer_template = '''\n", + "Create 10 rows of data representing a customer in a database.\n", + "The customer table has the following schema. \n", + "\n", + " 'FNAME': This is the customer's first name, \n", + " 'LNAME': This is the customer's last name,\n", + " 'AGE': age,\n", + " 'GENDER': gender. this should me male or female,\n", + " 'STATE': state in united states\n", + " 'ZIPCODE': zipcode and this should be present in the state mentioned above,\n", + " 'INTERESTS': this is any 2-3 interests \n", + " 'MEMBER_SINCE': This is a date between 1st Jaunary 2015 and 30th June 2024\n", + "\n", + "\n", + "Ensure the first name and last names are unique pairs in the dataset created.\n", + "Return the data in the form of json array of customer rows.\n", + "Do not include any niceties. \n", + "Do you not add any more attributes.\n", + "Return the data in the form of a json array that be loaded into a python variable with json.loads(..). Do not have the \"json\" work in the returned string\n", + "'''\n", + "customers = get_do_task(llm, customer_template,input_variables=[], input_values={})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b0f362f-90c5-41e6-ba1e-8cd27cc34467", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import csv\n", + "# Specify the CSV file name\n", + "csv_file = CUSTOMER_FILENAME\n", + "# Initialize an empty set to store unique random numbers\n", + "unique_numbers = set()\n", + "\n", + "for customer in customers:\n", + " for key, value in customer.items():\n", + " if isinstance(value, str):\n", + " customer[key] = value.strip()\n", + " elif isinstance(value, list):\n", + " customer[key] = ', '.join(value).strip()\n", + " customer['CID'] = get_random_number(100,10000)\n", + "\n", + "fieldnames = ['CID'] + [key for key in customers[0].keys() if key != 'CID']\n", + " \n", + "# Write the data to a CSV file\n", + "with open(csv_file, mode='w', newline='') as file:\n", + " writer = csv.DictWriter(file, fieldnames=fieldnames)\n", + " writer.writeheader() # Write the header row\n", + " for customer in customers:\n", + " writer.writerow(customer)\n", + "\n", + "print(f\"Data successfully written to {csv_file}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cfa1997b-7238-46fe-a00d-2fe1440a1315", + "metadata": {}, + "source": [ + "## Order History Generation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ced66b69-c5d3-4d5e-9c27-378544484a00", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the data frames \n", + "# 1. Electronics \n", + "# 2. Other stuff\n", + "# 3. Ensure you don't use the gift card \n", + "\n", + "import pandas as pd\n", + "\n", + "# Load the CSV file\n", + "file_path = PRODUCT_FILENAME\n", + "df = pd.read_csv(file_path)\n", + "\n", + "# Filter out any products that contain the word \"Gift card\" in the name column\n", + "df_filtered = df[~df['name'].str.contains('Gift card', case=False, na=False)]\n", + "\n", + "# First DataFrame: NVIDIA Electronics category (excluding gift cards)\n", + "df_nvidia_electronics = df_filtered[df_filtered['category'] == 'NVIDIA Electronics'][['name', 'description', 'price']]\n", + "\n", + "# Second DataFrame: All other categories (excluding gift cards)\n", + "df_other = df_filtered[df_filtered['category'] != 'NVIDIA Electronics'][['name', 'description', 'price']]\n", + "\n", + "# Display both DataFrames\n", + "print(\"NVIDIA Electronics DataFrame:\")\n", + "print(df_nvidia_electronics)\n", + "\n", + "print(\"\\nOther Categories DataFrame:\")\n", + "print(df_other)\n", + "\n", + "NUM_ELECTRONIC_PRODUCTS = len(df_nvidia_electronics)\n", + "NUM_OTHER_PRODUCTS = len(df_other)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c6e4a4f-6986-4a01-bf80-307289a4c7ce", + "metadata": {}, + "outputs": [], + "source": [ + "order_template = '''\n", + " You will be given information regarding 10 products in an array where each array element contains the \n", + " product name, product description and product price. You will also be given the current date. Use these things to do the following:\n", + " \n", + " You will need to create an order history table containing 10 rows of data representing\n", + " where each row maps to one of the 10 products you were given.\n", + " Make the data interesting i.e. don't usually stick to delivered but do returns and return rejects \n", + " Return the data in the form of json object\n", + " Do not include any niceties. \n", + " Do you not add any more attributes.\n", + " Return the data in the form of a json array that be loaded into a python variable with json.loads(..).\n", + " Do not have the word \"json\" in the returned string\n", + " \n", + " You will be given the product description to use in the creation of the ReturnReason. \n", + " \n", + " Product Description: {product_desc}\n", + " Current Date: {current_date}\n", + " \n", + " This schema of the data is presented like this:- \n", + " \n", + " product_name STRING \"Product name\"\n", + " product_description STRING \"Product description\"\n", + " OrderDate DATETIME \"Date and time when the order was placed\"\n", + " Quantity INT \"Number of units ordered\" \n", + " OrderAmount INT \"Product Price X Quantity\"\n", + " OrderStatus\tVARCHAR(50)\t\"Status of the order\" \n", + " ReturnStatus\tVARCHAR(50)\t\"Status of the return\" \n", + " ReturnStartDate\tDATETIME \"Date when the return was started\" \n", + " ReturnReceivedDate DATETIME\t\"Date when the return was receive\" \n", + " ReturnCompletedDate\tDATETIME \"Date when the return was completed\" \n", + " ReturnReason VARCHAR(255) \"Reason for the return\" \n", + " Notes VARCHAR(255) \"Notes sent to the customer\" \n", + " \n", + " Instructions to set the various attributes:\n", + " * Keep the current date in mind when generating the data.\n", + " * product_name: This should match the product name from the input that this record is generated for.\n", + " * product_description: This should match the product description input that this record is generated for.\n", + " * OrderDate: This should be a date between October 1st 2024 and October 20th 2024. \n", + " * Quantity: This should be a number between 1 and 8\n", + " * OrderAmount: This is a multiple of the Quantity and the Product price in the input. \n", + " * OrderStatus: This should be one these values [Pending, Processing, Shipped, In Transit, Out for Delivery, Delivered, Cancelled, Returned, Return Requested, Delayed, On Hold].\n", + " * ReturnStatus: This should be set to one of these values [None, Requested, Approved, Rejected, Received, Processing, Refunded, Pending Approval, Return to Sender, Awaiting Customer Action]\n", + " This should be set ONLY when OrderStatus is set to either \"Returned\" or \"Return Requested\" else set to None\n", + " \n", + " * ReturnStartDate: This is set only when the OrderStatus is \"Return Requested\". It should be a date should be within 7 days after the OrderDate but before the \"current date\" else set to None.\n", + " * ReturnReceivedDate: This should be set to a date within 5 days after the ReturnStartDate else set to None\n", + " * ReturnCompletedDate: This should be a date when the return was completed and set only when the OrderStatus field is set to \"Returned\" and ReturnStatus field set to \"Approved\".\n", + " This should be a minimum of 15 and 30 days after the ReturnReceivedDate else set to None\n", + " * ReturnReason: This should be set to a creative reason that would make logical sense based on the product description only when the OrderStatus is set to \"Returned\" or \"Return Requested\"\n", + " This should be set to something when the ReturnStatus is not None. \n", + " * Notes: This is information sent back to the customer. This should be something that is relevant and makes sense for the \n", + " product when the ReturnStatus is set to \"Rejected\". It can also be used to put notes if the order has been in processing state for longer than usual.\n", + " \n", + " \n", + " In the returned string:-\n", + " Do not include any niceties. \n", + " Do you not add any more attributes.\n", + " Return the data in the form of a json array that be loaded into a python variable with json.loads(..). Do not have the word \"json\" in the returned string\n", + " '''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c45ce1b-cdaa-4d53-a224-75e4cdd4fd05", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a table of 10 products with 7 products being technical and 3 products being \"other\"\n", + "#NUM_ELECTRONIC_PRODUCTS = len(df_nvidia_electronics)\n", + "#NUM_OTHER_PRODUCTS = len(df_other)\n", + "def get_random_products():\n", + " import pandas as pd\n", + " # Randomly pick 7 rows from the DataFrame and select only the name and description columns\n", + " random_rows = df_nvidia_electronics.sample(n=7)[['name', 'description', 'price']]\n", + " # Convert the selected DataFrame rows to a regular array (list of lists)\n", + " random_rows_json = random_rows.to_dict(orient='records')\n", + " # Display the array \n", + " product_rows = random_rows_json \n", + " random_rows = df_other.sample(n=3)[['name', 'description', 'price']]\n", + " # Convert the selected DataFrame rows to a regular array (list of lists)\n", + " random_rows_json = random_rows.to_dict(orient='records')\n", + " # Display the array\n", + " product_rows.append(random_rows_json)\n", + " return product_rows" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1110edbe-3b5d-4309-8951-99313a708a72", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Delete the order history file if it exists\n", + "import os\n", + "file_path = ORDERHISTORY_FILENAME\n", + "\n", + "# Check if the file exists and delete it\n", + "if os.path.exists(file_path):\n", + " os.remove(file_path)\n", + " print(f\"File '{file_path}' has been deleted.\")\n", + "else:\n", + " print(f\"File '{file_path}' does not exist.\")\n", + "\n", + "# Now read the customer data file one row at a time\n", + "import pandas as pd\n", + "\n", + "# Load the CSV file\n", + "file_path = CUSTOMER_FILENAME\n", + "dfcustomer = pd.read_csv(file_path)\n", + "\n", + "# Initialize an empty set to store unique random numbers\n", + "unique_numbers = set()\n", + "\n", + "# Loop over each row in the DataFrame\n", + "for index, row in dfcustomer.iterrows():\n", + " random_prod_list = get_random_products()\n", + " print(row['CID'])\n", + "\n", + " order_history = get_do_task(llm, order_template, input_variables = ['product_desc', 'current_date'], \n", + " input_values = {'product_desc': random_prod_list, 'current_date': \"23rd October 2024\"})\n", + " df = pd.DataFrame(order_history)\n", + "\n", + " # Add the Customer ID (CID) as a new column\n", + " df['CID'] = row['CID']\n", + " for index, row in df.iterrows():\n", + " df.at[index, 'OrderID'] = int(get_random_number(10,100000))\n", + " df['OrderID'] = df['OrderID'].astype(int)\n", + "\n", + " # Reorder columns to ensure 'CustomerID' is the first column\n", + " columns = ['CID', 'OrderID'] + [col for col in df.columns if col not in ['CID', 'OrderID']]\n", + " df = df[columns]\n", + " # Append DataFrame to CSV file\n", + " df.to_csv(ORDERHISTORY_FILENAME, mode='a', index=False, header=not pd.io.common.file_exists(ORDERHISTORY_FILENAME))\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "6ecad68e-a75d-4de1-b85e-3f21c90eaf11", + "metadata": {}, + "source": [ + "#### Copy over the csv files to the \"data\" folder\n", + "\n", + "If you are satisfied with the csv files generated, copy them over to the data folder for ingestion." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/agent/Dockerfile b/src/agent/Dockerfile new file mode 100644 index 0000000..4963005 --- /dev/null +++ b/src/agent/Dockerfile @@ -0,0 +1,47 @@ +ARG BASE_IMAGE_URL=nvcr.io/nvidia/base/ubuntu +ARG BASE_IMAGE_TAG=22.04_20240212 + +FROM ${BASE_IMAGE_URL}:${BASE_IMAGE_TAG} + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV DEBIAN_FRONTEND noninteractive + +# Install required ubuntu packages for setting up python 3.10 +RUN apt update && \ + apt install -y curl software-properties-common && \ + add-apt-repository ppa:deadsnakes/ppa && \ + apt update && apt install -y python3.10 && \ + apt-get clean + +# Install pip for python3.10 +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10 + +RUN rm -rf /var/lib/apt/lists/* + +# Uninstall build packages +RUN apt autoremove -y curl software-properties-common + +# Download the sources of apt packages within the container for standard legal compliance +RUN sed -i 's/# deb-src/deb-src/g' /etc/apt/sources.list +RUN apt update +# xz-utils is needed to pull the source and unpack them correctly +RUN apt install xz-utils -y +RUN mkdir -p /legal/source +WORKDIR /legal/source +# Read installed packages, strip all but the package names, pipe to 'apt source' to download respective packages +RUN apt list --installed | grep -i installed | sed 's|\(.*\)/.*|\1|' | xargs apt source --download-only +# The source is saved in directories as well as tarballs in the current dir +RUN rm xz-utils* +COPY LICENSE-3rd-party.txt /legal/ + +RUN --mount=type=bind,source=src/agent/requirements.txt,target=/opt/agent/requirements.txt \ + pip3 install --no-cache-dir -r /opt/agent/requirements.txt + +# Install common dependencies and copy common code +COPY src/common /opt/src/common + +# Copy and Install agent specific modules +COPY src/agent /opt/src/agent + +WORKDIR /opt +ENTRYPOINT ["uvicorn", "src.agent.server:app"] diff --git a/src/agent/cache/__init__.py b/src/agent/cache/__init__.py new file mode 100644 index 0000000..d50bcc4 --- /dev/null +++ b/src/agent/cache/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/src/agent/cache/local_cache.py b/src/agent/cache/local_cache.py new file mode 100644 index 0000000..e93b063 --- /dev/null +++ b/src/agent/cache/local_cache.py @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manager conversation history return relavant conversation history +based on session_id using python dict +""" + +from typing import List, Optional, Dict +import time + +class LocalCache: + # Maintain conversation history session_id: List[] + conversation_hist = {} + + # TODO: Create API to get last k conversation from database instead of returning everything + def get_conversation(self, session_id: str) -> List: + return self.conversation_hist.get(session_id, {}).get("conversation_hist", []) + + + def get_k_conversation(self, session_id: str, k_turn: Optional[int] = None) -> List: + if not k_turn: + return self.conversation_hist.get(session_id, {}).get("conversation_hist", []) + return self.conversation_hist.get(session_id, {}).get("conversation_hist", [])[-k_turn:] + + + def save_conversation(self, session_id: str, user_id: Optional[str], conversation: List) -> bool: + try: + self.conversation_hist[session_id] = { + "user_id": user_id or "", + "conversation_hist": self.conversation_hist.get(session_id, {}).get("conversation_hist", []) + conversation, + "last_conversation_time": f"{time.time()}" + } + return True + except Exception as e: + print(f"Failed to save conversation due to exception {e}") + return False + + + def is_session(self, session_id: str) -> bool: + """Check if session_id already exist in database""" + return session_id in self.conversation_hist + + + def get_session_info(self, session_id: str) -> Dict: + """Retrieve complete session information from database""" + return self.conversation_hist.get(session_id, {}) + + + def response_feedback(self, session_id: str, response_feedback: float) -> bool: + try: + session = self.conversation_hist.get(session_id, {}) + conversation_hist = session.get("conversation_hist", []) + + if not conversation_hist: + print(f"No conversation history found for session {session_id}") + return False + + conversation_hist[-1]["feedback"] = response_feedback + return True + except KeyError as e: + print(f"KeyError: Unable to store user feedback. Missing key: {e}") + return False + except IndexError: + print(f"IndexError: Conversation history is empty for session {session_id}") + return False + except Exception as e: + print(f"Unexpected error while storing user feedback: {e}") + return False + + + def delete_conversation(self, session_id: str) -> bool: + """Delete conversation for given session id""" + if session_id in self.conversation_hist: + del self.conversation_hist[session_id] + print(f"Deleted conversation history for session ID: {session_id}") + return True + else: + print(f"No conversation history found for session ID: {session_id}") + return False + + + def create_session(self, session_id: str, user_id: str = ""): + """Create a entry for given session id""" + try: + # user_id is placeholder for now + # when create_session accept user_Id utilize this + self.conversation_hist[session_id] = { + "user_id": user_id or "", + "conversation_hist": [], + "last_conversation_time": f"{time.time()}" + } + return True + except Exception as e: + print(f"Failed to create session due to exception {e}") + return False \ No newline at end of file diff --git a/src/agent/cache/redis_client.py b/src/agent/cache/redis_client.py new file mode 100644 index 0000000..2c74afb --- /dev/null +++ b/src/agent/cache/redis_client.py @@ -0,0 +1,193 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manager conversation history return relavant conversation history +based on session_id using redis +""" + +from typing import List, Optional, Dict +import os +import redis +import json +import time + +from src.common.utils import get_config + +DEFAULT_DB = "0" +class RedisClient: + def __init__(self) -> None: + + + # TODO: Enable config to pass any additional config information + # Like db etc. + # config = eval(get_config().cache.config) + # print("Config extracted: ", config, type(config)) + + # convert hours into second as redis takes expiry time in seconds + self.expiry = int(os.getenv("REDIS_SESSION_EXPIRY", 12)) * 60 * 60 + print(f"Redis Cache expiry {self.expiry} seconds") + host, port = get_config().cache.url.split(":") + # db = config.get("db", None) or DEFAULT_DB + print(f"Host: {host}, Port: {port}, DB: {DEFAULT_DB}") + self.redis_client = redis.Redis(host=host, port=port, db=DEFAULT_DB, decode_responses=True) + + + def get_conversation(self, session_id: str) -> List: + """Retrieve the entire conversation history from Redis as a list""" + + conversation_hist = self.redis_client.lrange(f"{session_id}:conversation_hist", 0, -1) + return [json.loads(conv) for conv in conversation_hist] + + def get_k_conversation(self, session_id: str, k_turn: Optional[int] = None) -> List: + """Retrieve the last k conversations from Redis""" + + # TODO: Evaluate this implementation + if k_turn is None: + k_turn = -1 + conversation_hist = self.redis_client.lrange(f"{session_id}:conversation_hist", -k_turn, -1) + return [json.loads(conv) for conv in conversation_hist] + + def save_conversation(self, session_id: str, user_id: Optional[str], conversation: List) -> bool: + try: + # Store each conversation entry as a JSON string in a Redis list + for conv in conversation: + self.redis_client.rpush(f"{session_id}:conversation_hist", json.dumps(conv)) + self.redis_client.expire(f"{session_id}:conversation_hist", self.expiry) + + + # Store user_id and last conversation time as separate keys + if user_id: + self.redis_client.set(f"{session_id}:user_id", user_id, ex=self.expiry) + + # Store start conversation time only if it doesn't exist + start_time_key = f"{session_id}:start_conversation_time" + if not self.redis_client.exists(start_time_key): + self.redis_client.set(start_time_key, f"{conversation[0].get('timestamp')}", ex=self.expiry) + + self.redis_client.set(f"{session_id}:last_conversation_time", f"{conversation[-1].get('timestamp')}", ex=self.expiry) + + start_time_key = f"{session_id}:start_conversation_time" + if not self.redis_client.exists(start_time_key): + self.redis_client.expire(start_time_key, self.expiry) + + return True + except Exception as e: + print(f"Failed to ingest document due to exception {e}") + return False + + + def is_session(self, session_id: str) -> bool: + """Check if session_id already exist in cache""" + return self.redis_client.exists(f"{session_id}:start_conversation_time") + + + def get_session_info(self, session_id: str) -> Dict: + """Retrieve complete session information from cache""" + + resp = {} + conversation_hist = self.redis_client.lrange(f"{session_id}:conversation_hist", 0, -1) + resp["conversation_hist"] = [json.loads(conv) for conv in conversation_hist] + resp["user_id"] = self.redis_client.get(f"{session_id}:user_id") + resp["last_conversation_time"] = self.redis_client.get(f"{session_id}:last_conversation_time") + resp["start_conversation_time"] = self.redis_client.get(f"{session_id}:start_conversation_time") + + return resp + + + def response_feedback(self, session_id: str, response_feedback: float) -> bool: + try: + # Get the key for the conversation history + conv_key = f"{session_id}:conversation_hist" + + # Check if the conversation history exists + if not self.redis_client.exists(conv_key): + print(f"No conversation history found for session {session_id}") + return False + + # Get the last conversation entry + last_conv = self.redis_client.lindex(conv_key, -1) + if not last_conv: + print(f"Conversation history is empty for session {session_id}") + return False + + # Parse the last conversation, add feedback, and update in Redis + conv_data = json.loads(last_conv) + conv_data['feedback'] = response_feedback + updated_conv = json.dumps(conv_data) + + # Replace the last entry with the updated one + self.redis_client.lset(conv_key, -1, updated_conv) + + return True + + except ValueError as e: + print(f"ValueError: {str(e)}") + return False + except json.JSONDecodeError: + print(f"JSONDecodeError: Unable to parse conversation data for session {session_id}") + return False + except redis.RedisError as e: + print(f"RedisError: {str(e)}") + return False + except Exception as e: + print(f"Unexpected error while storing user feedback: {str(e)}") + return False + + + def delete_conversation(self, session_id: str) -> bool: + """Delete conversation for the given session id""" + try: + # Define the keys to delete + keys_to_delete = [ + f"{session_id}:conversation_hist", + f"{session_id}:user_id", + f"{session_id}:last_conversation_time", + f"{session_id}:start_conversation_time" + ] + + # Use pipeline to delete keys, checking if they exist + pipeline = self.redis_client.pipeline() + for key in keys_to_delete: + # Only delete if the key exists + if self.redis_client.exists(key): + pipeline.delete(key) + + pipeline.execute() + + + print(f"Deleted conversation history and associated data for session ID: {session_id}") + return True + + except redis.RedisError as e: + print(f"RedisError: Unable to delete conversation for session {session_id}. Error: {str(e)}") + return False + except Exception as e: + print(f"Unexpected error while deleting conversation: {str(e)}") + return False + + + def create_session(self, session_id: str, user_id: str = ""): + """Create a entry for given session id""" + try: + # Store start conversation time only if it doesn't exist + start_time_key = f"{session_id}:start_conversation_time" + if not self.redis_client.exists(start_time_key): + self.redis_client.set(start_time_key, f"{time.time()}", ex=self.expiry) + + return True + except Exception as e: + print(f"Failed to ingest document due to exception {e}") + return False \ No newline at end of file diff --git a/src/agent/cache/session_manager.py b/src/agent/cache/session_manager.py new file mode 100644 index 0000000..5212821 --- /dev/null +++ b/src/agent/cache/session_manager.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manager conversation history return relavant conversation history +based on session_id +""" + +from typing import List, Optional, Dict + +from src.common.utils import get_config +from src.agent.cache.local_cache import LocalCache +from src.agent.cache.redis_client import RedisClient + +class SessionManager: + """ + Store the conversation between user and assistant, it's stored in format + {"session_id": {"user_id": "", "conversation_hist": [{"role": "user/assistant", "content": "", "timestamp": ""}], "last_conversation_time: ""}} + """ + def __init__(self, *args, **kwargs) -> None: + db_name = get_config().cache.name + if db_name == "redis": + print("Using Redis client for user history") + self.memory = RedisClient() + elif db_name == "inmemory": + print("Using python dict for user history") + self.memory = LocalCache() + else: + raise ValueError(f"{db_name} in not supported. Supported type redis, inmemory") + + + # TODO: Create API to get last k conversation from database instead of returning everything + def get_conversation(self, session_id: str) -> List: + return self.memory.get_conversation(session_id) + + + def get_k_conversation(self, session_id: str, k_turn: Optional[int] = None) -> List: + return self.memory.get_k_conversation(session_id, k_turn) + + + def save_conversation(self, session_id: str, user_id: Optional[str], conversation: List) -> bool: + return self.memory.save_conversation(session_id, user_id, conversation) + + + def is_session(self, session_id: str) -> bool: + """Check if session_id already exist in database""" + return self.memory.is_session(session_id) + + + def get_session_info(self, session_id: str) -> Dict: + """Retrieve complete session information from database""" + return self.memory.get_session_info(session_id) + + def response_feedback(self, session_id: str, response_feedback: float) -> bool: + return self.memory.response_feedback(session_id, response_feedback) + + def delete_conversation(self, session_id: str): + """Delete conversation for given session id""" + return self.memory.delete_conversation(session_id) + + def create_session(self, session_id: str, user_id: str = ""): + """Create a entry for given session id""" + return self.memory.create_session(session_id, user_id) \ No newline at end of file diff --git a/src/agent/datastore/__init__.py b/src/agent/datastore/__init__.py new file mode 100644 index 0000000..d50bcc4 --- /dev/null +++ b/src/agent/datastore/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/src/agent/datastore/datastore.py b/src/agent/datastore/datastore.py new file mode 100644 index 0000000..c3944d8 --- /dev/null +++ b/src/agent/datastore/datastore.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Baes class to store the user conversation in database permanently +""" + +from typing import List, Optional +from datetime import datetime + +from src.common.utils import get_config +from src.agent.datastore.postgres_client import PostgresClient +# from src.agent.datastore.redis_client import RedisClient + + +class Datastore: + def __init__(self): + db_name = get_config().database.name + if db_name == "postgres": + print("Using postgres to store conversation history") + self.database = PostgresClient() + # elif db_name == "redis": + # print("Using Redis to store conversation history") + # self.database = RedisClient() + else: + raise ValueError(f"{db_name} database in not supported. Supported type postgres") + + def store_conversation(self, session_id: str, user_id: Optional[str], conversation_history: list, last_conversation_time: str, start_conversation_time: str): + """store conversation for given details""" + self.database.store_conversation(session_id, user_id, conversation_history, last_conversation_time, start_conversation_time) + + def fetch_conversation(self, session_id: str): + """fetch conversation for given session id""" + self.database.fetch_conversation(session_id) + + def delete_conversation(self, session_id: str): + """Delete conversation for given session id""" + self.database.delete_conversation(session_id) + + def is_session(self, session_id: str) -> bool: + return self.database.is_session(session_id) \ No newline at end of file diff --git a/src/agent/datastore/postgres_client.py b/src/agent/datastore/postgres_client.py new file mode 100644 index 0000000..eb2ecf6 --- /dev/null +++ b/src/agent/datastore/postgres_client.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manager conversation history return relavant conversation history +based on session_id using postgres database +""" + +from typing import Optional +from sqlalchemy import create_engine, Column, String, DateTime, JSON +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from datetime import datetime +import json +import os + +from src.common.utils import get_config + +# TODO: Move this config to __init__ method +db_user = os.environ.get("POSTGRES_USER") +db_password = os.environ.get("POSTGRES_PASSWORD") +db_name = os.environ.get("POSTGRES_DB") + +settings = get_config() +# Postgres connection URL +DATABASE_URL = f"postgresql://{db_user}:{db_password}@{settings.database.url}/{db_name}?sslmode=disable" + +Base = declarative_base() +engine = create_engine(DATABASE_URL) +Session = sessionmaker(bind=engine) + +class ConversationHistory(Base): + __tablename__ = 'conversation_history' + + session_id = Column(String, primary_key=True) + user_id = Column(String, nullable=True) + last_conversation_time = Column(DateTime) + start_conversation_time = Column(DateTime) + conversation_data = Column(JSON) + +class PostgresClient: + def __init__(self): + self.engine = engine + Base.metadata.create_all(self.engine) + + def store_conversation(self, session_id: str, user_id: Optional[str], conversation_history: list, last_conversation_time: str, start_conversation_time: str): + session = Session() + try: + # Store last_conversation_time and start_conversation_time in datetime format for easy filtering + conversation = ConversationHistory( + session_id=session_id, + user_id=user_id if user_id else None, + last_conversation_time=datetime.fromtimestamp(float(last_conversation_time)), + start_conversation_time=datetime.fromtimestamp(float(start_conversation_time)), + conversation_data=json.dumps(conversation_history) + ) + session.merge(conversation) + session.commit() + except Exception as e: + print(f"Error storing conversation: {e}") + session.rollback() + finally: + session.close() + + def fetch_conversation(self, session_id: str): + session = Session() + try: + conversation = session.query(ConversationHistory).filter_by(session_id=session_id).first() + if conversation: + return { + 'session_id': conversation.session_id, + 'user_id': conversation.user_id, + 'last_conversation_time': conversation.last_conversation_time, + 'conversation_history': json.loads(conversation.conversation_data) + } + return None + except Exception as e: + print(f"Error fetching conversation: {e}") + return None + finally: + session.close() + + def delete_conversation(self, session_id: str): + session = Session() + try: + conversation = session.query(ConversationHistory).filter_by(session_id=session_id).first() + if conversation: + session.delete(conversation) + session.commit() + print(f"Conversation with session_id {session_id} deleted successfully.") + else: + print(f"No conversation found with session_id {session_id}.") + except Exception as e: + print(f"Error deleting conversation: {e}") + session.rollback() + finally: + session.close() + + def is_session(self, session_id: str) -> bool: + session = Session() + try: + exists = session.query(ConversationHistory).filter_by(session_id=session_id).first() is not None + return exists + except Exception as e: + print(f"Error checking session existence: {e}") + return False + finally: + session.close() diff --git a/src/agent/main.py b/src/agent/main.py new file mode 100644 index 0000000..edccafc --- /dev/null +++ b/src/agent/main.py @@ -0,0 +1,500 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from typing import Annotated, TypedDict, Dict +from langgraph.graph.message import AnyMessage, add_messages +from typing import Callable +from langchain_core.messages import ToolMessage, AIMessage, HumanMessage, SystemMessage +from typing import Annotated, Optional, Literal, TypedDict +from langchain_core.prompts.chat import ChatPromptTemplate +from langchain_core.prompts import MessagesPlaceholder +from langgraph.graph import END, StateGraph, START +from langgraph.prebuilt import tools_condition +from langchain_core.runnables import RunnableConfig +from src.agent.tools import ( + structured_rag, get_purchase_history, HandleOtherTalk, ProductValidation, + return_window_validation, update_return, get_recent_return_details, + ToProductQAAssistant, + ToOrderStatusAssistant, + ToReturnProcessing) +from src.agent.utils import get_product_name, create_tool_node_with_fallback, get_checkpointer, canonical_rag +from src.common.utils import get_llm, get_prompts + + +logger = logging.getLogger(__name__) +prompts = get_prompts() +# TODO get the default_kwargs from the Agent Server API +default_llm_kwargs = {"temperature": 0.2, "top_p": 0.7, "max_tokens": 1024} + +# STATE OF THE AGENT +class State(TypedDict): + messages: Annotated[list[AnyMessage], add_messages] + user_id: str + user_purchase_history: Dict + current_product: str + needs_clarification: bool + clarification_type: str + reason: str + +# NODES FOR THE AGENT +def validate_product_info(state: State, config: RunnableConfig): + # This node will take user history and find product name based on query + # If there are multiple name of no name specified in the graph then it will + + # This dict is to populate the user_purchase_history and product details if required + response_dict = {"needs_clarification": False} + if state["user_id"]: + # Update user purchase history based + response_dict.update({"user_purchase_history": get_purchase_history(state["user_id"])}) + + # Extracting product name which user is expecting + product_list = list(set([resp.get("product_name") for resp in response_dict.get("user_purchase_history", [])])) + + # Extract product name from query and filter from database + product_info = get_product_name(state["messages"], product_list) + + product_names = product_info.get("products_from_purchase", []) + product_in_query = product_info.get("product_in_query", "") + if len(product_names) == 0: + reason = "" + if product_in_query: + reason = f"{product_in_query}" + response_dict.update({"needs_clarification": True, "clarification_type": "no_product", "reason": reason}) + return response_dict + elif len(product_names) > 1: + reason = ", ".join(product_names) + response_dict.update({"needs_clarification": True, "clarification_type": "multiple_products", "reason": reason}) + return response_dict + else: + response_dict.update({"current_product": product_names[0]}) + + return response_dict + +async def handle_other_talk(state: State, config: RunnableConfig): + """Handles greetings and queries outside order status, returns, or products, providing polite redirection and explaining chatbot limitations.""" + + prompt = prompts.get("other_talk_template", "") + + prompt = ChatPromptTemplate.from_messages( + [ + ("system", prompt), + ("placeholder", "{messages}"), + ] + ) + + # LLM + llm_settings = config.get('configurable', {}).get("llm_settings", default_llm_kwargs) + llm = get_llm(**llm_settings) + llm = llm.with_config(tags=["should_stream"]) + + # Chain + small_talk_chain = prompt | llm + response = await small_talk_chain.ainvoke(state, config) + + return {"messages": [response]} + + +def create_entry_node(assistant_name: str) -> Callable: + def entry_node(state: State) -> dict: + tool_call_id = state["messages"][-1].tool_calls[0]["id"] + return { + "messages": [ + ToolMessage( + content=f"The assistant is now the {assistant_name}. Reflect on the above conversation between the host assistant and the user." + f" The user's intent is unsatisfied. Use the provided tools to assist the user. Remember, you are {assistant_name}," + " and the booking, update, other other action is not complete until after you have successfully invoked the appropriate tool." + " If the user changes their mind or needs help for other tasks, let the primary host assistant take control." + " Do not mention who you are - just act as the proxy for the assistant.", + tool_call_id=tool_call_id, + ) + ] + } + + return entry_node + +async def ask_clarification(state: State, config: RunnableConfig): + + # Extract the base prompt + base_prompt = prompts.get("ask_clarification")["base_prompt"] + previous_conversation = [m for m in state['messages'] if not isinstance(m, ToolMessage)] + base_prompt = base_prompt.format(previous_conversation=previous_conversation) + + purchase_history = state.get("user_purchase_history", []) + if state["clarification_type"] == "no_product" and state['reason'].strip(): + followup_prompt = prompts.get("ask_clarification")["followup"]["no_product"].format( + reason=state['reason'], + purchase_history=purchase_history + ) + elif not state['reason'].strip(): + followup_prompt = prompts.get("ask_clarification")["followup"]["default"].format(reason=purchase_history) + else: + followup_prompt = prompts.get("ask_clarification")["followup"]["default"].format(reason=state['reason']) + + # Combine base prompt and followup prompt + prompt = f"{base_prompt} {followup_prompt}" + + # LLM + llm_settings = config.get('configurable', {}).get("llm_settings", default_llm_kwargs) + llm = get_llm(**llm_settings) + llm = llm.with_config(tags=["should_stream"]) + + response = await llm.ainvoke(prompt, config) + + return {"messages": [response]} + +async def handle_product_qa(state: State, config: RunnableConfig): + + # Extract the previous_conversation + previous_conversation = [m for m in state['messages'] if not isinstance(m, ToolMessage) and m.content] + message_type_map = { + HumanMessage: "user", + AIMessage: "assistant", + SystemMessage: "system" + } + + # Serialized conversation + get_role = lambda x: message_type_map.get(type(x), None) + previous_conversation_serialized = [{"role": get_role(m), "content": m.content} for m in previous_conversation if m.content] + last_message = previous_conversation_serialized[-1]['content'] + + retireved_content = canonical_rag(query=last_message, conv_history=previous_conversation_serialized) + + # Use the RAG Template to generate the response + base_rag_prompt = prompts.get("rag_template") + rag_prompt = ChatPromptTemplate.from_messages( + [ + ("system", base_rag_prompt), + MessagesPlaceholder("chat_history") + "\n\nCONTEXT: {context}" + ] + ) + rag_prompt = rag_prompt.format(chat_history=previous_conversation, context=retireved_content) + + # LLM + llm_settings = config.get('configurable', {}).get("llm_settings", default_llm_kwargs) + llm = get_llm(**llm_settings) + llm = llm.with_config(tags=["should_stream"]) + + response = await llm.ainvoke(rag_prompt, config) + + return {"messages": [response]} + +class Assistant: + def __init__(self, prompt: str, tools: list): + self.prompt = prompt + self.tools = tools + + async def __call__(self, state: State, config: RunnableConfig): + while True: + + llm_settings = config.get('configurable', {}).get("llm_settings", default_llm_kwargs) + llm = get_llm(**llm_settings) + runnable = self.prompt | llm.bind_tools(self.tools) + last_message = state["messages"][-1] + messages = [] + if isinstance(last_message, ToolMessage) and last_message.name in ["structured_rag", "return_window_validation", "update_return", "get_purchase_history", "get_recent_return_details"]: + gen = runnable.with_config( + tags=["should_stream"], + callbacks=config.get( + "callbacks", [] + ), # <-- Propagate callbacks (Python <= 3.10) + ) + async for message in gen.astream(state): + messages.append(message.content) + result = AIMessage(content="".join(messages)) + else: + result = runnable.invoke(state) + + if not result.tool_calls and ( + not result.content + or isinstance(result.content, list) + and not result.content[0].get("text") + ): + messages = state["messages"] + [("user", "Respond with a real output.")] + state = {**state, "messages": messages} + messages = state["messages"] + [("user", "Respond with a real output.")] + state = {**state, "messages": messages} + else: + break + return {"messages": result} + +# order status Assistant +order_status_prompt_template = prompts.get("order_status_template", "") + +order_status_prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + order_status_prompt_template + ), + ("placeholder", "{messages}"), + ] +) + +order_status_safe_tools = [structured_rag] +order_status_tools = order_status_safe_tools + [ProductValidation] + +# Return Processing Assistant +return_processing_prompt_template = prompts.get("return_processing_template", "") + +return_processing_prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + return_processing_prompt_template + ), + ("placeholder", "{messages}"), + ] +) + +return_processing_safe_tools = [get_recent_return_details, return_window_validation] +return_processing_sensitive_tools = [update_return] +return_processing_tools = return_processing_safe_tools + return_processing_sensitive_tools + [ProductValidation] + +primary_assistant_prompt_template = prompts.get("primary_assistant_template", "") + +primary_assistant_prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + primary_assistant_prompt_template + ), + ("placeholder", "{messages}"), + ] +) + +primary_assistant_tools = [ + HandleOtherTalk, + ToProductQAAssistant, + ToOrderStatusAssistant, + ToReturnProcessing, + ] + +# BUILD THE GRAPH +builder = StateGraph(State) + + +# SUB AGENTS +# Create product_qa Assistant +builder.add_node( + "enter_product_qa", + handle_product_qa, +) + +builder.add_edge("enter_product_qa", END) + +builder.add_node("order_validation", validate_product_info) +builder.add_node("ask_clarification", ask_clarification) + +# Create order_status Assistant +builder.add_node( + "enter_order_status", create_entry_node("Order Status Assistant") +) +builder.add_node("order_status", Assistant(order_status_prompt, order_status_tools)) +builder.add_edge("enter_order_status", "order_status") +builder.add_node( + "order_status_safe_tools", + create_tool_node_with_fallback(order_status_safe_tools), +) + + +def route_order_status( + state: State, +) -> Literal[ + "order_status_safe_tools", + "order_validation", + "__end__" +]: + route = tools_condition(state) + if route == END: + return END + tool_calls = state["messages"][-1].tool_calls + tool_names = [t.name for t in order_status_safe_tools] + do_product_validation = any(tc["name"] == ProductValidation.__name__ for tc in tool_calls) + if do_product_validation: + return "order_validation" + if all(tc["name"] in tool_names for tc in tool_calls): + return "order_status_safe_tools" + return "order_status_sensitive_tools" + +builder.add_edge("order_status_safe_tools", "order_status") +builder.add_conditional_edges("order_status", route_order_status) + +# Create return_processing Assistant +builder.add_node("return_validation", validate_product_info) + +builder.add_node( + "enter_return_processing", + create_entry_node("Return Processing Assistant"), +) +builder.add_node("return_processing", Assistant(return_processing_prompt, return_processing_tools)) +builder.add_edge("enter_return_processing", "return_processing") + +builder.add_node( + "return_processing_safe_tools", + create_tool_node_with_fallback(return_processing_safe_tools), +) +builder.add_node( + "return_processing_sensitive_tools", + create_tool_node_with_fallback(return_processing_sensitive_tools), +) + + +def route_return_processing( + state: State, +) -> Literal[ + "return_processing_safe_tools", + "return_processing_sensitive_tools", + "return_validation", + "__end__", +]: + route = tools_condition(state) + if route == END: + return END + tool_calls = state["messages"][-1].tool_calls + do_product_validation = any(tc["name"] == ProductValidation.__name__ for tc in tool_calls) + if do_product_validation: + return "return_validation" + tool_names = [t.name for t in return_processing_safe_tools] + if all(tc["name"] in tool_names for tc in tool_calls): + return "return_processing_safe_tools" + return "return_processing_sensitive_tools" + + +builder.add_edge("return_processing_sensitive_tools", "return_processing") +builder.add_edge("return_processing_safe_tools", "return_processing") +builder.add_conditional_edges("return_processing", route_return_processing) + + +def user_info(state: State): + return {"user_purchase_history": get_purchase_history(state["user_id"])} + +builder.add_node("fetch_purchase_history", user_info) +builder.add_edge(START, "fetch_purchase_history") +builder.add_edge("ask_clarification", END) + +# Primary assistant +builder.add_node("primary_assistant", Assistant(primary_assistant_prompt, primary_assistant_tools)) +builder.add_node( + "other_talk", handle_other_talk +) + +# Add "primary_assistant_tools", if necessary +def route_primary_assistant( + state: State, +) -> Literal[ + "enter_product_qa", + "enter_order_status", + "enter_return_processing", + "other_talk", + "__end__", +]: + route = tools_condition(state) + if route == END: + return END + tool_calls = state["messages"][-1].tool_calls + if tool_calls: + if tool_calls[0]["name"] == ToProductQAAssistant.__name__: + return "enter_product_qa" + elif tool_calls[0]["name"] == ToOrderStatusAssistant.__name__: + return "enter_order_status" + elif tool_calls[0]["name"] == ToReturnProcessing.__name__: + return "enter_return_processing" + elif tool_calls[0]["name"] == HandleOtherTalk.__name__: + return "other_talk" + raise ValueError("Invalid route") + +builder.add_edge("other_talk", END) + +# The assistant can route to one of the delegated assistants, +# directly use a tool, or directly respond to the user +builder.add_conditional_edges( + "primary_assistant", + route_primary_assistant, + { + "enter_product_qa": "enter_product_qa", + "enter_order_status": "enter_order_status", + "enter_return_processing": "enter_return_processing", + "other_talk":"other_talk", + END: END, + }, +) + + +def is_order_product_valid(state: State) -> Literal[ + "ask_clarification", + "order_status" +]: + """Conditional edge from validation node to decide if we should ask followup questions""" + if state["needs_clarification"] == True: + return "ask_clarification" + return "order_status" + +def is_return_product_valid(state: State) -> Literal[ + "ask_clarification", + "return_processing" +]: + """Conditional edge from validation node to decide if we should ask followup questions""" + if state["needs_clarification"] == True: + return "ask_clarification" + return "return_processing" + +builder.add_conditional_edges( + "order_validation", + is_order_product_valid +) +builder.add_conditional_edges( + "return_validation", + is_return_product_valid +) + +builder.add_edge("fetch_purchase_history", "primary_assistant") + + +# Allow multiple async loop togeather +# This is needed to create checkpoint as it needs async event loop +# TODO: Move graph build into a async function and call that to remove nest_asyncio +import nest_asyncio +nest_asyncio.apply() + +# To run the async main function +import asyncio + +memory = None +pool = None + +# TODO: Remove pool as it's not getting used +# WAR: It's added so postgres does not close it's session +async def get_checkpoint(): + global memory, pool + memory, pool = await get_checkpointer() + +asyncio.run(get_checkpoint()) + +# Compile +graph = builder.compile(checkpointer=memory, + interrupt_before=["return_processing_sensitive_tools"], + #interrupt_after=["ask_human"] + ) + + +try: + # Generate the PNG image from the graph + png_image_data = graph.get_graph(xray=True).draw_mermaid_png() + # Save the image to a file in the current directory + with open("graph_image.png", "wb") as f: + f.write(png_image_data) +except Exception as e: + # This requires some extra dependencies and is optional + logger.info(f"An error occurred: {e}") \ No newline at end of file diff --git a/src/agent/prompt.yaml b/src/agent/prompt.yaml new file mode 100644 index 0000000..ef20e26 --- /dev/null +++ b/src/agent/prompt.yaml @@ -0,0 +1,123 @@ +primary_assistant_template: | + You are a helpful customer support assistant for NVIDIA Gear Store. + Your primary role is to assist the user to answer customer queries. + If a customer asks about his products, his order status, or queries related to processing return, + delegate the task to the appropriate specialized assistant by invoking the corresponding tool. You are not able to make these types of changes yourself. + Only the specialized assistants are given permission to do this for the user. + Provide detailed information to the customer, and always double-check the database before concluding that information is unavailable. + The user is not aware of the different specialized assistants, so do not mention them; just quietly delegate through function calls. + When searching, be persistent. Expand your query bounds if the first search returns no results. + If a search comes up empty, expand your search before giving up. + If the customer asks anything outside the scope of order status, returns, or their products, invoke the `HandleOtherTalk` tool to manage the conversation seamlessly. + The current user id is : {user_id} + +other_talk_template: | + You are the virtual AI assistant built by nv engineers. You are running using NVIDIA NIMs(NVIDIA Inference Microservice) on NVIDIA GPU. + You are responsible for handling greetings, general conversations, and any offbeat or unusual queries. + If a customer greets you with "hi," "hello," or similar expressions, respond warmly and in a welcoming manner. + For off-topic or unusual queries, politely acknowledge the customer's message and gently guide the conversation back to relevant topics where you can provide assistance. + If you're unable to help with a particular request, empathetically explain the chatbot's limitations and offer helpful suggestions on where the customer can find more information. + + Here are some key principles to follow: + - Respond warmly and professionally to greetings like "hi," "hello," or "how are you?" + - For general questions outside your scope, apologize politely and explain the limitations of your system. + - Gently guide the user back to topics related to their orders, returns, or products if possible. + - Provide helpful suggestions for getting more detailed assistance (e.g., contacting customer support). + - Keep your responses short. + - Do not make up any information that you are not sure of. + + + If needed, here are some examples of polite responses to common situations: + - If the user greets: "Hello! How can I assist you today?" + - If the user asks for something outside your capabilities: "I'm here to help with queries about your orders, returns, and NVIDIA products. For other questions, I recommend contacting customer support, who can assist further." + - If the user persists in asking about topics outside your scope: "I understand your question is important, but I'm limited to assisting with order-related matters. Please feel free to reach out to customer support for more detailed help." + + Remember to always be polite, warm, and provide clear guidance to the customer about what you can and cannot assist with. + + +return_processing_template: | + You are a highly intelligent chatbot designed to assist users with queries related to processing a product return request. + At a high level, you handle customer requests for returning orders, perform return, checking return status, and addressing return-related Q&A. + You are provided with tools to validate the return window, and to update the return to the database if it is within the return window and order status is delivered. + Use the following guidelines + Consider only the current product for processing return. + Get the order status and order date from the purchase history below. + When processing a return, ensure the order status is delivered and check if it is within the return window. + Call the update_return tool only if it is within the return window and order status is delivered; otherwise, respond to the user's query based on the current order status and the return_window_validation tool's output. + Always give a complete response to the user. + When searching, be persistent. Expand your query bounds if the first search returns no results. + Always rely on tool outputs to generate responses with complete accuracy. Do not hallucinate. + If insufficient evidence is available, respond formally stating that there is not enough information to provide an answer. + + The current user id is : {user_id} + The current product is : {current_product} + The purchase history is: {user_purchase_history} + +order_status_template: | + You are a highly intelligent chatbot designed to assist users with queries related to their purchase history. + Use the necessary tools to fetch relevant information and answer the query based on that information. + Consider only the current product when processing order status. Ensure that your inputs to the tools include the name of the current product. + If there is valid a tool output, provide a proper response to the query based on it. + Always rely on tool outputs to generate responses with complete accuracy. Do not hallucinate. + If insufficient evidence is available, respond formally stating that there is not enough information to provide an answer. + + The current user id is: {user_id} + The current product is: {current_product} + +ask_clarification: + base_prompt: | + You are a highly intelligent AI assistant, and you are asking a follow-up question regarding product disambiguation. + + The previous conversation: {previous_conversation} + Keep the follow-up question concise. + followup: + no_product: | + Write a follow up question indicating user didn't purchase {reason}. + + Inquire if they are referring to one of the products from their purchase history. For example: + "I don't see {reason} listed in your purchase history. Are you perhaps referring to one of these products instead: {purchase_history}? + default: | + Write a follow up question indicating which one you are referring to among {reason}. + + +get_product_name: + base_prompt: | + You are an AI assistant tasked with extracting the name of a product or item that a user has purchased or is inquiring about from their query. Your goal is to identify the most likely product name based on the context provided. + + Rules: + 1. Extract only one product name. + 2. If no specific product is mentioned, return null. + 3. If no product can be confidently identified, return "null" as the product name. + + User query: {query} + + conversation_prompt: | + Given the conversation between user and assistant, find the product name they are talking about. If no product name is mentioned, return null. + + Conversation: {messages} + + fallback_prompt: | + You are an AI assistant tasked with extracting the name of a product or item that a user has purchased or is inquiring about from their query. Your goal is to identify the most likely product name based on the context provided. + + Rules: + 1. Give priority to the last product discussed. + 2. Extract only one product name. + 3. If no specific product is mentioned, return null. + 4. If no product can be confidently identified, return "null" as the product name. + + User conversation: {messages} + +rag_template: | + You are the virtual AI assistant built by nv engineers for question-answering tasks. + You will respond to the chat history based on the provided context. + For off-topic or unusual queries or questions our of context, politely acknowledge the customer's message and gently guide the conversation back to relevant topics where you can provide assistance. + If you're unable to help with a particular request, empathetically explain the chatbot's limitations and offer helpful suggestions on where the customer can find more information. + Never provide any product suggestion or recommendation to user. Your only goal is to answer users question based on provided context. + + Here are some key principles to follow: + - Respond warmly and professionally + - For general questions outside your scope or context, apologize politely and explain the limitations of your system. + - Do not recommend or suggest product. + - Do not off topic or out of context questions. + + Remember to always be polite, warm, and provide clear guidance to the customer. diff --git a/src/agent/requirements.txt b/src/agent/requirements.txt new file mode 100644 index 0000000..d021e81 --- /dev/null +++ b/src/agent/requirements.txt @@ -0,0 +1,16 @@ +fastapi==0.115.2 +uvicorn[standard]==0.27.1 +starlette==0.40.0 +langchain-nvidia-ai-endpoints==0.2.2 +dataclass-wizard==0.22.3 +langchain==0.2.16 +langgraph==0.2.3 +redis==5.0.8 +psycopg2-binary==2.9.9 +SQLAlchemy==2.0.31 +prometheus_client==0.21.0 +bleach==6.1.0 +psycopg-pool==3.2.2 +langgraph-checkpoint-postgres==1.0.9 +psycopg-binary==3.2.3 +nest-asyncio==1.6.0 \ No newline at end of file diff --git a/src/agent/server.py b/src/agent/server.py new file mode 100644 index 0000000..8ba64d1 --- /dev/null +++ b/src/agent/server.py @@ -0,0 +1,586 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The definition of the Llama Index chain server.""" +import os +from uuid import uuid4 +import logging +from typing import List +import importlib +import bleach +import time +import prometheus_client +import asyncio +import random +import re +from traceback import print_exc + +from fastapi import FastAPI, Request, HTTPException, Response +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from pydantic import BaseModel, Field, validator, constr +from src.agent.cache.session_manager import SessionManager +from src.agent.datastore.datastore import Datastore +from src.agent.utils import remove_state_from_checkpointer + +from langgraph.errors import GraphRecursionError +from langchain_core.messages import ToolMessage +from langgraph.errors import GraphRecursionError + +logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper()) +logger = logging.getLogger(__name__) + +tags_metadata = [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness.", + }, + {"name": "Feedback", "description": "APIs for storing useful information for data flywheel."}, + {"name": "Session Management", "description": "APIs for managing sessions."}, + {"name": "Inference", "description": "Core APIs for interacting with the agent."}, +] + +# create the FastAPI server +app = FastAPI(title="Agent API's for AI Virtual Assistant for Customer Service", + description="This API schema describes all the core agentic endpoints exposed by the AI Virtual Assistant for Customer Service NIM Blueprint", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc", + openapi_tags=tags_metadata, +) + +# Allow access in browser from RAG UI and Storybook (development) +origins = [ + "*" +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +EXAMPLE_DIR = "./" + +# List of fallback responses sent out for any Exceptions from /generate endpoint +FALLBACK_RESPONSES = [ + "Please try re-phrasing, I am likely having some trouble with that question.", + "I will get better with time, please try with a different question.", + "I wasn't able to process your input. Let's try something else.", + "Something went wrong. Could you try again in a few seconds with a different question?", + "Oops, that proved a tad difficult for me, can you retry with another question?" +] + +class Message(BaseModel): + """Definition of the Chat Message type.""" + role: str = Field(description="Role for a message AI, User and System", default="user", max_length=256, pattern=r'[\s\S]*') + content: str = Field(description="The input query/prompt to the pipeline.", default="Hello what can you do?", max_length=131072, pattern=r'[\s\S]*') + + @validator('role') + def validate_role(cls, value): + """ Field validator function to validate values of the field role""" + value = bleach.clean(value, strip=True) + valid_roles = {'user', 'assistant', 'system'} + if value.lower() not in valid_roles: + raise ValueError("Role must be one of 'user', 'assistant', or 'system'") + return value.lower() + + @validator('content') + def sanitize_content(cls, v): + """ Field validator function to santize user populated feilds from HTML""" + v = bleach.clean(v, strip=True) + if not v: # Check for empty string + raise ValueError("Message content cannot be empty.") + return v + +class Prompt(BaseModel): + """Definition of the Prompt API data type.""" + messages: List[Message] = Field(..., description="A list of messages comprising the conversation so far. The roles of the messages must be alternating between user and assistant. The last input message should have role user. A message with the the system role is optional, and must be the very first message if it is present.", max_items=50000) + user_id: str = Field(None, description="A unique identifier representing your end-user.") + session_id: str = Field(..., description="A unique identifier representing the session associated with the response.") + +class ChainResponseChoices(BaseModel): + """ Definition of Chain response choices""" + index: int = Field(default=0, ge=0, le=256, format="int64") + message: Message = Field(default=Message()) + finish_reason: str = Field(default="", max_length=4096, pattern=r'[\s\S]*') + +class ChainResponse(BaseModel): + """Definition of Chain APIs resopnse data type""" + id: str = Field(default="", max_length=100000, pattern=r'[\s\S]*') + choices: List[ChainResponseChoices] = Field(default=[], max_items=256) + session_id: str = Field(None, description="A unique identifier representing the session associated with the response.") + +class DocumentSearch(BaseModel): + """Definition of the DocumentSearch API data type.""" + + query: str = Field(description="The content or keywords to search for within documents.", max_length=131072, pattern=r'[\s\S]*', default="") + top_k: int = Field(description="The maximum number of documents to return in the response.", default=4, ge=0, le=25, format="int64") + +class DocumentChunk(BaseModel): + """Represents a chunk of a document.""" + content: str = Field(description="The content of the document chunk.", max_length=131072, pattern=r'[\s\S]*', default="") + filename: str = Field(description="The name of the file the chunk belongs to.", max_length=4096, pattern=r'[\s\S]*', default="") + score: float = Field(..., description="The relevance score of the chunk.") + +class DocumentSearchResponse(BaseModel): + """Represents a response from a document search.""" + chunks: List[DocumentChunk] = Field(..., description="List of document chunks.", max_items=256) + +class DocumentsResponse(BaseModel): + """Represents the response containing a list of documents.""" + documents: List[constr(max_length=131072, pattern=r'[\s\S]*')] = Field(description="List of filenames.", max_items=1000000, default=[]) + +class HealthResponse(BaseModel): + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +class CreateSessionResponse(BaseModel): + session_id: str = Field(max_length=4096) + +class EndSessionResponse(BaseModel): + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +class DeleteSessionResponse(BaseModel): + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +class FeedbackRequest(BaseModel): + """Definition of the Feedback Request data type.""" + feedback: float = Field(..., description="A unique identifier representing your end-user.", ge=-1.0, le=1.0) + session_id: str = Field(..., description="A unique identifier representing the session associated with the response.") + +class FeedbackResponse(BaseModel): + """Definition of the Feedback Request data type.""" + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +@app.on_event("startup") +def import_example() -> None: + """ + Import the example class from the specified example file. + + """ + + file_location = os.path.join(EXAMPLE_DIR, os.environ.get("EXAMPLE_PATH", "basic_rag/llamaindex")) + + for root, dirs, files in os.walk(file_location): + for file in files: + if file == "main.py": + # Import the specified file dynamically + spec = importlib.util.spec_from_file_location(name="main", location=os.path.join(root, file)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Get the Agent app + app.agent = module + break # Stop the loop once we find and load agent.py + + # Initialize session manager during startup + app.session_manager = SessionManager() + + # Initialize database to store conversation permanently + app.database = Datastore() + +@app.exception_handler(RequestValidationError) +async def request_validation_exception_handler( + request: Request, exc: RequestValidationError +) -> JSONResponse: + return JSONResponse( + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": jsonable_encoder(exc.errors(), exclude={"input"})}) + +@app.get("/health", tags=["Health"], response_model=HealthResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def health_check(): + """ + Perform a Health Check + + Returns 200 when service is up. This does not check the health of downstream services. + """ + + response_message = "Service is up." + return HealthResponse(message=response_message) + + +@app.get("/metrics", tags=["Health"]) +async def get_metrics(): + return Response(content=prometheus_client.generate_latest(), media_type="text/plain") + + +@app.get("/create_session", tags=["Session Management"], response_model=CreateSessionResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def create_session(): + + # Try for fix number of time, if no unique session_id is found raise Error + for _ in range(5): + session_id = str(uuid4()) + + # Ensure session_id created does not exist in cache + if not app.session_manager.is_session(session_id): + # Ensure session_id created does not exist in datastore (permanenet store like postgres) + if not app.database.is_session(session_id): + # Create a session on cache for validation + app.session_manager.create_session(session_id) + return CreateSessionResponse(session_id=session_id) + + raise HTTPException(status_code=500, detail="Unable to generate session_id") + + +@app.get("/end_session", tags=["Session Management"], response_model=EndSessionResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def end_session(session_id): + logger.info(f"Fetching conversation for {session_id} from cache") + session_info = app.session_manager.get_session_info(session_id) + logger.info(f"Session INFO: {session_info}") + if not session_info or not session_info.get("start_conversation_time", None): + logger.info("No conversation found in session") + return EndSessionResponse(message="Session not found. Create session before trying out") + + if session_info.get("last_conversation_time"): + # If there is no conversation history then don't port it to datastore + logger.info(f"Storing conversation for {session_id} in database") + app.database.store_conversation(session_id, session_info.get("user_id"), session_info.get("conversation_hist"), session_info.get("last_conversation_time"), session_info.get("start_conversation_time")) + + # Once the conversation is ended and ported to permanent storage, clear cache with given session_id + logger.info(f"Deleting conversation for {session_id} from cache") + app.session_manager.delete_conversation(session_id) + + return EndSessionResponse(message="Session ended") + + +@app.delete("/delete_session", tags=["Session Management"], response_model=DeleteSessionResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def delete_session(session_id): + logger.info(f"Deleting conversation for {session_id}") + session_info = app.session_manager.get_session_info(session_id) + if not session_info: + logger.info("No conversation found in session") + return DeleteSessionResponse(message="Session info not found") + + logger.info(f"Deleting conversation for {session_id} from cache") + app.session_manager.delete_conversation(session_id) + + logger.info(f"Deleting conversation for {session_id} in database") + app.database.delete_conversation(session_id) + + logger.info(f"Deleting checkpointer for {session_id}") + remove_state_from_checkpointer(session_id) + return EndSessionResponse(message="Session info deleted") + + +def fallback_response_generator(sentence: str, session_id: str = ""): + """Mock response generator to simulate streaming predefined fallback responses.""" + + # Simulate breaking the sentence into chunks (e.g., by word) + sentence_chunks = sentence.split() # Split the sentence by words + resp_id = str(uuid4()) # unique response id for every query + # Send each chunk (word) in the response + for chunk in sentence_chunks: + chain_response = ChainResponse(session_id=session_id, sentiment="") + response_choice = ChainResponseChoices( + index=0, + message=Message(role="assistant", content=f"{chunk} ") + ) + chain_response.id = resp_id + chain_response.choices.append(response_choice) + yield "data: " + str(chain_response.json()) + "\n\n" + + # End with [DONE] response + chain_response = ChainResponse(session_id=session_id, sentiment="") + response_choice = ChainResponseChoices(message=Message(role="assistant", content=" "), finish_reason="[DONE]") + chain_response.id = resp_id + chain_response.choices.append(response_choice) + yield "data: " + str(chain_response.json()) + "\n\n" + + +@app.post( + "/generate", + tags=["Inference"], + response_model=ChainResponse, + responses={ + 500: { + "description": "Internal Server Error", + "content": {"application/json": {"example": {"detail": "Internal server error occurred"}}}, + } + }, +) +async def generate_answer(request: Request, + prompt: Prompt) -> StreamingResponse: + """Generate and stream the response to the provided prompt.""" + + logger.info(f"Input at /generate endpoint of Agent: {prompt.dict()}") + + try: + user_query_timestamp = time.time() + + # Handle invalid session id + if not app.session_manager.is_session(prompt.session_id): + logger.error(f"No session_id created {prompt.session_id}. Please create session id before generate request.") + print_exc() + return StreamingResponse(fallback_response_generator(sentence=random.choice(FALLBACK_RESPONSES), session_id=prompt.session_id), media_type="text/event-stream") + + chat_history = prompt.messages + # The last user message will be the query for the rag or llm chain + last_user_message = next((message.content for message in reversed(chat_history) if message.role == 'user'), None) + + # Normalize the last user input and remove non-ascii characters + last_user_message = re.sub(r'[^\x00-\x7F]+', '', last_user_message) # Remove all non-ascii characters + last_user_message = re.sub(r'[\u2122\u00AE]', '', last_user_message) # Remove standard trademark and copyright symbols + last_user_message = last_user_message.replace("~", "-") + logger.info(f"Normalized user input: {last_user_message}") + + # Keep copy of unmodified query to store in db + user_query = last_user_message + + log_level=os.environ.get('LOGLEVEL', 'INFO').upper() + debug_langgraph = False + if log_level == "DEBUG": + debug_langgraph = True + + recursion_limit = int(os.environ.get('GRAPH_RECURSION_LIMIT', '6')) + + async def response_generator(): + + try: + resp_id = str(uuid4()) + is_exception = False + # Variable to track if this is the first yield + is_first_yield = True + resp_str = "" + last_content = "" + + logger.info(f"Chat History: {app.session_manager.get_conversation(prompt.session_id)}") + config = {"recursion_limit": recursion_limit, + "configurable": {"thread_id": prompt.session_id, "chat_history": app.session_manager.get_conversation(prompt.session_id)}} + # Check for the interrupt + snapshot = await app.agent.graph.aget_state(config) + if not snapshot.next: + input_for_graph = {"messages":[("human", last_user_message)], "user_id": prompt.user_id} + else: + if last_user_message.strip().startswith(("Yes", "yes", "Y", "y")): + # Just continue + input_for_graph = None + else: + last_item = snapshot.values.get("messages")[-1] + if last_item and hasattr(last_item, "tool_calls") and last_item.tool_calls: + input_for_graph = { + "messages": [ + ToolMessage( + tool_call_id=last_item.tool_calls[0]["id"], + content=f"API call denied by user. Reasoning: '{last_user_message}'. Continue assisting, accounting for the user's input.", + ) + ] + } + elif not hasattr(last_item, "tool_calls"): + input_for_graph = {"messages":[("human", last_user_message)], "user_id": prompt.user_id} + else: + input_for_graph = None + try: + function_start_time = time.time() + + async for event in app.agent.graph.astream_events(input_for_graph, version="v2", config=config, debug=debug_langgraph): + kind = event["event"] + tags = event.get("tags", []) + if kind == "on_chain_end" and event['data'].get('output', "") == '__end__': + end_msgs = event['data']['input']['messages'] + last_content = end_msgs[-1].content + if kind == "on_chat_model_stream" and "should_stream" in tags: + content = event["data"]["chunk"].content + resp_str += content + if content: + chain_response = ChainResponse() + response_choice = ChainResponseChoices( + index=0, + message=Message( + role="assistant", + content=content + ) + ) + chain_response.id = resp_id + chain_response.session_id = prompt.session_id + chain_response.choices.append(response_choice) + logger.debug(response_choice) + # Check if this is the first yield + if is_first_yield: + logger.info(f"Execution time until first yield: {time.time() - function_start_time}") + is_first_yield = False + yield "data: " + str(chain_response.json()) + "\n\n" + + # If resp_str is empty after the loop, use the last AI message content + # If there is no Streaming response + if not resp_str and last_content: + chain_response = ChainResponse() + response_choice = ChainResponseChoices( + index=0, + message=Message( + role="assistant", + content=last_content + ) + ) + chain_response.id = resp_id + chain_response.session_id = prompt.session_id + chain_response.choices.append(response_choice) + yield "data: " + str(chain_response.json()) + "\n\n" + resp_str = last_content + logger.debug(f"Using last AI message content as the final response: {last_content}") + + snapshot = await app.agent.graph.aget_state(config) + # If there is a snapshot ask the user for return confirmation + if snapshot.next: + user_confirmation = "Do you approve of the process the return? Type 'y' to continue; otherwise, explain your requested changed." + chain_response = ChainResponse() + response_choice = ChainResponseChoices( + index=0, + message=Message( + role="assistant", + content=user_confirmation + ) + ) + chain_response.id = resp_id + chain_response.session_id = prompt.session_id + chain_response.choices.append(response_choice) + logger.debug(response_choice) + yield "data: " + str(chain_response.json()) + "\n\n" + # Check for the interrupt + + except GraphRecursionError as ge: + logger.error(f"Graph Recursion Error. Error details: {ge}") + is_exception = True + + except AttributeError as attr_err: + # Catch any specific attribute errors and log them + logger.error(f"AttributeError: {attr_err}") + print_exc() + is_exception = True + except asyncio.CancelledError as e: + logger.error(f"Task was cancelled. Details: {e}") + print_exc() + is_exception = True + except Exception as e: + logger.error(f"Sending empty response. Unexpected error in response_generator: {e}") + print_exc() + is_exception = True + + if is_exception: + logger.error("Sending back fallback responses since an exception was raised.") + is_exception = False + for data in fallback_response_generator(sentence=random.choice(FALLBACK_RESPONSES), session_id=prompt.session_id): + yield data + + chain_response = ChainResponse() + # Initialize content with space to overwrite default response + response_choice = ChainResponseChoices( + index=0, + message=Message( + role="assistant", + content=' ' + ), + finish_reason="[DONE]" + ) + + logger.info(f"Conversation saved:\nSession ID: {prompt.session_id}\nQuery: {last_user_message}\nResponse: {resp_str}") + app.session_manager.save_conversation( + prompt.session_id, + prompt.user_id or "", + [ + {"role": "user", "content": user_query, "timestamp": f"{user_query_timestamp}"}, + {"role": "assistant", "content": resp_str, "timestamp": f"{time.time()}"}, + ], + ) + + chain_response.id = resp_id + chain_response.session_id = prompt.session_id + chain_response.choices.append(response_choice) + logger.debug(response_choice) + yield "data: " + str(chain_response.json()) + "\n\n" + + return StreamingResponse(response_generator(), media_type="text/event-stream") + + # Catch any unhandled exceptions + except asyncio.CancelledError as e: + # Handle the cancellation gracefully + logger.error("Unhandled Server interruption before response completion. Details: {e}") + print_exc() + return StreamingResponse(fallback_response_generator(sentence=random.choice(FALLBACK_RESPONSES), session_id=prompt.session_id), media_type="text/event-stream") + except Exception as e: + logger.error(f"Unhandled Error from /generate endpoint. Error details: {e}") + print_exc() + return StreamingResponse(fallback_response_generator(sentence=random.choice(FALLBACK_RESPONSES), session_id=prompt.session_id), media_type="text/event-stream") + + +@app.post("/feedback/response", tags=["Feedback"], response_model=FeedbackResponse, responses={ + 404: { + "description": "Session Not Found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def store_last_response_feedback( + request: Request, + feedback: FeedbackRequest, +) -> FeedbackResponse: + """Store user feedback for the last response in a conversation session.""" + try: + logger.info(f"Storing user feedback for last response for session {feedback.session_id}") + app.session_manager.response_feedback(feedback.session_id, feedback.feedback) + return FeedbackResponse(message="Response feedback saved successfully") + except Exception as e: + logger.error(f"Error in GET /feedback/response endpoint. Error details: {e}") + return FeedbackResponse(message="Failed to store response feedback") diff --git a/src/agent/tools.py b/src/agent/tools.py new file mode 100644 index 0000000..55ae3e1 --- /dev/null +++ b/src/agent/tools.py @@ -0,0 +1,285 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from pydantic import BaseModel, Field +import requests +from datetime import datetime, timedelta +from functools import lru_cache +from langchain_core.tools import tool +import psycopg2 +import psycopg2.extras +from urllib.parse import urlparse +import logging + +from src.common.utils import get_prompts, get_config + +structured_rag_uri = os.getenv('STRUCTURED_RAG_URI', 'http://structured-retriever:8081') +structured_rag_search = f"{structured_rag_uri}/search" + +prompts = get_prompts() +logger = logging.getLogger(__name__) + +@tool +@lru_cache +def structured_rag(query: str, user_id: str) -> str: + """Use this for answering personalized queries about orders, returns, refunds, and account-specific issues.""" + entry_doc_search = {"query": query, "top_k": 4, "user_id": user_id} + aggregated_content = "" + try: + response = requests.post(structured_rag_search, json=entry_doc_search) + # Extract and aggregate the content + logger.info(f"Actual Structured Response : {response}") + if response.status_code != 200: + raise ValueError(f"Error while retireving docs: {response.json()}") + + aggregated_content = "\n".join(chunk["content"] for chunk in response.json().get("chunks", [])) + # Check if aggregated_content contains the specific phrase in a case-insensitive manner + if any(x in aggregated_content.lower() for x in ["no records found", "error:"]): + raise ValueError("No records found for the specified criteria.") + return aggregated_content + except Exception as e: + logger.info(f"Some error within the structured_rag {e}, sending purchase_history") + return get_purchase_history(user_id) + + +@tool +@lru_cache +def get_purchase_history(user_id: str) -> str: + """Retrieves the recent return and order details for a user, + including order ID, product name, status, relevant dates, quantity, and amount.""" + + SQL_QUERY = f""" + SELECT order_id, product_name, order_date, order_status, quantity, order_amount, return_status, + return_start_date, return_received_date, return_completed_date, return_reason, notes + FROM public.customer_data + WHERE customer_id={user_id} + ORDER BY order_date DESC + LIMIT 15; + """ + + app_database_url = get_config().database.url + + # Parse the URL + parsed_url = urlparse(f"//{app_database_url}", scheme='postgres') + + # Extract host and port + host = parsed_url.hostname + port = parsed_url.port + + db_params = { + 'dbname': os.getenv("CUSTOMER_DATA_DB",'customer_data'), + 'user': os.getenv('POSTGRES_USER', None), + 'password': os.getenv('POSTGRES_PASSWORD', None), + 'host': host, + 'port': port + } + + # Using context manager for connection and cursor + with psycopg2.connect(**db_params) as conn: + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute(SQL_QUERY) + result = cur.fetchall() + + # Returning result as a list of dictionaries + return [dict(row) for row in result] + + +@tool +@lru_cache +def get_recent_return_details(user_id: str) -> str: + """Retrieves the recent return details for a user, including order ID, product name, return status, and relevant dates.""" + + return get_purchase_history(user_id) + + +@tool +@lru_cache +def return_window_validation(order_date: str) -> str: + """Use this to check the return window for validation. Use 'YYYY-MM-DD' for the order date.""" + + return_window_time=os.environ.get('RETURN_WINDOW_THRESHOLD_DAYS', 15) + try: + # Parse the order date + order_date = datetime.strptime(order_date, "%Y-%m-%d") + + # Get today's date + today = os.environ.get('RETURN_WINDOW_CURRENT_DATE', "") + + if today: + today = datetime.strptime(today, "%Y-%m-%d") + else: + today = datetime.now() + + # Parse the return window time + return_days = int(return_window_time) + + # Calculate the return window end date + return_window_end = order_date + timedelta(days=return_days) + + # Check if the product is within the return window + if today <= return_window_end: + days_left = (return_window_end - today).days + return f"The product is eligible for return. {days_left} day(s) left in the return window." + else: + days_passed = (today - return_window_end).days + return f"The return window has expired. It ended {days_passed} day(s) ago." + except ValueError: + return "Invalid date format. Please use 'YYYY-MM-DD' for the order date." + +@tool +@lru_cache +def update_return(user_id: str, current_product: str, order_id: str) -> str: + """Use this to update return status in the database.""" + + # Query to retrieve the order details + SELECT_QUERY = f""" + SELECT order_id, product_name, order_date, order_status + FROM public.customer_data + WHERE customer_id='{user_id}' AND product_name='{current_product}' AND order_id='{order_id}' + ORDER BY order_date DESC + LIMIT 1; + """ + + # Query to update the return_status + UPDATE_QUERY = f""" + UPDATE public.customer_data + SET return_status = 'Requested' + WHERE customer_id='{user_id}' AND product_name='{current_product}' AND order_id='{order_id}'; + """ + + app_database_url = get_config().database.url + + # Parse the URL + parsed_url = urlparse(f"//{app_database_url}", scheme='postgres') + + # Extract host and port + host = parsed_url.hostname + port = parsed_url.port + + db_params = { + 'dbname': os.getenv("CUSTOMER_DATA_DB",'customer_data'), + 'user': os.getenv('POSTGRES_USER', None), + 'password': os.getenv('POSTGRES_PASSWORD', None), + 'host': host, + 'port': port + } + + # Using context manager for connection and cursor + with psycopg2.connect(**db_params) as conn: + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + # Execute the SELECT query to verify the order details + cur.execute(SELECT_QUERY) + result = cur.fetchone() + + # If the order exists, update the return status + if result: + cur.execute(UPDATE_QUERY) + conn.commit() # Commit the transaction to apply the update + return f"Return status for order_id {order_id} has been updated to 'Requested'." + else: + return f"No matching order found for user_id {user_id}, product_name {current_product}, and order_id {order_id}." + +class ToProductQAAssistant(BaseModel): + """ + Transfers work to a specialized assistant to handle Product QA. + Answers generic queries about products, including their descriptions, specifications, warranties, usage instructions, and troubleshooting issues. + Can also address queries based on product manuals, product catalogs, FAQs, policy documents, and general product-related inquiries. + Can also answer queries about the NVIDIA Gear Store's product offerings, policies, order management, shipping information, payment methods, returns, and customer service contacts. + """ + query: str = Field( + description="The question or issue related to the product. This can involve asking about product specifications, usage guidelines, troubleshooting, warranty details, or other product-related concerns." + ) + + class Config: + json_schema_extra = { + "example": { + "query": "What is the warranty period for this model, and does it cover screen burn-in issues?" + } + } + +class ToOrderStatusAssistant(BaseModel): + """ + Delegates queries specifically related to orders or purchase history to a specialized assistant. + This assistant handles inquiries regarding Order ID, Order Date, Quantity, Order Amount, Order Status, + and any other questions related to the user's purchase history. + """ + + query: str = Field( + description="The specific query regarding the order or purchase history, such as order status, delivery updates, or historical purchase information." + ) + user_id: str = Field( + description="The unique identifier of the user." + ) + + class Config: + json_schema_extra = { + "example": { + "query": "What is the current status of my order?", + "user_id": "1" + }, + "example 2": { + "query": "How many items were ordered on 2024-10-15?", + "user_id": "2" + } + } + +class ToReturnProcessing(BaseModel): + """ + Transfers work to a specialized assistant which handles processing of a product return request. + This assistant handles inquiries regarding return transactions, including return status, relevant dates, + reasons for return, notes, and any other questions related to return processing. + """ + + query: str = Field( + description="The specific return-related query, such as the status of the return, refund details, or return policy." + ) + user_id: str = Field( + description="The unique identifier of the user requesting the return." + ) + + class Config: + json_schema_extra = { + "example": { + "query": "I wanted to return my product?", + "user_id": "1" + } + } + +class HandleOtherTalk(BaseModel): + """Handles greetings and other absurd queries by offering polite redirection and clearly explaining the limitations of the chatbot.""" + + message: str # The message sent by the customer. + + class Config: + json_schema_extra = { + "example": { + "message": "Hello", + } + } + +class ProductValidation(BaseModel): + """ + Utilize this to identify the specific order or product the user is referring to in the conversation, + especially when the current product is unclear or unknown. + """ + + message: str # The message sent by the customer. + + class Config: + json_schema_extra = { + "example": { + "message": "What is the order status for my RTX", + } + } \ No newline at end of file diff --git a/src/agent/utils.py b/src/agent/utils.py new file mode 100644 index 0000000..18640ce --- /dev/null +++ b/src/agent/utils.py @@ -0,0 +1,242 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +import os +import logging +from typing import Dict +from pydantic import BaseModel, Field +from urllib.parse import urlparse + +import requests + +from psycopg_pool import AsyncConnectionPool +from psycopg.rows import dict_row +import psycopg2 + +from src.common.utils import get_llm, get_prompts, get_config +from langchain_core.prompts.chat import ChatPromptTemplate +from langchain_core.messages import HumanMessage +from langchain_core.messages import ToolMessage +from langchain_core.runnables import RunnableLambda +from langgraph.checkpoint.memory import MemorySaver +from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver +from langgraph.prebuilt import ToolNode + +prompts = get_prompts() +logger = logging.getLogger(__name__) + +# TODO get the default_kwargs from the Agent Server API +default_llm_kwargs = {"temperature": 0, "top_p": 0.7, "max_tokens": 1024} + +canonical_rag_url = os.getenv('CANONICAL_RAG_URL', 'http://unstructured-retriever:8081') +canonical_rag_search = f"{canonical_rag_url}/search" + +def get_product_name(messages, product_list) -> Dict: + """Given the user message and list of product find list of items which user might be talking about""" + + # First check product name in query + # If it's not in query, check in conversation + # Once the product name is known we will search for product name from database + # We will return product name from list and actual name detected. + + llm = get_llm(**default_llm_kwargs) + + class Product(BaseModel): + name: str = Field(..., description="Name of the product talked about.") + + prompt_text = prompts.get("get_product_name")["base_prompt"] + prompt = ChatPromptTemplate.from_messages( + [ + ("system", prompt_text), + ] + ) + llm = llm.with_structured_output(Product) + + chain = prompt | llm + # query to be used for document retrieval + # Get the last human message instead of messages[-2] + last_human_message = next((m.content for m in reversed(messages) if isinstance(m, HumanMessage)), None) + response = chain.invoke({"query": last_human_message}) + + product_name = response.name + + # Check if product name is in query + if product_name == 'null': + + # Check for produt name in user conversation + fallback_prompt_text = prompts.get("get_product_name")["fallback_prompt"] + prompt = ChatPromptTemplate.from_messages( + [ + ("system", fallback_prompt_text), + ] + ) + + llm = get_llm(**default_llm_kwargs) + llm = llm.with_structured_output(Product) + + chain = prompt | llm + # query to be used for document retrieval + response = chain.invoke({"messages": messages}) + + product_name = response.name + # Check if it's partial name exists or not + if product_name == 'null': + return {} + + def filter_products_by_name(name, products): + # TODO: Replace this by llm call to check if that can take care of cases like + # spelling mistakes or words which are seperated + # TODO: Directly make sql query with wildcard + name_lower = name.lower() + + # Check for exact match first + exact_match = [product for product in products if product.lower() == name_lower] + if exact_match: + return exact_match + + # If no exact match, fall back to partial matches + name_parts = [part for part in re.split(r'\s+', name_lower) if part.lower() != 'nvidia'] + # Match only if all parts of the search term are found in the product name + matching_products = [ + product for product in products + if all(part in product.lower() for part in name_parts if part) + ] + + return matching_products + + matching_products = filter_products_by_name(product_name, product_list) + + return { + "product_in_query": product_name, + "products_from_purchase": list(set([product for product in matching_products])) + } + + +def handle_tool_error(state) -> dict: + error = state.get("error") + tool_calls = state["messages"][-1].tool_calls + return { + "messages": [ + ToolMessage( + content=f"Error: {repr(error)}\n please fix your mistakes.", + tool_call_id=tc["id"], + ) + for tc in tool_calls + ] + } + + +def create_tool_node_with_fallback(tools: list) -> dict: + return ToolNode(tools).with_fallbacks( + [RunnableLambda(handle_tool_error)], exception_key="error" + ) + + +async def get_checkpointer() -> tuple: + settings = get_config() + + if settings.checkpointer.name == "postgres": + print(f"Using {settings.checkpointer.name} hosted on {settings.checkpointer.url} for checkpointer") + db_user = os.environ.get("POSTGRES_USER") + db_password = os.environ.get("POSTGRES_PASSWORD") + db_name = os.environ.get("POSTGRES_DB") + db_uri = f"postgresql://{db_user}:{db_password}@{settings.checkpointer.url}/{db_name}?sslmode=disable" + connection_kwargs = { + "autocommit": True, + "prepare_threshold": 0, + "row_factory": dict_row, + } + + # Initialize PostgreSQL checkpointer + pool = AsyncConnectionPool( + conninfo=db_uri, + min_size=2, + kwargs=connection_kwargs, + ) + checkpointer = AsyncPostgresSaver(pool) + await checkpointer.setup() + return checkpointer, pool + elif settings.checkpointer.name == "inmemory": + print(f"Using MemorySaver as checkpointer") + return MemorySaver(), None + else: + raise ValueError(f"Only inmemory and postgres is supported chckpointer type") + + +def remove_state_from_checkpointer(session_id): + + settings = get_config() + if settings.checkpointer.name == "postgres": + # Handle cleanup for PostgreSQL checkpointer + # Currently, there is no langgraph checkpointer API to remove data directly. + # The following tables are involved in storing checkpoint data: + # - checkpoint_blobs + # - checkpoint_writes + # - checkpoints + # Note: checkpoint_migrations table can be skipped for deletion. + try: + app_database_url = settings.checkpointer.url + + # Parse the URL + parsed_url = urlparse(f"//{app_database_url}", scheme='postgres') + + # Extract host and port + host = parsed_url.hostname + port = parsed_url.port + + # Connect to your PostgreSQL database + connection = psycopg2.connect( + dbname=os.getenv('POSTGRES_DB', None), + user=os.getenv('POSTGRES_USER', None), + password=os.getenv('POSTGRES_PASSWORD', None), + host=host, + port=port + ) + cursor = connection.cursor() + + # Execute delete commands + cursor.execute("DELETE FROM checkpoint_blobs WHERE thread_id = %s", (session_id,)) + cursor.execute("DELETE FROM checkpoint_writes WHERE thread_id = %s", (session_id,)) + cursor.execute("DELETE FROM checkpoints WHERE thread_id = %s", (session_id,)) + + # Commit the changes + connection.commit() + logger.info(f"Deleted rows with thread_id: {session_id}") + + except Exception as e: + logger.info(f"Error occurred while deleting data from checkpointer: {e}") + # Optionally rollback if needed + if connection: + connection.rollback() + finally: + # Close the cursor and connection + if cursor: + cursor.close() + if connection: + connection.close() + else: + # For other supported checkpointer(i.e. inmemory) we don't need cleanup + pass + +def canonical_rag(query: str, conv_history: list) -> str: + """Use this for answering generic queries about products, specifications, warranties, usage, and issues.""" + + entry_doc_search = {"query": query, "top_k": 4, "conv_history": conv_history} + response = requests.post(canonical_rag_search, json=entry_doc_search).json() + + # Extract and aggregate the content + aggregated_content = "\n".join(chunk["content"] for chunk in response.get("chunks", [])) + + return aggregated_content \ No newline at end of file diff --git a/src/analytics/Dockerfile b/src/analytics/Dockerfile new file mode 100644 index 0000000..5668ef2 --- /dev/null +++ b/src/analytics/Dockerfile @@ -0,0 +1,46 @@ +ARG BASE_IMAGE_URL=nvcr.io/nvidia/base/ubuntu +ARG BASE_IMAGE_TAG=22.04_20240212 + +FROM ${BASE_IMAGE_URL}:${BASE_IMAGE_TAG} + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV DEBIAN_FRONTEND noninteractive + +# Install required ubuntu packages for setting up python 3.10 +RUN apt update && \ + apt install -y curl software-properties-common && \ + add-apt-repository ppa:deadsnakes/ppa && \ + apt update && apt install -y python3.10 && \ + apt-get clean + +# Install pip for python3.10 +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10 + +RUN rm -rf /var/lib/apt/lists/* + +# Uninstall build packages +RUN apt autoremove -y curl software-properties-common + +# Download the sources of apt packages within the container for standard legal compliance +RUN sed -i 's/# deb-src/deb-src/g' /etc/apt/sources.list +RUN apt update +# xz-utils is needed to pull the source and unpack them correctly +RUN apt install xz-utils -y +RUN mkdir -p /legal/source +WORKDIR /legal/source +# Read installed packages, strip all but the package names, pipe to 'apt source' to download respective packages +RUN apt list --installed | grep -i installed | sed 's|\(.*\)/.*|\1|' | xargs apt source --download-only +# The source is saved in directories as well as tarballs in the current dir +RUN rm xz-utils* +COPY LICENSE-3rd-party.txt /legal/ + +# Install common dependencies and copy common code +COPY src/common /opt/src/common + +# Copy and Install analytics specific modules +COPY src/analytics /opt/src/analytics +RUN --mount=type=bind,source=src/analytics/requirements.txt,target=/opt/analytics/requirements.txt \ + pip3 install --no-cache-dir -r /opt/analytics/requirements.txt + +WORKDIR /opt +ENTRYPOINT ["uvicorn", "src.analytics.server:app"] diff --git a/src/analytics/README.md b/src/analytics/README.md new file mode 100644 index 0000000..0fd5ba8 --- /dev/null +++ b/src/analytics/README.md @@ -0,0 +1,7 @@ +## Analytics Microservice Configuration + +### Data Persistence Configuration +You can control whether the application stores data in the database using the `PERSIST_DATA` environment variable. + +Default Value: true (data will be persisted). +Set to false: Data will not be stored in the database. diff --git a/src/analytics/datastore/__init__.py b/src/analytics/datastore/__init__.py new file mode 100644 index 0000000..a08b2c2 --- /dev/null +++ b/src/analytics/datastore/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/analytics/datastore/postgres_client.py b/src/analytics/datastore/postgres_client.py new file mode 100644 index 0000000..7eec1ad --- /dev/null +++ b/src/analytics/datastore/postgres_client.py @@ -0,0 +1,357 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manager conversation history return relavant conversation history +based on session_id using postgres database +""" + +from typing import Optional, List +from sqlalchemy import create_engine, Column, String, DateTime, JSON, Float +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from datetime import datetime, timedelta +import psycopg2.extras +import psycopg2 +import logging +import json +import os + +from src.common.utils import get_config + +# TODO: Move this config to __init__ method +db_user = os.environ.get("POSTGRES_USER") +db_password = os.environ.get("POSTGRES_PASSWORD") +db_name = os.environ.get("POSTGRES_DB") + +logger = logging.getLogger(__name__) + +settings = get_config() +# Postgres connection URL +DATABASE_URL = f"postgresql://{db_user}:{db_password}@{settings.database.url}/{db_name}?sslmode=disable" + +Base = declarative_base() +engine = create_engine(DATABASE_URL) +Session = sessionmaker(bind=engine) + +class ConversationHistory(Base): + __tablename__ = 'conversation_history' + + session_id = Column(String, primary_key=True) + user_id = Column(String, nullable=True) + last_conversation_time = Column(DateTime) + start_conversation_time = Column(DateTime) + conversation_data = Column(JSON) + + +class Summary(Base): + __tablename__ = 'summary' + + session_id = Column(String, primary_key=True) + start_time = Column(String) + end_time = Column(String) + sentiment = Column(String) + summary = Column(String) + conversation_data = Column(JSON) + +class Feedback(Base): + __tablename__ = 'feedback' + + session_id = Column(String, primary_key=True) + sentiment = Column(Float) # Store feedback of sentiment + summary = Column(Float) # Store feedback of summary + session = Column(Float) # Store feedback of session + +class PostgresClient: + def __init__(self): + self.engine = engine + Base.metadata.create_all(self.engine) + + def store_conversation(self, session_id: str, user_id: Optional[str], conversation_history: list, last_conversation_time: str, start_conversation_time: str): + session = Session() + try: + # Store last_conversation_time and start_conversation_time in datetime format for easy filtering + conversation = ConversationHistory( + session_id=session_id, + user_id=user_id if user_id else None, + last_conversation_time=datetime.fromtimestamp(float(last_conversation_time)), + start_conversation_time=datetime.fromtimestamp(float(start_conversation_time)), + conversation_data=json.dumps(conversation_history) + ) + session.merge(conversation) + session.commit() + except Exception as e: + logger.error(f"Error storing conversation: {e}") + session.rollback() + finally: + session.close() + + + def get_conversation(self, session_id: str): + session = Session() + try: + conversation = session.query(ConversationHistory).filter_by(session_id=session_id).first() + if conversation: + return json.loads(conversation.conversation_data) + raise ValueError(f"Session with ID {session_id} not found") + + except ValueError as ve: + # Log the ValueError and propagate it + logger.warning(f"{ve}") + raise # Propagate the ValueError to the caller + except Exception as e: + logger.error(f"Error fetching conversation: {e}") + raise + finally: + session.close() + + + def fetch_conversation(self, session_id: str): + session = Session() + try: + conversation = session.query(ConversationHistory).filter_by(session_id=session_id).first() + if conversation: + return { + 'session_id': conversation.session_id, + 'user_id': conversation.user_id, + 'last_conversation_time': conversation.last_conversation_time, + 'conversation_history': json.loads(conversation.conversation_data) + } + return None + except Exception as e: + logger.warning(f"Error fetching conversation: {e}") + return None + finally: + session.close() + + + def is_session(self, session_id: str) -> bool: + session = Session() + try: + # Query to check if the session_id exists + exists = session.query(ConversationHistory.session_id).filter_by(session_id=session_id).first() is not None + return exists + except Exception as e: + logger.info(f"Error checking session: {e}") + return False + finally: + session.close() + + + def list_sessions_for_user(self, user_id: str): + session = Session() + try: + # Query to get all session IDs for the user + conversations = session.query(ConversationHistory).filter_by(user_id=user_id).all() + + result = [] + for conv in conversations: + conversation_hist = json.loads(conv.conversation_data) + if conversation_hist: + start_time = datetime.fromtimestamp(float(conversation_hist[0].get("timestamp"))) if conversation_hist[0].get("timestamp") else None + end_time = datetime.fromtimestamp(float(conversation_hist[-1].get("timestamp"))) if conversation_hist[-1].get("timestamp") else None + else: + start_time = None + end_time = None + + result.append({ + "session_id": conv.session_id, + "start_time": start_time, + "end_time": end_time + }) + + return result + except Exception as e: + logger.info(f"Error listing sessions for user: {e}") + return [] + finally: + session.close() + + + def save_summary_and_sentiment(self, session_id, session_info): + """Save the summary, sentiment in PostgreSQL""" + session = Session() + try: + summary = Summary( + session_id=session_id, + start_time=session_info.get("start_time"), + end_time=session_info.get("end_time"), + sentiment=session_info.get("sentiment"), + summary=session_info.get("summary"), + conversation_data=json.dumps(session_info.get("messages")) + ) + session.merge(summary) + session.commit() + except Exception as e: + logger.info(f"Error saving summary and sentiment: {e}") + session.rollback() + finally: + session.close() + + def get_session_summary_and_sentiment(self, session_id): + """Retrieve summary and sentiment from PostgreSQL or Redis""" + session = Session() + try: + # Check PostgreSQL first + summary = session.query(Summary).filter_by(session_id=session_id).first() + if summary: + return { + "summary": summary.summary, + "sentiment": summary.sentiment, + "start_time": summary.start_time, + "end_time": summary.end_time + } + return {} + except Exception as e: + logger.info(f"Error retrieving session summary and sentiment: {e}") + return {} + finally: + session.close() + + + def save_query_sentiment(self, session_id, session_info): + """Save query sentiment in PostgreSQL""" + session = Session() + try: + query_sentiment = Summary( + session_id=session_id, + start_time=session_info.get("start_time"), + end_time=session_info.get("end_time"), + conversation_data=json.dumps(session_info.get("messages")) + ) + session.merge(query_sentiment) + session.commit() + except Exception as e: + logger.info(f"Error saving query sentiment: {e}") + session.rollback() + finally: + session.close() + + def get_query_sentiment(self, session_id): + """Retrieve query sentiment from PostgreSQL""" + session = Session() + try: + query_sentiment = session.query(Summary).filter_by(session_id=session_id).first() + if query_sentiment.conversation_data: + return { + "messages": json.loads(query_sentiment.conversation_data), + "start_time": query_sentiment.start_time, + "end_time": query_sentiment.end_time + } + return {} + except Exception as e: + logger.info(f"Error retrieving query sentiment: {e}") + return {} + finally: + session.close() + + + def save_sentiment_feedback(self, session_id: str, sentiment_feedback: float): + """Retrieve query sentiment from PostgreSQL""" + session = Session() + try: + feedback = Feedback( + session_id=session_id, + sentiment=sentiment_feedback, + ) + session.merge(feedback) + session.commit() + except Exception as e: + logger.info(f"Error while saving sentiment feedback : {e}") + finally: + session.close() + + def save_summary_feedback(self, session_id: str, summary_feedback: float): + session = Session() + try: + feedback = Feedback( + session_id=session_id, + summary=summary_feedback, + ) + session.merge(feedback) + session.commit() + except Exception as e: + logger.info(f"Error while saving sentiment feedback : {e}") + finally: + session.close() + + def save_session_feedback(self, session_id: str, session_feedback: float): + session = Session() + try: + feedback = Feedback( + session_id=session_id, + session=session_feedback, + ) + session.merge(feedback) + session.commit() + except Exception as e: + logger.info(f"Error while saving session feedback : {e}") + return {} + finally: + session.close() + + def get_purchase_history(self, user_id: str) -> List[str]: + """Use this to retrieve the user's purchase history.""" + + # TODO: Add filter logic based on time. Like product pruchased in last 5 days + SQL_QUERY = f""" + SELECT * + FROM customer_data + WHERE customer_id={user_id}; + """ + host, port = settings.database.url.split(":") + + db_params = { + 'dbname': os.environ.get("CUSTOMER_DATA_DB"), + 'user': db_user, + 'password': db_password, + 'host': host, + 'port': port + } + + # Using context manager for connection and cursor + with psycopg2.connect(**db_params) as conn: + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute(SQL_QUERY) + result = cur.fetchall() + + # Returning result as a list of dictionaries + return [dict(row) for row in result] + + def get_conversations_in_last_h_hours(self, hours: int): + # Calculate the time threshold + threshold_time = datetime.now() - timedelta(hours=hours) + + # Create a session + session = Session() + + # Query conversations that occurred in the last h hours + conversations = session.query(ConversationHistory).filter( + ConversationHistory.last_conversation_time >= threshold_time + ).all() + + # Close the session + session.close() + + result = [] + + for conv in conversations: + result.append({ + "session_id": conv.session_id, + "user_id": conv.user_id, + "start_conversation_time": conv.start_conversation_time, + "last_conversation_time": conv.last_conversation_time + }) + return result diff --git a/src/analytics/datastore/redis_client.py b/src/analytics/datastore/redis_client.py new file mode 100644 index 0000000..18277fa --- /dev/null +++ b/src/analytics/datastore/redis_client.py @@ -0,0 +1,194 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manager conversation history return relavant conversation history +based on session_id using redis +""" + +import json +import time +from datetime import datetime +from typing import Any, Dict, List, Optional + +import redis +from src.common.utils import get_config + +# To store and fetch conversation we use database 0 +DEFAULT_DB_CONVERSATION = "0" +# To store sentiment and summary we use database 1 +DEFAULT_DB_SUMMARY = "1" +# To store feedback we use database 2 +DEFAULT_DB_FEEDBACK = "2" + + +class RedisClient: + def __init__(self) -> None: + host, port = get_config().database.url.split(":") + # db = config.get("db", None) or DEFAULT_DB + print(f"Host: {host}, Port: {port}") + # Redis client to get and store conversation + self.redis_client_conversation = redis.Redis( + host=host, port=port, db=DEFAULT_DB_CONVERSATION, decode_responses=True + ) + # Redis client to get and store summary + self.redis_client_summary = redis.Redis( + host=host, port=port, db=DEFAULT_DB_SUMMARY, decode_responses=True + ) + self.redis_client_feedback = redis.Redis( + host=host, port=port, db=DEFAULT_DB_FEEDBACK, decode_responses=True + ) + + + def get_conversation(self, session_id: str) -> List: + """Retrieve the entire conversation history from Redis as a list""" + + conversation_hist = self.redis_client_conversation.lrange( + f"{session_id}:conversation_hist", 0, -1 + ) + return [json.loads(conv) for conv in conversation_hist] + + + def get_k_conversation(self, session_id: str, k_turn: Optional[int] = None) -> List: + """Retrieve the last k conversations from Redis""" + + # TODO: Evaluate this implementation + if k_turn is None: + k_turn = -1 + conversation_hist = self.redis_client_conversation.lrange( + f"{session_id}:conversation_hist", -k_turn, -1 + ) + return [json.loads(conv) for conv in conversation_hist] + + + def save_conversation( + self, session_id: str, user_id: Optional[str], conversation: List + ) -> bool: + try: + # Store each conversation entry as a JSON string in a Redis list + for conv in conversation: + self.redis_client_conversation.rpush( + f"{session_id}:conversation_hist", json.dumps(conv) + ) + + # Store user_id and last conversation time as separate keys + if user_id: + self.redis_client_conversation.set(f"{session_id}:user_id", user_id) + self.redis_client_conversation.set( + f"{session_id}:last_conversation_time", f"{time.time()}" + ) + + return True + except Exception as e: + print(f"Failed to ingest document due to exception {e}") + return False + + + def is_session(self, session_id: str) -> bool: + """Check if session_id already exist in database""" + return self.redis_client_conversation.exists(session_id) + + + def list_sessions_for_user(self, user_id) -> Dict[str, Any]: + """ + List all session IDs for a given user ID along with start and end times. + + Parameters: + user_id (str): The user ID to filter sessions by. + + Returns: + list: A list of dictionaries containing session ID, start time, and end time. + """ + sessions = [] + # TODO: Optimize this, instead of traversing over all values + # we can maintain a another list with user: [session] mapping + for key in self.redis_client_conversation.scan_iter("*:user_id"): + if self.redis_client_conversation.get(key) == user_id: + session_id = key.split(":")[0] + conversation_hist = self.redis_client_conversation.lrange( + f"{session_id}:conversation_hist", 0, -1 + ) + + # Convert conversation history from JSON strings to dictionaries + conversation_hist = [json.loads(conv) for conv in conversation_hist] + + sessions.append( + { + "session_id": session_id, + "start_time": ( + datetime.fromtimestamp( + float(conversation_hist[0].get("timestamp")) + ) + if conversation_hist + else None + ), + "end_time": ( + datetime.fromtimestamp( + float(conversation_hist[-1].get("timestamp")) + ) + if conversation_hist + else None + ), + } + ) + return sessions + + + def get_session_summary_and_sentiment(self, session_id): + session_info = {} + if self.redis_client_summary.exists(f"{session_id}:summary"): + session_info["summary"] = self.redis_client_summary.get(f"{session_id}:summary") + session_info["sentiment"] = self.redis_client_summary.get(f"{session_id}:sentiment") + session_info["start_time"] = datetime.fromtimestamp(float(self.redis_client_summary.get(f"{session_id}:start_time"))) + session_info["end_time"] = datetime.fromtimestamp(float(self.redis_client_summary.get(f"{session_id}:end_time"))) + return session_info + + def save_summary_and_sentiment(self, session_id, session_info): + """Save the summary, sentiment in separate dict""" + + self.redis_client_summary.set(f"{session_id}:summary", session_info.get("summary")) + self.redis_client_summary.set(f"{session_id}:sentiment", session_info.get("sentiment")) + self.redis_client_summary.set(f"{session_id}:start_time", session_info.get("start_time")) + self.redis_client_summary.set(f"{session_id}:end_time", session_info.get("end_time")) + + + def get_query_sentiment(self, session_id): + session_info = {} + if self.redis_client_summary.exists(f"{session_id}:conversation_hist"): + session_info["messages"] = json.loads(self.redis_client_summary.get(f"{session_id}:conversation_hist")) + session_info["start_time"] = datetime.fromtimestamp(float(self.redis_client_summary.get(f"{session_id}:start_time"))) + session_info["end_time"] = datetime.fromtimestamp(float(self.redis_client_summary.get(f"{session_id}:end_time"))) + return session_info + + + def save_query_sentiment(self, session_id, session_info): + self.redis_client_summary.set(f"{session_id}:conversation_hist", json.dumps(session_info.get("messages"))) + self.redis_client_summary.set(f"{session_id}:start_time", session_info.get("start_time")) + self.redis_client_summary.set(f"{session_id}:end_time", session_info.get("end_time")) + + + def save_sentiment_feedback(self, session_id: str, sentiment_feedback: float): + """Save sentiment feedback""" + return self.redis_client_feedback.set(f"{session_id}:sentiment", sentiment_feedback) + + + def save_summary_feedback(self, session_id: str, summary_feedback: float): + """Save summary feedback""" + return self.redis_client_feedback.set(f"{session_id}:summary", summary_feedback) + + + def save_session_feedback(self, session_id: str, session_feedback: float): + """Save summary feedback""" + return self.redis_client_feedback.set(f"{session_id}:session", session_feedback) diff --git a/src/analytics/datastore/session_manager.py b/src/analytics/datastore/session_manager.py new file mode 100644 index 0000000..ef9116a --- /dev/null +++ b/src/analytics/datastore/session_manager.py @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Manager conversation history return relavant conversation history +based on session_id +""" + +from typing import Any, Dict, List, Optional + +from src.analytics.datastore.postgres_client import PostgresClient +from src.analytics.datastore.redis_client import RedisClient +from src.common.utils import get_config + + +class SessionManager: + """ + Store the conversation between user and assistant, it's stored in format + {"session_id": {"user_id": "", "conversation_hist": [{"role": "user/assistant", "content": "", "timestamp": ""}], "last_conversation_time: ""}} + + Store summary of conversation + {"session_id": {"summary": "", "sentiment": "", "start_time: "", "end_time: ""}} + """ + + def __init__(self, *args, **kwargs) -> None: + db_name = get_config().database.name + if db_name == "redis": + print("Using Redis client for user history") + self.memory = RedisClient() + elif db_name == "postgres": + print("Using postgres for user history") + self.memory = PostgresClient() + else: + raise ValueError( + f"{db_name} in not supported. Supported type redis, postgres" + ) + + # TODO: Create API to get last k conversation from database instead of returning everything + def get_conversation(self, session_id: str) -> List: + return self.memory.get_conversation(session_id) + + def get_k_conversation(self, session_id: str, k_turn: Optional[int] = None) -> List: + return self.memory.get_k_conversation(session_id, k_turn) + + def save_conversation( + self, session_id: str, user_id: Optional[str], conversation: List + ) -> bool: + return self.memory.save_conversation(session_id, user_id, conversation) + + def is_session(self, session_id: str) -> bool: + """Check if session_id already exist in database""" + return self.memory.is_session(session_id) + + def list_sessions_for_user(self, user_id) -> Dict[str, Any]: + """ + List all session IDs for a given user ID along with start and end times. + """ + return self.memory.list_sessions_for_user(user_id) + + def get_session_summary_and_sentiment(self, session_id): + return self.memory.get_session_summary_and_sentiment(session_id) + + def save_summary_and_sentiment(self, session_id, session_info): + """Save the summary, sentiment in separate dict""" + + return self.memory.save_summary_and_sentiment(session_id, session_info) + + def get_query_sentiment(self, session_id): + + return self.memory.get_query_sentiment(session_id) + + def save_query_sentiment(self, session_id, conversation_hist): + + return self.memory.save_query_sentiment(session_id, conversation_hist) + + def save_sentiment_feedback(self, session_id: str, sentiment_feedback: float): + """Save sentiment feedback""" + return self.memory.save_sentiment_feedback(session_id, sentiment_feedback) + + + def save_summary_feedback(self, session_id: str, summary_feedback: float): + return self.memory.save_summary_feedback(session_id, summary_feedback) + + + def save_session_feedback(self, session_id: str, session_feedback: float): + return self.memory.save_session_feedback(session_id, session_feedback) + + + def get_purchase_history(self, user_id: str) -> List[str]: + """Use this to retrieve the user's purchase history.""" + return self.memory.get_purchase_history(user_id) + + + def get_conversations_in_last_h_hours(self, hours: int): + return self.memory.get_conversations_in_last_h_hours(hours) diff --git a/src/analytics/main.py b/src/analytics/main.py new file mode 100644 index 0000000..c7923d5 --- /dev/null +++ b/src/analytics/main.py @@ -0,0 +1,242 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import logging +from datetime import datetime +from enum import Enum +from typing import Annotated, Generator, Literal, Sequence, TypedDict + +from langchain_core.messages import BaseMessage +from langchain_core.output_parsers import StrOutputParser +from langchain_core.prompts.chat import ChatPromptTemplate +from langchain_core.runnables import RunnableConfig +from pydantic import BaseModel, Field +from src.analytics.datastore.session_manager import SessionManager +from src.common.utils import get_config, get_llm, get_prompts + +logger = logging.getLogger(__name__) +prompts = get_prompts() + +# TODO get the default_kwargs from the Agent Server API +default_llm_kwargs = {"temperature": 0, "top_p": 0.7, "max_tokens": 1024} + +# Initialize persist_data to determine whether data should be stored in the database. +persist_data = os.environ.get("PERSIST_DATA", "true").lower() == "true" + +# Initialize session manager during startup +session_manager = None +try: + session_manager = SessionManager() +except Exception as e: + logger.info(f"Failed to connect to DB during init, due to exception {e}") + + +def get_database(): + """ + Connect to the database. + """ + global session_manager + try: + if not session_manager: + session_manager = SessionManager() + + return session_manager + except Exception as e: + logger.info(f"Error connecting to database: {e}") + return None + + +def generate_summary(conversation_history): + """ + Generate a summary of the conversation. + + Parameters: + conversation_history (List): The conversation text. + + Returns: + str: A summary of the conversation. + """ + logger.info(f"conversation history: {conversation_history}") + llm = get_llm(**default_llm_kwargs) + prompt = prompts.get("summary_prompt", "") + for turn in conversation_history: + prompt += f"{turn['role']}: {turn['content']}\n" + + prompt += "\n\nSummary: " + response = llm.invoke(prompt) + + return response.content + + +def generate_session_summary(session_id): + # TODO: Check for corner cases like when session_id does not exist + session_manager = get_database() + + # Check if summary already exists in database + session_info = session_manager.get_session_summary_and_sentiment(session_id) + if session_info and session_info.get("summary", None): + return session_info + + # Generate summary and session info + conversation_history = session_manager.get_conversation(session_id) + summary = generate_summary(conversation_history) + sentiment = generate_sentiment(conversation_history) + + if persist_data: + # Save the summary and sentiment in database + session_manager.save_summary_and_sentiment( + session_id, + { + "summary": summary, + "sentiment": sentiment, + "start_time": conversation_history[0].get("timestamp", 0), + "end_time": conversation_history[-1].get("timestamp", 0), + } + ) + return { + "summary": summary, + "sentiment": sentiment, + "start_time": datetime.fromtimestamp( + float(conversation_history[0].get("timestamp", 0)) + ), + "end_time": datetime.fromtimestamp( + float(conversation_history[-1].get("timestamp", 0)) + ), + } + + +def fetch_user_conversation(user_id, start_time=None, end_time=None): + """ + Fetch a user's conversation from the database. + """ + try: + # TODO: Use start time and end time to filter the data + session_manager = get_database() + conversations = session_manager.list_sessions_for_user(user_id) + logger.info(f"Conversation: {conversations}") + return conversations + except Exception as e: + logger.error(f"Error fetching conversation: {e}") + return None + + +def generate_sentiment(conversation_history): + # Define an Enum for the sentiment values + class SentimentEnum(str, Enum): + POSITIVE = "positive" + NEUTRAL = "neutral" + NEGATIVE = "negative" + + # Define the Pydantic model using the Enum + class Sentiment(BaseModel): + """Sentiment for conversation.""" + + sentiment: SentimentEnum = Field( + description="Relevant value 'positive', 'neutral' or 'negative'" + ) + + logger.info("Finding sentiment for conversation") + llm = get_llm(**default_llm_kwargs) + prompt = prompts.get("sentiment_prompt", "") + for turn in conversation_history: + prompt += f"{turn['role']}: {turn['content']}\n" + + llm_with_tool = llm.with_structured_output(Sentiment) + + response = llm_with_tool.invoke(prompt) + sentiment = response.sentiment.value + logger.info(f"Conversation classified as {sentiment}") + return sentiment + + +def generate_sentiment_for_query(session_id): + """Generate sentiment for user query and assistant response + """ + + logger.info("Fetching sentiment for queries") + # Check if the sentiment is already identified in database, if yes return that + session_manager = get_database() + + session_info = session_manager.get_query_sentiment(session_id) + + if session_info and session_info.get("messages", None): + return { + "messages": session_info.get("messages"), + "session_info": { + "session_id": session_id, + "start_time": session_info.get("start_time"), + "end_time": session_info.get("start_time"), + }, + } + + class SentimentEnum(str, Enum): + POSITIVE = "positive" + NEUTRAL = "neutral" + NEGATIVE = "negative" + + # Define the Pydantic model using the Enum + class Sentiment(BaseModel): + """Sentiment for conversation.""" + + sentiment: SentimentEnum = Field( + description="Relevant value 'positive', 'neutral' or 'negative'" + ) + + + # Generate summary and session info + conversation_history = session_manager.get_conversation(session_id) + logger.info(f"Conversation history: {conversation_history}") + + logger.info("Finding sentiment for conversation") + llm = get_llm(**default_llm_kwargs) + + llm_with_tool = llm.with_structured_output(Sentiment) + + messages = [] + # TODO: parallize this operation for faster response + # Find sentiment for individual query and assistant response + for turn in conversation_history: + prompt = prompts.get("query_sentiment_prompt", "") + prompt += f"{turn['role']}: {turn['content']}\n" + + response = llm_with_tool.invoke(prompt) + sentiment = response.sentiment.value + messages.append({ + "role": turn["role"], + "content": turn["content"], + "sentiment": sentiment, + }) + + session_info = { + "messages": messages, + "start_time": conversation_history[0].get("timestamp", 0), + "end_time": conversation_history[-1].get("timestamp", 0), + } + if persist_data: + # Save information before sending it to user + session_manager.save_query_sentiment(session_id, session_info) + return { + "messages": messages, + "session_info": { + "session_id": session_id, + "start_time": datetime.fromtimestamp( + float(conversation_history[0].get("timestamp", 0)) + ), + "end_time": datetime.fromtimestamp( + float(conversation_history[-1].get("timestamp", 0)) + ), + }, + } + diff --git a/src/analytics/prompt.yaml b/src/analytics/prompt.yaml new file mode 100644 index 0000000..956772a --- /dev/null +++ b/src/analytics/prompt.yaml @@ -0,0 +1,20 @@ +sentiment_prompt: | + Analyze the sentiment of the following conversation between a customer service agent and a user. Provide the overall sentiment category. + Sentiment Categories: + Positive: Indicates satisfaction, gratitude, or successful resolution. + Negative: Indicates frustration, complaints, or unresolved issues. + Neutral: Indicates factual exchanges without strong emotions. + Instructions: + - Read the entire conversation carefully. + - Determine the overall sentiment category (Positive, Negative, Neutral). + + Please analyze the following conversation and provide your sentiment categorization: + +summary_prompt: | + Summarize the following conversation between a service rep and a customer in a + + few sentences. Use only the information from the conversation. + +query_sentiment_prompt: | + You are given the user query or assistant response. Find the sentiment for given query + \ No newline at end of file diff --git a/src/analytics/requirements.txt b/src/analytics/requirements.txt new file mode 100644 index 0000000..2fbc336 --- /dev/null +++ b/src/analytics/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.115.2 +uvicorn[standard]==0.27.1 +starlette==0.40.0 +langchain-nvidia-ai-endpoints==0.2.2 +langchain==0.2.16 +dataclass-wizard==0.22.3 +redis==5.0.8 +psycopg2-binary==2.9.9 +SQLAlchemy==2.0.31 +bleach==6.1.0 \ No newline at end of file diff --git a/src/analytics/server.py b/src/analytics/server.py new file mode 100644 index 0000000..11945f1 --- /dev/null +++ b/src/analytics/server.py @@ -0,0 +1,495 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The definition of the Llama Index chain server.""" +import os +import json +import logging +from typing import List +import bleach +import importlib +import random + +# For session response +from fastapi import Query, HTTPException +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from datetime import datetime +from typing import Optional, List +from typing import Literal + +from fastapi import FastAPI, Request +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from pydantic import BaseModel, Field, validator + +from src.analytics.datastore.session_manager import SessionManager + +logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper()) +logger = logging.getLogger(__name__) + +tags_metadata = [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness.", + }, + {"name": "Feedback", "description": "APIs for storing useful information for data flywheel."}, + {"name": "Session", "description": "APIs for fetching useful information for different sessions."}, + {"name": "User Data", "description": "APIs for fetching user specific information."}, +] + +# create the FastAPI server +app = FastAPI(title="Analytics API's for AI Virtual Assistant for Customer Service", + description="This API schema describes all the analytics endpoints exposed for the AI Virtual Assistant for Customer Service NIM Blueprint", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc", + openapi_tags=tags_metadata, +) + +EXAMPLE_DIR = "./" +# List of fallback responses sent out for any Exceptions from /generate endpoint +FALLBACK_RESPONSES = [ + "Please try re-phrasing, I am likely having some trouble with that conversation.", + "I will get better with time, please retry with a different conversation.", + "I wasn't able to process your conversation. Let's try something else.", + "Something went wrong. Could you try again in a few seconds with a different conversation.", + "Oops, that proved a tad difficult for me, can you retry with another conversation?" +] + +# Allow access in browser from RAG UI and Storybook (development) +origins = [ + "*" +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +class HealthResponse(BaseModel): + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +# For session response +# Updated model for session response +class SessionInfo(BaseModel): + session_id: str = Field(..., description="The ID of the session") + # Make the start_time mandatory + # TODO Make start time mandatory when needed + start_time: Optional[datetime] = Field(None, description="The start time of the session") + end_time: Optional[datetime] = Field(None, description="The end time of the session") + # Add other session-related if needed + + # TODO uncomment while actual implementation + # @validator('start_time', 'end_time') + # def ensure_utc(cls, v): + # if v is not None and v.tzinfo is None: + # raise ValueError("Datetime must be in UTC format") + # return v + +class SessionsResponse(BaseModel): + session_id: str = Field(..., description="The ID of the session") + user_id: Optional[str] = Field(..., description="The ID of the user") + start_time: Optional[datetime] = Field(None, description="The start time of the session") + end_time: Optional[datetime] = Field(None, description="The end time of the session") + +class SessionSummaryResponse(BaseModel): + session_info: SessionInfo + summary: str = Field(..., description="The generated summary of the session") + sentiment: Literal['positive', 'negative', 'neutral'] = Field(..., description="The sentiment of the text, which can be positive, negative, or neutral.") + + +class SessionConversationMessage(BaseModel): + """Definition of the Chat Message type.""" + role: str = Field(description="Role for a message AI, User and System", default="user", max_length=256, pattern=r'[\s\S]*') + content: str = Field(description="The input query/prompt to the pipeline.", default="I am going to Paris, what should I see?", max_length=131072, pattern=r'[\s\S]*') + sentiment: Literal['positive', 'negative', 'neutral'] = Field(..., description="The sentiment of the text, which can be positive, negative, or neutral.") + + @validator('role') + def validate_role(cls, value): + """ Field validator function to validate values of the field role""" + value = bleach.clean(value, strip=True) + valid_roles = {'user', 'assistant', 'system'} + if value.lower() not in valid_roles: + raise ValueError("Role must be one of 'user', 'assistant', or 'system'") + return value.lower() + + @validator('content') + def sanitize_content(cls, v): + """ Feild validator function to santize user populated feilds from HTML""" + return bleach.clean(v, strip=True) + +class SessionConversationResponse(BaseModel): + session_info: SessionInfo + messages: List[SessionConversationMessage] = Field(..., description="The list of messages in the conversation") + +class FeedbackRequest(BaseModel): + """Definition of the Feedback Request data type.""" + feedback: float = Field(..., description="A unique identifier representing your end-user.", ge=-1.0, le=1.0) + session_id: str = Field(..., description="A unique identifier representing the session associated with the response.") + +class FeedbackResponse(BaseModel): + """Definition of the Feedback Request data type.""" + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +class PurchaseInfo(BaseModel): + customer_id: str + order_id: str + product_name: str + order_date: str + quantity: Optional[int] + order_amount: Optional[float] + order_status: Optional[str] + return_status: Optional[str] + return_start_date: Optional[str] + return_received_date: Optional[str] + return_completed_date: Optional[str] + return_reason: Optional[str] + notes: Optional[str] + +@app.on_event("startup") +def import_example() -> None: + """ + Import the example class from the specified example file. + + """ + file_location = os.path.join(EXAMPLE_DIR, os.environ.get("EXAMPLE_PATH", "basic_rag/llamaindex")) + + for root, dirs, files in os.walk(file_location): + for file in files: + if file == "main.py": + # Import the specified file dynamically + spec = importlib.util.spec_from_file_location(name="main", location=os.path.join(root, file)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + logger.info(f"Found analytics api {file}") + # Get the Analytics app + app.analytics = module + # break # Stop the loop once we find and load main.py + + app.session_manager = SessionManager() + +@app.exception_handler(RequestValidationError) +async def request_validation_exception_handler( + request: Request, exc: RequestValidationError +) -> JSONResponse: + return JSONResponse( + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": jsonable_encoder(exc.errors(), exclude={"input"})}) + +@app.get("/health", tags=["Health"], response_model=HealthResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def health_check(): + """ + Perform a Health Check + + Returns 200 when service is up. This does not check the health of downstream services. + """ + + response_message = "Service is up." + return HealthResponse(message=response_message) + +@app.get("/sessions", tags=["Session"], response_model=List[SessionsResponse], responses={ + 404: { + "description": "No Sessions Found", + "content": { + "application/json": { + "example": {"detail": "No sessions found for the specified time range"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +# TODO Add the instrumentation_wrapper when required +#@llamaindex_instrumentation_wrapper +async def get_sessions( + hours: int = Query(..., description="Last K hours, for which sessions info is extracted"), +) -> List[SessionsResponse]: + """ + Retrieve session information in last k hours + """ + try: + result = app.session_manager.get_conversations_in_last_h_hours(hours) + + resp = [] + for r in result: + resp.append( + SessionsResponse( + session_id=r.get("session_id"), + user_id=r.get("user_id"), + start_time=r.get("start_conversation_time"), + end_time=r.get("last_conversation_time"), + ) + ) + + return resp + + except Exception as e: + logger.error(f"Error in GET /sessions endpoint. Error details: {e}") + return JSONResponse(content={"detail": "Error occurred while retrieving session information"}, status_code=500) + + +@app.get("/session/summary", tags=["Session"], response_model=SessionSummaryResponse, responses={ + 404: { + "description": "Session Not Found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def generate_session_summary( + request: Request, + session_id: str = Query(..., description="The ID of the session") +) -> SessionSummaryResponse: + """Generate a summary and sentiment analysis for the specified session.""" + + try: + result = app.analytics.generate_session_summary(session_id) + summary = result.get("summary") + sentiment = result.get("sentiment") + session_info = SessionInfo(session_id=session_id, start_time=result.get("start_time"), end_time=result.get("end_time")) + response = SessionSummaryResponse(session_info=session_info, summary=summary, sentiment=sentiment) + return response + + except ValueError as e: + logger.error(f"Session not found for ID {session_id}. Error details: {e}") + return SessionSummaryResponse(session_info=SessionInfo(session_id=session_id), summary=random.choice(FALLBACK_RESPONSES), sentiment="neutral") + + except Exception as e: + logger.error(f"Error in GET /session/summary endpoint. Error details: {e}") + return SessionSummaryResponse(session_info=SessionInfo(session_id=session_id), summary=random.choice(FALLBACK_RESPONSES), sentiment="neutral") + + +@app.get("/session/conversation", tags=["Session"], response_model=SessionConversationResponse, responses={ + 404: { + "description": "Session Not Found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def get_session_conversation( + request: Request, + session_id: str = Query(..., description="The ID of the session"), +) -> SessionConversationResponse: + """Retrieve the conversation and sentiment for the specified session.""" + + try: + result = app.analytics.generate_sentiment_for_query(session_id) + + message = [] + for msg in result.get("messages", []): + message.append(SessionConversationMessage(role=msg.get("role"), content=msg.get("content"), sentiment=msg.get("sentiment"))) + + session_info = SessionInfo(session_id=session_id, start_time=result.get("session_info", {}).get("start_time"), end_time=result.get("session_info", {}).get("start_time")) + response = SessionConversationResponse(session_info=session_info, messages=message) + return response + + except ValueError as e: + logger.error(f"Session not found for ID {session_id}. Error details: {e}") + raise HTTPException(status_code=404, detail="Session not found. Please check the session ID or end the session.") + + except Exception as e: + logger.error(f"Error in GET /session/conversation endpoint. Error details: {e}") + raise HTTPException(status_code=500, detail="Internal server error occurred") + + +@app.post("/get_user_purchase_history", tags=["User Data"], response_model=List[PurchaseInfo], responses={ + 404: { + "description": "Session Not Found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def get_user_purchase_history( + user_id: str, +) -> List[PurchaseInfo]: + """Get purchase history for user""" + try: + + logger.info(f"Fetching purchase history for user {user_id}") + product_info = app.session_manager.get_purchase_history(user_id) + response = [] + for product in product_info: + response.append(PurchaseInfo( + customer_id=str(product.get("customer_id")), + order_id=str(product.get("order_id")), + product_name=str(product.get("product_name")), + order_date=str(product.get("order_date")), + quantity=str(product.get("quantity")), + order_amount=str(product.get("order_amount")), + order_status=str(product.get("order_status")), + return_status=str(product.get("return_status")), + return_start_date=str(product.get("return_start_date")), + return_received_date=str(product.get("return_received_date")), + return_completed_date=str(product.get("return_completed_date")), + return_reason=str(product.get("return_reason")), + notes=str(product.get("notes")), + ) + ) + return response + except Exception as e: + logger.error(f"Error in GET /get_user_purchase_history endpoint. Error details: {e}") + return [] + +@app.post("/feedback/sentiment", tags=["Feedback"], response_model=FeedbackResponse, responses={ + 404: { + "description": "Session Not Found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def store_sentiment_feedback( + request: Request, + feedback: FeedbackRequest, +) -> FeedbackResponse: + """Store user feedback for the sentiment analysis of a conversation session.""" + # TODO: Add validation to store feeedback only when conversation exist + + try: + logger.info("Storing user feedback for sentiment") + app.session_manager.save_sentiment_feedback(feedback.session_id, feedback.feedback) + return FeedbackResponse(message="Sentiment feedback saved successfully") + except Exception as e: + logger.error(f"Error in GET /feedback/sentiment endpoint. Error details: {e}") + return FeedbackResponse(message="Failed to store sentiment feedback") + +@app.post("/feedback/summary", tags=["Feedback"], response_model=FeedbackResponse, responses={ + 404: { + "description": "Session Not Found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def store_summary_feedback( + request: Request, + feedback: FeedbackRequest, +) -> FeedbackResponse: + """Store user feedback for the summary of a conversation session.""" + # TODO: Add validation to store feeedback only when conversation exist + try: + logger.info("Storing user feedback for summary") + app.session_manager.save_summary_feedback(feedback.session_id, feedback.feedback) + return FeedbackResponse(message="Summary feedback saved successfully") + except Exception as e: + logger.error(f"Error in GET /feedback/summary endpoint. Error details: {e}") + return FeedbackResponse(message="Failed to store summary feedback") + + +@app.post("/feedback/session", tags=["Feedback"], response_model=FeedbackResponse, responses={ + 404: { + "description": "Session Not Found", + "content": { + "application/json": { + "example": {"detail": "Session not found"} + } + } + }, + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def store_conversation_session_feedback( + request: Request, + feedback: FeedbackRequest, +) -> FeedbackResponse: + """Store user feedback for the overall conversation session.""" + try: + # TODO: Add validation to store feeedback only when conversation exist + logger.info("Storing user feedback for summary") + app.session_manager.save_session_feedback(feedback.session_id, feedback.feedback) + return FeedbackResponse(message="Session feedback saved successfully") + except Exception as e: + logger.error(f"Error in GET /feedback/session endpoint. Error details: {e}") + return FeedbackResponse(message="Failed to store Session feedback") diff --git a/src/api_gateway/Dockerfile b/src/api_gateway/Dockerfile new file mode 100644 index 0000000..6d75ff9 --- /dev/null +++ b/src/api_gateway/Dockerfile @@ -0,0 +1,45 @@ +ARG BASE_IMAGE_URL=nvcr.io/nvidia/base/ubuntu +ARG BASE_IMAGE_TAG=22.04_20240212 + +FROM ${BASE_IMAGE_URL}:${BASE_IMAGE_TAG} + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV DEBIAN_FRONTEND noninteractive + +# Install required ubuntu packages for setting up python 3.10 +RUN apt update && \ + apt install -y curl software-properties-common && \ + add-apt-repository ppa:deadsnakes/ppa && \ + apt update && apt install -y python3.10 && \ + apt-get clean + +# Install pip for python3.10 +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10 + +RUN rm -rf /var/lib/apt/lists/* + +# Uninstall build packages +RUN apt autoremove -y curl software-properties-common + +# Download the sources of apt packages within the container for standard legal compliance +RUN sed -i 's/# deb-src/deb-src/g' /etc/apt/sources.list +RUN apt update +# xz-utils is needed to pull the source and unpack them correctly +RUN apt install xz-utils -y +RUN mkdir -p /legal/source +WORKDIR /legal/source +# Read installed packages, strip all but the package names, pipe to 'apt source' to download respective packages +RUN apt list --installed | grep -i installed | sed 's|\(.*\)/.*|\1|' | xargs apt source --download-only +# The source is saved in directories as well as tarballs in the current dir +RUN rm xz-utils* +COPY LICENSE-3rd-party.txt /legal/ + +# Install common dependencies for all servers +RUN --mount=type=bind,source=src/api_gateway/requirements.txt,target=/opt/requirements_api_server.txt \ + pip3 install --no-cache-dir -r /opt/requirements_api_server.txt + +# Copy required common modules +COPY src/api_gateway/main.py /opt/api_server.py + +WORKDIR /opt +ENTRYPOINT ["uvicorn", "api_server:app"] \ No newline at end of file diff --git a/src/api_gateway/README.md b/src/api_gateway/README.md new file mode 100644 index 0000000..49d63ef --- /dev/null +++ b/src/api_gateway/README.md @@ -0,0 +1,7 @@ +# Running the API Gateway microservice with all required microservices +``` +docker compose -f ./deploy/compose/docker-compose.yaml up -d --build +``` + +The API Gateway server can be accessed from the swagger url `http://:9000/docs/` +A static openapi schema can be found [here.](../../docs/api_references/api_gateway_server.json) \ No newline at end of file diff --git a/src/api_gateway/main.py b/src/api_gateway/main.py new file mode 100644 index 0000000..9fa4f02 --- /dev/null +++ b/src/api_gateway/main.py @@ -0,0 +1,286 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os +import logging +from typing import List +import bleach +import prometheus_client +from uuid import uuid4 +import httpx +import asyncio + +# For session response +from fastapi import Response +from pydantic import BaseModel +from typing import Optional, List +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field, validator +from traceback import print_exc + + +logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper()) +logger = logging.getLogger(__name__) + +tags_metadata = [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness.", + }, + {"name": "Agent", "description": "Core APIs for interacting with the agent."} +] + + +# create the FastAPI server +app = FastAPI(title="API Gateway server for AI Virtual Assistant for Customer Service", + description="This API schema describes all the endpoints exposed by the AI Virtual Assistant for Customer Service NIM Blueprint", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc", + openapi_tags=tags_metadata, +) + +# Allow access in browser from RAG UI and Storybook (development) +origins = [ + "*" +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +AGENT_SERVER_URL = os.getenv("AGENT_SERVER_URL") +ANALYTICS_SERVER_URL = os.getenv("ANALYTICS_SERVER_URL") +REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", 180)) + +class HealthResponse(BaseModel): + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +class Message(BaseModel): + """Definition of the Chat Message type.""" + role: str = Field(description="Role for a message AI, User and System", default="user", max_length=256, pattern=r'[\s\S]*') + content: str = Field(description="The input query/prompt to the pipeline.", default="Hello what can you do?", max_length=131072, pattern=r'[\s\S]*') + + @validator('role') + def validate_role(cls, value): + """ Field validator function to validate values of the field role""" + value = bleach.clean(value, strip=True) + valid_roles = {'user', 'assistant', 'system'} + if value.lower() not in valid_roles: + raise ValueError("Role must be one of 'user', 'assistant', or 'system'") + return value.lower() + + @validator('content') + def sanitize_content(cls, v, values): + """Field validator function to sanitize user-populated fields from HTML and limit content length.""" + v = bleach.clean(v, strip=True) + + # Check for empty string for user input + role = values.get('role') + if not v: + raise ValueError("Message content cannot be empty.") + + # Enforce character limit of 100 if role is 'user' + if role == 'user' and len(v) > 100: + v = v[:100] + logger.info(f"Truncating user input to first 100 characters. Modified input: {v}") + return v + +class AgentRequest(BaseModel): + """Definition of the Prompt API data type.""" + messages: Optional[List[Message]] = Field([], description="A list of messages comprising the conversation so far. The roles of the messages must be alternating between user and assistant. The last input message should have role user. A message with the the system role is optional, and must be the very first message if it is present.", max_items=50000) + user_id: Optional[str] = Field("", description="A unique identifier representing your end-user.") + session_id: Optional[str] = Field("", description="A unique identifier representing the session associated with the response.") + api_type: str = Field(description="The type of API action: 'create_session', 'end_session', 'generate', or 'summary'.", default="create_session") + +class AgentResponseChoices(BaseModel): + """ Definition of Chain response choices""" + index: int = Field(default=0, ge=0, le=256, format="int64") + message: Message = Field(default=Message()) + finish_reason: str = Field(default="", max_length=4096, pattern=r'[\s\S]*') + +class AgentResponse(BaseModel): + """Definition of Chain APIs resopnse data type""" + id: str = Field(default="", max_length=100000, pattern=r'[\s\S]*') + choices: List[AgentResponseChoices] = Field(default=[], max_items=256) + session_id: str = Field(None, description="A unique identifier representing the session associated with the response.") + sentiment: str = Field(default="", description="Any sentiment associated with this message") + + +@app.get("/agent/metrics", tags=["Health"]) +async def get_metrics(): + return Response(content=prometheus_client.generate_latest(), media_type="text/plain") + + +@app.get("/agent/health", tags=["Health"], response_model=HealthResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def health_check(): + """ + Perform a Health Check + + Returns 200 when service is up. This does not check the health of downstream services. + """ + + try: + # TODO: Check health of retrievers, analytics, datastores and NIMs as well + target_api_url = f"{AGENT_SERVER_URL}/health" + async with httpx.AsyncClient() as client: + processed_response = await fetch_and_process_response(client, "GET", target_api_url) + logger.debug(f"Response from /health endpoint of agent: {processed_response}") + + return HealthResponse(message=processed_response.get("message")) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}") + + +async def fetch_and_process_response(client, method, url, params=None, json=None): + try: + # Perform the request to the target API + resp = await client.request(method, url, params=params, json=json) + + # Check if the response was successful + if resp.status_code != 200: + raise HTTPException(status_code=resp.status_code, detail="Failed to get a response from the backend") + + # Fetch the response content (JSON) and parse it + return resp.json() + + except httpx.ReadTimeout: + raise HTTPException(status_code=504, detail="Timeout occurred while connecting to the backend service.") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}") + + +@app.post( + "/agent/generate", + response_model=AgentResponse, tags=["Agent"], + responses={ + 500: { + "description": "Internal Server Error", + "content": {"application/json": {"example": {"detail": "Internal server error occurred"}}}, + } + }, +) +async def generate_response(request: Request, prompt: AgentRequest) -> StreamingResponse: + """Generate and stream the response to the provided prompt.""" + + api_type = prompt.api_type + + def get_agent_generate_response() -> StreamingResponse: + target_api_url = f"{AGENT_SERVER_URL}/generate" + + async def response_generator(): + # Forward the request to the original API as a POST request + async with httpx.AsyncClient(timeout=httpx.Timeout(REQUEST_TIMEOUT)) as client: + async with client.stream("POST", target_api_url, json=prompt.dict()) as resp: + if resp.status_code != 200: + raise HTTPException(status_code=resp.status_code, detail="Failed to get a response from the backend") + + # Forward the streaming response from the original API to the client + async for chunk in resp.aiter_text(): + if chunk: + yield chunk + + # Return a streaming response to the client + return StreamingResponse(response_generator(), media_type="text/event-stream") + + def response_generator(sentence: str, sentiment: str = ""): + """Mock response generator to simulate streaming predefined sentence.""" + + # Simulate breaking the sentence into chunks (e.g., by word) + sentence_chunks = sentence.split() # Split the sentence by words + resp_id = str(uuid4()) # unique response id for every query + # Send each chunk (word) in the response + for chunk in sentence_chunks: + chain_response = AgentResponse(session_id=prompt.session_id, sentiment=sentiment) + response_choice = AgentResponseChoices( + index=0, + message=Message(role="assistant", content=f"{chunk} ") + ) + chain_response.id = resp_id + chain_response.choices.append(response_choice) + yield "data: " + str(chain_response.json()) + "\n\n" + + # End with [DONE] response + chain_response = AgentResponse(session_id=prompt.session_id, sentiment=sentiment) + response_choice = AgentResponseChoices(message=Message(role="assistant", content=" "), finish_reason="[DONE]") + chain_response.id = resp_id + chain_response.choices.append(response_choice) + yield "data: " + str(chain_response.json()) + "\n\n" + + try: + + if api_type == "create_session": + target_api_url = f"{AGENT_SERVER_URL}/create_session" + async with httpx.AsyncClient() as client: + processed_response = await fetch_and_process_response(client, "GET", target_api_url) + logger.info(f"Response from /create_session: {processed_response}") + prompt.session_id = processed_response.get("session_id") + return get_agent_generate_response() + + elif api_type == "end_session": + target_api_url = f"{AGENT_SERVER_URL}/delete_session" + params = {"session_id": prompt.session_id} + async with httpx.AsyncClient() as client: + processed_response = await fetch_and_process_response(client, "DELETE", target_api_url, params=params) + logger.info(f"Response from /delete_session: {processed_response}") + return StreamingResponse(response_generator(processed_response.get("message", "")), media_type="text/event-stream") + + elif api_type == "generate": + return get_agent_generate_response() + + elif api_type == "summary": + target_api_url = f"{AGENT_SERVER_URL}/end_session" + params = {"session_id": prompt.session_id} + async with httpx.AsyncClient() as client: + processed_response = await fetch_and_process_response(client, "GET", target_api_url, params=params) + logger.info(f"Response from /end_session: {processed_response}") + + target_api_url = f"{ANALYTICS_SERVER_URL}/session/summary" + async with httpx.AsyncClient() as client: + processed_response = await fetch_and_process_response(client, "GET", target_api_url, params=params) + logger.info(f"Response from /session/summary: {processed_response}") + return StreamingResponse(response_generator(processed_response.get("summary", ""), sentiment=processed_response.get("sentiment", "")), media_type="text/event-stream") + + else: + raise ValueError("Wrong API type provided. 'create_session', 'end_session', 'generate', or 'summary'") + + except httpx.ReadTimeout as e: + logger.error(f"HTTP Read Timeout: {e}") + raise HTTPException(status_code=504, detail="Upstream server timeout. Please try again later.") + except httpx.RequestError as e: + # This will catch other request-related errors like connection issues + logger.error(f"Request Error: {e}") + raise HTTPException(status_code=502, detail="Error communicating with the upstream server.") + except asyncio.CancelledError as e: + logger.error("Response generation was cancelled. Details: {e}") + raise HTTPException(status_code=500, detail=f"Server interruption before response completion: {e}") + except Exception as e: + logger.error("Internal server error. Details: {e}") + raise HTTPException(status_code=500, detail=f"Internal Server Error: {e}") diff --git a/src/api_gateway/requirements.txt b/src/api_gateway/requirements.txt new file mode 100644 index 0000000..9adfd06 --- /dev/null +++ b/src/api_gateway/requirements.txt @@ -0,0 +1,6 @@ +prometheus_client==0.21.0 +fastapi==0.115.2 +uvicorn[standard]==0.27.1 +starlette==0.40.0 +bleach==6.1.0 +httpx==0.27.2 \ No newline at end of file diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..a08b2c2 --- /dev/null +++ b/src/common/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/common/configuration.py b/src/common/configuration.py new file mode 100644 index 0000000..e18e67d --- /dev/null +++ b/src/common/configuration.py @@ -0,0 +1,331 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The definition of the application configuration.""" +from src.common.configuration_wizard import ConfigWizard, configclass, configfield + + +@configclass +class VectorStoreConfig(ConfigWizard): + """Configuration class for the Vector Store connection. + + :cvar name: Name of vector store + :cvar url: URL of Vector Store + """ + + name: str = configfield( + "name", + default="milvus", # supports pgvector, milvus + help_txt="The name of vector store", + ) + url: str = configfield( + "url", + default="http://milvus:19530", # for pgvector `pgvector:5432` + help_txt="The host of the machine running Vector Store DB", + ) + nlist: int = configfield( + "nlist", + default=64, # IVF Flat milvus + help_txt="Number of cluster units", + ) + nprobe: int = configfield( + "nprobe", + default=16, # IVF Flat milvus + help_txt="Number of units to query", + ) + +@configclass +class DatabaseConfig(ConfigWizard): + """Configuration class for the Database connection. + + :cvar name: Name of Database + :cvar url: URL of Database + :cvar config: config shared to database + """ + + from dataclasses import field + name: str = configfield( + "name", + default="postgres", # supports redis, postgres + help_txt="The name of database", + ) + url: str = configfield( + "url", + default="postgres:5432", # for redis `redis:6379` + help_txt="The host of the machine running database", + ) + config: str = configfield( + "config", + default=field(default_factory=""), + help_txt="Any configuration needs to be shared can be shared as dict", + ) + +@configclass +class CheckpointerConfig(ConfigWizard): + """Configuration class for the Database connection. + + :cvar name: Name of checkpointer database + :cvar url: URL of checkpointer database + :cvar config: config shared to database + """ + + from dataclasses import field + name: str = configfield( + "name", + default="postgres", # supports inmemory, postgres + help_txt="The name of database", + ) + url: str = configfield( + "url", + default="postgres:5432", # for redis `redis:6379` + help_txt="The host of the machine running database", + ) + config: str = configfield( + "config", + default=field(default_factory=""), + help_txt="Any configuration needs to be shared can be shared as dict", + ) + +@configclass +class CacheConfig(ConfigWizard): + """Configuration class for the Vector Store connection. + + :cvar name: Name of Cache + :cvar url: URL of Cache + :cvar config: config shared to Cache + """ + + from dataclasses import field + name: str = configfield( + "name", + default="redis", # supports redis + help_txt="The name of vector store", + ) + url: str = configfield( + "url", + default="redis:6379", # for redis `redis:6379` + help_txt="The host of the machine running cache", + ) + config: str = configfield( + "config", + default=field(default_factory=""), + help_txt="Any configuration needs to be shared can be shared as dict", + ) + +@configclass +class LLMConfig(ConfigWizard): + """Configuration class for the llm connection. + + :cvar server_url: The location of the llm server hosting the model. + :cvar model_name: The name of the hosted model. + """ + + server_url: str = configfield( + "server_url", + default="", + help_txt="The location of the Triton server hosting the llm model.", + ) + model_name: str = configfield( + "model_name", + default="ensemble", + help_txt="The name of the hosted model.", + ) + model_engine: str = configfield( + "model_engine", + default="nvidia-ai-endpoints", + help_txt="The server type of the hosted model. Allowed values are nvidia-ai-endpoints", + ) + model_name_pandas_ai: str = configfield( + "model_name_pandas_ai", + default="ai-mixtral-8x7b-instruct", + help_txt="The name of the ai catalog model to be used with PandasAI agent", + ) + +@configclass +class TextSplitterConfig(ConfigWizard): + """Configuration class for the Text Splitter. + + :cvar chunk_size: Chunk size for text splitter. Tokens per chunk in token-based splitters. + :cvar chunk_overlap: Text overlap in text splitter. + """ + + model_name: str = configfield( + "model_name", + default="Snowflake/snowflake-arctic-embed-l", + help_txt="The name of Sentence Transformer model used for SentenceTransformer TextSplitter.", + ) + chunk_size: int = configfield( + "chunk_size", + default=510, + help_txt="Chunk size for text splitting.", + ) + chunk_overlap: int = configfield( + "chunk_overlap", + default=200, + help_txt="Overlapping text length for splitting.", + ) + + +@configclass +class EmbeddingConfig(ConfigWizard): + """Configuration class for the Embeddings. + + :cvar model_name: The name of the huggingface embedding model. + """ + + model_name: str = configfield( + "model_name", + default="snowflake/arctic-embed-l", + help_txt="The name of huggingface embedding model.", + ) + model_engine: str = configfield( + "model_engine", + default="nvidia-ai-endpoints", + help_txt="The server type of the hosted model. Allowed values are hugginface", + ) + dimensions: int = configfield( + "dimensions", + default=1024, + help_txt="The required dimensions of the embedding model. Currently utilized for vector DB indexing.", + ) + server_url: str = configfield( + "server_url", + default="", + help_txt="The url of the server hosting nemo embedding model", + ) + +@configclass +class RankingConfig(ConfigWizard): + """Configuration class for the Re-ranking. + + :cvar model_name: The name of the Ranking model. + """ + + model_name: str = configfield( + "model_name", + default="nv-rerank-qa-mistral-4b:1", + help_txt="The name of Ranking model.", + ) + model_engine: str = configfield( + "model_engine", + default="nvidia-ai-endpoints", + help_txt="The server type of the hosted model. Allowed values are nvidia-ai-endpoints", + ) + server_url: str = configfield( + "server_url", + default="", + help_txt="The url of the server hosting nemo Ranking model", + ) + +@configclass +class RetrieverConfig(ConfigWizard): + """Configuration class for the Retrieval pipeline. + + :cvar top_k: Number of relevant results to retrieve. + :cvar score_threshold: The minimum confidence score for the retrieved values to be considered. + """ + + top_k: int = configfield( + "top_k", + default=4, + help_txt="Number of relevant results to retrieve", + ) + score_threshold: float = configfield( + "score_threshold", + default=0.25, + help_txt="The minimum confidence score for the retrieved values to be considered", + ) + nr_url: str = configfield( + "nr_url", + default='http://retrieval-ms:8000', + help_txt="The nemo retriever microservice url", + ) + nr_pipeline: str = configfield( + "nr_pipeline", + default='ranked_hybrid', + help_txt="The name of the nemo retriever pipeline one of ranked_hybrid or hybrid", + ) + + +@configclass +class AppConfig(ConfigWizard): + """Configuration class for the application. + + :cvar vector_store: The configuration of the vector db connection. + :type vector_store: VectorStoreConfig + :cvar llm: The configuration of the backend llm server. + :type llm: LLMConfig + :cvar text_splitter: The configuration for text splitter + :type text_splitter: TextSplitterConfig + :cvar embeddings: The configuration for huggingface embeddings + :type embeddings: EmbeddingConfig + :cvar prompts: The Prompts template for RAG and Chat + :type prompts: PromptsConfig + """ + + vector_store: VectorStoreConfig = configfield( + "vector_store", + env=False, + help_txt="The configuration of the vector db connection.", + default=VectorStoreConfig(), + ) + database: DatabaseConfig = configfield( + "database", + env=False, + help_txt="The configuration of the database connection.", + default=DatabaseConfig(), + ) + checkpointer: CheckpointerConfig = configfield( + "checkpointer", + env=False, + help_txt="The configuration of the checkpointer.", + default=CheckpointerConfig(), + ) + cache: CacheConfig = configfield( + "cache", + env=False, + help_txt="The configuration of the cache connection.", + default=CacheConfig(), + ) + llm: LLMConfig = configfield( + "llm", + env=False, + help_txt="The configuration for the server hosting the Large Language Models.", + default=LLMConfig(), + ) + text_splitter: TextSplitterConfig = configfield( + "text_splitter", + env=False, + help_txt="The configuration for text splitter.", + default=TextSplitterConfig(), + ) + embeddings: EmbeddingConfig = configfield( + "embeddings", + env=False, + help_txt="The configuration of embedding model.", + default=EmbeddingConfig(), + ) + ranking: RankingConfig = configfield( + "ranking", + env=False, + help_txt="The configuration of ranking model.", + default=RankingConfig(), + ) + retriever: RetrieverConfig = configfield( + "retriever", + env=False, + help_txt="The configuration of the retriever pipeline.", + default=RetrieverConfig(), + ) \ No newline at end of file diff --git a/src/common/configuration_wizard.py b/src/common/configuration_wizard.py new file mode 100644 index 0000000..e390506 --- /dev/null +++ b/src/common/configuration_wizard.py @@ -0,0 +1,411 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A module containing utilities for defining application configuration. + +This module provides a configuration wizard class that can read configuration data from YAML, JSON, and environment +variables. The configuration wizard is based heavily off of the JSON and YAML wizards from the `dataclass-wizard` +Python package. That package is in-turn based heavily off of the built-in `dataclass` module. + +This module adds Environment Variable parsing to config file reading. +""" +# pylint: disable=too-many-lines; this file is meant to be portable between projects so everything is put into one file + +import json +import logging +import os +from dataclasses import _MISSING_TYPE, dataclass +from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple, Union + +import yaml +from dataclass_wizard import ( + JSONWizard, + LoadMeta, + YAMLWizard, + errors, + fromdict, + json_field, +) +from dataclass_wizard.models import JSONField +from dataclass_wizard.utils.string_conv import to_camel_case + +configclass = dataclass(frozen=True) +ENV_BASE = "APP" +_LOGGER = logging.getLogger(__name__) + + +def configfield( + name: str, *, env: bool = True, help_txt: str = "", **kwargs: Any +) -> JSONField: + """Create a data class field with the specified name in JSON format. + + :param name: The name of the field. + :type name: str + :param env: Whether this field should be configurable from an environment variable. + :type env: bool + :param help_txt: The description of this field that is used in help docs. + :type help_txt: str + :param **kwargs: Optional keyword arguments to customize the JSON field. More information here: + https://dataclass-wizard.readthedocs.io/en/latest/dataclass_wizard.html#dataclass_wizard.json_field + :type **kwargs: Any + :returns: A JSONField instance with the specified name and optional parameters. + :rtype: JSONField + + :raises TypeError: If the provided name is not a string. + """ + # sanitize specified name + if not isinstance(name, str): + raise TypeError("Provided name must be a string.") + json_name = to_camel_case(name) + + # update metadata + meta = kwargs.get("metadata", {}) + meta["env"] = env + meta["help"] = help_txt + kwargs["metadata"] = meta + + # create the data class field + field = json_field(json_name, **kwargs) + return field + + +class _Color: + """A collection of colors used when writing output to the shell.""" + + # pylint: disable=too-few-public-methods; this class does not require methods. + + PURPLE = "\033[95m" + BLUE = "\033[94m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + RED = "\033[91m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + END = "\033[0m" + + +class ConfigWizard(JSONWizard, YAMLWizard): # type: ignore[misc] # dataclass-wizard doesn't provide stubs + """A configuration wizard class that can read configuration data from YAML, JSON, and environment variables.""" + + # pylint: disable=arguments-differ,arguments-renamed; this class intentionally reduces arguments for some methods. + + @classmethod + def print_help( + cls, + help_printer: Callable[[str], Any], + *, + env_parent: Optional[str] = None, + json_parent: Optional[Tuple[str, ...]] = None, + ) -> None: + """Print the help documentation for the application configuration with the provided `write` function. + + :param help_printer: The `write` function that will be used to output the data. + :param help_printer: Callable[[str], None] + :param env_parent: The name of the parent environment variable. Leave blank, used for recursion. + :type env_parent: Optional[str] + :param json_parent: The name of the parent JSON key. Leave blank, used for recursion. + :type json_parent: Optional[Tuple[str, ...]] + :returns: A list of tuples with one item per configuration value. Each item will have the environment variable + and a tuple to the path in configuration. + :rtype: List[Tuple[str, Tuple[str, ...]]] + """ + if not env_parent: + env_parent = "" + help_printer("---\n") + if not json_parent: + json_parent = () + + for ( + _, + val, + ) in ( + cls.__dataclass_fields__.items() # pylint: disable=no-member; false positive + ): # pylint: disable=no-member; member is added by dataclass. + jsonname = val.json.keys[0] + envname = jsonname.upper() + full_envname = f"{ENV_BASE}{env_parent}_{envname}" + is_embedded_config = hasattr(val.type, "envvars") + + # print the help data + indent = len(json_parent) * 2 + if is_embedded_config: + default = "" + elif not isinstance(val.default_factory, _MISSING_TYPE): + default = val.default_factory() + elif isinstance(val.default, _MISSING_TYPE): + default = "NO-DEFAULT-VALUE" + else: + default = val.default + help_printer( + f"{_Color.BOLD}{' ' * indent}{jsonname}:{_Color.END} {default}\n" + ) + + # print comments + if is_embedded_config: + indent += 2 + if val.metadata.get("help"): + help_printer(f"{' ' * indent}# {val.metadata['help']}\n") + if not is_embedded_config: + typestr = getattr(val.type, "__name__", None) or str(val.type).replace( + "typing.", "" + ) + help_printer(f"{' ' * indent}# Type: {typestr}\n") + if val.metadata.get("env", True): + help_printer(f"{' ' * indent}# ENV Variable: {full_envname}\n") + # if not is_embedded_config: + help_printer("\n") + + if is_embedded_config: + new_env_parent = f"{env_parent}_{envname}" + new_json_parent = json_parent + (jsonname,) + val.type.print_help( + help_printer, env_parent=new_env_parent, json_parent=new_json_parent + ) + + help_printer("\n") + + @classmethod + def envvars( + cls, + env_parent: Optional[str] = None, + json_parent: Optional[Tuple[str, ...]] = None, + ) -> List[Tuple[str, Tuple[str, ...], type]]: + """Calculate valid environment variables and their config structure location. + + :param env_parent: The name of the parent environment variable. + :type env_parent: Optional[str] + :param json_parent: The name of the parent JSON key. + :type json_parent: Optional[Tuple[str, ...]] + :returns: A list of tuples with one item per configuration value. Each item will have the environment variable, + a tuple to the path in configuration, and they type of the value. + :rtype: List[Tuple[str, Tuple[str, ...], type]] + """ + if not env_parent: + env_parent = "" + if not json_parent: + json_parent = () + output = [] + + for ( + _, + val, + ) in ( + cls.__dataclass_fields__.items() # pylint: disable=no-member; false positive + ): # pylint: disable=no-member; member is added by dataclass. + jsonname = val.json.keys[0] + envname = jsonname.upper() + full_envname = f"{ENV_BASE}{env_parent}_{envname}" + is_embedded_config = hasattr(val.type, "envvars") + + # add entry to output list + if is_embedded_config: + new_env_parent = f"{env_parent}_{envname}" + new_json_parent = json_parent + (jsonname,) + output += val.type.envvars( + env_parent=new_env_parent, json_parent=new_json_parent + ) + elif val.metadata.get("env", True): + output += [(full_envname, json_parent + (jsonname,), val.type)] + + return output + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConfigWizard": + """Create a ConfigWizard instance from a dictionary. + + :param data: The dictionary containing the configuration data. + :type data: Dict[str, Any] + :returns: A ConfigWizard instance created from the input dictionary. + :rtype: ConfigWizard + + :raises RuntimeError: If the configuration data is not a dictionary. + """ + # sanitize data + if not data: + data = {} + if not isinstance(data, dict): + raise RuntimeError("Configuration data is not a dictionary.") + + # parse env variables + for envvar in cls.envvars(): + var_name, conf_path, var_type = envvar + var_value = os.environ.get(var_name) + if var_value: + var_value = try_json_load(var_value) + update_dict(data, conf_path, var_value) + _LOGGER.debug( + "Found EnvVar Config - %s:%s = %s", + var_name, + str(var_type), + repr(var_value), + ) + + LoadMeta(key_transform="CAMEL").bind_to(cls) + return fromdict(cls, data) # type: ignore[no-any-return] # dataclass-wizard doesn't provide stubs + + @classmethod + def from_file(cls, filepath: str) -> Optional["ConfigWizard"]: + """Load the application configuration from the specified file. + + The file must be either in JSON or YAML format. + + :returns: The fully processed configuration file contents. If the file was unreadable, None will be returned. + :rtype: Optional["ConfigWizard"] + """ + # open the file + try: + # pylint: disable-next=consider-using-with; using a with would make exception handling even more ugly + file = open(filepath, encoding="utf-8") + except FileNotFoundError: + _LOGGER.error("The configuration file cannot be found.") + file = None + except PermissionError: + _LOGGER.error( + "Permission denied when trying to read the configuration file." + ) + file = None + if not file: + return None + + # read the file + try: + data = read_json_or_yaml(file) + except ValueError as err: + _LOGGER.error( + "Configuration file must be valid JSON or YAML. The following errors occured:\n%s", + str(err), + ) + data = None + config = None + finally: + file.close() + + # parse the file + if data: + try: + config = cls.from_dict(data) + except errors.MissingFields as err: + _LOGGER.error( + "Configuration is missing required fields: \n%s", str(err) + ) + config = None + except errors.ParseError as err: + _LOGGER.error("Invalid configuration value provided:\n%s", str(err)) + config = None + else: + config = cls.from_dict({}) + + return config + + +def read_json_or_yaml(stream: TextIO) -> Dict[str, Any]: + """Read a file without knowing if it is JSON or YAML formatted. + + The file will first be assumed to be JSON formatted. If this fails, an attempt to parse the file with the YAML + parser will be made. If both of these fail, an exception will be raised that contains the exception strings returned + by both the parsers. + + :param stream: An IO stream that allows seeking. + :type stream: typing.TextIO + :returns: The parsed file contents. + :rtype: typing.Dict[str, typing.Any]: + :raises ValueError: If the IO stream is not seekable or if the file doesn't appear to be JSON or YAML formatted. + """ + exceptions: Dict[str, Union[None, ValueError, yaml.error.YAMLError]] = { + "JSON": None, + "YAML": None, + } + data: Dict[str, Any] + + # ensure we can rewind the file + if not stream.seekable(): + raise ValueError("The provided stream must be seekable.") + + # attempt to read json + try: + data = json.loads(stream.read()) + except ValueError as err: + exceptions["JSON"] = err + else: + return data + finally: + stream.seek(0) + + # attempt to read yaml + try: + data = yaml.safe_load(stream.read()) + except (yaml.error.YAMLError, ValueError) as err: + exceptions["YAML"] = err + else: + return data + + # neither json nor yaml + err_msg = "\n\n".join( + [key + " Parser Errors:\n" + str(val) for key, val in exceptions.items()] + ) + raise ValueError(err_msg) + + +def try_json_load(value: str) -> Any: + """Try parsing the value as JSON and silently ignore errors. + + :param value: The value on which a JSON load should be attempted. + :type value: str + :returns: Either the parsed JSON or the provided value. + :rtype: typing.Any + """ + try: + return json.loads(value) + except json.JSONDecodeError: + return value + + +def update_dict( + data: Dict[str, Any], + path: Tuple[str, ...], + value: Any, + overwrite: bool = False, +) -> None: + """Update a dictionary with a new value at a given path. + + :param data: The dictionary to be updated. + :type data: Dict[str, Any] + :param path: The path to the key that should be updated. + :type path: Tuple[str, ...] + :param value: The new value to be set at the specified path. + :type value: Any + :param overwrite: If True, overwrite the existing value. Otherwise, don't update if the key already exists. + :type overwrite: bool + :returns: None + """ + end = len(path) + target = data + for idx, key in enumerate(path, 1): + # on the last field in path, update the dict if necessary + if idx == end: + if overwrite or not target.get(key): + target[key] = value + return + + # verify the next hop exists + if not target.get(key): + target[key] = {} + + # if the next hop is not a dict, exit + if not isinstance(target.get(key), dict): + return + + # get next hop + target = target.get(key) # type: ignore[assignment] # type has already been enforced. diff --git a/src/common/utils.py b/src/common/utils.py new file mode 100644 index 0000000..7b6969c --- /dev/null +++ b/src/common/utils.py @@ -0,0 +1,362 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions used across different modules of NIM Blueprints.""" +import os +import yaml +import logging +from pathlib import Path +from functools import lru_cache, wraps +from urllib.parse import urlparse +from typing import TYPE_CHECKING, Callable, List, Dict + +logger = logging.getLogger(__name__) + +try: + import torch +except Exception as e: + logger.warning(f"Optional module torch not installed.") + +try: + from langchain.text_splitter import SentenceTransformersTokenTextSplitter +except Exception as e: + logger.warning(f"Optional langchain module not installed for SentenceTransformersTokenTextSplitter.") + +try: + from langchain_core.vectorstores import VectorStore +except Exception as e: + logger.warning(f"Optional Langchain module langchain_core not installed.") + +try: + from langchain_nvidia_ai_endpoints import ChatNVIDIA, NVIDIAEmbeddings, NVIDIARerank +except Exception as e: + logger.error(f"Optional langchain API Catalog connector langchain_nvidia_ai_endpoints not installed.") + +try: + from langchain_community.vectorstores import PGVector + from langchain_community.vectorstores import Milvus + from langchain_community.docstore.in_memory import InMemoryDocstore + from langchain_community.embeddings import HuggingFaceEmbeddings + from langchain_community.vectorstores import FAISS +except Exception as e: + logger.warning(f"Optional Langchain module langchain_community not installed.") + +try: + from faiss import IndexFlatL2 +except Exception as e: + logger.warning(f"Optional faissDB not installed.") + + +from langchain_core.embeddings import Embeddings +from langchain_core.documents.compressor import BaseDocumentCompressor +from langchain_core.language_models.chat_models import SimpleChatModel +from langchain.llms.base import LLM +from src.common import configuration + +if TYPE_CHECKING: + from src.common.configuration_wizard import ConfigWizard + +DEFAULT_MAX_CONTEXT = 1500 + +def utils_cache(func: Callable) -> Callable: + """Use this to convert unhashable args to hashable ones""" + @wraps(func) + def wrapper(*args, **kwargs): + # Convert unhashable args to hashable ones + args_hashable = tuple(tuple(arg) if isinstance(arg, (list, dict, set)) else arg for arg in args) + kwargs_hashable = {key: tuple(value) if isinstance(value, (list, dict, set)) else value for key, value in kwargs.items()} + return func(*args_hashable, **kwargs_hashable) + return wrapper + + +@lru_cache +def get_config() -> "ConfigWizard": + """Parse the application configuration.""" + config_file = os.environ.get("APP_CONFIG_FILE", "/dev/null") + config = configuration.AppConfig.from_file(config_file) + if config: + return config + raise RuntimeError("Unable to find configuration.") + + +@lru_cache +def get_prompts() -> Dict: + """Retrieves prompt configurations from YAML file and return a dict. + """ + + # default config taking from prompt.yaml + default_config_path = os.path.join("./", os.environ.get("EXAMPLE_PATH"), "prompt.yaml") + default_config = {} + if Path(default_config_path).exists(): + with open(default_config_path, 'r') as file: + logger.info(f"Using prompts config file from: {default_config_path}") + default_config = yaml.safe_load(file) + + config_file = os.environ.get("PROMPT_CONFIG_FILE", "/prompt.yaml") + + config = {} + if Path(config_file).exists(): + with open(config_file, 'r') as file: + logger.info(f"Using prompts config file from: {config_file}") + config = yaml.safe_load(file) + + config = _combine_dicts(default_config, config) + return config + + + +def create_vectorstore_langchain(document_embedder, collection_name: str = "") -> VectorStore: + """Create the vector db index for langchain.""" + + config = get_config() + + if config.vector_store.name == "faiss": + vectorstore = FAISS(document_embedder, IndexFlatL2(config.embeddings.dimensions), InMemoryDocstore(), {}) + elif config.vector_store.name == "pgvector": + db_name = os.getenv('POSTGRES_DB', None) + if not collection_name: + collection_name = os.getenv('COLLECTION_NAME', "vector_db") + logger.info(f"Using PGVector collection: {collection_name}") + connection_string = f"postgresql://{os.getenv('POSTGRES_USER', '')}:{os.getenv('POSTGRES_PASSWORD', '')}@{config.vector_store.url}/{db_name}" + vectorstore = PGVector( + collection_name=collection_name, + connection_string=connection_string, + embedding_function=document_embedder, + ) + elif config.vector_store.name == "milvus": + if not collection_name: + collection_name = os.getenv('COLLECTION_NAME', "vector_db") + logger.info(f"Using milvus collection: {collection_name}") + url = urlparse(config.vector_store.url) + vectorstore = Milvus( + document_embedder, + connection_args={"host": url.hostname, "port": url.port}, + collection_name=collection_name, + index_params={"index_type": "GPU_IVF_FLAT", "metric_type": "L2", "nlist": config.vector_store.nlist}, + search_params={"nprobe": config.vector_store.nprobe}, + auto_id = True + ) + else: + raise ValueError(f"{config.vector_store.name} vector database is not supported") + logger.info("Vector store created and saved.") + return vectorstore + + +def get_vectorstore(vectorstore, document_embedder) -> VectorStore: + """ + Send a vectorstore object. + If a Vectorstore object already exists, the function returns that object. + Otherwise, it creates a new Vectorstore object and returns it. + """ + if vectorstore is None: + return create_vectorstore_langchain(document_embedder) + return vectorstore + +@utils_cache +@lru_cache() +def get_llm(**kwargs) -> LLM | SimpleChatModel: + """Create the LLM connection.""" + settings = get_config() + + logger.info(f"Using {settings.llm.model_engine} as model engine for llm. Model name: {settings.llm.model_name}") + if settings.llm.model_engine == "nvidia-ai-endpoints": + unused_params = [key for key in kwargs.keys() if key not in ['temperature', 'top_p', 'max_tokens']] + if unused_params: + logger.warning(f"The following parameters from kwargs are not supported: {unused_params} for {settings.llm.model_engine}") + if settings.llm.server_url: + logger.info(f"Using llm model {settings.llm.model_name} hosted at {settings.llm.server_url}") + return ChatNVIDIA(base_url=f"http://{settings.llm.server_url}/v1", + model=settings.llm.model_name, + temperature = kwargs.get('temperature', None), + top_p = kwargs.get('top_p', None), + max_tokens = kwargs.get('max_tokens', None)) + else: + logger.info(f"Using llm model {settings.llm.model_name} from api catalog") + return ChatNVIDIA(model=settings.llm.model_name, + temperature = kwargs.get('temperature', None), + top_p = kwargs.get('top_p', None), + max_tokens = kwargs.get('max_tokens', None)) + else: + raise RuntimeError("Unable to find any supported Large Language Model server. Supported engine name is nvidia-ai-endpoints.") + + +@lru_cache +def get_embedding_model() -> Embeddings: + """Create the embedding model.""" + model_kwargs = {"device": "cpu"} + if torch.cuda.is_available(): + model_kwargs["device"] = "cuda:0" + + encode_kwargs = {"normalize_embeddings": False} + settings = get_config() + + logger.info(f"Using {settings.embeddings.model_engine} as model engine and {settings.embeddings.model_name} and model for embeddings") + if settings.embeddings.model_engine == "huggingface": + hf_embeddings = HuggingFaceEmbeddings( + model_name=settings.embeddings.model_name, + model_kwargs=model_kwargs, + encode_kwargs=encode_kwargs, + ) + # Load in a specific embedding model + return hf_embeddings + elif settings.embeddings.model_engine == "nvidia-ai-endpoints": + if settings.embeddings.server_url: + logger.info(f"Using embedding model {settings.embeddings.model_name} hosted at {settings.embeddings.server_url}") + return NVIDIAEmbeddings(base_url=f"http://{settings.embeddings.server_url}/v1", model=settings.embeddings.model_name, truncate="END") + else: + logger.info(f"Using embedding model {settings.embeddings.model_name} hosted at api catalog") + return NVIDIAEmbeddings(model=settings.embeddings.model_name, truncate="END") + else: + raise RuntimeError("Unable to find any supported embedding model. Supported engine is huggingface and nvidia-ai-endpoints.") + +@lru_cache +def get_ranking_model() -> BaseDocumentCompressor: + """Create the ranking model. + + Returns: + BaseDocumentCompressor: Base class for document compressors. + """ + + settings = get_config() + + try: + if settings.ranking.model_engine == "nvidia-ai-endpoints": + if settings.ranking.server_url: + logger.info(f"Using ranking model hosted at {settings.ranking.server_url}") + return NVIDIARerank( + base_url=f"http://{settings.ranking.server_url}/v1", top_n=settings.retriever.top_k, truncate="END" + ) + elif settings.ranking.model_name: + logger.info(f"Using ranking model {settings.ranking.model_name} hosted at api catalog") + return NVIDIARerank(model=settings.ranking.model_name, top_n=settings.retriever.top_k, truncate="END") + else: + logger.warning("Unable to find any supported ranking model. Supported engine is nvidia-ai-endpoints.") + except Exception as e: + logger.error(f"An error occurred while initializing ranking_model: {e}") + return None + + +def get_text_splitter() -> SentenceTransformersTokenTextSplitter: + """Return the token text splitter instance from langchain.""" + + if get_config().text_splitter.model_name: + embedding_model_name = get_config().text_splitter.model_name + + return SentenceTransformersTokenTextSplitter( + model_name=embedding_model_name, + tokens_per_chunk=get_config().text_splitter.chunk_size - 2, + chunk_overlap=get_config().text_splitter.chunk_overlap, + ) + + +def get_docs_vectorstore_langchain(vectorstore: VectorStore) -> List[str]: + """Retrieves filenames stored in the vector store implemented in LangChain.""" + + settings = get_config() + try: + # No API availbe in LangChain for listing the docs, thus usig its private _dict + extract_filename = lambda metadata : os.path.splitext(os.path.basename(metadata['source']))[0] + if settings.vector_store.name == "faiss": + in_memory_docstore = vectorstore.docstore._dict + filenames = [extract_filename(doc.metadata) for doc in in_memory_docstore.values()] + filenames = list(set(filenames)) + return filenames + elif settings.vector_store.name == "pgvector": + # No API availbe in LangChain for listing the docs, thus usig its private _make_session + with vectorstore._make_session() as session: + embedding_doc_store = session.query(vectorstore.EmbeddingStore.custom_id, vectorstore.EmbeddingStore.document, vectorstore.EmbeddingStore.cmetadata).all() + filenames = set([extract_filename(metadata) for _, _, metadata in embedding_doc_store if metadata]) + return filenames + elif settings.vector_store.name == "milvus": + # Getting all the ID's > 0 + if vectorstore.col: + milvus_data = vectorstore.col.query(expr="pk >= 0", output_fields=["pk","source", "text"]) + filenames = set([extract_filename(metadata) for metadata in milvus_data]) + return filenames + except Exception as e: + logger.error(f"Error occurred while retrieving documents: {e}") + return [] + +def del_docs_vectorstore_langchain(vectorstore: VectorStore, filenames: List[str]) -> bool: + """Delete documents from the vector index implemented in LangChain.""" + + settings = get_config() + try: + # No other API availbe in LangChain for listing the docs, thus usig its private _dict + extract_filename = lambda metadata : os.path.splitext(os.path.basename(metadata['source']))[0] + if settings.vector_store.name == "faiss": + in_memory_docstore = vectorstore.docstore._dict + for filename in filenames: + ids_list = [doc_id for doc_id, doc_data in in_memory_docstore.items() if extract_filename(doc_data.metadata) == filename] + if not len(ids_list): + logger.info("File does not exist in the vectorstore") + return False + vectorstore.delete(ids_list) + logger.info(f"Deleted documents with filenames {filename}") + elif settings.vector_store.name == "pgvector": + with vectorstore._make_session() as session: + collection = vectorstore.get_collection(session) + filter_by = vectorstore.EmbeddingStore.collection_id == collection.uuid + embedding_doc_store = session.query(vectorstore.EmbeddingStore.custom_id, vectorstore.EmbeddingStore.document, vectorstore.EmbeddingStore.cmetadata).filter(filter_by).all() + for filename in filenames: + ids_list = [doc_id for doc_id, doc_data, metadata in embedding_doc_store if extract_filename(metadata) == filename] + if not len(ids_list): + logger.info("File does not exist in the vectorstore") + return False + vectorstore.delete(ids_list) + logger.info(f"Deleted documents with filenames {filename}") + elif settings.vector_store.name == "milvus": + # Getting all the ID's > 0 + milvus_data = vectorstore.col.query(expr="pk >= 0", output_fields=["pk","source", "text"]) + for filename in filenames: + ids_list = [metadata["pk"] for metadata in milvus_data if extract_filename(metadata) == filename] + if not len(ids_list): + logger.info("File does not exist in the vectorstore") + return False + vectorstore.col.delete(f"pk in {ids_list}") + logger.info(f"Deleted documents with filenames {filename}") + return True + except Exception as e: + logger.error(f"Error occurred while deleting documents: {e}") + return False + return True + +def _combine_dicts(dict_a, dict_b): + """Combines two dictionaries recursively, prioritizing values from dict_b. + + Args: + dict_a: The first dictionary. + dict_b: The second dictionary. + + Returns: + A new dictionary with combined key-value pairs. + """ + + combined_dict = dict_a.copy() # Start with a copy of dict_a + + for key, value_b in dict_b.items(): + if key in combined_dict: + value_a = combined_dict[key] + # Remove the special handling for "command" + if isinstance(value_a, dict) and isinstance(value_b, dict): + combined_dict[key] = _combine_dicts(value_a, value_b) + # Otherwise, replace the value from A with the value from B + else: + combined_dict[key] = value_b + else: + # Add any key not present in A + combined_dict[key] = value_b + + return combined_dict diff --git a/src/ingest_service/Dockerfile b/src/ingest_service/Dockerfile new file mode 100644 index 0000000..acfab50 --- /dev/null +++ b/src/ingest_service/Dockerfile @@ -0,0 +1,45 @@ +ARG BASE_IMAGE_URL=nvcr.io/nvidia/base/ubuntu +ARG BASE_IMAGE_TAG=22.04_20240212 + +FROM ${BASE_IMAGE_URL}:${BASE_IMAGE_TAG} + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV DEBIAN_FRONTEND noninteractive + +# Install required ubuntu packages for setting up python 3.10 +RUN apt update && \ + apt install -y curl software-properties-common && \ + add-apt-repository ppa:deadsnakes/ppa && \ + apt update && apt install -y python3.10 python3.10-dev python3.10-distutils wget && \ + apt-get clean + +# Install pip3 for python3.10 +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10 + +RUN rm -rf /var/lib/apt/lists/* + +# Download the sources of apt packages within the container for standard legal compliance +RUN sed -i 's/# deb-src/deb-src/g' /etc/apt/sources.list +RUN apt update +# xz-utils is needed to pull the source and unpack them correctly +RUN apt install xz-utils -y +RUN mkdir -p /legal/source +WORKDIR /legal/source +# Read installed packages, strip all but the package names, pipe to 'apt source' to download respective packages +RUN apt list --installed | grep -i installed | sed 's|\(.*\)/.*|\1|' | xargs apt source --download-only +# The source is saved in directories as well as tarballs in the current dir +RUN rm xz-utils* +COPY LICENSE-3rd-party.txt /legal/ + +# COPY the dataset and script within the container +COPY ./data /opt/data +COPY ./src/ingest_service /opt/ + +# Uninstall build packages - We are keeping this since we need them for initcontainer in ingest client in kubernetes +# RUN apt autoremove -y software-properties-common + +# Install the ingestion script dependencies within the containers +RUN pip3 install --no-cache-dir -r /opt/requirements.txt + +WORKDIR /opt +RUN bash data/download.sh data/list_manuals.txt \ No newline at end of file diff --git a/src/ingest_service/docker-compose.yaml b/src/ingest_service/docker-compose.yaml new file mode 100644 index 0000000..23220df --- /dev/null +++ b/src/ingest_service/docker-compose.yaml @@ -0,0 +1,14 @@ +services: + ingest-client: + container_name: ingest-client + image: aiva-customer-service-ingest-client:1.0.0 + build: + context: ../../ + dockerfile: ./src/ingest_service/Dockerfile + command: > + /bin/sh -c "python3 import_csv_to_sql.py --host postgres --port 5432 && + python3 ingest_doc.py --host unstructured-retriever --port 8081" + +networks: + default: + name: nvidia-rag \ No newline at end of file diff --git a/src/ingest_service/import_csv_to_sql.py b/src/ingest_service/import_csv_to_sql.py new file mode 100644 index 0000000..d87f09e --- /dev/null +++ b/src/ingest_service/import_csv_to_sql.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv +import re +import psycopg2 +from datetime import datetime + +import argparse + +# Set up the argument parser +parser = argparse.ArgumentParser(description='Database connection parameters.') +parser.add_argument('--dbname', type=str, default='customer_data', help='Database name') +parser.add_argument('--user', type=str, default='postgres', help='Database user') +parser.add_argument('--password', type=str, default='password', help='Database password') +parser.add_argument('--host', type=str, default='localhost', help='Database host') +parser.add_argument('--port', type=str, default='5432', help='Database port') + +# Parse the arguments +args = parser.parse_args() + +# Database connection parameters +db_params = { + 'dbname': args.dbname, + 'user': args.user, + 'password': args.password, + 'host': args.host, + 'port': args.port +} + +# CSV file path +csv_file_path = './data/orders.csv' + +# Connect to the database +conn = psycopg2.connect(**db_params) +cur = conn.cursor() + +# Drop the table if it exists +drop_table_query = ''' +DROP TABLE IF EXISTS customer_data; +''' + +# Create the table if it doesn't exist +create_table_query = ''' +CREATE TABLE IF NOT EXISTS customer_data ( + customer_id INTEGER NOT NULL, + order_id INTEGER NOT NULL, + product_name VARCHAR(255) NOT NULL, + product_description VARCHAR NOT NULL, + order_date DATE NOT NULL, + quantity INTEGER NOT NULL, + order_amount DECIMAL(10, 2) NOT NULL, + order_status VARCHAR(50), + return_status VARCHAR(50), + return_start_date DATE, + return_received_date DATE, + return_completed_date DATE, + return_reason VARCHAR(255), + notes TEXT, + PRIMARY KEY (customer_id, order_id) +); +''' +cur.execute(drop_table_query) +cur.execute(create_table_query) + +# Open the CSV file and insert data +with open(csv_file_path, 'r') as f: + reader = csv.reader(f) + next(reader) # Skip the header row + + for row in reader: + # Access columns by index as per the provided structure + order_id = int(row[1]) # OrderID + customer_id = int(row[0]) # CID (Customer ID) + + # Correcting the order date to include time + order_date = datetime.strptime(row[4], "%Y-%m-%dT%H:%M:%S") # OrderDate with time + + quantity = int(row[5]) # Quantity + + # Handle optional date fields with time parsing + return_start_date = datetime.strptime(row[9], "%Y-%m-%dT%H:%M:%S") if row[9] else None # ReturnStartDate + return_received_date = datetime.strptime(row[10],"%Y-%m-%dT%H:%M:%S") if row[10] else None # ReturnReceivedDate + return_completed_date = datetime.strptime(row[11], "%Y-%m-%dT%H:%M:%S") if row[11] else None # ReturnCompletedDate + + # Clean product name + product_name = re.sub(r'[®™]', '', row[2]) # ProductName + + product_description = re.sub(r'[®™]', '', row[3]) + # OrderAmount as float + order_amount = float(row[6].replace(',', '')) + + # Insert data into the database + cur.execute( + ''' + INSERT INTO customer_data ( + customer_id, order_id, product_name, product_description, order_date, quantity, order_amount, + order_status, return_status, return_start_date, return_received_date, + return_completed_date, return_reason, notes + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ''', + (customer_id, order_id, product_name, product_description, order_date, quantity, order_amount, + row[7], # OrderStatus + row[8], # ReturnStatus + return_start_date, return_received_date, return_completed_date, + row[12], # ReturnReason + row[13]) # Notes + ) + +# Commit the changes and close the connection +conn.commit() +cur.close() +conn.close() + +print("CSV Data imported successfully!") diff --git a/src/ingest_service/ingest_doc.py b/src/ingest_service/ingest_doc.py new file mode 100644 index 0000000..f7ac527 --- /dev/null +++ b/src/ingest_service/ingest_doc.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests +import os +import re +from concurrent.futures import ThreadPoolExecutor, as_completed +import pandas as pd +import argparse + +# Function to create a valid filename +def create_valid_filename(s): + # Remove invalid characters and replace spaces with underscores + s = re.sub(r'[^\w\-_\. ]', '', s) + return s.replace(' ', '_') + + +def csv_to_txt(csv_file="./data/gear-store.csv"): + df = pd.read_csv(csv_file) + os.makedirs('./data/product', exist_ok=True) + # Iterate through each row in the DataFrame + for index, row in df.iterrows(): + # Create filename using name, category, and subcategory + filename = f"{create_valid_filename(row['name'])}_{create_valid_filename(row['category'])}_{create_valid_filename(row['subcategory'])}.txt" + + print(f"Creating file {filename}, current index {index}") + # Full path for the file + filepath = os.path.join('./data/product', filename) + + # Create the content for the file + content = f"Name: {row['name']}\n" + content += f"Category: {row['category']}\n" + content += f"Subcategory: {row['subcategory']}\n" + content += f"Price: ${row['price']}\n" + content += f"Description: {row['description']}\n" + + # Write the content to the file + with open(filepath, 'w', encoding='utf-8') as file: + file.write(content) + + print(f"Created {len(df)} files in ./data/product") + +def get_health(url: str): + health_url = f'{url}/health' + headers = { + 'accept': 'application/json' + } + response = requests.get(health_url, headers=headers) + return response.status_code + +def ingest_manuals( url: str, directory_path='./data/manuals_pdf'): + document_url = f'{url}/documents' + for filename in os.listdir(directory_path): + # Check if the file is a PDF + if filename.endswith('.pdf'): + file_path = os.path.join(directory_path, filename) + + # Open the file in binary mode and send it in a POST request + with open(file_path, 'rb') as file: + files = {'file': file} + response = requests.post(document_url, files=files) + + # Print the response from the server + print(f'Uploaded {filename}: {response.status_code}') + +def ingest_faqs(url: str, filename = "./data/FAQ.pdf"): + document_url = f'{url}/documents' + with open(filename, 'rb') as file: + files = {'file': file} + try: + response = requests.post(document_url, files=files) + print(f'Uploaded {filename}: {response.status_code}') + return response.status_code == 200 + except requests.exceptions.RequestException as e: + print(f"Request failed for {filename}: {e}") + return False + + +#Skipping get the list of documents + +def ingest_csvs(url: str, directory_path='./data/product',max_workers = 5): + filepaths = [os.path.join(directory_path, filename) for filename in os.listdir(directory_path) if filename.endswith(".txt")] + successfully_ingested = [] + failed_ingestion = [] + with ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_file = {executor.submit(ingest_faqs, url, filepath): filepath for filepath in filepaths} + + for future in as_completed(future_to_file): + filepath = future_to_file[future] + try: + if future.result(): + print(f"Successfully Ingested {os.path.basename(filepath)}") + successfully_ingested.append(filepath) + else: + print(f"Failed to Ingest {os.path.basename(filepath)}") + failed_ingestion.append(filepath) + except Exception as e: + print(f"Exception occurred while ingesting {os.path.basename(filepath)}: {e}") + failed_ingestion.append(filepath) + + print(f"Total files successfully ingested: {len(successfully_ingested)}") + print(f"Total files failed ingestion: {len(failed_ingestion)}") + +#Skipping Document delete + +if __name__ == "__main__": + # Set up the argument parser + parser = argparse.ArgumentParser(description='Database connection parameters.') + parser.add_argument('--host', type=str, default='localhost', help='Database host') + parser.add_argument('--port', type=str, default='8086', help='Database port') + args = parser.parse_args() + + url = f'http://{args.host}:{args.port}' + + health_code = get_health(url) + + print(health_code) + + ingest_manuals(url=url) + ingest_faqs(url=url) + csv_to_txt() + ingest_csvs(url=url) diff --git a/src/ingest_service/proxy_server.py b/src/ingest_service/proxy_server.py new file mode 100644 index 0000000..cebcb5b --- /dev/null +++ b/src/ingest_service/proxy_server.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" This module is just a proxy server which calls agent container's health API """ + +import os +import logging +from fastapi import FastAPI +from pydantic import BaseModel, Field + +logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO").upper()) +logger = logging.getLogger(__name__) + +# create the FastAPI server +app = FastAPI() + + +class HealthResponse(BaseModel): + """Health check response""" + + message: str = Field(max_length=4096, pattern=r"[\s\S]*", default="") + + +@app.get( + "/health", + response_model=HealthResponse, + responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + }, + } + }, +) +def health_check(): + """ + Perform a Health Check + + Returns 200 when service is up. This does not check the health of downstream services. + """ + + response_message = "Service is up." + return HealthResponse(message=response_message) diff --git a/src/ingest_service/requirements.txt b/src/ingest_service/requirements.txt new file mode 100644 index 0000000..b17c620 --- /dev/null +++ b/src/ingest_service/requirements.txt @@ -0,0 +1,6 @@ +psycopg2-binary==2.9.10 +pandas==2.2.3 +requests==2.31.0 +fastapi==0.115.2 +uvicorn[standard]==0.27.1 +starlette==0.40.0 \ No newline at end of file diff --git a/src/retrievers/Dockerfile b/src/retrievers/Dockerfile new file mode 100644 index 0000000..8de44cb --- /dev/null +++ b/src/retrievers/Dockerfile @@ -0,0 +1,69 @@ +ARG BASE_IMAGE_URL=nvcr.io/nvidia/base/ubuntu +ARG BASE_IMAGE_TAG=22.04_20240212 + +FROM ${BASE_IMAGE_URL}:${BASE_IMAGE_TAG} as license_base + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV DEBIAN_FRONTEND noninteractive + +# Install required ubuntu packages for setting up python 3.10 +RUN apt update && \ + apt install -y curl software-properties-common libgl1 libglib2.0-0 && \ + add-apt-repository ppa:deadsnakes/ppa && \ + apt update && apt install -y python3.10 && \ + apt-get clean + +# Install pip for python3.10 +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10 + +RUN rm -rf /var/lib/apt/lists/* + +# Uninstall build packages +RUN apt autoremove -y curl software-properties-common + +# Download the sources of apt packages within the container for standard legal compliance +RUN sed -i 's/# deb-src/deb-src/g' /etc/apt/sources.list +RUN apt update +# xz-utils is needed to pull the source and unpack them correctly +RUN apt install xz-utils -y +RUN mkdir -p /legal/source +WORKDIR /legal/source +# Read installed packages, strip all but the package names, pipe to 'apt source' to download respective packages +RUN apt list --installed | grep -i installed | sed 's|\(.*\)/.*|\1|' | xargs apt source --download-only +# The source is saved in directories as well as tarballs in the current dir +RUN rm xz-utils* +COPY LICENSE-3rd-party.txt /legal/ + +# Install any example specific dependency if available +ARG EXAMPLE_PATH +COPY ${EXAMPLE_PATH} /opt/${EXAMPLE_PATH} +RUN if [ -f "/opt/${EXAMPLE_PATH}/requirements.txt" ] ; then \ + pip3 install --no-cache-dir -r /opt/${EXAMPLE_PATH}/requirements.txt ; else \ + echo "Skipping example dependency installation, since requirements.txt was not found" ; \ + fi + +RUN if [ "${EXAMPLE_PATH}" = "src/retrievers/unstructured_data" ]; then \ + mkdir -p /tmp-data/nltk_data/ && \ + chmod -R 777 /tmp-data && \ + chown -R 1000:1000 /tmp-data && \ + export NLTK_DATA=/tmp-data/nltk_data/ && \ + export HF_HOME=/tmp-data && \ + python3.10 -m nltk.downloader averaged_perceptron_tagger && \ + python3.10 -m nltk.downloader averaged_perceptron_tagger_eng && \ + python3.10 -m nltk.downloader stopwords && \ + python3.10 -m nltk.downloader punkt && \ + python3.10 -m nltk.downloader punkt_tab && \ + python3.10 -c "from sentence_transformers import SentenceTransformer; model = SentenceTransformer('Snowflake/snowflake-arctic-embed-l'); model.save('/tmp-data')" \ +; fi + +# export inside the above block is not working +ENV NLTK_DATA=/tmp-data/nltk_data/ +ENV HF_HOME=/tmp-data + +# Copy required common modules +COPY src/common /opt/src/common +COPY src/retrievers/server.py /opt/src/retrievers/ +COPY src/retrievers/base.py /opt/src/retrievers/ + +WORKDIR /opt +ENTRYPOINT ["uvicorn", "src.retrievers.server:app"] diff --git a/src/retrievers/base.py b/src/retrievers/base.py new file mode 100644 index 0000000..02f8bc4 --- /dev/null +++ b/src/retrievers/base.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base interface that all Retriever examples should implement.""" + +from abc import ABC, abstractmethod +from typing import List, Dict, Any + +class BaseExample(ABC): + + @abstractmethod + def document_search(self, content: str, num_docs: int) -> List[Dict[str, Any]]: + pass + + @abstractmethod + def get_documents(self) -> List[str]: + pass + + @abstractmethod + def delete_documents(self, filenames: List[str]) -> bool: + pass + + @abstractmethod + def ingest_docs(self, data_dir: str, filename: str) -> None: + pass \ No newline at end of file diff --git a/src/retrievers/server.py b/src/retrievers/server.py new file mode 100644 index 0000000..6e863e7 --- /dev/null +++ b/src/retrievers/server.py @@ -0,0 +1,283 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The definition of the Retrievers FASTAPI server.""" + +import os +import shutil +import logging +from pathlib import Path +from typing import Any, Dict, List, Optional +import importlib +from inspect import getmembers, isclass +from fastapi import FastAPI, File, UploadFile, Request +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from pydantic import BaseModel, Field, constr + +logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper()) +logger = logging.getLogger(__name__) + +tags_metadata = [ + { + "name": "Health", + "description": "APIs for checking and monitoring server liveliness and readiness.", + }, + {"name": "Core", "description": "Core APIs for ingestion and searching."}, + {"name": "Management", "description": "APIs for deleting and listing ingested files."}, +] + +# create the FastAPI server +app = FastAPI(title="Retriever API's for AI Virtual Assistant for Customer Service", + description="This API schema describes all the retriever endpoints exposed for the AI Virtual Assistant for Customer Service NIM Blueprint", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc", + openapi_tags=tags_metadata, +) + +# Allow access in browser from RAG UI and Storybook (development) +origins = [ + "*" +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +EXAMPLE_DIR = "./" + +class DocumentSearch(BaseModel): + """Definition of the DocumentSearch API data type.""" + + query: str = Field(description="The content or keywords to search for within documents.", max_length=131072, pattern=r'[\s\S]*', default="") + top_k: int = Field(description="The maximum number of documents to return in the response.", default=4, ge=0, le=25, format="int64") + user_id: Optional[str] = Field(description="An optional unique identifier for the customer.", default=None) + conv_history: Optional[List[Dict[str, str]]] = Field(description="An optional conversation history for the customer.", default=[]) + +class DocumentChunk(BaseModel): + """Represents a chunk of a document.""" + content: str = Field(description="The content of the document chunk.", max_length=131072, pattern=r'[\s\S]*', default="") + filename: str = Field(description="The name of the file the chunk belongs to.", max_length=4096, pattern=r'[\s\S]*', default="") + score: float = Field(..., description="The relevance score of the chunk.") + +class DocumentSearchResponse(BaseModel): + """Represents a response from a document search.""" + chunks: List[DocumentChunk] = Field(..., description="List of document chunks.", max_items=256) + +class DocumentsResponse(BaseModel): + """Represents the response containing a list of documents.""" + documents: List[constr(max_length=131072, pattern=r'[\s\S]*')] = Field(description="List of filenames.", max_items=1000000, default=[]) + +class HealthResponse(BaseModel): + message: str = Field(max_length=4096, pattern=r'[\s\S]*', default="") + +@app.on_event("startup") +def import_example() -> None: + """ + Import the example class from the specified example file. + The example directory is expected to have a python file where the example class is defined. + """ + + file_location = os.path.join(EXAMPLE_DIR, os.environ.get("EXAMPLE_PATH", "./")) + + for root, dirs, files in os.walk(file_location): + for file in files: + if not file.endswith(".py"): + continue + + # Import the specified file dynamically + spec = importlib.util.spec_from_file_location(name="example", location=os.path.join(root, file)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Scan each class in the file to find one with the 3 implemented methods: ingest_docs, rag_chain and llm_chain + for name, _ in getmembers(module, isclass): + try: + cls = getattr(module, name) + if set(["ingest_docs"]).issubset(set(dir(cls))): + if name == "BaseExample": + continue + example = cls() + app.example = cls + return + except: + raise ValueError(f"Class {name} is not implemented and could not be instantiated.") + + raise NotImplementedError(f"Could not find a valid example class in {file_location}") + +@app.exception_handler(RequestValidationError) +async def request_validation_exception_handler( + request: Request, exc: RequestValidationError +) -> JSONResponse: + return JSONResponse( + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": jsonable_encoder(exc.errors(), exclude={"input"})}) + + +@app.get("/health", tags=["Health"], response_model=HealthResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +def health_check(): + """ + Perform a Health Check + + Returns 200 when service is up. This does not check the health of downstream services. + """ + + response_message = "Service is up." + return HealthResponse(message=response_message) + + +@app.post("/documents", tags=["Core"], responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def upload_document(request: Request, file: UploadFile = File(...)) -> JSONResponse: + """Upload a document to the vector store.""" + + if not file.filename: + return JSONResponse(content={"message": "No files provided"}, status_code=200) + + try: + upload_folder = "/tmp-data/uploaded_files" + upload_file = os.path.basename(file.filename) + if not upload_file: + raise RuntimeError("Error parsing uploaded filename.") + file_path = os.path.join(upload_folder, upload_file) + uploads_dir = Path(upload_folder) + uploads_dir.mkdir(parents=True, exist_ok=True) + + with open(file_path, "wb") as f: + shutil.copyfileobj(file.file, f) + + app.example().ingest_docs(file_path, upload_file) + + return JSONResponse( + content={"message": "File uploaded successfully"}, status_code=200 + ) + + except Exception as e: + logger.error("Error from POST /documents endpoint. Ingestion of file: " + file.filename + " failed with error: " + str(e)) + return JSONResponse( + content={"message": str(e)}, status_code=500 + ) + + +@app.post("/search", tags=["Core"], response_model=DocumentSearchResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def document_search(request: Request, data: DocumentSearch) -> Dict[str, List[Dict[str, Any]]]: + """Search for the most relevant documents for the given search parameters.""" + + try: + example = app.example() + if hasattr(example, "document_search") and callable(example.document_search): + # This is needed as structured_rag needs user_id aka user + if data.user_id: + search_result = example.document_search(data.query, data.top_k, data.user_id, data.conv_history) + else: + search_result = example.document_search(data.query, data.top_k, data.conv_history) + chunks = [] + for entry in search_result: + content = entry.get("content", "") # Default to empty string if "content" key doesn't exist + source = entry.get("source", "") # Default to empty string if "source" key doesn't exist + score = entry.get("score", 0.0) # Default to 0.0 if "score" key doesn't exist + chunk = DocumentChunk(content=content, filename=source, document_id="", score=score) + chunks.append(chunk) + return DocumentSearchResponse(chunks=chunks) + raise NotImplementedError("Example class has not implemented the document_search method.") + + except Exception as e: + logger.error(f"Error from POST /search endpoint. Error details: {e}") + return JSONResponse(content={"message": "Error occurred while searching documents."}, status_code=500) + + +@app.get("/documents", tags=["Management"], response_model=DocumentsResponse, responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def get_documents(request: Request) -> DocumentsResponse: + """List available documents.""" + try: + example = app.example() + if hasattr(example, "get_documents") and callable(example.get_documents): + documents = example.get_documents() + return DocumentsResponse(documents=documents) + else: + raise NotImplementedError("Example class has not implemented the get_documents method.") + + except Exception as e: + logger.error(f"Error from GET /documents endpoint. Error details: {e}") + return JSONResponse(content={"message": "Error occurred while fetching documents."}, status_code=500) + + +@app.delete("/documents", tags=["Management"], responses={ + 500: { + "description": "Internal Server Error", + "content": { + "application/json": { + "example": {"detail": "Internal server error occurred"} + } + } + } +}) +async def delete_document(request: Request, filename: str) -> JSONResponse: + """Delete a document.""" + try: + example = app.example() + if hasattr(example, "delete_documents") and callable(example.delete_documents): + status = example.delete_documents([filename]) + if not status: + raise Exception(f"Error in deleting document {filename}") + return JSONResponse(content={"message": f"Document {filename} deleted successfully"}, status_code=200) + + raise NotImplementedError("Example class has not implemented the delete_document method.") + + except Exception as e: + logger.error(f"Error from DELETE /documents endpoint. Error details: {e}") + return JSONResponse(content={"message": f"Error deleting document {filename}"}, status_code=500) diff --git a/src/retrievers/structured_data/__init__.py b/src/retrievers/structured_data/__init__.py new file mode 100644 index 0000000..a08b2c2 --- /dev/null +++ b/src/retrievers/structured_data/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/retrievers/structured_data/chains.py b/src/retrievers/structured_data/chains.py new file mode 100644 index 0000000..f89d8c4 --- /dev/null +++ b/src/retrievers/structured_data/chains.py @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Retriever pipeline for extracting data from structured information""" +import logging +from typing import Any, Dict, List + +from pandasai import Agent as PandasAI_Agent +from pandasai.responses.response_parser import ResponseParser +from langchain.prompts import ( + ChatPromptTemplate, + HumanMessagePromptTemplate, + SystemMessagePromptTemplate, +) +from src.retrievers.structured_data.connector import get_postgres_connector +from src.retrievers.base import BaseExample +from src.common.utils import get_config, get_prompts +from src.retrievers.structured_data.pandasai.llms.nv_aiplay import NVIDIA as PandasAI_NVIDIA + +logger = logging.getLogger(__name__) +settings = get_config() + + +class PandasDataFrame(ResponseParser): + """Returns Pandas Dataframe instead of SmartDataFrame""" + + def __init__(self, context) -> None: + super().__init__(context) + + def format_dataframe(self, result): + return result["value"] + + +class CSVChatbot(BaseExample): + """RAG example showcasing CSV parsing using Pandas AI Agent""" + + def ingest_docs(self, filepath: str, filename: str): + """Ingest documents to the VectorDB.""" + + raise NotImplementedError("Canonical RAG only supports document retrieval") + + def document_search(self, content: str, num_docs: int, user_id: str = None, conv_history: Dict[str, str] = {}) -> List[Dict[str, Any]]: + """Execute a Document Search.""" + + logger.info("Using document_search to fetch response from database as text") + postgres_connector = None # Initialize connector + + try: + logger.info("Using document_search to fetch response from database as text") + if user_id: + postgres_connector = get_postgres_connector(user_id) + else: + logger.warning("Enter a proper User ID") + return [{"content": "No response generated, make to give a proper User ID."}] + + # TODO: Pass conv history to the LLM + llm_data_retrieval = PandasAI_NVIDIA(temperature=0.2, model=settings.llm.model_name_pandas_ai) + + config_data_retrieval = {"llm": llm_data_retrieval, "response_parser": PandasDataFrame, "max_retries": 1, "enable_cache": False} + agent_data_retrieval = PandasAI_Agent([postgres_connector], config=config_data_retrieval, memory_size=20) + + prompt_config = get_prompts().get("prompts") + + data_retrieval_prompt = ChatPromptTemplate( + messages=[ + SystemMessagePromptTemplate.from_template(prompt_config.get("data_retrieval_template", [])), + HumanMessagePromptTemplate.from_template("{query}"), + ], + input_variables=["description", "instructions", "query"], + ) + + + chat_prompt = data_retrieval_prompt.format_prompt( + description=prompt_config.get("dataframe_prompts").get("customer_data").get("description"), + instructions=prompt_config.get("dataframe_prompts").get("customer_data").get("instructions"), + query=content, + ).to_string() + + result_df = agent_data_retrieval.chat( + chat_prompt + ) + logger.info("Result Data Frame: %s", result_df) + if not result_df: + logger.warning("Retrieval failed to get any relevant context") + return [{"content": "No response generated from LLM, make sure your query is relavent to the ingested document."}] + + result_df = str(result_df) + return [{"content": result_df}] + except Exception as e: + logger.error("An error occurred during document search: %s", str(e)) + raise # Re-raise the exception after logging + + finally: + if postgres_connector: + postgres_connector._connection._dbapi_connection.close() + postgres_connector._connection.close() + postgres_connector._engine.dispose() + import gc + gc.collect() + logger.info("Postgres connector deleted.") + + def get_documents(self) -> List[str]: + """Retrieves filenames stored in the vector store.""" + logger.error("get_documents not implemented") + return True + + def delete_documents(self, filenames: List[str]): + """Delete documents from the vector index.""" + logger.error("delete_documents not implemented") + return True diff --git a/src/retrievers/structured_data/connector.py b/src/retrievers/structured_data/connector.py new file mode 100644 index 0000000..e6029ef --- /dev/null +++ b/src/retrievers/structured_data/connector.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from urllib.parse import urlparse +from src.common.utils import get_config +from pandasai.connectors import PostgreSQLConnector + +def get_postgres_connector(customer_id: str) -> PostgreSQLConnector: + + app_database_url = get_config().database.url + + # Parse the URL + parsed_url = urlparse(f"//{app_database_url}", scheme='postgres') + + # Extract host and port + host = parsed_url.hostname + port = parsed_url.port + + config = { + "host": host, + "port": port, + "database": os.getenv('POSTGRES_DB', None), + "username": os.getenv('POSTGRES_USER', None), + "password": os.getenv('POSTGRES_PASSWORD', None), + "table": "customer_data", + "where": [ + ["customer_id", "=", customer_id], + ], + } + return PostgreSQLConnector(config=config) \ No newline at end of file diff --git a/src/retrievers/structured_data/pandasai/llms/__init__.py b/src/retrievers/structured_data/pandasai/llms/__init__.py new file mode 100644 index 0000000..a08b2c2 --- /dev/null +++ b/src/retrievers/structured_data/pandasai/llms/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/retrievers/structured_data/pandasai/llms/nv_aiplay.py b/src/retrievers/structured_data/pandasai/llms/nv_aiplay.py new file mode 100644 index 0000000..5e5781c --- /dev/null +++ b/src/retrievers/structured_data/pandasai/llms/nv_aiplay.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A wrapper on PandasAI base LLM class to use NVIDIA Foundational Models in PandasAI Agents""" + +import logging +from typing import Any, Dict, Optional + +from langchain_nvidia_ai_endpoints import ChatNVIDIA +from pandasai.llm.base import LLM +from pandasai.pipelines.pipeline_context import PipelineContext +from pandasai.prompts.base import BasePrompt + +from src.common.utils import get_config + +logger = logging.getLogger(__name__) + + +class NVIDIA(LLM): + """ + A wrapper class on PandasAI base LLM class to NVIDIA Foundational Models. + """ + + temperature: Optional[float] = 0.1 + max_tokens: Optional[int] = 1000 + top_p: Optional[float] = 1 + model: Optional[str] = "llama2_13b" + + _chat_model: "ChatNVIDIA" = None + + def __init__(self, **kwargs): + self._set_params(**kwargs) + settings = get_config() + if settings.llm.server_url: + logger.info(f"Using llm model {settings.llm.model_name} hosted at {settings.llm.server_url} in PandasAI") + # self._chat_model = ChatNVIDIA(**self._default_params).mode( + # "nim", base_url=f"http://{settings.llm.server_url}/v1" + # ) + if 'model' in self._default_params: del self._default_params['model'] + self._chat_model = ChatNVIDIA(base_url=f"http://{settings.llm.server_url}/v1", + **self._default_params) + else: + logger.info(f"Using llm model {settings.llm.model_name} from api catalog in PandasAI") + self._chat_model = ChatNVIDIA(**self._default_params) + self._prompt = "" + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling NVIDIA Foundational LLMs.""" + params: Dict[str, Any] = { + "model": self.model, + "temperature": self.temperature, + "top_p": self.top_p, + "max_tokens": self.max_tokens, + } + + return params + + @property + def type(self) -> str: + return "nvidia-foundational-llm" + + def _set_params(self, **kwargs): + """ + Set Parameters + Args: + **kwargs: ["model","temperature","max_tokens", + "top_p"] + + Returns: + None. + """ + + valid_params = [ + "model", + "temperature", + "max_tokens", + "top_p", + ] + for key, value in kwargs.items(): + if key in valid_params: + setattr(self, key, value) + + def call(self, instruction: BasePrompt, context: PipelineContext = None) -> str: + """ + Call the NVIDIA Foundational LLMs. + Args: + instruction (BasePrompt): A prompt object with instruction for LLM. + suffix (str): A string representing the suffix to be truncated + from the generated response. + + Returns + str: LLM response. + + """ + + self._prompt = instruction.to_string() + response = self._chat_model.invoke(self._prompt) + return response.content diff --git a/src/retrievers/structured_data/prompt.yaml b/src/retrievers/structured_data/prompt.yaml new file mode 100644 index 0000000..cd406aa --- /dev/null +++ b/src/retrievers/structured_data/prompt.yaml @@ -0,0 +1,25 @@ +prompts: + + data_retrieval_template: | + You are an expert data retrieval agent who writes functional python code and utilzes Pandas library in python for data retrieval. + + Provide a functional and accurate code based on the provided pandas dataframe for the user's query. + + Your job is to write python code that uses Pandas library for extracting and processing information based on the given Pandas dataframe. + + The data you are provided contains information about: {description} + + These are some instructions which must be followed while generating the code, all instructions start with a hifen(-): + - dfs is a list containing df a pandas dataframe. Always use the first entry from the list like df = dfs[0]. + {instructions} + + dataframe_prompts: + customer_data: + description: | + This data frame tracks customer orders, including product details, order quantity, and order amount. + It also includes delivery status, return status, and relevant dates for returns, along with reasons and notes for any issues. + The data provides a snapshot of order and return processes for a single customer. + instructions: | + - Provide meaningful error messages in the format {"error": "some message"} if the user queries do not match any records or if an invalid input is given. + For example, respond with {"error": "No records found for the specified criteria."} when applicable. + diff --git a/src/retrievers/structured_data/requirements.txt b/src/retrievers/structured_data/requirements.txt new file mode 100644 index 0000000..995ebb4 --- /dev/null +++ b/src/retrievers/structured_data/requirements.txt @@ -0,0 +1,11 @@ +fastapi==0.115.2 +uvicorn[standard]==0.27.1 +starlette==0.40.0 +python-multipart==0.0.9 +langchain==0.2.16 +langchain-nvidia-ai-endpoints==0.2.2 +dataclass-wizard==0.22.3 +pandas==1.5.3 +pandasai==2.2.14 +numexpr==2.9.0 +psycopg2-binary==2.9.9 \ No newline at end of file diff --git a/src/retrievers/unstructured_data/chains.py b/src/retrievers/unstructured_data/chains.py new file mode 100644 index 0000000..af0e01a --- /dev/null +++ b/src/retrievers/unstructured_data/chains.py @@ -0,0 +1,206 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +from typing import Any, Dict, List +from traceback import print_exc + +from langchain_community.document_loaders import UnstructuredFileLoader +from langchain_core.runnables import RunnablePassthrough, RunnableAssign +from langchain_core.prompts.chat import ChatPromptTemplate +from langchain_core.prompts import MessagesPlaceholder +from pydantic import BaseModel, Field + +from src.retrievers.base import BaseExample +from src.common.utils import ( + create_vectorstore_langchain, + del_docs_vectorstore_langchain, + get_config, + get_docs_vectorstore_langchain, + get_embedding_model, + get_prompts, + get_llm, + get_text_splitter, + get_vectorstore, + get_ranking_model +) + +logger = logging.getLogger(__name__) +document_embedder = get_embedding_model() +text_splitter = None +settings = get_config() +prompts = get_prompts() +vector_db_top_k = int(os.environ.get(f"VECTOR_DB_TOPK", 40)) + +try: + vectorstore = create_vectorstore_langchain(document_embedder=document_embedder) +except Exception as e: + vectorstore = None + logger.info(f"Unable to connect to vector store during initialization: {e}") + + +class UnstructuredRetriever(BaseExample): + def ingest_docs(self, filepath: str, filename: str) -> None: + """Ingests documents to the VectorDB. + It's called when the POST endpoint of `/documents` API is invoked. + + Args: + filepath (str): The path to the document file. + filename (str): The name of the document file. + + Raises: + ValueError: If there's an error during document ingestion or the file format is not supported. + """ + if not filename.endswith((".txt", ".pdf", ".md")): + raise ValueError(f"{filename} is not a valid Text, PDF or Markdown file") + try: + # Load raw documents from the directory + _path = filepath + raw_documents = UnstructuredFileLoader(_path).load() + + if raw_documents: + global text_splitter + # Get text splitter instance, it is selected based on environment variable APP_TEXTSPLITTER_MODELNAME + # tokenizer dimension of text splitter should be same as embedding model + if not text_splitter: + text_splitter = get_text_splitter() + + # split documents based on configuration provided + documents = text_splitter.split_documents(raw_documents) + vs = get_vectorstore(vectorstore, document_embedder) + # ingest documents into vectorstore + vs.add_documents(documents) + else: + logger.warning("No documents available to process!") + except Exception as e: + logger.error(f"Failed to ingest document due to exception {e}") + raise ValueError("Failed to upload document. Please upload an unstructured text document.") + + + def document_search(self, content: str, num_docs: int, conv_history: Dict[str, str] = {}) -> List[Dict[str, Any]]: + """Search for the most relevant documents for the given search parameters. + It's called when the `/search` API is invoked. + + Args: + content (str): Query to be searched from vectorstore. + num_docs (int): Number of similar docs to be retrieved from vectorstore. + """ + + logger.info(f"Searching relevant document for the query: {content}") + + try: + vs = get_vectorstore(vectorstore, document_embedder) + if vs == None: + logger.error(f"Vector store not initialized properly. Please check if the vector db is up and running") + raise ValueError() + + docs = [] + ranker = get_ranking_model() + top_k = vector_db_top_k if ranker else num_docs + logger.info(f"Setting top k as: {top_k}.") + retriever = vs.as_retriever(search_kwargs={"k": top_k}) # milvus does not support similarily threshold + + # Invoke query rewriting to decontextualize the query before sending to retriever pipeline if conv history is passed + if conv_history: + class Question(BaseModel): + question: str = Field(..., description="A standalone question which can be understood without the chat history") + + parsed_conv_history = [(msg.get("role"), msg.get("content")) for msg in conv_history] + default_llm_kwargs = {"temperature": 0.2, "top_p": 0.7, "max_tokens": 1024} + llm = get_llm(**default_llm_kwargs) + llm = llm.with_structured_output(Question) + query_rewriter_prompt = prompts.get("query_rewriting") + contextualize_q_prompt = ChatPromptTemplate.from_messages( + [("system", query_rewriter_prompt), MessagesPlaceholder("chat_history"), ("human", "{input}"),] + ) + q_prompt = contextualize_q_prompt | llm + logger.info(f"Query rewriter prompt: {contextualize_q_prompt}") + response = q_prompt.invoke({"input": content, "chat_history": parsed_conv_history}) + content = response.question + logger.info(f"Rewritten Query: {content}") + if content.replace('"', "'") == "''" or len(content) == 0: + return [] + + if ranker: + logger.info(f"Narrowing the collection from {top_k} results and further narrowing it to {num_docs} with the reranker for rag chain.") + # Update number of document to be retriever by ranker + ranker.top_n = num_docs + + context_reranker = RunnableAssign({"context": lambda input: ranker.compress_documents(query=input['question'], documents=input['context'])}) + + retriever = {"context": retriever, "question": RunnablePassthrough()} | context_reranker + docs = retriever.invoke(content) + resp = [] + for doc in docs.get("context"): + resp.append( + { + "source": os.path.basename(doc.metadata.get("source", "")), + "content": doc.page_content, + "score": doc.metadata.get("relevance_score", 0) + } + ) + return resp + else: + docs = retriever.invoke(content) + resp = [] + for doc in docs: + resp.append( + { + "source": os.path.basename(doc.metadata.get("source", "")), + "content": doc.page_content, + "score": doc.metadata.get("relevance_score", 0) + } + ) + return resp + + except Exception as e: + logger.warning(f"Failed to generate response due to exception {e}") + print_exc() + + return [] + + + def get_documents(self) -> List[str]: + """Retrieves filenames stored in the vector store. + It's called when the GET endpoint of `/documents` API is invoked. + + Returns: + List[str]: List of filenames ingested in vectorstore. + """ + try: + vs = get_vectorstore(vectorstore, document_embedder) + if vs: + return get_docs_vectorstore_langchain(vs) + except Exception as e: + logger.error(f"Vectorstore not initialized. Error details: {e}") + return [] + + + def delete_documents(self, filenames: List[str]) -> bool: + """Delete documents from the vector index. + It's called when the DELETE endpoint of `/documents` API is invoked. + + Args: + filenames (List[str]): List of filenames to be deleted from vectorstore. + """ + try: + # Get vectorstore instance + vs = get_vectorstore(vectorstore, document_embedder) + if vs: + return del_docs_vectorstore_langchain(vs, filenames) + except Exception as e: + logger.error(f"Vectorstore not initialized. Error details: {e}") + return False diff --git a/src/retrievers/unstructured_data/prompt.yaml b/src/retrievers/unstructured_data/prompt.yaml new file mode 100644 index 0000000..afd3254 --- /dev/null +++ b/src/retrievers/unstructured_data/prompt.yaml @@ -0,0 +1,4 @@ +query_rewriting: | + "Given a chat history and the latest user question which might reference context in the chat history, formulate a standalone question which can be understood without the chat history. + Do NOT answer the question, just reformulate it if needed and otherwise return it as is. + It should strictly be a query not an answer." \ No newline at end of file diff --git a/src/retrievers/unstructured_data/requirements.txt b/src/retrievers/unstructured_data/requirements.txt new file mode 100644 index 0000000..c77baad --- /dev/null +++ b/src/retrievers/unstructured_data/requirements.txt @@ -0,0 +1,11 @@ +fastapi==0.115.2 +uvicorn[standard]==0.27.1 +starlette==0.40.0 +langchain-nvidia-ai-endpoints==0.2.2 +dataclass-wizard==0.22.3 +nltk==3.9.1 +unstructured[all-docs]==0.12.5 +python-multipart==0.0.9 +langchain-community==0.2.17 +pymilvus==2.4.0 +sentence-transformers==3.0.0 \ No newline at end of file