Repository: GoogleCloudPlatform/Open_Data_QnA Branch: main Commit: 19960bb38ba2 Files: 253 Total size: 1.0 MB Directory structure: gitextract_yfzkgoxq/ ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── OWNERS ├── README.md ├── SECURITY.md ├── agents/ │ ├── BuildSQLAgent.py │ ├── DebugSQLAgent.py │ ├── DescriptionAgent.py │ ├── EmbedderAgent.py │ ├── ResponseAgent.py │ ├── ValidateSQLAgent.py │ ├── VisualizeAgent.py │ ├── __init__.py │ └── core.py ├── app.py ├── backend-apis/ │ ├── README.md │ ├── __init__.py │ ├── main.py │ └── policy.yaml ├── config.ini ├── dbconnectors/ │ ├── BQConnector.py │ ├── FirestoreConnector.py │ ├── PgConnector.py │ ├── __init__.py │ └── core.py ├── docs/ │ ├── README.md │ ├── architecture.md │ ├── best_practices.md │ ├── changelog.md │ ├── config_guide.md │ ├── faq.md │ └── repo_structure.md ├── embeddings/ │ ├── __init__.py │ ├── kgq_embeddings.py │ ├── retrieve_embeddings.py │ └── store_embeddings.py ├── env_setup.py ├── frontend/ │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── database.indexes.json │ ├── database.rules.json │ ├── firebase_setup.json │ ├── frontend-flutter/ │ │ ├── .flutter-plugins │ │ ├── .flutter-plugins-dependencies │ │ ├── Open Data QnA - Working Sheet V2 - sample_questions_UI copy.csv │ │ ├── Open_Data_QnA_sample_questions_v3 copy.csv │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── android/ │ │ │ ├── .gitignore │ │ │ ├── app/ │ │ │ │ ├── build.gradle │ │ │ │ ├── google-services.json │ │ │ │ └── src/ │ │ │ │ ├── debug/ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── main/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ ├── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── pilotcap/ │ │ │ │ │ │ └── ttmd/ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── res/ │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21/ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values/ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night/ │ │ │ │ │ └── styles.xml │ │ │ │ └── profile/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── build.gradle │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ ├── nl2sql_oss_android.iml │ │ │ └── settings.gradle │ │ ├── build/ │ │ │ └── web/ │ │ │ └── .last_build_id │ │ ├── ios/ │ │ │ ├── .gitignore │ │ │ ├── Flutter/ │ │ │ │ ├── AppFrameworkInfo.plist │ │ │ │ ├── Debug.xcconfig │ │ │ │ └── Release.xcconfig │ │ │ ├── Podfile │ │ │ ├── Runner/ │ │ │ │ ├── AppDelegate.swift │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── README.md │ │ │ │ ├── Base.lproj/ │ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ │ └── Main.storyboard │ │ │ │ ├── GoogleService-Info.plist │ │ │ │ ├── Info.plist │ │ │ │ └── Runner-Bridging-Header.h │ │ │ ├── Runner.xcodeproj/ │ │ │ │ ├── project.pbxproj │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ │ └── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ └── Runner.xcscheme │ │ │ ├── Runner.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata/ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── RunnerTests/ │ │ │ └── RunnerTests.swift │ │ ├── lib/ │ │ │ ├── firebase_options.dart │ │ │ ├── main.dart │ │ │ ├── screens/ │ │ │ │ ├── bot.dart │ │ │ │ ├── bot_chat_view.dart │ │ │ │ ├── disclaimer.dart │ │ │ │ └── settings.dart │ │ │ ├── services/ │ │ │ │ ├── display_stepper/ │ │ │ │ │ ├── display_stepper_cubit.dart │ │ │ │ │ └── display_stepper_state.dart │ │ │ │ ├── first_question/ │ │ │ │ │ ├── first_question_cubit.dart │ │ │ │ │ └── first_question_state.dart │ │ │ │ ├── load_question/ │ │ │ │ │ ├── load_question_cubit.dart │ │ │ │ │ └── load_question_state.dart │ │ │ │ ├── new_suggestions/ │ │ │ │ │ ├── new_suggestion_cubit.dart │ │ │ │ │ └── new_suggestion_state.dart │ │ │ │ ├── text_to_doc_question/ │ │ │ │ │ ├── text_to_doc_question_cubit.dart │ │ │ │ │ └── text_to_doc_question_state.dart │ │ │ │ ├── update_expert_mode/ │ │ │ │ │ ├── update_expert_mode_cubit.dart │ │ │ │ │ └── update_expert_mode_state.dart │ │ │ │ ├── update_popular_questions/ │ │ │ │ │ ├── update_popular_questions_cubit.dart │ │ │ │ │ └── update_popular_questions_state.dart │ │ │ │ └── update_stepper/ │ │ │ │ ├── update_stepper_cubit.dart │ │ │ │ └── update_stepper_state.dart │ │ │ └── utils/ │ │ │ ├── Input_custom.dart │ │ │ ├── TextToDocParameter.dart │ │ │ ├── custom_input_field.dart │ │ │ ├── most_popular_questions.dart │ │ │ ├── pdf_viewer.dart │ │ │ ├── stepper_expert_info.dart │ │ │ └── tabbed_container.dart │ │ ├── nl2sql_oss.iml │ │ ├── pubspec.yaml │ │ ├── test/ │ │ │ └── widget_test.dart │ │ └── web/ │ │ ├── index 01.49.28.html │ │ ├── index.html │ │ └── manifest.json │ ├── frontend.yaml │ ├── package.json │ ├── server.ts │ ├── src/ │ │ ├── app/ │ │ │ ├── agent-chat/ │ │ │ │ ├── agent-chat.component.html │ │ │ │ ├── agent-chat.component.scss │ │ │ │ ├── agent-chat.component.spec.ts │ │ │ │ └── agent-chat.component.ts │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.server.ts │ │ │ ├── app.module.ts │ │ │ ├── business-user/ │ │ │ │ ├── business-user.component.html │ │ │ │ ├── business-user.component.scss │ │ │ │ ├── business-user.component.spec.ts │ │ │ │ └── business-user.component.ts │ │ │ ├── grouping-modal/ │ │ │ │ ├── grouping-modal.component.html │ │ │ │ ├── grouping-modal.component.scss │ │ │ │ ├── grouping-modal.component.spec.ts │ │ │ │ └── grouping-modal.component.ts │ │ │ ├── header/ │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.scss │ │ │ │ ├── header.component.spec.ts │ │ │ │ └── header.component.ts │ │ │ ├── home/ │ │ │ │ ├── home.component.html │ │ │ │ ├── home.component.scss │ │ │ │ ├── home.component.spec.ts │ │ │ │ └── home.component.ts │ │ │ ├── http.interceptor.ts │ │ │ ├── login/ │ │ │ │ ├── login.component.html │ │ │ │ ├── login.component.scss │ │ │ │ ├── login.component.spec.ts │ │ │ │ └── login.component.ts │ │ │ ├── login-button/ │ │ │ │ ├── login-button.component.html │ │ │ │ ├── login-button.component.scss │ │ │ │ ├── login-button.component.spec.ts │ │ │ │ └── login-button.component.ts │ │ │ ├── menu/ │ │ │ │ ├── menu.component.html │ │ │ │ ├── menu.component.scss │ │ │ │ ├── menu.component.spec.ts │ │ │ │ └── menu.component.ts │ │ │ ├── prism/ │ │ │ │ ├── prism.component.html │ │ │ │ ├── prism.component.scss │ │ │ │ ├── prism.component.spec.ts │ │ │ │ ├── prism.component.ts │ │ │ │ └── prism.d.ts │ │ │ ├── scenario-list/ │ │ │ │ ├── scenario-list.component.html │ │ │ │ ├── scenario-list.component.scss │ │ │ │ ├── scenario-list.component.spec.ts │ │ │ │ └── scenario-list.component.ts │ │ │ ├── shared/ │ │ │ │ └── services/ │ │ │ │ ├── chat.service.spec.ts │ │ │ │ ├── chat.service.ts │ │ │ │ ├── home.service.spec.ts │ │ │ │ ├── home.service.ts │ │ │ │ ├── login.service.spec.ts │ │ │ │ ├── login.service.ts │ │ │ │ ├── shared.service.spec.ts │ │ │ │ └── shared.service.ts │ │ │ ├── upload-template/ │ │ │ │ ├── upload-template.component.html │ │ │ │ ├── upload-template.component.scss │ │ │ │ ├── upload-template.component.spec.ts │ │ │ │ └── upload-template.component.ts │ │ │ ├── user-journey/ │ │ │ │ ├── user-journey.component.html │ │ │ │ ├── user-journey.component.scss │ │ │ │ ├── user-journey.component.spec.ts │ │ │ │ └── user-journey.component.ts │ │ │ └── user-photo/ │ │ │ ├── user-photo.component.html │ │ │ ├── user-photo.component.scss │ │ │ ├── user-photo.component.spec.ts │ │ │ └── user-photo.component.ts │ │ ├── assets/ │ │ │ ├── .gitkeep │ │ │ └── constants.ts │ │ ├── index.html │ │ ├── main.server.ts │ │ ├── main.ts │ │ ├── styles/ │ │ │ └── variables.scss │ │ └── styles.scss │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── notebooks/ │ ├── 0_CopyDataToBigQuery.ipynb │ ├── 0_CopyDataToCloudSqlPG.ipynb │ ├── 1_Setup_OpenDataQnA.ipynb │ ├── 2_Run_OpenDataQnA.ipynb │ └── 3_LoadKnownGoodSQL.ipynb ├── opendataqna.py ├── prompts.yaml ├── pyproject.toml ├── scripts/ │ ├── .~lock.Scenarios Sample.csv# │ ├── Scenarios Sample.csv │ ├── __init__.py │ ├── copy_select_table_column_bigquery.py │ ├── data_source_list.csv │ ├── data_source_list_sample.csv │ ├── known_good_sql.csv │ ├── save_config.py │ └── tables_columns_descriptions.csv ├── terraform/ │ ├── .gitignore │ ├── README.md │ ├── backend.tf │ ├── bq.tf │ ├── embeddings-setup.tf │ ├── frontend.tf │ ├── iam.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── pg-vector.tf │ ├── scripts/ │ │ ├── backend-deployment.sh │ │ ├── copy-firebase-json.sh │ │ ├── create-and-store-embeddings.py │ │ ├── deploy-all.sh │ │ ├── execute-gcloud-cmd.sh │ │ ├── execute-python-files.sh │ │ ├── frontend-deployment.sh │ │ └── install-dependencies.sh │ ├── templates/ │ │ ├── config.ini.tftpl │ │ └── constants.ts.tftpl │ ├── terraform.tfvars.sample │ ├── variables.tf │ └── versions.tf └── utilities/ ├── __init__.py └── imgs/ └── aa ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .venv/ __pycache__/ agents/__pycache__/ application_default_credentials.json databases/__pycache__/ embeddings/__pycache__/ utils/__pycache__/ */__pycache__/ .DS_Store poetry.lock dist/ test-pypi-token.txt firebase.json .firebaserc config_copy.ini eval/ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct also applies outside the project spaces when the Project Steward has a reasonable belief that an individual's behavior may have a negative impact on the project or its community. ## Conflict Resolution We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project’s code of conduct. If you see someone violating the code of conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe. Reports should be directed to *googleapis-stewards@google.com*, the Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to receive and address reported violations of the code of conduct. They will then work with a committee consisting of representatives from the Open Source Programs Office and the Google Open Source Strategy team. If for any reason you are uncomfortable reaching out to the Project Steward, please email opensource@google.com. We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone's safety, we may take action without notice. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute We'd love to accept your patches and contributions to this project. ## Before you begin ### Sign our Contributor License Agreement Contributions to this project must be accompanied by a [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. If you or your current employer have already signed the Google CLA (even if it was for a different project), you probably don't need to do it again. Visit to see your current agreements or to sign a new one. ### Review our community guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google/conduct/). ## Contribution process ### Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ================================================ FILE: Dockerfile ================================================ # Use the official lightweight Python image. # https://hub.docker.com/_/python FROM python:3.9-slim # Allow statements and log messages to immediately appear in the Knative logs ENV PYTHONUNBUFFERED True # Copy local code to the container image. ENV APP_HOME /app WORKDIR $APP_HOME COPY . . # Install production dependencies. RUN pip install poetry RUN poetry install # Run the web service on container startup. Here we use the gunicorn # webserver, with one worker process and 8 threads. # For environments with multiple CPU cores, increase the number of workers # to be equal to the cores available. # Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. # CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app CMD HOME=/root poetry run gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 backend-apis.main:app ================================================ FILE: 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. ================================================ FILE: MANIFEST.in ================================================ # -*- coding: utf-8 -*- # # Copyright 2023 Google LLC # # 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. # Generated by synthtool. DO NOT EDIT! include README.rst LICENSE recursive-include third_party * recursive-include bigframes *.json *.proto py.typed recursive-include tests * global-exclude *.py[co] global-exclude __pycache__ # Exclude scripts for samples readmegen prune scripts/readme-gen ================================================ FILE: OWNERS ================================================ msubasioglu@google.com steveswalker@google.com kpatlolla@google.com srilakshmil@google.com mokshazna@google.com ================================================ FILE: README.md ================================================

aaie image

logo

Open Data QnA - Chat with your SQL Database

_______________
🚨 Version 2.0.0 is now live! Refer to the Release Notes for detailed information on updates and fixes. 🚨
_______________ ✨ Overview ------------- The **Open Data QnA** python library enables you to chat with your databases by leveraging LLM Agents on Google Cloud. Open Data QnA enables a conversational approach to interacting with your data. Ask questions about your PostgreSQL or BigQuery databases in natural language and receive informative responses, without needing to write SQL. Open Data QnA leverages Large Language Models (LLMs) to bridge the gap between human language and database queries, streamlining data analysis and decision-making. ![Alt Text](utilities/imgs/Teaser.gif) **Key Features:** * **Conversational Querying with Multiturn Support:** Ask questions naturally, without requiring SQL knowledge and ask follow up questions. * **Table Grouping:** Group tables under one usecase/user grouping name which can help filtering your large number tables for LLMs to understand about. * **Multi Schema/Dataset Support:** You can group tables from different schemas/datasets for embedding and asking questions against. * **Prompt Customization and Additional Context:** The prompts that are being used are loaded from a yaml file and it also give you ability to add extra context as well * **SQL Generation:** Automatically generates SQL queries based on your questions. * **Query Refinement:** Validates and debugs queries to ensure accuracy. * **Natural Language Responses:** DRun queries and present results in clear, easy-to-understand language. * **Visualizations (Optional):** Explore data visually with generated charts. * **Extensible:** Customize and integrate with your existing workflows(API, UI, Notebooks). It is built on a modular design and currently supports the following components: ### Database Connectors * **Google Cloud SQL for PostgreSQL** * **Google BigQuery** * **Google Firestore(for storing session logs)** ### Vector Stores * **PGVector on Google Cloud SQL for PostgreSQL** * **BigQuery Vector Store** ### Agents * **BuildSQLAgent:** An agent specialized in generating SQL queries for BigQuery or PostgreSQL databases. It analyzes user questions, available table schemas, and column descriptions to construct syntactically and semantically correct SQL queries, adapting its process based on the target database type. * **ValidateSQLAgent:** An agent that validates the syntax and semantic correctness of SQL queries. It uses a language model to analyze queries against a database schema and returns a JSON response indicating validity and potential errors. * **DebugSQLAgent:** An agent designed to debug and refine SQL queries for BigQuery or PostgreSQL databases. It interacts with a chat-based language model to iteratively troubleshoot queries, using error messages to generate alternative, correct queries. * **DescriptionAgent:** An agent specialized in generating descriptions for database tables and columns. It leverages a large language model to create concise and informative descriptions that aid in understanding data structures and facilitate SQL query generation. * **EmbedderAgent:** An agent specialized in generating text embeddings using Large Language Models (LLMs). It supports direct interaction with Vertex AI's TextEmbeddingModel or uses LangChain's VertexAIEmbeddings for a simplified interface. * **ResponseAgent:** An agent that generates natural language responses to user questions based on SQL query results. It acts as a data assistant, interpreting SQL results and transforming them into user-friendly answers using a language model. * **VisualizeAgent:** An agent that generates JavaScript code for Google Charts based on user questions and SQL results. It suggests suitable chart types and constructs the JavaScript code to create visualizations of the data. **Note:** the library was formerly named Talk2Data. You may still find artifacts with the old naming in this repository. 📏 Architecture -------------

aaie image

A detailed description of the Architecture can be found [`here`](/docs/architecture.md) in the docs. 🧬 Repository Structure ------------- Details on the Repository Structure can be found [`here`](/docs/repo_structure.md) in the docs. 🏁 Getting Started: Main Repository ------------- ### Clone the repository and switch to the correct directory git clone git@github.com:GoogleCloudPlatform/Open_Data_QnA.git cd Open_Data_QnA ### 🚧 **Prerequisites** Make sure that Google Cloud CLI and Python >= 3.10 are installed before moving ahead! You can refer to the link below for guidance Installation Guide: https://cloud.google.com/sdk/docs/install Download Python: https://www.python.org/downloads/ ℹ️ **You can setup this solution with three approaches. Choose one based on your requirements:** - **A)** Using [Jupyter Notebooks](#a-jupyter-notebook-based-approach) (For better view at what is happening at each stage of the solution) - **B)** Using [CLI](#b-command-line-interface-cli-based-approach) (For ease of use and running with simple python commands, without the need to understand every step of the solution) - **C)** Using [terraform deployment](#c-using-terraform-to-deploy-the-solution) including your backend APIs with UI ### A) Jupyter Notebook Based Approach #### 💻 **Install Code Dependencies (Create and setup venv)** #### **All commands in this cell to be run on the terminal (typically Ctrl+Shift+`) where your notebooks are running** Install the dependencies by running the poetry commands below ``` # Install poetry pip uninstall poetry -y pip install poetry --quiet #Run the poetry commands below to set up the environment poetry lock #resolve dependecies (also auto create poetry venv if not exists) poetry install --quiet #installs dependencies poetry env info #Displays the evn just created and the path to it poetry shell #this command should activate your venv and you should see it enters into the venv ##inside the activated venv shell [] #If you are running on Worbench instance where the service account used has required permissions to run this solution you can skip the below gcloud auth commands and get to next kernel creation section gcloud auth login # Use this or below command to authenticate gcloud auth application-default login gcloud services enable \ serviceusage.googleapis.com \ cloudresourcemanager.googleapis.com --project <> ``` Chose the relevant instructions based on where you are running the notebook **For IDEs like Cloud Shell Editor, VS Code** For IDEs adding Juypter Extensions will automatically give you option to change the kernel. If not, manually select the python interpreter in your IDE (The exact is shown in the above cell. Path would look like e.g. /home/admin_/opendata/.venv/bin/python or ~cache/user/opendataqna/.venv/bin/python) Proceed to the Step 1 below **For Jupyter Lab or Jupyter Environments on Workbench etc** Create Kernel for with the envrionment created ``` pip install jupyter ipython kernel install --name "openqna-venv" --user ``` Restart your kernel or close the exsiting notebook and open again, you should now see the "openqna-venv" in the kernel drop down **What did we do here?** * Created Application Default Credentials to use for the code * Added venv to kernel to select for running the notebooks (For standalone Jupyter setups like Workbench etc) #### 1. Run the [1_Setup_OpenDataQnA](/notebooks/1_Setup_OpenDataQnA.ipynb) (Run Once for Initial Setup) This notebook guides you through the setup and execution of the Open Data QnA application. It provides comprehensive instructions for setup the solution. #### 2. Run the [2_Run_OpenDataQnA](/notebooks/2_Run_OpenDataQnA.ipynb) This notebook guides you by reading the configuration you setup with [1_Setup_OpenDataQnA](/1_Setup_OpenDataQnA) and running the pipeline to answer questions about your data. #### 3. [Loading Known Good SQL Examples](/notebooks/3_LoadKnownGoodSQL.ipynb) In case you want to separately load Known Good SQLs please run this notebook once the config variables are setup in config.ini file. This can be run multiple times just to load the known good sql queries and create embeddings for it. ___________ ### B) Command Line Interface (CLI) Based Approach #### 1. Add Configuration values for the solution in [config.ini](/config.ini) For setup we require details for vector store, source database etc. Edit the [config.ini](/config.ini) file and add values for the parameters based of below information. ℹ️ Follow the guidelines from the [config guide document](/docs/config_guide.md) to populate your [config.ini](/config.ini) file. **Sources to connect** - This solution lets you setup multiple data source at same time. - You can group multiple tables from different datasets or schema into a grouping and provide the details - If your dataset/schema has many tables and you want to run the solution against few you should specifically choose a group for that tables only **Format for data_source_list.csv** **source | user_grouping | schema | table** **source** - Supported Data Sources. #Options: bigquery , cloudsql-pg **user_grouping** - Logical grouping or use case name for tables from same or different schema/dataset. When left black it default to the schema value in the next column **schema** - schema name for postgres or dataset name in bigquery **table** - name of the tables to run the solutions against. Leave this column blank after filling schema/dataset if you want to run solution for whole dataset/schema Update the [data_source_list.csv](/scripts/data_source_list.csv) according for your requirement. Note that the source details filled in the csv should have already be present. If not please use the Copy Notebooks if you want the demo source setup. Enabled Data Sources: * PostgreSQL on Google Cloud SQL (Copy Sample Data: [0_CopyDataToCloudSqlPG.ipynb](0_CopyDataToCloudSqlPG.ipynb)) * BigQuery (Copy Sample Data: [0_CopyDataToBigQuery.ipynb](0_CopyDataToBigQuery.ipynb)) #### 2. Creating Virtual Environment and Install Dependencies ``` pip install poetry --quiet poetry lock poetry install --quiet poetry env info poetry shell ``` Authenticate your credentials ``` gcloud auth login or gcloud auth application-default login ``` ``` gcloud services enable \ serviceusage.googleapis.com \ cloudresourcemanager.googleapis.com --project <> ``` ``` gcloud auth application-default set-quota-project <> ``` Enable APIs for the solution setup ``` gcloud services enable \ cloudapis.googleapis.com \ compute.googleapis.com \ iam.googleapis.com \ run.googleapis.com \ sqladmin.googleapis.com \ aiplatform.googleapis.com \ bigquery.googleapis.com \ firestore.googleapis.com --project <> ``` #### 3. Run [env_setup.py](/env_setup.py) to create vector store based on the configuration you did in Step 1 ``` python env_setup.py ``` #### 4. Run [opendataqna.py](/opendataqna.py) to run the pipeline you just setup The Open Data QnA SQL Generation tool can be conveniently used from your terminal or command prompt using a simple CLI interface. Here's how: ``` python opendataqna.py --session_id "122133131f--ade-eweq" --user_question "What is most 5 common genres we have?" --user_grouping "MovieExplorer-bigquery" ``` Where *session_id* : Keep this unique unique same for follow up questions. *user_question* : Enter your question in string *user_grouping* : Enter the BQ_DATASET_NAME for BigQuery sources or PG_SCHEMA for PostgreSQL sources (refer your [data_source_list.csv](/scripts/data_source_list.csv) file) **Optional Parameters** You can customize the pipeline's behavior using optional parameters. Here are some common examples: ``` # Enable the SQL debugger: python opendataqna.py --session_id="..." --user_question "..." --user_grouping "..." --run_debugger # Execute the final generated SQL: python opendataqna.py --session_id="..." --user_question "..." --user_grouping "..." --execute_final_sql # Change the number of debugging rounds: python opendataqna.py --session_id="..." --user_question "..." --user_grouping "..." --debugging_rounds 5 # Adjust similarity thresholds: python opendataqna.py --session_id="..." --user_question "..." --user_grouping "..." --table_similarity_threshold 0.25 --column_similarity_threshold 0.4 ``` You can find a full list of available options and their descriptions by running: ``` python opendataqna.py --help ``` ### C) Using Terraform to deploy the solution The provided terraform streamlines the setup of this solution and serves as a blueprint for deployment. The script provides a one-click, one-time deployment option. However, it doesn't include CI/CD capabilities and is intended solely for initial setup. > [!NOTE] > Current version of the Terraform Google Cloud provider does not support deployment of a few resources, this solution uses null_resource to create those resources using Google Cloud SDK. Prior to executing terraform, ensure that the below mentioned steps have been completed. #### Data Sources Set Up 1. Source data should already be available. If you do not have readily available source data, use the notebooks [0_CopyDataToBigQuery.ipynb](/notebooks/0_CopyDataToBigQuery.ipynb) or [0_CopyDataToCloudSqlPG.ipynb](/notebooks/0_CopyDataToCloudSqlPG.ipynb) based on the preferred source to populate sample data. 2. Ensure that the [data_source_list.csv](/scripts/data_source_list.csv) is populated with the list of datasources to be used in this solution. Terraform will take care of creating the embeddings in the destination. Use [data_source_list_sample.csv](/scripts/data_source_list_sample.csv) to fill the [data_source_list.csv](/scripts/data_source_list.csv) 3. If you want to use known good sqls for few shot prompting, ensure that the [known_good_sql.csv](/scripts/known_good_sql.csv) is populated with the required data. Terraform will take care of creating the embeddings in the destination. #### Enable Firebase Firebase will be used to host the frontend of the application. 1. Go to https://console.firebase.google.com/ 1. Select add project and load your Google Cloud Platform project 1. Add Firebase to one of your existing Google Cloud projects 1. Confirm Firebase billing plan 1. Continue and complete #### Terraform deployment > [!NOTE] > Terraform apply command for this application uses gcloud config to fetch & pass the set project id to the scripts. Please ensure that gcloud config has been set to your intended project id before proceeding. > [!IMPORTANT] > The Terraform scripts require specific IAM permissions to function correctly. The user needs either the broad `roles/resourcemanager.projectIamAdmin` role or a custom role with tailored permissions to manage IAM policies and roles. > Additionally, one script TEMPORARILY disables Domain Restricted Sharing Org Policies to enable the creation of a public endpoint. This requires the user to also have the `roles/orgpolicy.policyAdmin` role. 1. Install [terraform 1.7 or higher](https://developer.hashicorp.com/terraform/install). 1. [OPTIONAL] Update default values of variables in [variables.tf](/terraform/variables.tf) according to your preferences. You can find the description for each variable inside the file. This file will be used by terraform to get information about the resources it needs to deploy. If you do not update these, terraform will use the already specified default values in the file. 1. Move to the terraform directory in the terminal ``` cd Open_Data_QnA/terraform #If you are running this outside Cloud Shell you need to set up your Google Cloud SDK Credentials gcloud config set project gcloud auth application-default set-quota-project gcloud services enable \ serviceusage.googleapis.com \ cloudresourcemanager.googleapis.com --project <> sh ./scripts/deploy-all.sh ``` This script will perform the following steps: 1. **Run terraform scripts** - These terraform scripts will generate all the GCP resources and configuration files required for the frontend & backend. It will also generate embeddings and store it in the destination vector db. 1. **Deploy cloud run backend service with latest backend code** - The terraform in the previous step uses a dummy container image to deploy the initial version of cloud run service. This is the step where the actual backend code gets deployed. 1. **Deploy frontend app** - All the config files, web app etc required to create the frontend are deployed via terraform. However, the actual UI deployment takes place in this step. ### After deployment ***Auth Provider*** You need to enable at least one authentication provider in Firebase, you can enable it using the following steps: 1. Go to https://console.firebase.google.com/project/your_project_id/authentication/providers (change the `your_project_id` value) 2. Click on Get Started (if needed) 3. Select Google and enable it 4. Set the name for the project and support email for project 5. Save This should deploy you end to end solution in the project with firebase web url For detailed steps and known issues refer to README.md under [`/terraform`](/terraform/) 🖥️ Build a angular based frontend for this solution --------------------------------------------------- Deploy backend apis for the solution, refer to the README.md under [`/backend-apis`](/backend-apis/). This APIs are designed with work with the frontend and provide access to run the solution. Once the backend APIs deployed successfully deploy the frontend for the solution, refer to the README.md under [`/frontend`](/frontend/). 📗 FAQs and Best Practices ------------- If you successfully set up the solution accelerator and want to start optimizing to your needs, you can follow the tips in the [`Best Practice doc`](/docs/best_practices.md). Additionally, if you stumble across any problems, take a look into the [`FAQ`](/docs/faq.md). If neither of these resources helps, feel free to reach out to us directly by raising an Issue. 🧹 CleanUp Resources ------------- To clean up the resources provisioned in this solution, use commands below to remove them using gcloud/bq: For cloudsql-pgvector as vector store : [Delete SQL Instance]() ``` gcloud sql instances delete -q ``` Delete BigQuery Dataset Created for Logs and Vector Store : [Remove BQ Dataset]() ``` bq rm -r -f -d ``` (For Backend APIs)Remove the Cloud Run service : [Delete Service]() ``` gcloud run services delete ``` For frontend, based on firebase: [Remove the firebase app]() 📄 Documentation ------------- * [Open Data QnA Source Code (GitHub)]() * [Open Data QnA usage notebooks](/notebooks) * [`Architecture`](/docs/architecture.md) * [`FAQ`](/docs/faq.md) * [`Best Practice doc`](/docs/best_practices.md) 🚧 Quotas and limits ------------------ [BigQuery quotas]() including hardware, software, and network components. [Gemini quotas](). 🪪 License ------- Open Data QnA is distributed with the [Apache-2.0 license](). It also contains code derived from the following third-party packages: * [pandas]() * [Python]() 🧪 Disclaimer ---------- This repository provides an open-source solution accelerator designed to streamline your development process. Please be aware that all resources associated with this accelerator will be deployed within your own Google Cloud Platform (GCP) instances. It is imperative that you thoroughly test all components and configurations in a non-production environment before integrating any part of this accelerator with your production data or systems. While we strive to provide a secure and reliable solution, we cannot be held responsible for any data loss, service disruptions, or other issues that may arise from the use of this accelerator. By utilizing this repository, you acknowledge that you are solely responsible for the deployment, management, and security of the resources deployed within your GCP environment. If you encounter any issues or have concerns about potential risks, please refrain from using this accelerator in a production setting. We encourage responsible and informed use of this open-source solution. 🙋 Getting Help ---------- If you have any questions or if you found any problems with this repository, please report through GitHub issues. ================================================ FILE: SECURITY.md ================================================ # Security Policy To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). The Google Security Team will respond within 5 working days of your report on g.co/vulnz. We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. ================================================ FILE: agents/BuildSQLAgent.py ================================================ from abc import ABC from vertexai.language_models import CodeChatModel from vertexai.generative_models import GenerativeModel, Content, Part, GenerationConfig from .core import Agent import pandas as pd import json from datetime import datetime from dbconnectors import pgconnector,bqconnector,firestoreconnector from utilities import PROMPTS, format_prompt from google.cloud.aiplatform import telemetry import vertexai from utilities import PROJECT_ID, PG_REGION from vertexai.generative_models import GenerationConfig vertexai.init(project=PROJECT_ID, location=PG_REGION) class BuildSQLAgent(Agent, ABC): agentType: str = "BuildSQLAgent" def __init__(self, model_id = 'gemini-1.5-pro'): super().__init__(model_id=model_id) def build_sql(self,source_type,user_grouping, user_question,session_history,tables_schema,columns_schema, similar_sql, max_output_tokens=2048, temperature=0.4, top_p=1, top_k=32): not_related_msg=f'''select 'Question is not related to the dataset' as unrelated_answer;''' if source_type=='bigquery': from dbconnectors import bq_specific_data_types specific_data_types = bq_specific_data_types() else: from dbconnectors import pg_specific_data_types specific_data_types = pg_specific_data_types() if f'usecase_{source_type}_{user_grouping}' in PROMPTS: usecase_context = PROMPTS[f'usecase_{source_type}_{user_grouping}'] else: usecase_context = "No extra context for the usecase is provided" context_prompt = PROMPTS[f'buildsql_{source_type}'] context_prompt = format_prompt(context_prompt, specific_data_types = specific_data_types, not_related_msg = not_related_msg, usecase_context = usecase_context, similar_sql=similar_sql, tables_schema=tables_schema, columns_schema = columns_schema) # print(f"Prompt to Build SQL: \n{context_prompt}") # Chat history Retrieval chat_history=[] for entry in session_history: timestamp = entry["timestamp"] timestamp_str = timestamp.isoformat(timespec='auto') user_message = Content( parts=[Part.from_text(entry["user_question"])], role="user" ) bot_message = Content( parts=[Part.from_text(entry["bot_response"])], role="assistant" ) chat_history.extend([user_message, bot_message]) # Add both to the history # print("Chat History Retrieved") if self.model_id == 'codechat-bison-32k': with telemetry.tool_context_manager('opendataqna-buildsql-v2'): chat_session = self.model.start_chat(context=context_prompt) elif 'gemini' in self.model_id: with telemetry.tool_context_manager('opendataqna-buildsql-v2'): # print("SQL Builder Agent : " + str(self.model_id)) config = GenerationConfig( max_output_tokens=max_output_tokens, temperature=temperature, top_p=top_p, top_k=top_k ) chat_session = self.model.start_chat(history=chat_history,response_validation=False) chat_session.send_message(context_prompt) else: raise ValueError('Invalid Model Specified') if session_history is None or not session_history: concated_questions = None re_written_qe = None previous_question = None previous_sql = None else: concated_questions,re_written_qe=self.rewrite_question(user_question,session_history) previous_question, previous_sql = self.get_last_sql(session_history) build_context_prompt=f""" Below is the previous user question from this conversation and its generated sql. Previous Question: {previous_question} Previous Generated SQL : {previous_sql} Respond with Generate SQL for User Question : {user_question} """ # print("BUILD CONTEXT ::: "+str(build_context_prompt)) with telemetry.tool_context_manager('opendataqna-buildsql-v2'): response = chat_session.send_message(build_context_prompt, stream=False) generated_sql = (str(response.text)).replace("```sql", "").replace("```", "") generated_sql = (str(response.text)).replace("```sql", "").replace("```", "") # print(generated_sql) return generated_sql def rewrite_question(self,question,session_history): formatted_history='' concat_questions='' for i, _row in enumerate(session_history,start=1): user_question = _row['user_question'] sql_query = _row['bot_response'] # print(user_question) formatted_history += f"User Question - Turn :: {i} : {user_question}\n" formatted_history += f"Generated SQL - Turn :: {i}: {sql_query}\n\n" concat_questions += f"{user_question} " # print(formatted_history) context_prompt = f""" Your main objective is to rewrite and refine the question passed based on the session history of question and sql generated. Refine the given question using the provided session history to produce a queryable statement. The refined question should be self-contained, requiring no additional context for accurate SQL generation. Make sure all the information is included in the re-written question Below is the previous session history: {formatted_history} Question to rewrite: {question} """ re_written_qe = str(self.generate_llm_response(context_prompt)) print("*"*25 +"Re-written question for the follow up:: "+"*"*25+"\n"+str(re_written_qe)) return str(concat_questions),str(re_written_qe) def get_last_sql(self,session_history): for entry in reversed(session_history): if entry.get("bot_response"): return entry["user_question"],entry["bot_response"] return None ================================================ FILE: agents/DebugSQLAgent.py ================================================ from abc import ABC import vertexai from vertexai.language_models import CodeChatModel from vertexai.generative_models import GenerativeModel,GenerationConfig from google.cloud.aiplatform import telemetry from dbconnectors import pgconnector, bqconnector from utilities import PROMPTS, format_prompt from .core import Agent import pandas as pd import json from utilities import PROJECT_ID, PG_REGION vertexai.init(project=PROJECT_ID, location=PG_REGION) class DebugSQLAgent(Agent, ABC): """ An agent designed to debug and refine SQL queries for BigQuery or PostgreSQL databases. This agent interacts with a chat-based language model (CodeChat or Gemini) to iteratively troubleshoot SQL queries. It receives feedback in the form of error messages and uses the model's capabilities to generate alternative queries that address the identified issues. The agent strives to maintain the original intent of the query while ensuring its syntactic and semantic correctness. Attributes: agentType (str): Indicates the type of agent, fixed as "DebugSQLAgent". model_id (str): The ID of the chat model to use for debugging. Valid options are: - "codechat-bison-32k" - "gemini-1.0-pro" - "gemini-ultra" Methods: init_chat(source_type, tables_schema, columns_schema, sql_example) -> ChatSession: Initializes a chat session with the chosen chat model. Args: source_type (str): The database type ("bigquery" or "postgresql"). tables_schema (str): A description of the available tables and their columns. columns_schema (str): Detailed descriptions of the columns in the tables. sql_example (str, optional): An example SQL query for reference. Defaults to "-No examples provided..-". Returns: ChatSession: The initiated chat session object. rewrite_sql_chat(chat_session, question, error_df) -> str: Generates an alternative SQL query based on the chat session, original query, and error message. Args: chat_session (ChatSession): The active chat session. question (str): The original SQL query. error_df (pandas.DataFrame): The error message as a DataFrame. Returns: str: The rewritten SQL query. start_debugger(source_type, query, user_question, SQLChecker, tables_schema, columns_schema, AUDIT_TEXT, similar_sql, DEBUGGING_ROUNDS, LLM_VALIDATION) -> Tuple[str, bool, str]: Args: source_type (str): The database type ("bigquery" or "postgresql"). query (str): The initial SQL query to debug. user_question (str): The user's original question for reference. SQLChecker: An object to validate the SQL syntax. tables_schema (str): Table schema information. columns_schema (str): Detailed column descriptions. AUDIT_TEXT (str): Textual audit trail of the debugging process. similar_sql (str, optional): Example SQL queries. Defaults to "-No examples provided..-". DEBUGGING_ROUNDS (int, optional): Maximum debugging attempts. Defaults to 2. LLM_VALIDATION (bool, optional): Whether to use LLM for syntax validation. Defaults to True. Returns: Tuple[str, bool, str]: - The final refined SQL query (or the original if unchanged). - A boolean indicating if the final query is considered invalid. - The updated AUDIT_TEXT with debugging steps. """ agentType: str = "DebugSQLAgent" def __init__(self, model_id = 'gemini-1.5-pro'): super().__init__(model_id=model_id) def init_chat(self,source_type,user_grouping, tables_schema,columns_schema,similar_sql="-No examples provided..-"): if f'usecase_{source_type}_{user_grouping}' in PROMPTS: usecase_context = PROMPTS[f'usecase_{source_type}_{user_grouping}'] else: usecase_context = "No extra context for the usecase is provided" context_prompt = PROMPTS[f'debugsql_{source_type}'] context_prompt = format_prompt(context_prompt, usecase_context = usecase_context, similar_sql=similar_sql, tables_schema=tables_schema, columns_schema = columns_schema) # print(f"Prompt to Debug SQL after formatting: \n{context_prompt}") if self.model_id == 'codechat-bison-32k': with telemetry.tool_context_manager('opendataqna-debugsql-v2'): chat_session = self.model.start_chat(context=context_prompt) elif 'gemini' in self.model_id: with telemetry.tool_context_manager('opendataqna-debugsql-v2'): chat_session = self.model.start_chat(response_validation=False) chat_session.send_message(context_prompt) else: raise ValueError('Invalid Chat Model Specified') return chat_session def rewrite_sql_chat(self, chat_session, sql, question, error_df): context_prompt = f""" What is an alternative SQL statement to address the error mentioned below? Present a different SQL from previous ones. It is important that the query still answer the original question. All columns selected must be present on tables mentioned on the join section. Avoid repeating suggestions. {sql} {question} {error_df} """ if self.model_id =='codechat-bison-32k': with telemetry.tool_context_manager('opendataqna-debugsql-v2'): response = chat_session.send_message(context_prompt) resp_return = (str(response.candidates[0])).replace("```sql", "").replace("```", "") elif 'gemini' in self.model_id: with telemetry.tool_context_manager('opendataqna-debugsql-v2'): response = chat_session.send_message(context_prompt, stream=False) resp_return = (str(response.text)).replace("```sql", "").replace("```", "") else: raise ValueError('Invalid Model Id') return resp_return def start_debugger (self, source_type, user_grouping, query, user_question, SQLChecker, tables_schema, columns_schema, AUDIT_TEXT, similar_sql="-No examples provided..-", DEBUGGING_ROUNDS = 2, LLM_VALIDATION=False): i = 0 STOP = False invalid_response = False chat_session = self.init_chat(source_type,user_grouping,tables_schema,columns_schema,similar_sql) sql = query.replace("```sql","").replace("```","").replace("EXPLAIN ANALYZE ","") AUDIT_TEXT=AUDIT_TEXT+"\n\nEntering the debugging steps!" while (not STOP): json_syntax_result={ "valid":True, "errors":"None"} # Check if LLM Validation is enabled if LLM_VALIDATION: # sql = query.replace("```sql","").replace("```","").replace("EXPLAIN ANALYZE ","") json_syntax_result = SQLChecker.check(source_type,user_question,tables_schema,columns_schema, sql) else: json_syntax_result['valid'] = True AUDIT_TEXT=AUDIT_TEXT+"\nLLM Validation is deactivated. Jumping directly to dry run execution." if json_syntax_result['valid'] is True: AUDIT_TEXT=AUDIT_TEXT+"\nGenerated SQL is syntactically correct as per LLM Validation!" # print(AUDIT_TEXT) if source_type=='bigquery': connector=bqconnector else: connector=pgconnector correct_sql, exec_result_df = connector.test_sql_plan_execution(sql) if not correct_sql: AUDIT_TEXT=AUDIT_TEXT+"\nGenerated SQL failed on execution! Here is the feedback from bigquery dryrun/ explain plan: \n" + str(exec_result_df) rewrite_result = self.rewrite_sql_chat(chat_session, sql, user_question, exec_result_df) print('\n Rewritten and Cleaned SQL: ' + str(rewrite_result)) AUDIT_TEXT=AUDIT_TEXT+"\nRewritten and Cleaned SQL: \n' + str({rewrite_result})" sql = str(rewrite_result).replace("```sql","").replace("```","").replace("EXPLAIN ANALYZE ","") else: STOP = True else: print(f'\nGenerated qeury failed on syntax check as per LLM Validation!\nError Message from LLM: {json_syntax_result} \nRewriting the query...') AUDIT_TEXT=AUDIT_TEXT+'\nGenerated qeury failed on syntax check as per LLM Validation! \nError Message from LLM: '+ str(json_syntax_result) + '\nRewriting the query...' syntax_err_df = pd.read_json(json.dumps(json_syntax_result)) rewrite_result=self.rewrite_sql_chat(chat_session, sql, user_question, syntax_err_df) print(rewrite_result) AUDIT_TEXT=AUDIT_TEXT+'\n Rewritten SQL: ' + str(rewrite_result) sql=str(rewrite_result).replace("```sql","").replace("```","").replace("EXPLAIN ANALYZE ","") i+=1 if i > DEBUGGING_ROUNDS: AUDIT_TEXT=AUDIT_TEXT+ "Exceeded the number of iterations for correction!" AUDIT_TEXT=AUDIT_TEXT+ "The generated SQL can be invalid!" STOP = True invalid_response=True # After the while is completed if i > DEBUGGING_ROUNDS: invalid_response=True return sql, invalid_response, AUDIT_TEXT ================================================ FILE: agents/DescriptionAgent.py ================================================ from abc import ABC from .core import Agent class DescriptionAgent(Agent, ABC): """ An agent specialized in generating descriptions for database tables and columns. This agent leverages a large language model to create concise and informative descriptions that aid in understanding the structure and content of database elements. The generated descriptions can be valuable for documenting schemas, enhancing data exploration, and facilitating SQL query generation. Attributes: agentType (str): Indicates the type of agent, fixed as "DescriptionAgent". Methods: generate_llm_response(prompt) -> str: Generates a response from the underlying language model based on the given prompt. Args: prompt (str): The prompt to feed into the language model. Returns: str: The generated text response, cleaned of any SQL-related formatting artifacts. generate_missing_descriptions(source, table_desc_df, column_name_df) -> Tuple[pd.DataFrame, pd.DataFrame]: Generates missing table and column descriptions using the language model. Args: source (str): The source of the database schema ("bigquery"). table_desc_df (pd.DataFrame): A DataFrame containing table metadata with potential missing descriptions. column_name_df (pd.DataFrame): A DataFrame containing column metadata with potential missing descriptions. Returns: Tuple[pd.DataFrame, pd.DataFrame]: - The updated `table_desc_df` with generated table descriptions. - The updated `column_name_df` with generated column descriptions. """ agentType: str = "DescriptionAgent" def generate_llm_response(self,prompt): context_query = self.model.generate_content(prompt,safety_settings=self.safety_settings,stream=False) return str(context_query.candidates[0].text).replace("```sql", "").replace("```", "") def generate_missing_descriptions(self,source,table_desc_df, column_name_df): llm_generated=0 print("\n\n") for index, row in table_desc_df.iterrows(): if row['table_description'] is None or row['table_description']=='NA': q=f"table_name == '{row['table_name']}' and table_schema == '{row['table_schema']}'" if source=='bigquery': context_prompt = f""" Generate short and crisp description for the table {row['project_id']}.{row['table_schema']}.{row['table_name']} Remember that this desciprtion should help LLMs to help build better SQL for any quries related to this table. Parameters: - column metadata: {column_name_df.query(q).to_markdown(index = False)} - table metadata: {table_desc_df.query(q).to_markdown(index = False)} DO NOT generate description that is more than two lines """ else: context_prompt = f""" Generate short and crisp description for the table {row['table_schema']}.{row['table_name']} Remember that this desciprtions should help LLMs to help build better SQL for any quries related to this table. Parameters: - column metadata: {column_name_df.query(q).to_markdown(index = False)} - table metadata: {table_desc_df.query(q).to_markdown(index = False)} DO NOT generate description that is more than two lines """ table_desc_df.at[index,'table_description']=self.generate_llm_response(context_prompt) print(f"Generated table description for {row['table_schema']}.{row['table_name']}") llm_generated=llm_generated+1 print("LLM generated "+ str(llm_generated) + " Table Descriptions") llm_generated = 0 print("\n\n") for index, row in column_name_df.iterrows(): # print(row['column_description']) if row['column_description'] is None or row['column_description']=='': q=f"table_name == '{row['table_name']}' and table_schema == '{row['table_schema']}'" if source=='bigquery': context_prompt = f""" Generate short and crisp description for the column {row['project_id']}.{row['table_schema']}.{row['table_name']}.{row['column_name']} Remember that this description should help LLMs to help generate better SQL for any queries related to these columns. Consider the below information while generating the description Name of the column : {row['column_name']} Data type of the column is : {row['data_type']} Details of the table of this column are below: {table_desc_df.query(q).to_markdown(index=False)} Column Contrainst of this column are : {row['column_constraints']} DO NOT generate description that is more than two lines """ else: context_prompt = f""" Generate short and crisp description for the column {row['table_schema']}.{row['table_name']}.{row['column_name']} Remember that this description should help LLMs to help generate better SQL for any queries related to these columns. Consider the below information while generating the description Name of the column : {row['column_name']} Data type of the column is : {row['data_type']} Details of the table of this column are below: {table_desc_df.query(q).to_markdown(index=False)} Column Contrainst of this column are : {row['column_constraints']} DO NOT generate description that is more than two lines """ column_name_df.at[index,'column_description']=self.generate_llm_response(prompt=context_prompt) print(f"Generated column description for {row['table_schema']}.{row['table_name']}.{row['column_name']}") llm_generated=llm_generated+1 print("LLM generated "+ str(llm_generated) + " Column Descriptions") return table_desc_df,column_name_df ================================================ FILE: agents/EmbedderAgent.py ================================================ from abc import ABC from .core import Agent from vertexai.language_models import TextEmbeddingModel class EmbedderAgent(Agent, ABC): """ An agent specialized in generating text embeddings using Large Language Models (LLMs). This agent supports two modes for generating embeddings: 1. "vertex": Directly interacts with the Vertex AI TextEmbeddingModel. 2. "lang-vertex": Uses LangChain's VertexAIEmbeddings for a streamlined interface. Attributes: agentType (str): Indicates the type of agent, fixed as "EmbedderAgent". mode (str): The embedding generation mode ("vertex" or "lang-vertex"). model: The underlying embedding model (Vertex AI TextEmbeddingModel or LangChain's VertexAIEmbeddings). Methods: create(question) -> list: Generates text embeddings for the given question(s). Args: question (str or list): The text input for which embeddings are to be generated. Can be a single string or a list of strings. Returns: list: A list of embedding vectors. Each embedding vector is represented as a list of floating-point numbers. Raises: ValueError: If the input `question` is not a string or list, or if the specified `mode` is invalid. """ agentType: str = "EmbedderAgent" def __init__(self, mode, embeddings_model='text-embedding-004'): if mode == 'vertex': self.mode = mode self.model = TextEmbeddingModel.from_pretrained(embeddings_model) elif mode == 'lang-vertex': self.mode = mode from langchain.embeddings import VertexAIEmbeddings self.model = VertexAIEmbeddings() else: raise ValueError('EmbedderAgent mode must be either vertex or lang-vertex') def create(self, question): """Text embedding with a Large Language Model.""" if self.mode == 'vertex': if isinstance(question, str): embeddings = self.model.get_embeddings([question]) for embedding in embeddings: vector = embedding.values return vector elif isinstance(question, list): vector = list() for q in question: embeddings = self.model.get_embeddings([q]) for embedding in embeddings: vector.append(embedding.values) return vector else: raise ValueError('Input must be either str or list') elif self.mode == 'lang-vertex': vector = self.embeddings_service.embed_documents(question) return vector ================================================ FILE: agents/ResponseAgent.py ================================================ import json from abc import ABC from .core import Agent from utilities import PROMPTS, format_prompt from vertexai.generative_models import HarmCategory, HarmBlockThreshold from google.cloud.aiplatform import telemetry import vertexai from utilities import PROJECT_ID, PG_REGION vertexai.init(project=PROJECT_ID, location=PG_REGION) class ResponseAgent(Agent, ABC): """ An agent that generates natural language responses to user questions based on SQL query results. This agent acts as a data assistant, interpreting SQL query results and transforming them into user-friendly, natural language answers. It utilizes a language model (currently Gemini) to craft responses that effectively convey the information derived from the data. Attributes: agentType (str): Indicates the type of agent, fixed as "ResponseAgent". Methods: run(user_question, sql_result) -> str: Generates a natural language response to the user's question based on the SQL query result. Args: user_question (str): The question asked by the user in natural language. sql_result (str): The result of the SQL query executed to answer the question. Returns: str: The generated natural language response. """ agentType: str = "ResponseAgent" def run(self, user_question, sql_result): context_prompt = PROMPTS['nl_reponse'] context_prompt = format_prompt(context_prompt, user_question = user_question, sql_result = sql_result) # print(f"Prompt for Natural Language Response: \n{context_prompt}") if 'gemini' in self.model_id: with telemetry.tool_context_manager('opendataqna-response-v2'): context_query = self.model.generate_content(context_prompt,safety_settings=self.safety_settings, stream=False) generated_sql = str(context_query.candidates[0].text) else: with telemetry.tool_context_manager('opendataqna-response-v2'): context_query = self.model.predict(context_prompt, max_output_tokens = 8000, temperature=0) generated_sql = str(context_query.candidates[0]) return generated_sql ================================================ FILE: agents/ValidateSQLAgent.py ================================================ import json from abc import ABC from .core import Agent from utilities import PROMPTS, format_prompt class ValidateSQLAgent(Agent, ABC): """ An agent that validates the syntax and semantic correctness of SQL queries. This agent leverages a language model (currently Gemini) to analyze a given SQL query against a provided database schema. It assesses whether the query is valid according to a set of predefined guidelines and generates a JSON response indicating the validity status and any potential errors. Attributes: agentType (str): Indicates the type of agent, fixed as "ValidateSQLAgent". Methods: check(user_question, tables_schema, columns_schema, generated_sql) -> dict: Determines the validity of an SQL query and identifies potential errors. Args: user_question (str): The original question posed by the user (used for context). tables_schema (str): A description of the database tables and their relationships. columns_schema (str): Detailed descriptions of the columns within the tables. generated_sql (str): The SQL query to be validated. Returns: dict: A JSON-formatted dictionary with the following keys: - "valid": A boolean value indicating whether the query is valid or not. - "errors": A string describing any errors found in the query (empty if valid). """ agentType: str = "ValidateSQLAgent" def check(self,source_type, user_question, tables_schema, columns_schema, generated_sql): context_prompt = PROMPTS['validatesql'] context_prompt = format_prompt(context_prompt, source_type = source_type, user_question = user_question, tables_schema = tables_schema, columns_schema = columns_schema, generated_sql=generated_sql) # print(f"Prompt to Validate SQL after formatting: \n{context_prompt}") if "gemini" in self.model_id: context_query = self.model.generate_content(context_prompt, stream=False) generated_sql = str(context_query.candidates[0].text) else: context_query = self.model.predict(context_prompt, max_output_tokens = 8000, temperature=0) generated_sql = str(context_query.candidates[0]) json_syntax_result = json.loads(str(generated_sql).replace("```json","").replace("```","")) # print('\n SQL Syntax Validity:' + str(json_syntax_result['valid'])) # print('\n SQL Syntax Error Description:' +str(json_syntax_result['errors']) + '\n') return json_syntax_result ================================================ FILE: agents/VisualizeAgent.py ================================================ #This agent generates google charts code for displaying charts on web application #Generates two charts with elements "chart-div" and "chart-div-1" #Code is in javascript from abc import ABC from vertexai.language_models import CodeChatModel from vertexai.generative_models import GenerativeModel,HarmCategory,HarmBlockThreshold from .core import Agent from utilities import PROMPTS, format_prompt from agents import ValidateSQLAgent import pandas as pd import json from google.cloud.aiplatform import telemetry import vertexai from utilities import PROJECT_ID, PG_REGION vertexai.init(project=PROJECT_ID, location=PG_REGION) class VisualizeAgent(Agent, ABC): """ An agent that generates JavaScript code for Google Charts based on user questions and SQL results. This agent analyzes the user's question and the corresponding SQL query results to determine suitable chart types. It then constructs JavaScript code that uses Google Charts to create visualizations based on the data. Attributes: agentType (str): Indicates the type of agent, fixed as "VisualizeAgent". model_id (str): The ID of the language model used for chart type suggestion and code generation. model: The language model instance. Methods: getChartType(user_question, generated_sql) -> str: Suggests the two most suitable chart types based on the user's question and the generated SQL query. Args: user_question (str): The natural language question asked by the user. generated_sql (str): The SQL query generated to answer the question. Returns: str: A JSON string containing two keys, "chart_1" and "chart_2", each representing a suggested chart type. getChartPrompt(user_question, generated_sql, chart_type, chart_div, sql_results) -> str: Creates a prompt for the language model to generate the JavaScript code for a specific chart. Args: user_question (str): The user's question. generated_sql (str): The generated SQL query. chart_type (str): The desired chart type (e.g., "Bar Chart", "Pie Chart"). chart_div (str): The HTML element ID where the chart will be rendered. sql_results (str): The results of the SQL query in JSON format. Returns: str: The prompt for the language model to generate the chart code. generate_charts(user_question, generated_sql, sql_results) -> dict: Generates JavaScript code for two Google Charts based on the given inputs. Args: user_question (str): The user's question. generated_sql (str): The generated SQL query. sql_results (str): The results of the SQL query in JSON format. Returns: dict: A dictionary containing two keys, "chart_div" and "chart_div_1", each holding the generated JavaScript code for a chart. """ agentType: str ="VisualizeAgent" def __init__(self): self.model_id = 'gemini-1.5-pro' self.model = GenerativeModel("gemini-1.5-pro-001") def getChartType(self,user_question, generated_sql): chart_type_prompt = PROMPTS['visualize_chart_type'] chart_type_prompt = format_prompt(chart_type_prompt, user_question = user_question, generated_sql = generated_sql) chart_type=self.model.generate_content(chart_type_prompt, stream=False).candidates[0].text # print(chart_type) # chart_type = model.predict(map_prompt, max_output_tokens = 1024, temperature= 0.2).candidates[0].text return chart_type.replace("\n", "").replace("```", "").replace("json", "").replace("```html", "").replace("```", "").replace("js\n","").replace("json\n","").replace("python\n","").replace("javascript","") def getChartPrompt(self,user_question, generated_sql, chart_type, chart_div, sql_results): chart_prompt = PROMPTS['visualize_generate_chart_code'] chart_prompt = format_prompt(chart_prompt, user_question = user_question, generated_sql = generated_sql, chart_type = chart_type, chart_div = chart_div, sql_results = sql_results) # print(f"Prompt to generate code for google charts visualization after formatting: \n{chart_prompt}") return chart_prompt def generate_charts(self,user_question,generated_sql,sql_results): chart_type = self.getChartType(user_question,generated_sql) # chart_type = chart_type.split(",") # chart_list = [x.strip() for x in chart_type] chart_json = json.loads(chart_type) chart_list =[chart_json['chart_1'],chart_json['chart_2']] print("Charts Suggested : " + str(chart_list)) context_prompt=self.getChartPrompt(user_question,generated_sql,chart_list[0],"chart_div",sql_results) context_prompt_1=self.getChartPrompt(user_question,generated_sql,chart_list[1],"chart_div_1",sql_results) safety_settings: Optional[dict] = { HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, } with telemetry.tool_context_manager('opendataqna-visualize-v2'): context_query = self.model.generate_content(context_prompt,safety_settings=safety_settings, stream=False) context_query_1 = self.model.generate_content(context_prompt_1,safety_settings=safety_settings, stream=False) google_chart_js={"chart_div":context_query.candidates[0].text.replace("```json", "").replace("```", "").replace("json", "").replace("```html", "").replace("```", "").replace("js","").replace("json","").replace("python","").replace("javascript",""), "chart_div_1":context_query_1.candidates[0].text.replace("```json", "").replace("```", "").replace("json", "").replace("```html", "").replace("```", "").replace("js","").replace("json","").replace("python","").replace("javascript","")} return google_chart_js ================================================ FILE: agents/__init__.py ================================================ from .BuildSQLAgent import BuildSQLAgent from .ValidateSQLAgent import ValidateSQLAgent from .DebugSQLAgent import DebugSQLAgent from .EmbedderAgent import EmbedderAgent from .ResponseAgent import ResponseAgent from .VisualizeAgent import VisualizeAgent from .DescriptionAgent import DescriptionAgent __all__ = ["BuildSQLAgent", "ValidateSQLAgent", "DebugSQLAgent", "EmbedderAgent", "ResponseAgent","VisualizeAgent", "DescriptionAgent"] ================================================ FILE: agents/core.py ================================================ """ Provides the base class for all Agents """ from abc import ABC import vertexai from google.cloud.aiplatform import telemetry from vertexai.language_models import TextGenerationModel from vertexai.language_models import CodeGenerationModel from vertexai.language_models import CodeChatModel from vertexai.generative_models import GenerativeModel from vertexai.generative_models import HarmCategory,HarmBlockThreshold from utilities import PROJECT_ID, PG_REGION vertexai.init(project=PROJECT_ID, location=PG_REGION) class Agent(ABC): """ The core class for all Agents """ agentType: str = "Agent" def __init__(self, model_id:str): """ model_id is the Model ID for initialization """ self.model_id = model_id if model_id == 'code-bison-32k': with telemetry.tool_context_manager('opendataqna'): self.model = CodeGenerationModel.from_pretrained('code-bison-32k') elif model_id == 'text-bison-32k': with telemetry.tool_context_manager('opendataqna'): self.model = TextGenerationModel.from_pretrained('text-bison-32k') elif model_id == 'codechat-bison-32k': with telemetry.tool_context_manager('opendataqna'): self.model = CodeChatModel.from_pretrained("codechat-bison-32k") elif model_id == 'gemini-1.0-pro': with telemetry.tool_context_manager('opendataqna'): # print("Model is gemini 1.0 pro") self.model = GenerativeModel("gemini-1.0-pro-001") self.safety_settings: Optional[dict] = { HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, } elif model_id == 'gemini-1.5-flash': with telemetry.tool_context_manager('opendataqna'): # print("Model is gemini 1.5 flash") self.model = GenerativeModel("gemini-1.5-flash-preview-0514") self.safety_settings: Optional[dict] = { HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, } elif model_id == 'gemini-1.5-pro': with telemetry.tool_context_manager('opendataqna'): # print("Model is gemini 1.5 Pro") self.model = GenerativeModel("gemini-1.5-pro-001") self.safety_settings: Optional[dict] = { HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, } else: raise ValueError("Please specify a compatible model.") def generate_llm_response(self,prompt): context_query = self.model.generate_content(prompt,safety_settings=self.safety_settings,stream=False) return str(context_query.candidates[0].text).replace("```sql", "").replace("```", "").rstrip("\n") def rewrite_question(self,question,session_history): formatted_history='' concat_questions='' for i, _row in enumerate(session_history,start=1): user_question = _row['user_question'] # print(user_question) formatted_history += f"User Question - Turn :: {i} : {user_question}\n" concat_questions += f"{user_question} " # print(formatted_history) context_prompt = f""" Your main objective is to rewrite and refine the question based on the previous questions that has been asked. Refine the given question using the provided questions history to produce a standalone question with full context. The refined question should be self-contained, requiring no additional context for answering it. Make sure all the information is included in the re-written question. You just need to respond with the re-written question. Below is the previous questions history: {formatted_history} Question to rewrite: {question} """ re_written_qe = str(self.generate_llm_response(context_prompt)) print("*"*25 +"Re-written question:: "+"*"*25+"\n"+str(re_written_qe)) return str(concat_questions),str(re_written_qe) ================================================ FILE: app.py ================================================ import streamlit as st import pandas as pd import json from streamlit.components.v1 import html from streamlit.logger import get_logger from opendataqna import generate_uuid, get_all_databases, run_pipeline, get_kgq import asyncio logger = get_logger(__name__) # Initialize session state variables if they don't exist if "session_id" not in st.session_state: st.session_state.session_id = generate_uuid() st.session_state.kgq = [] st.session_state.user_grouping = None logger.info(f"New Session Created - {st.session_state.session_id}") def get_known_databases(): """Retrieves a list of available database schemas from the backend. This function fetches a list of database schemas from the backend API. These schemas represent the available datasets that users can query. Returns: list: A list of database schema names. """ logger.info("Getting list of all user databases") json_groupings, _ = get_all_databases() json_groupings = json.loads(json_groupings) groupings = [item["table_schema"] for item in json_groupings if isinstance(item, dict)] logger.info(f"user_groupings - {str(groupings)}") return groupings def get_known_sql(selected_schema): """Retrieves known good SQL queries (KGQs) for a specific database schema. This function fetches a DataFrame containing KGQs for the given schema. KGQs are pre-defined SQL queries that can be used as examples or suggestions. Args: selected_schema (str): The name of the database schema. Returns: pd.DataFrame: A DataFrame containing KGQs for the specified schema. """ data = get_kgq(selected_schema) parsed_data = list(eval(data[0])) df = pd.DataFrame(parsed_data) return df def generate_sql_results(selected_schema,user_question): """Generates SQL query, executes it, and returns results and response. This function orchestrates the process of generating an SQL query based on the user's question and selected schema, executing the query, and generating a natural language response based on the results. Args: selected_schema (str): The name of the selected database schema. user_question (str): The user's natural language question. Returns: tuple: A tuple containing the generated SQL query (str), the query results as a Pandas DataFrame, and the generated natural language response (str). """ logger.info(f"generating response for user question - {user_question}") logger.info(f"selected user groouping - {selected_schema}") final_sql, results_df, response = asyncio.run( run_pipeline( st.session_state.session_id, user_question, selected_schema, RUN_DEBUGGER=True, EXECUTE_FINAL_SQL=True, DEBUGGING_ROUNDS=2, LLM_VALIDATION=False, Embedder_model='vertex', # Options: 'vertex' or 'vertex-lang' SQLBuilder_model='gemini-1.5-pro', SQLChecker_model='gemini-1.5-pro', SQLDebugger_model='gemini-1.5-pro', Responder_model='gemini-1.5-pro', num_table_matches=5, num_column_matches=10, table_similarity_threshold=0.1, column_similarity_threshold=0.1, example_similarity_threshold=0.1, num_sql_matches=3 ) ) return(final_sql, results_df, response) def generate_response(prompt): """Generates and displays a response to the user's prompt. This function takes a user prompt as input, generates an SQL query and response using the `generate_sql_results` function, and displays the results in a conversational format using Streamlit's chat message feature. Args: prompt (str): The user's input prompt. """ for msg in st.session_state.messages: st.chat_message(msg["role"]).write(msg["content"]) st.chat_message("user").write(prompt) st.session_state.messages.append({"role": "assistant", "content": msg}) msg = "Generating Response" st.session_state.messages.append({"role": "assistant", "content": msg}) st.chat_message("assistant").write(msg) query, results, response = generate_sql_results(st.session_state.user_grouping, prompt) msg = query st.session_state.messages.append({"role": "assistant", "content": msg}) st.chat_message("assistant").write(msg) msg = response st.session_state.messages.append({"role": "assistant", "content": msg}) st.chat_message("assistant").write(msg) with st.chat_message("assistant"): st.dataframe(results) st.session_state.messages.append({"role": "assistant", "content": results}) st.set_page_config(page_title='Open Data QnA', page_icon="📊", initial_sidebar_state="expanded", layout='wide') st.markdown(""" """, unsafe_allow_html=True) st.title("Open Data QnA") with st.sidebar: st.session_state.user_grouping = st.selectbox( 'Select Table Groupings', get_known_databases()) if st.button("New Query"): st.session_state.session_id = generate_uuid() st.session_state.messages.clear() st.rerun() if "messages" not in st.session_state: st.session_state["messages"] = [{"role": "assistant", "content": "Frequently Asked Questions"}] if st.session_state.user_grouping is not None: df = get_known_sql(st.session_state.user_grouping) for index, row in df.iterrows(): url = text = row["example_user_question"] st.session_state.kgq.append(text) if prompt := st.chat_input(): generate_response(prompt) ================================================ FILE: backend-apis/README.md ================================================

Create Endpoints

Here we are going to create publicly accessible endpoints (no authentication) . If you're working on a managed GCP project, it is common that there would be Domain Restricted Sharing Org Policies that will not allow the creation of a public facing endpoint. So we can allow all the domains and re-enable the same policy so that we don’t change the existing policy. Please run the below command before proceeding ahead. You need to have Organization Policy Admin rights to run the below commands. ``` export PROJECT_ID= ``` ``` cd Open_Data_QnA/backend-apis gcloud resource-manager org-policies set-policy --project=$PROJECT_ID policy.yaml #This command will create policy that overrides to allow all domain ``` Create the service account and add roles to run the solution backend for the APIs ``` gcloud iam service-accounts create opendataqna --project=$PROJECT_ID gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:opendataqna@$PROJECT_ID.iam.gserviceaccount.com --role='roles/cloudsql.client' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:opendataqna@$PROJECT_ID.iam.gserviceaccount.com --role='roles/bigquery.admin' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:opendataqna@$PROJECT_ID.iam.gserviceaccount.com --role='roles/aiplatform.user' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:opendataqna@$PROJECT_ID.iam.gserviceaccount.com --role='roles/datastore.owner' --project=$PROJECT_ID --quiet ``` **Technologies** * **Programming language:** Python * **Framework:** Flask **Before you start :** Ensure all variables in your config.ini file are correct, especially those for your Postgres instance and BigQuery dataset. If you need to change the Postgres instance or BigQuery dataset values, update the config.ini file before proceeding. The endpoints deployed here are completely customized for the UI built in this demo solution. Feel free to customize the endpoint if needed for different UI/frontend. The gcloud run deploy command create a cloud build that uses the Dockerfile in the OpenDataQnA folder ***Deploy endpoints to Cloud Run*** ``` export PROJECT_ID= ``` ``` export SERVICE_NAME=opendataqna #change the name if needed export DEPLOY_REGION=us-central1 #change the cloud run deployment region if needed ``` Enable the cloud build API to deploy the endpoints ``` gcloud services enable cloudbuild.googleapis.com --project $PROJECT_ID ``` Get default service account for compute engine and cloud build to deploy the cloud run and add IAM Roles for deployment ``` export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") export DEFAULT_CE_SA=$(gcloud iam service-accounts list --project=$PROJECT_ID --format="value(EMAIL)" --filter="EMAIL ~ $PROJECT_NUMBER-compute@developer.gserviceaccount.com") gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CE_SA --role='roles/storage.admin' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CE_SA --role='roles/artifactregistry.admin' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CE_SA --role='roles/firebase.admin' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CE_SA --role='roles/cloudbuild.builds.builder' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CE_SA --role='roles/logging.logWriter' --project=$PROJECT_ID --quiet export DEFAULT_CB_SA=$PROJECT_NUMBER'@cloudbuild.gserviceaccount.com' gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CB_SA --role='roles/firebase.admin' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CB_SA --role='roles/serviceusage.apiKeysAdmin' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CB_SA --role='roles/cloudbuild.builds.builder' --project=$PROJECT_ID --quiet gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$DEFAULT_CB_SA --role='roles/artifactregistry.admin' --project=$PROJECT_ID --quiet ``` ``` cd Open_Data_QnA gcloud beta run deploy $SERVICE_NAME --region $DEPLOY_REGION --source . --service-account=opendataqna@$PROJECT_ID.iam.gserviceaccount.com --service-min-instances=1 --allow-unauthenticated --project=$PROJECT_ID #if you are deploying cloud run application for the first time in the project you will be prompted for a couple of settings. Go ahead and type Yes. ``` Once the deployment is done successfully you should be able to see the Service URL (endpoint point) link as shown below. Please keep this handy to add this in the frontend or you can get this uri from the cloud run page in the GCP Console. e.g. *https://OpenDataQnA-aeiouAEI-uc.a.run.app* Test if the endpoints are working with below command. This should return the dataset your created in the source env setup notebook. ``` curl /available_databases ```

aaie image

Delete the Org Policy on the Project that's created above. Do not run this if you haven’t created the org policy above ``` gcloud resource-manager org-policies delete iam.allowedPolicyMemberDomains --project=$PROJECT_ID ``` **API Details** All the payloads are in JSON format 1. List Databases : Get the available databases in the vector store that solution can run against URI: {Service URL}/available_databases Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/available_databases Method: GET Request Payload : NONE Request response: ``` { "Error": "", "KnownDB": "[{\"table_schema\":\"imdb-postgres\"},{\"table_schema\":\"retail-postgres\"}]", "ResponseCode": 200 } ``` 2. Known SQL : Get suggestive questions (previously asked/examples added) for selected database URI: /get_known_sql Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/get_known_sql Method: POST Request Payload : ``` { "user_grouping":"retail" } ``` Request response: ``` { "Error": "", "KnownSQL": "[{\"example_user_question\":\"Which city had maximum number of sales and what was the count?\",\"example_generated_sql\":\"select st.city_id, count(st.city_id) as city_sales_count from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by city_sales_count desc limit 1;\"}]", "ResponseCode": 200 } ``` 3. SQL Generation : Generate the SQL for the input question asked against a database URI: /generate_sql Method: POST Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/get_known_sql Request payload: ``` { "session_id":"", "user_id":"harry@hogwarts.com", "user_question":"Which city had maximum number of sales?", "user_grouping":"retail" } ``` Request response: ``` { "Error": "", "GeneratedSQL": " select st.city_id from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by count(*) desc limit 1;", "ResponseCode": 200, "SessionID":"1iuu2u-k1ij2-kkkhhj12131" } ``` 4. Execute SQL : Run the SQL statement against provided database source URI:/run_query Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/run_query Method: POST Request payload: ``` { "user_grouping": "retail", "generated_sql":"select st.city_id from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by count(*) desc limit 1;", "session_id":"1iuu2u-k1ij2-kkkhhj12131" } ``` Request response: ``` { "SessionID":"1iuu2u-k1ij2-kkkhhj12131", "Error": "", "KnownDB": "[{\"city_id\":\"C014\"}]", "ResponseCode": 200 } ``` 5. Embedd SQL : To embed known good SQLs to your example embeddings URI:/embed_sql Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/embed_sql METHOD: POST Request Payload: ``` { "session_id":"1iuu2u-k1ij2-kkkhhj12131", "user_question":"Which city had maximum number of sales?", "generated_sql":"select st.city_id from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by count(*) desc limit 1;", "user_grouping":"retail" } ``` Request response: ``` { "ResponseCode" : 201, "Message" : "Example SQL has been accepted for embedding", "Error":"", "SessionID":"1iuu2u-k1ij2-kkkhhj12131" } ``` 6. Generate Visualization Code : To generated javascript Google Charts code based on the SQL Results and display them on the UI As per design we have two visualizations suggested showing up when the user clicks the visualize button. Hence two divs are send as part of the response “chart_div”, “chart_div_1” to bind them to that element in the UI If you are only looking to setup enpoint you can stop here. In case you require the demo app (frontend UI) built in the solution, proceed to the next step. URI:/generate_viz Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/generate_viz METHOD: POST Request Payload: ``` { "session_id":"1iuu2u-k1ij2-kkkhhj12131" , "user_question": "What are top 5 product skus that are ordered?", "sql_generated": "SELECT productSKU as ProductSKUCode, sum(total_ordered) as TotalOrderedItems FROM `inbq1-joonix.demo.sales_sku` group by productSKU order by sum(total_ordered) desc limit 5", "sql_results": [ { "ProductSKUCode": "GGOEGOAQ012899", "TotalOrderedItems": 456 }, { "ProductSKUCode": "GGOEGDHC074099", "TotalOrderedItems": 334 }, { "ProductSKUCode": "GGOEGOCB017499", "TotalOrderedItems": 319 }, { "ProductSKUCode": "GGOEGOCC077999", "TotalOrderedItems": 290 }, { "ProductSKUCode": "GGOEGFYQ016599", "TotalOrderedItems": 253 } ] } ``` Request response: ``` { "SessionID":"1iuu2u-k1ij2-kkkhhj12131", "Error": "", "GeneratedChartjs": { "chart_div": "google.charts.load('current', {\n packages: ['corechart']\n});\ngoogle.charts.setOnLoadCallback(drawChart);\n\nfunction drawChart() {\n var data = google.visualization.arrayToDataTable([\n ['Product SKU', 'Total Ordered Items'],\n ['GGOEGOAQ012899', 456],\n ['GGOEGDHC074099', 334],\n ['GGOEGOCB017499', 319],\n ['GGOEGOCC077999', 290],\n ['GGOEGFYQ016599', 253],\n ]);\n\n var options = {\n title: 'Top 5 Product SKUs Ordered',\n width: 600,\n height: 300,\n hAxis: {\n textStyle: {\n fontSize: 12\n }\n },\n vAxis: {\n textStyle: {\n fontSize: 12\n }\n },\n legend: {\n textStyle: {\n fontSize: 12\n }\n },\n bar: {\n groupWidth: '50%'\n }\n };\n\n var chart = new google.visualization.BarChart(document.getElementById('chart_div'));\n\n chart.draw(data, options);\n}\n", "chart_div_1": "google.charts.load('current', {'packages':['corechart']});\ngoogle.charts.setOnLoadCallback(drawChart);\nfunction drawChart() {\n var data = google.visualization.arrayToDataTable([\n ['ProductSKUCode', 'TotalOrderedItems'],\n ['GGOEGOAQ012899', 456],\n ['GGOEGDHC074099', 334],\n ['GGOEGOCB017499', 319],\n ['GGOEGOCC077999', 290],\n ['GGOEGFYQ016599', 253]\n ]);\n\n var options = {\n title: 'Top 5 Product SKUs that are Ordered',\n width: 600,\n height: 300,\n hAxis: {\n textStyle: {\n fontSize: 5\n }\n },\n vAxis: {\n textStyle: {\n fontSize: 5\n }\n },\n legend: {\n textStyle: {\n fontSize: 10\n }\n },\n bar: {\n groupWidth: \"60%\"\n }\n };\n\n var chart = new google.visualization.ColumnChart(document.getElementById('chart_div_1'));\n\n chart.draw(data, options);\n}\n" }, "ResponseCode": 200 } ``` 7. Get Results : To directly get the sql results in JSON format URI:/get_results Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/embed_sql METHOD: POST Request Payload: ``` { "user_question":"Which city had maximum number of sales?", "user_database":"retail" } ``` Request response: ``` { "Error": "", "GeneratedResults": "[{\"city_id\":\"C014\"}]", "ResponseCode": 200 } ``` ### For setting up the demo UI with these endpoints please refer to README.md under [`/frontend`](/frontend/) ================================================ FILE: backend-apis/__init__.py ================================================ ================================================ FILE: backend-apis/main.py ================================================ # -*- coding: utf-8 -*- # Copyright 2024 Google LLC # # 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. from flask import Flask, request, jsonify, render_template, Response import asyncio from collections.abc import Callable import logging as log import json import datetime import urllib import re import time import textwrap import pandas as pd from flask_cors import CORS import os import sys import firebase_admin from firebase_admin import credentials, auth from functools import wraps firebase_admin.initialize_app() from opendataqna import get_all_databases,get_kgq,generate_sql,embed_sql,get_response,get_results,visualize module_path = os.path.abspath(os.path.join('.')) sys.path.append(module_path) def jwt_authenticated(func: Callable[..., int]) -> Callable[..., int]: @wraps(func) async def decorated_function(*args, **kwargs): header = request.headers.get("Authorization", None) if header: token = header.split(" ")[1] try: print("TOKEN::"+str(token)) decoded_token = firebase_admin.auth.verify_id_token(token) except Exception as e: log.exception(e) return Response(status=403, response=f"Error with authentication: {e}") else: return Response(status=401) request.uid = decoded_token["uid"] print("USER:: "+str(request.uid)) return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs) return decorated_function RUN_DEBUGGER = True DEBUGGING_ROUNDS = 2 LLM_VALIDATION = False EXECUTE_FINAL_SQL = True Embedder_model = 'vertex' SQLBuilder_model = 'gemini-1.5-pro' SQLChecker_model = 'gemini-1.5-pro' SQLDebugger_model = 'gemini-1.5-pro' num_table_matches = 5 num_column_matches = 10 table_similarity_threshold = 0.3 column_similarity_threshold = 0.3 example_similarity_threshold = 0.3 num_sql_matches = 3 app = Flask(__name__) cors = CORS(app, resources={r"/*": {"origins": "*"}}) @app.route("/available_databases", methods=["GET"]) # @jwt_authenticated def getBDList(): result,invalid_response=get_all_databases() if not invalid_response: responseDict = { "ResponseCode" : 200, "KnownDB" : result, "Error":"" } else: responseDict = { "ResponseCode" : 500, "KnownDB" : "", "Error":result } return jsonify(responseDict) @app.route("/embed_sql", methods=["POST"]) # @jwt_authenticated async def embedSql(): envelope = str(request.data.decode('utf-8')) envelope=json.loads(envelope) user_grouping=envelope.get('user_grouping') generated_sql = envelope.get('generated_sql') user_question = envelope.get('user_question') session_id = envelope.get('session_id') embedded, invalid_response=await embed_sql(session_id,user_grouping,user_question,generated_sql) if not invalid_response: responseDict = { "ResponseCode" : 201, "Message" : "Example SQL has been accepted for embedding", "SessionID" : session_id, "Error":"" } return jsonify(responseDict) else: responseDict = { "ResponseCode" : 500, "KnownDB" : "", "SessionID" : session_id, "Error":embedded } return jsonify(responseDict) @app.route("/run_query", methods=["POST"]) # @jwt_authenticated def getSQLResult(): envelope = str(request.data.decode('utf-8')) envelope=json.loads(envelope) user_question = envelope.get('user_question') user_grouping = envelope.get('user_grouping') generated_sql = envelope.get('generated_sql') session_id = envelope.get('session_id') result_df,invalid_response=get_results(user_grouping,generated_sql) if not invalid_response: _resp,invalid_response=get_response(session_id,user_question,result_df.to_json(orient='records')) if not invalid_response: responseDict = { "ResponseCode" : 200, "KnownDB" : result_df.to_json(orient='records'), "NaturalResponse" : _resp, "SessionID" : session_id, "Error":"" } else: responseDict = { "ResponseCode" : 500, "KnownDB" : result_df.to_json(orient='records'), "NaturalResponse" : _resp, "SessionID" : session_id, "Error":"" } else: _resp=result_df responseDict = { "ResponseCode" : 500, "KnownDB" : "", "NaturalResponse" : _resp, "SessionID" : session_id, "Error":result_df } return jsonify(responseDict) @app.route("/get_known_sql", methods=["POST"]) # @jwt_authenticated def getKnownSQL(): print("Extracting the known SQLs from the example embeddings.") envelope = str(request.data.decode('utf-8')) envelope=json.loads(envelope) user_grouping = envelope.get('user_grouping') result,invalid_response=get_kgq(user_grouping) if not invalid_response: responseDict = { "ResponseCode" : 200, "KnownSQL" : result, "Error":"" } else: responseDict = { "ResponseCode" : 500, "KnownSQL" : "", "Error":result } return jsonify(responseDict) @app.route("/generate_sql", methods=["POST"]) # @jwt_authenticated async def generateSQL(): print("Here is the request payload ") envelope = str(request.data.decode('utf-8')) print("Here is the request payload " + envelope) envelope=json.loads(envelope) user_question = envelope.get('user_question') user_grouping = envelope.get('user_grouping') session_id = envelope.get('session_id') user_id = envelope.get('user_id') generated_sql,session_id,invalid_response = await generate_sql(session_id, user_question, user_grouping, RUN_DEBUGGER, DEBUGGING_ROUNDS, LLM_VALIDATION, Embedder_model, SQLBuilder_model, SQLChecker_model, SQLDebugger_model, num_table_matches, num_column_matches, table_similarity_threshold, column_similarity_threshold, example_similarity_threshold, num_sql_matches, user_id=user_id) if not invalid_response: responseDict = { "ResponseCode" : 200, "GeneratedSQL" : generated_sql, "SessionID" : session_id, "Error":"" } else: responseDict = { "ResponseCode" : 500, "GeneratedSQL" : "", "SessionID" : session_id, "Error":generated_sql } return jsonify(responseDict) @app.route("/generate_viz", methods=["POST"]) # @jwt_authenticated async def generateViz(): envelope = str(request.data.decode('utf-8')) # print("Here is the request payload " + envelope) envelope=json.loads(envelope) user_question = envelope.get('user_question') generated_sql = envelope.get('generated_sql') sql_results = envelope.get('sql_results') session_id = envelope.get('session_id') chart_js='' try: chart_js, invalid_response = visualize(session_id,user_question,generated_sql,sql_results) if not invalid_response: responseDict = { "ResponseCode" : 200, "GeneratedChartjs" : chart_js, "Error":"", "SessionID":session_id } else: responseDict = { "ResponseCode" : 500, "GeneratedSQL" : "", "SessionID":session_id, "Error": chart_js } return jsonify(responseDict) except Exception as e: # util.write_log_entry("Cannot generate the Visualization!!!, please check the logs!" + str(e)) responseDict = { "ResponseCode" : 500, "GeneratedSQL" : "", "SessionID":session_id, "Error":"Issue was encountered while generating the Google Chart, please check the logs!" + str(e) } return jsonify(responseDict) @app.route("/summarize_results", methods=["POST"]) # @jwt_authenticated async def getSummary(): envelope = str(request.data.decode('utf-8')) envelope=json.loads(envelope) user_question = envelope.get('user_question') sql_results = envelope.get('sql_results') result,invalid_response=get_response(user_question,sql_results) if not invalid_response: responseDict = { "ResponseCode" : 200, "summary_response" : result, "Error":"" } else: responseDict = { "ResponseCode" : 500, "summary_response" : "", "Error":result } return jsonify(responseDict) @app.route("/natural_response", methods=["POST"]) # @jwt_authenticated async def getNaturalResponse(): envelope = str(request.data.decode('utf-8')) #print("Here is the request payload " + envelope) envelope=json.loads(envelope) user_question = envelope.get('user_question') user_grouping = envelope.get('user_grouping') generated_sql,session_id,invalid_response = await generate_sql(user_question, user_grouping, RUN_DEBUGGER, DEBUGGING_ROUNDS, LLM_VALIDATION, Embedder_model, SQLBuilder_model, SQLChecker_model, SQLDebugger_model, num_table_matches, num_column_matches, table_similarity_threshold, column_similarity_threshold, example_similarity_threshold, num_sql_matches) if not invalid_response: result_df,invalid_response=get_results(user_grouping,generated_sql) if not invalid_response: result,invalid_response=get_response(user_question,result_df.to_json(orient='records')) if not invalid_response: responseDict = { "ResponseCode" : 200, "summary_response" : result, "Error":"" } else: responseDict = { "ResponseCode" : 500, "summary_response" : "", "Error":result } else: responseDict = { "ResponseCode" : 500, "KnownDB" : "", "Error":result_df } else: responseDict = { "ResponseCode" : 500, "GeneratedSQL" : "", "Error":generated_sql } return jsonify(responseDict) @app.route("/get_results", methods=["POST"]) async def getResultsResponse(): envelope = str(request.data.decode('utf-8')) #print("Here is the request payload " + envelope) envelope=json.loads(envelope) user_question = envelope.get('user_question') user_database = envelope.get('user_database') generated_sql,invalid_response = await generate_sql(user_question, user_database, RUN_DEBUGGER, DEBUGGING_ROUNDS, LLM_VALIDATION, Embedder_model, SQLBuilder_model, SQLChecker_model, SQLDebugger_model, num_table_matches, num_column_matches, table_similarity_threshold, column_similarity_threshold, example_similarity_threshold, num_sql_matches) if not invalid_response: result_df,invalid_response=get_results(user_database,generated_sql) if not invalid_response: responseDict = { "ResponseCode" : 200, "GeneratedResults" : result_df.to_json(orient='records'), "Error":"" } else: responseDict = { "ResponseCode" : 500, "GeneratedResults" : "", "Error":result_df } else: responseDict = { "ResponseCode" : 500, "GeneratedResults" : "", "Error":generated_sql } return jsonify(responseDict) if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) ================================================ FILE: backend-apis/policy.yaml ================================================ constraint: constraints/iam.allowedPolicyMemberDomains listPolicy: allValues: ALLOW ================================================ FILE: config.ini ================================================ [CONFIG] embedding_model = vertex description_model = gemini-1.5-pro vector_store = bigquery-vector debugging = yes logging = yes kgq_examples = yes firestore_region = us-central1 use_session_history = yes use_column_samples = no [GCP] project_id = three-p-o [PGCLOUDSQL] pg_region = us-central1 pg_instance = pg15-opendataqna pg_database = opendataqna-db pg_user = pguser pg_password = pg123 [BIGQUERY] bq_dataset_region = us-central1 bq_opendataqna_dataset_name = opendataqna bq_log_table_name = audit_log_table ================================================ FILE: dbconnectors/BQConnector.py ================================================ """ BigQuery Connector Class """ from google.cloud import bigquery from google.cloud import bigquery_connection_v1 as bq_connection from dbconnectors import DBConnector from abc import ABC from datetime import datetime import google.auth import pandas as pd from google.cloud.exceptions import NotFound def get_auth_user(): credentials, project_id = google.auth.default() if hasattr(credentials, 'service_account_email'): return credentials.service_account_email else: return "Not Determined" def bq_specific_data_types(): return ''' BigQuery offers a wide variety of datatypes to store different types of data effectively. Here's a breakdown of the available categories: Numeric Types - INTEGER (INT64): Stores whole numbers within the range of -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Ideal for non-fractional values. FLOAT (FLOAT64): Stores approximate floating-point numbers with a range of -1.7E+308 to 1.7E+308. Suitable for decimals with a degree of imprecision. NUMERIC: Stores exact fixed-precision decimal numbers, with up to 38 digits of precision and 9 digits to the right of the decimal point. Useful for precise financial and accounting calculations. BIGNUMERIC: Similar to NUMERIC but with even larger scale and precision. Designed for extreme precision in calculations. Character Types - STRING: Stores variable-length Unicode character sequences. Enclosed using single, double, or triple quotes. Boolean Type - BOOLEAN: Stores logical values of TRUE or FALSE (case-insensitive). Date and Time Types - DATE: Stores dates without associated time information. TIME: Stores time information independent of a specific date. DATETIME: Stores both date and time information (without timezone information). TIMESTAMP: Stores an exact moment in time with microsecond precision, including a timezone component for global accuracy. Other Types BYTES: Stores variable-length binary data. Distinguished from strings by using 'B' or 'b' prefix in values. GEOGRAPHY: Stores points, lines, and polygons representing locations on the Earth's surface. ARRAY: Stores an ordered collection of zero or more elements of the same (non-ARRAY) data type. STRUCT: Stores an ordered collection of fields, each with its own name and data type (can be nested). This list covers the most common datatypes in BigQuery. ''' class BQConnector(DBConnector, ABC): """ A connector class for interacting with BigQuery databases. This class provides methods for connecting to BigQuery, executing queries, retrieving results as DataFrames, logging interactions, and managing embeddings. Attributes: project_id (str): The Google Cloud project ID where the BigQuery dataset resides. region (str): The region where the BigQuery dataset is located. dataset_name (str): The name of the BigQuery dataset to interact with. opendataqna_dataset (str): Name of the dataset to use for OpenDataQnA functionalities. audit_log_table_name (str): Name of the table to store audit logs. client (bigquery.Client): The BigQuery client instance for executing queries. Methods: getconn() -> bigquery.Client: Establishes a connection to BigQuery and returns a client object. retrieve_df(query) -> pd.DataFrame: Executes a SQL query and returns the results as a pandas DataFrame. make_audit_entry(source_type, user_grouping, model, question, generated_sql, found_in_vector, need_rewrite, failure_step, error_msg, FULL_LOG_TEXT) -> str: Logs an audit entry to BigQuery, recording details of the interaction and the generated SQL query. create_vertex_connection(connection_id) -> None: Creates a Vertex AI connection for remote model usage in BigQuery. create_embedding_model(connection_id, embedding_model) -> None: Creates or replaces an embedding model in BigQuery using a Vertex AI connection. retrieve_matches(mode, user_grouping, qe, similarity_threshold, limit) -> list: Retrieves the most similar table schemas, column schemas, or example queries based on the given mode and parameters. getSimilarMatches(mode, user_grouping, qe, num_matches, similarity_threshold) -> str: Returns a formatted string containing similar matches found for tables, columns, or examples. getExactMatches(query) -> str or None: Checks if the exact question is present in the example SQL set and returns the corresponding SQL query if found. test_sql_plan_execution(generated_sql) -> Tuple[bool, str]: Tests the execution plan of a generated SQL query in BigQuery. Returns a tuple indicating success and a message. return_table_schema_sql(dataset, table_names=None) -> str: Returns a SQL query to retrieve table schema information from a BigQuery dataset. return_column_schema_sql(dataset, table_names=None) -> str: Returns a SQL query to retrieve column schema information from a BigQuery dataset. """ def __init__(self, project_id:str, region:str, opendataqna_dataset:str, audit_log_table_name:str): self.project_id = project_id self.region = region self.opendataqna_dataset = opendataqna_dataset self.audit_log_table_name = audit_log_table_name self.client=self.getconn() def getconn(self): client = bigquery.Client(project=self.project_id) return client def retrieve_df(self,query): return self.client.query_and_wait(query).to_dataframe() def make_audit_entry(self, source_type, user_grouping, model, question, generated_sql, found_in_vector, need_rewrite, failure_step, error_msg, FULL_LOG_TEXT): # global FULL_LOG_TEXT auth_user=get_auth_user() PROJECT_ID = self.project_id table_id= PROJECT_ID+ '.' + self.opendataqna_dataset + '.' + self.audit_log_table_name now = datetime.now() table_exists=False client = self.getconn() df1 = pd.DataFrame(columns=[ 'source_type', 'project_id', 'user', 'user_grouping', 'model_used', 'question', 'generated_sql', 'found_in_vector', 'need_rewrite', 'failure_step', 'error_msg', 'execution_time', 'full_log' ]) new_row = { "source_type":source_type, "project_id":str(PROJECT_ID), "user":str(auth_user), "user_grouping": user_grouping, "model_used": model, "question": question, "generated_sql": generated_sql, "found_in_vector":found_in_vector, "need_rewrite":need_rewrite, "failure_step":failure_step, "error_msg":error_msg, "execution_time": now, "full_log": FULL_LOG_TEXT } df1.loc[len(df1)] = new_row db_schema=[ # Specify the type of columns whose type cannot be auto-detected. For # example the "title" column uses pandas dtype "object", so its # data type is ambiguous. bigquery.SchemaField("source_type", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("project_id", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("user", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("user_grouping", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("model_used", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("question", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("generated_sql", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("found_in_vector", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("need_rewrite", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("failure_step", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("error_msg", bigquery.enums.SqlTypeNames.STRING), bigquery.SchemaField("execution_time", bigquery.enums.SqlTypeNames.TIMESTAMP), bigquery.SchemaField("full_log", bigquery.enums.SqlTypeNames.STRING), ] try: client.get_table(table_id) # Make an API request. # print("Table {} already exists.".format(table_id)) table_exists=True except NotFound: print("Table {} is not found. Will create this log table".format(table_id)) table_exists=False if table_exists is True: # print('Performing streaming insert') errors = client.insert_rows_from_dataframe(table=table_id, dataframe=df1, selected_fields=db_schema) # Make an API request. if errors == [[]]: print("Logged the run") else: print("Encountered errors while inserting rows: {}".format(errors)) else: job_config = bigquery.LoadJobConfig(schema=db_schema,write_disposition="WRITE_TRUNCATE") # pandas_gbq.to_gbq(df1, table_id, project_id=PROJECT_ID) # replace to replace table; append to append to a table client.load_table_from_dataframe(df1,table_id,job_config=job_config) # replace to replace table; append to append to a table # df1.loc[len(df1)] = new_row # pandas_gbq.to_gbq(df1, table_id, project_id=PROJECT_ID, if_exists='append') # replace to replace table; append to append to a table # print('\n Query added to BQ log table \n') return 'Completed the logging step' def create_vertex_connection(self, connection_id : str): client=bq_connection.ConnectionServiceClient() cloud_resource_properties = bq_connection.types.CloudResourceProperties() new_connection=bq_connection.Connection(cloud_resource=cloud_resource_properties) response= client.create_connection(parent=f'projects/{self.project_id}/locations/{self.region}',connection=new_connection,connection_id=connection_id) def create_embedding_model(self,connection_id: str, embedding_model: str): client = self.getconn() client.query_and_wait(f'''CREATE OR REPLACE MODEL `{self.project_id}.{self.opendataqna_dataset}.EMBEDDING_MODEL` REMOTE WITH CONNECTION `{self.project_id}.{self.region}.{connection_id}` OPTIONS (ENDPOINT = '{embedding_model}');''') def retrieve_matches(self, mode, user_grouping, qe, similarity_threshold, limit): """ This function retrieves the most similar table_schema and column_schema. Modes can be either 'table', 'column', or 'example' """ matches = [] if mode == 'table': sql = '''select base.content as tables_content from vector_search( (SELECT * FROM `{}.table_details_embeddings` WHERE user_grouping = '{}'), "embedding", (SELECT {} as qe), top_k=> {},distance_type=>"COSINE") where 1-distance > {} ''' elif mode == 'column': sql='''select base.content as columns_content from vector_search( (SELECT * FROM `{}.tablecolumn_details_embeddings` WHERE user_grouping = '{}'), "embedding", (SELECT {} as qe), top_k=> {}, distance_type=>"COSINE") where 1-distance > {} ''' elif mode == 'example': sql='''select base.example_user_question, base.example_generated_sql from vector_search ( (SELECT * FROM `{}.example_prompt_sql_embeddings` WHERE user_grouping = '{}'), "embedding", (select {} as qe), top_k=> {}, distance_type=>"COSINE") where 1-distance > {} ''' else: ValueError("No valid mode. Must be either table, column, or example") name_txt = '' results=self.client.query_and_wait(sql.format('{}.{}'.format(self.project_id,self.opendataqna_dataset),user_grouping,qe,limit,similarity_threshold)).to_dataframe() # CHECK RESULTS if len(results) == 0: print(f"Did not find any results for {mode}. Adjust the query parameters.") else: print(f"Found {len(results)} similarity matches for {mode}.") if mode == 'table': name_txt = '' for _ , r in results.iterrows(): name_txt=name_txt+r["tables_content"]+"\n" elif mode == 'column': name_txt = '' for _ ,r in results.iterrows(): name_txt=name_txt+r["columns_content"]+"\n" elif mode == 'example': name_txt = '' for _ , r in results.iterrows(): example_user_question=r["example_user_question"] example_sql=r["example_generated_sql"] name_txt = name_txt + "\n Example_question: "+example_user_question+ "; Example_SQL: "+example_sql else: ValueError("No valid mode. Must be either table, column, or example") name_txt = '' matches.append(name_txt) return matches def getSimilarMatches(self, mode, user_grouping, qe, num_matches, similarity_threshold): if mode == 'table': match_result= self.retrieve_matches(mode, user_grouping, qe, similarity_threshold, num_matches) match_result = match_result[0] # print(match_result) elif mode == 'column': match_result= self.retrieve_matches(mode, user_grouping, qe, similarity_threshold, num_matches) match_result = match_result[0] elif mode == 'example': match_result= self.retrieve_matches(mode, user_grouping, qe, similarity_threshold, num_matches) if len(match_result) == 0: match_result = None else: match_result = match_result[0] return match_result def getExactMatches(self, query): """Checks if the exact question is already present in the example SQL set""" check_history_sql=f"""SELECT example_user_question,example_generated_sql FROM `{self.project_id}.{self.opendataqna_dataset}.example_prompt_sql_embeddings` WHERE lower(example_user_question) = lower("{query}") LIMIT 1; """ exact_sql_history = self.client.query_and_wait(check_history_sql).to_dataframe() if exact_sql_history[exact_sql_history.columns[0]].count() != 0: sql_example_txt = '' exact_sql = '' for index, row in exact_sql_history.iterrows(): example_user_question=row["example_user_question"] example_sql=row["example_generated_sql"] exact_sql=example_sql sql_example_txt = sql_example_txt + "\n Example_question: "+example_user_question+ "; Example_SQL: "+example_sql # print("Found a matching question from the history!" + str(sql_example_txt)) final_sql=exact_sql else: print("No exact match found for the user prompt") final_sql = None return final_sql def test_sql_plan_execution(self, generated_sql): try: exec_result_df="" job_config=bigquery.QueryJobConfig(dry_run=True, use_query_cache=False) query_job = self.client.query(generated_sql,job_config=job_config) # print(query_job) exec_result_df=("This query will process {} bytes.".format(query_job.total_bytes_processed)) correct_sql = True print(exec_result_df) return correct_sql, exec_result_df except Exception as e: return False,str(e) def return_table_schema_sql(self, dataset, table_names=None): """ Returns the SQL query to be run on 'Source DB' to get the Table Schema The SQL query below returns a df containing the cols table_schema, table_name, table_description, table_columns (with cols in the table) for the schema specified above, e.g. 'retail' - table_schema: e.g. retail - table_name: name of the table inside the schema, e.g. products - table_description: text descriptor, can be empty - table_columns: aggregate of the col names inside the table """ user_dataset = self.project_id + '.' + dataset table_filter_clause = "" if table_names: # Extract individual table names from the input string #table_names = [name.strip() for name in table_names[1:-1].split(",")] # Handle the string as a list formatted_table_names = [f"'{name}'" for name in table_names] table_filter_clause = f"""AND TABLE_NAME IN ({', '.join(formatted_table_names)})""" table_schema_sql = f""" (SELECT TABLE_CATALOG as project_id, TABLE_SCHEMA as table_schema , TABLE_NAME as table_name, OPTION_VALUE as table_description, (SELECT STRING_AGG(column_name, ', ') from `{user_dataset}.INFORMATION_SCHEMA.COLUMNS` where TABLE_NAME= t.TABLE_NAME and TABLE_SCHEMA=t.TABLE_SCHEMA) as table_columns FROM `{user_dataset}.INFORMATION_SCHEMA.TABLE_OPTIONS` as t WHERE OPTION_NAME = "description" {table_filter_clause} ORDER BY project_id, table_schema, table_name) UNION ALL (SELECT TABLE_CATALOG as project_id, TABLE_SCHEMA as table_schema , TABLE_NAME as table_name, "NA" as table_description, (SELECT STRING_AGG(column_name, ', ') from `{user_dataset}.INFORMATION_SCHEMA.COLUMNS` where TABLE_NAME= t.TABLE_NAME and TABLE_SCHEMA=t.TABLE_SCHEMA) as table_columns FROM `{user_dataset}.INFORMATION_SCHEMA.TABLES` as t WHERE NOT EXISTS (SELECT 1 FROM `{user_dataset}.INFORMATION_SCHEMA.TABLE_OPTIONS` WHERE OPTION_NAME = "description" AND TABLE_NAME= t.TABLE_NAME and TABLE_SCHEMA=t.TABLE_SCHEMA) {table_filter_clause} ORDER BY project_id, table_schema, table_name) """ return table_schema_sql def return_column_schema_sql(self, dataset, table_names=None): """ Returns the SQL query to be run on 'Source DB' to get the column schema The SQL query below returns a df containing the cols table_schema, table_name, column_name, data_type, column_description, table_description, primary_key, column_constraints for the schema specified above, e.g. 'retail' - table_schema: e.g. retail - table_name: name of the tables inside the schema, e.g. products - column_name: name of each col in each table in the schema, e.g. id_product - data_type: data type of each col - column_description: col descriptor, can be empty - table_description: text descriptor, can be empty - primary_key: whether the col is PK; if yes, the field contains the col_name - column_constraints: e.g. "Primary key for this table" """ user_dataset = self.project_id + '.' + dataset table_filter_clause = "" if table_names: # table_names = [name.strip() for name in table_names[1:-1].split(",")] # Handle the string as a list formatted_table_names = [f"'{name}'" for name in table_names] table_filter_clause = f"""AND C.TABLE_NAME IN ({', '.join(formatted_table_names)})""" column_schema_sql = f""" SELECT C.TABLE_CATALOG as project_id, C.TABLE_SCHEMA as table_schema, C.TABLE_NAME as table_name, C.COLUMN_NAME as column_name, C.DATA_TYPE as data_type, C.DESCRIPTION as column_description, CASE WHEN T.CONSTRAINT_TYPE="PRIMARY KEY" THEN "This Column is a Primary Key for this table" WHEN T.CONSTRAINT_TYPE = "FOREIGN_KEY" THEN "This column is Foreign Key" ELSE NULL END as column_constraints FROM `{user_dataset}.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS` C LEFT JOIN `{user_dataset}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS` T ON C.TABLE_CATALOG = T.TABLE_CATALOG AND C.TABLE_SCHEMA = T.TABLE_SCHEMA AND C.TABLE_NAME = T.TABLE_NAME AND T.ENFORCED ='YES' LEFT JOIN `{user_dataset}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE` K ON K.CONSTRAINT_NAME=T.CONSTRAINT_NAME AND C.COLUMN_NAME = K.COLUMN_NAME WHERE 1=1 {table_filter_clause} ORDER BY project_id, table_schema, table_name, column_name; """ return column_schema_sql def get_column_samples(self,columns_df): sample_column_list=[] for index, row in columns_df.iterrows(): get_column_sample_sql=f'''SELECT STRING_AGG(CAST(value AS STRING)) as sample_values FROM UNNEST((SELECT APPROX_TOP_COUNT({row["column_name"]},5) as osn FROM `{row["project_id"]}.{row["table_schema"]}.{row["table_name"]}` ))''' column_samples_df=self.retrieve_df(get_column_sample_sql) # display(column_samples_df) sample_column_list.append(column_samples_df['sample_values'].to_string(index=False)) columns_df["sample_values"]=sample_column_list return columns_df ================================================ FILE: dbconnectors/FirestoreConnector.py ================================================ from google.cloud import firestore from google.cloud.exceptions import NotFound import time from dbconnectors import DBConnector from abc import ABC import uuid def create_unique_id(): """Creates a unique ID using the UUID4 algorithm. Returns: A string representing a unique ID. """ return str(uuid.uuid1()) class FirestoreConnector(DBConnector, ABC): def __init__(self, project_id:str, firestore_database:str): """Initializes the Firestore connection and authentication.""" self.db = firestore.Client(project=project_id,database=firestore_database) def log_chat(self,session_id, user_question, bot_response,user_id="TEST",): """Logs a chat message to Firestore. Args: session_id (str): The ID of the chat session. user_id (str): The ID of the user who sent the message. user_question (str): The question the user asked. bot_response (str): The response from the bot. """ log_chat = { "session_id": session_id, "user_id": user_id, "user_question": user_question, "bot_response": bot_response, "timestamp": firestore.SERVER_TIMESTAMP, } self.db.collection("session_logs").document().set(log_chat) def get_chat_logs_for_session(self,session_id): """Gets all chat logs for a given session. Args: session_id (str): The ID of the chat session. """ sessions_log_ref = self.db.collection("session_logs") # sessions_log_ref=sessions_log_ref.order_by("timestamp") query= sessions_log_ref.where(filter=firestore.FieldFilter("session_id","==",session_id)) # query = sessions_log_ref.where("session_id", "==", session_id).order_by("timestamp") # Note: Use of CollectionRef stream() is prefered to get() docs = query.stream() session_history=[] for doc in docs: session_history.append(doc.to_dict()) # Add values to the list sorted_session_history=sorted(session_history,key=lambda x: x["timestamp"]) return [{'user_question': item['user_question'], 'bot_response': item['bot_response'],'timestamp':item['timestamp']} for item in sorted_session_history] ================================================ FILE: dbconnectors/PgConnector.py ================================================ """ PostgreSQL Connector Class """ import asyncpg from google.cloud.sql.connector import Connector from sqlalchemy import create_engine import pandas as pd from sqlalchemy.sql import text from pgvector.asyncpg import register_vector import asyncio from pg8000.exceptions import DatabaseError from utilities import root_dir from google.cloud.sql.connector import Connector from dbconnectors import DBConnector from abc import ABC def pg_specific_data_types(): return ''' PostgreSQL offers a wide variety of datatypes to store different types of data effectively. Here's a breakdown of the available categories: Numeric datatypes - SMALLINT: Stores small-range integers between -32768 and 32767. INTEGER: Stores typical integers between -2147483648 and 2147483647. BIGINT: Stores large-range integers between -9223372036854775808 and 9223372036854775807. DECIMAL(p,s): Stores arbitrary precision numbers with a maximum of p digits and s digits to the right of the decimal point. NUMERIC: Similar to DECIMAL but with additional features like automatic scaling. REAL: Stores single-precision floating-point numbers with an approximate range of -3.4E+38 to 3.4E+38. DOUBLE PRECISION: Stores double-precision floating-point numbers with an approximate range of -1.7E+308 to 1.7E+308. Character datatypes - CHAR(n): Fixed-length character string with a specified length of n characters. VARCHAR(n): Variable-length character string with a maximum length of n characters. TEXT: Variable-length string with no maximum size limit. CHARACTER VARYING(n): Alias for VARCHAR(n). CHARACTER: Alias for CHAR. Monetary datatypes - MONEY: Stores monetary amounts with two decimal places. Date/Time datatypes - DATE: Stores dates without time information. TIME: Stores time of day without date information (optionally with time zone). TIMESTAMP: Stores both date and time information (optionally with time zone). INTERVAL: Stores time intervals between two points in time. Binary types - BYTEA: Stores variable-length binary data. BIT: Stores single bits. BIT VARYING: Stores variable-length bit strings. Other types - BOOLEAN: Stores true or false values. UUID: Stores universally unique identifiers. XML: Stores XML data. JSON: Stores JSON data. ENUM: Stores user-defined enumerated values. RANGE: Stores ranges of data values. This list covers the most common datatypes in PostgreSQL. ''' class PgConnector(DBConnector, ABC): """ A connector class for interacting with PostgreSQL databases. This class provides methods for establishing connections to PostgreSQL instances, executing SQL queries, retrieving results as DataFrames, caching known SQL queries, and managing embeddings. It utilizes the `pg8000` library for connections and the `asyncpg` library for asynchronous operations. Attributes: project_id (str): The Google Cloud project ID where the PostgreSQL instance resides. region (str): The region where the PostgreSQL instance is located. instance_name (str): The name of the PostgreSQL instance. database_name (str): The name of the database to connect to. database_user (str): The username for authentication. database_password (str): The password for authentication. pool (Engine): A SQLAlchemy engine object for managing database connections. Methods: getconn() -> connection: Establishes a connection to the PostgreSQL instance and returns a connection object. retrieve_df(query) -> pd.DataFrame: Executes a SQL query and returns the results as a pandas DataFrame. Handles potential database errors. cache_known_sql() -> None: Caches known good SQL queries into a PostgreSQL table for future reference. retrieve_matches(mode, user_grouping, qe, similarity_threshold, limit) -> list: Retrieves similar matches (table schemas, column schemas, or example queries) from the database based on the given mode, query embedding (`qe`), similarity threshold, and limit. getSimilarMatches(mode, user_grouping, qe, num_matches, similarity_threshold) -> str: Gets similar matches for tables, columns, or examples asynchronously, formatting the results into a string. test_sql_plan_execution(generated_sql) -> Tuple[bool, pd.DataFrame]: Tests the execution plan of a generated SQL query in PostgreSQL. Returns a tuple indicating success and the result DataFrame. getExactMatches(query) -> str or None: Checks if the exact question is present in the example SQL set and returns the corresponding SQL query if found. return_column_schema_sql(schema) -> str: Returns a SQL query to retrieve column schema information from a PostgreSQL schema. return_table_schema_sql(schema) -> str: Returns a SQL query to retrieve table schema information from a PostgreSQL schema. """ def __init__(self, project_id:str, region:str, instance_name:str, database_name:str, database_user:str, database_password:str): self.project_id = project_id self.region = region self.instance_name = instance_name self.database_name = database_name self.database_user = database_user self.database_password = database_password self.pool = create_engine( "postgresql+pg8000://", creator=self.getconn, ) def getconn(self): """ function to return the database connection object """ # initialize Connector object connector = Connector() conn = connector.connect( f"{self.project_id}:{self.region}:{self.instance_name}", "pg8000", user=f"{self.database_user}", password=f"{self.database_password}", db=f"{self.database_name}" ) return conn def retrieve_df(self, query): """ TODO: Description """ result_df=pd.DataFrame() try: with self.pool.connect() as db_conn: df = pd.read_sql(text(query), con=db_conn) result_df = df # print('\n Return from code execution: ' + str(result_df) ) return result_df except Exception as e: print(f"Database Error: {e}") df = pd.DataFrame({'Error. Message': e}, index=[0]) return df async def cache_known_sql(self): df = pd.read_csv(f"{root_dir}/{scripts}/known_good_sql.csv") df = df.loc[:, ["prompt", "sql", "database_name"]] df = df.dropna() loop = asyncio.get_running_loop() async with Connector(loop=loop) as connector: # # Create connection to Cloud SQL database. conn: asyncpg.Connection = await connector.connect_async( f"{self.project_id}:{self.region}:{self.instance_name}", "asyncpg", user=f"{self.database_user}", password=f"{self.database_password}", db=f"{self.database_name}", ) await register_vector(conn) # Delete the table if it exists. await conn.execute("DROP TABLE IF EXISTS query_example_embeddings CASCADE") # Create the `query_example_embeddings` table. await conn.execute( """CREATE TABLE query_example_embeddings( prompt TEXT, sql TEXT, user_grouping TEXT)""" ) # Copy the dataframe to the 'query_example_embeddings' table. tuples = list(df.itertuples(index=False)) await conn.copy_records_to_table( "query_example_embeddings", records=tuples, columns=list(df), timeout=10000 ) await conn.close() async def retrieve_matches(self, mode, user_groupinguping, qe, similarity_threshold, limit): """ This function retrieves the most similar table_schema and column_schema. Modes can be either 'table', 'column', or 'example' """ matches = [] loop = asyncio.get_running_loop() async with Connector(loop=loop) as connector: # # Create connection to Cloud SQL database. conn: asyncpg.Connection = await connector.connect_async( f"{self.project_id}:{self.region}:{self.instance_name}", "asyncpg", user=f"{self.database_user}", password=f"{self.database_password}", db=f"{self.database_name}", ) await register_vector(conn) # Prepare the SQL depending on 'mode' if mode == 'table': sql = """ SELECT content as tables_content, 1 - (embedding <=> $1) AS similarity FROM table_details_embeddings WHERE 1 - (embedding <=> $1) > $2 AND user_grouping = $4 ORDER BY similarity DESC LIMIT $3 """ elif mode == 'column': sql = """ SELECT content as columns_content, 1 - (embedding <=> $1) AS similarity FROM tablecolumn_details_embeddings WHERE 1 - (embedding <=> $1) > $2 AND user_grouping = $4 ORDER BY similarity DESC LIMIT $3 """ elif mode == 'example': sql = """ SELECT user_grouping, example_user_question, example_generated_sql, 1 - (embedding <=> $1) AS similarity FROM example_prompt_sql_embeddings WHERE 1 - (embedding <=> $1) > $2 AND user_grouping = $4 ORDER BY similarity DESC LIMIT $3 """ else: ValueError("No valid mode. Must be either table, column, or example") name_txt = '' # print(sql,qe,similarity_threshold,limit,user_grouping) # FETCH RESULTS FROM POSTGRES DB results = await conn.fetch( sql, qe, similarity_threshold, limit, user_groupinguping ) # CHECK RESULTS if len(results) == 0: print(f"Did not find any results for {mode}. Adjust the query parameters.") else: print(f"Found {len(results)} similarity matches for {mode}.") if mode == 'table': name_txt = '' for r in results: name_txt=name_txt+r["tables_content"]+"\n\n" elif mode == 'column': name_txt = '' for r in results: name_txt=name_txt+r["columns_content"]+"\n\n " elif mode == 'example': name_txt = '' for r in results: example_user_question=r["example_user_question"] example_sql=r["example_generated_sql"] # print(example_user_question+"\nThreshold::"+str(r["similarity"])) name_txt = name_txt + "\n Example_question: "+example_user_question+ "; Example_SQL: "+example_sql else: ValueError("No valid mode. Must be either table, column, or example") name_txt = '' matches.append(name_txt) # Close the connection to the database. await conn.close() return matches async def getSimilarMatches(self, mode, user_grouping, qe, num_matches, similarity_threshold): if mode == 'table': match_result=await self.retrieve_matches(mode, user_grouping, qe, similarity_threshold, num_matches) match_result = match_result[0] elif mode == 'column': match_result=await self.retrieve_matches(mode, user_grouping, qe, similarity_threshold, num_matches) match_result = match_result[0] elif mode == 'example': match_result=await self.retrieve_matches(mode, user_grouping, qe, similarity_threshold, num_matches) if len(match_result) == 0: match_result = None else: match_result = match_result[0] return match_result def test_sql_plan_execution(self, generated_sql): try: exec_result_df = pd.DataFrame() sql = f"""EXPLAIN ANALYZE {generated_sql}""" exec_result_df = self.retrieve_df(sql) if not exec_result_df.empty: if str(exec_result_df.iloc[0]).startswith('Error. Message'): correct_sql = False else: print('\n No need to rewrite the query. This seems to work fine and returned rows...') correct_sql = True else: print('\n No need to rewrite the query. This seems to work fine but no rows returned...') correct_sql = True return correct_sql, exec_result_df except Exception as e: return False,str(e) def getExactMatches(self, query): """ Checks if the exact question is already present in the example SQL set """ check_history_sql=f"""SELECT example_user_question,example_generated_sql FROM example_prompt_sql_embeddings WHERE lower(example_user_question) = lower('{query}') LIMIT 1; """ exact_sql_history = self.retrieve_df(check_history_sql) if exact_sql_history[exact_sql_history.columns[0]].count() != 0: sql_example_txt = '' exact_sql = '' for index, row in exact_sql_history.iterrows(): example_user_question=row["example_user_question"] example_sql=row["example_generated_sql"] exact_sql=example_sql sql_example_txt = sql_example_txt + "\n Example_question: "+example_user_question+ "; Example_SQL: "+example_sql # print("Found a matching question from the history!" + str(sql_example_txt)) final_sql=exact_sql else: print("No exact match found for the user prompt") final_sql = None return final_sql def return_column_schema_sql(self, schema, table_names=None): """ This SQL returns a df containing the cols table_schema, table_name, column_name, data_type, column_description, table_description, primary_key, column_constraints for the schema specified above, e.g. 'retail' - table_schema: e.g. retail - table_name: name of the table inside the schema, e.g. products - column_name: name of each col in each table in the schema, e.g. id_product - data_type: data type of each col - column_description: col descriptor, can be empty - table_description: text descriptor, can be empty - primary_key: whether the col is PK; if yes, the field contains the col_name - column_constraints: e.g. "Primary key for this table" """ table_filter_clause = "" if table_names: # table_names = [name.strip() for name in table_names[1:-1].split(",")] # Handle the string as a list formatted_table_names = [f"'{name}'" for name in table_names] table_filter_clause = f"""and table_name in ({', '.join(formatted_table_names)})""" column_schema_sql = f''' WITH columns_schema AS (select c.table_schema,c.table_name,c.column_name,c.data_type,d.description as column_description, obj_description(c1.oid) as table_description from information_schema.columns c inner join pg_class c1 on c.table_name=c1.relname inner join pg_catalog.pg_namespace n on c.table_schema=n.nspname and c1.relnamespace=n.oid left join pg_catalog.pg_description d on d.objsubid=c.ordinal_position and d.objoid=c1.oid where c.table_schema='{schema}' {table_filter_clause}) , pk_schema as (SELECT table_name, column_name AS primary_key FROM information_schema.key_column_usage WHERE TABLE_SCHEMA='{schema}' {table_filter_clause} AND CONSTRAINT_NAME like '%_pkey%' ORDER BY table_name, primary_key), fk_schema as (SELECT table_name, column_name AS foreign_key FROM information_schema.key_column_usage WHERE TABLE_SCHEMA='{schema}' {table_filter_clause} AND CONSTRAINT_NAME like '%_fkey%' ORDER BY table_name, foreign_key) select lr.*, case when primary_key is not null then 'Primary key for this table' when foreign_key is not null then CONCAT('Foreign key',column_description) else null END as column_constraints from (select l.*,r.primary_key from columns_schema l left outer join pk_schema r on l.table_name=r.table_name and l.column_name=r.primary_key) lr left outer join fk_schema rt on lr.table_name=rt.table_name and lr.column_name=rt.foreign_key ; ''' return column_schema_sql def return_table_schema_sql(self, schema, table_names=None): """ This SQL returns a df containing the cols table_schema, table_name, table_description, table_columns (with cols in the table) for the schema specified above, e.g. 'retail' - table_schema: e.g. retail - table_name: name of the table inside the schema, e.g. products - table_description: text descriptor, can be empty - table_columns: aggregate of the col names inside the table """ table_filter_clause = "" if table_names: # Extract individual table names from the input string #table_names = [name.strip() for name in table_names[1:-1].split(",")] # Handle the string as a list formatted_table_names = [f"'{name}'" for name in table_names] table_filter_clause = f"""and table_name in ({', '.join(formatted_table_names)})""" table_schema_sql = f''' SELECT table_schema, table_name,table_description, array_to_string(array_agg(column_name), ' , ') as table_columns FROM (select c.table_schema,c.table_name,c.column_name,c.ordinal_position,c.column_default,c.data_type,d.description, obj_description(c1.oid) as table_description from information_schema.columns c inner join pg_class c1 on c.table_name=c1.relname inner join pg_catalog.pg_namespace n on c.table_schema=n.nspname and c1.relnamespace=n.oid left join pg_catalog.pg_description d on d.objsubid=c.ordinal_position and d.objoid=c1.oid where c.table_schema='{schema}' {table_filter_clause} ) data GROUP BY table_schema, table_name, table_description ORDER BY table_name; ''' return table_schema_sql def get_column_samples(self,columns_df): sample_column_list=[] for index, row in columns_df.iterrows(): get_column_sample_sql=f'''SELECT most_common_vals AS sample_values FROM pg_stats WHERE tablename = '{row["table_name"]}' AND schemaname = '{row["table_schema"]}' AND attname = '{row["column_name"]}' ''' column_samples_df=self.retrieve_df(get_column_sample_sql) # display(column_samples_df) sample_column_list.append(column_samples_df['sample_values'].to_string(index=False).replace("{","").replace("}","")) columns_df["sample_values"]=sample_column_list return columns_df ================================================ FILE: dbconnectors/__init__.py ================================================ from .core import DBConnector from .PgConnector import PgConnector, pg_specific_data_types from .BQConnector import BQConnector, bq_specific_data_types from .FirestoreConnector import FirestoreConnector from utilities import (PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION,BQ_REGION, BQ_OPENDATAQNA_DATASET_NAME,BQ_LOG_TABLE_NAME) pgconnector = PgConnector(PROJECT_ID, PG_REGION, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD) bqconnector = BQConnector(PROJECT_ID,BQ_REGION,BQ_OPENDATAQNA_DATASET_NAME,BQ_LOG_TABLE_NAME) firestoreconnector = FirestoreConnector(PROJECT_ID,"opendataqna-session-logs") __all__ = ["pgconnector", "pg_specific_data_types", "bqconnector","firestoreconnector"] ================================================ FILE: dbconnectors/core.py ================================================ """ Provides the base class for all Connectors """ from abc import ABC class DBConnector(ABC): """ The core class for all Connectors """ connectorType: str = "Base" def __init__(self, project_id:str, region:str, instance_name:str, database_name:str, database_user:str, database_password:str, dataset_name:str): """ Args: project_id (str | None): GCP Project Id. dataset_name (str): TODO """ self.project_id = project_id self.region = region self.instance_name = instance_name self.database_name = database_name self.database_user = database_user self.database_password = database_password self.dataset_name = dataset_name ================================================ FILE: docs/README.md ================================================ This directory contains documentation and resources to help you understand and use the Open Data QnA library effectively. ## Contents * **README.md:** This file. Provides an overview of the documentation in this directory. * **best_practices.md:** Best practices and guidelines for using the library, including recommended configurations, tips for improving performance, and common pitfalls to avoid. * **faq.md:** Frequently asked questions about the library, covering common issues, troubleshooting tips, and general usage guidance. * **repo_structure.md:** A detailed explanation of the library's repository structure, including the purpose of each file and directory, and how to navigate the codebase. ## How to Use This Documentation **Start with the README.md on the root dir:** This file provides a high-level overview and guides you to the relevant resources. **Consult the FAQ:** If you have any questions or encounter issues, check the FAQ section for possible solutions and answers. **Explore Best Practices:** For optimizing your usage and getting the most out of the library, review the best practices document. **Understand the Codebase:** If you want to dive deeper into the library's code, refer to the repository structure document for a detailed explanation of how the code is organized. ================================================ FILE: docs/architecture.md ================================================ Architecture -------------

aaie image

Architecture Summary ------------- Open Data QnA operates in a sequence of well-defined steps, orchestrating various agents to process user queries and generate informative responses: * **Vector Store Creation:** The vector store is initialized, storing embeddings of known good SQL queries, table schemas, and column details. This serves as a knowledge base for retrieval-augmented generation (RAG). * **RAG (Retrieval-Augmented Generation):** User queries are embedded and compared to the vector store to retrieve relevant context (table/column details and similar past queries) for improved query generation. * **SQL Generation (BuildSQLAgent):** The BuildSQLAgent leverages the retrieved context and the user's natural language question to generate an initial SQL query. * **Optional Validation (ValidateSQLAgent):** If enabled, the ValidateSQLAgent assesses the generated SQL for syntactic and semantic correctness. * **Optional Debugging (DebugSQLAgent):** If the initial SQL is invalid and debugging is enabled, the DebugSQLAgent iteratively refines the query based on error feedback. * **SQL Execution (Dry Run/Explain):** The refined SQL query is tested with a dry run (BigQuery) or explain plan (PostgreSQL) to estimate resource usage and identify potential errors. * **SQL Execution (Full Run):** If the query is deemed valid, it's executed against the database to fetch the results. * **Response Generation (ResponseAgent):** The ResponseAgent analyzes the SQL results and the user's question to generate a natural language response, providing a clear and concise answer. * **Optional Visualization (VisualizeAgent):** If enabled, the VisualizeAgent suggests suitable chart types and generates JavaScript code for Google Charts to display the SQL results in a visually appealing manner. **Key Points:** * **Modularity:** Each step is handled by a specialized agent, allowing for flexibility and customization. * **RAG Enhancement:** The use of retrieval-augmented generation leverages existing knowledge for better query formulation. * **Validation and Debugging:** Optional agents enhance the reliability and accuracy of generated queries. * **Informative Responses:** The ResponseAgent aims to provide meaningful and contextually relevant answers. * **Visual Appeal:** The optional visualization adds an interactive layer to the user experience. ================================================ FILE: docs/best_practices.md ================================================ # Open Data QnA: Best Practices ## General Usage ### Select the Right Database Connector: Choose between `PgConnector`(Google Cloud SQL PostgreSQL) and `BQConnector`(BigQuery) to match your specific database. ### Prepare your data: Ensure your database tables are structured logically with appropriate column names and data types. We further recommend adding concise descriptions to tables and columns to provide the LLM agents with the necessary context. Additionally, please ensure that the overall data quality of your database is good - if you have pattern mismatches or missing values, these will impact the performance of the Open Data QnA solution. ### Start simple: Begin with straightforward questions and fewer tables and progressively experiment with more complex queries and adding more tables. ### Leverage the ‘Known Good SQL’ Cache The `Known Good SQL` cache can (and should) be populated with example user question <-> SQL query pairs relating to your use case. This benefits the solution in two ways: Caching layer reduces latency: if a known user question is found in the cache that exactly matches (meaning, each char is matching, down to punctuation) the new input question, the known good SQL query is fetched and SQL generation will be skipped. In Context Learning: if a known user question is found to be similar to one of the existing queries in the cache, the similar user question is retrieved along with the corresponding SQL query and used as a few-shot example in the prompt for the SQL Generation agent. The user can specify how many example values should be retrieved to use as few-shot examples. We recommend using 3-5 examples, but this further depends on the variations of user questions you expect in your use case. ### Explore Visualizations Utilize the `VisualizeAgent`to generate charts and graphs for a more intuitive understanding of your data. However, make sure to only run the agent on queries that the pipeline has flagged as ‘valid’. ## Customization & Optimization ### Agent Modification The `core`Agent class (agents/core.py) specifies the models supported for the different agents in the Open Data QnA solution. In version 1, these are: - Code Bison ('code-bison-32k') - Text Bison ('text-bison-32k') - Codechat Bison ('codechat-bison-32k') - Gemini 1.0 pro ('gemini-1.0-pro') You can set the different models for each agent when calling the pipeline_run function (see below under `Pipeline Run Configurations`). ### Prompt Engineering Each of the defined agents has their own prompt specified in its agent class file. BuildSQLAgent.py: prompts for BigQuery and PostgreSQL SQL Generation. DebugSQLAgent.py: prompts for debugging for either BQ or PG queries. DescriptionAgent.py: prompts for generating missing table and column descriptions. ResponseAgent.py: prompt to generate a natural language response, answering the user question by using the output of the generated SQL query. ValidateSQLAgent.py: prompt to classify a given SQL as valid or invalid. VisualizeAgent.py two prompts; one for proposing a fitting graph / plot for a given question <-> SQL pair; the other for generating the visualization. ### Pipeline Run Configurations Additionally to changing the base models and the prompts, it is advisable to experiment with different configuration settings of the pipeline run function: ``` async def run_pipeline(user_question, RUN_DEBUGGER=True, EXECUTE_FINAL_SQL=True, DEBUGGING_ROUNDS = 2, LLM_VALIDATION=True, SQLBuilder_model= 'gemini-1.0-pro', SQLChecker_model= 'gemini-1.0-pro', SQLDebugger_model= 'gemini-1.0-pro', Responder_model= 'gemini-1.0-pro', num_table_matches = 5, num_column_matches = 10, table_similarity_threshold = 0.3, column_similarity_threshold = 0.3, example_similarity_threshold = 0.3, num_sql_matches=3) ``` Args: * **user_question (str):** The natural language question to answer. * **RUN_DEBUGGER (bool, optional):** Whether to run the SQL debugger. Defaults to True. It is recommended to use the debugger for improved SQL Generation accuracy. * **DEBUGGING_ROUNDS (int, optional):** The number of debugging rounds. Defaults to 2. We suggest using a value between 2-5, depending on your accuracy and latency requirements. * **EXECUTE_FINAL_SQL (bool, optional):** Whether to execute the final SQL query. Defaults to True. You can disable the SQL execution. This will leave you with the generated SQL query as a response, skipping the retrieval of the execution result and the response generation. * **LLM_VALIDATION (bool, optional):** Whether to use LLM for SQL validation during debugging. Defaults to True. You can disable the SQL Validator if you have specific latency requirements. When disabled, the Debugger will execute a dry run to retrieve any errors from the database call and debug accordingly. * **SQLBuilder_model (str, optional):** The name of the SQL building model. Defaults to 'gemini-1.0-pro'. * **SQLChecker_model (str, optional):** The name of the SQL validation model. Defaults to 'gemini-1.0-pro'. * **SQLDebugger_model (str, optional):** The name of the SQL debugging model. Defaults to 'gemini-1.0-pro'. * **Responder_model (str, optional):** The name of the response generation model. Defaults to 'gemini-1.0-pro'. * **num_table_matches (int, optional):** The number of similar tables to retrieve. Defaults to 5. These will be used when calling the SQL Generation Agent. We recommend setting this higher if you have high variations in your database and user queries. * **num_column_matches (int, optional):** The number of similar columns to retrieve. Defaults to 10. These will be used when calling the SQL Generation Agent. We recommend setting this higher if you have high variations in your database and user queries. * **table_similarity_threshold (float, optional):** The similarity threshold for tables. Defaults to 0.3. Start with higher values and gradually decrease them if you’re not getting enough relevant results. * **column_similarity_threshold (float, optional):** The similarity threshold for columns. Defaults to 0.3. Start with higher values and gradually decrease them if you’re not getting enough relevant results. * **example_similarity_threshold (float, optional):** The similarity threshold for example questions. Defaults to 0.3. Start with higher values and gradually decrease them if you’re not getting enough relevant results. * **num_sql_matches (int, optional):** The number of similar SQL queries to retrieve. Defaults to 3. ================================================ FILE: docs/changelog.md ================================================ # Release Notes - Open Data QnA v2.0.0 This major release brings significant improvements and new features to Open Data QnA. ## Multi turn capabilities Ability to interact back and forth with the database in a context. Initial v1 was established with a single turn query. In this release, we have created a multi turn architecture that saves the session info, previous query information and can answer accordingly. For more information on the architecture: link ## Table Grouping Initial v1 was tied to single dataset processing and all the tables under this dataset. In reality, users most likely want to restrict the tables and add other datasets if needed. This table grouping provides a way for users to be able to define their scope

aaie image

## Data Sampling We provide a sampling of data values in a column to provide contextual information to the SQL Generation agent. For this, top 5 values are retrieved for every column in the specified tables. This information is aggregated and stored back into the vector store, and is retrieved during the retrieval process.

aaie image

aaie image

## Data summarization In the initial V1 release, the results were in tabular format. With this release , we provide summarized answers in a natural language format that can be integrated into a chatbot. User does have an option to still get the tabular and visualized results based on their settings.

aaie image

## Resolving ambiguities The multi-turn approach helps to resolve ambiguities in the questions, by allowing the user to provide follow-up questions and clarifications. Furthermore, it is possible to provide additional context in the instruction prompt to let the LLM resolve ambiguities before triggering the pipeline. This can be achieved with the help of a LLM router added as a first layer before the Open Data QnA pipeline. These clarification questions can help provide more context to the SQL creation. Ambiguities can be categorized into semantic, application, business and database context. With this release we look for semantic and business level context and resolve such ambiguities through the chat interface. ## UX through Flutter and Streamlit In addition to the AngularJS, we have added support through Flutter as part of the release which can be found under the front end code folder. Furthermore, to enable more efficient development, we have added support for streamlit, so users can quickly iterate and test in a dev frontend before deploying to Angular or Flutter. # Release Notes - Open Data QnA v1.2.0 This release brings significant improvements and new features to enhance the stability, functionality, and user experience of the Open Data QnA. ## 🗝️ Key Enhancements: * **Enhanced Functionality:** Added the ability to specify a list of table names to be processed in BQ, instead of parsing all tables in a dataset. * **Improved Debugging:** The SQL debugger now incorporates the user's question into its prompts, leading to more accurate and relevant debugging suggestions. * **Simplified Setup:** Streamlined notebook setup and environment variable management for a smoother user experience. * **Quickstart**: Added a standalone notebook for quick experimentation with the overall approach, limited to BQ. * **Flexible Configuration:** Introduced optional arguments for the CLI pipeline, allowing users to customize various parameters like table and column similarity thresholds. * **Code Refinements:** Removed hardcoded embedding models and added a save_config function for cleaner configuration management. * **Bug Fixes:** Resolved various bugs, including issues with root directory checking, utility initialization, source type determination, and safety settings. * **Expanded Documentation:** Added comprehensive docstrings to functions for better clarity and understanding. ## 📈 Additional Improvements: * **Code Cleanup:** Removed unnecessary files and redundant code, improving overall code maintainability. * **Updated README:** Improved the README file with clearer instructions and updated information. * **Enhanced User Interface:** Introduced a CLI approach (experimental) for more streamlined interaction. ## 🐜 Bug Fixes: * Fixed bugs in standalone notebook functionality. * Removed telemetry test code. * Corrected embedding distances in BigQuery. * Resolved various typos and inconsistencies in the codebase. This release marks a significant step forward in the development of the Open Data QnA SQL Generation tool, making it more reliable, flexible, and user-friendly. We encourage you to upgrade and explore the new features! ================================================ FILE: docs/config_guide.md ================================================ ## Follow the below guide to populate your config.ini file: ______________ **[CONFIG]** **embedding_model = vertex** *;Options: 'vertex' or 'vertex-lang'* **description_model = gemini-1.0-pro** *;Options 'gemini-1.0-pro', 'gemini-1.5-pro', 'text-bison-32k', 'gemini-1.5-flash'* **vector_store = cloudsql-pgvector** *;Options: 'bigquery-vector', 'cloudsql-pgvector'* **debugging = yes** *;if debugging is enabled. yes or no* **logging = yes** *;if logging is enabled. yes or no* **kgq_examples = yes** *;if known-good-queries are provided. yes or no.* **use_session_history = yes** *;if you want to use current session's questions without re-evaluating them* **use_column_samples = yes** *;if you want the solution to collect some samples values from the data source columns to imporve understanding of values. yes or no* **[GCP]** **project_id = my_project** *;your GCP project* *; fill out the values below if you want to use PG as your vector database:* **[PGCLOUDSQL]** **pg_region = us-central1** **pg_instance = pg15-opendataqna** **pg_database = opendataqna-db** **pg_user = pguser** **pg_password = pg123** *; fill out the values below if you want to use BQ as your vector database:* **[BIGQUERY]** *; the remaining values are the settings for the BQ vector store / log dataset and table created by the solution:* **bq_dataset_region = us-central1** **bq_opendataqna_dataset_name = opendataqna** **bq_log_table_name = audit_log_table** **firestore_region = us-central** *;region for NoSQL DB firestore region to deploy* ________________ ================================================ FILE: docs/faq.md ================================================ # Open Data QnA: FAQ ## Source and Vector Store Setup **Q: If new to the vector store concept, which vector store would you recommend?** A: Both the vector stores (pgvector and bigquery vector) are created using embedding model as you specify and also the vector search for both the vector stores are using cosine similarity to find the nearest matches. You can choose bigquery vector as that avoids any extra resource like cloudsql. Vector Embeddings and Search ________ **Q: Why are my example SQLs not being pulled as few-shot examples for the question asked even though the question is almost similar?** A: Verify if the embedding of the example question has happened successfully. Check the retrieval SQL written to pull the similar sqls for a few shot examples. If the cosine similarity logic is wrong that might be the reason for the issue. Correct the SQL to pull required similarity based SQLs ## Accuracy and Latency **Q: How accurate are the results?** A: Depending on the context, the more accurate these are helpful with accuracy. Building blocks such as known good sql, validation all help with accuracy ________ **Q: How is the latency overall?** A: Ambiguous questions have increased latency. If latency is a factor, would suggest adding caching layer and reducing validation steps V2 is also coming up with resolving ambiguity ## Overall Solution **Q: How do I get started quickly?** A: The quickest way is to follow the "Quickstart with Open Data QnA: Standalone BigQuery Notebook." It provides a simplified experience using BigQuery. If you need more customization, follow the instructions for setting up the main repository. ________ **Q: Which databases does Open Data QnA currently support?** A: Currently, it supports Google Cloud SQL for PostgreSQL and Google BigQuery. ________ **Q: What are the requirements to use Open Data QnA?** A: You'll need: A Google Cloud Project An active database (PostgreSQL or BigQuery) Python 3.9 or higher Required Python packages (listed in requirements.txt) ________ **Q: Can I customize the behavior of the agents?** A: Yes, the agents are designed to be modular and extensible. You can modify their code or create your own custom agents. ________ **Q: How do I incorporate my own known good SQL queries into the system?** A: Follow the setup instructions or use the "3. Loading Known Good SQL Examples" notebook to add your own SQL queries to the vector store. This will improve the accuracy of query generation through RAG. ________ **Q: How do I set the table, column, and example similarity thresholds?** A: These thresholds are used during the Retrieval-Augmented Generation (RAG) process to determine how similar your query is to the stored embeddings. Table Similarity Threshold: Determines how closely a user's query needs to match a table name in the vector store to be considered relevant. Higher values make the matching stricter. Column Similarity Threshold: Similar to the table threshold, but for column names. Example Similarity Threshold: Controls how closely a user's query needs to match a known good SQL query example to be considered similar. You can adjust these thresholds when running the pipeline_run function. Start with the default values and experiment to find what works best for your specific data and queries. Generally, start with higher values and gradually decrease them if you're not getting enough relevant results. ________ **Q: Can I visualize the results of my queries?** A: Yes, the VisualizeAgent can generate JavaScript code for Google Charts to create visualizations of your data. ________ **Q: Are all building blocks mandatory?** A: No. They can be replaced ________ **Q: Can this be tested against any database?** A: Tested against Oracle and Snowflake ________ **Q: How are the competitors doing?** A: Few langchain labs, some experimenting with agents ________ **Q: I created a test colab with langchain and a simple implementation. Why complicate it?** A: If your environment is not complex, we would suggest to leverage your simplified approach, or look into the [standalone notebook](/notebooks/(standalone)Run_OpenDataQnA.ipynb) ================================================ FILE: docs/repo_structure.md ================================================ Repository Structure ------------- ``` . ├── agents └── __init__.py └── core.py └── BuildSQLAgent.py └── DebugSQLAgent.py └── DescriptionAgent.py └── EmbedderAgent.py └── ResponseAgent.py └── ValidateSQLAgent.py └── VisualizeAgent.py └── Dockerfile └── backend-apis └── __init__.py └── policy.yaml └── main.py └── dbconnectors └── __init__.py └── core.py └── PgConnector.py └── BQConnector.py └── docs └── best_practices.md └── faq.md └── repo_structure.md └── embeddings └── __init__.py └── retrieve_embeddings.py └── store_embeddings.py └── kgq_embeddings.py └── frontend └── notebooks └── 0_CopyDataToBigQuery.ipynb └── 0_CopyDataToCloudSqlPG.ipynb └── 1_Setup_OpenDataQnA.ipynb └── 2_Run_OpenDataQnA.ipynb └── 3_LoadKnownGoodSQL.ipynb └── scripts └── tables_columns_descriptions.csv └── copy_select_table_column_bigquery.csv └── data_source_list.csv └── known_good_sql.csv └── save_config.py └── Scenarios Sample.csv └── utilities └── __init__.py └── prompts.yaml └── pyproject.toml └── config.ini └── env_setup.py └── opendataqna.py ``` - [`/agents`](/agents): Source code for the LLM Agents. - [`/backend-apis`](/backend-apis/) : Cloud Run based api deployement for frontend to demo the solution on a UI - [`/dbconnectors`](/dbconnectors): Source code for database connectors. - [`/docs`](/docs): Documentations, including FAQ & Best Practices for using this library. - [`/embeddings`](/embeddings): Source code for creating and storing embeddings. - [`/retrieve_embeddings.py`](/embeddings/retrieve_embeddings.py): Source code for retrieving table schema and embedding creation. - [`/store_embeddings.py`](/embeddings/store_embeddings.py): Source code for storing table schema embeddings in Vector Store. - [`/kgq_embeddings.py`](/embeddings/kgq_embeddings.py): Source code for loading good sqls and creating embeddings in the Vector Store) - [`/frontend`](/frontend) : Angular based frontend code to deploy demo app using the API developed with [`/main.py`](backend-apis/main.py) - [`/notebooks`](/notebooks): Sample notebooks demonstrating the usage of this library. - [`/scripts`](/scripts): Additional scripts for initial setup. - [`/Sample Scenarios.csv`](/scripts/Scenarios%20Sample.csv): Sample Scenarios file that can used to load them on the frontend UI for demos - [`/copy_select_table_column_bigquery.py`](/scripts/copy_select_table_column_bigquery.py): Code Sample to copy select tables and columns from one BQ table to another; add table and column descriptions from csv file. - [`/tables_columns_descriptions.csv`](/scripts/tables_columns_descriptions.csv): CSV file containing table and column names and descriptions to be copied - [`/known_good_sql.csv`](/scripts/known_good_sql.csv): CSV files - [`/data_source_list.csv`](/scripts/data_source_list.csv): Data Source CSV File to mention the list of tables and source type etc. - [`/Dockerfile`](/Dockerfile): Dockerfile for deployment of backend apis. It is placed at the root folder to give it right context and access to the files. - [`/env_setup.py`](/env_setup.py): Python file for initial setup. - [`/opendataqna.py`](/opendataqna.py): Python file for running the main pipeline. - [`/prompts.yaml`](/prompts.yaml): Yaml file that contains the prompts used by the solution. It also provides users the ability to prompt extra context for the use case if any. ================================================ FILE: embeddings/__init__.py ================================================ from .retrieve_embeddings import retrieve_embeddings from .store_embeddings import store_schema_embeddings from .kgq_embeddings import store_kgq_embeddings, setup_kgq_table, load_kgq_df __all__ = ["retrieve_embeddings", "store_schema_embeddings","store_kgq_embeddings", "setup_kgq_table", "load_kgq_df"] ================================================ FILE: embeddings/kgq_embeddings.py ================================================ import os import asyncio import asyncpg import pandas as pd import numpy as np from pgvector.asyncpg import register_vector from google.cloud.sql.connector import Connector from langchain_community.embeddings import VertexAIEmbeddings from google.cloud import bigquery from dbconnectors import pgconnector from agents import EmbedderAgent from sqlalchemy.sql import text from utilities import PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION, BQ_OPENDATAQNA_DATASET_NAME, BQ_REGION embedder = EmbedderAgent('vertex') async def setup_kgq_table( project_id, instance_name, database_name, schema, database_user, database_password, region, VECTOR_STORE = "cloudsql-pgvector"): """ This function sets up or refreshes the Vector Store for Known Good Queries (KGQ) """ if VECTOR_STORE=='bigquery-vector': # Create BQ Client client=bigquery.Client(project=project_id) # Delete an old table # client.query_and_wait(f'''DROP TABLE IF EXISTS `{project_id}.{schema}.example_prompt_sql_embeddings`''') # Create a new emptry table client.query_and_wait(f'''CREATE TABLE IF NOT EXISTS `{project_id}.{schema}.example_prompt_sql_embeddings` ( user_grouping string NOT NULL, example_user_question string NOT NULL, example_generated_sql string NOT NULL, embedding ARRAY)''') elif VECTOR_STORE=='cloudsql-pgvector': loop = asyncio.get_running_loop() async with Connector(loop=loop) as connector: # Create connection to Cloud SQL database conn: asyncpg.Connection = await connector.connect_async( f"{project_id}:{region}:{instance_name}", # Cloud SQL instance connection name "asyncpg", user=f"{database_user}", password=f"{database_password}", db=f"{database_name}", ) # Drop on old table # await conn.execute("DROP TABLE IF EXISTS example_prompt_sql_embeddings") # Create a new emptry table await conn.execute( """CREATE TABLE IF NOT EXISTS example_prompt_sql_embeddings( user_grouping VARCHAR(1024) NOT NULL, example_user_question text NOT NULL, example_generated_sql text NOT NULL, embedding vector(768))""" ) else: raise ValueError("Not a valid parameter for a vector store.") async def store_kgq_embeddings(df_kgq, project_id, instance_name, database_name, schema, database_user, database_password, region, VECTOR_STORE = "cloudsql-pgvector" ): """ Create and save the Known Good Query Embeddings to Vector Store """ if VECTOR_STORE=='bigquery-vector': client=bigquery.Client(project=project_id) example_sql_details_chunked = [] for _, row_aug in df_kgq.iterrows(): example_user_question = str(row_aug['prompt']) example_generated_sql = str(row_aug['sql']) example_grouping = str(row_aug['user_grouping']) emb = embedder.create(example_user_question) r = {"example_grouping":example_grouping,"example_user_question": example_user_question,"example_generated_sql": example_generated_sql,"embedding": emb} example_sql_details_chunked.append(r) example_prompt_sql_embeddings = pd.DataFrame(example_sql_details_chunked) client.query_and_wait(f'''CREATE TABLE IF NOT EXISTS `{project_id}.{schema}.example_prompt_sql_embeddings` ( user_grouping string NOT NULL, example_user_question string NOT NULL, example_generated_sql string NOT NULL, embedding ARRAY)''') for _, row in example_prompt_sql_embeddings.iterrows(): client.query_and_wait(f'''DELETE FROM `{project_id}.{schema}.example_prompt_sql_embeddings` WHERE user_grouping= '{row["example_grouping"]}' and example_user_question= "{row["example_user_question"]}" ''' ) # embedding=np.array(row["embedding"]) cleaned_sql = row["example_generated_sql"].replace("\r", " ").replace("\n", " ") client.query_and_wait(f'''INSERT INTO `{project_id}.{schema}.example_prompt_sql_embeddings` VALUES ("{row["example_grouping"]}","{row["example_user_question"]}" , "{cleaned_sql}",{row["embedding"]} )''') elif VECTOR_STORE=='cloudsql-pgvector': loop = asyncio.get_running_loop() async with Connector(loop=loop) as connector: # Create connection to Cloud SQL database conn: asyncpg.Connection = await connector.connect_async( f"{project_id}:{region}:{instance_name}", # Cloud SQL instance connection name "asyncpg", user=f"{database_user}", password=f"{database_password}", db=f"{database_name}", ) example_sql_details_chunked = [] for _, row_aug in df_kgq.iterrows(): example_user_question = str(row_aug['prompt']) example_generated_sql = str(row_aug['sql']) example_grouping = str(row_aug['user_grouping']) emb = embedder.create(example_user_question) r = {"example_grouping":example_grouping,"example_user_question": example_user_question,"example_generated_sql": example_generated_sql,"embedding": emb} example_sql_details_chunked.append(r) example_prompt_sql_embeddings = pd.DataFrame(example_sql_details_chunked) for _, row in example_prompt_sql_embeddings.iterrows(): await conn.execute( "DELETE FROM example_prompt_sql_embeddings WHERE user_grouping= $1 and example_user_question=$2", row["example_grouping"], row["example_user_question"]) cleaned_sql = row["example_generated_sql"].replace("\r", " ").replace("\n", " ") await conn.execute( "INSERT INTO example_prompt_sql_embeddings (user_grouping, example_user_question, example_generated_sql, embedding) VALUES ($1, $2, $3, $4)", row["example_grouping"], row["example_user_question"], cleaned_sql, str(row["embedding"]), ) await conn.close() else: raise ValueError("Not a valid parameter for a vector store.") def load_kgq_df(): import pandas as pd def is_root_dir(): current_dir = os.getcwd() notebooks_path = os.path.join(current_dir, "notebooks") agents_path = os.path.join(current_dir, "agents") return os.path.exists(notebooks_path) or os.path.exists(agents_path) if is_root_dir(): current_dir = os.getcwd() root_dir = current_dir else: root_dir = os.path.abspath(os.path.join(os.getcwd(), '..')) file_path = root_dir + "/scripts/known_good_sql.csv" # Load the file df_kgq = pd.read_csv(file_path) df_kgq = df_kgq.loc[:, ["prompt", "sql", "user_grouping"]] df_kgq = df_kgq.dropna() return df_kgq if __name__ == '__main__': from utilities import PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION VECTOR_STORE = "cloudsql-pgvector" current_dir = os.getcwd() root_dir = os.path.expanduser('~') # Start at the user's home directory while current_dir != root_dir: for dirpath, dirnames, filenames in os.walk(current_dir): config_path = os.path.join(dirpath, 'known_good_sql.csv') if os.path.exists(config_path): file_path = config_path # Update root_dir to the found directory break # Stop outer loop once found current_dir = os.path.dirname(current_dir) print("Known Good SQL Found at Path :: "+file_path) # Load the file df_kgq = pd.read_csv(file_path) df_kgq = df_kgq.loc[:, ["prompt", "sql", "database_name"]] df_kgq = df_kgq.dropna() print('Known Good SQLs Loaded into a Dataframe') asyncio.run(setup_kgq_table(PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION, VECTOR_STORE)) asyncio.run(store_kgq_embeddings(df_kgq, PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION, VECTOR_STORE)) ================================================ FILE: embeddings/retrieve_embeddings.py ================================================ import re import io import sys import pandas as pd from dbconnectors import pgconnector,bqconnector from agents import EmbedderAgent, ResponseAgent, DescriptionAgent from utilities import EMBEDDING_MODEL, DESCRIPTION_MODEL, USE_COLUMN_SAMPLES embedder = EmbedderAgent(EMBEDDING_MODEL) # responder = ResponseAgent('gemini-1.0-pro') descriptor = DescriptionAgent(DESCRIPTION_MODEL) def get_embedding_chunked(textinput, batch_size): for i in range(0, len(textinput), batch_size): request = [x["content"] for x in textinput[i : i + batch_size]] response = embedder.create(request) # Vertex Textmodel Embedder # Store the retrieved vector embeddings for each chunk back. for x, e in zip(textinput[i : i + batch_size], response): x["embedding"] = e # Store the generated embeddings in a pandas dataframe. out_df = pd.DataFrame(textinput) return out_df def retrieve_embeddings(SOURCE, SCHEMA="public", table_names = None): """ Augment all the DB schema blocks to create document for embedding """ if SOURCE == "cloudsql-pg": table_schema_sql = pgconnector.return_table_schema_sql(SCHEMA,table_names=table_names) table_desc_df = pgconnector.retrieve_df(table_schema_sql) column_schema_sql = pgconnector.return_column_schema_sql(SCHEMA,table_names=table_names) column_name_df = pgconnector.retrieve_df(column_schema_sql) #GENERATE MISSING DESCRIPTIONS table_desc_df,column_name_df= descriptor.generate_missing_descriptions(SOURCE,table_desc_df,column_name_df) #ADD SAMPLES VALUES FOR COLUMNS column_name_df["sample_values"]=None if USE_COLUMN_SAMPLES: column_name_df = pgconnector.get_column_samples(column_name_df) ### TABLE EMBEDDING ### """ This SQL returns a df containing the cols table_schema, table_name, table_description, table_columns (with cols in the table) for the schema specified above, e.g. 'retail' """ table_details_chunked = [] for index_aug, row_aug in table_desc_df.iterrows(): cur_table_name = str(row_aug['table_name']) cur_table_schema = str(row_aug['table_schema']) curr_col_names = str(row_aug['table_columns']) curr_tbl_desc = str(row_aug['table_description']) table_detailed_description=f""" Table Name: {cur_table_name} | Schema Name: {cur_table_schema} | Table Description - {curr_tbl_desc}) | Columns List: [{curr_col_names}]""" r = {"table_schema": cur_table_schema,"table_name": cur_table_name,"content": table_detailed_description} table_details_chunked.append(r) table_details_embeddings = get_embedding_chunked(table_details_chunked, 10) ### COLUMN EMBEDDING ### """ This SQL returns a df containing the cols table_schema, table_name, column_name, data_type, column_description, table_description, primary_key, column_constraints for the schema specified above, e.g. 'retail' """ column_details_chunked = [] for index_aug, row_aug in column_name_df.iterrows(): cur_table_name = str(row_aug['table_name']) cur_table_owner = str(row_aug['table_schema']) curr_col_name = str(row_aug['table_schema'])+'.'+str(row_aug['table_name'])+'.'+str(row_aug['column_name']) curr_col_datatype = str(row_aug['data_type']) curr_col_description = str(row_aug['column_description']) curr_col_constraints = str(row_aug['column_constraints']) curr_column_name = str(row_aug['column_name']) curr_column_samples = str(row_aug['sample_values']) column_detailed_description=f"""Schema Name:{cur_table_owner} | Column Name: {curr_col_name} (Data type: {curr_col_datatype}) | Table Name: {cur_table_name} | (column description: {curr_col_description})(constraints: {curr_col_constraints}) | (Sample Values in the Column: {curr_column_samples})""" r = {"table_schema": cur_table_owner,"table_name": cur_table_name,"column_name":curr_column_name, "content": column_detailed_description} column_details_chunked.append(r) column_details_embeddings = get_embedding_chunked(column_details_chunked, 10) elif SOURCE=='bigquery': table_schema_sql = bqconnector.return_table_schema_sql(SCHEMA, table_names=table_names) table_desc_df = bqconnector.retrieve_df(table_schema_sql) column_schema_sql = bqconnector.return_column_schema_sql(SCHEMA, table_names=table_names) column_name_df = bqconnector.retrieve_df(column_schema_sql) #GENERATE MISSING DESCRIPTIONS table_desc_df,column_name_df= descriptor.generate_missing_descriptions(SOURCE,table_desc_df,column_name_df) #ADD SAMPLES VALUES FOR COLUMNS column_name_df["sample_values"]=None if USE_COLUMN_SAMPLES: column_name_df = bqconnector.get_column_samples(column_name_df) #TABLE EMBEDDINGS table_details_chunked = [] for index_aug, row_aug in table_desc_df.iterrows(): cur_project_name =str(row_aug['project_id']) cur_table_name = str(row_aug['table_name']) cur_table_schema = str(row_aug['table_schema']) curr_col_names = str(row_aug['table_columns']) curr_tbl_desc = str(row_aug['table_description']) table_detailed_description=f""" Full Table Name : {cur_project_name}.{cur_table_schema}.{cur_table_name} | Table Columns List: [{curr_col_names}] | Table Description: {curr_tbl_desc} """ r = {"table_schema": cur_table_schema,"table_name": cur_table_name,"content": table_detailed_description} table_details_chunked.append(r) table_details_embeddings = get_embedding_chunked(table_details_chunked, 10) ### COLUMN EMBEDDING ### """ This SQL returns a df containing the cols table_schema, table_name, column_name, data_type, column_description, table_description, primary_key, column_constraints for the schema specified above, e.g. 'retail' """ column_details_chunked = [] for index_aug, row_aug in column_name_df.iterrows(): cur_project_name =str(row_aug['project_id']) cur_table_name = str(row_aug['table_name']) cur_table_owner = str(row_aug['table_schema']) curr_col_name = str(row_aug['table_schema'])+'.'+str(row_aug['table_name'])+'.'+str(row_aug['column_name']) curr_col_datatype = str(row_aug['data_type']) curr_col_description = str(row_aug['column_description']) curr_col_constraints = str(row_aug['column_constraints']) curr_column_name = str(row_aug['column_name']) curr_column_samples = str(row_aug['sample_values']) column_detailed_description=f""" Column Name: {curr_col_name}| Full Table Name : {cur_project_name}.{cur_table_schema}.{cur_table_name} | Data type: {curr_col_datatype}| Column description: {curr_col_description}| Column Constraints: {curr_col_constraints}| Sample Values in the Column : {curr_column_samples}""" r = {"table_schema": cur_table_owner,"table_name": cur_table_name,"column_name":curr_column_name, "content": column_detailed_description} column_details_chunked.append(r) column_details_embeddings = get_embedding_chunked(column_details_chunked, 10) return table_details_embeddings, column_details_embeddings if __name__ == '__main__': SOURCE = 'cloudsql-pg' t, c = retrieve_embeddings(SOURCE, SCHEMA="public") ================================================ FILE: embeddings/store_embeddings.py ================================================ import asyncio import asyncpg import pandas as pd import numpy as np from pgvector.asyncpg import register_vector from google.cloud.sql.connector import Connector from langchain_community.embeddings import VertexAIEmbeddings from google.cloud import bigquery from dbconnectors import pgconnector from agents import EmbedderAgent from sqlalchemy.sql import text from utilities import VECTOR_STORE, PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION, BQ_OPENDATAQNA_DATASET_NAME, BQ_REGION, EMBEDDING_MODEL embedder = EmbedderAgent(EMBEDDING_MODEL) async def store_schema_embeddings(table_details_embeddings, tablecolumn_details_embeddings, project_id, instance_name, database_name, schema, database_user, database_password, region, VECTOR_STORE): """ Store the vectorised table and column details in the DB table. This code may run for a few minutes. """ if VECTOR_STORE == "cloudsql-pgvector": loop = asyncio.get_running_loop() async with Connector(loop=loop) as connector: # Create connection to Cloud SQL database. conn: asyncpg.Connection = await connector.connect_async( f"{project_id}:{region}:{instance_name}", # Cloud SQL instance connection name "asyncpg", user=f"{database_user}", password=f"{database_password}", db=f"{database_name}", ) await conn.execute("CREATE EXTENSION IF NOT EXISTS vector") await register_vector(conn) # await conn.execute(f"DROP SCHEMA IF EXISTS {pg_schema} CASCADE") # await conn.execute(f"CREATE SCHEMA {pg_schema}") # await conn.execute("DROP TABLE IF EXISTS table_details_embeddings") # Create the `table_details_embeddings` table to store vector embeddings. await conn.execute( """CREATE TABLE IF NOT EXISTS table_details_embeddings( source_type VARCHAR(100) NOT NULL, user_grouping VARCHAR(100) NOT NULL, table_schema VARCHAR(1024) NOT NULL, table_name VARCHAR(1024) NOT NULL, content TEXT, embedding vector(768))""" ) # Store all the generated embeddings back into the database. for index, row in table_details_embeddings.iterrows(): await conn.execute( f""" MERGE INTO table_details_embeddings AS target USING (SELECT $1::text AS source_type, $2::text AS user_grouping, $3::text AS table_schema, $4::text AS table_name, $5::text AS content, $6::vector AS embedding) AS source ON target.user_grouping = source.user_grouping AND target.table_name = source.table_name WHEN MATCHED THEN UPDATE SET source_type = source.source_type, table_schema = source.table_schema, content = source.content, embedding = source.embedding WHEN NOT MATCHED THEN INSERT (source_type, user_grouping, table_schema, table_name, content, embedding) VALUES (source.source_type, source.user_grouping, source.table_schema, source.table_name, source.content, source.embedding); """, row["source_type"], row["user_grouping"], row["table_schema"], row["table_name"], row["content"], np.array(row["embedding"]), ) # await conn.execute("DROP TABLE IF EXISTS tablecolumn_details_embeddings") # Create the `table_details_embeddings` table to store vector embeddings. await conn.execute( """CREATE TABLE IF NOT EXISTS tablecolumn_details_embeddings( source_type VARCHAR(100) NOT NULL, user_grouping VARCHAR(100) NOT NULL, table_schema VARCHAR(1024) NOT NULL, table_name VARCHAR(1024) NOT NULL, column_name VARCHAR(1024) NOT NULL, content TEXT, embedding vector(768))""" ) # Store all the generated embeddings back into the database. for index, row in tablecolumn_details_embeddings.iterrows(): await conn.execute( f""" MERGE INTO tablecolumn_details_embeddings AS target USING (SELECT $1::text AS source_type, $2::text AS user_grouping, $3::text AS table_schema, $4::text AS table_name, $5::text AS column_name, $6::text AS content, $7::vector AS embedding) AS source ON target.user_grouping = source.user_grouping AND target.table_name = source.table_name AND target.column_name = source.column_name WHEN MATCHED THEN UPDATE SET source_type = source.source_type, table_schema = source.table_schema, content = source.content, embedding = source.embedding WHEN NOT MATCHED THEN INSERT (source_type, user_grouping, table_schema, table_name, column_name, content, embedding) VALUES (source.source_type, source.user_grouping, source.table_schema, source.table_name, source.column_name, source.content, source.embedding); """, row["source_type"], row["user_grouping"], row["table_schema"], row["table_name"], row["column_name"], row["content"], np.array(row["embedding"]), ) await conn.execute("CREATE EXTENSION IF NOT EXISTS vector") await register_vector(conn) # await conn.execute("DROP TABLE IF EXISTS example_prompt_sql_embeddings") await conn.execute( """CREATE TABLE IF NOT EXISTS example_prompt_sql_embeddings( user_grouping VARCHAR(1024) NOT NULL, example_user_question text NOT NULL, example_generated_sql text NOT NULL, embedding vector(768))""" ) await conn.close() elif VECTOR_STORE == "bigquery-vector": client=bigquery.Client(project=project_id) #Store table embeddings client.query_and_wait(f'''CREATE TABLE IF NOT EXISTS `{project_id}.{schema}.table_details_embeddings` ( source_type string NOT NULL, user_grouping string NOT NULL, table_schema string NOT NULL, table_name string NOT NULL, content string, embedding ARRAY)''') #job_config = bigquery.LoadJobConfig(write_disposition="WRITE_TRUNCATE") delete_conditions = table_details_embeddings[['user_grouping', 'table_name']].apply(tuple, axis=1).tolist() where_clause = " OR ".join([f"(user_grouping = '{cond[0]}' AND table_name = '{cond[1]}')" for cond in delete_conditions]) delete_query = f""" DELETE FROM `{project_id}.{schema}.table_details_embeddings` WHERE {where_clause} """ client.query_and_wait(delete_query) client.load_table_from_dataframe(table_details_embeddings,f'{project_id}.{schema}.table_details_embeddings') #Store column embeddings client.query_and_wait(f'''CREATE TABLE IF NOT EXISTS `{project_id}.{schema}.tablecolumn_details_embeddings` ( source_type string NOT NULL,user_grouping string NOT NULL, table_schema string NOT NULL, table_name string NOT NULL, column_name string NOT NULL, content string, embedding ARRAY)''') #job_config = bigquery.LoadJobConfig(write_disposition="WRITE_TRUNCATE") delete_conditions = tablecolumn_details_embeddings[['user_grouping', 'table_name', 'column_name']].apply(tuple, axis=1).tolist() where_clause = " OR ".join([f"(user_grouping = '{cond[0]}' AND table_name = '{cond[1]}' AND column_name = '{cond[2]}')" for cond in delete_conditions]) delete_query = f""" DELETE FROM `{project_id}.{schema}.tablecolumn_details_embeddings` WHERE {where_clause} """ client.query_and_wait(delete_query) client.load_table_from_dataframe(tablecolumn_details_embeddings,f'{project_id}.{schema}.tablecolumn_details_embeddings') client.query_and_wait(f'''CREATE TABLE IF NOT EXISTS `{project_id}.{schema}.example_prompt_sql_embeddings` ( user_grouping string NOT NULL, example_user_question string NOT NULL, example_generated_sql string NOT NULL, embedding ARRAY)''') else: raise ValueError("Please provide a valid Vector Store.") return "Embeddings are stored successfully" async def add_sql_embedding(user_question, generated_sql, database): emb=embedder.create(user_question) if VECTOR_STORE == "cloudsql-pgvector": # sql= f'''MERGE INTO example_prompt_sql_embeddings as tgt # using (SELECT '{user_question}' as example_user_question) as src # on tgt.example_user_question=src.example_user_question # when not matched then # insert (table_schema, example_user_question,example_generated_sql,embedding) # values('{database}','{user_question}','{generated_sql}','{(emb)}') # when matched then update set # table_schema = '{database}', # example_generated_sql = '{generated_sql}', # embedding = '{(emb)}' ''' # # print(sql) # conn=pgconnector.pool.connect() # await conn.execute(text(sql)) # pgconnector.retrieve_df(sql) loop = asyncio.get_running_loop() async with Connector(loop=loop) as connector: # Create connection to Cloud SQL database. conn: asyncpg.Connection = await connector.connect_async( f"{PROJECT_ID}:{PG_REGION}:{PG_INSTANCE}", # Cloud SQL instance connection name "asyncpg", user=f"{PG_USER}", password=f"{PG_PASSWORD}", db=f"{PG_DATABASE}", ) await conn.execute("CREATE EXTENSION IF NOT EXISTS vector") await register_vector(conn) await conn.execute("DELETE FROM example_prompt_sql_embeddings WHERE user_grouping= $1 and example_user_question=$2", database, user_question) cleaned_sql =generated_sql.replace("\r", " ").replace("\n", " ") await conn.execute( "INSERT INTO example_prompt_sql_embeddings (user_grouping, example_user_question, example_generated_sql, embedding) VALUES ($1, $2, $3, $4)", database, user_question, cleaned_sql, np.array(emb), ) elif VECTOR_STORE == "bigquery-vector": client=bigquery.Client(project=PROJECT_ID) client.query_and_wait(f'''CREATE TABLE IF NOT EXISTS `{PROJECT_ID}.{BQ_OPENDATAQNA_DATASET_NAME}.example_prompt_sql_embeddings` ( user_grouping string NOT NULL, example_user_question string NOT NULL, example_generated_sql string NOT NULL, embedding ARRAY)''') client.query_and_wait(f'''DELETE FROM `{PROJECT_ID}.{BQ_OPENDATAQNA_DATASET_NAME}.example_prompt_sql_embeddings` WHERE user_grouping= '{database}' and example_user_question= "{user_question}" ''' ) # embedding=np.array(row["embedding"]) cleaned_sql = generated_sql.replace("\r", " ").replace("\n", " ") client.query_and_wait(f'''INSERT INTO `{PROJECT_ID}.{BQ_OPENDATAQNA_DATASET_NAME}.example_prompt_sql_embeddings` VALUES ("{database}","{user_question}" , "{cleaned_sql}",{emb})''') return 1 if __name__ == '__main__': from retrieve_embeddings import retrieve_embeddings from utilities import PG_SCHEMA, PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION VECTOR_STORE = "cloudsql-pgvector" t, c = retrieve_embeddings(VECTOR_STORE, PG_SCHEMA) asyncio.run(store_schema_embeddings(t, c, PROJECT_ID, PG_INSTANCE, PG_DATABASE, PG_SCHEMA, PG_USER, PG_PASSWORD, PG_REGION, VECTOR_STORE = VECTOR_STORE)) ================================================ FILE: env_setup.py ================================================ import asyncio from google.cloud import bigquery import google.api_core from embeddings import retrieve_embeddings, store_schema_embeddings, setup_kgq_table, load_kgq_df, store_kgq_embeddings from utilities import ( PG_REGION, PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, BQ_REGION, EXAMPLES, LOGGING, VECTOR_STORE, PROJECT_ID, BQ_OPENDATAQNA_DATASET_NAME,FIRESTORE_REGION) import subprocess import time if VECTOR_STORE == 'bigquery-vector': DATASET_REGION = BQ_REGION elif VECTOR_STORE == 'cloudsql-pgvector': DATASET_REGION = PG_REGION def setup_postgresql(pg_instance, pg_region, pg_database, pg_user, pg_password): """Sets up a PostgreSQL Cloud SQL instance with a database and user. Args: pg_instance (str): Name of the Cloud SQL instance. pg_region (str): Region where the instance should be located. pg_database (str): Name of the database to create. pg_user (str): Name of the user to create. pg_password (str): Password for the user. """ # Check if Cloud SQL instance exists describe_cmd = ["gcloud", "sql", "instances", "describe", pg_instance, "--format=value(databaseVersion)"] describe_process = subprocess.run(describe_cmd, capture_output=True, text=True) if describe_process.returncode == 0: if describe_process.stdout.startswith("POSTGRES"): print("Found existing Postgres Cloud SQL Instance!") else: raise RuntimeError("Existing Cloud SQL instance is not PostgreSQL") else: print("Creating new Cloud SQL instance...") create_cmd = [ "gcloud", "sql", "instances", "create", pg_instance, "--database-version=POSTGRES_15", "--region", pg_region, "--cpu=1", "--memory=4GB", "--root-password", pg_password, "--database-flags=cloudsql.iam_authentication=On" ] subprocess.run(create_cmd, check=True) # Raises an exception if creation fails # Wait for instance to be ready print("Waiting for instance to be ready...") time.sleep(9999) # You might need to adjust this depending on how long it takes # Create the database list_cmd = ["gcloud", "sql", "databases", "list", "--instance", pg_instance] list_process = subprocess.run(list_cmd, capture_output=True, text=True) if pg_database in list_process.stdout: print("Found existing Postgres Cloud SQL database!") else: print("Creating new Cloud SQL database...") create_db_cmd = ["gcloud", "sql", "databases", "create", pg_database, "--instance", pg_instance] subprocess.run(create_db_cmd, check=True) # Create the user create_user_cmd = [ "gcloud", "sql", "users", "create", pg_user, "--instance", pg_instance, "--password", pg_password ] subprocess.run(create_user_cmd, check=True) print(f"PG Database {pg_database} in instance {pg_instance} is ready.") def create_vector_store(): """ Initializes the environment and sets up the vector store for Open Data QnA. This function performs the following steps: 1. Loads configurations from the "config.ini" file. 2. Determines the data source (BigQuery or CloudSQL PostgreSQL) and sets the dataset region accordingly. 3. If the vector store is "cloudsql-pgvector" and the data source is not CloudSQL PostgreSQL, it creates a new PostgreSQL dataset for the vector store. 4. If logging is enabled or the vector store is "bigquery-vector", it creates a BigQuery dataset for the vector store and logging table. 5. It creates a Vertex AI connection for the specified model and embeds the table schemas and columns into the vector database. 6. If embeddings are stored in BigQuery, creates a table column_details_embeddings in the BigQuery Dataset. 7. It generates the embeddings for the table schemas and column descriptions, and then inserts those embeddings into the BigQuery table. Configuration: - Requires the following environment variables to be set in "config.ini": - `DATA_SOURCE`: The data source (e.g., "bigquery" or "cloudsql-pg"). - `VECTOR_STORE`: The type of vector store (e.g., "bigquery-vector" or "cloudsql-pgvector"). - `BQ_REGION`: The BigQuery region. - `PROJECT_ID`: The Google Cloud project ID. - `BQ_OPENDATAQNA_DATASET_NAME`: The name of the BigQuery dataset for Open Data QnA. - `LOGGING`: Whether logging is enabled. - If `VECTOR_STORE` is "cloudsql-pgvector" and `DATA_SOURCE` is not "cloudsql-pg": - Requires additional environment variables for PostgreSQL instance setup. Returns: None Raises: RuntimeError: If there are errors during the setup process (e.g., dataset creation failure). """ print("Initializing environment setup.") print("Loading configurations from config.ini file.") print("Vector Store source set to: ", VECTOR_STORE) # Create PostgreSQL Instance is data source is different from PostgreSQL Instance if VECTOR_STORE == 'cloudsql-pgvector' : print("Generating pg dataset for vector store.") # Parameters for PostgreSQL Instance pg_region = DATASET_REGION pg_instance = "pg15-opendataqna" pg_database = "opendataqna-db" pg_user = "pguser" pg_password = "pg123" pg_schema = 'pg-vector-store' setup_postgresql(pg_instance, pg_region, pg_database, pg_user, pg_password) # Create a new data set on Bigquery to use for the logs table if LOGGING or VECTOR_STORE == 'bigquery-vector': if LOGGING: print("Logging is enabled") if VECTOR_STORE == 'bigquery-vector': print("Vector store set to 'bigquery-vector'") print(f"Generating Big Query dataset {BQ_OPENDATAQNA_DATASET_NAME}") client=bigquery.Client(project=PROJECT_ID) dataset_ref = f"{PROJECT_ID}.{BQ_OPENDATAQNA_DATASET_NAME}" # Create the dataset if it does not exist already try: client.get_dataset(dataset_ref) print("Destination Dataset exists") except google.api_core.exceptions.NotFound: print("Cannot find the dataset hence creating.......") dataset=bigquery.Dataset(dataset_ref) dataset.location=DATASET_REGION client.create_dataset(dataset) print(str(dataset_ref)+" is created") def get_embeddings(): """Generates and returns embeddings for table schemas and column descriptions. This function performs the following steps: 1. Retrieves table schema and column description data based on the specified data source (BigQuery or PostgreSQL). 2. Generates embeddings for the retrieved data using the configured embedding model. 3. Returns the generated embeddings for both tables and columns. Returns: Tuple[pd.DataFrame, pd.DataFrame]: A tuple containing two pandas DataFrames: - table_schema_embeddings: Embeddings for the table schemas. - col_schema_embeddings: Embeddings for the column descriptions. Configuration: This function relies on the following configuration variables: - DATA_SOURCE: The source database ("bigquery" or "cloudsql-pg"). - BQ_DATASET_NAME (if DATA_SOURCE is "bigquery"): The BigQuery dataset name. - BQ_TABLE_LIST (if DATA_SOURCE is "bigquery"): The list of BigQuery tables to process. - PG_SCHEMA (if DATA_SOURCE is "cloudsql-pg"): The PostgreSQL schema name. """ print("Generating embeddings from source db schemas") import pandas as pd import os current_dir = os.getcwd() root_dir = os.path.expanduser('~') # Start at the user's home directory while current_dir != root_dir: for dirpath, dirnames, filenames in os.walk(current_dir): config_path = os.path.join(dirpath, 'data_source_list.csv') if os.path.exists(config_path): file_path = config_path # Update root_dir to the found directory break # Stop outer loop once found current_dir = os.path.dirname(current_dir) print("Source Found at Path :: "+file_path) # Load the file df_src = pd.read_csv(file_path) df_src = df_src.loc[:, ["source", "user_grouping", "schema","table"]] df_src = df_src.sort_values(by=["source","user_grouping","schema","table"]) #If no schema Error Out if df_src['schema'].astype(str).str.len().min()==0 or df_src['schema'].isna().any(): raise ValueError("Schema column cannot be empty") #Group by for all the tables filtered df=df_src.groupby(['source','schema'])['table'].agg(lambda x: list(x.dropna().unique())).reset_index() df['table']=df['table'].apply(lambda x: None if pd.isna(x).any() else x) print("The Embeddings are extracted for the below combinations") print(df) table_schema_embeddings=pd.DataFrame(columns=['source_type','join_by','table_schema', 'table_name', 'content','embedding']) col_schema_embeddings=pd.DataFrame(columns=['source_type','join_by','table_schema', 'table_name', 'column_name', 'content','embedding']) for _, row in df.iterrows(): DATA_SOURCE = row['source'] SCHEMA = row['schema'] TABLE_LIST = row['table'] _t, _c = retrieve_embeddings(DATA_SOURCE, SCHEMA=SCHEMA, table_names=TABLE_LIST) _t["source_type"]=DATA_SOURCE _c["source_type"]=DATA_SOURCE if not TABLE_LIST: _t["join_by"]=DATA_SOURCE+"_"+SCHEMA+"_"+SCHEMA _c["join_by"]=DATA_SOURCE+"_"+SCHEMA+"_"+SCHEMA table_schema_embeddings = pd.concat([table_schema_embeddings,_t],ignore_index=True) col_schema_embeddings = pd.concat([col_schema_embeddings,_c],ignore_index=True) df_src['join_by'] = df_src.apply( lambda row: f"{row['source']}_{row['schema']}_{row['schema']}" if pd.isna(row['table']) else f"{row['source']}_{row['schema']}_{row['table']}",axis=1) table_schema_embeddings['join_by'] = table_schema_embeddings['join_by'].fillna(table_schema_embeddings['source_type'] + "_" + table_schema_embeddings['table_schema'] + "_" + table_schema_embeddings['table_name']) col_schema_embeddings['join_by'] = col_schema_embeddings['join_by'].fillna(col_schema_embeddings['source_type'] + "_" + col_schema_embeddings['table_schema'] + "_" + col_schema_embeddings['table_name']) table_schema_embeddings = table_schema_embeddings.merge(df_src[['join_by', 'user_grouping']], on='join_by', how='left') table_schema_embeddings.drop(columns=["join_by"],inplace=True) #Replace NaN values in group to default to the schema table_schema_embeddings['user_grouping'] = table_schema_embeddings['user_grouping'].fillna(table_schema_embeddings['table_schema']+"-"+table_schema_embeddings['source_type']) col_schema_embeddings = col_schema_embeddings.merge(df_src[['join_by', 'user_grouping']], on='join_by', how='left') col_schema_embeddings.drop(columns=["join_by"],inplace=True) #Replace NaN values in group to default to the schema col_schema_embeddings['user_grouping'] = col_schema_embeddings['user_grouping'].fillna(col_schema_embeddings['table_schema']+"-"+col_schema_embeddings['source_type']) print("Table and Column embeddings are created") return table_schema_embeddings, col_schema_embeddings async def store_embeddings(table_schema_embeddings, col_schema_embeddings): """ Stores table and column embeddings into the specified vector store. This asynchronous function saves precomputed embeddings for table schemas and column descriptions into either BigQuery or PostgreSQL (with pgvector extension) based on the VECTOR_STORE configuration. Args: table_schema_embeddings (pd.DataFrame): Embeddings for the table schemas. col_schema_embeddings (pd.DataFrame): Embeddings for the column descriptions. Configuration: This function relies on the following configuration variables: - VECTOR_STORE: Determines the target vector store ("bigquery-vector" or "cloudsql-pgvector"). - PROJECT_ID, BQ_REGION, BQ_OPENDATAQNA_DATASET_NAME (if VECTOR_STORE is "bigquery-vector"): Configuration for BigQuery storage. - PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION (if VECTOR_STORE is "cloudsql-pgvector"): Configuration for PostgreSQL storage. Returns: None """ print("Storing embeddings back to the vector store.") if VECTOR_STORE=='bigquery-vector': await(store_schema_embeddings(table_details_embeddings=table_schema_embeddings, tablecolumn_details_embeddings=col_schema_embeddings, project_id=PROJECT_ID, instance_name=None, database_name=None, schema=BQ_OPENDATAQNA_DATASET_NAME, database_user=None, database_password=None, region=BQ_REGION, VECTOR_STORE = VECTOR_STORE )) elif VECTOR_STORE=='cloudsql-pgvector': await(store_schema_embeddings(table_details_embeddings=table_schema_embeddings, tablecolumn_details_embeddings=col_schema_embeddings, project_id=PROJECT_ID, instance_name=PG_INSTANCE, database_name=PG_DATABASE, schema=None, database_user=PG_USER, database_password=PG_PASSWORD, region=PG_REGION, VECTOR_STORE = VECTOR_STORE )) print("Table and Column embeddings are saved to vector store") async def create_kgq_sql_table(): """ Creates a table for storing Known Good Query (KGQ) embeddings in the vector store. This asynchronous function conditionally sets up a table to store known good SQL queries and their embeddings, which are used to provide examples to the LLM during query generation. The table is created only if the `EXAMPLES` configuration variable is set to 'yes'. If not, it prints a warning message encouraging the user to create a query cache for better results. Configuration: This function relies on the following configuration variables: - EXAMPLES: Determines whether to create the KGQ table ('yes' to create). - VECTOR_STORE: Specifies the target vector store ("bigquery-vector" or "cloudsql-pgvector"). - PROJECT_ID, BQ_REGION, BQ_OPENDATAQNA_DATASET_NAME (if VECTOR_STORE is "bigquery-vector"): Configuration for BigQuery storage. - PG_INSTANCE, PG_DATABASE, PG_USER, PG_PASSWORD, PG_REGION (if VECTOR_STORE is "cloudsql-pgvector"): Configuration for PostgreSQL storage. Returns: None """ if EXAMPLES: print("Creating kgq table in vector store.") # Delete any old tables and create a new table to KGQ embeddings if VECTOR_STORE=='bigquery-vector': await(setup_kgq_table(project_id=PROJECT_ID, instance_name=None, database_name=None, schema=BQ_OPENDATAQNA_DATASET_NAME, database_user=None, database_password=None, region=BQ_REGION, VECTOR_STORE = VECTOR_STORE )) elif VECTOR_STORE=='cloudsql-pgvector': await(setup_kgq_table(project_id=PROJECT_ID, instance_name=PG_INSTANCE, database_name=PG_DATABASE, schema=None, database_user=PG_USER, database_password=PG_PASSWORD, region=PG_REGION, VECTOR_STORE = VECTOR_STORE )) else: print("⚠️ WARNING: No Known Good Queries are provided to create query cache for Few shot examples!") print("Creating a query cache is highly recommended for best outcomes") print("If no Known Good Queries for the dataset are availabe at this time, you can use 3_LoadKnownGoodSQL.ipynb to load them later!!") async def store_kgq_sql_embeddings(): """ Stores known good query (KGQ) embeddings into the specified vector store. This asynchronous function reads known good SQL queries from the "known_good_sql.csv" file and stores their embeddings in either BigQuery or PostgreSQL (with pgvector) depending on the `VECTOR_STORE` configuration. This process is only performed if the `EXAMPLES` configuration variable is set to 'yes'. Otherwise, a warning message is displayed, highlighting the importance of creating a query cache. Configuration: - Requires the "known_good_sql.csv" file to be present in the project directory. - Relies on the following configuration variables: - `EXAMPLES`: Determines whether to store KGQ embeddings ('yes' to store). - `VECTOR_STORE`: Specifies the target vector store ("bigquery-vector" or "cloudsql-pgvector"). - `PROJECT_ID`, `BQ_REGION`, `BQ_OPENDATAQNA_DATASET_NAME` (if VECTOR_STORE is "bigquery-vector"): Configuration for BigQuery storage. - `PG_INSTANCE`, `PG_DATABASE`, `PG_USER`, `PG_PASSWORD`, `PG_REGION` (if VECTOR_STORE is "cloudsql-pgvector"): Configuration for PostgreSQL storage. Returns: None """ if EXAMPLES: print("Reading contents of known_good_sql.csv") # Load the contents of the known_good_sql.csv file into a dataframe df_kgq = load_kgq_df() print("Storing kgq embeddings in vector store table.") # Add KGQ to the vector store if VECTOR_STORE=='bigquery-vector': await(store_kgq_embeddings(df_kgq, project_id=PROJECT_ID, instance_name=None, database_name=None, schema=BQ_OPENDATAQNA_DATASET_NAME, database_user=None, database_password=None, region=BQ_REGION, VECTOR_STORE = VECTOR_STORE )) elif VECTOR_STORE=='cloudsql-pgvector': await(store_kgq_embeddings(df_kgq, project_id=PROJECT_ID, instance_name=PG_INSTANCE, database_name=PG_DATABASE, schema=None, database_user=PG_USER, database_password=PG_PASSWORD, region=PG_REGION, VECTOR_STORE = VECTOR_STORE )) print('kgq embeddings stored.') else: print("⚠️ WARNING: No Known Good Queries are provided to create query cache for Few shot examples!") print("Creating a query cache is highly recommended for best outcomes") print("If no Known Good Queries for the dataset are availabe at this time, you can use 3_LoadKnownGoodSQL.ipynb to load them later!!") def create_firestore_db(firestore_region=FIRESTORE_REGION,firestore_database="opendataqna-session-logs"): # Check if Firestore database exists database_exists_cmd = [ "gcloud", "firestore", "databases", "list", "--filter", f"name=projects/{PROJECT_ID}/databases/{firestore_database}", "--format", "value(name)" # Extract just the name if found ] database_exists_process = subprocess.run( database_exists_cmd, capture_output=True, text=True ) if database_exists_process.returncode == 0 and database_exists_process.stdout: if database_exists_process.stdout.startswith(f"projects/{PROJECT_ID}/databases/{firestore_database}"): print("Found existing Firestore database with this name already!") else: raise RuntimeError("Issue with checking if the firestore db exists or not") else: # Create Firestore database print("Creating new Firestore database...") create_db_cmd = [ "gcloud", "firestore", "databases", "create", "--database", firestore_database, "--location", firestore_region, "--project", PROJECT_ID ] subprocess.run(create_db_cmd, check=True) # Raise exception on failure # Potential wait for database readiness (optional) time.sleep(30) # May not be strictly necessary for basic use if __name__ == '__main__': # Setup vector store for embeddings create_vector_store() # Generate embeddings for tables and columns table_schema_embeddings, col_schema_embeddings = get_embeddings() # Store table/column embeddings (asynchronous) asyncio.run(store_embeddings(table_schema_embeddings, col_schema_embeddings)) # Create table for known good queries (if enabled) asyncio.run(create_kgq_sql_table()) # Store known good query embeddings (if enabled) asyncio.run(store_kgq_sql_embeddings()) create_firestore_db() ================================================ FILE: frontend/.gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # Compiled output /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: frontend/README.md ================================================

Deploy Frontend Demo UI

**Technologies and Components** * **Framework:** Angular * **Hosting Platform:** Firebase **Note** : This UI demo doesn't configure any domain restrictions. If you choose to build one refer to this link https://firebase.google.com/docs/functions/auth-blocking-events?gen=2nd#only_allowing_registration_from_a_specific_domain 1. Install the firebase tools to run CLI commands ``` cd Open_Data_QnA gcloud services enable firebase.googleapis.com --project=$PROJECT_ID # Enable firebase management API npm install -g firebase-tools ``` ``` export PROJECT_ID= export REGION= ``` 2. Build the firebase community builder image Cloud Build provides a Firebase community builder image that you can use to invoke firebase commands in Cloud Build. To use this builder in a Cloud Build config file, you must first build the image and push it to the Container Registry in your project. **Note**:*Please complete the steps carely and use the same project which you are going to host the app* Follow detailed instructions: 1. Navigate to your project root directory. 2. Clone the cloud-builders-community repository: ``` git clone https://github.com/GoogleCloudPlatform/cloud-builders-community.git ``` 3. Navigate to the firebase builder image: ``` cd cloud-builders-community/firebase ``` 4. Submit the builder to your project, where REGION is one of the supported build regions: ``` gcloud builds submit --region=$REGION . --project=$PROJECT_ID ``` 5. Navigate back to your project root directory: ``` cd ../.. ``` 6. Remove the repository from your root directory: ``` rm -rf cloud-builders-community/ ``` 3. Create and Initialize Firebase ``` cd Open_Data_QnA/frontend rm firebase.json .firebaserc firebase login --no-localhost ## Below command can be used re authenticate in case of authentication errors firebase login --reauth --no-localhost #If incase there are old firebase files ``` ``` firebase init hosting ## Select "Add Firebase to an existing Google Cloud Platfrom Project" ## For the public directory prompt provide >> /dist/frontend/browser ## Rewrite all URLs to index prompt enter >> Yes (Enter No for any other options) ## You should now see firebase.json created in the folder ``` ``` ## To modify the contents for this solution update it using the cp command as below cp firebase_setup.json firebase.json ``` ``` ## Run below command to create a webapp to host your application firebase apps:create --project $PROJECT_ID ## Select Web and Provide name : "opendataqna" ``` ``` ## Below command provides the initialization code to add to your constant file firebase apps:sdkconfig --project $PROJECT_ID ``` 4. Enable Google Authentication in Firebase Console - Go to the Firebase console (https://console.firebase.google.com/). - Select your project. - Navigate to "Authentication" -> "Sign-in method". - Click "Add new provider" and select "Google". - Provide a support email and click "Enable". This will enable Google authentication for your project. 5. Update the Config Code and Endpoint URLs for the frontend In the file [`/frontend/src/assets/constants.ts`](/frontend/src/assets/constants.ts) * Replace the config object with the one you copied in the above step * Replace the ENDPOINT_OPENDATAQNA value with the Service URL from the Endpoint Deployment section in the backend-apis README.md ***Note that these variables need to be exported using "export" keyword. So make sure export is mentioned for both the variables***

aaie image

6. Deploy the app Run the below commands on the terminal ``` cd Open_Data_QnA/frontend ``` ``` gcloud builds submit . --config frontend.yaml --substitutions _FIREBASE_PROJECT_ID=$PROJECT_ID --project=$PROJECT_ID ``` --------- --------- You can see the app URL at the end of successful deployment > Once deployed login if your Google Account > Select Business User > Select a database in the dropdown (top right) > Type in the Query > Hit Query A successful SQL generated will be show as below with result as below

aaie image

aaie image

Hit Visualize to see the results and charts as below

aaie image

---- ---- **API Details** All the payloads are in JSON format 1. List Databases : Get the available databases in the vector store that solution can run against URI: {Service URL}/available_databases Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/available_databases Method: GET Request Payload : NONE Request response: ``` { "Error": "", "KnownDB": "[{\"table_schema\":\"imdb-postgres\"},{\"table_schema\":\"retail-postgres\"}]", "ResponseCode": 200 } ``` 2. Known SQL : Get suggestive questions (previously asked/examples added) for selected database URI: /get_known_sql Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/get_known_sql Method: POST Request Payload : ``` { "user_grouping":"retail" } ``` Request response: ``` { "Error": "", "KnownSQL": "[{\"example_user_question\":\"Which city had maximum number of sales and what was the count?\",\"example_generated_sql\":\"select st.city_id, count(st.city_id) as city_sales_count from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by city_sales_count desc limit 1;\"}]", "ResponseCode": 200 } ``` 3. SQL Generation : Generate the SQL for the input question asked against a database URI: /generate_sql Method: POST Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/get_known_sql Request payload: ``` { "session_id":"", "user_id":"harry@hogwarts.com", "user_question":"Which city had maximum number of sales?", "user_grouping":"retail" } ``` Request response: ``` { "Error": "", "GeneratedSQL": " select st.city_id from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by count(*) desc limit 1;", "ResponseCode": 200, "SessionID":"1iuu2u-k1ij2-kkkhhj12131" } ``` 4. Execute SQL : Run the SQL statement against provided database source URI:/run_query Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/run_query Method: POST Request payload: ``` { "user_grouping": "retail", "generated_sql":"select st.city_id from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by count(*) desc limit 1;", "session_id":"1iuu2u-k1ij2-kkkhhj12131" } ``` Request response: ``` { "SessionID":"1iuu2u-k1ij2-kkkhhj12131", "Error": "", "KnownDB": "[{\"city_id\":\"C014\"}]", "ResponseCode": 200 } ``` 5. Embedd SQL : To embed known good SQLs to your example embeddings URI:/embed_sql Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/embed_sql METHOD: POST Request Payload: ``` { "session_id":"1iuu2u-k1ij2-kkkhhj12131", "user_question":"Which city had maximum number of sales?", "generated_sql":"select st.city_id from retail.sales as s join retail.stores as st on s.id_store = st.id_store group by st.city_id order by count(*) desc limit 1;", "user_grouping":"retail" } ``` Request response: ``` { "ResponseCode" : 201, "Message" : "Example SQL has been accepted for embedding", "Error":"", "SessionID":"1iuu2u-k1ij2-kkkhhj12131" } ``` 6. Generate Visualization Code : To generated javascript Google Charts code based on the SQL Results and display them on the UI As per design we have two visualizations suggested showing up when the user clicks the visualize button. Hence two divs are send as part of the response “chart_div”, “chart_div_1” to bind them to that element in the UI If you are only looking to setup enpoint you can stop here. In case you require the demo app (frontend UI) built in the solution, proceed to the next step. URI:/generate_viz Complete URL Sample : https://OpenDataQnA-aeiouAEI-uc.a.run.app/generate_viz METHOD: POST Request Payload: ``` { "session_id":"1iuu2u-k1ij2-kkkhhj12131" , "user_question": "What are top 5 product skus that are ordered?", "sql_generated": "SELECT productSKU as ProductSKUCode, sum(total_ordered) as TotalOrderedItems FROM `inbq1-joonix.demo.sales_sku` group by productSKU order by sum(total_ordered) desc limit 5", "sql_results": [ { "ProductSKUCode": "GGOEGOAQ012899", "TotalOrderedItems": 456 }, { "ProductSKUCode": "GGOEGDHC074099", "TotalOrderedItems": 334 }, { "ProductSKUCode": "GGOEGOCB017499", "TotalOrderedItems": 319 }, { "ProductSKUCode": "GGOEGOCC077999", "TotalOrderedItems": 290 }, { "ProductSKUCode": "GGOEGFYQ016599", "TotalOrderedItems": 253 } ] } ``` Request response: ``` { "SessionID":"1iuu2u-k1ij2-kkkhhj12131", "Error": "", "GeneratedChartjs": { "chart_div": "google.charts.load('current', {\n packages: ['corechart']\n});\ngoogle.charts.setOnLoadCallback(drawChart);\n\nfunction drawChart() {\n var data = google.visualization.arrayToDataTable([\n ['Product SKU', 'Total Ordered Items'],\n ['GGOEGOAQ012899', 456],\n ['GGOEGDHC074099', 334],\n ['GGOEGOCB017499', 319],\n ['GGOEGOCC077999', 290],\n ['GGOEGFYQ016599', 253],\n ]);\n\n var options = {\n title: 'Top 5 Product SKUs Ordered',\n width: 600,\n height: 300,\n hAxis: {\n textStyle: {\n fontSize: 12\n }\n },\n vAxis: {\n textStyle: {\n fontSize: 12\n }\n },\n legend: {\n textStyle: {\n fontSize: 12\n }\n },\n bar: {\n groupWidth: '50%'\n }\n };\n\n var chart = new google.visualization.BarChart(document.getElementById('chart_div'));\n\n chart.draw(data, options);\n}\n", "chart_div_1": "google.charts.load('current', {'packages':['corechart']});\ngoogle.charts.setOnLoadCallback(drawChart);\nfunction drawChart() {\n var data = google.visualization.arrayToDataTable([\n ['ProductSKUCode', 'TotalOrderedItems'],\n ['GGOEGOAQ012899', 456],\n ['GGOEGDHC074099', 334],\n ['GGOEGOCB017499', 319],\n ['GGOEGOCC077999', 290],\n ['GGOEGFYQ016599', 253]\n ]);\n\n var options = {\n title: 'Top 5 Product SKUs that are Ordered',\n width: 600,\n height: 300,\n hAxis: {\n textStyle: {\n fontSize: 5\n }\n },\n vAxis: {\n textStyle: {\n fontSize: 5\n }\n },\n legend: {\n textStyle: {\n fontSize: 10\n }\n },\n bar: {\n groupWidth: \"60%\"\n }\n };\n\n var chart = new google.visualization.ColumnChart(document.getElementById('chart_div_1'));\n\n chart.draw(data, options);\n}\n" }, "ResponseCode": 200 } ``` ================================================ FILE: frontend/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "projects": { "frontend": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/frontend", "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ "zone.js" ], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss" ], "scripts": [], "server": "src/main.server.ts", "prerender": true, "ssr": { "entry": "server.ts" } }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "1mb", "maximumError": "2mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { "buildTarget": "frontend:build" }, "configurations": { "production": { "buildTarget": "frontend:build:production" }, "development": { "buildTarget": "frontend:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "frontend:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "polyfills": [ "zone.js", "zone.js/testing" ], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [] } } } } }, "cli": { "analytics": false } } ================================================ FILE: frontend/database.indexes.json ================================================ { "indexes": [ { "collectionGroup": "session_logs", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "user_id", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] } ], "fieldOverrides": [] } ================================================ FILE: frontend/database.rules.json ================================================ rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read: if true; allow write: if false; } } } ================================================ FILE: frontend/firebase_setup.json ================================================ { "hosting": { "public": "/dist/frontend/browser", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] }, "firestore": [ { "database": "opendataqna-session-logs", "rules": "database.rules.json", "indexes": "database.indexes.json" } ] } ================================================ FILE: frontend/frontend-flutter/.flutter-plugins ================================================ # This is a generated file; do not edit or check into version control. audio_waveforms=/Users/raimeur/.pub-cache/hosted/pub.dev/audio_waveforms-1.0.5/ cloud_firestore=/Users/raimeur/.pub-cache/hosted/pub.dev/cloud_firestore-5.4.0/ cloud_firestore_web=/Users/raimeur/.pub-cache/hosted/pub.dev/cloud_firestore_web-4.2.0/ emoji_picker_flutter=/Users/raimeur/.pub-cache/hosted/pub.dev/emoji_picker_flutter-1.6.4/ file_picker=/Users/raimeur/.pub-cache/hosted/pub.dev/file_picker-8.0.6/ file_selector_linux=/Users/raimeur/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.2+1/ file_selector_macos=/Users/raimeur/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4/ file_selector_windows=/Users/raimeur/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+2/ firebase_auth=/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/ firebase_auth_web=/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_auth_web-5.12.5/ firebase_core=/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_core-3.4.0/ firebase_core_web=/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_core_web-2.17.5/ flutter_inappwebview=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview-6.0.0/ flutter_inappwebview_android=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_android-1.0.13/ flutter_inappwebview_ios=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_ios-1.0.13/ flutter_inappwebview_macos=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_macos-1.0.11/ flutter_inappwebview_web=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_web-1.0.8/ flutter_keyboard_visibility=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility-6.0.0/ flutter_keyboard_visibility_linux=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_linux-1.0.0/ flutter_keyboard_visibility_macos=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_macos-1.0.0/ flutter_keyboard_visibility_web=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_web-2.0.0/ flutter_keyboard_visibility_windows=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_windows-1.0.0/ flutter_plugin_android_lifecycle=/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.21/ image_picker=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker-1.1.2/ image_picker_android=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+10/ image_picker_for_web=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.4/ image_picker_ios=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12/ image_picker_linux=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+1/ image_picker_macos=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+1/ image_picker_web=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_web-4.0.0/ image_picker_windows=/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/ libphonenumber_plugin=/Users/raimeur/.pub-cache/hosted/pub.dev/libphonenumber_plugin-0.3.3/ libphonenumber_web=/Users/raimeur/.pub-cache/hosted/pub.dev/libphonenumber_web-0.3.2/ path_provider=/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider-2.1.4/ path_provider_android=/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_android-2.2.9/ path_provider_foundation=/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/ path_provider_linux=/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ path_provider_windows=/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ pointer_interceptor=/Users/raimeur/.pub-cache/hosted/pub.dev/pointer_interceptor-0.10.1+1/ pointer_interceptor_ios=/Users/raimeur/.pub-cache/hosted/pub.dev/pointer_interceptor_ios-0.10.1/ pointer_interceptor_web=/Users/raimeur/.pub-cache/hosted/pub.dev/pointer_interceptor_web-0.10.2/ shared_preferences=/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences-2.3.0/ shared_preferences_android=/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.0/ shared_preferences_foundation=/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.0/ shared_preferences_linux=/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.0/ shared_preferences_web=/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.0/ shared_preferences_windows=/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.0/ url_launcher=/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher-6.3.0/ url_launcher_android=/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.8/ url_launcher_ios=/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.1/ url_launcher_linux=/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_linux-3.1.1/ url_launcher_macos=/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.0/ url_launcher_web=/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.1/ url_launcher_windows=/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.2/ ================================================ FILE: frontend/frontend-flutter/.flutter-plugins-dependencies ================================================ {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"audio_waveforms","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/audio_waveforms-1.0.5/","native_build":true,"dependencies":[]},{"name":"cloud_firestore","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/cloud_firestore-5.4.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"emoji_picker_flutter","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/emoji_picker_flutter-1.6.4/","native_build":true,"dependencies":[]},{"name":"file_picker","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/file_picker-8.0.6/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_core-3.4.0/","native_build":true,"dependencies":[]},{"name":"flutter_inappwebview_ios","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_ios-1.0.13/","native_build":true,"dependencies":[]},{"name":"flutter_keyboard_visibility","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility-6.0.0/","native_build":true,"dependencies":[]},{"name":"image_picker_ios","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12/","native_build":true,"dependencies":[]},{"name":"libphonenumber_plugin","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/libphonenumber_plugin-0.3.3/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"pointer_interceptor_ios","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/pointer_interceptor_ios-0.10.1/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.1/","native_build":true,"dependencies":[]}],"android":[{"name":"audio_waveforms","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/audio_waveforms-1.0.5/","native_build":true,"dependencies":[]},{"name":"cloud_firestore","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/cloud_firestore-5.4.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"emoji_picker_flutter","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/emoji_picker_flutter-1.6.4/","native_build":true,"dependencies":[]},{"name":"file_picker","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/file_picker-8.0.6/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"firebase_auth","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_core-3.4.0/","native_build":true,"dependencies":[]},{"name":"flutter_inappwebview_android","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_android-1.0.13/","native_build":true,"dependencies":[]},{"name":"flutter_keyboard_visibility","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility-6.0.0/","native_build":true,"dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.21/","native_build":true,"dependencies":[]},{"name":"image_picker_android","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+10/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"libphonenumber_plugin","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/libphonenumber_plugin-0.3.3/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_android-2.2.9/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.8/","native_build":true,"dependencies":[]}],"macos":[{"name":"cloud_firestore","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/cloud_firestore-5.4.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"emoji_picker_flutter","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/emoji_picker_flutter-1.6.4/","native_build":true,"dependencies":[]},{"name":"file_selector_macos","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_core-3.4.0/","native_build":true,"dependencies":[]},{"name":"flutter_inappwebview_macos","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_macos-1.0.11/","native_build":true,"dependencies":[]},{"name":"flutter_keyboard_visibility_macos","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_macos-1.0.0/","native_build":false,"dependencies":[]},{"name":"image_picker_macos","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+1/","native_build":false,"dependencies":["file_selector_macos"]},{"name":"path_provider_foundation","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.0/","native_build":true,"dependencies":[]}],"linux":[{"name":"emoji_picker_flutter","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/emoji_picker_flutter-1.6.4/","native_build":true,"dependencies":[]},{"name":"file_selector_linux","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.2+1/","native_build":true,"dependencies":[]},{"name":"flutter_keyboard_visibility_linux","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_linux-1.0.0/","native_build":false,"dependencies":[]},{"name":"image_picker_linux","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+1/","native_build":false,"dependencies":["file_selector_linux"]},{"name":"path_provider_linux","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.0/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_linux-3.1.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"cloud_firestore","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/cloud_firestore-5.4.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"emoji_picker_flutter","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/emoji_picker_flutter-1.6.4/","native_build":true,"dependencies":[]},{"name":"file_selector_windows","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+2/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_core-3.4.0/","native_build":true,"dependencies":[]},{"name":"flutter_keyboard_visibility_windows","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_windows-1.0.0/","native_build":false,"dependencies":[]},{"name":"image_picker_windows","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/","native_build":false,"dependencies":["file_selector_windows"]},{"name":"path_provider_windows","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.0/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.2/","native_build":true,"dependencies":[]}],"web":[{"name":"cloud_firestore_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/cloud_firestore_web-4.2.0/","dependencies":["firebase_core_web"]},{"name":"emoji_picker_flutter","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/emoji_picker_flutter-1.6.4/","dependencies":[]},{"name":"file_picker","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/file_picker-8.0.6/","dependencies":[]},{"name":"firebase_auth_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_auth_web-5.12.5/","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/firebase_core_web-2.17.5/","dependencies":[]},{"name":"flutter_inappwebview_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_inappwebview_web-1.0.8/","dependencies":[]},{"name":"flutter_keyboard_visibility_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/flutter_keyboard_visibility_web-2.0.0/","dependencies":[]},{"name":"image_picker_for_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.4/","dependencies":[]},{"name":"image_picker_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/image_picker_web-4.0.0/","dependencies":[]},{"name":"libphonenumber_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/libphonenumber_web-0.3.2/","dependencies":[]},{"name":"pointer_interceptor_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/pointer_interceptor_web-0.10.2/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.0/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/raimeur/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.1/","dependencies":[]}]},"dependencyGraph":[{"name":"audio_waveforms","dependencies":[]},{"name":"cloud_firestore","dependencies":["cloud_firestore_web","firebase_core"]},{"name":"cloud_firestore_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"emoji_picker_flutter","dependencies":["shared_preferences"]},{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"flutter_inappwebview","dependencies":["flutter_inappwebview_android","flutter_inappwebview_ios","flutter_inappwebview_macos","flutter_inappwebview_web"]},{"name":"flutter_inappwebview_android","dependencies":[]},{"name":"flutter_inappwebview_ios","dependencies":[]},{"name":"flutter_inappwebview_macos","dependencies":[]},{"name":"flutter_inappwebview_web","dependencies":[]},{"name":"flutter_keyboard_visibility","dependencies":["flutter_keyboard_visibility_linux","flutter_keyboard_visibility_macos","flutter_keyboard_visibility_web","flutter_keyboard_visibility_windows"]},{"name":"flutter_keyboard_visibility_linux","dependencies":[]},{"name":"flutter_keyboard_visibility_macos","dependencies":[]},{"name":"flutter_keyboard_visibility_web","dependencies":[]},{"name":"flutter_keyboard_visibility_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_web","dependencies":[]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"libphonenumber_plugin","dependencies":["libphonenumber_web"]},{"name":"libphonenumber_web","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"pointer_interceptor","dependencies":["pointer_interceptor_ios","pointer_interceptor_web"]},{"name":"pointer_interceptor_ios","dependencies":[]},{"name":"pointer_interceptor_web","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2024-09-10 18:23:23.537388","version":"3.22.2"} ================================================ FILE: frontend/frontend-flutter/Open Data QnA - Working Sheet V2 - sample_questions_UI copy.csv ================================================ user_grouping,scenario,question MovieExplorer-bigquery,Genres,What are the top 5 most common movie genres in the dataset? MovieExplorer-bigquery,Genres,How many are musicals? MovieExplorer-bigquery,Genres,Romance? MovieExplorer-bigquery,Movie,What is the average user rating of the God Father movie? MovieExplorer-bigquery,Movie,Which year was it released? MovieExplorer-bigquery,Movie,How long is it? MovieExplorer-bigquery,Movie,Who is the lead actor? MovieExplorer-bigquery,Movie,Director MovieExplorer-bigquery,Movie,Cast WorldCensus-cloudsql-pg,Life Expectancy,What is the life expectancy for men and women in a United States in 2022? WorldCensus-cloudsql-pg,Life Expectancy,In India? WorldCensus-cloudsql-pg,Life Expectancy,Which country has highest male life expectancy? WorldCensus-cloudsql-pg,Life Expectancy,Female life expectancy? WorldCensus-cloudsql-pg,Population Density,What are the top 5 coutries with highest population density in 2024? WorldCensus-cloudsql-pg,Population Density,What are the birth and death rates in these counties? WorldCensus-cloudsql-pg,Sex Ratio at Birth,What is the sex ratio at birth in China in 2023? WorldCensus-cloudsql-pg,Sex Ratio at Birth,Which country has the highest? WorldCensus-cloudsql-pg,Sex Ratio at Birth,Whats the world average? ================================================ FILE: frontend/frontend-flutter/Open_Data_QnA_sample_questions_v3 copy.csv ================================================ user_grouping,scenario,question,main_question MovieExplorer-bigquery,Genres,What are the top 5 most common movie genres in the dataset?,Y MovieExplorer-bigquery,Genres,How many are musicals?,N MovieExplorer-bigquery,Genres,Romance?,N MovieExplorer-bigquery,Movie,What is the average user rating of the God Father movie?,Y MovieExplorer-bigquery,Movie,Which year was it released?,N MovieExplorer-bigquery,Movie,How long is it?,N MovieExplorer-bigquery,Movie,Who is the lead actor?,N MovieExplorer-bigquery,Movie,Director,N MovieExplorer-bigquery,Movie,Cast,N MovieExplorer-bigquery,Movie,Who is the actor playing the role of the godfather in the Godfather movie?,Y MovieExplorer-bigquery,Movie,and the one playing the role of Sony?,N MovieExplorer-bigquery,Movie,How many people saw the Godfather?,y WorldCensus-cloudsql-pg,Life Expectancy,What is the life expectancy for men and women in a United States in 2022?,Y WorldCensus-cloudsql-pg,Life Expectancy,In India?,N WorldCensus-cloudsql-pg,Life Expectancy,Which country has highest male life expectancy?,N WorldCensus-cloudsql-pg,Life Expectancy,Female life expectancy?,N WorldCensus-cloudsql-pg,Population Density,What are the top 5 coutries with highest population density in 2024?,Y WorldCensus-cloudsql-pg,Population Density,What are the birth and death rates in these counties?,N WorldCensus-cloudsql-pg,Sex Ratio at Birth,What is the sex ratio at birth in China in 2023?,Y WorldCensus-cloudsql-pg,Sex Ratio at Birth,Which country has the highest?,N WorldCensus-cloudsql-pg,Sex Ratio at Birth,What is the world average?,N WorldCensus-cloudsql-pg,Sex Ratio at Birth,What country has the lowest male ration?,Y WorldCensus-cloudsql-pg,Sex Ratio at Birth,Since when?,N ================================================ FILE: frontend/frontend-flutter/README.md ================================================ # Deploy the Flutter-based Frontend demo UI

aaie image

## Technologies and Components In order to use easily the Open Data QnA SDK and the backend you have just installed (please refer to README.md related to the backend on this repo for the details if not installed yet) you need to have a front end. This page explains how to install the frontend provided by this solution gracefully so that you can jump start the use of the Open Data QnA solution. Obviously, you can pretty much develop your own frontend and call the APIs available of the backend deployments. There are 2 versions of the frontend, one written in Angular and one written in Flutter, an Open Souce UI framework written in dart language and backed up by Google. Functionality-wise, they are both equivalent even though some minor differences may exist. This readme is about installing the Flutter-based frontend. For more information on Flutter : - [Flutter documentation](https://docs.flutter.dev/?_gl=1*17csnxq*_ga*MTI3NTU2MjQxMC4xNzI1ODc2Njg5*_ga_04YGWK0175*MTcyNTg3NjY4OS4xLjAuMTcyNTg3NjY4OS4wLjAuMA..) The frontend needs to be deployed to Firebase, a Mobile Backend as a Service. It has a free tier call "Spark Plan" allowing you to use the services required to run the frontend. For more details on Firebase, plase have a look at the documentation below: - [Firebase pricing tiers](https://firebase.google.com/pricing) - [Firebase Documentation](https://firebase.google.com/docs) ## Getting Started ### Installing Flutter and dart SDK #### Installing Flutter The first step is to install the Flutter framework. To build the Flutter app, you can either use an IDE, like Visual Studio Code with the Flutter SDK (and plugin) and relevant extension, or just the Flutter's command-line tools to build the app manually. This guide only explains how to use the Flutter's command-line tools via the installation of the Flutter SDK bundle (that also contains the dart SDK). Please click on the link below. - [Flutter SDK installation](https://docs.flutter.dev/get-started/install) You'll end-up on the the landing page below. 1- Click on the platform corresponding to the OS of the desktop you'll install the frontend on. As an example, let's use Windows :

2- Click on the Web type of app :

3- Click on the flutter_windows_3.24.2-stable.zip button (version will vary over time) 4- Move the zip file into the target folder you want, let's say "dev" 5- Extract the archive

6- Update the PATH environment variable : %USERPROFILE%\dev\flutter\bin 7- Test the installation by typing the "flutter doctor" command :

### Installing Firebase tools 1- Install the Firebase CLI In order to interact easily with Firebase, you have to install the Firebase CLI. You need to install npm first as a prerequisite: ``` npm install -g firebase-tools ``` With these Firebase CLI commands, you're able to authenticate to Firebase and do the deployment of the frontend on the Firebase Hosting service. 2- Test the Firebase CLI In order to very the Firebase CLI are working fine, login to Firebase using the command below. ``` firebase login ``` For more details on the installation, please look at the link below: - [Firebase CLI installation documentation](https://firebase.google.com/docs/cli#windows-npm) ### Installing Flutterfire CLI Flutter is tightly integrated to Firebase. The flutterfire_cli command,which is part of the FlutterFire CLI, is specifically designed for Flutter projects. It streamlines the process of connecting your Flutter app to Firebase and generating the necessary configuration files (firebase_options.dart) for different platforms (Android, iOS, web, etc.). Install it from any directory, we'll need it later on. ``` dart pub global activate flutterfire_cli ``` You may need to modify the PATH environment variable to access the flutterfire CLI. On Windows, the binary is stored in : %USERPROFILE%\AppData\Local\Pub\Cache\bin ### Creating and configuring the Firebase project 1- Go to the Firebase console Click on "Get started with a Firebase project" ``` https://console.firebase.google.com/ ```

2- Give a name to the project Use the same name you used during the backend installation. As an example, let's us "opendataqna"

3- Google Analytics You can choose to activate Google Analytics or not. This is not required for the app to work. For the sake of completness, let's use it as it is proposed by default.

Just click on the "Continue" button.

Then click on the "Create project" button. Once done, you have access to your newly Firebase project

Alternatively, you can also use the "flutterfire configure" CLI to create the Firebase project. 4- List the project using the Firebase CLI Now go back to your terminal and list this new Firebase project : ``` firebase projects:list ```

### Creating the Firestore Database The frontend requires a Firestore database (which is a Firebase service) to work. The free tier only allows the creation of 1 database that has the default name (derived from the project name) 1-On the Firebase console, click on the Firestore menu

2- Select the location

3- Select the production mode

4- Modify the security rules The front end needs to read and write on different collections in order to store the configuration, the know good SQL and the imported questions. For that, you need to change the security rules like below (please take appropriate measures to protect access to the database):

### Enabling Sign-In method The app requires that you authenticate using your Google account, whether it is personal or professional. 1- Go to the Firebase Authentication menu For that, go to the Firebase console and click on the Authentication menu and then on the Sing-in method tab:

2- Choose Google as the Provider Click on the Google icon to enable federated identity via Google:

3- Update the project level settings with the info below

### Installing the frontend Now that the Firebase project is created, let's get the source code of the Flutter frontend. 1- Create a folder for your project Creat folder that will contain the source code and the Firebase configuration. Let's call it opendatadna as well: ``` mkdir OpenDataQnA ``` 2- clone the source code from the repository Go to the opendataqna folder and clone the source code of the frontend app using the command below: ``` git clone https://github.com/GoogleCloudPlatform/Open_Data_QnA.git cd frontend/frontend-flutter ``` 3- Registering the frontend app to Firebase In order to register the app to Firebase and selected the Firebase project it will leave in, use the flutterfire configure command: ``` flutterfire configure ``` You'll be guided by the command via a couple of questions :

You can check on the Firebase console that app has been registered to the opendataqna project by clicking on "Project Overview":

4- Deploy the app to Firebase Hosting Firebase Hosting is a Firebase service allowing to host a web app. If not logged in yet to Firebase, do so with the firebase ``` firebase login ``` You also need to initialize the Firebase project. That will update the firebase.json file create by flutterfire configuration by adding the service you want to use (hosting). For that make sure you're at the root of the project : ``` cd OpenData/frontend/frontend-flutter ``` then add the command below to help Firebase hosting better understand and integrate with Flutter in order to optimize the build and deployment: ``` firebase experiments:enable webframeworks ``` Then type the command below: ``` firebase init hosting ```

Answer the questions asked, and a firebase.json file will be generated at the root of the project that looks like the one below:

We're now ready to deploy the app on Firebase Hosting. For that, still at the root level of the project (frontend-flutter folder) type the command below to deploy the app to Firebase Hosting service: ``` firebase deploy ```

Once the app has been built and deployed, it is available via the Hosting URL that shows up at the bottom of the output of the forebase deploy commad and has teh ofrm: https://.web.app ### Using the frontend 1- Setup the config_frontend config file The app requires some configuration to work, like the URI of the endpoint created during the backend installation and other information. Create a json file and name it config_frontend.json (the name does not matter) and copy paste the json object below: ``` { "endpoint_opendataqnq": "https://opendataqna-ol22ywferse-uc.v.run.app", "firestore_database_id": "opendataqna-session-logs", "firestore_history_collection": "session_logs", "firestore_cfg_collection": "front_end_flutter_cfg", "expert_mode": true, "anonymized_data": false, "firebase_app_name": "opendataqna", "imported_questions": "imported_questions" } ``` Change the values based on your setup. - endpoint_opendataqnq : contains the URI of the backend created (String) - firestore_database_id : name of the database you created earlier. If it is the default database, use "default" (String) - firestore_history_collection : name of the collection used to store all the known-good sql, questions, answers, user_id and timestamp (String) - firestore_cfg_collection : name of this file (String) - expert_mode : if true, activates the expert mode (boolean) - anonymized_data : if true, activates data anonymization - firebase_app_name : name of the app as registered to the Firebase project as it shows up on the Project overview on the Firebase console (String) - imported_questions : name of the collection used to store imported questions 2- Access the app Access the app using the https://.web.app link generated during the deployment of the app. Accept the terms and conditions and agree.

3- Authenticate yourself using your Google account

4- Landing page You end up on the landing page below.

- The stepper shows the status and the processing time of the request. It only shows up when the user is in Expert mode (more on that below) - The app is keeping track of the questions and answers (and SQL request) taht were successful in the Firestore database. When the app is launched, At most, the 4 last questions asked are displayed. Ckicking on any of them fills out automatically the input text field - the humberger menu is collapsed by default so that the app can provide more real estate for the canvas. 5- Menu

- New chat : Allows to reset the context so that the next query is not answered based on the previous answers - Import : This allows the user to import questions - Imported questions: import a csv file containing questions that are asked often. Ot only shows up in Expert mode. There are 2 falvors of this cvs file that are supported : - 3 columns that have to be : user_grouping,scenario,question An example of such a file is provided in frontend-flutter/script/Open Data QnA - Working Sheet V2 - sample_questions_UI copy.csv) ``` user_grouping,scenario,question MovieExplorer-bigquery,Genres,What are the top 5 most common movie genres in the dataset? MovieExplorer-bigquery,Genres,How many are musicals? MovieExplorer-bigquery,Genres,Romance? MovieExplorer-bigquery,Movie,What is the average user rating of the God Father movie? MovieExplorer-bigquery,Movie,Which year was it released? MovieExplorer-bigquery,Movie,How long is it? MovieExplorer-bigquery,Movie,Who is the lead actor? MovieExplorer-bigquery,Movie,Director MovieExplorer-bigquery,Movie,Cast WorldCensus-cloudsql-pg,Life Expectancy,What is the life expectancy for men and women in a United States in 2022? WorldCensus-cloudsql-pg,Life Expectancy,In India? WorldCensus-cloudsql-pg,Life Expectancy,Which country has highest male life expectancy? WorldCensus-cloudsql-pg,Life Expectancy,Female life expectancy? WorldCensus-cloudsql-pg,Population Density,What are the top 5 coutries with highest population density in 2024? WorldCensus-cloudsql-pg,Population Density,What are the birth and death rates in these counties? WorldCensus-cloudsql-pg,Sex Ratio at Birth,What is the sex ratio at birth in China in 2023? WorldCensus-cloudsql-pg,Sex Ratio at Birth,Which country has the highest? ``` - - 4 columns that have to be : user_grouping,scenario,question,main_question This format allows to add an indent to the follow-up question for the sake of clarity. An example of such a file is provided in frontend-flutter/script/Open_Data_QnA_sample_questions_v3.csv) ``` user_grouping,scenario,question,main_question MovieExplorer-bigquery,Genres,What are the top 5 most common movie genres in the dataset?,Y MovieExplorer-bigquery,Genres,How many are musicals?,N MovieExplorer-bigquery,Genres,Romance?,N MovieExplorer-bigquery,Movie,What is the average user rating of the God Father movie?,Y MovieExplorer-bigquery,Movie,Which year was it released?,N MovieExplorer-bigquery,Movie,How long is it?,N MovieExplorer-bigquery,Movie,Who is the lead actor?,N MovieExplorer-bigquery,Movie,Director,N MovieExplorer-bigquery,Movie,Cast,N MovieExplorer-bigquery,Movie,Who is the actor playing the role of the godfather in the Godfather movie?,Y MovieExplorer-bigquery,Movie,and the one playing the role of Sony?,N MovieExplorer-bigquery,Movie,How many people saw the Godfather?,y WorldCensus-cloudsql-pg,Life Expectancy,What is the life expectancy for men and women in a United States in 2022?,Y WorldCensus-cloudsql-pg,Life Expectancy,In India?,N WorldCensus-cloudsql-pg,Life Expectancy,Which country has highest male life expectancy?,N WorldCensus-cloudsql-pg,Life Expectancy,Female life expectancy?,N WorldCensus-cloudsql-pg,Population Density,What are the top 5 coutries with highest population density in 2024?,Y WorldCensus-cloudsql-pg,Population Density,What are the birth and death rates in these counties?,N WorldCensus-cloudsql-pg,Sex Ratio at Birth,What is the sex ratio at birth in China in 2023?,Y WorldCensus-cloudsql-pg,Sex Ratio at Birth,Which country has the highest?,N WorldCensus-cloudsql-pg,Sex Ratio at Birth,What is the world average?,N WorldCensus-cloudsql-pg,Sex Ratio at Birth,What country has the lowest male ration?,Y WorldCensus-cloudsql-pg,Sex Ratio at Birth,Since when?,N ``` - user_grouping : name of the dataset or database - scenario : used to tag your questions that are related to the same topic - question: question in natural language - main_question : when the value is Y, this is a fully understable and contextual question (let's call it main question). When the value is N, it is a follow-up question of the main question and does not have a full context - Once loaded, the questions show up like this (4 colums format)

- History : Gives the list of questions types during the session - Most popular questions : Lists all the historical questions sorted by the number they have been typed - Settings : Allows to - upload the config_frontend.json file which is stored on Firestore so that fater the app is relaunched the configuration is kept - anonymize data (in case the user wants to do a demo but not show real data) - set the expert mode - All the other options are not implemented

6- Suggestions When ever a question is clicked, whether from the 4 last question cards, the imported questions, the history or most popular questions, 3 suggestions are showing up at the bottom of the app. These suggestions are actually coming from the known good sql stored on the backend.

Clicking on one of these suggestions also triggers 3 other suggestions. 7- Auto-completion The app is proposing autocompletion each time a character is typed. The questions suggested for the autocompletion are coming from the known good sql stored on the backed.

8- Expert mode As said before, when enabled, the Expert mode allows to display the imported questions and the stepper. Clicking on a step, once completed, display details of this step :

================================================ FILE: frontend/frontend-flutter/analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: frontend/frontend-flutter/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks ================================================ FILE: frontend/frontend-flutter/android/app/build.gradle ================================================ plugins { id "com.android.application" // START: FlutterFire Configuration id 'com.google.gms.google-services' // END: FlutterFire Configuration id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } android { namespace "com.pilotcap.ttmd" compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.pilotcap.ttmd" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } } flutter { source '../..' } dependencies {} ================================================ FILE: frontend/frontend-flutter/android/app/google-services.json ================================================ { "project_info": { "project_number": "7413174684", "project_id": "dic-caa-ra1", "storage_bucket": "dic-caa-ra1.appspot.com" }, "client": [ { "client_info": { "mobilesdk_app_id": "1:7413174684:android:f4ed287d5da149fce5f859", "android_client_info": { "package_name": "com.pilotcap.ttmd" } }, "oauth_client": [], "api_key": [ { "current_key": "AIzaSyCytznMTfW7u99AuEQRbdiqmfaCAoMh0Bc" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [] } } } ], "configuration_version": "1" } ================================================ FILE: frontend/frontend-flutter/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: frontend/frontend-flutter/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: frontend/frontend-flutter/android/app/src/main/kotlin/com/pilotcap/ttmd/MainActivity.kt ================================================ package com.pilotcap.ttmd import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { } ================================================ FILE: frontend/frontend-flutter/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: frontend/frontend-flutter/android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: frontend/frontend-flutter/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: frontend/frontend-flutter/android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: frontend/frontend-flutter/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: frontend/frontend-flutter/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: frontend/frontend-flutter/android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip ================================================ FILE: frontend/frontend-flutter/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true ================================================ FILE: frontend/frontend-flutter/android/nl2sql_oss_android.iml ================================================ ================================================ FILE: frontend/frontend-flutter/android/settings.gradle ================================================ pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath } settings.ext.flutterSdkPath = flutterSdkPath() includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } plugins { id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false // START: FlutterFire Configuration id "com.google.gms.google-services" version "4.3.15" apply false // END: FlutterFire Configuration } include ":app" ================================================ FILE: frontend/frontend-flutter/build/web/.last_build_id ================================================ 5389598c2a36d064db55a8edf57e320f ================================================ FILE: frontend/frontend-flutter/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: frontend/frontend-flutter/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 12.0 ================================================ FILE: frontend/frontend-flutter/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: frontend/frontend-flutter/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: frontend/frontend-flutter/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: frontend/frontend-flutter/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: frontend/frontend-flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: frontend/frontend-flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: frontend/frontend-flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: frontend/frontend-flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: frontend/frontend-flutter/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: frontend/frontend-flutter/ios/Runner/GoogleService-Info.plist ================================================ API_KEY AIzaSyB5RZFwdDakT3zrJ7-DMum0IHCSfyLqtBQ GCM_SENDER_ID 7413174684 PLIST_VERSION 1 BUNDLE_ID com.pilotcap.ttmd PROJECT_ID dic-caa-ra1 STORAGE_BUCKET dic-caa-ra1.appspot.com IS_ADS_ENABLED IS_ANALYTICS_ENABLED IS_APPINVITE_ENABLED IS_GCM_ENABLED IS_SIGNIN_ENABLED GOOGLE_APP_ID 1:7413174684:ios:9a2317f461e3a05ce5f859 ================================================ FILE: frontend/frontend-flutter/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Ttmd CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ttmd CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ FILE: frontend/frontend-flutter/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 104157D68C004C47E46A0CC5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA7A5FA34E36E0E31106D75D /* Pods_RunnerTests.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B9A15A94E93BB2ECB5A0330B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 232A579B11BD1BA63E216F6D /* Pods_Runner.framework */; }; CD1D1C48D4206E8BEA39AABB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = BA68F78AEC387609C8F55641 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0BC3F20A14D692495160648B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 232A579B11BD1BA63E216F6D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 7378EF21874F9A1DFF00F4F0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7D9D3E084497F437970DDDAA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B1C6DDD94FCC20EC357B6FDE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; BA68F78AEC387609C8F55641 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; D6C5A2A3DB181063A7E47D6B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; DF41D8FEA220BF2CA2DE0E0D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; FA7A5FA34E36E0E31106D75D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B9A15A94E93BB2ECB5A0330B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; C8A669C122AD2A677A6E05F1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 104157D68C004C47E46A0CC5 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 4609EEA3E5E4780CEBAC8AD1 /* Frameworks */ = { isa = PBXGroup; children = ( 232A579B11BD1BA63E216F6D /* Pods_Runner.framework */, FA7A5FA34E36E0E31106D75D /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, DEFF93311464728E4415F42A /* Pods */, 4609EEA3E5E4780CEBAC8AD1 /* Frameworks */, BA68F78AEC387609C8F55641 /* GoogleService-Info.plist */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; DEFF93311464728E4415F42A /* Pods */ = { isa = PBXGroup; children = ( 7D9D3E084497F437970DDDAA /* Pods-Runner.debug.xcconfig */, 7378EF21874F9A1DFF00F4F0 /* Pods-Runner.release.xcconfig */, 0BC3F20A14D692495160648B /* Pods-Runner.profile.xcconfig */, DF41D8FEA220BF2CA2DE0E0D /* Pods-RunnerTests.debug.xcconfig */, B1C6DDD94FCC20EC357B6FDE /* Pods-RunnerTests.release.xcconfig */, D6C5A2A3DB181063A7E47D6B /* Pods-RunnerTests.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( A0EA34C1A77C0EA52B0275F1 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, C8A669C122AD2A677A6E05F1 /* Frameworks */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( E45AEE757B505F5B8C631A7D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 93B12CA83BE59495BC4E20EC /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, CD1D1C48D4206E8BEA39AABB /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 93B12CA83BE59495BC4E20EC /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; A0EA34C1A77C0EA52B0275F1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; E45AEE757B505F5B8C631A7D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = RRL2F46W5P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.pilotcap.ttmd; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = DF41D8FEA220BF2CA2DE0E0D /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pilotcap.ttmd.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = B1C6DDD94FCC20EC357B6FDE /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pilotcap.ttmd.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = D6C5A2A3DB181063A7E47D6B /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.pilotcap.ttmd.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = RRL2F46W5P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.pilotcap.ttmd; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = RRL2F46W5P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.pilotcap.ttmd; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: frontend/frontend-flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: frontend/frontend-flutter/ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: frontend/frontend-flutter/lib/firebase_options.dart ================================================ // File generated by FlutterFire CLI. // ignore_for_file: type=lint import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; /// Default [FirebaseOptions] for use with your Firebase apps. /// /// Example: /// ```dart /// import 'firebase_options.dart'; /// // ... /// await Firebase.initializeApp /// options: DefaultFirebaseOptions.currentPlatform, /// ); /// ``` class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) { return web; } switch (defaultTargetPlatform) { case TargetPlatform.android: return android; case TargetPlatform.iOS: return ios; case TargetPlatform.macOS: throw UnsupportedError( 'DefaultFirebaseOptions have not been configured for macos - ' 'you can reconfigure this by running the FlutterFire CLI again.', ); case TargetPlatform.windows: throw UnsupportedError( 'DefaultFirebaseOptions have not been configured for windows - ' 'you can reconfigure this by running the FlutterFire CLI again.', ); case TargetPlatform.linux: throw UnsupportedError( 'DefaultFirebaseOptions have not been configured for linux - ' 'you can reconfigure this by running the FlutterFire CLI again.', ); default: throw UnsupportedError( 'DefaultFirebaseOptions are not supported for this platform.', ); } } static const FirebaseOptions web = FirebaseOptions( apiKey: 'AIzaSyDQJNMaGcmhjuqsyXDpsXtLj4YSvMr8jkI', appId: '1:603067936438:web:2d6b41a3c67c155651c017', messagingSenderId: '603067936438', projectId: 'odqna-ai-seminar', authDomain: 'odqna-ai-seminar.firebaseapp.com', storageBucket: 'odqna-ai-seminar.appspot.com', ); static const FirebaseOptions android = FirebaseOptions( apiKey: 'AIzaSyCytznMTfW7u99AuEQRbdiqmfaCAoMh0Bc', appId: '1:7413174684:android:f4ed287d5da149fce5f859', messagingSenderId: '7413174684', projectId: 'dic-caa-ra1', storageBucket: 'dic-caa-ra1.appspot.com', ); static const FirebaseOptions ios = FirebaseOptions( apiKey: 'AIzaSyB5RZFwdDakT3zrJ7-DMum0IHCSfyLqtBQ', appId: '1:7413174684:ios:9a2317f461e3a05ce5f859', messagingSenderId: '7413174684', projectId: 'dic-caa-ra1', storageBucket: 'dic-caa-ra1.appspot.com', iosBundleId: 'com.pilotcap.ttmd', ); } ================================================ FILE: frontend/frontend-flutter/lib/main.dart ================================================ import 'dart:ui' as ui; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:simple_gradient_text/simple_gradient_text.dart'; import 'package:ttmd/screens/bot.dart'; import 'package:ttmd/screens/disclaimer.dart'; import 'package:ttmd/screens/settings.dart' as ts; import 'package:ttmd/services/display_stepper/display_stepper_cubit.dart'; import 'package:ttmd/services/display_stepper/display_stepper_state.dart'; import 'package:ttmd/services/first_question/first_question_cubit.dart'; import 'package:ttmd/services/first_question/first_question_state.dart'; import 'package:ttmd/services/load_question/load_question_cubit.dart'; import 'dart:async'; import 'dart:convert'; import 'package:easy_sidemenu/easy_sidemenu.dart'; import 'package:expandable_tree_menu/expandable_tree_menu.dart'; import 'package:ttmd/services/load_question/load_question_state.dart'; import 'package:intl/intl.dart'; import 'package:badges/badges.dart' as badges; import 'package:ttmd/services/new_suggestions/new_suggestion_cubit.dart'; import 'package:ttmd/services/new_suggestions/new_suggestion_state.dart'; import 'package:ttmd/services/update_expert_mode/update_expert_mode_cubit.dart'; import 'package:ttmd/services/update_popular_questions/update_popular_questions_cubit.dart'; import 'package:ttmd/services/update_popular_questions/update_popular_questions_state.dart'; import 'package:ttmd/services/update_stepper/update_stepper_cubit.dart'; import 'package:ttmd/services/update_stepper/update_stepper_state.dart'; import 'package:ttmd/utils/TextToDocParameter.dart'; import 'package:ttmd/utils/most_popular_questions.dart'; import 'package:ttmd/utils/pdf_viewer.dart'; import 'package:ttmd/utils/stepper_expert_info.dart'; import 'package:expansion_tile_card/expansion_tile_card.dart'; import 'package:flutter/services.dart'; import 'package:flutter_json_viewer/flutter_json_viewer.dart'; import 'dart:html' as html; import 'package:firebase_core/firebase_core.dart'; import 'firebase_options.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:csv/csv.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/rendering.dart'; late final FirebaseApp app; late final FirebaseAuth auth; late User currentUser; late String LastName; late String userID; late FirebaseFirestore db; final GlobalKey navigatorKey = GlobalKey(); Map myRoutes = { '/landingPage': (context) => ContentTtmd(title: 'Open data QnA'), //'/pdfViewer': (context) => PdfViewer(), '/settings': (context) => ts.Settings(db), }; void main() async { WidgetsFlutterBinding.ensureInitialized(); /*app = await Firebase.initializeApp( name: 'opendataqna', options: DefaultFirebaseOptions.web, // currentPlatform, );*/ app = await Firebase.initializeApp(options: DefaultFirebaseOptions.web); auth = FirebaseAuth.instanceFor(app: app); /*db = await FirebaseFirestore.instanceFor( app: app, databaseId: 'opendataqna-session-logs');*/ db = await FirebaseFirestore.instanceFor(app: app); print('Main: main() : auth = $auth'); print('Main: main() : db = $db'); print('Main: main() : db.databaseId = ${db.databaseId}'); //FirebaseAuth.instance auth.authStateChanges().listen((User? user) { if (user != null) { print("Main : user.uid = ${user.uid}"); currentUser = user; } }); runApp(ttmd()); } class ttmd extends StatelessWidget { ttmd({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (context) => LoadQuestionCubit(), ), BlocProvider( create: (context) => FirstQuestionCubit(), ), BlocProvider( create: (context) => UpdatePopularQuestionsCubit(), ), BlocProvider( create: (context) => UpdateStepperCubit(), ), BlocProvider( create: (context) => NewSuggestionCubit(), ), BlocProvider( create: (context) => DisplayStepperCubit(), ), BlocProvider( create: (context) => UpdateExpertModeCubit(), ), ], child: MaterialApp( debugShowCheckedModeBanner: false, //navigatorKey: navigatorKey, title: 'Open data QnA', theme: ThemeData( appBarTheme: AppBarTheme( backgroundColor: Colors.white, ), checkboxTheme: CheckboxThemeData( fillColor: WidgetStateProperty.resolveWith((states) { if (!states.contains(WidgetState.selected)) { return Colors.white; } return Colors.green; }), checkColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return Colors.red; } return null; }))), onGenerateRoute: (settings) { print('Main: ttmd : build() : onGenerateRoute : START'); if (settings.name == '/landingPage' && !TextToDocParameter.isAuthenticated) { print( 'Main: ttmd : build() : onGenerateRoute : attempting to access /landingPage without authentication'); return MaterialPageRoute( //builder: (context) => LoginScreen(), builder: (context) => Disclaimer(auth), ); } else if (settings.name == '/pdfViewer' && !TextToDocParameter.isAuthenticated) { print( 'Main: ttmd : build() : onGenerateRoute : attempting to access /pdfViewer without authentication'); return MaterialPageRoute( //builder: (context) => LoginScreen(), builder: (context) => Disclaimer(auth), ); } else if (settings.name == '/settings' && !TextToDocParameter.isAuthenticated) { print( 'Main: ttmd : build() : onGenerateRoute : attempting to access /settings without authentication'); return MaterialPageRoute( //builder: (context) => LoginScreen(), builder: (context) => Disclaimer(auth), ); } else { print( 'Main: ttmd : build() : onGenerateRoute : attempting to access ${settings.name!} with proper authentication'); if (settings.name == '/pdfViewer') { final args = settings.arguments as Map; return MaterialPageRoute( builder: (context) { return PdfViewer(bytes: args["bytes"]); }, ); } else { return MaterialPageRoute( builder: myRoutes[settings.name!]!, ); } } // Let the default routing handle other routes }, initialRoute: '/', home: Disclaimer(auth), ), ); } } class ContentTtmd extends StatefulWidget { const ContentTtmd({super.key, required this.title}); final String title; @override State createState() => _ContentTtmdState(); } class _ContentTtmdState extends State { late GlobalKey _botKey; int _selectedIndex = 0; int _selectedIndexNavRail = 0; PageController pageController = PageController(); SideMenuController sideMenu = SideMenuController(); SideMenuExpansionItem? historySideMenuExpansionItem; List? childrenHistorySideMenuItem; Map? mostPopularQuestionsMap = {}; int currentStep = 0; List stepperExpertInfoList = []; bool isFirstQuestionStatus = true; List> nodes = []; final List _destinations = [ 'New Chat', 'History', 'Most popular questions', 'Summary Extract', //'Help', 'Settings' ]; bool _isExpanded = false; bool _isQuestionExpanded = false; Size? screenSize; bool _isFirstQuestionNotAskedYet = true; TextEditingController textEditingController = TextEditingController(); Bot? bot; double overallProcessingTime = 0; bool useExpertMode = false; bool isTextToDoC = false; bool isTextToDoC1 = false; bool light = true; Container? suggestionContainer; List> importedQuestionTreeNodeList = [ TreeNode("No questions available") ]; InAppWebViewController? webViewController; //late FirebaseFirestore db; List userGroupingList = []; String selectedValue = ""; final ValueNotifier updateSelection = ValueNotifier(false); final ValueNotifier importedQuestionNotifier = ValueNotifier(false); final ValueNotifier selectedValueNotifier = ValueNotifier(null); //final TextEditingController _dropdownController = TextEditingController(); List> CreateNodesMostPopularQuestion( List mostPopularQuestionsList) { List> nodes = []; for (int i = 0; i < mostPopularQuestionsList.length; i++) { if (mostPopularQuestionsList[i].question.length != 0) nodes.add(TreeNode(mostPopularQuestionsList[i].time + "|||" + mostPopularQuestionsList[i].question + "|||" + mostPopularQuestionsList[i].count.toString())); } return nodes; } void _addData(String data, bool isHistory) { List? mostPopularQuestionsList = []; int lenghtTmp = 0; String timeString = ""; timeString = displayDateTime(); if (data.length == 0) return; print("Main: _addData : BEFORE ADD : nodes.length = ${nodes.length}"); print("Main: _addData : BEFORE ADD : data = ${data}"); print("Main: _addData : BEFORE ADD : nodes = ${nodes}"); if (isHistory) nodes.add(TreeNode(timeString + "|||" + data)); //displayDateTime() else nodes.add(TreeNode(data)); //add questions to mostPopularQuestionsMap if (mostPopularQuestionsMap!.containsKey(data)) { print('Main: _addData : mostPopularQuestionsMap contains $data'); print( 'Main: _addData : mostPopularQuestionsMap : BEFORE INCREMENT : (mostPopularQuestionsMap![data] as MostPopularQ).count = ${(mostPopularQuestionsMap![data] as MostPopularQ).count}'); mostPopularQuestionsMap![data]!.count = mostPopularQuestionsMap![data]!.count + 1; mostPopularQuestionsMap![data]!.time = timeString; print( 'Main: _addData : mostPopularQuestionsMap : AFTER INCREMENT : (mostPopularQuestionsMap![data] as MostPopularQ).count = ${(mostPopularQuestionsMap![data] as MostPopularQ).count}'); } else { print('Main: _addData : mostPopularQuestionsMap does not contain $data'); mostPopularQuestionsMap![data] = MostPopularQ(data, 1, timeString); } var sortedByValueMap = Map.fromEntries( mostPopularQuestionsMap!.entries.toList() ..sort((e1, e2) => e2.value.count.compareTo(e1.value.count))); print( 'Main: _addData : sortedByValueMap.length = ${sortedByValueMap.length}'); print('Main: _addData : sortedByValueMap = $sortedByValueMap'); int countEntries = 0; for (var entry in sortedByValueMap.entries) { mostPopularQuestionsList!.add(entry.value); if (countEntries == 2 || countEntries == sortedByValueMap.length - 1) break; countEntries++; } //countEntries = 0; print( "Main: _addData : BEFORE FILLING : mostPopularQuestionsList.length = ${mostPopularQuestionsList.length}"); print( "Main: _addData : BEFORE FILLING : mostPopularQuestionsList = ${mostPopularQuestionsList}"); lenghtTmp = mostPopularQuestionsList.length; //Fill mostPopularQuestionsList with dummy entries if there are not 3 most popular questions for (int i = 0; i <= (2 - lenghtTmp); i++) { mostPopularQuestionsList!.add(MostPopularQ("", 0, "")); } print( "Main: _addData : AFTER FILLING : mostPopularQuestionsList.length = ${mostPopularQuestionsList.length}"); print( "Main: _addData : AFTER FILLING : mostPopularQuestionsList = ${mostPopularQuestionsList}"); //Update the 3 most popular questions BlocProvider.of(context) .updateMostPopularQuestions( mostPopularQuestionsList: mostPopularQuestionsList!, time: timeString); print("Main: _addData : AFTER : nodes.length = ${nodes.length}"); print("Main: _addData : AFTER : nodes = ${nodes}"); } void _nodeSelected(context, nodeValue) { int scenarioNumber = 0; String questionTmp = ""; print('Main : _nodeSelected() : START'); print( 'Main : _nodeSelected() : nodeValue.toString() = ${nodeValue.toString()}'); questionTmp = nodeValue.toString().split(":")[0]; print('Main : _nodeSelected() : questionTmp = $questionTmp'); if (questionTmp.length <= 2) { //we don't expect to have more than 99 questions, so 2 digits are enough scenarioNumber = int.parse(questionTmp); print( 'Main : _nodeSelected() : questionTmp.length <=2 : scenarioNumber = $scenarioNumber'); } if (nodeValue.toString().contains(":")) { print('Main : _nodeSelected() : nodeValue.toString().contains(":")'); //scenario#:question:genre:user_grouping in the future, may add a 5th element : main_question print( 'Main : _nodeSelected() : nodeValue.toString().contains(":") : nodeValue.toString().split(":")[2] = ${nodeValue.toString().split(":")[2]}'); textEditingController.text = nodeValue.toString().split(":")[1]; BlocProvider.of(context).generateNewSuggestions( int.parse(nodeValue.toString().split(":")[0]), textEditingController.text, isACannedQuestion: false, userGrouping: nodeValue.toString().split(":")[3]); print( "Main: _nodeSelected() : textEditingController.text = = ${textEditingController.text}"); //setting on the UI the current user_grouping based on the question clicked assuming next question is for the same user_grouping selectedValueNotifier.value = nodeValue.toString().split(":")[3]; TextToDocParameter.currentUserGrouping = nodeValue.toString().split(":")[3]; TextToDocParameter.currentScenarioName = nodeValue.toString().split(":")[2]; } else return; //textEditingController.text = nodeValue.toString(); } void _nodeSelected1(context, nodeValue) { var val = nodeValue.toString().split("|||"); textEditingController.text = val[1]; /*final route = MaterialPageRoute(builder: (context) => DetailPage(value: nodeValue)); Navigator.of(context).push(route);*/ } /// Build the Node widget at a specific node in the tree Widget _nodeBuilder(context, nodeValue) { return Card( margin: EdgeInsets.symmetric(vertical: 1), child: Padding( padding: const EdgeInsets.all(8.0), child: Text(nodeValue.toString()), )); } Widget _nodeBuilder2(context, nodeValue) { String? badgeCount; String scenarioTitle = ""; String question = ""; List tmpList = []; print("Main : _nodeBuilder2() : START"); print("Main : _nodeBuilder2() : nodeValue = $nodeValue"); if (nodeValue.toString().contains(":")) { //question#: question tmpList = nodeValue.toString().split(":"); scenarioTitle = tmpList[2]; //"Scenario " + tmpList[0]; question = tmpList[1]; } else if (nodeValue.toString().contains(" - ")) { //Scenario x - user_grouping -#of questions tmpList = nodeValue.toString().split(" - "); badgeCount = tmpList[2]; question = tmpList[0] + ' - ' + tmpList[1]; } bool isScenario = nodeValue.toString().contains("Scenario"); print("Main : _nodeBuilder2() : scenarioTitle = $scenarioTitle"); print("Main : _nodeBuilder2() : question = $question"); return Card( margin: EdgeInsets.symmetric(vertical: 1), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Padding( padding: const EdgeInsets.only(right: 9.0), child: !isScenario ? CircleAvatar( backgroundColor: Colors.green, radius: 12, child: Text("Q${TextToDocParameter.questionCount++}", style: TextStyle(color: Colors.white, fontSize: 9))) : badges.Badge( badgeContent: Text(badgeCount!), child: Icon(Icons.account_tree_sharp), ), ), title: !isScenario ? Text(scenarioTitle, style: TextStyle(fontSize: 9, color: Colors.indigoAccent)) : null, subtitle: !isScenario ? Text(question) : Text(" " + question, style: TextStyle(fontWeight: FontWeight.bold)), ), ], )); } Widget _nodeBuilder1(context, nodeValue) { var val = nodeValue.toString().split("|||"); return Card( margin: EdgeInsets.symmetric(vertical: 1), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Padding( padding: const EdgeInsets.only(right: 10.0), child: CircleAvatar( backgroundColor: Colors.green, radius: 12, child: Icon(Icons.question_mark, size: 10, color: Colors.white)), ), title: Text(val[0], style: TextStyle(fontSize: 8, color: Colors.indigoAccent)), subtitle: Text(val[1]), ), ], )); } Widget _nodeBuilder3(context, nodeValue) { var val = nodeValue.toString().split("|||"); //val[0] => timestamp //val[1] => question //val[2] => Count return Card( margin: EdgeInsets.symmetric(vertical: 1), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: Padding( padding: const EdgeInsets.only(right: 20.0), child: /*2, child: Icon(Icons.question_mark, size: 10, color: Colors.white))*/ badges.Badge( badgeContent: Text(val[2]), child: Icon(Icons.question_mark), ), ), title: Text(val[0], style: TextStyle(fontSize: 8, color: Colors.indigoAccent)), subtitle: Text(val[1]), ), ], )); } FutureBuilder>?> _createSideMenu() { print('Main : _createSideMenu() : START'); TextToDocParameter.questionCount = 1; return FutureBuilder( future: loadQuestionsFromFirestore(), builder: (context, snapshot) { return SideMenu( controller: sideMenu, style: SideMenuStyle( // showTooltip: false, displayMode: SideMenuDisplayMode.open, showHamburger: true, hoverColor: Colors.blue[100], selectedHoverColor: Colors.blue[100], selectedColor: Colors.lightBlue, selectedTitleTextStyle: const TextStyle(color: Colors.black), selectedIconColor: Colors.white, ), title: Column( children: [ ConstrainedBox( constraints: const BoxConstraints( maxHeight: 100, maxWidth: 250, ), child: Image.asset( 'assets/images/ttmd_logo1.png', //'assets/images/drawer_header1.png', ), ), const Divider( indent: 8.0, endIndent: 8.0, ), ], ), footer: Padding( padding: const EdgeInsets.only(top: 8.0), child: ConstrainedBox( constraints: const BoxConstraints( maxHeight: 80, ), child: Container( color: const Color.fromARGB( 255, 240, 240, 240), // Colors.grey.withOpacity(1),//// child: Column( children: [ const Divider( indent: 8.0, endIndent: 8.0, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.max, children: [ CircleAvatar( backgroundImage: //const AssetImage('assets/images/john_smith.jpeg'), NetworkImage(TextToDocParameter.picture), radius: 30, ), Text("${TextToDocParameter.email}", style: TextStyle( color: Colors.blueAccent, fontSize: 12.0), textAlign: TextAlign.end), ], ), ], ), ), ), ), items: [ SideMenuItem( title: 'Open data QnA', onTap: (index, _) { //sideMenu.changePage(index); }, icon: const Icon(Icons.home), tooltipContent: "Open data QnA", ), SideMenuItem( title: 'New chat', onTap: (index, _) { TextToDocParameter.sessionId = ""; print( "Main: _createSideMenu() : New chat : : TextToDocParameter.sessionId = ${TextToDocParameter.sessionId}"); }, icon: const Icon(Icons.restart_alt_outlined), ), if (snapshot.hasData) //if (TextToDocParameter.expert_mode) SideMenuExpansionItem( title: "Imported questions", icon: const Icon(Icons.manage_history_outlined), children: [ SideMenuItem( builder: (context, displayMode) { return ExpandableTree( nodes: importedQuestionTreeNodeList, nodeBuilder: _nodeBuilder2, onSelect: (node) => _nodeSelected(context, node), ); }, onTap: (index, _) { //sideMenu.changePage(index); }, ) ], ), SideMenuExpansionItem( title: "History", icon: const Icon(Icons.manage_history_outlined), children: [ SideMenuItem( builder: (context, displayMode) { return BlocBuilder( builder: (context, state) { print( "Main: _createSideMenu() : BlocBuilder : state = $state"); print( "Main: _createSideMenu() : BlocBuilder : state.question = ${state.question}"); print( "Main: _createSideMenu() : BlocBuilder : state.time = ${state.time}"); _addData(state.question!, true); return ExpandableTree( nodes: nodes, nodeBuilder: _nodeBuilder1, onSelect: (node) => _nodeSelected1(context, node), ); }, ); }, onTap: (index, _) { //sideMenu.changePage(index); }, ) ], ), SideMenuExpansionItem( title: "Most popular questions", icon: const Icon(Icons.manage_history_outlined), children: [ SideMenuItem( builder: (context, displayMode) { return BlocBuilder( builder: (context, state) { print( "Main: _createSideMenu() : Most popular questions : BlocBuilder : state.status = ${state.status}"); print( "Main: _createSideMenu() : Most popular questions : BlocBuilder : state.mostPopularQuestionsList = ${state.mostPopularQuestionsList}"); //_addData(state.question!, true); List> nodesMostPopularQuestions = CreateNodesMostPopularQuestion( state.mostPopularQuestionsList!); return ExpandableTree( nodes: nodesMostPopularQuestions, nodeBuilder: _nodeBuilder3, onSelect: (node) => _nodeSelected1(context, node), ); }, ); }, onTap: (index, _) { //sideMenu.changePage(index); }, ) ], ), SideMenuItem( title: 'Import', onTap: (index, _) async { var questionList = await importQuestions(); if (questionList.length == 0) { print( "Main: _createSideMenu() : SideMenuItem : Import : onTap() : questionList is empty"); return; } //sort the list of list based on the name of the database : //grouping, scenario, question //Remove the header questionList.removeAt(0); questionList.sort((a, b) => a.first.compareTo(b.first)); //Save questions into SaveImportedQuestionsToFirestore(questionList); for (var entry in questionList) print( "Main: _createSideMenu() : SideMenuItem : Import : onTap() : sorted questionList = ${entry[0]}, ${entry[2]}"); setState(() { importedQuestionTreeNodeList = createQuestionList(questionList); //print("Main: _createSideMenu() : SideMenuItem : Import : setState() : importedQuestionTreeNodeList = ${importedQuestionTreeNodeList}"); }); const snackBar = SnackBar( content: Text('Importing questions.'), duration: Duration(seconds: 3), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); }, icon: const Icon(Icons.file_upload_outlined), ), /* //Commented out because the export of the answers (images and tables) to a pdf is not implemented yet SideMenuItem( title: 'Export', onTap: (index, _) { _createPDF(); const snackBar = SnackBar( content: Text('Exporting data to a pdf file.'), duration: Duration(seconds: 5), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); }, icon: const Icon(Icons.import_export_outlined), ),*/ /*SideMenuItem( // do not delete title: 'Clear Firestore history', onTap: (index, _) async { int count = 0; //get all the documents corresponding to the user id try { var querySnapshot = await db .collection("session_logs") .where("user_id", isEqualTo: TextToDocParameter.userID) .get(); print( "Main: _createSideMenu() : Clear Firestore history : querySnapshot.docs.length = ${querySnapshot.docs.length}"); print( "Main: _createSideMenu() : Clear Firestore history : querySnapshot = ${querySnapshot}"); //delete all these documents //await db.collection("session_logs").doc('FLoOKBwvVJ8mXkzu06He').delete(); for (var docSnapshot in querySnapshot.docs) { print( 'Main: _createSideMenu() : Clear Firestore history : ${docSnapshot.id} => ${docSnapshot.data()}'); print( 'Main: _createSideMenu() : Clear Firestore history : docSnapshot.reference ${docSnapshot.reference}'); db .collection("session_logs") .doc('${docSnapshot.id}') .delete(); count++; } } catch (e) { print( 'Main: _createSideMenu() : Clear Firestore history : EXCEPTION : $e'); } var snackBar = SnackBar( content: Text('Deleted $count questions from Firestore'), duration: Duration(seconds: 3), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); }, icon: const Icon(Icons.auto_delete_outlined), ),*/ SideMenuItem( title: 'Settings', onTap: (index, _) async { await Navigator.pushNamed(context, '/settings', arguments: {"dummy": ""}); print( "Main : createSideMenu() : TextToDocParameter.expert_mode = ${TextToDocParameter.expert_mode}"); setState(() { //useExpertMode = ts.Settings.isExpert; //useExpertMode = TextToDocParameter.expert_mode; TextToDocParameter.expert_mode; }); //GalleryScreen }, icon: const Icon(Icons.settings), ), ], ); }, )!; } @override void initState() { super.initState(); setup(); } Future setup() async { _botKey = GlobalKey(); bot = Bot(key: _botKey, textEditingController: textEditingController, db: db); //Initialize stepper states BlocProvider.of(context).updateStepperStatusUploaded( status: StepperStatus.initial, message: "Please enter a question.", stateStepper: StepState.disabled, isActiveStepper: false); print( "main: setup() : After BlocProvider.of(context).updateStepperStatusUploaded() : stepper initialized"); } Future initializeFirestore() async { await loadCfgFromFirestore(); //await loadQuestionsFromFirestore(); } Future>?> loadQuestionsFromFirestore() async { print('Main: loadQuestionsFromFirestore() : START'); List>? questionList = []; print( 'Main: loadQuestionsFromFirestore() : TextToDocParameter.imported_questions = ${TextToDocParameter.imported_questions}'); print( 'Main: loadQuestionsFromFirestore() : TextToDocParameter.userID = ${TextToDocParameter.userID}'); try { var querySnapshot = await db! .collection("imported_questions") .where("user_id", isEqualTo: TextToDocParameter.userID) .orderBy('scenario', descending: false) .orderBy('order') .get(); for (var docSnapshot in querySnapshot.docs) { final data = docSnapshot.data() as Map; print( 'Main: loadQuestionsFromFirestore() : data["user_grouping"] = ${data["user_grouping"]} : data["scenario"] = ${data["scenario"]} : data["question"] = ${data["question"]}'); questionList.add([ data["user_grouping"], data["scenario"], data["question"], data["main_question"] ]); } print( 'Main: loadQuestionsFromFirestore() : questionList.length = ${questionList.length}'); print( 'Main: loadQuestionsFromFirestore() : questionList = ${questionList}'); //create the questionList from Firestore if (questionList.length == 0) questionList = null; else importedQuestionTreeNodeList = createQuestionList(questionList); /*BlocProvider.of(context) .updateExpertMode(TextToDocParameter.expert_mode);*/ } catch (e) { print('Main: loadQuestionsFromFirestore() : EXCEPTION : e = $e'); } finally { return questionList; } } Future loadCfgFromFirestore() async { /*db = await FirebaseFirestore.instanceFor( app: app, databaseId: 'opendataqna-session-logs');*/ print("main: loadCfgFromFirestore() : db = $db"); if (TextToDocParameter.userID.isEmpty) { print( "main: loadCfgFromFirestore() : TextToDocParameter.userID is empty = ${TextToDocParameter.userID}"); WidgetsBinding.instance.addPostFrameCallback((_) { noCfgStoredinFirestore(); }); return; } try { print( "main: loadCfgFromFirestore() : TextToDocParameter.userID = ${TextToDocParameter.userID}"); DocumentSnapshot doc = await db! .collection("front_end_flutter_cfg") .doc('${TextToDocParameter.userID}') .get(); if (doc != null) { final data = doc.data() as Map; TextToDocParameter.anonymized_data = data["anonymized_data"]; TextToDocParameter.expert_mode = data["expert_mode"]; TextToDocParameter.endpoint_opendataqnq = data["endpoint_opendataqnq"]; TextToDocParameter.firestore_database_id = data["firestore_database_id"]; TextToDocParameter.firebase_app_name = data["firebase_app_name"]; TextToDocParameter.firestore_history_collection = data["firestore_history_collection"]; TextToDocParameter.firestore_cfg_collection = data["firestore_cfg_collection"]; TextToDocParameter.imported_questions = data["imported_questions"]; print( "main: loadCfgFromFirestore() : TextToDocParameter.anonymized_data = ${TextToDocParameter.anonymized_data}"); print( "main: loadCfgFromFirestore() : TextToDocParameter.expert_mode = ${TextToDocParameter.expert_mode}"); print( "main: loadCfgFromFirestore() : TextToDocParameter.firestore_database_id = ${TextToDocParameter.firestore_database_id}"); print( "main: loadCfgFromFirestore() : TextToDocParameter.endpoint_opendataqnq = ${TextToDocParameter.endpoint_opendataqnq}"); print( "main: loadCfgFromFirestore() : TextToDocParameter.firebase_app_name = ${TextToDocParameter.firebase_app_name}"); print( "main: loadCfgFromFirestore() : TextToDocParameter.firestore_history_collection = ${TextToDocParameter.firestore_history_collection}"); print( "main: loadCfgFromFirestore() : TextToDocParameter.firestore_cfg_collection = ${TextToDocParameter.firestore_cfg_collection}"); print( "main: loadCfgFromFirestore() : TextToDocParameter.imported_questions = ${TextToDocParameter.imported_questions}"); BlocProvider.of(context) .displayStepper(TextToDocParameter.expert_mode); } else { print("main: loadCfgFromFirestore() : doc == null"); WidgetsBinding.instance.addPostFrameCallback((_) { noCfgStoredinFirestore(); }); } } catch (e) { print("main: loadCfgFromFirestore() : EXCEPTION ON FIRESTORE : e = $e"); WidgetsBinding.instance.addPostFrameCallback((_) { noCfgStoredinFirestore(); }); } } Future> _getUserGrouping() async { print('Main : _getUserGrouping() : START'); List resp = []; await initializeFirestore(); print('Main : _getUserGrouping() : bot = $bot'); Map? _headers = { "Content-Type": "application/json", //"Authorization": " Bearer ${client!.credentials.accessToken.toString()}", }; try { print( 'Main : _getUserGrouping() : url = ${TextToDocParameter.endpoint_opendataqnq}/available_databases'); var response = await html.HttpRequest.requestCrossOrigin( '${TextToDocParameter.endpoint_opendataqnq}/available_databases', method: "GET"); print('Main : _getUserGrouping() : response = ' + response.toString()); final jsonData = jsonDecode(response); if (jsonData != null) { print('Main : _getUserGrouping() : jsonData = $jsonData'); /* Expected response : { "Error": "", "KnownDB": "[{\"table_schema\":\"imdb-postgres\"},{\"table_schema\":\"retail-postgres\"}]", "ResponseCode": 200 }*/ var knownSqlMap = jsonDecode(jsonData['KnownDB']); print('Main : _getUserGrouping() : knownSqlMap = ${knownSqlMap}'); print( 'Main : _getUserGrouping() : knownSqlMap[0] = ${knownSqlMap[0].toString()}'); for (int i = 0; i < knownSqlMap.length; i++) { for (var entry in knownSqlMap[i].entries) { print('${entry.key} : ${entry.value}'); if (entry.key == "table_schema") resp.add(entry.value); } } } else { resp.add(""); } } catch (e) { print('Main : _getUserGrouping() : EXCEPTION = $e'); throw Exception('Failed to get earning calls question suggestions: $e'); } finally { print('Main : _getUserGrouping() : resp = $resp'); return resp; } } Future>> _getQuestions() async { var questionList = await _getLastQuestions(); print("Main : _getQuestions() : questionList = $questionList"); return questionList; } @override Widget build(BuildContext context) { debugPaintSizeEnabled = false; screenSize = MediaQuery.of(context).size; bool _isEarningCalls1Hovered = false; bool _isEarningCalls2Hovered = false; bool _isBookingAnalysisHovered = false; bool _isFunelAnalysisHovered = false; print("Main : build() : START"); print("Main : build() : TuserGroupingList = ${userGroupingList}"); print( "Main : build() : TextToDocParameter.currentUserGrouping = ${TextToDocParameter.currentUserGrouping}"); return Scaffold( appBar: AppBar( title: Image.asset('assets/images/cymbal_logo.png', scale: 2), centerTitle: false, actions: [ Padding( padding: const EdgeInsets.only(right: 100), child: Container( width: 320, child: FutureBuilder( future: _getUserGrouping(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Padding( padding: const EdgeInsets.only(right: 135.0, left: 135.0), child: Container( width: 50, height: 50, child: /*Text('TEST')*/ CircularProgressIndicator()), ); // Loading indicator } else if (snapshot.hasError) { return Text('-Error-: ${snapshot.error}'); // Error handling } else { if (snapshot.data!.isNotEmpty) { TextToDocParameter.currentUserGrouping = snapshot.data![0]; TextToDocParameter.userGroupingList = snapshot.data!; selectedValueNotifier.value = snapshot.data!.first; print( "main: build() : TextToDocParameter.currentUserGrouping = ${TextToDocParameter.currentUserGrouping}"); print("main: build() : selectedValue = ${selectedValue}"); } return ValueListenableBuilder( valueListenable: selectedValueNotifier, builder: (context, value, child) { return Container( width: 300, child: Row( children: [ Text("User Grouping: ", style: TextStyle( fontSize: 13.0, color: Colors.black)), Expanded( child: DropdownButton( value: value, //icon: const Icon(Icons.arrow_downward), elevation: 16, style: const TextStyle( fontSize: 13.0, color: Colors.deepPurple), onChanged: (String? value) { selectedValueNotifier.value = value!; TextToDocParameter.currentUserGrouping = value!; print( 'Main: build() : DropdownButton : onChanged() : TextToDocParameter.currentUserGrouping => ${TextToDocParameter.currentUserGrouping}'); }, items: snapshot.data! .map>( (String value) { return DropdownMenuItem( value: value, child: Text(value), ); }).toList(), ), ), ], ), ); }); } }, ), ), ), ], leading: Text(""), ), body: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ _createSideMenu()!, const VerticalDivider( width: 0, ), Expanded( child: Stack( children: [ Container( padding: EdgeInsets.only(left: 100, right: 100), child: Column( mainAxisAlignment: MainAxisAlignment.start, //mainAxisSize: MainAxisSize.min, children: [ BlocBuilder( builder: (context, state) { print( "Main: build() : BlocBuilder : START"); if (state.status == displayStepperStatus.display_stepper) { print( "Main: build() : BlocBuilder : state =${state.status}"); return Flexible( fit: FlexFit.loose, child: Padding( padding: const EdgeInsets.only(bottom: 5), child: _buildStepper(), ), flex: 2); } else if (state.status == displayStepperStatus.remove_stepper) { print( "Main: build() : BlocBuilder : state =${state.status}"); return Text(""); } else { print( "Main: build() : BlocBuilder : ERROR : state =$state"); return Text("Unable to load user_grouping"); } }, ), bot!, BlocBuilder( builder: (context, state) { if (state.status == NewSuggestionStateStatus.loaded) { print( "Main: build() : BlocBuilder : START : state =$state"); return makeSuggestions( state.suggestionList!, state.scenarioNumber!); } else { return makeSuggestions(["x:", "x:", "x:"], 0); } }, ), //const Spacer(flex: 1,), ], ), ), BlocBuilder( builder: (context, state) { print( "Main : build() : BlocBuilder : START"); if (state.status == firstQuestionStatus.display_welcome_message) { print( "Main : build() : BlocBuilder : state.status == firstQuestionStatus.display_welcome_message"); return Positioned( //top: 0, //left: 200, //200, child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Container(height: 100), Image.asset( //UNCOMMENT "assets/images/gemini.png"!, height: 90, width: 90, fit: BoxFit.cover, ), Padding( padding: const EdgeInsets.only(bottom: 50), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Hello ${TextToDocParameter.firstName} ! ", style: TextStyle( fontSize: 40.0, fontWeight: FontWeight.bold)), GradientText( 'How can I help you today ?', style: TextStyle( fontSize: 40.0, fontWeight: FontWeight.bold), colors: [ Colors.blue, Colors.red, Colors.teal, ], ), ], ), const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Learn more by selecting a card below\n', style: TextStyle( fontSize: 30.0, fontWeight: FontWeight.normal, color: Colors.black)) ]) ], ), ), FutureBuilder( future: _getQuestions(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); // Loading indicator } else if (snapshot.hasError) { return Text('*Error*: ${snapshot.error}'); // Error handling } else { return Container( width: screenSize!.width / 1.5, child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (snapshot.data!.length >= 1) createCardsSuggestion( title: snapshot.data![0] ['scenario_name'] ?? "Error - No title", text: snapshot.data![0] ['user_question'] ?? "Error - No question", scenarioNumber: 2, isHovered: _isEarningCalls1Hovered, imagePath: "assets/images/last_questions.png", timeStamp: snapshot.data![0] ['timestamp'] ?? "Error - No timestamp", userGrouping: snapshot.data![0] ['user_grouping']), if (snapshot.data!.length >= 2) createCardsSuggestion( title: snapshot.data![1] ['scenario_name'] ?? "Error - No title", text: snapshot.data![1] ['user_question'] ?? "Error - No question", scenarioNumber: 2, isHovered: _isEarningCalls1Hovered, imagePath: "assets/images/last_questions.png", timeStamp: snapshot.data![1] ['timestamp'] ?? "Error - No timestamp", userGrouping: snapshot.data![0] ['user_grouping']), ], ), Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (snapshot.data!.length >= 3) createCardsSuggestion( title: snapshot.data![2][ 'scenario_name'] ?? "Error - No title", text: snapshot.data![2][ 'user_question'] ?? "Error - No question", scenarioNumber: 2, isHovered: _isEarningCalls1Hovered, imagePath: "assets/images/last_questions.png", timeStamp: snapshot .data![2] ['timestamp'] ?? "Error - No timestamp", userGrouping: snapshot.data![0] ['user_grouping']), if (snapshot.data!.length >= 4) createCardsSuggestion( title: snapshot.data![3][ 'scenario_name'] ?? "Error - No title", text: snapshot.data![3][ 'user_question'] ?? "Error - No question", scenarioNumber: 2, isHovered: _isEarningCalls1Hovered, imagePath: "assets/images/last_questions.png", timeStamp: snapshot .data![3] ['timestamp'] ?? "Error - No timestamp", userGrouping: snapshot.data![0] ['user_grouping']), ]) ], ), ); } }), //UNCOMMENT ], ), //UNCOMMENT ); } else { print( "Main : build() : BlocBuilder : state.status != firstQuestionStatus.display_welcome_message"); isFirstQuestionStatus = false; return Text(""); } }, ), ], ), ), ], ), // This trailing comma makes auto-formatting nicer for build methods. ); } void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } Future> _createPDF() async { //Method not called because the export menu has been commented out. //This will remain commented out until it is possible to screenshot web views List bytes = []; return bytes; } String displayDateTime() { String? dateTimeS; final now = DateTime.now(); dateTimeS = DateFormat('yyyy-MM-dd HH:mm:ss').format(now); return dateTimeS!; } Widget createCards( {required String? imagePath, required String? title, required String subTitle, required String text, required int badgeCount}) { return Expanded( child: Container( width: screenSize!.width / 3.5, //height: screenSize!.height / 6, child: Card( elevation: 10, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), // Define how the card's content should be clipped clipBehavior: Clip.antiAliasWithSaveLayer, // Define the child widget of the card child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Add padding around the row widget Padding( padding: const EdgeInsets.all(10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Add an image widget to display an image Image.asset( imagePath!, height: 70, width: 70, fit: BoxFit.cover, ), // Add some spacing between the image and the text Container(width: 20), // Add an expanded widget to take up the remaining horizontal space Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Add some spacing between the top of the card and the title Container(height: 5), // Add a title widget Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( title!, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF37474F)), ), badges.Badge( position: badges.BadgePosition.topEnd( top: -10, end: -12), showBadge: true, ignorePointer: false, onTap: () {}, badgeContent: Text( badgeCount < 10 ? " " + badgeCount.toString() : badgeCount.toString(), style: TextStyle( fontSize: 15, color: Colors .white)), //Icon(Icons.check, color: Colors.white, size: 14), badgeAnimation: badges.BadgeAnimation.rotation( animationDuration: Duration(seconds: 1), colorChangeAnimationDuration: Duration(seconds: 1), loopAnimation: false, curve: Curves.fastOutSlowIn, colorChangeAnimationCurve: Curves.easeInCubic, ), badgeStyle: badges.BadgeStyle( shape: badges.BadgeShape.instagram, badgeColor: Colors.red, //padding: EdgeInsets.all(5), borderRadius: BorderRadius.circular(4), borderSide: BorderSide(color: Colors.white, width: 2), elevation: 2, ), //child: Text('Badge'), ), ], ), // Add some spacing between the title and the subtitle Container(height: 5), // Add a subtitle widget Text( subTitle!, style: Theme.of(context) .textTheme .bodyMedium! .copyWith( color: Colors.grey[500], ), ), // Add some spacing between the subtitle and the text Container(height: 10), // Add a text widget to display some text Text( text.length <= 35 ? text! : text!.substring(0, 35) + ' ...', style: Theme.of(context) .textTheme .titleMedium! .copyWith( color: Colors.grey[700], ), maxLines: 1, ), Row(children: [ Spacer(), TextButton( style: TextButton.styleFrom( foregroundColor: Colors.transparent, ), child: const Text( "LOAD", style: TextStyle(color: Color(0xFFFF4081)), ), onPressed: () { print( 'Bot() : build() : TextButton : onPressed() : START : text = $text'); //BlocProvider.of(context).loadQuestionToChat("test"); //setState(() { textEditingController.text = text; _isFirstQuestionNotAskedYet = false; //}); }, ), ]) ], ), ), ], ), ), ], ), ), ), ); } Padding makeSuggestions( List listRandomQuestions, int scenarioNumber) { print("Main: makeSuggestions() : START"); TextToDocParameter.lastScenarioNumber = scenarioNumber; return Padding( padding: const EdgeInsets.only(bottom: 20), child: Container( width: screenSize!.width, //height: screenSize!.height / 20, color: Colors.white, child: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ if (listRandomQuestions.length >= 1 && listRandomQuestions[0] != "x:") Expanded( child: ConstrainedBox( constraints: BoxConstraints( minWidth: 100.0, // Set minimum width maxWidth: 400.0, // Set maximum width ), child: InkWell( onTap: () { //textEditingController.text = "Can you provide the Top 10 pace platinum accounts with booking value and YOY growth %?"; textEditingController.text = listRandomQuestions[0].split(":")[1]; _isFirstQuestionNotAskedYet = false; BlocProvider.of(context) .generateNewSuggestions( scenarioNumber, textEditingController.text, isACannedQuestion: false); print( "Main: makeSuggestions() : textEditingController.text = = ${textEditingController.text}"); }, child: Card( elevation: 5, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), child: Padding( padding: const EdgeInsets.all(5.0), child: Row( children: [ Image.asset( "assets/images/suggestions.png", height: 20, width: 20, fit: BoxFit.cover, ), Flexible( child: Text( //' Can you provide the Top 10 pace platinum accounts with booking value and YOY growth %? ', listRandomQuestions[0].split(":")[1], style: TextStyle( decoration: TextDecoration.underline, fontSize: 15, ), softWrap: true, overflow: TextOverflow.ellipsis, maxLines: 2), ), ], ), ), ), ), ), ), //Container(width: 10), if (listRandomQuestions.length >= 2 && listRandomQuestions[1] != "x:") Expanded( child: ConstrainedBox( constraints: BoxConstraints( minWidth: 100.0, // Set minimum width maxWidth: 400.0, // Set maximum width ), child: InkWell( onTap: () { textEditingController.text = listRandomQuestions[1].split(":")[1]; _isFirstQuestionNotAskedYet = false; BlocProvider.of(context) .generateNewSuggestions( scenarioNumber, textEditingController.text, isACannedQuestion: false); print( "Main: makeSuggestions() : textEditingController.text = = ${textEditingController.text}"); }, child: Card( elevation: 5, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), child: Padding( padding: const EdgeInsets.all(5.0), child: Row( children: [ Image.asset( "assets/images/suggestions.png", height: 20, width: 20, fit: BoxFit.cover, ), Flexible( child: Text( listRandomQuestions[1].split(":")[1], style: TextStyle( decoration: TextDecoration.underline, fontSize: 15, ), softWrap: true, overflow: TextOverflow.ellipsis, maxLines: 2), ), ], ), ), ), ), ), ), //Container(width: 10), if (listRandomQuestions.length >= 3 && listRandomQuestions[2] != "x:") Expanded( child: ConstrainedBox( constraints: BoxConstraints( minWidth: 100.0, // Set minimum width maxWidth: 400.0, // Set maximum width ), child: InkWell( onTap: () { textEditingController.text = listRandomQuestions[2].split(":")[1]; _isFirstQuestionNotAskedYet = false; BlocProvider.of(context) .generateNewSuggestions( scenarioNumber, textEditingController.text, isACannedQuestion: false); print( "Main: makeSuggestions() : textEditingController.text = = ${textEditingController.text}"); }, child: Card( elevation: 5, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), child: Padding( padding: const EdgeInsets.all(5.0), child: Row( children: [ Image.asset( "assets/images/suggestions.png", height: 20, width: 20, fit: BoxFit.cover, ), Flexible( child: Text( //' Can you add CM sold margin to above list? ', listRandomQuestions[2].split(":")[1], style: TextStyle( decoration: TextDecoration.underline, fontSize: 15, ), softWrap: true, overflow: TextOverflow.ellipsis, maxLines: 2), ), ], ), ), ), ), ), ), ])), ); } Widget createCardsSuggestion( {required String? title, required String text, required int scenarioNumber, required bool isHovered, required String imagePath, required String timeStamp, required String userGrouping}) { print('Main : createCardsSuggestion() : START'); print('Main : createCardsSuggestion() : START : text = $text'); print('Main : createCardsSuggestion() : START : title = $title'); print('Main : createCardsSuggestion() : START : userGrouping = $userGrouping'); return Expanded( child: InkWell( onTap: () { print( 'Main : createCardsSuggestion() : TextButton : onPressed() : START : text = $text'); textEditingController.text = text; _isFirstQuestionNotAskedYet = false; //Use the user_grouping associated to the question //selectedValueNotifier.value = title; selectedValueNotifier.value = userGrouping; //Setting the current user_grouping value TextToDocParameter.currentUserGrouping = userGrouping; BlocProvider.of(context).generateNewSuggestions( scenarioNumber, text, isACannedQuestion: false, userGrouping: userGrouping); }, onHover: (value) { }, child: Container( width: screenSize!.width / 2.5, height: screenSize!.height / 6, child: Card( color: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), // Define how the card's content should be clipped clipBehavior: Clip.antiAliasWithSaveLayer, // Define the child widget of the card child: Padding( padding: const EdgeInsets.all(10), child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.asset( imagePath!, height: 50, width: 50, fit: BoxFit.cover, ), Container(width: 20), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, //mainAxisSize: MainAxisSize.max, children: [ // Add some spacing between the top of the card and the title Container(height: 5), // Add a title widget Flexible( //flex: 1, child: Container( color: isHovered ? Colors.blueGrey : null, child: Text( title!, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF37474F)), ), ), ), // Add some spacing between the title and the subtitle Container(height: 5), // Add some spacing between the subtitle and the text // Add a text widget to display some text Flexible( //flex: 6, child: Container( color: isHovered ? Colors.blueGrey : null, child: Text( text, style: Theme.of(context) .textTheme .titleMedium! .copyWith( color: Colors.grey[700], ), maxLines: 6, softWrap: true, overflow: TextOverflow.visible, ), ), ), Container(height: 10), Flexible( //flex: 6, child: Container( padding: EdgeInsets.only(top: 5), color: isHovered ? Colors.blueGrey : null, child: Text( 'Question typed on the ' + timeStamp, style: Theme.of(context) .textTheme .titleSmall! .copyWith( color: Colors.blueAccent, ), maxLines: 6, softWrap: true, overflow: TextOverflow.visible, ), ), ), ], ), ), ], ), ), ), ), ), ); } Widget _createDebugInfoCard(Widget leading, String title, String subtitle, String imageTrailingPath, Widget info, String infoText) { final ButtonStyle flatButtonStyle = TextButton.styleFrom( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4.0)), ), ); return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: ExpansionTileCard( //key: cardA, initiallyExpanded: true, leading: leading, title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ Flexible( flex: 5, child: Text(title, style: TextStyle(fontWeight: FontWeight.bold)), ), Flexible( flex: 1, child: Image.asset(width: 75, height: 75, imageTrailingPath)) ], ), subtitle: Text(subtitle, style: TextStyle(fontSize: 12)), children: [ const Divider( thickness: 1.0, height: 1.0, ), Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), child: info, ), ), ButtonBar( alignment: MainAxisAlignment.spaceBetween, buttonHeight: 52.0, buttonMinWidth: 90.0, children: [ TextButton( style: flatButtonStyle, onPressed: () async { await Clipboard.setData(ClipboardData(text: infoText)); }, child: const Column( children: [ Icon(Icons.copy), Padding( padding: EdgeInsets.symmetric(vertical: 2.0), ), Text('Copy'), ], ), ), ], ), ], ), ); } Container _buildStepper() { final canCancel = currentStep > 0; final canContinue = currentStep < 3; StepState? stateStepperUploaded; bool? isActiveUploaded; StepState? stateStepperExtracted; bool? isActiveExtracted; StepState? stateStepperCompared; bool? isActiveCompared; StepState? stateStepperCommitted; bool? isActiveCommitted; StepState? stateStepperEnterQuestion; bool? isActiveEnterQuestion; String? messageStepperEnterQuestion = ""; StepState? stateStepperGenerateSQL; bool? isActiveGenerateSQL; String? messageStepperGenerateSQL = ""; StepState? stateStepperRunQuery; bool? isActiveRunQuery; String? messageStepperRunQuery = ""; StepState? stateStepperGetGraphDescription; bool? isActiveGetGraphDescription; String? messageStepperGraphDescription = ""; StepState? stateStepperGetTextSummary; bool? isActiveGetTextSummary; String? messageStepperGetTextSummary = ""; String? message = ""; void setStepsStates(UpdateStepperState state) { print('Main : _buildStepper() : setStepsStates() : Start'); print( 'Main : _buildStepper() : setStepsStates() : status = ${state.status}'); print('Main : _buildStepper() : currentStep : $currentStep'); switch (state.status) { case StepperStatus.initial: print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.initial'); currentStep = 0; stateStepperEnterQuestion = StepState.disabled; isActiveEnterQuestion = false; stateStepperGenerateSQL = StepState.disabled; isActiveGenerateSQL = false; stateStepperRunQuery = StepState.disabled; isActiveRunQuery = false; stateStepperGetGraphDescription = StepState.disabled; isActiveGetGraphDescription = false; stateStepperGetTextSummary = StepState.disabled; isActiveGetTextSummary = false; message = state.message; break; case StepperStatus.enter_question: stepperExpertInfoList.clear(); currentStep = 0; print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.enter_question'); stateStepperEnterQuestion = StepState.complete; isActiveEnterQuestion = true; stateStepperGenerateSQL = StepState.disabled; isActiveGenerateSQL = false; stateStepperRunQuery = StepState.disabled; isActiveRunQuery = false; stateStepperGetGraphDescription = StepState.disabled; isActiveGetGraphDescription = false; stateStepperGetTextSummary = StepState.disabled; isActiveGetTextSummary = false; messageStepperEnterQuestion = "Question entered in 0 s"; overallProcessingTime = 0; stepperExpertInfoList.add(StepperExpertInfo()); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.enter_question : stepperExpertInfoList.length = ${stepperExpertInfoList.length}'); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.enter_question : stepperExpertInfoList = ${stepperExpertInfoList}'); break; case StepperStatus.generate_sql: currentStep = 1; print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.generate_sql'); stateStepperEnterQuestion = StepState.complete; isActiveEnterQuestion = true; stateStepperGenerateSQL = StepState.complete; isActiveGenerateSQL = true; stateStepperRunQuery = StepState.disabled; isActiveRunQuery = false; stateStepperGetGraphDescription = StepState.disabled; isActiveGetGraphDescription = false; stateStepperGetTextSummary = StepState.disabled; isActiveGetTextSummary = false; messageStepperGenerateSQL = state.message! + " " + ((state.debugInfo.stepDuration!.toDouble()) / 1000).toString() + " s"; overallProcessingTime = overallProcessingTime + (state.debugInfo.stepDuration!.toDouble()) / 1000; stepperExpertInfoList.add(state.debugInfo); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.generate_sql : stepperExpertInfoList.length = ${stepperExpertInfoList.length}'); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.generate_sql : stepperExpertInfoList = ${stepperExpertInfoList}'); break; case StepperStatus.run_query: currentStep = 2; print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.run_query'); stateStepperEnterQuestion = StepState.complete; isActiveEnterQuestion = true; stateStepperGenerateSQL = StepState.complete; isActiveGenerateSQL = true; stateStepperRunQuery = StepState.complete; isActiveRunQuery = true; stateStepperGetGraphDescription = StepState.disabled; isActiveGetGraphDescription = false; stateStepperGetTextSummary = StepState.disabled; isActiveGetTextSummary = false; messageStepperRunQuery = state.message! + " " + ((state.debugInfo.stepDuration!.toDouble()) / 1000).toString() + " s"; overallProcessingTime = overallProcessingTime + (state.debugInfo.stepDuration!.toDouble()) / 1000; stepperExpertInfoList.add(state.debugInfo); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.run_query : stepperExpertInfoList.length = ${stepperExpertInfoList.length}'); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.run_query : stepperExpertInfoList = ${stepperExpertInfoList}'); break; case StepperStatus.get_graph_description: currentStep = 3; print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.get_graph_description'); stateStepperEnterQuestion = StepState.complete; isActiveEnterQuestion = true; stateStepperGenerateSQL = StepState.complete; isActiveGenerateSQL = true; stateStepperRunQuery = StepState.complete; isActiveRunQuery = true; stateStepperGetGraphDescription = StepState.complete; isActiveGetGraphDescription = true; stateStepperGetTextSummary = StepState.disabled; isActiveGetTextSummary = false; messageStepperGraphDescription = state.message! + " " + ((state.debugInfo.stepDuration!.toDouble()) / 1000).toString() + " s"; overallProcessingTime = overallProcessingTime + (state.debugInfo.stepDuration!.toDouble()) / 1000; stepperExpertInfoList.add(state.debugInfo); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.get_graph_description : stepperExpertInfoList.length = ${stepperExpertInfoList.length}'); print( 'Main : _buildStepper() : setStepsStates() : Switch: StepperStatus.get_graph_description : stepperExpertInfoList = ${stepperExpertInfoList}'); break; default: currentStep = 0; stateStepperUploaded = StepState.disabled; isActiveUploaded = false; stateStepperExtracted = StepState.disabled; isActiveExtracted = false; stateStepperCompared = StepState.disabled; isActiveCompared = false; stateStepperCommitted = StepState.disabled; isActiveCommitted = false; message = state.message; } } Future _dialogRequestGenerated( BuildContext context, StepperExpertInfo debugInfo, String title) { print( "Main: _dialogRequestGenerated() : title = $title : debugInfo.response = ${debugInfo.response}"); return showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(title), content: SingleChildScrollView( child: Container( width: screenSize!.width / 2, child: Column( children: [ _createDebugInfoCard( Icon(Icons.link), 'URI POST', 'URI used in this step.', 'assets/images/https.png', Text( debugInfo.uri!, style: Theme.of(context) .textTheme .bodyMedium! .copyWith(fontSize: 16), ), debugInfo.uri!), _createDebugInfoCard( //FaIcon(icon), Icon(Icons.https), 'HTTPS Body', 'Json body of the request', 'assets/images/json.png', Container( child: JsonViewer(jsonDecode(debugInfo.body!))), debugInfo.body!), _createDebugInfoCard( //FaIcon(icon), Icon(Icons.https), 'HTTPS Headers', 'Headers sent in the HTTPS request.', 'assets/images/https_header.png', Container( child: JsonViewer(jsonDecode(debugInfo.header!))), debugInfo.header!), _createDebugInfoCard( FaIcon(FontAwesomeIcons.reply), 'HTTPS Response', 'HTTPS body of the response', 'assets/images/json.png', (!debugInfo.response!.contains('syntax error') && !debugInfo.response!.contains('SyntaxError') ) ? Container( child: JsonViewer(jsonDecode(debugInfo.response!..replaceAll('"', '\\"')))) : Container(child: Text(debugInfo.response!)), debugInfo.response!), _createDebugInfoCard( FaIcon(FontAwesomeIcons.code), 'Status Code', 'HTTPS answer status code', 'assets/images/status_code.png', Text( debugInfo.statusCode!.toString(), style: Theme.of(context) .textTheme .bodyMedium! .copyWith(fontSize: 16), ), debugInfo.statusCode!.toString()), _createDebugInfoCard( Icon(Icons.timer), 'Processing Time', 'Step duration in seconds', 'assets/images/elapsed_time.png', Text( (debugInfo.stepDuration!.toDouble() / 1000) .toString() + " s", style: Theme.of(context) .textTheme .bodyMedium! .copyWith(fontSize: 16), ), (debugInfo.stepDuration!.toDouble() / 1000) .toString() + " s"), //Text("SQL request\n: ${debugInfo.generatedSQLText}"), ], )), ), //Text(DicInfoExtractedMap.toString(),), actions: [ TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.labelLarge, ), child: const Text('Ok'), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } return Container( child: BlocBuilder( builder: (context, state) { setStepsStates(state); return Stepper( controlsBuilder: (BuildContext context, ControlsDetails controls) { return Container(); //to remove the continue and cancel buttons }, type: StepperType.horizontal, currentStep: currentStep, onStepTapped: (int index) { String title = ""; print( 'HomePage() : build() : Stepper : onStepTapped() : index = $index'); switch (index) { case 0: title = ""; break; case 1: title = "SQL Request Generation"; break; case 2: title = "Data Retrieval"; break; case 3: title = "Graph Description Retrieval"; break; default: title = ""; break; } print( 'HomePage() : build() : END : Stepper : onStepTapped() : index = $index'); print( 'HomePage() : build() : END : Stepper : onStepTapped() : TextToDocParameter.isTextTodocGlobal = ${TextToDocParameter.isTextTodocGlobal}'); print( 'HomePage() : build() : END : Stepper : onStepTapped() : stepperExpertInfoList.length = ${stepperExpertInfoList.length}'); //temporary knob to dispaly info about TextToDoc request/answer if (TextToDocParameter.isTextTodocGlobal && index == 3 && stepperExpertInfoList.length == 2) { print( 'HomePage() : build() : END : Stepper : onStepTapped() : if(TextToDocParameter.isTextTodocGlobal && index == 4 && stepperExpertInfoList.length == 2 ) = $index'); stepperExpertInfoList.insert(1, StepperExpertInfo()); stepperExpertInfoList.insert(1, StepperExpertInfo()); } if (TextToDocParameter.isTextTodocGlobal && index < 3) { return; } //end of temporary knob _dialogRequestGenerated( context, stepperExpertInfoList[index], title); }, onStepContinue: () {}, onStepCancel: () {}, steps: [ Step( title: Text("Question Typed", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0)), subtitle: Text(messageStepperEnterQuestion!), state: stateStepperEnterQuestion!, //stateStepperUploaded!, isActive: isActiveEnterQuestion!, //isActiveUploaded!, content: LimitedBox( maxWidth: screenSize!.width, //100, maxHeight: 40, child: Container( color: Colors.black12, //CupertinoColors.systemGrey, child: Center( child: Padding( padding: const EdgeInsets.only(right: 8, left: 8), child: Text(message!, style: TextStyle( fontWeight: FontWeight.normal, fontSize: 18.0, color: stateStepperEnterQuestion! == StepState.complete ? Colors.green : Colors.red)), ))), ), ), Step( title: Text("SQL Request Generated", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0)), subtitle: Text(messageStepperGenerateSQL!), state: stateStepperGenerateSQL!, //stateStepperExtracted!, isActive: isActiveGenerateSQL!, //isActiveExtracted!, content: LimitedBox( maxWidth: screenSize!.width, //100, maxHeight: 40, child: Container( color: Colors.black12, child: Center( child: Padding( padding: const EdgeInsets.only(right: 8, left: 8), child: Text(message!, style: TextStyle( fontWeight: FontWeight.normal, fontSize: 18.0, color: Colors.green)), ), )), ), ), Step( title: Text("Data Retrieved", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0)), subtitle: Text(messageStepperRunQuery!), state: stateStepperRunQuery!, //StepState.disabled, //stateStepperCompared!, isActive: isActiveRunQuery!, //isActiveCompared!, content: LimitedBox( maxWidth: screenSize!.width, //100, maxHeight: 40, child: Container( color: Colors.black12, child: Center( child: Padding( padding: const EdgeInsets.only(right: 8, left: 8), child: Text(message!, style: TextStyle( fontWeight: FontWeight.normal, fontSize: 18.0, color: Colors.green)), ))), ), ), Step( title: Text("Graph & Table Generated", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18.0)), subtitle: Text(messageStepperGraphDescription!), state: stateStepperGetGraphDescription!, //StepState.disabled, //stateStepperCompared!, isActive: isActiveGetGraphDescription!, //isActiveCompared!, content: LimitedBox( maxWidth: screenSize!.width, //100, maxHeight: 40, child: Container( color: Colors.black12, child: Center( child: Padding( padding: const EdgeInsets.only(right: 8, left: 8), child: Text( "The overall time to generate the natural language answer is ${double.parse(overallProcessingTime.toStringAsFixed(2))} s.", style: TextStyle( fontWeight: FontWeight.normal, fontSize: 18.0, color: Colors.green)), ))), ), ), ], ); }, ), ); } Future loadImageAsset(String assetPath) async { final ByteData data = await rootBundle.load(assetPath); final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); final ui.FrameInfo frameInfo = await codec.getNextFrame(); return frameInfo.image; } Future> convertImageToListInt(String assetPath) async { ui.Image image = await loadImageAsset(assetPath); ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); return byteData!.buffer.asUint8List(); } Future _loadFile(String uri) async { final byteData = await rootBundle.load(uri); final buffer = byteData.buffer; Uint8List bytes = buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes); return bytes; } Future>> importQuestions() async { print('Main: ttmd : importQuestions() : START'); List>? rowsAsListOfValues; final filePickerResult = await FilePicker.platform.pickFiles( allowMultiple: false, allowedExtensions: ['csv'], type: FileType.custom, dialogTitle: "Import questions", ); if (filePickerResult != null) { print( 'Main: ttmd : importQuestions() : fileName = ${filePickerResult.files.single.name}'); print( 'Main: ttmd : importQuestions() : size = ${filePickerResult.files.single.size}'); //print('Main: ttmd : importQuestions() : path = ${filePickerResult.files.single.path}'); Uint8List fileBytes = filePickerResult.files.single.bytes!; String fileContent = utf8.decode(fileBytes); List lines = fileContent.split('\n'); print('Main: ttmd : importQuestions() : lines.length = ${lines.length}'); rowsAsListOfValues = const CsvToListConverter( fieldDelimiter: ',', textDelimiter: '"', textEndDelimiter: '"') .convert(fileContent); print( 'Main: ttmd : importQuestions() : rowsAsListOfValues = ${rowsAsListOfValues}'); if (rowsAsListOfValues!.length < 2 || rowsAsListOfValues[0].length > 4 || (rowsAsListOfValues[0].length <= 4 && (rowsAsListOfValues[0][0] != "user_grouping" || rowsAsListOfValues[0][1] != "scenario" || rowsAsListOfValues[0][2] != "question"))) { print( "Main: importQuestions() : WRONG FORMAT : rowsAsListOfValues[0] = ${rowsAsListOfValues[0]}"); checkImportedCSVFile(); return [[]]; } for (var entry in rowsAsListOfValues) { if (rowsAsListOfValues[1].length == 4) { print( "Main: importQuestions() : rowsAsListOfValues = ${entry[0]}, ${entry[2]}, ${entry[3]}"); if (rowsAsListOfValues[1].length != 4) checkImportedCSVFile(); } if (rowsAsListOfValues[1].length == 3) { print( "Main: importQuestions() : rowsAsListOfValues = ${entry[0]}, ${entry[2]}"); if (rowsAsListOfValues[1].length != 3) checkImportedCSVFile(); } } } return rowsAsListOfValues!; } void checkImportedCSVFile() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('Alert'), content: Text( "The imported CVS file does not have the right format or any entry.\n" + "Please follow the format below (CVS comma separated):\n" + "grouping,\tscenario,\tquestion\n" + "grouping1,\tscenario1,\tquestion1\n\n" + "For more info, please look at:\n" + "https://github.com/GoogleCloudPlatform/Open_Data_QnA/blob/main/scripts/known_good_sql.csv", softWrap: true, overflow: TextOverflow.visible), //SwitchExample(), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); // Close the dialog }, child: Text('OK'), ), ], ); }, ); } void noCfgStoredinFirestore() { showDialog( context: context, //navigatorKey.currentContext!, builder: (BuildContext context) { return AlertDialog( title: Row( // Use a Row to align the icon and title children: [ Icon(Icons.warning, color: Colors.red), SizedBox(width: 8), // Add some spacing Text('Alert'), ], ), content: SelectableText.rich( TextSpan( children: [ TextSpan( text: "You must first create and load a configuration file\n" + "using the Settings -> Upload frontend config file option.\n" + 'For that, copy and paste the content below in a file\n' + "that you'll name config_frontend.json:\n\n"), TextSpan( text: "{\n", style: TextStyle(color: Colors.black), ), TextSpan( text: '"endpoint_opendataqnq": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: '"",\n', style: TextStyle(color: Colors.green), ), TextSpan( text: '"firestore_database_id": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: '"opendataqna-session-logs",\n', style: TextStyle(color: Colors.green), ), TextSpan( text: '"firestore_history_collection": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: '"session_logs",\n', style: TextStyle(color: Colors.green), ), TextSpan( text: '"firestore_cfg_collection": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: '"front_end_flutter_cfg",\n', style: TextStyle(color: Colors.green), ), TextSpan( text: '"expert_mode": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: ',\n', style: TextStyle(color: Colors.red), ), TextSpan( text: '"anonymized_data": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: ',\n', style: TextStyle(color: Colors.red), ), TextSpan( text: '"firebase_app_name": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: '"opendataqna"\n', style: TextStyle(color: Colors.green), ), TextSpan( text: '"imported_questions": ', style: TextStyle(color: Colors.blueAccent), ), TextSpan( text: '"imported_questions"\n', style: TextStyle(color: Colors.green), ), TextSpan( text: '}', style: TextStyle(color: Colors.black), ), ], ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); // Close the dialog }, child: Text('OK'), ), ], ); }, ); } List> createQuestionList(List> questionList) { List> finalNodeList = []; List>? nodeList = []; List>? nodeEmbeddedList; String scenario_nameCurrent = ""; int count = 1; int countQuestions = 0; bool isNewScenario = true; String nodeTmp = ""; print("Main: createQuestionList() : START"); print( "Main: createQuestionList() : questionList.length = ${questionList.length}"); print( "Main: createQuestionList() : questionList[0].length = ${questionList[0].length}"); for (var entry in questionList) print( "Main: createQuestionList() : questionList = ${entry[1]}, ${entry[2]}"); scenario_nameCurrent = (questionList[0][1] as String).trim().toLowerCase(); nodeTmp = ""; for (int i = 0; i < questionList.length; i++) { print( "Main: createQuestionList() : START LOOP : i = $i : scenario_nameCurrent = $scenario_nameCurrent"); if (i < questionList.length - 1) { print( "Main: createQuestionList() : START LOOP : i = $i : $i < ${questionList.length - 1}"); //We're on the same scenario if (scenario_nameCurrent == (questionList[i][1] as String).trim().toLowerCase()) { print( "Main: createQuestionList() : LOOP : i = $i : scenario_nameCurrent = questionList[i][1] as String).trim().toLowerCase() = $scenario_nameCurrent"); //processQuestionRow(i); if (isNewScenario) { print( "Main: createQuestionList() : LOOP : i = $i : This is a new sceanrio : isNewScenario = $isNewScenario"); nodeList = []; nodeEmbeddedList = []; finalNodeList.add(TreeNode( 'Scenario $count - ${questionList[i][1]} - ${getQuestionCount(questionList, scenario_nameCurrent)}', subNodes: nodeList!)); isNewScenario = false; } nodeTmp = "$count:" + questionList[i][2] + ":" + questionList[i][1] + ":" + questionList[i][0]; print( "Main: createQuestionList() : LOOP : i = $i : scenario_nameCurrent = $scenario_nameCurrent : nodeTmp = $nodeTmp"); if ((questionList[i][3] as String).trim().toLowerCase() == "y" && (questionList[i + 1][3] as String).trim().toLowerCase() == "y" && (questionList[i + 1][1] as String).trim().toLowerCase() == scenario_nameCurrent) { print( "Main: createQuestionList() : LOOP : i = $i : scenario_nameCurrent = $scenario_nameCurrent : main_question$i = ${(questionList[i][3] as String).trim().toLowerCase()} : main_question${i + 1} = ${(questionList[i + 1][3] as String).trim().toLowerCase()}"); nodeList! .add(TreeNode(nodeTmp, subNodes: nodeEmbeddedList!)); } else if ((questionList[i][3] as String).trim().toLowerCase() == "y" && (questionList[i + 1][1] as String).trim().toLowerCase() != scenario_nameCurrent) { print( "Main: createQuestionList() : LOOP : i = $i : scenario_nameCurrent = $scenario_nameCurrent : main_question$i = ${(questionList[i][3] as String).trim().toLowerCase()} : main_question${i + 1} = ${(questionList[i + 1][3] as String).trim().toLowerCase()} : questionList[i+1][1] = ${questionList[i + 1][1]} != scenario_nameCurrent = $scenario_nameCurrent"); nodeList!.add(TreeNode(nodeTmp)); } else if ((questionList[i][3] as String).trim().toLowerCase() == "y" && (questionList[i + 1][3] as String).trim().toLowerCase() == "n") { //this is a new main question nodeEmbeddedList = []; print( "Main: createQuestionList() : LOOP : i = $i : scenario_nameCurrent = $scenario_nameCurrent : main_question$i = ${(questionList[i][3] as String).trim().toLowerCase()} != main_question${i + 1} = ${(questionList[i + 1][3] as String).trim().toLowerCase()}"); /*nodeEmbeddedList!.add(TreeNode("$count:" + questionList[i][2] + ":" + questionList[i][1] + ":" + questionList[i][0])); nodeList!.add(TreeNode(nodeTmp, subNodes: nodeEmbeddedList!));*/ nodeList! .add(TreeNode(nodeTmp, subNodes: nodeEmbeddedList!)); } else if ((questionList[i][3] as String).trim().toLowerCase() == "n" && (questionList[i + 1][3] as String).trim().toLowerCase() == "n") { //this is a follow-up question print( "Main: createQuestionList() : LOOP : i = $i : scenario_nameCurrent = $scenario_nameCurrent : main_question$i = ${(questionList[i][3] as String).trim().toLowerCase()} == main_question${i + 1} = ${(questionList[i + 1][3] as String).trim().toLowerCase()}"); nodeEmbeddedList!.add(TreeNode("$count:" + questionList[i][2] + ":" + questionList[i][1] + ":" + questionList[i][0])); } else if ((questionList[i][3] as String).trim().toLowerCase() == "n" && (questionList[i + 1][3] as String).trim().toLowerCase() == "y") { //this is a new main question print( "Main: createQuestionList() : LOOP : i = $i : scenario_nameCurrent = $scenario_nameCurrent : main_question$i = ${(questionList[i][3] as String).trim().toLowerCase()} == main_question${i + 1} = ${(questionList[i + 1][3] as String).trim().toLowerCase()}"); nodeEmbeddedList!.add(TreeNode("$count:" + questionList[i][2] + ":" + questionList[i][1] + ":" + questionList[i][0])); } } else { print( "Main: createQuestionList() : i = $i : $scenario_nameCurrent != ${questionList[i][1]} : countQuestions = $countQuestions"); isNewScenario = true; count++; //processQuestionRow(i); nodeList = []; nodeEmbeddedList = []; finalNodeList.add(TreeNode( 'Scenario $count - ${questionList[i][1]} - ${getQuestionCount(questionList, questionList[i][1])}', subNodes: nodeList!)); isNewScenario = false; nodeTmp = "$count:" + questionList[i][2] + ":" + questionList[i][1] + ":" + questionList[i][0]; print( "Main: createQuestionList() : i = $i : $scenario_nameCurrent != ${questionList[i][1]} : nodeTmp = $nodeTmp"); nodeList!.add(TreeNode(nodeTmp, subNodes: nodeEmbeddedList!)); scenario_nameCurrent = (questionList[i][1] as String).trim().toLowerCase(); print( "Main: createQuestionList() : i = $i : new scenario_nameCurrent = $scenario_nameCurrent "); } } //end of if i < questionList.length - 1 else if (i == questionList.length - 1) { print( "Main: createQuestionList() : LOOP : Last row : i = $i : i = ${questionList.length - 1}"); if ((questionList[i][3] as String).trim().toLowerCase() == "y") { print( "Main: createQuestionList() : LOOP : Last row : i = $i : scenario_nameCurrent = $scenario_nameCurrent : main_question$i = ${(questionList[i][3] as String).trim().toLowerCase()}"); //nodeList!.add(TreeNode(nodeTmp, subNodes: nodeEmbeddedList!)); nodeList!.add(TreeNode("$count:" + questionList[i][2] + ":" + questionList[i][1] + ":" + questionList[i][0])); } else { //this is a new main question //nodeEmbeddedList = []; print( "Main: createQuestionList() : LOOP : Last row : i = $i : scenario_nameCurrent = $scenario_nameCurrent : main_question$i = ${(questionList[i][3] as String).trim().toLowerCase()}"); nodeEmbeddedList!.add(TreeNode("$count:" + questionList[i][2] + ":" + questionList[i][1] + ":" + questionList[i][0])); } } } //end of for print("Main: createQuestionList() : END : count = $count"); return finalNodeList; } int getQuestionCount(List> questionList, String scenario) { int scenarioCount = 0; bool hit = false; print("Main: getQuestionCount() : START "); for (int i = 0; i < questionList.length; i++) { if (scenario.toLowerCase() == (questionList[i][1] as String).trim().toLowerCase()) { hit = true; scenarioCount++; } else { if (hit) break; } } print("Main: getQuestionCount() : scenarioCount = $scenarioCount"); return scenarioCount; } Future>> _getLastQuestions() async { List> resp = []; print("Main : _getLastQuestions() : START"); print( 'Main : _getLastQuestions() : TextToDocParameter.userID = ${TextToDocParameter.userID}'); print('Main : _getLastQuestions() : db = ${db}'); print('Main : _getLastQuestions() : db.app = ${db.app}'); print('Main : _getLastQuestions() : db.databaseId = ${db.databaseId}'); var querySnapshot = await db .collection("session_logs") .where("user_id", isEqualTo: TextToDocParameter.userID) .limit(4) .orderBy('timestamp', descending: true) .get(); print( "Main : _getLastQuestions() : querySnapshot.docs.length = ${querySnapshot.docs.length}"); print("Main : _getLastQuestions() : querySnapshot = ${querySnapshot}"); for (var docSnapshot in querySnapshot.docs) { print( 'Main : _getLastQuestions() : ${docSnapshot.id} => ${docSnapshot.data()}'); resp.add({ "user_question": "${docSnapshot.data()['user_question']}", "timestamp": "${DateTime.fromMillisecondsSinceEpoch(docSnapshot.data()['timestamp'].seconds * 1000)}", "user_grouping": "${docSnapshot.data().containsKey('user_grouping') ? docSnapshot.data()['user_grouping'] : 'no data'}", "scenario_name": "${docSnapshot.data().containsKey('scenario_name') ? docSnapshot.data()['scenario_name'] : 'no data'}" }); } return resp; } Future> _getLastQuestionsOld(String userGrouping) async { List respLLMQuestion = []; List respCannedQuestions = []; List resp = []; List tmpQuestions = []; String body = ""; Uri url; String question1 = ""; String question2 = ""; String question3 = ""; String question4 = ""; String timeString = ""; String originalQuestion = ""; print( 'NewSuggestionCubit : getLastQuestions() : generateNewSuggestions : START'); print( 'NewSuggestionCubit : getLastQuestions() : userGrouping = $userGrouping'); timeString = displayDateTime(); //Create the header Map? _headers = { "Content-Type": "application/json", //"Authorization": " Bearer ${client!.credentials.accessToken.toString()}", }; //Create the body body = '''{ "user_grouping": "$userGrouping" }'''; try { var response = await html.HttpRequest.requestCrossOrigin( '${TextToDocParameter.endpoint_opendataqnq}/get_known_sql', method: "POST", sendData: body); print('NewSuggestionCubit : getLastQuestions() : response = ' + response.toString()); final jsonData = jsonDecode(response); if (jsonData != null) { print('NewSuggestionCubit: getLastQuestions() : jsonData = $jsonData'); //KnownSQL = [{"example_user_question": "question1", "example_generated_sql": "sql1"}, // {"example_user_question": "question2", "example_generated_sql": "sql2"}, // ...] var knownSql = jsonData["KnownSQL"].replaceAll(RegExp(r'((\\n)|(\\r))'), ''); var knownSqlMap = jsonDecode(knownSql); print( 'NewSuggestionCubit: getLastQuestions() : knownSqlMap = ${knownSqlMap}'); print( 'NewSuggestionCubit: getLastQuestions() : knownSqlMap[0] = ${knownSqlMap[0].toString()}'); for (int i = 0; i < knownSqlMap.length; i++) { for (var entry in knownSqlMap[i].entries) { print('${entry.key} : ${entry.value}'); if (entry.key == "example_user_question") tmpQuestions.add(entry.value); } } print('Main: getLastQuestions() : tmpQuestions = ${tmpQuestions}'); question1 = tmpQuestions[0] ?? "No suggestion for question1"; question2 = tmpQuestions[1] ?? "No suggestion for question2"; question3 = tmpQuestions[2] ?? "No suggestion for question3"; question4 = tmpQuestions[3] ?? "No suggestion for question4"; //Adding scenarioNumber + ":" to have same format as for Business KPI questions question1 = "2:" + question1; question2 = "2:" + question2; question3 = "2:" + question3; question4 = "2:" + question4; print('Main: getLastQuestions() : question1 = ${question1}'); print('Main: getLastQuestions() : question2 = ${question2}'); print('Main: getLastQuestions() : question3 = ${question3}'); print('Main: getLastQuestions() : question4 = ${question4}'); } } catch (e) { print( 'Main: getLastQuestions() : Not a canned question : EXCEPTION = $e'); throw Exception('Failed to get earning calls question suggestions: $e'); } finally { resp.add(question1); resp.add(question2); resp.add(question3); resp.add(question4); print('Main: getLastQuestions() : resp = $resp'); return resp; } } void SaveImportedQuestionsToFirestore( List> questionList) async { int count = 0; print('Main: SaveImportedQuestionsToFirestore() : START'); print( 'Main: SaveImportedQuestionsToFirestore() : questionList = $questionList'); if (questionList.length > 0) { try { //Delete all former questions for that user var querySnapshot = await db! .collection("${TextToDocParameter.imported_questions}") .where("user_id", isEqualTo: TextToDocParameter.userID) .get(); for (var docSnapshot in querySnapshot.docs) { db.collection("imported_questions").doc('${docSnapshot.id}').delete(); } //create new questions to be stored on Firestore for (int i = 0; i < questionList.length; i++) { List list = questionList[i]; print('Main: SaveImportedQuestionsToFirestore() : List = $List'); Map questionMap = {}; questionMap['user_grouping'] = list[0]; questionMap['scenario'] = list[1]; questionMap['question'] = list[2]; questionMap['user_id'] = TextToDocParameter.userID; if (list.length == 4) questionMap['main_question'] = list[3]; else questionMap['main_question'] = "Y"; questionMap['order'] = count++; db .collection("imported_questions") .doc("question$i") .set(questionMap); } } catch (e) { print('Main: SaveImportedQuestionsToFirestore() : EXCEPTION : e = $e'); } } } } class Config { static bool isUseFeedback = false; static bool isUseColorMode = false; static bool isUseDashboards = false; static bool isUseReports = false; static bool isExpert = false; static bool isUseLog = false; static bool isAnonymizedMode = false; } ================================================ FILE: frontend/frontend-flutter/lib/screens/bot.dart ================================================ import 'dart:typed_data'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:intl/intl.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'dart:convert'; import 'dart:math'; import 'package:file_picker/file_picker.dart'; import 'package:bubble/bubble.dart'; import 'package:http/http.dart' as http; import 'package:deep_pick/deep_pick.dart'; import 'package:ttmd/utils/stepper_expert_info.dart'; import '../services/first_question/first_question_cubit.dart'; import '../services/load_question/load_question_cubit.dart'; import '../services/new_suggestions/new_suggestion_cubit.dart'; import '../services/update_stepper/update_stepper_cubit.dart'; import '../services/update_stepper/update_stepper_state.dart'; import '../utils/TextToDocParameter.dart'; import '../utils/custom_input_field.dart' as cif; import '../utils/custom_input_field.dart'; import 'dart:html' as html; import 'package:screenshot/screenshot.dart'; import '../utils/tabbed_container.dart'; import 'package:simple_http_api/simple_http_api.dart'; import "dart:js" as js; // For the testing purposes, you should probably use https://pub.dev/packages/uuid. String randomString() { final random = Random.secure(); final values = List.generate(16, (i) => random.nextInt(255)); return base64UrlEncode(values); } class Bot extends StatefulWidget { final TextEditingController? textEditingController; final FirebaseFirestore? db; const Bot({Key? key, this.textEditingController, this.db}) : super(key: key); @override State createState() => BotState(); } class BotState extends State with SingleTickerProviderStateMixin { final List _messages = []; final Map _graphsImagesMap = {}; final _user = types.User( id: '82091008-a484-4a89-ae75-a22bf8d6f3ac', firstName: '${TextToDocParameter.firstName}', lastName: '${TextToDocParameter.lastName}' ); final _userAvatar = types.User( id: '82091010-a484-4a89-ae75-a22bf8d6f3ab', firstName: '${TextToDocParameter.firstName}', lastName: '${TextToDocParameter.lastName}' ); final _user1 = const types.User( id: '82091009-a485-4a90-ae76-a22bf8d6f3ad', firstName: 'Open Data QnA', lastName: 'Assistant'); String textMLKit = ""; String textDocAi = ""; String responsePalMBody = ""; String streamingText = ""; String requestPalMBody = ""; String responseDLPMBody = ""; String requestDLPBody = ""; List sourceList = []; Map> mapSource = Map(); String colorBubble = "user"; Chat? chat; Size? screenSize; bool isFirstQuestion = true; bool isProcessing = false; ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController1 = ScreenshotController(); List> tableKeyList = []; Map tableKeyMap = {}; String? imageId; bool isGraphKeyAdded = false; bool isTableKeyAdded = false; Map mainQuestionsFollowUpQuestions = {}; late TabController _tabController; bool _isThumbsUpHovered = false; bool _isThumbsDownHovered = false; bool _isCopyHovered = false; static bool isTextToDoc = false; int _selectedIndex = 0; InAppWebViewController? webViewController; Map mapAnonymisationGraph = {}; Container? graphContainer; @override void initState() { setup(); super.initState(); } @override void dispose() { _tabController!.dispose(); webViewController!.dispose(); super.dispose(); } Future setup() async { _tabController = TabController(length: 2, vsync: this); } @override Widget build(BuildContext context) { screenSize = MediaQuery.of(context).size; chat = Chat( emptyState: Text(""), avatarBuilder: avatarBuilder, customBottomWidget: CustomInputField( onAttachmentPressed: _handleAttachmentPressed, onSendPressed: _handleSendPressed, db: widget.db, options: cif.InputOptions( textEditingController: widget.textEditingController)), customMessageBuilder: customMessageBuilder, //inputOptions: InputOptions(textEditingController: widget.textEditingController), messages: _messages, onSendPressed: _handleSendPressed, //onAttachmentPressed: _handleImageSelection, //onAttachmentPressed: _handleFileSelection onAttachmentPressed: _handleAttachmentPressed, onMessageTap: _handleMessageTap, onPreviewDataFetched: _handlePreviewDataFetched, showUserAvatars: true, showUserNames: true, bubbleBuilder: _bubbleBuilder, user: _user, messageWidthRatio: 0.9, theme: DefaultChatTheme( seenIcon: Text( 'read', style: TextStyle( fontSize: 10.0, ), ), backgroundColor: Color( 0xFFF0F2F6), //Color.fromRGBO(242, 242, 242, 1.0),//Colors.black12, messageMaxWidth: screenSize!.width, ), ); print( " bot: build() : TextToDocParameter.isTextTodocGlobal = ${TextToDocParameter.isTextTodocGlobal}"); return Flexible( fit: FlexFit.loose, flex: 9, child: Stack(children: [ Screenshot(child: chat!, controller: screenshotController1), isProcessing ? Positioned( left: 650, top: 300, child: SizedBox( width: 100, height: 100, child: CircularProgressIndicator( strokeWidth: 6, ), ), ) : dummyFunction(), ]), ); } Text dummyFunction() { print("Bot: dummyFunction() : START"); Future.delayed(const Duration(seconds: 2), () { generateSnapshot(); }); return Text(""); } Future generateSnapshot() async { print("Bot: generateSnapshot() : START"); //Does not work for now because the flutter_inappwebview is not supported by the creenshot package //it return a blank image. InAppWebViewController.takeScreenshot() is not implemented for the web platform. //So commenting out the code below : /*if (isGraphKeyAdded == true && !TextToDocParameter.isTextTodocGlobal) { print( "Bot: generateSnapshot() : isGraphKeyAdded = $isGraphKeyAdded"); print("Bot: generateSnapshot() : imageId = ${imageId}"); Uint8List img = await _generateImage(graphContainer!); _graphsImagesMap[imageId!] = img; isGraphKeyAdded = false; print("Bot: generateSnapshot() : isGraphKeyAdded = $isGraphKeyAdded"); print("Bot: generateSnapshot() : _graphsImagesMap.length = ${_graphsImagesMap.length}"); } */ if (tableKeyList.isNotEmpty && isTableKeyAdded == true && !TextToDocParameter.isTextTodocGlobal) { print( "Bot: generateSnapshot() : tableKeyList.length = ${tableKeyList.length}"); print( "Bot: generateSnapshot() : tableKeyList.isNotEmpty = ${tableKeyList.isNotEmpty} && isTableKeyAdded = $isTableKeyAdded"); print( "Bot: generateSnapshot() : tableKeyList.last = ${tableKeyList.last.toString()}"); /* Commenting out this code until syncfusion_flutter_pdfviewer package is replaced PaginatedDataTable pdfGrid = await tableKeyList.last.currentState!.widget; print("Bot: generateSnapshot() : pdfGrid = $pdfGrid"); tableKeyMap[tableKeyList.last.toString()] = pdfGrid!; print( "Bot: generateSnapshot() : tableKeyMap.length = ${tableKeyMap.length}"); print("Bot: generateSnapshot() : isTableKeyAdded = $isTableKeyAdded"); */ isTableKeyAdded = false; } //tableKeyMap } void _addMessage(types.Message message) { print('Bot : _addMessage() message.text = ' + message.toJson().toString()); setState(() { _messages.insert(0, message); }); print('Bot : _addMessage() : _messages.length = ' + _messages.length.toString()); print('Bot : _addMessage() : _messages = ' + _messages.toString()); } void _handleSendPressed(types.PartialText message) { final textMessage = types.TextMessage( author: _userAvatar, createdAt: DateTime.now().millisecondsSinceEpoch, id: randomString(), text: message.text, type: types.MessageType.text, metadata: {"dataSource": "user"}); bool isACannedQuestion = false; print('Bot : _handleSendPressed() textMessage.text = ' + textMessage.text); print('Bot : _handleSendPressed() textMessage.author.firstName = ' + textMessage.author.firstName!); _addMessage(textMessage); //Remove the welcome message after the first question asked if (isFirstQuestion) { print('Bot : _handleSendPressed() : isFirstQuestion = ' + isFirstQuestion.toString()); isFirstQuestion = false; BlocProvider.of(context).removeWelcomeMessage(); } print( "main: initState() : After BlocProvider.of(context).updateStepperStatusUploaded() : TextToDocParameter.lastScenarioNumber = ${TextToDocParameter.lastScenarioNumber}"); BlocProvider.of(context).generateNewSuggestions( TextToDocParameter.lastScenarioNumber, message.text, lastCannedQuestion: TextToDocParameter.lastCannedQuestion, isACannedQuestion: isACannedQuestion); //Update the question history on the side menu //BlocProvider.of(context).loadQuestionToChat(question: message.text, time: displayDateTime()); BlocProvider.of(context) .loadQuestionToChat(question: message.text, time: "rr"); //Update stepper state BlocProvider.of(context).updateStepperStatusUploaded( status: StepperStatus.enter_question, message: "Question entered.", stateStepper: StepState.complete, isActiveStepper: true); print( "main: initState() : After BlocProvider.of(context).updateStepperStatusUploaded() : stepper initialized"); _handleReceivedResponse(textMessage.text, "text"); setState(() { isProcessing = true; }); } void _handleReceivedResponse(String msg, String type) async { String mime = ""; String id = randomString(); bool isText = true; dynamic dataViz; GlobalKey tableKey = GlobalKey(); imageId = randomString(); PaginatedDataTable? tableGrid; //TextToDocParameter.isTextTodocGlobal = true; print('Bot : _handleReceivedResponse(): START '); print('Bot : _handleReceivedResponse(): isTextToDoc = ' + isTextToDoc.toString()); print( 'Bot : _handleReceivedResponse(): TextToDocParameter.isTextTodocGlobal = ${TextToDocParameter.isTextTodocGlobal}'); switch (type) { case "text": mime = "application/json"; break; case "pdf": mime = "application/pdf"; break; case "png": //case "image": mime = "image/png"; break; case "jpeg": //case "image": mime = "image/jpeg"; break; default: print('Error, out of range : index = $type '); } print('Bot : _handleReceivedResponse(): type = ' + type + ' : mime = ' + mime); print('Bot : _handleReceivedResponse(): msg = ' + msg); if (!TextToDocParameter.isTextTodocGlobal) { //NL2SQL request print('Bot : _handleReceivedResponse(): USING NL2SQL'); //Get generated reponse var rep = await getChatResponseNew(msg, mime, id, user: _user1); print( 'Bot : _handleReceivedResponse(): back from getChatResponseNew() : rep = $rep'); //graphConfig = rep![2] as GraphConfig; dataViz = rep![2]; if ((rep![0] as String).length == 0) { //knownDB rep[0] = '[{"response": "The request did not return meaningful information. It could be because the question has not been formulated properly or some context is missing."}]'; } else { //The request has been successful and an entry has been created on ${TextToDocParameter.firestore_database_id} //Adding the user_grouping and scenario_name to the entry because as of now it does not contain this data. //If in the future user_grouping is added, _updateUserGroupingInSessionLogs() below can be removed _updateUserGroupingInSessionLogs(); tableGrid = createPaginatedTable(rep[0] as String); tableKeyList.add(tableKey); isTableKeyAdded = true; print( 'Bot : _handleReceivedResponse(): tableKey = $tableKey : isTableKeyAdded = $isTableKeyAdded'); } isText = rep[1] as bool; print( 'Bot : _handleReceivedResponse(): repFinal = ' + (rep[0] as String)); print( 'Bot : _handleReceivedResponse(): isText = ' + (rep[1].toString())); print( 'Bot : _handleReceivedResponseNew() : CustomMessage : isText = ${isText}'); if (isText) { print( 'Bot : _handleReceivedResponseNew() : isText = $isText : dataViz = ${dataViz}'); imageId = "no_image"; } else { print( 'Bot : _handleReceivedResponseNew() : CustomMessage : imageId = $imageId'); } final customMessage = types.CustomMessage( author: _user1, createdAt: DateTime.now().millisecondsSinceEpoch, id: randomString(), type: types.MessageType.custom, metadata: { "graph": dataViz, "textSummary": rep[3] as String, "imageId": imageId, "dataSource": tableGrid, "tableKey": tableKey.toString(), }); _addMessage(customMessage); } else { print('Bot : _handleReceivedResponse(): USING TEXT2DOC'); var rep = await getChatResponseTextToDoCStream(msg, mime, id, user: _user1); imageId = "no_image"; final customMessage = types.CustomMessage( author: _user1, createdAt: DateTime.now().millisecondsSinceEpoch, id: randomString(), type: types.MessageType.custom, metadata: { "graph": dataViz, "textSummary": rep![3] as String, "imageId": imageId, "dataSource": null, "tableKey": tableKey.toString(), "stream": rep![4] as Stream> ?? null, "stopWatch": rep![5] as Stopwatch ?? null }); _addMessage(customMessage); } setState(() { isProcessing = false; }); } Future?> getChatResponseTextToDoCStream( String msg, String mime, String id, {types.User? user}) async { List? reqResp = []; List RespList = []; Uri url; String userQuestion = ""; String body = ""; print('Bot : getChatResponseTextToDoCStream() : START'); url = Uri.parse( 'https://colab-cloudrun-template-ra1-uz6w7mqrka-uc.a.run.app/generate_streaming'); print('Bot : getChatResponseTextToDoCStream() : url = ' + url.host + url.path); Map? headers = { "Content-Type": "$mime", }; print('Bot : getChatResponseTextToDoCStream() : headers = $headers'); userQuestion = msg; print( 'Bot : getChatResponseTextToDoCStream() : userQuestion = $userQuestion'); body = '''{ "query": "${userQuestion}" }'''; print('Bot : getChatResponseTextToDoCStream() : request_body = $body'); var eventSource = EventSource(url, ApiMethod.post); eventSource.setHeaders(headers); final cancelToken = TimingToken(Duration(seconds: 5)); final stopwatchtextToDoc = Stopwatch()..start(); //final stream = eventSource.send(body: body, cancelToken: cancelToken); var stream = eventSource.send(body: body).asBroadcastStream(); print('Bot : getChatResponseTextToDoCStream() : stream = $stream'); RespList.add("test"); // 0 : body of the answer RespList.add(true); //1 : is text RespList.add("dump"); // 2 : Graph config //RespList.add("Your request can not be answered right now. Please try again."); RespList.add("firstChunck"); // 3: text RespList.add(stream); //4: stream RespList.add(stopwatchtextToDoc); //5: stopwatchtextToDoc print('Bot : getChatResponseTextToDoCStream() : END'); return RespList; } Future waitForFirstSSEData( Stream> stream, EventSource eventSource) async { int count = 0; print('Bot : waitForFirstSSEData() : START'); stream.listen( (event) { if (eventSource.isWeb) { print('Bot : waitForFirstSSEData(): eventSource.isWeb'); print('Bot : waitForFirstSSEData(): count = $count'); print('Bot : waitForFirstSSEData(): event.chunk = ${event.chunk}'); responsePalMBody = responsePalMBody + event.chunk.toString(); var answerPlainTextChunck = pickFromJson(event.chunk.toString(), 'response').asStringOrNull(); print( 'Bot : waitForFirstSSEData(): answerPlainTextChunck = ${answerPlainTextChunck}'); setState(() { streamingText = streamingText + answerPlainTextChunck!; }); if (count == 0) { final textMessage = types.TextMessage( author: _user1, createdAt: DateTime.now().millisecondsSinceEpoch, id: randomString(), text: streamingText, type: types.MessageType.text, metadata: {"dataSource": null}); _addMessage(textMessage); } count++; } else { print('Bot : waitForFirstSSEData(): eventSource.isNotWeb'); final encoding = event.getEncoding(); print( 'Bot : waitForFirstSSEData(): eventSource.isNotWeb : encoding.decode(event.chunk as List) = ${encoding.decode(event.chunk as List)}'); } }, onError: (err) => print('Bot : waitForFirstSSEData(): err = $err'), onDone: () { eventSource.close; streamingText = ""; }, ); return ""; } Future?> getChatResponseTextToDoC( String msg, String mime, String id, {types.User? user}) async { List? reqResp = []; List RespList = []; Uri url; String userQuestion = ""; String body = ""; String finalAnswer = ""; print('Bot : getChatResponseTextToDoC() : START'); url = Uri.parse( 'https://multi-modal-rag-dgujjntxuq-uc.a.run.app/generate-answer'); print('Bot : getChatResponseTextToDoC() : url = ' + url.host + url.path); print('Bot : getChatResponseTextToDoC() : mime = ' + mime); Map? _headers = { "Content-Type": "$mime", }; userQuestion = msg; print('Bot : getChatResponseTextToDoC() : userQuestion = ' + userQuestion); body = '''{ "query": "${userQuestion}" }'''; print('Bot : getChatResponseTextToDoC() : request_body = ' + body); final stopwatchtextToDoc = Stopwatch()..start(); final _response = await http.post(url, headers: _headers, body: body); stopwatchtextToDoc.stop(); if (_response.statusCode == 200) { print('Bot : getChatResponseTextToDoC() : Status code 200 '); responsePalMBody = _response.body .replaceAll(RegExp(r'(\\u003cb|\\u003e|\\u003c|\\u003e|(\/n))'), ''); var answerPlainText = pickFromJson(responsePalMBody, 'response').asStringOrNull(); if (answerPlainText != null) { finalAnswer = answerPlainText; print( 'Bot : getChatResponseTextToDoC() : answerPlainText != null : finalAnswer = ' + finalAnswer); } else { finalAnswer = "Your request can not be answered right now. Please try again."; print( 'Bot : getChatResponseTextToDoC() : answerPlainText == null : finalAnswer = ' + finalAnswer); } reqResp.add(body); reqResp.add(responsePalMBody); print( 'Bot : getChatResponseTextToDoC() : /generate_answer : reqResp[0] = ' + reqResp[0]); print( 'Bot : getChatResponseTextToDoC() : /generate_answer : reqResp[1] = ' + reqResp[1]); RespList.add(responsePalMBody); // 0 : body of the answer RespList.add(true); //1 : is text RespList.add("dump"); // 2 : Grah config RespList.add(finalAnswer); //3 : answer in plain text //Update stepper state to get_text_summary BlocProvider.of(context).updateStepperStatusUploaded( status: StepperStatus.get_text_summary, message: "NL answer received in", stateStepper: StepState.complete, isActiveStepper: true, debugInfo: StepperExpertInfo( uri: url!.host + url!.path, body: body, header: jsonEncode(_headers!), response: responsePalMBody, summary: finalAnswer, statusCode: _response.statusCode, stepDuration: stopwatchtextToDoc.elapsed.inMilliseconds, )); } else { print( 'Bot : getChatResponseTextToDoC() : _response.statusCode = ${_response.statusCode}'); RespList.add("test"); // 0 : body of the answer RespList.add(true); //1 : is text RespList.add("dump"); // 2 : Grah config RespList.add( "Your request can not be answered right now. Please try again."); //3 : answer in plain text } print('Bot : getChatResponseTextToDoC() : END'); return RespList; } Future ShowCapturedWidget( BuildContext context, Uint8List capturedImage) { print("Bot: ShowCapturedWidget() : START"); return showDialog( useSafeArea: false, context: context, builder: (context) => Scaffold( appBar: AppBar( title: Text("Captured widget screenshot"), ), body: Center(child: Image.memory(capturedImage)), ), ); } List transfromDynamicListToStringList(List list) { List dataList = []; print( 'Bot: transfromDynamicListToStringList() : list.length = ${list.length}'); print('Bot: transfromDynamicListToStringList() : list = $list'); for (int i = 0; i <= list.length - 1; i++) { dataList.add(list.elementAt(i) as String); } if (dataList is List) print( 'BarGraph: transfromDynamicListToStringList() : dataList is of type List'); print( 'BarGraph: transfromDynamicListToStringList() : dataList = $dataList'); return dataList; } void _handleFileSelection() async { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['pdf'], ); if (result != null && result.files.single.path != null) { String res = ""; final message = types.FileMessage( author: _user, createdAt: DateTime.now().millisecondsSinceEpoch, id: randomString(), name: result.files.single.name, size: result.files.single.size, uri: result.files.single.path!, ); _addMessage(message); print('Bot : _handleReceivedResponse() uri = ' + message.uri); textDocAi = "not implemented yet"; //await extractTextMLKit(message.uri); res = textDocAi.replaceAll('"', " "); print('Bot : _handleReceivedResponse() : textDocAi = ' + res); _handleReceivedResponse( 'Ecris en français un résumé du texte ci-dessous en mois de 50 mots:\n' + res, "png"); } } void _handleMessageTap(BuildContext _, types.Message message) async { print('Bot : _handleMessageTap() : DEBUT = '); if (message is types.FileMessage) { print('Bot : _handleMessageTap() : types.FileMessage : message.uri = ' + message.uri); } } void _handlePreviewDataFetched( types.TextMessage message, types.PreviewData previewData, ) { final index = _messages.indexWhere((element) => element.id == message.id); final updatedMessage = (_messages[index] as types.TextMessage).copyWith( previewData: previewData, ); setState(() { _messages[index] = updatedMessage; }); } void _handleAttachmentPressed() { showModalBottomSheet( context: context, builder: (BuildContext context) => SafeArea( child: SizedBox( height: 144, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Align( alignment: AlignmentDirectional.centerStart, child: Text('Photo'), ), ), TextButton( onPressed: () { Navigator.pop(context); _handleFileSelection(); }, child: const Align( alignment: AlignmentDirectional.centerStart, child: Text('File'), ), ), TextButton( onPressed: () => Navigator.pop(context), child: const Align( alignment: AlignmentDirectional.centerStart, child: Text('Cancel'), ), ), ], ), ), ), ); } Widget _bubbleBuilder( Widget child, { required message, required nextMessageInGroup, }) { String colorString = colorBubble; return Container( width: screenSize!.width, child: Bubble( child: child, color: message.author.id != '82091010-a484-4a89-ae75-a22bf8d6f3ab' ? const Color(0xfff5f5f7) //Color(0xfff5f5f7) : const Color(0xffffffff), //Color(0xfffaf9de), margin: null, nip: BubbleNip.no, ), ); } Widget customMessageBuilder(types.CustomMessage customMessage, {required int messageWidth}) { print('Bot : customMessageBuilder(): START'); String inputString = ""; return Container( //width: 500, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ //Display the firstname and lastname Container( padding: const EdgeInsets.only(top: 20, left: 20), width: screenSize!.width, child: Text(_user1.firstName! + " " + _user1.lastName! + "\n", style: TextStyle( fontSize: 12, color: Colors.green, fontWeight: FontWeight.bold), textAlign: TextAlign.left)), //Display answer !customMessage.metadata!.containsKey('stream') || customMessage.metadata!['textSummary'] != "firstChunck" ? Container( padding: const EdgeInsets.only(top: 20, left: 20), width: screenSize!.width, child: Text(customMessage.metadata!['textSummary'], textAlign: TextAlign.start, style: TextStyle(fontSize: 16)), ) : StreamBuilder>( stream: customMessage.metadata!['stream'], builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { print( 'Bot : customMessageBuilder(): StreamBuilder : snapshot.connectionState : WAITING'); streamingText = ""; return SizedBox( width: 100, height: 100, child: CircularProgressIndicator( strokeWidth: 6, ), ); // Display a loading indicator when waiting for data. } else if (snapshot.hasError) { print( 'Bot : customMessageBuilder(): StreamBuilder : snapshot.hasError : ERROR'); return Text( 'Error: ${snapshot.error}'); // Display an error message if an error occurs. } else if (!snapshot.hasData) { print( 'Bot : customMessageBuilder(): StreamBuilder : !snapshot.hasData : No data available'); return Text( 'No data available'); // Display a message when no data is available. } else { print( 'Bot : customMessageBuilder(): StreamBuilder : snapshot.hasData'); var answerPlainTextChunck = pickFromJson( snapshot.data!.chunk.toString(), 'response') .asStringOrNull(); if (answerPlainTextChunck != "end_stream") { //streamingText = streamingText + answerPlainTextChunck!; inputString = inputString + answerPlainTextChunck!; print( 'Bot : customMessageBuilder(): StreamBuilder : snapshot.hasData : answerPlainTextChunck != end_stream'); //customMessage.metadata!['eventSource'].close(); } else { customMessage.metadata!['textSummary'] = inputString; print( 'Bot : customMessageBuilder(): StreamBuilder : snapshot.hasData : answerPlainTextChunck == end_stream'); Stopwatch stopwatchStreaming = customMessage.metadata!['stopWatch'] as Stopwatch; stopwatchStreaming.stop(); //Update stepper state to get_text_summary BlocProvider.of(context) .updateStepperStatusUploaded( status: StepperStatus.get_text_summary, message: "NL answer received in", stateStepper: StepState.complete, isActiveStepper: true, debugInfo: StepperExpertInfo( uri: "https://colab-cloudrun-template-ra1-uz6w7mqrka-uc.a.run.app/generate_streaming", body: '{"N/A": "N/A"}', header: '{"Content-Type": "app/json}', response: responsePalMBody, summary: inputString, statusCode: 200, stepDuration: stopwatchStreaming.elapsed .inMilliseconds, //stopwatchtextToDoc.elapsed.inMilliseconds, )); } return Container( padding: const EdgeInsets.only(top: 20, left: 20), width: screenSize!.width, child: Text(inputString, //streamingText, textAlign: TextAlign.start, style: TextStyle(fontSize: 16)), ); } }), if (customMessage.metadata!['dataSource'] != null && !customMessage.metadata!['textSummary'] .contains("The request did not return meaningful information")) Container( child: TabbedContainer( initialIndex: customMessage.metadata!['graph'] != null ? 1 : 0, controller: _tabController, tabs: const [ Tab(text: "Table", icon: Icon(Icons.table_rows_sharp)), Tab(text: "Graph", icon: Icon(Icons.bar_chart)), ], tabViews: [ Center( child: SingleChildScrollView(child: Container(width: screenSize!.width ,child: customMessage.metadata!['dataSource'] ?? Text('No Data')))), Center( child: getGoogleGraph(customMessage.metadata!['graph']) ?? Text('No Graph')), ], ), ), SizedBox(height: 40), //Add feedback Container( width: screenSize!.width / 10, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), color: Colors.black12, // Adjust the radius as needed ), child: Padding( padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ InkWell( onTap: () { showFeedbackDialog(); }, onHover: (value) { setState(() { _isThumbsUpHovered = value; }); }, child: Tooltip( message: "Provide your feedback on the generated answer", child: new Image( image: new AssetImage("assets/images/thumbs_up1.png"), width: 20, height: 20, color: _isThumbsUpHovered ? Colors.grey.withOpacity(0.5) : null, colorBlendMode: _isThumbsUpHovered ? BlendMode.modulate : null, fit: BoxFit.scaleDown, alignment: Alignment.center, ), ), ), InkWell( onTap: () { showFeedbackDialog(); }, onHover: (value) { setState(() { _isThumbsDownHovered = value; }); }, child: Tooltip( message: "Provide your feedback on the generated answer", child: new Image( image: new AssetImage("assets/images/thumbs_down1.png"), width: 20, height: 20, color: _isThumbsDownHovered ? Colors.grey.withOpacity(0.5) : null, colorBlendMode: _isThumbsDownHovered ? BlendMode.modulate : null, fit: BoxFit.scaleDown, alignment: Alignment.center, ), ), ), InkWell( onTap: () { print("Bot: copy : onTap : START"); print('Bot : customMessageBuilder() : copy : onTap: () : START'); copyGraphToClipBoard(customMessage.metadata!['imageId'], customMessage.metadata!['textSummary']); }, onHover: (value) { setState(() { _isCopyHovered = value; }); }, child: Tooltip( message: "Copy the answer", child: new Image( image: new AssetImage("assets/images/copy1.png"), width: 20, height: 20, color: _isCopyHovered ? Colors.grey.withOpacity(0.5) : null, colorBlendMode: _isCopyHovered ? BlendMode.modulate : null, fit: BoxFit.scaleDown, alignment: Alignment.center, ), ), ), ], ), ), ) //customMessage.metadata!['graph'], ], )); } Widget getGoogleGraph(dynamic dataViz) { print(" bot: getGoogleGraph() : START"); Container container; if (dataViz!["chart_div"]! != null) { print( 'bot: getGoogleGraph() : dataViz!["chart_div"]! = ${dataViz!["chart_div"]!}'); String htmlPre = """
"""; container = Container( child: InAppWebView( initialData: InAppWebViewInitialData( data: htmlPre + "\n" + dataViz!["chart_div"]!.replaceAll('width: 600,','width: 1200,').replaceAll('height: 300,', 'height: 600,') + htmlPost + "\n"), onWebViewCreated: (controller) { webViewController = controller; }, ), ); isGraphKeyAdded = true; graphContainer = container; return container; } else { return Text("No Chart", style: TextStyle( fontSize: 16, color: Colors.indigoAccent, fontWeight: FontWeight.bold)); } } Widget getGoogleTable(dynamic dataViz) { print("bot: getGoogleTable() : START"); if (dataViz!["chart_div_1"]! != null) { print( 'bot: getGoogleTable() : dataViz!["chart_div_1"]! = ${dataViz!["chart_div_1"]!}'); String htmlPre = """
"""; return Container( child: InAppWebView( initialData: InAppWebViewInitialData( data: htmlPre + "\n" + dataViz!["chart_div_1"]!.replaceAll('width: 600,','width: 1200,').replaceAll('height: 300,', 'height: 600,') + htmlPost + "\n"), onWebViewCreated: (controller) { webViewController = controller; }, ), ); } else return Text("No data", style: TextStyle( fontSize: 16, color: Colors.indigoAccent, fontWeight: FontWeight.bold)); } Future _generateImage(Widget widget) async { print('Bot : _generateImage() : START'); //Uint8List capturedImage = await screenshotController.captureFromWidget(widget); => not supported in Flutter Web for now Uint8List? capturedImage = await webViewController!.takeScreenshot(); print('Bot : _generateImage() : capturedImage != null)'); _graphsImagesMap[imageId!] = capturedImage!; //ShowCapturedWidget(context, capturedImage!); return capturedImage; } void showFeedbackDialog() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text("Please rate this answer"), content: ConstrainedBox( constraints: BoxConstraints(maxHeight: 260.0), child: Container( //width: screenSize!.width / 2, child: Padding( padding: const EdgeInsets.all(8.0), child: Column(children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ElevatedButton( child: Text("Good answer", style: TextStyle(color: Colors.white)), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, elevation: 0, ), onPressed: () {}, ), SizedBox(width: 30), ElevatedButton( child: Text("Partial answer", style: TextStyle(color: Colors.white)), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, elevation: 0, ), onPressed: () {}, ), SizedBox(width: 30), ElevatedButton( child: Text("Incorrect answer", style: TextStyle(color: Colors.white)), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, elevation: 0, ), onPressed: () {}, ), ], ), SizedBox(height: 50), TextField( decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Please provide additionnal feedback (optional)', ), maxLines: null, minLines: 5, ), ]), ), ), ), //Text(DicInfoExtractedMap.toString(),), actions: [ TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.labelLarge, ), child: const Text('Submit'), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } Future copyGraphToClipBoard(String imageKey, String summaryText) async { print('Bot : copyGraphToClipBoard() : START'); print('Bot : copyGraphToClipBoard() : imageKey = $imageKey'); print('Bot : copyGraphToClipBoard() : summaryText = $summaryText'); //For now, the copy button only copies text. Copy of images/widgets is supported, so I'm commenting out the copy of image until //the flutter_inappwebview package used to render Google Charts is implementing InAppWebViewController.takeScreenshot() on Flutter Web. if(false) { //if (imageKey != "no_image") { print('Bot : copyGraphToClipBoard() : imageKey != "no_image"'); print('Bot : copyGraphToClipBoard() : _graphsImagesMap.length = ${_graphsImagesMap.length}'); Uint8List imgBytes = _graphsImagesMap[imageKey!]!; if (imgBytes != null) { print('Bot : copyGraphToClipBoard() : imgBytes != null'); final base64Image = base64Encode(imgBytes); try { js.context.callMethod('copyBase64ImageToClipboard', [base64Image]); } catch (e) { print('Bot : copyGraphToClipBoard() : EXCEPTION : e = $e'); } } } else if (summaryText != null || summaryText.length > 0) { print('Bot : copyGraphToClipBoard() : summaryText != null || summaryText.length > 0'); await Clipboard.setData(ClipboardData(text: summaryText)); } else { print('Bot : copyGraphToClipBoard() : else'); await Clipboard.setData(ClipboardData(text: "No data available.")); } } Future?> getChatResponseNew(String msg, String mime, String id, {types.User? user}) async { List? reqResp = []; List RespList = []; Uri urlGenerateSQL; Uri urlRunQuery; String generatedSQLText = ""; String userQuestion = ""; int statusCodeRunQuery = 0; String jsonResponseRunQuery = ""; print('Bot : getChatResponseNew() : START '); urlGenerateSQL = Uri.parse('${TextToDocParameter.endpoint_opendataqnq}/generate_sql'); print('Bot : getChatResponseNew() : urlGenerateSQL = ' + urlGenerateSQL.host + urlGenerateSQL.path); print('Bot : getChatResponseNew() : mime = ' + mime); Map? _headers = { "Content-Type": "$mime", }; print( 'Bot : getChatResponseNew() : BEFORE prepending main question : msg = ' + msg); if (mainQuestionsFollowUpQuestions.containsKey(msg)) { msg = mainQuestionsFollowUpQuestions[msg]; } print( 'Bot : getChatResponseNew() : AFTER prepending main question : msg = ' + msg); userQuestion = msg; print('Bot : getChatResponseNew() : userQuestion = ' + userQuestion); String _body1 = '''{ "session_id" :"${TextToDocParameter.sessionId}", "user_id":"${TextToDocParameter.userID}", "user_question":"${userQuestion}", "user_grouping":"${TextToDocParameter.currentUserGrouping}" }'''; print('Bot : getChatResponseNew() : _body1 = ' + _body1); final stopwatchGenerateSQL = Stopwatch()..start(); final _response = await http.post(urlGenerateSQL, headers: _headers, body: _body1); stopwatchGenerateSQL.stop(); responsePalMBody = _response.body.replaceAll( RegExp(r'(\\u003cb|\\u003e|\\u003c|\\u003e|(\/n)|(\\r))'), ''); print('Bot : getChatResponseNew() : responsePalMBody = $responsePalMBody'); var error = pickFromJson(responsePalMBody!, 'Error').asStringOrNull(); print('Bot : getChatResponseNew() : error = $error'); print( 'Bot : getChatResponseNew() : _response.statusCode = ${_response.statusCode}'); if (_response.statusCode == 200 && responsePalMBody!.toLowerCase().contains("select") && (error!.length == 0 ?? false)) { print( 'Bot : getChatResponseNew() : _response.statusCode == 200 && (error!.length == 0 ?? false )'); TextToDocParameter.sessionId = pickFromJson(responsePalMBody!, 'SessionID').asStringOrNull()!; print( 'Bot : getChatResponseNew() : _response.statusCode == 200 && (error!.length == 0 ?? false ) : TextToDocParameter.sessionId = ${TextToDocParameter.sessionId}'); reqResp.add(_body1); reqResp.add(responsePalMBody); print('Bot : getChatResponseNew() : /generate_sql : reqResp[0] = ' + reqResp[0]); print('Bot : getChatResponseNew() : /generate_sql : reqResp[1] = ' + reqResp[1]); //get the generated SQL query generatedSQLText = extractContentGenerateSQL(responsePalMBody); //Update stepper state to generate_sql BlocProvider.of(context).updateStepperStatusUploaded( status: StepperStatus.generate_sql, message: "SQL request generated.", stateStepper: StepState.complete, isActiveStepper: true, debugInfo: StepperExpertInfo( uri: urlGenerateSQL.host + urlGenerateSQL.path, body: _body1, header: jsonEncode(_headers), response: responsePalMBody, generatedSQLText: generatedSQLText, statusCode: _response.statusCode, stepDuration: stopwatchGenerateSQL.elapsed.inMilliseconds, answerList: [generatedSQLText])); print( "main: getChatResponseNew() : After BlocProvider.of(context).updateStepperStatusUploaded() : generate_sql"); print( "main: getChatResponseNew() : generate_sql : generatedSQLText = $generatedSQLText"); urlRunQuery = Uri.parse('${TextToDocParameter.endpoint_opendataqnq}/run_query'); String _body2 = '''{ "user_question": "${userQuestion}", "user_grouping": "${TextToDocParameter.currentUserGrouping}", "generated_sql": "${generatedSQLText}", "session_id" : "${TextToDocParameter.sessionId}" }'''; //send the request to get the results in tabular format var stopwatchRunQuery = Stopwatch()..start(); final _responseTabResults = await http.post(urlRunQuery, headers: _headers, body: _body2); stopwatchRunQuery.stop(); statusCodeRunQuery = _responseTabResults.statusCode; jsonResponseRunQuery = _responseTabResults.body; print('Bot : getChatResponseNew() : tabular results: urlRunQuery = ' + urlRunQuery.toString()); print( 'Bot : getChatResponseNew() : tabular results : _body2 = ' + _body2); print( 'Bot : getChatResponseNew() : tabular results: _responseTabResults.body = ' + jsonResponseRunQuery); if (TextToDocParameter.anonymized_data) { print( 'Bot : getChatResponseNew() : tabular results: TextToDocParameter.anonymized_data = ${TextToDocParameter.anonymized_data}'); jsonResponseRunQuery = anonymizedData(jsonResponseRunQuery!); } //return extractContent(_responseTabResults.body, id, user: user!); return extractContentResultsOpenDataQnA( jsonResponseRunQuery: jsonResponseRunQuery, userQuestion: userQuestion, generatedSQLText: generatedSQLText, urlRunQuery: urlRunQuery, bodyRunQuery: _body2, headersRunQuery: _headers, statusCodeRunQuery: statusCodeRunQuery, elapsedTimeRunQuery: stopwatchRunQuery.elapsed.inMilliseconds); } else { //Update stepper state to generate_sql BlocProvider.of(context).updateStepperStatusUploaded( status: StepperStatus.generate_sql, message: "SQL request generated in", stateStepper: StepState.complete, isActiveStepper: true, debugInfo: StepperExpertInfo( uri: urlGenerateSQL.host + urlGenerateSQL.path, body: _body1, header: jsonEncode(_headers), response: responsePalMBody, generatedSQLText: "An error occurred, no SQL request has been generated.", statusCode: _response.statusCode, stepDuration: stopwatchGenerateSQL.elapsed.inMilliseconds, answerList: [ "An error occurred, no SQL request has been generated." ])); print( 'Bot : getChatResponseNew() : tabular results : _response.statusCode = ${_response.statusCode} and error attribute is set'); RespList.add(""); RespList.add(true); RespList.add(""); RespList.add( "The request did not return meaningful information. It could be because the question has not been formulated properly or some context is missing."); return RespList; } } String extractContentGenerateSQL(String jsonResponse) { print("bot() : extractContentGenerateSQL() : START"); print("bot() : extractContentGenerateSQL() : jsonResponse = $jsonResponse"); String generatedSQLText = ""; var error = pickFromJson(jsonResponse, 'Error'); var generatedSQLTmp = pickFromJson(jsonResponse, 'GeneratedSQL').asStringOrNull(); var responseCode = pickFromJson(jsonResponse, 'ResponseCode'); print( "bot() : extractContentGenerateSQL() : generatedSQLTmp = ${generatedSQLTmp}"); generatedSQLText = generatedSQLTmp! .replaceAll('\n', ' ') .replaceAll('"', '\\"'); //generates an exception if null => fix it print( "bot() : extractContentGenerateSQL() : generatedSQLText = ${generatedSQLText}; "); return generatedSQLText!; } Future> extractContentResultsOpenDataQnA( {String? jsonResponseRunQuery, String? userQuestion, String? generatedSQLText, Uri? urlRunQuery, String? bodyRunQuery, Map? headersRunQuery, int? statusCodeRunQuery, int? elapsedTimeRunQuery}) async { //String generatedSQLText = ""; List RespList = []; bool isText = true; dynamic googleChartVizRes; String? naturalResponseText = ""; String textSummary = "The request did not return meaningful information. It could be because the question has not been formulated properly or some context is missing."; String? knownDB = ""; String? error = ""; error = pickFromJson(jsonResponseRunQuery!, 'Error').asStringOrNull(); knownDB = pickFromJson(jsonResponseRunQuery!, 'KnownDB').asStringOrNull(); var responseCode = pickFromJson(jsonResponseRunQuery!, 'ResponseCode'); naturalResponseText = pickFromJson(jsonResponseRunQuery!, 'NaturalResponse').asStringOrNull(); print( "bot() : extractContentResultsOpenDataQnA() : knownDB.length = ${knownDB!.length}; "); print( "bot() : extractContentResultsOpenDataQnA() : knownDB = ${knownDB}; "); print( "bot() : extractContentResultsOpenDataQnA() : bodyRunQuery = ${bodyRunQuery}; "); //Update stepper state to run_query BlocProvider.of(context).updateStepperStatusUploaded( status: StepperStatus.run_query, message: "Tabular data retrieved in", stateStepper: StepState.complete, isActiveStepper: true, debugInfo: StepperExpertInfo( uri: urlRunQuery!.host + urlRunQuery!.path, body: bodyRunQuery, header: jsonEncode(headersRunQuery!), response: jsonResponseRunQuery .replaceAll("\"[", "[") .replaceAll("]\"", "]") .replaceAll("\\\"", "\""), knownDB: knownDB, statusCode: statusCodeRunQuery, stepDuration: elapsedTimeRunQuery, answerList: [knownDB])); print( "main: extractContentResultsOpenDataQnA() : After BlocProvider.of(context).updateStepperStatusUploaded() : run_query"); try { if (knownDB != "[]" && error!.length == 0 ?? false) { print("bot() : extractContentResults() : VALID ANSWER"); var knowDBJson = jsonDecode(knownDB!); //Check if it is worth displaying data. If just one row is returned, no use. //if (true) { if ((knowDBJson!.length <= 1 && (knowDBJson![0] as Map).length <= 1) || (knowDBJson![0] as Map).length > 2) { print( "bot() : extractContentResultsOpenDataQnA() : knowDBJson!.length <= 1 and MAP has at most 1 element"); isText = true; //Get graph description in case we want to display a graph googleChartVizRes = await getDataVisualization( userQuestion!, knownDB!, generatedSQLText!); print( "bot() : extractContentResultsOpenDataQnA() : googleChartVizRes = ${googleChartVizRes}"); } else { print( "bot() : extractContentResultsOpenDataQnA() : knowDBJson!.length > 1 or MAP has at least 1 element"); isText = false; //Get graph description in case we want to display a graph googleChartVizRes = await getDataVisualization( userQuestion!, knownDB!, generatedSQLText!); print( "bot() : extractContentResultsOpenDataQnA() : googleChartVizRes = ${googleChartVizRes}"); } print( "bot() : extractContentResultsOpenDataQnA() : isText = ${isText}; "); //get ML summarize //textSummary = await getTextSummary(userQuestion!, knownDB!); RespList.add(knownDB!); RespList.add(isText); RespList.add(googleChartVizRes!); RespList.add(naturalResponseText!.trim()); } else { print("bot() : extractContentResultsOpenDataQnA() : UNVALID ANSWER"); RespList.add(""); RespList.add(true); RespList.add({"chart_div": "empty", "chart_div_1": "empty"}); RespList.add( "The request did not return meaningful information. It could be because the question has not been formulated properly or some context is missing."); } } catch (e) { print("bot() : extractContentResultsOpenDataQnA() : EXCEPTION : $e"); } finally { print("bot() : extractContentResultsOpenDataQnA() : FINALLY CLAUSE "); RespList.add(knownDB!); RespList.add(isText); RespList.add(googleChartVizRes!); RespList.add(naturalResponseText!); return RespList; } } Future getDataVisualization( String question, String tabularAnswer, String generatedSQLText) async { dynamic generatedChartjsMap; //Create the header Map? _headers = { "Content-Type": "application/json", }; String tmpReplace = tabularAnswer.replaceAll('"', '\\"'); print('Bot : getDataVisualization() : tnpReplace = ' + tmpReplace); //Create the body String _body = '''{ "user_question": "$question", "sql_generated": "${generatedSQLText}", "sql_results": "${tmpReplace}", "session_id" : "${TextToDocParameter.sessionId}" }'''; print('Bot : getDataVisualization() : _body = ' + _body); try { var stopwatchGetDataVisulization = Stopwatch()..start(); print('Bot: getDataVisualization() : BEFORE HttpRequest'); var response = await html.HttpRequest.requestCrossOrigin( '${TextToDocParameter.endpoint_opendataqnq}/generate_viz', method: "POST", sendData: _body); print('Bot: getDataVisualization() : AFTER HttpRequest'); stopwatchGetDataVisulization.stop(); print('Bot : getDataVisualization() : response = ' + response.toString()); var jsonData = jsonDecode(response); if (jsonData != null) { print('Bot: getDataVisualization() : jsonData = $jsonData'); generatedChartjsMap = jsonData["GeneratedChartjs"]; print( 'Bot: getDataVisualization() : generatedChartjsMap = $generatedChartjsMap'); if(TextToDocParameter.anonymized_data) { //update generatedChartjsMap mapAnonymisationGraph.forEach((key, value) { print( 'Bot : getDataVisualization() : update jsonData : key = $key, value = $value'); generatedChartjsMap['chart_div'] = generatedChartjsMap['chart_div'].replaceAll(key, value); }); } //Update stepper state to get_graph_description BlocProvider.of(context) .updateStepperStatusUploaded( status: StepperStatus.get_graph_description, message: "Graph generated", stateStepper: StepState.complete, isActiveStepper: true, debugInfo: StepperExpertInfo( uri: "${TextToDocParameter.endpoint_opendataqnq}/generate_viz", body: _body .replaceAll("\"[", "[") .replaceAll("]\"", "]") .replaceAll("\\\"", "\""), header: '''{"Header not accessible": "CORS headers are not accessible as they are sent directly by the web browser"}''', response: response .replaceAll("\"[", "[") .replaceAll("]\"", "]") .replaceAll("\\\"", "\""), statusCode: 0, stepDuration: stopwatchGetDataVisulization.elapsed.inMilliseconds, )); print( "Bot: getDataVisualization() : After BlocProvider.of(context).updateStepperStatusUploaded() : get Google Charts"); } else { print('Bot: getDataVisualization() : jsonData is null'); } } catch (e) { generatedChartjsMap = {"chart_div": "empty", "chart_div_1": "empty"}; print('Bot: getDataVisualization() : EXCEPTION : error = $e'); } finally { return generatedChartjsMap!; } } List setBubbleColor(String text, {types.User? user}) { String tmp = ""; String userType = ""; const String datastoreGenerated = "ap :"; const String nMatchLLMGenerated = "nh :"; int startIndex = 0; List rep = []; print('Bot : setBubbleColor() : text = ' + text); if (user != null) if (_user.id == user!.id) userType = "user"; if (text.contains(datastoreGenerated)) { startIndex = text.indexOf(datastoreGenerated) + datastoreGenerated.length + 1; colorBubble = "datastoreData"; } else if (text.contains(nMatchLLMGenerated)) { startIndex = text.indexOf(nMatchLLMGenerated) + datastoreGenerated.length + 1; colorBubble = "noMatchLLM"; } else if (userType == "user") { colorBubble = "user"; } else { colorBubble = "regularDF"; } tmp = text.substring(startIndex); print('Bot : setBubbleColor() : startIndex = ' + startIndex.toString()); print('Bot : setBubbleColor() : tmp = ' + tmp); print('Bot : setBubbleColor() : colorBubble = ' + colorBubble); rep.add(tmp); rep.add(colorBubble); return rep; } int countOccurences(String mainString, String search) { int lInx = 0; int count = 0; while (lInx != -1) { lInx = mainString.indexOf(search, lInx); if (lInx != -1) { count++; lInx += search.length; } } return count; } Future _dialogExtension(BuildContext context, String extension) { return showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text("Format d'image non supporté", style: TextStyle( fontWeight: FontWeight.normal, fontSize: 22.0, color: Colors.blue)), content: Text( "Le format d'image $extension n'est pas supporté.\n" + "Choisissez une image du type :\ngif, tiff, tif, jpg, jpeg, png, bmp, webp", ), actions: [ TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.labelLarge, ), child: const Text('Ok'), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } String displayDateTime() { String? dateTimeS; final now = DateTime.now(); dateTimeS = DateFormat('yyyy-MM-dd HH:mm:ss').format(now); return dateTimeS!; } List get messages { return _messages; } Map get graphsImages { return _graphsImagesMap; } Map get tableKeysMap { return tableKeyMap; } Widget avatarBuilder(types.User user) { bool isUserAvatar = user.id == '82091010-a484-4a89-ae75-a22bf8d6f3ab'; return CircleAvatar( backgroundColor: Colors.green, backgroundImage: isUserAvatar ? NetworkImage(TextToDocParameter.picture) : null, radius: 16, child: !isUserAvatar ? Text( "TA", ) : null, ); } String anonymizedData(String responseRunQuery) { String anonymizedData = ""; Random random = new Random(); mapAnonymisationGraph.clear(); print("Bot : anonymizedData() : START"); print("Bot : anonymizedData() : responseRunQuery = ${responseRunQuery}"); var responseRunQueryJson = jsonDecode(responseRunQuery); print( "Bot : anonymizedData() : responseRunQueryJson = ${responseRunQueryJson}"); var knownDB = jsonDecode(responseRunQueryJson["KnownDB"]); String naturalResponse = responseRunQueryJson["NaturalResponse"]; print("Bot : anonymizedData() : knownDB = ${knownDB}"); print("Bot : anonymizedData() : naturalResponse = ${naturalResponse}"); for (int i = 0; i < knownDB.length; i++) { var entry = knownDB[i]; print("Bot : anonymizedData() : for : i = $i : entry = ${entry}"); entry.forEach((key, value) { print('Bot : anonymizedData() : key = $key : value = $value'); try { if (value is double) { print( 'Bot : anonymizedData() : i = $i : value = $value is of type double'); double randomNumber = random.nextDouble() * value; int truncatedInt = (randomNumber * 100).toInt(); randomNumber = truncatedInt / 100; print( 'Bot : anonymizedData() : i = $i : randomNumber = $randomNumber'); entry[key] = randomNumber; NumberFormat formatter = NumberFormat("#,##0", "en_US"); // Adjust locale if needed String formattedNumber = formatter.format(value); mapAnonymisationGraph[formattedNumber] = formatter.format(randomNumber);//randomNumber.toString(); } if (value is String) { print( 'Bot : anonymizedData() : i = $i : value = $value is of type String'); String randomString = generateRandomString(value.toString().length); print( 'Bot : anonymizedData() : i = $i : randomString = $randomString'); entry[key] = randomString; mapAnonymisationGraph[value.toString()] = randomString; } } catch (e) { print('Bot : anonymizedData() : PARSING EXCEPTION = $e'); } }); } responseRunQueryJson["KnownDB"] = jsonEncode(knownDB); //update NaturalResponse mapAnonymisationGraph.forEach((key, value) { print( 'Bot : anonymizedData() : update NaturalResponse : key = $key, value = $value'); naturalResponse = naturalResponse.replaceAll(key,value); }); print("Bot : anonymizedData() : END : knownDB = ${knownDB}"); print("Bot : anonymizedData() : END : naturalResponse = ${naturalResponse}"); responseRunQueryJson["NaturalResponse"] = naturalResponse; print( "Bot : anonymizedData() : END : responseRunQueryJson = ${responseRunQueryJson}"); return jsonEncode(responseRunQueryJson); } String generateRandomString(int length) { const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; Random _rnd = Random(); return String.fromCharCodes(Iterable.generate( length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)))); } Future _updateUserGroupingInSessionLogs() async { int count = 0; print( "Bot: _updateUserGroupingInSessionLogs() : START"); //get all the documents corresponding to the user_id of the current user try { var querySnapshot = await widget.db! .collection("${TextToDocParameter.firestore_history_collection}") .where("user_id", isEqualTo: TextToDocParameter.userID) .orderBy('timestamp', descending: true) .limit(1) .get(); print( "Bot: _updateUserGroupingInSessionLogs() : Add user_grouping : querySnapshot.docs.length = ${querySnapshot.docs.length}"); print( "Bot: _updateUserGroupingInSessionLogs() : Add user_grouping : querySnapshot = ${querySnapshot}"); //update all these documents with the user_grouping scenario_name if(TextToDocParameter.currentScenarioName.isEmpty) TextToDocParameter.currentScenarioName = "Scenario"; for (var docSnapshot in querySnapshot.docs) { print( 'Bot: _updateUserGroupingInSessionLogs() : Add user_grouping : TextToDocParameter.currentUserGrouping = ${TextToDocParameter.currentUserGrouping}'); print( 'Bot: _updateUserGroupingInSessionLogs() : Add user_grouping : TextToDocParameter.currentScenarioName = ${TextToDocParameter.currentScenarioName}'); print( 'Bot: _updateUserGroupingInSessionLogs() : Add user_grouping : ${docSnapshot.id} => ${docSnapshot.data()}'); widget.db!.collection("${TextToDocParameter.firestore_history_collection}").doc('${docSnapshot.id}').set( {"user_grouping": "${TextToDocParameter.currentUserGrouping}", "scenario_name": "${TextToDocParameter.currentScenarioName}"}, SetOptions(merge: true)); count++; } } catch (e) { print( 'Bot: _updateUserGroupingInSessionLogs() : Add user_grouping : EXCEPTION : $e'); } var snackBar = SnackBar( content: Text('Updated $count questions on ${TextToDocParameter.firestore_database_id} collection'), duration: Duration(seconds: 3), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); } PaginatedDataTable? createPaginatedTable(String data) { print("bot() : createPaginatedTable() : START"); print("bot() : createPaginatedTable() : data = $data"); List dataColumnList = []; List dataList = jsonDecode(data!); print("bot() : createPaginatedTable() : dataList = $dataList"); print("bot() : createPaginatedTable() : dataList.length = ${dataList.length}"); //get headers of columns var entry = dataList.first; print("bot() : createPaginatedTable() : entry = ${entry}"); print("bot() : createPaginatedTable() : entry.length = ${entry.length}"); print("bot() : createPaginatedTable() : entry['tconst'] = ${entry['tconst']}"); print("bot() : createPaginatedTable() : entry['original_title'] = ${entry['original_title']}"); print("bot() : createPaginatedTable() : entry['average_rating'] = ${entry['average_rating']}"); print("bot() : createPaginatedTable() : entry['title_type'] = ${entry['title_type']}"); for (var element in (entry as Map).entries) { print('bot() : createPaginatedTable() : Key: ${element.key}, Value: ${element.value}'); dataColumnList.add(DataColumn(label: Text(element.key.toString(), style: TextStyle(fontWeight: FontWeight.bold, color : Colors.white)))); } if (dataList.length != 0) { print("bot() : createPaginatedTable() : dataList.length != 0}"); var rowsList = dataList.map((data) { List dataCellsList = []; for (var element in (data as Map).entries) { print('bot() : createPaginatedTable() : Key: ${element.key}, Value: ${element.value}'); DataCell cell = DataCell(Text(element.value.toString())); dataCellsList.add(cell); } return DataRow(cells: dataCellsList); }).toList(); return PaginatedDataTable( //header: Text('Results'), headingRowColor: WidgetStateProperty.all(Colors.blue), rowsPerPage: 3, // Customize as needed columns: dataColumnList, source: OpenDataQnASource(rowsList as List), ); } else return null; } } class OpenDataQnASource extends DataTableSource { final List data; OpenDataQnASource(this.data); @override int get rowCount => data.length; @override DataRow? getRow(int index) { if (index < data.length) { return data[index]; } else { return null; } } @override bool get isRowCountApproximate => false; @override int get selectedRowCount => 0; } ================================================ FILE: frontend/frontend-flutter/lib/screens/bot_chat_view.dart ================================================ import 'package:chatview/chatview.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class BotChatView extends StatefulWidget { const BotChatView({Key? key}) : super(key: key); @override State createState() => BotChatViewState(); } class BotChatViewState extends State { ChatUser? currentUser; ChatUser? bot; ChatController? _chatController; @override void initState() { super.initState(); currentUser = ChatUser( id: '0', name: 'User', profilePhoto: "https://raw.githubusercontent.com/SimformSolutionsPvtLtd/flutter_showcaseview/master/example/assets/simform.png", ); bot = ChatUser( id: '1', name: 'Bot', profilePhoto: "https://raw.githubusercontent.com/SimformSolutionsPvtLtd/flutter_showcaseview/master/example/assets/simform.png", ); _chatController = ChatController( initialMessageList: [ Message( id: '0', message: "Hi Bot!", createdAt: DateTime.now(), sendBy: '0', // userId of who sends the message ), Message( id: '1', message: "Hi!", createdAt: DateTime.now(), sendBy: '1', ), ], scrollController: ScrollController(), chatUsers: [ currentUser!, bot!, ], ); } @override Widget build(BuildContext context) { return Scaffold( body: ChatView( chatBackgroundConfig: ChatBackgroundConfiguration( backgroundImage: "https://raw.githubusercontent.com/SimformSolutionsPvtLtd/flutter_showcaseview/master/example/assets/simform.png",//"assets/images/background.png", messageTimeIconColor: Colors.white, messageTimeTextStyle: TextStyle(color: Colors.white), defaultGroupSeparatorConfig: DefaultGroupSeparatorConfiguration( textStyle: TextStyle( color: Colors.white, fontSize: 17, ), ), backgroundColor: Color(0xffFCD8DC), ), currentUser: currentUser!, chatController: _chatController!, onSendTap: onSendTap, chatViewState: ChatViewState.hasMessages ), ); } void onSendTap(String message, ReplyMessage replyMessage, MessageType messageType){ final message = Message( id: '2', message: "How are you", createdAt: DateTime.now(), sendBy: currentUser!.id, replyMessage: replyMessage, messageType: messageType, ); _chatController!.addMessage(message); } } ================================================ FILE: frontend/frontend-flutter/lib/screens/disclaimer.dart ================================================ import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import '../utils/TextToDocParameter.dart'; late String firstName; late String lastName; late String userID; class Disclaimer extends StatefulWidget { late final FirebaseAuth auth; Disclaimer(this.auth, {super.key}); @override State createState() => DisclaimerState(); } class DisclaimerState extends State { bool isChecked = false; bool isButtonClicked = false; Color _buttonColor = Colors.blueAccent; @override Widget build(BuildContext context) { Size screenSize = MediaQuery.sizeOf(context); return Material( child: !isButtonClicked ? Container( //height: screenSize.height, color: Colors.black, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 35.0, right: 35.0, top: 35.0, bottom: 35.0), child: Text('Google Cloud | Applied AI Engineering', style: TextStyle(fontSize: 20, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(left: 95.0, top: 0), child: Container( width: screenSize!.width / 2, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Open Data QnA', style: TextStyle( fontSize: 70, color: Colors.white, fontWeight: FontWeight.bold)), Padding( padding: const EdgeInsets.only(top: 5), child: Text('Demo', style: TextStyle( fontSize: 40, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(top: 15.0), child: Text( 'User Journey | Architecture Diagram | GCP Console Demo', style: TextStyle( fontSize: 23, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(top: 15.0), child: Divider()), Text( 'This is a functioning solution demo for CEs/FSRs to present to prospects to demonstrate the capabilities of Google Cloud AI products.\n\n' 'It is not an officially supported Google Cloud product or service and is provided without any guarantees of performance or maintenance. Because of the tool\'s limited and experimental nature, we may need to make modifications to, including potentially taking down, the tool on short, or no, notice.\n\n' 'You may provide feedback and suggestions about this tool to Google, and Google and its Affiliates may use any feedback or suggestions provided without restriction and without obligation to you.\n\n' 'Your use of the tool must always comply with our Terms of Service (including the Acceptable Use Policy), or the offline variant of the terms that govern your use of the Google Cloud Platform Services.\n\n' 'During your use of the tool, information is provided for informational purposes only. The data inputed are dummy data.\n\n', maxLines: null, style: TextStyle( fontSize: 18, color: Colors.white, height: 1.2)), CheckboxListTile( title: Text( "By checking this box, you accept and agree to the above terms and conditions of this tool.", style: TextStyle( fontSize: 18, color: Colors.white)), value: isChecked, onChanged: (bool? newValue) { setState(() { isChecked = newValue!; }); }, controlAffinity: ListTileControlAffinity.leading, checkColor: Colors.white, activeColor: Colors.purple, tileColor: Colors.white, contentPadding: EdgeInsets.zero, ), Padding( padding: const EdgeInsets.only(top: 15.0, bottom: 15.0), child: ElevatedButton( onPressed: () { setState(() { isButtonClicked = true; }); }, style: ElevatedButton.styleFrom( backgroundColor: isChecked ? Colors.blue : Colors.blueAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius .zero, // Ensures sharp corners ), ), child: Text('Accept and Agree', style: TextStyle( color: isChecked ? Colors.white : Colors.grey, fontSize: 18)), ), ) ], )), ), ], ), ), ) : /*Container( //height: screenSize.height, color: Colors.black, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(35.0), child: Text('Google Cloud | Applied AI Engineering', style: TextStyle(fontSize: 20, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(left: 95.0, top: 50), child: Container( width: screenSize!.width / 2, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Open Data QnA', style: TextStyle( fontSize: 70, color: Colors.white, fontWeight: FontWeight.bold)), Padding( padding: const EdgeInsets.only(top: 5), child: Text('Demo', style: TextStyle( fontSize: 40, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(top: 15.0), child: Text( 'User Journey | Architecture Diagram | GCP Console Demo', style: TextStyle( fontSize: 23, color: Colors.white)), ), Padding( padding: const EdgeInsets.only(top: 15.0), child: Divider()), Text( 'This is a functioning solution demo for CEs/FSRs to present to prospects to demonstrate the capabilities of Google Cloud AI products.\n\n' 'It is not an officially supported Google Cloud product or service and is provided without any guarantees of performance or maintenance. Because of the tool\'s limited and experimental nature, we may need to make modifications to, including potentially taking down, the tool on short, or no, notice.\n\n' 'You may provide feedback and suggestions about this tool to Google, and Google and its Affiliates may use any feedback or suggestions provided without restriction and without obligation to you.\n\n' 'Your use of the tool must always comply with our Terms of Service (including the Acceptable Use Policy), or the offline variant of the terms that govern your use of the Google Cloud Platform Services.\n\n' 'During your use of the tool, information is provided for informational purposes only. The data inputed are dummy data.\n\n', maxLines: null, style: TextStyle( fontSize: 18, color: Colors.white, height: 1.2)), CheckboxListTile( title: Text( "By checking this box, you accept and agree to the above terms and conditions of this tool.", style: TextStyle( fontSize: 18, color: Colors.white)), value: isChecked, onChanged: (bool? newValue) { setState(() { isChecked = newValue!; }); }, controlAffinity: ListTileControlAffinity.leading, checkColor: Colors.white, activeColor: Colors.purple, tileColor: Colors.white, contentPadding: EdgeInsets.zero, ), Padding( padding: const EdgeInsets.only(top: 15.0), child: ElevatedButton( onPressed: () { setState(() { isButtonClicked = true; }); }, style: ElevatedButton.styleFrom( backgroundColor: isChecked ? Colors.blue : Colors.blueAccent, shape: RoundedRectangleBorder( borderRadius: BorderRadius .zero, // Ensures sharp corners ), ), child: Text('Accept and Agree', style: TextStyle( color: isChecked ? Colors.white : Colors.grey, fontSize: 18)), ), ) ], ), )), ), ], ), )*/ Container( color: Colors.black, child: Center( child: Container( //width: screenSize.width / 3, height: screenSize.height / 6, decoration: BoxDecoration( // Background color color: Colors.white, borderRadius: BorderRadius.circular(20.0), // 10-pixel rounded corners ), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(top: 15.0), child: ElevatedButton( onPressed: () { firebaseAuthentication(); }, style: ElevatedButton.styleFrom( backgroundColor: Color(0xFFeeecec), shape: RoundedRectangleBorder( borderRadius: BorderRadius.zero, ), side: BorderSide( width: 1.0, color: Colors.black, ), ), child: Row( // Use a Row to arrange the icon and text mainAxisSize: MainAxisSize .min, // Make the Row as small as its children children: [ Image.asset( 'assets/images/google_icon.png', width: 25, height: 25, fit: BoxFit.scaleDown, ), // Replace with your image path SizedBox( width: 8), // Add some spacing between the icon and text Text('Sign in with Google', style: TextStyle( color: Colors.black, fontSize: 14)) ], ), ), ), Padding( padding: const EdgeInsets.only( right: 40.0, left: 40.0, top: 20, bottom: 30), child: Text('To use this demo, you have to sign in', style: TextStyle(color: Colors.black, fontSize: 16)), ), ], ), ), ), ), ); } Future signInWithGoogle() async { print('Disclaimer: signInWithGoogle() : START'); // Create a new provider GoogleAuthProvider googleProvider = GoogleAuthProvider(); googleProvider .addScope('https://www.googleapis.com/auth/contacts.readonly'); googleProvider.setCustomParameters({'login_hint': 'user@example.com'}); // Once signed in, return the UserCredential //return await FirebaseAuth.instance.signInWithPopup(googleProvider); return await widget.auth.signInWithPopup(googleProvider); } Future firebaseAuthentication() async { print('Disclaimer: firebaseAuthentication() : START'); print('Disclaimer: firebaseAuthentication() : auth= ${widget.auth}'); String? res = null; try { final credential = await signInWithGoogle(); print('Disclaimer: firebaseAuthentication() : credential = $credential'); final user = credential.user; print('Disclaimer: firebaseAuthentication() : user.uid = ${user!.uid}.'); print( 'Disclaimer: firebaseAuthentication() : credential.additionalUserInfo.profile["given_name"] = ${credential.additionalUserInfo!.profile!["given_name"]}'); print( 'Disclaimer: firebaseAuthentication() : credential.additionalUserInfo.profile["family_name"] = ${credential.additionalUserInfo!.profile!["family_name"]}'); TextToDocParameter.firstName = credential.additionalUserInfo!.profile!["given_name"]; TextToDocParameter.lastName = credential.additionalUserInfo!.profile!["family_name"]; TextToDocParameter.email = credential.additionalUserInfo!.profile!["email"]; TextToDocParameter.userID = credential.user!.uid; TextToDocParameter.picture = credential.additionalUserInfo!.profile!["picture"]; print( 'Disclaimer: firebaseAuthentication() : TextToDocParameter.userID = ${TextToDocParameter.userID}'); TextToDocParameter.isAuthenticated = true; print( "Disclaimer: firebaseAuthentication() : TextToDocParameter.isAuthenticated = ${TextToDocParameter.isAuthenticated}"); print('Disclaimer: firebaseAuthentication() : user?.uid = ${user?.uid}'); print( 'Disclaimer: firebaseAuthentication() : email = ${TextToDocParameter.email}'); print( 'Disclaimer: firebaseAuthentication() : picture = ${TextToDocParameter.picture}'); Navigator.of(context).pushReplacementNamed('/landingPage'); } on FirebaseAuthException catch (e) { if (e.code == 'user-not-found') { print('Disclaimer: firebaseAuthentication() : User does not exists'); res = 'User does not exists'; return res; } else if (e.code == 'wrong-password') { print('Disclaimer: firebaseAuthentication() : Password does not match'); res = 'Password does not match'; return res; } else { print('Disclaimer: firebaseAuthentication() : ${e.code}'); print('Disclaimer: firebaseAuthentication() : ${e.message}'); res = e.message; return res; } } } } ================================================ FILE: frontend/frontend-flutter/lib/screens/settings.dart ================================================ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:flutter/material.dart'; import '../services/display_stepper/display_stepper_cubit.dart'; import '../utils/TextToDocParameter.dart'; import 'package:flutter/services.dart'; class Settings extends StatefulWidget { Settings(FirebaseFirestore this.db, {Key? key}) : super(key: key); bool useFeedback = false; bool useColorMode = false; bool useDashboards = false; bool useReports = false; bool useExpertMode = false; bool useLog = false; bool useAnonymizedMode = false; static bool isLoadConfig = false; static bool isUseFeedback = false; static bool isUseColorMode = false; static bool isUseDashboards = false; static bool isUseReports = false; static bool isExpert = false; static bool isUseLog = false; static bool isAnonymizedMode = false; FirebaseFirestore db; @override State createState() => SettingsState(); } class SettingsState extends State { @override Widget build(BuildContext context) { print("Settings : build() : START"); print("Settings : build() : widget.db = ${widget.db}"); return Scaffold( appBar: AppBar( title: Text('Open data QnA', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.purple)), centerTitle: false, leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { Navigator.pop(context, Settings.isExpert); }, ), actions: [ Padding( padding: const EdgeInsets.only(right: 100), child: IconButton( icon: Image.asset('assets/images/cymbal_logo.png'), onPressed: () { ; }, ), ), ], ), body: SettingsList( sections: [ SettingsSection( title: Text('General', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), tiles: [ SettingsTile.switchTile( initialValue: TextToDocParameter.isLoadConfig, onToggle: (value) { setState(() { //Settings.isLoadConfig = value; TextToDocParameter.isLoadConfig = value; print( "Settings : build() : TextToDocParameter.isLoadConfig = ${TextToDocParameter.isLoadConfig}"); importFrontEndCfgFile(); }); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/config_frontend.png', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Upload frontend config file'), ], ), description: Text("Set required app's parameters"), ), SettingsTile.switchTile( initialValue: widget.useFeedback, onToggle: (value) { setState(() { //useFeedback = value; }); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/feedback.png', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Feedback (not implemented yet)'), ], ), description: Text('Send feedback on generated answers'), ), SettingsTile.switchTile( initialValue: widget.useColorMode, onToggle: (value) { setState(() { //useColorMode = value; }); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/color_mode.png', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Enable dark mode (not implemented yet)'), ], )), SettingsTile.switchTile( initialValue: TextToDocParameter.anonymized_data,//Settings.isAnonymizedMode, onToggle: (value) { setState(() { //Settings.isAnonymizedMode = value; TextToDocParameter.anonymized_data = value; //print("Settings : build() : widget.useExpertMode = ${widget.useExpertMode}"); print( "Settings : build() : TextToDocParameter.anonymized_data = ${TextToDocParameter.anonymized_data}"); updateFrontEndFlutterCfg(parameter: "anonymized_data", value: value); }); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/anonymized.jpeg', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Enable anonymization of data'), ], )), ], ), SettingsSection( title: Text('Statistics', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), tiles: [ SettingsTile.switchTile( initialValue: widget.useDashboards, onToggle: (value) { setState(() { //useDashboards = value; }); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/statistics.png', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Dashboards (not implemented yet)'), ], ), description: Text('Get visibility on activities'), ), SettingsTile.switchTile( initialValue: widget.useReports, onToggle: (value) { setState(() { //useReports = value; }); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/reports.png', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Reports (not implemented yet)'), ], ), description: Text('Export activity reports in pdf'), ), ], ), SettingsSection( title: Text('Troubleshooting', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), tiles: [ SettingsTile.switchTile( initialValue: TextToDocParameter.expert_mode,//Settings.isExpert, onToggle: (value) { setState(() { //widget.useExpertMode = value; //Settings.isExpert = value; TextToDocParameter.expert_mode = value; //Config.isExpert = value; //print("Settings : build() : widget.useExpertMode = ${widget.useExpertMode}"); print( "Settings : build() : TextToDocParameter.expert_mode = ${TextToDocParameter.expert_mode}"); BlocProvider.of(context).displayStepper(value); }); updateFrontEndFlutterCfg(parameter: "expert_mode", value: value); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/troubleshooting.png', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Expert mode'), ], ), description: Text( 'Get workflow details and internal technical informations'), ), SettingsTile.switchTile( initialValue: widget.useLog, onToggle: (value) { setState(() { //useLog = value; }); }, title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Image.asset( 'assets/images/logs.png', height: 70, width: 70, fit: BoxFit.cover, ), Container(width: 20), Text('Enable logs (not implemented yet)'), ], ), description: Text('Generate logs for troubleshooting'), ), ], ), ], ), ); } void importFrontEndCfgFile() async { print('Settings: importFrontEndCfgFile() : START'); List>? rowsAsListOfValues; final filePickerResult = await FilePicker.platform.pickFiles( allowMultiple: false, allowedExtensions: ['json'], type: FileType.custom, dialogTitle: "Import Frontend json Config File", ); if (filePickerResult != null) { print( 'Settings: importFrontEndCfgFile() : fileName = ${filePickerResult.files.single.name}'); print( 'Settings: importFrontEndCfgFile() : size = ${filePickerResult.files.single.size}'); //print('Settings: importFrontEndCfgFile() : path = ${filePickerResult.files.single.path}'); Uint8List fileBytes = filePickerResult.files.single.bytes!; String fileContent = utf8.decode(fileBytes); print('Settings: importFrontEndCfgFile() : fileContent = ${fileContent}'); var cfg = jsonDecode(fileContent); print('Settings: importFrontEndCfgFile() : cfg = ${cfg}'); if (cfg != null) { setState(() { TextToDocParameter.isLoadConfig = false; TextToDocParameter.anonymized_data = cfg["anonymized_data"]; TextToDocParameter.expert_mode = cfg["expert_mode"]; TextToDocParameter.endpoint_opendataqnq = cfg["endpoint_opendataqnq"]; TextToDocParameter.firestore_database_id = cfg["firestore_database_id"]; TextToDocParameter.firebase_app_name = cfg["firebase_app_name"]; TextToDocParameter.firestore_history_collection = cfg["firestore_history_collection"]; TextToDocParameter.firestore_cfg_collection = cfg["firestore_cfg_collection"]; TextToDocParameter.imported_questions = cfg["imported_questions"]; }); if (TextToDocParameter.anonymized_data != null && TextToDocParameter.expert_mode != null && TextToDocParameter.endpoint_opendataqnq != null && TextToDocParameter.firestore_database_id != null && TextToDocParameter.firebase_app_name != null && TextToDocParameter.firestore_history_collection != null && TextToDocParameter.firestore_cfg_collection != null && TextToDocParameter.imported_questions != null ) { print('Settings: importFrontEndCfgFile() : Trying to update front_end_flutter_cfg'); try { widget.db .collection("${TextToDocParameter.firestore_cfg_collection}") .doc('${TextToDocParameter.userID}') .set({ "endpoint_opendataqnq": "${TextToDocParameter.endpoint_opendataqnq}", "firestore_database_id": "${TextToDocParameter.firestore_database_id}", "expert_mode": TextToDocParameter.expert_mode, "anonymized_data": TextToDocParameter.anonymized_data, "firebase_app_name": "${TextToDocParameter.firebase_app_name}", "firestore_history_collection": "${TextToDocParameter.firestore_history_collection}", "firestore_cfg_collection": "${TextToDocParameter.firestore_cfg_collection}", "imported_questions": "${TextToDocParameter.imported_questions}" }); showSuccessfulUploadMsg(); } catch (e) { print('Settings: importFrontEndCfgFile() : EXCEPTION : ${e}'); displayCfgUploadErrorMsg(); } } else { print('Settings: importFrontEndCfgFile() : some fields if cfg are null, could not update firestore_cfg_collection'); displayCfgUploadErrorMsg(); } } } } void showSuccessfulUploadMsg() { showDialog( context: context,//navigatorKey.currentContext!, builder: (BuildContext context) { return AlertDialog( title: Row( // Use a Row to align the icon and title children: [ Icon(Icons.info, color: Colors.blueAccent), SizedBox(width: 8), // Add some spacing Text('Information'), ], ), content: Text('The json configuration file has been\nuploaded successfully to Firestore.'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); // Close the dialog }, child: Text('OK'), ), ], ); }, ); } void displayCfgUploadErrorMsg() { showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return AlertDialog( title: Row( // Use a Row to align the icon and title children: [ Icon(Icons.warning, color: Colors.red), SizedBox(width: 8), // Add some spacing Text('Alert'), ], ), content: SelectableText.rich( TextSpan( children: [ TextSpan(text: "Please check you have uploaded a config_frontend.json\n" + "file similar as the one below:\n\n"), TextSpan(text: "{\n", style: TextStyle(color: Colors.black),), TextSpan(text:'"endpoint_opendataqnq": ', style: TextStyle(color: Colors.blueAccent),), TextSpan(text:'"",\n', style: TextStyle(color: Colors.green),), TextSpan(text:'"firestore_database_id": ', style: TextStyle(color: Colors.blueAccent),), TextSpan(text:'"opendataqna-session-logs",\n', style: TextStyle(color: Colors.green),), TextSpan(text:'"firestore_history_collection": ', style: TextStyle(color: Colors.blueAccent),), TextSpan(text:'"session_logs",\n', style: TextStyle(color: Colors.green),), TextSpan(text:'"firestore_cfg_collection": ', style: TextStyle(color: Colors.blueAccent),), TextSpan(text:'"front_end_flutter_cfg",\n', style: TextStyle(color: Colors.green),), TextSpan(text:'"expert_mode": ', style: TextStyle(color: Colors.blueAccent),), TextSpan(text:',\n', style: TextStyle(color: Colors.red),), TextSpan(text:'"anonymized_data": ', style: TextStyle(color: Colors.blueAccent),), TextSpan(text:',\n', style: TextStyle(color: Colors.red),), TextSpan(text:'"firebase_app_name": ', style: TextStyle(color: Colors.blueAccent),), TextSpan(text:'"opendataqna"\n', style: TextStyle(color: Colors.green),), TextSpan(text:'}', style: TextStyle(color: Colors.black),), ], ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); // Close the dialog }, child: Text('OK'), ), ], ); }, ); } void updateFrontEndFlutterCfg({required String parameter, required bool value}) { print("Settings : updateFrontEndFlutterCfg() : START"); print("Settings : updateFrontEndFlutterCfg() : widget.db = ${widget.db}"); print("Settings : updateFrontEndFlutterCfg() : parameter = ${parameter}"); print("Settings : updateFrontEndFlutterCfg() : value = ${value}"); print( 'Settings: updateFrontEndFlutterCfg() : TextToDocParameter.firestore_cfg_collection = ${TextToDocParameter.firestore_cfg_collection}'); print( 'Settings: updateFrontEndFlutterCfg() : TextToDocParameter.userID = ${TextToDocParameter.userID}'); //update the document in front_end_flutter_cfg collection corresponding to the user_id to refelect the chnage of the TextToDocParameter.expert_mode parameter try { widget.db! .collection("${TextToDocParameter.firestore_cfg_collection}") .doc('${TextToDocParameter.userID}') .set( {"$parameter": value}, SetOptions(merge: true)); print( 'Settings: updateFrontEndFlutterCfg() : update firestore_history_collection.expert_mode successfully: parameter = $parameter : value = $value'); } catch (e) { print( 'Settings: updateFrontEndFlutterCfg() : update firestore_history_collection.expert_mode : EXCEPTION : $e'); } } } class Config { static bool isUseFeedback = false; static bool isUseColorMode = false; static bool isUseDashboards = false; static bool isUseReports = false; static bool isExpert = false; static bool isUseLog = false; static bool isAnonymizedMode = false; @override String toString() { return isExpert.toString(); } } ================================================ FILE: frontend/frontend-flutter/lib/services/display_stepper/display_stepper_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:ttmd/services/display_stepper/display_stepper_state.dart'; import '../../utils/TextToDocParameter.dart'; class DisplayStepperCubit extends Cubit { DisplayStepperCubit() : super(DisplayStepperState( status: displayStepperStatus.remove_stepper)); Future displayStepper(bool isDisplay) async { print( 'DisplayStepperCubit : displayStepper() : DEBUT '); if(isDisplay) emit(state.copyWith( status: displayStepperStatus.display_stepper)); else emit(state.copyWith( status: displayStepperStatus.remove_stepper)); } } ================================================ FILE: frontend/frontend-flutter/lib/services/display_stepper/display_stepper_state.dart ================================================ import 'package:equatable/equatable.dart'; enum displayStepperStatus {display_stepper,remove_stepper} class DisplayStepperState extends Equatable { final displayStepperStatus status; DisplayStepperState({ this.status = displayStepperStatus.remove_stepper, }); @override List get props => [this.status]; DisplayStepperState copyWith({ displayStepperStatus? status, }) { print('DisplayStepperState : copyWith() : status = ' + status.toString()); return DisplayStepperState( status: status ?? this.status, ); } } ================================================ FILE: frontend/frontend-flutter/lib/services/first_question/first_question_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:ttmd/services/first_question/first_question_state.dart'; class FirstQuestionCubit extends Cubit { FirstQuestionCubit() : super(FirstQuestionState( status: firstQuestionStatus.display_welcome_message , message: "")); Future removeWelcomeMessage({String? message = ""}) async { print( 'FirstQuestionCubit : removeWelcomeMessage() : DEBUT '); emit(state.copyWith( status: firstQuestionStatus.remove_welcome_message , message: message)); } } ================================================ FILE: frontend/frontend-flutter/lib/services/first_question/first_question_state.dart ================================================ import 'package:equatable/equatable.dart'; enum firstQuestionStatus {display_welcome_message,remove_welcome_message} class FirstQuestionState extends Equatable { final firstQuestionStatus status; final String? message; FirstQuestionState({ this.status = firstQuestionStatus.display_welcome_message, this.message = "", }); @override List get props => [this.status,this.message!]; FirstQuestionState copyWith({ firstQuestionStatus? status, String? message }) { print('FirstQuestionState : copyWith() : message = ' + message!); return FirstQuestionState( status: status ?? this.status, message: message ?? this.message, ); } } ================================================ FILE: frontend/frontend-flutter/lib/services/load_question/load_question_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:ttmd/services/load_question/load_question_state.dart'; import 'package:http/http.dart' as http; import 'dart:html' as html; import 'dart:convert'; class LoadQuestionCubit extends Cubit { LoadQuestionCubit() : super(LoadQuestionState( status: LoadQuestionStatus.initial, question: "")); Future loadQuestionToChat({String? question, String ? time}) async { print( 'LoadQuestionCubit : loadQuestionToChat() : DEBUT '); emit(state.copyWith( status: LoadQuestionStatus.loaded, question: question, time: time)); } } ================================================ FILE: frontend/frontend-flutter/lib/services/load_question/load_question_state.dart ================================================ import 'package:equatable/equatable.dart'; enum LoadQuestionStatus {initial,loaded, error} class LoadQuestionState extends Equatable { final LoadQuestionStatus status; final String? question; final String? time; LoadQuestionState({ this.status = LoadQuestionStatus.initial, this.question = "", this.time = "" }); @override List get props => [this.status,this.question!, this.time!]; LoadQuestionState copyWith({ LoadQuestionStatus? status, String? question, String? time }) { print('LoadQuestionState : copyWith() : question = $question! : time = $time'); return LoadQuestionState( status: status ?? this.status, question: question ?? this.question, time: time ?? this.time ); } } ================================================ FILE: frontend/frontend-flutter/lib/services/new_suggestions/new_suggestion_cubit.dart ================================================ import 'dart:convert'; import 'dart:math'; import 'package:bloc/bloc.dart'; import 'package:intl/intl.dart'; import 'package:ttmd/utils/TextToDocParameter.dart'; import 'package:ttmd/services/new_suggestions/new_suggestion_state.dart'; import 'dart:html' as html; class NewSuggestionCubit extends Cubit { NewSuggestionCubit() : super(NewSuggestionState( status: NewSuggestionStateStatus.initial, suggestionList: const ["x:", "x:", "x:"], time: "", scenarioNumber: 0)); Future generateNewSuggestions(int scenarioNumber, String question, {String? lastCannedQuestion, bool? isACannedQuestion, String? userGrouping}) async { List respLLMQuestion = []; List respCannedQuestions = []; List resp = []; List tmpQuestions = []; String body = ""; Uri url; String question1 = ""; String question2 = ""; String question3 = ""; String question4 = ""; String timeString = ""; String originalQuestion = ""; print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : START'); print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : userGrouping = $userGrouping'); print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : TextToDocParameter.lastCannedQuestion = ${TextToDocParameter.lastCannedQuestion}'); print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : lastCannedQuestion= ${lastCannedQuestion}'); print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : scenarioNumber = $scenarioNumber'); print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : question = $question'); timeString = displayDateTime(); //TextToDocParameter.lastCannedQuestion = question; //TextToDocParameter.lastCannedQuestion = lastCannedQuestion?? ""; isACannedQuestion = isACannedQuestion ?? false; print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : Not a canned question : isACannedQuestion = $isACannedQuestion'); originalQuestion = question; print( 'NewSuggestionCubit : NewSuggestionCubit() : generateNewSuggestions : Not a canned question : originalQuestion = $originalQuestion'); //Create the header Map? _headers = { "Content-Type": "application/json", //"Authorization": " Bearer ${client!.credentials.accessToken.toString()}", }; //Create the body body = '''{ "user_grouping": "$userGrouping" }'''; print( 'NewSuggestionCubit : generateNewSuggestions() : Not a canned question : body = ' + body); try { var response = await html.HttpRequest.requestCrossOrigin( '${TextToDocParameter.endpoint_opendataqnq}/get_known_sql', method: "POST", sendData: body); print( 'NewSuggestionCubit : generateNewSuggestions() : Not a canned question : response = ' + response.toString()); final jsonData = jsonDecode(response); if (jsonData != null) { print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : jsonData = $jsonData'); //KnownSQL = [{"example_user_question": "question1", "example_generated_sql": "sql1"}, // {"example_user_question": "question2", "example_generated_sql": "sql2"}, // ...] var knownSql = jsonData["KnownSQL"].replaceAll(RegExp(r'((\\n)|(\\r))'), ''); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSql = $knownSql'); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSql.runtimeType = ${knownSql.runtimeType}'); if (knownSql is Map) print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSql is a Map'); if (knownSql is List) print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSql is a List'); if (knownSql is String) print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSql is a String'); var knownSqlMap = jsonDecode(knownSql); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSqlMap.runtimeType = ${knownSqlMap.runtimeType}'); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSqlMap[0].runtimeType = ${knownSqlMap[0].runtimeType}'); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSqlMap = ${knownSqlMap}'); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : knownSqlMap[0] = ${knownSqlMap[0].toString()}'); for (int i = 0; i < knownSqlMap.length; i++) { for (var entry in knownSqlMap[i].entries) { print('${entry.key} : ${entry.value}'); if (entry.key == "example_user_question") tmpQuestions.add(entry.value); } } print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : Before pickUpRandomQuestion() : tmpQuestions = ${tmpQuestions}'); tmpQuestions = pickUpRandomQuestion(question, tmpQuestions); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : After pickUpRandomQuestion() : tmpQuestions = ${tmpQuestions}'); if (tmpQuestions.length > 0) question1 = tmpQuestions[0] ?? "No suggestion for question1"; if (tmpQuestions.length > 1) question2 = tmpQuestions[1] ?? "No suggestion for question2"; if (tmpQuestions.length > 2) question3 = tmpQuestions[2] ?? "No suggestion for question3"; if (tmpQuestions.length > 3) question4 = tmpQuestions[3] ?? "No suggestion for question4"; //Adding scenarioNumber + ":" and ":userGrouping" to have same format as for Business KPI questions question1 = "$scenarioNumber:" + question1 + ":" + userGrouping!; question2 = "$scenarioNumber:" + question2 + ":" + userGrouping!; question3 = "$scenarioNumber:" + question3 + ":" + userGrouping!; question4 = "$scenarioNumber:" + question4 + ":" + userGrouping!; print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : question1 = ${question1}'); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : question2 = ${question2}'); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : question3 = ${question3}'); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : question4 = ${question4}'); } } catch (e) { print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : EXCEPTION = $e'); throw Exception('Failed to get suggestions: $e'); } finally { respLLMQuestion.add(question1); respLLMQuestion.add(question2); respLLMQuestion.add(question3); respLLMQuestion.add(question4); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question :scenarioNumber = $scenarioNumber >= 9 : respLLMQuestion = $respLLMQuestion'); //2024-07-14 respCannedQuestions = pickUpNextQuestions(scenarioNumber, question); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : scenarioNumber = $scenarioNumber >= 9 : respCannedQuestions = $respCannedQuestions'); resp.addAll(respLLMQuestion); print( 'NewSuggestionCubit: generateNewSuggestions() : Not a canned question : scenarioNumber = $scenarioNumber >= 9 : resp = $resp'); } emit(state.copyWith( status: NewSuggestionStateStatus.loaded, suggestionList: resp, time: timeString, scenarioNumber: scenarioNumber)); } Future getAllquestions(String userGrouping) async { List resp = []; String body = ""; String timeString = ""; print('NewSuggestionCubit : getAllquestions() : START'); timeString = displayDateTime(); //Create the header Map? _headers = { "Content-Type": "application/json", //"Authorization": " Bearer ${client!.credentials.accessToken.toString()}", }; //Create the body body = '''{ "user_grouping": "$userGrouping" }'''; print('NewSuggestionCubit : getAllquestions() : body = ' + body); try { var response = await html.HttpRequest.requestCrossOrigin( '${TextToDocParameter.endpoint_opendataqnq}/get_known_sql', method: "POST", sendData: body); print('NewSuggestionCubit : getAllquestions() : response = ' + response.toString()); final jsonData = jsonDecode(response); if (jsonData != null) { print('NewSuggestionCubit: getAllquestions() : jsonData = $jsonData'); //KnownSQL = [{"example_user_question": "question1", "example_generated_sql": "sql1"}, // {"example_user_question": "question2", "example_generated_sql": "sql2"}, // ...] var knownSql = jsonData["KnownSQL"].replaceAll(RegExp(r'((\\n)|(\\r))'), ''); print('NewSuggestionCubit: getAllquestions() : knownSql = $knownSql'); var knownSqlMap = jsonDecode(knownSql); for (int i = 0; i < knownSqlMap.length; i++) { for (var entry in knownSqlMap[i].entries) { print('${entry.key} : ${entry.value}'); if (entry.key == "example_user_question") resp.add(entry.value); } } } } catch (e) { print('NewSuggestionCubit: getAllquestions() : EXCEPTION = $e'); throw Exception('Failed to get questions: $e'); } finally { print('NewSuggestionCubit: getAllquestions() : resp = ${resp}'); } emit(state.copyWith( status: NewSuggestionStateStatus.all_questions_loaded, suggestionList: resp, time: timeString, scenarioNumber: 0, userGrouping: userGrouping)); } String displayDateTime() { String? dateTimeS; final now = DateTime.now(); dateTimeS = DateFormat('yyyy-MM-dd HH:mm:ss').format(now); return dateTimeS!; } List pickUpNextQuestions(int scenarioNumber, String question) { List list = []; return list; } int getIndexOfQuestion( int scenarioNumber, String question, List listQuestionsScenario) { print('NewSuggestionCubit: getIndexOfQuestion() : START'); print( 'NewSuggestionCubit: getIndexOfQuestion() : question = $scenarioNumber:$question'); for (String q in listQuestionsScenario) print('q = $q'); int index = 0; index = listQuestionsScenario.indexOf("$scenarioNumber:$question"); print('NewSuggestionCubit: getIndexOfQuestion() : index = $index'); return index; } List pickUpRandomQuestion( String question, List questionList) { List list = []; int lengthScenario = 0; Random random = new Random(); List listQuestionsScenario = []; int randomNumber = 0; String candidateQuestion = ""; Map map = {}; print('NewSuggestionCubit: pickUpRandomQuestion() : START'); print('NewSuggestionCubit: pickUpRandomQuestion() : question = $question'); for (int i = 0; list.length <= 3; i++) { randomNumber = random.nextInt(questionList.length); candidateQuestion = questionList[randomNumber]; print( 'NewSuggestionCubit: pickUpRandomQuestion() : candidateQuestion = $candidateQuestion'); if (question != candidateQuestion) { print( 'NewSuggestionCubit: pickUpRandomQuestion() : if(question != candidateQuestion) : candidateQuestion = $candidateQuestion'); if (!map.containsKey(candidateQuestion)) { print( 'NewSuggestionCubit: pickUpRandomQuestion() : !map.containsKey(candidateQuestion : adding candidateQuestion'); map[candidateQuestion] = candidateQuestion; list.add(candidateQuestion); } } //listQuestionsScenario may contain less than 3 questions (including the asked question //so we check that and break the loop if we retreived all the candidate questions if (list.length == questionList.length - 1) { break; } } print('NewSuggestionCubit: pickUpRandomQuestion() : list = $list'); return list; } } ================================================ FILE: frontend/frontend-flutter/lib/services/new_suggestions/new_suggestion_state.dart ================================================ import 'package:equatable/equatable.dart'; enum NewSuggestionStateStatus {initial,loading,loaded, all_questions_loaded} class NewSuggestionState extends Equatable { final NewSuggestionStateStatus status; final List? suggestionList; final String? time; final int? scenarioNumber; final String? userGrouping; NewSuggestionState({ this.status = NewSuggestionStateStatus.initial, this.suggestionList = const ["x:","x:", "x:"], this.time = "", this.scenarioNumber = 0, this.userGrouping = "" }); @override List get props => [this.status,this.suggestionList!, this.time!, this.scenarioNumber!, this.userGrouping!]; NewSuggestionState copyWith({ NewSuggestionStateStatus? status, List? suggestionList, String? time, int? scenarioNumber, String? userGrouping }) { print('NewSuggestionState : copyWith() : suggestion = $suggestionList'); return NewSuggestionState( status: status ?? this.status, suggestionList: suggestionList ?? this.suggestionList, time: time ?? this.time, scenarioNumber: scenarioNumber ?? this.scenarioNumber, userGrouping: userGrouping ?? this.userGrouping ); } } ================================================ FILE: frontend/frontend-flutter/lib/services/text_to_doc_question/text_to_doc_question_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:ttmd/services/text_to_doc_question/text_to_doc_question_state.dart'; class TextToDocQuestionCubit extends Cubit { TextToDocQuestionCubit() : super(TextToDocQuestionState( status: textToDocStatus.not_text_to_doc , message: "")); Future switchToTextToDoc({required bool isTextToDoc, String? message = ""}) async { print( 'TextToDocQuestionCubit : switchToTextToDoc() : DEBUT '); print( 'TextToDocQuestionCubit : switchToTextToDoc() : isTextToDoc = $isTextToDoc '); if(isTextToDoc) { emit(state.copyWith( status: textToDocStatus.text_to_doc, message: message)); } else { emit(state.copyWith( status: textToDocStatus.not_text_to_doc, message: message)); } } } ================================================ FILE: frontend/frontend-flutter/lib/services/text_to_doc_question/text_to_doc_question_state.dart ================================================ import 'package:equatable/equatable.dart'; enum textToDocStatus {not_text_to_doc,text_to_doc} class TextToDocQuestionState extends Equatable { textToDocStatus status; String? message; TextToDocQuestionState({ this.status = textToDocStatus.not_text_to_doc, this.message = "", }); @override List get props => [this.status,this.message!]; TextToDocQuestionState copyWith({ textToDocStatus? status, String? message }) { print('TextToDocQuestionState : copyWith() : status = $status : message = $message'); this.status = status!; this.message = message; return TextToDocQuestionState( status: status ?? this.status, message: message ?? this.message, ); } } ================================================ FILE: frontend/frontend-flutter/lib/services/update_expert_mode/update_expert_mode_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:ttmd/services/update_expert_mode/update_expert_mode_state.dart'; import '../../utils/TextToDocParameter.dart'; class UpdateExpertModeCubit extends Cubit { UpdateExpertModeCubit() : super(UpdateExpertModeState( status: updateExpertModeStatus.expert_mode_off)); Future updateExpertMode(bool isDisplay) async { print( 'UpdateExpertModeCubit : updateExpertMode() : DEBUT '); if(isDisplay) emit(state.copyWith( status: updateExpertModeStatus.expert_mode_on)); else emit(state.copyWith( status: updateExpertModeStatus.expert_mode_off)); } } ================================================ FILE: frontend/frontend-flutter/lib/services/update_expert_mode/update_expert_mode_state.dart ================================================ import 'package:equatable/equatable.dart'; enum updateExpertModeStatus {expert_mode_on,expert_mode_off} class UpdateExpertModeState extends Equatable { final updateExpertModeStatus status; UpdateExpertModeState({ this.status = updateExpertModeStatus.expert_mode_off, }); @override List get props => [this.status]; UpdateExpertModeState copyWith({ updateExpertModeStatus? status, }) { print('UpdateExpertModeState : copyWith() : status = ' + status.toString()); return UpdateExpertModeState( status: status ?? this.status, ); } } ================================================ FILE: frontend/frontend-flutter/lib/services/update_popular_questions/update_popular_questions_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:ttmd/services/update_popular_questions/update_popular_questions_state.dart'; import '../../utils/most_popular_questions.dart'; class UpdatePopularQuestionsCubit extends Cubit { UpdatePopularQuestionsCubit() : super(UpdatePopularQuestionsState( status: UpdateMostPopularQuestionStatus.initial, mostPopularQuestionsList: [ MostPopularQ("", 0, ""), MostPopularQ("", 0, ""), MostPopularQ("", 0, "") ], time: "")); Future updateMostPopularQuestions( {List? mostPopularQuestionsList, String? time}) async { print( 'UpdatePopularQuestionsCubit : UpdatePopularQuestionsCubit() : DEBUT '); emit(state.copyWith( status: UpdateMostPopularQuestionStatus.loaded, mostPopularQuestionsList: mostPopularQuestionsList, time: time)); } } ================================================ FILE: frontend/frontend-flutter/lib/services/update_popular_questions/update_popular_questions_state.dart ================================================ import 'package:equatable/equatable.dart'; import '../../utils/most_popular_questions.dart'; enum UpdateMostPopularQuestionStatus {initial,loaded} class UpdatePopularQuestionsState extends Equatable { final UpdateMostPopularQuestionStatus status; final List? mostPopularQuestionsList; final String? time; UpdatePopularQuestionsState({ this.status = UpdateMostPopularQuestionStatus.initial, this.mostPopularQuestionsList, this.time = "" }); @override List get props => [this.status,this.mostPopularQuestionsList!, this.time!]; UpdatePopularQuestionsState copyWith({ UpdateMostPopularQuestionStatus? status, List? mostPopularQuestionsList, String? time }) { print('UpdatePopularQuestionsState : copyWith() : List? mostPopularQuestionsList = ' + mostPopularQuestionsList!.toString()); return UpdatePopularQuestionsState( status: status ?? this.status, mostPopularQuestionsList: mostPopularQuestionsList ?? this.mostPopularQuestionsList, time: time ?? this.time ); } } ================================================ FILE: frontend/frontend-flutter/lib/services/update_stepper/update_stepper_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:ttmd/services/update_stepper/update_stepper_state.dart'; import 'package:flutter/material.dart'; import 'dart:html' as html; import 'dart:convert'; import 'package:ttmd/utils/stepper_expert_info.dart'; class UpdateStepperCubit extends Cubit { //List? dicRows; //String? message; //StepState? stateStepper; //bool? isActiveStepper; UpdateStepperCubit() : super(UpdateStepperState( status: StepperStatus.initial, message: "Initial state", stateStepper: StepState.disabled, isActiveStepper: false, debugInfo: StepperExpertInfo())); Future updateStepperStatusUploaded( {required StepperStatus status, required String message, required StepState stateStepper, required bool isActiveStepper, StepperExpertInfo? debugInfo}) async { print('UpdateStepperCubit : updateStepperStatusUploaded() : DEBUT '); print('UpdateStepperCubit : updateStepperStatusUploaded() : status = $status'); print('UpdateStepperCubit : updateStepperStatusUploaded() : message = $message'); print( 'UpdateStepperCubit : updateStepperStatusUploaded() : stateStepper = $stateStepper'); print( 'UpdateStepperCubit : updateStepperStatusUploaded() : isActiveStepper = $isActiveStepper'); print( 'UpdateStepperCubit : updateStepperStatusUploaded() : debugInfo = $debugInfo'); try { emit(state.copyWith( status: status, message: message, stateStepper: stateStepper, isActiveStepper: isActiveStepper, debugInfo: debugInfo )); } catch (e) { print( 'UpdateStepperCubit : updateStepperStatusUploaded() : EXCEPTION : ' + e.toString()); emit(state.copyWith( status: StepperStatus.error, message: "Une erreur s'est produite.", stateStepper: StepState.error, isActiveStepper: false, debugInfo: StepperExpertInfo() )); } } } ================================================ FILE: frontend/frontend-flutter/lib/services/update_stepper/update_stepper_state.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import '../../utils/stepper_expert_info.dart'; enum StepperStatus { initial, uploaded, extracted, compared, committed, error, enter_question, generate_sql, run_query, get_graph_description, get_text_summary } class UpdateStepperState extends Equatable { final StepperStatus status; String? message; StepState? stateStepper; bool? isActiveStepper; final StepperExpertInfo debugInfo; UpdateStepperState( {this.status = StepperStatus.initial, this.message = "", this.stateStepper = StepState.disabled, this.isActiveStepper = false, this.debugInfo = const StepperExpertInfo()}); @override List get props => [ this.status, this.message!, this.stateStepper!, this.isActiveStepper!, this.debugInfo! ]; UpdateStepperState copyWith( {StepperStatus? status, String? message, StepState? stateStepper, bool? isActiveStepper, StepperExpertInfo? debugInfo}) { print( 'UpdateStepperState : copyWith() : status = $status : message = $message : stateStepper = $stateStepper : isActiveStepper = $isActiveStepper : debugInfo = $debugInfo'); return UpdateStepperState( status: status ?? this.status, message: message ?? this.message, stateStepper: stateStepper ?? this.stateStepper, isActiveStepper: isActiveStepper ?? this.isActiveStepper, debugInfo: debugInfo ?? this.debugInfo); } } ================================================ FILE: frontend/frontend-flutter/lib/utils/Input_custom.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; class InputCustom extends Input { final bool? isAttachmentUploading; final VoidCallback? onAttachmentPressed; final void Function(types.PartialText) onSendPressed; final InputOptions options; //TextEditingController? _textController; InputCustom({ super.key, this.isAttachmentUploading, this.onAttachmentPressed, required this.onSendPressed, this.options = const InputOptions(), }): super( isAttachmentUploading: isAttachmentUploading, onAttachmentPressed: onAttachmentPressed, onSendPressed: onSendPressed, options: options, ); } ================================================ FILE: frontend/frontend-flutter/lib/utils/TextToDocParameter.dart ================================================ class TextToDocParameter { static bool isTextTodocGlobal = false; static bool isAuthenticated = false; static bool anonymized_data = false; static int lastScenarioNumber = 0; static String lastCannedQuestion = ""; static String sessionId = ""; static String userID = ""; static String currentUserGrouping = ""; static String currentScenarioName = ""; static String email = ""; static String firstName = ""; static String lastName = ""; static String picture = ""; static bool isLoadConfig = false; static bool expert_mode = false; static String endpoint_opendataqnq = ""; static String firebase_app_name = ""; static String firestore_database_id = ""; static String firestore_history_collection = ""; static String firestore_cfg_collection = ""; static String imported_questions = ""; static int questionCount = 1; static List suggestionsList = []; static List userGroupingList = []; } ================================================ FILE: frontend/frontend-flutter/lib/utils/custom_input_field.dart ================================================ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/src/models/input_clear_mode.dart'; import 'package:flutter_chat_ui/src/models/send_button_visibility_mode.dart'; import 'package:flutter_chat_ui/src/util.dart'; import 'package:flutter_chat_ui/src/widgets/state/inherited_chat_theme.dart'; import 'package:flutter_chat_ui/src/widgets/state/inherited_l10n.dart'; import 'package:flutter_chat_ui/src/widgets/input/attachment_button.dart'; import 'package:flutter_chat_ui/src/widgets/input/input_text_field_controller.dart'; import 'package:flutter_chat_ui/src/widgets/input/send_button.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'dart:html' as html; import '../services/new_suggestions/new_suggestion_cubit.dart'; import '../services/new_suggestions/new_suggestion_state.dart'; import 'TextToDocParameter.dart'; /// A class that represents bottom bar widget with a text field, attachment and /// send buttons inside. By default hides send button when text field is empty. class CustomInputField extends StatefulWidget { /// Creates [Input] widget. const CustomInputField({ super.key, this.isAttachmentUploading, this.onAttachmentPressed, required this.onSendPressed, this.db, this.options = const InputOptions(), }); /// Whether attachment is uploading. Will replace attachment button with a /// [CircularProgressIndicator]. Since we don't have libraries for /// managing media in dependencies we have no way of knowing if /// something is uploading so you need to set this manually. final bool? isAttachmentUploading; final FirebaseFirestore? db; /// See [AttachmentButton.onPressed]. final VoidCallback? onAttachmentPressed; /// Will be called on [SendButton] tap. Has [types.PartialText] which can /// be transformed to [types.TextMessage] and added to the messages list. final void Function(types.PartialText) onSendPressed; /// Customisation options for the [Input]. final InputOptions options; @override State createState() => _InputState(); } /// [Input] widget state. class _InputState extends State { List suggestionsList = []; late final _inputFocusNode = FocusNode( onKeyEvent: (node, event) { if (event.physicalKey == PhysicalKeyboardKey.enter && !HardwareKeyboard.instance.physicalKeysPressed.any( (el) => { PhysicalKeyboardKey.shiftLeft, PhysicalKeyboardKey.shiftRight, }.contains(el), )) { if (kIsWeb && _textController.value.isComposingRangeValid) { return KeyEventResult.ignored; } if (event is KeyDownEvent) { _handleSendPressed(); } return KeyEventResult.handled; } else { return KeyEventResult.ignored; } }, ); bool _sendButtonVisible = false; late TextEditingController _textController; @override void initState() { super.initState(); _textController = widget.options.textEditingController ?? InputTextFieldController(); _handleSendButtonVisibilityModeChange(); } void _handleSendButtonVisibilityModeChange() { _textController.removeListener(_handleTextControllerChange); if (widget.options.sendButtonVisibilityMode == SendButtonVisibilityMode.hidden) { _sendButtonVisible = false; } else if (widget.options.sendButtonVisibilityMode == SendButtonVisibilityMode.editing) { _sendButtonVisible = _textController.text.trim() != ''; _textController.addListener(_handleTextControllerChange); } else { _sendButtonVisible = true; } } void _handleSendPressed() { print( "CustomInputField: build() : _inputBuilder() : TypeAheadField : _handleSendPressed()"); final trimmedText = _textController.text.trim(); if (trimmedText != '') { final partialText = types.PartialText(text: trimmedText); widget.onSendPressed(partialText); if (widget.options.inputClearMode == InputClearMode.always) { _textController.clear(); } } } void _handleTextControllerChange() { if (_textController.value.isComposingRangeValid) { return; } setState(() { _sendButtonVisible = _textController.text.trim() != ''; }); } Widget _inputBuilder() { final query = MediaQuery.of(context); final buttonPadding = InheritedChatTheme.of(context) .theme .inputPadding .copyWith(left: 16, right: 16); final safeAreaInsets = isMobile ? EdgeInsets.fromLTRB( query.padding.left, 0, query.padding.right, query.viewInsets.bottom + query.padding.bottom, ) : EdgeInsets.zero; final textPadding = InheritedChatTheme.of(context) .theme .inputPadding .copyWith(left: 0, right: 0) .add( EdgeInsets.fromLTRB( widget.onAttachmentPressed != null ? 0 : 24, 0, _sendButtonVisible ? 0 : 24, 0, ), ); return Focus( autofocus: !widget.options.autofocus, child: Padding( padding: InheritedChatTheme.of(context).theme.inputMargin, child: Material( borderRadius: InheritedChatTheme.of(context).theme.inputBorderRadius, color: InheritedChatTheme.of(context).theme.inputBackgroundColor, surfaceTintColor: InheritedChatTheme.of(context).theme.inputSurfaceTintColor, elevation: InheritedChatTheme.of(context).theme.inputElevation, child: Container( decoration: BoxDecoration( color: Color(0xFFF0F2F6), // Background color ), //InheritedChatTheme.of(context).theme.inputContainerDecoration, padding: safeAreaInsets, child: Row( textDirection: TextDirection.ltr, children: [ /*if (widget.onAttachmentPressed != null) AttachmentButton( isLoading: widget.isAttachmentUploading ?? false, onPressed: widget.onAttachmentPressed, padding: buttonPadding, ),*/ Container(width: 30), Expanded( child: Padding( padding: textPadding, child: FutureBuilder( future: getAllquestions(), builder: (context, snapshot) { if(snapshot.hasData) { suggestionsList = snapshot.data!; print( "CustomInputField: build() : _inputBuilder() : suggestionList.length = ${suggestionsList.length}"); } return TypeAheadField( hideOnEmpty: true, controller: _textController, direction: VerticalDirection.up, loadingBuilder: (context) => const Text('Loading...'), onSelected: (entry) { _textController.text = entry.suggestion!; entry.scenarioNumber; BlocProvider.of(context) .generateNewSuggestions( entry.scenarioNumber!, entry.suggestion!, isACannedQuestion: false, userGrouping: entry.userGrouping ); }, itemBuilder: (context, entry) { print( "CustomInputField: build() : TypeAheadField : _inputBuilder(): itemBuilder: entry = $entry"); return ListTile( title: Text(entry.suggestion!), ); }, suggestionsCallback: _textController.text.length <= 1 ? suggestionsEmptyCallback : suggestionsCallback, builder: (context, _textController, inputFocusNode) { print( "CustomInputField: build() : TypeAheadField : _inputBuilder(): builder: focusNode = $inputFocusNode"); return TextField( enabled: widget.options.enabled, autocorrect: widget.options.autocorrect, autofocus: widget.options.autofocus, enableSuggestions: widget.options.enableSuggestions, controller: _textController, cursorColor: InheritedChatTheme.of(context) .theme .inputTextCursorColor, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular( 50.0), // Adjust the radius as needed ), //labelText: 'Password', filled: true, fillColor: Colors.white, suffixIcon: Visibility( visible: _sendButtonVisible, child: IconButton( onPressed: () { print( "CustomInputField: build() : _inputBuilder() : TypeAheadField : IconButton : onPressed: ()"); _handleSendPressed(); }, icon: Icon(Icons.send), ), ), ), focusNode: inputFocusNode, keyboardType: widget.options.keyboardType, maxLines: 5, minLines: 1, onChanged: widget.options.onTextChanged, onTap: widget.options.onTextFieldTap, style: TextStyle(color: Colors.black), textCapitalization: TextCapitalization.sentences, ); }, ); }, )), ), Container(width: 30), ], ), ), ), ), ); } Future loadCfgFromFirestore() async { /*db = await FirebaseFirestore.instanceFor( app: app, databaseId: 'opendataqna-session-logs');*/ print("CustomInputField: loadCfgFromFirestore() : db = $widget.db"); if (TextToDocParameter.userID.isEmpty) { print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.userID is empty = ${TextToDocParameter.userID}"); return; } try { print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.userID = ${TextToDocParameter.userID}"); DocumentSnapshot doc = await widget.db! .collection("front_end_flutter_cfg") .doc('${TextToDocParameter.userID}') .get(); if (doc != null) { final data = doc.data() as Map; TextToDocParameter.anonymized_data = data["anonymized_data"]; TextToDocParameter.expert_mode = data["expert_mode"]; TextToDocParameter.endpoint_opendataqnq = data["endpoint_opendataqnq"]; TextToDocParameter.firestore_database_id = data["firestore_database_id"]; TextToDocParameter.firebase_app_name = data["firebase_app_name"]; TextToDocParameter.firestore_history_collection = data["firestore_history_collection"]; TextToDocParameter.firestore_cfg_collection = data["firestore_cfg_collection"]; TextToDocParameter.imported_questions = data["imported_questions"]; print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.anonymized_data = ${TextToDocParameter.anonymized_data}"); print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.expert_mode = ${TextToDocParameter.expert_mode}"); print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firestore_database_id = ${TextToDocParameter.firestore_database_id}"); print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.endpoint_opendataqnq = ${TextToDocParameter.endpoint_opendataqnq}"); print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firebase_app_name = ${TextToDocParameter.firebase_app_name}"); print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firestore_history_collection = ${TextToDocParameter.firestore_history_collection}"); print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.firestore_cfg_collection = ${TextToDocParameter.firestore_cfg_collection}"); print( "CustomInputField: loadCfgFromFirestore() : TextToDocParameter.imported_questions = ${TextToDocParameter.imported_questions}"); } else { print("CustomInputField: loadCfgFromFirestore() : doc == null"); } } catch (e) { print("CustomInputField: loadCfgFromFirestore() : EXCEPTION ON FIRESTORE : e = $e"); //https://www.acodeblog.com/post/2022/5/29/flutter-showdialog-without-context-using-the-navigatorkey } } Future> getAllquestions() async { List resp = []; List userGroupingList = []; print('CustomInputField: getAllquestions() : START'); await loadCfgFromFirestore(); userGroupingList = await _getUserGrouping(); print('CustomInputField: getAllquestions() : userGroupingList = ${userGroupingList}'); for (String userGrouping in userGroupingList) { var list = await getAllquestionsFromUserGroup(userGrouping); resp.addAll((list as List) .map((question) => Suggestion( suggestion: question, userGrouping: userGrouping!)) .toList()); print('CustomInputField: getAllquestions() : userGrouping = $userGrouping : resp.length = ${resp.length}'); } print('CustomInputField: getAllquestions() : END : resp.length = ${resp.length}'); return resp; } Future> getAllquestionsFromUserGroup(String userGrouping) async { List resp = []; String body = ""; print('CustomInputField : getAllquestionsFromUserGroup() : START'); //Create the header Map? _headers = { "Content-Type": "application/json", //"Authorization": " Bearer ${client!.credentials.accessToken.toString()}", }; //Create the body body = '''{ "user_grouping": "$userGrouping" }'''; print('CustomInputField : getAllquestionsFromUserGroup() : body = ' + body); try { var response = await html.HttpRequest.requestCrossOrigin( '${TextToDocParameter.endpoint_opendataqnq}/get_known_sql', method: "POST", sendData: body); print('CustomInputField : getAllquestionsFromUserGroup() : response = ' + response.toString()); final jsonData = jsonDecode(response); if (jsonData != null) { print('CustomInputField: getAllquestionsFromUserGroup() : jsonData = $jsonData'); //KnownSQL = [{"example_user_question": "question1", "example_generated_sql": "sql1"}, // {"example_user_question": "question2", "example_generated_sql": "sql2"}, // ...] var knownSql = jsonData["KnownSQL"].replaceAll(RegExp(r'((\\n)|(\\r))'), ''); print('CustomInputField: getAllquestionsFromUserGroup() : knownSql = $knownSql'); var knownSqlMap = jsonDecode(knownSql); for (int i = 0; i < knownSqlMap.length; i++) { for (var entry in knownSqlMap[i].entries) { print('${entry.key} : ${entry.value}'); if (entry.key == "example_user_question") resp.add(entry.value); } } } } catch (e) { print('CustomInputField: getAllquestionsFromUserGroup() : EXCEPTION = $e'); throw Exception('Failed to get questions: $e'); } finally { print('CustomInputField: getAllquestionsFromUserGroup() : END : resp = ${resp}'); return resp; } } Future> _getUserGrouping() async { print('CustomInputField : _getUserGrouping() : START'); List resp = []; Map? _headers = { "Content-Type": "application/json", //"Authorization": " Bearer ${client!.credentials.accessToken.toString()}", }; try { print( 'CustomInputField : _getUserGrouping() : url = ${TextToDocParameter.endpoint_opendataqnq}/available_databases'); var response = await html.HttpRequest.requestCrossOrigin( '${TextToDocParameter.endpoint_opendataqnq}/available_databases', method: "GET"); print('CustomInputField : _getUserGrouping() : response = ' + response.toString()); final jsonData = jsonDecode(response); if (jsonData != null) { print('CustomInputField : _getUserGrouping() : jsonData = $jsonData'); /* Expected response : { "Error": "", "KnownDB": "[{\"table_schema\":\"imdb-postgres\"},{\"table_schema\":\"retail-postgres\"}]", "ResponseCode": 200 }*/ var knownSqlMap = jsonDecode(jsonData['KnownDB']); print('CustomInputField : _getUserGrouping() : knownSqlMap = ${knownSqlMap}'); print( 'CustomInputField : _getUserGrouping() : knownSqlMap[0] = ${knownSqlMap[0].toString()}'); for (int i = 0; i < knownSqlMap.length; i++) { for (var entry in knownSqlMap[i].entries) { print('${entry.key} : ${entry.value}'); if (entry.key == "table_schema") resp.add(entry.value); } } } else { resp.add(""); } } catch (e) { print('CustomInputField : _getUserGrouping() : EXCEPTION = $e'); throw Exception('Failed to get earning calls question suggestions: $e'); } finally { print('CustomInputField : _getUserGrouping() : resp = $resp'); return resp; } } @override void didUpdateWidget(covariant CustomInputField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.options.sendButtonVisibilityMode != oldWidget.options.sendButtonVisibilityMode) { _handleSendButtonVisibilityModeChange(); } } @override void dispose() { _inputFocusNode.dispose(); _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { print("CustomInputField: build(): START"); return GestureDetector( onTap: () => _inputFocusNode.requestFocus(), child: _inputBuilder(), ); } Future> suggestionsCallback(String pattern) async => Future>.delayed( Duration(milliseconds: 300), () => suggestionsList.where((entry) { final nameLower = entry.suggestion!.toLowerCase(); final patternLower = pattern.toLowerCase();//pattern.toLowerCase().split(' ').join(''); return nameLower.contains(patternLower); }).toList(), ); Future> suggestionsEmptyCallback(String pattern) async => Future>.delayed(Duration(milliseconds: 300), () { return []; /*return [ Suggestion( suggestion: "looking for suggestions ...", scenarioNumber: 0) ].where((entry) { final nameLower = entry.suggestion!.toLowerCase(); final patternLower = pattern.toLowerCase().split(' ').join(''); return nameLower.contains(patternLower); }).toList()*/ ; }); } @immutable class InputOptions { const InputOptions({ this.inputClearMode = InputClearMode.always, this.keyboardType = TextInputType.multiline, this.onTextChanged, this.onTextFieldTap, this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing, this.textEditingController, this.autocorrect = true, this.autofocus = false, this.enableSuggestions = true, this.enabled = true, }); /// Controls the [Input] clear behavior. Defaults to [InputClearMode.always]. final InputClearMode inputClearMode; /// Controls the [Input] keyboard type. Defaults to [TextInputType.multiline]. final TextInputType keyboardType; /// Will be called whenever the text inside [TextField] changes. final void Function(String)? onTextChanged; /// Will be called on [TextField] tap. final VoidCallback? onTextFieldTap; /// Controls the visibility behavior of the [SendButton] based on the /// [TextField] state inside the [Input] widget. /// Defaults to [SendButtonVisibilityMode.editing]. final SendButtonVisibilityMode sendButtonVisibilityMode; /// Custom [TextEditingController]. If not provided, defaults to the /// [InputTextFieldController], which extends [TextEditingController] and has /// additional fatures like markdown support. If you want to keep additional /// features but still need some methods from the default [TextEditingController], /// you can create your own [InputTextFieldController] (imported from this lib) /// and pass it here. final TextEditingController? textEditingController; /// Controls the [TextInput] autocorrect behavior. Defaults to [true]. final bool autocorrect; /// Whether [TextInput] should have focus. Defaults to [false]. final bool autofocus; /// Controls the [TextInput] enableSuggestions behavior. Defaults to [true]. final bool enableSuggestions; /// Controls the [TextInput] enabled behavior. Defaults to [true]. final bool enabled; } class Suggestion { final String suggestion; final String userGrouping; final int? scenarioNumber; Suggestion({ required this.suggestion, required this.userGrouping, this.scenarioNumber = 0, }); } ================================================ FILE: frontend/frontend-flutter/lib/utils/most_popular_questions.dart ================================================ class MostPopularQ { int count; String time; String question; MostPopularQ(this.question,this.count, this.time); @override String toString() { return "{question = $question : count = $count : time = $time}"; } } ================================================ FILE: frontend/frontend-flutter/lib/utils/pdf_viewer.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; //Not a usable class to display PDF files. //I keep it though as a stub for future development class PdfViewer extends StatefulWidget { final List bytes; PdfViewer({super.key, required this.bytes}); @override _PdfViewerState createState() => _PdfViewerState(); } class _PdfViewerState extends State { @override initState() { print('_PdfViewerState : initState() : START'); super.initState(); } @override Widget build(BuildContext context) { print('_PdfViewerState : build() : START'); return MaterialApp( debugShowCheckedModeBanner: false, title: 'PDFViewer', theme: ThemeData( appBarTheme: AppBarTheme( backgroundColor: Colors.white, ), ), home: Scaffold( appBar: AppBar( leading: IconButton( icon: Icon(Icons.arrow_back), onPressed: () { Navigator.pop(context); }, ), title: Text('mySummaryDoc.pdf'), actions: [ IconButton( icon: Icon( Icons.search, color: Colors.black, ), onPressed: () { }, ), ], ), body: Center( child: Container( child: Text("Needs to be impemented")), ), ), ); } } ================================================ FILE: frontend/frontend-flutter/lib/utils/stepper_expert_info.dart ================================================ class StepperExpertInfo { final String? uri; final String? body; final String? header; final String? response; final String? generatedSQLText; final List ? answerList; final String? knownDB; final String? finalNLAnswer; final int? statusCode; final int? stepDuration; final String? graphTitle; final String? xAxisTitle; final String? yAxisTitle; final String? summary; const StepperExpertInfo({ this.uri = "", this.body = '{"message" : "No data"}', this.header = '{"message" : "No data"}', this.response = '{"message" : "No data"}', this.generatedSQLText = "", this.knownDB = "", this.finalNLAnswer = "", this.statusCode = 0, this.stepDuration = 0, this.graphTitle = "", this.xAxisTitle = "", this.yAxisTitle = "", this.summary = "", this.answerList = const [""] }); } ================================================ FILE: frontend/frontend-flutter/lib/utils/tabbed_container.dart ================================================ import 'package:flutter/material.dart'; class TabbedContainer extends StatefulWidget { final List tabs; final List tabViews; final TabController controller; final int initialIndex; const TabbedContainer({ super.key, required this.tabs, required this.tabViews, required this.controller, required this.initialIndex }); @override _TabbedContainerState createState() => _TabbedContainerState(); } class _TabbedContainerState extends State with SingleTickerProviderStateMixin { int _currentIndex = 0; @override void initState() { super.initState(); _currentIndex = widget.initialIndex; widget.controller.index = _currentIndex; // Initialize /*widget.controller.addListener(() { setState(() { _currentIndex = widget.controller.index; }); });*/ } @override Widget build(BuildContext context) { return Container( height : 500, child: Column( children: [ TabBar( controller: widget.controller, indicatorSize: TabBarIndicatorSize.tab, tabs: widget.tabs), Flexible( fit: FlexFit.loose, child: TabBarView(controller: widget.controller, children: widget.tabViews) ), ], ), ); } } ================================================ FILE: frontend/frontend-flutter/nl2sql_oss.iml ================================================ ================================================ FILE: frontend/frontend-flutter/pubspec.yaml ================================================ name: ttmd description: "A new Flutter project." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: '>=3.2.3 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 googleapis: ^13.1.0 googleapis_auth: ^1.5.0 path_provider: ^2.1.2 font_awesome_flutter: ^10.7.0 url_launcher: ^6.2.5 deep_pick: ^1.0.0 flutter_chat_ui: ^1.6.12 flutter_chat_types: ^3.6.2 image_picker: ^1.0.7 file_picker: ^8.0.5 bubble: ^1.2.1 chatview: ^1.3.1 simple_gradient_text: ^1.3.0 flutter_bloc: ^8.1.4 equatable: ^2.0.5 easy_sidemenu: ^0.6.0 expandable_tree_menu: ^0.1.0-dev.5 intl: ^0.18.1 badges: ^3.1.2 expansion_tile_card: ^3.0.0 flutter_json_viewer: ^1.0.1 flutter_login: ^5.0.0 screenshot: ^3.0.0 flutter_settings_ui: ^3.0.1 flutter_typeahead: ^5.2.0 simple_http_api: ^1.1.1 firebase_auth: ^5.1.3 firebase_core: ^3.3.0 js: ^0.7.1 csv: ^6.0.0 image_picker_web: ^4.0.0 flutter_inappwebview: ^6.0.0 cloud_firestore: ^5.4.0 dependency_overrides: libphonenumber_plugin: ^0.3.3 libphonenumber_web: ^0.3.2 js: ^0.7.1 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - assets/images/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: frontend/frontend-flutter/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility in the flutter_test package. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ttmd/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } ================================================ FILE: frontend/frontend-flutter/web/index 01.49.28.html ================================================ ttmd ================================================ FILE: frontend/frontend-flutter/web/index.html ================================================ frontend_flutter ================================================ FILE: frontend/frontend-flutter/web/manifest.json ================================================ { "name": "frontend_flutter", "short_name": "frontend_flutter", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: frontend/frontend.yaml ================================================ steps: - name: 'node:21' args: - install - '--legacy-peer-deps' dir: frontend entrypoint: npm - name: 'node:21' args: - install - '--legacy-peer-deps' - '@popperjs/core' dir: frontend entrypoint: npm - name: 'node:21' dir: frontend script: | #!/usr/bin/env bash npm run build || true - name: gcr.io/google.com/cloudsdktool/cloud-sdk dir: frontend entrypoint: gsutil - name: 'gcr.io/${_FIREBASE_PROJECT_ID}/firebase' args: - use - '${_FIREBASE_PROJECT_ID}' dir: frontend - name: 'gcr.io/${_FIREBASE_PROJECT_ID}/firebase' args: - deploy - '--only' - hosting dir: frontend - name: 'gcr.io/${_FIREBASE_PROJECT_ID}/firebase' args: - deploy - '--only' - firestore dir: frontend timeout: 1200s ================================================ FILE: frontend/package.json ================================================ { "name": "frontend", "version": "0.0.0", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "serve:ssr:frontend": "node dist/frontend/server/server.mjs" }, "private": true, "dependencies": { "@angular/animations": "^17.0.0", "@angular/cdk": "^17.0.5", "@angular/common": "^17.0.0", "@angular/compiler": "^17.0.0", "@angular/core": "^17.0.0", "@angular/fire": "^17.0.1", "@angular/forms": "^17.0.0", "@angular/material": "^17.0.5", "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/platform-server": "^17.0.0", "@angular/router": "^17.0.0", "@angular/ssr": "^17.0.9", "angular-google-charts": "^2.2.3", "bootstrap": "^5.3.2", "chart.js": "^4.4.1", "deps": "^1.0.0", "express": "^4.21.1", "firebase": "^10.12.2", "ng2-charts": "^5.0.4", "prismjs": "^1.29.0", "rxjs": "~7.8.0", "sql-formatter": "^15.2.0", "tslib": "^2.3.0", "zone.js": "~0.14.2" }, "devDependencies": { "@angular-devkit/build-angular": "^17.0.10", "@angular/cli": "^17.0.9", "@angular/compiler-cli": "^17.0.0", "@types/express": "^4.17.17", "@types/jasmine": "~5.1.0", "@types/node": "^18.18.0", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.2.2" } } ================================================ FILE: frontend/server.ts ================================================ import { APP_BASE_HREF } from '@angular/common'; import { CommonEngine } from '@angular/ssr'; import express from 'express'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import bootstrap from './src/main.server'; // The Express app is exported so that it can be used by serverless Functions. export function app(): express.Express { const server = express(); const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const indexHtml = join(serverDistFolder, 'index.server.html'); const commonEngine = new CommonEngine(); server.set('view engine', 'html'); server.set('views', browserDistFolder); // Example Express Rest API endpoints // server.get('/api/**', (req, res) => { }); // Serve static files from /browser server.get('*.*', express.static(browserDistFolder, { maxAge: '1y' })); // All regular routes use the Angular engine server.get('*', (req, res, next) => { const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap, documentFilePath: indexHtml, url: `${protocol}://${headers.host}${originalUrl}`, publicPath: browserDistFolder, providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], }) .then((html) => res.send(html)) .catch((err) => next(err)); }); return server; } function run(): void { const port = process.env['PORT'] || 4000; // Start up the Node server const server = app(); server.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); }); } run(); ================================================ FILE: frontend/src/app/agent-chat/agent-chat.component.html ================================================ adjust
Question Sent
Converted to SQL
{{msg?.generate_sql?.responseTime}}
Data retrieved
{{msg.run_query?.responseTime}}
Visualization generated
{{msg.visualize?.responseTime}}
{{msg.generate_sql.Error}}
{{msg.run_query?.NaturalResponse}}
Table View
{{col | uppercase}} {{element[col]}}

Source

{{dataSetName}}

Owner

OpenData QnA

Project ID

Demo Project

Grouping

{{dataSet}}

Is this what you were looking for?

================================================ FILE: frontend/src/app/agent-chat/agent-chat.component.scss ================================================ input { font-family: 'Google Sans'; font-style: normal; font-weight: 500; font-size: 14px; } mat-icon { color: blue; } table { width: 100%; } @media (max-width: 768px) { /* I need the code here */ .summarize-results { flex: 0 50%; } } .btnSave { border-color: transparent; min-width: 146px; height: 41px; background: #1A73E8; border-radius: 100px; font-family: 'Google Sans'; font-style: normal; font-weight: 500; font-size: 14px; line-height: 20px; display: flex; align-items: center; text-align: center; justify-content: center; letter-spacing: 0.25px; color: #FFFFFF; } .example-container mat-form-field+mat-form-field { margin-left: 8px; } .img-align { display: flex; width: 100%; flex-wrap: wrap; justify-content: center; } mat-table { border: 1px solid #D5D5D5; border-radius: 10px; } mat-cell, #dataSources mat-header-cell { border: none !important; } .query-ref { padding: 20px; font-size: 14px; font-family: Google Sans; color: #575757; } .mat-tab-body-content { overflow: hidden !important } .spinner-loading-result-table { margin: 10px; } ::ng-deep .custom-style { background-color: brown; color: white; border-radius: 10px; } .cursor-ref { cursor: pointer; } .h1-ref { font-size: 35px !important; font-family: Google Sans; font-weight: 600; text-align: center; margin-bottom: 50px; } .container-fluid { display: flex; flex-direction: column; } .wrap-background { background-color: #F3F6FC; border-radius: 25px; } .chart_prep { align-items: center; justify-content: center; display: flex; flex-direction: column; gap: 15px } .feedback-label { color: #525252; font-family: "Google Sans"; font-size: 16px; font-style: normal; font-weight: 500; line-height: 20px; letter-spacing: -0.154px; } .datasource-prep { width: 425px; height: 250px; border-radius: 20px; border: 1px solid #cdc8c8; display: flex; flex-direction: row; padding: 20px; flex-wrap: wrap; background-color: #FFF; } .right-text { width: 50%; font-family: Google Sans; font-size: 14px; font-style: normal; font-weight: 400; letter-spacing: 0.10000000149011612px; text-align: left; } .left-text { width: 50%; font-family: Google Sans; font-size: 14px; font-weight: 700; font-style: normal; letter-spacing: 0.5px; text-align: left; } .btns { display: flex; flex-direction: row; } @media (max-width: 768px) { .btns { flex-direction: column; } .btns .share { width: 20%; } .datasource-prep { width: auto; } .result-content { flex-direction: column; } } .copyBtn { border: none; float: right; background: transparent; } .table-responsive { display: block; width: 100%; overflow-x: auto; } .mat-table { width: 100%; max-width: 100%; margin-bottom: 1rem; display: table; border-collapse: collapse; margin: 0px; } .mat-row, .mat-header-row { display: table-row; } .mat-cell, .mat-header-cell { word-wrap: initial; display: table-cell; padding: 0px 5px; line-break: unset; width: auto; white-space: nowrap; overflow: hidden; vertical-align: middle; } table { border-radius: 25px; } table td, table th { padding: 2px 10px; font-size: 14px; font-family: Google Sans; } .result-content { display: flex; flex-direction: row; align-items: center; gap: 20px; overflow: auto; } .table-prep { border-radius: 20px; border: 1px solid #cdc8c8; padding-bottom: 20px; padding-top: 20px; margin-bottom: 15px; } .example-list { width: 100px; border: solid 1px #cdc8c8; border-radius: 5px; background: #fff; text-align: center; padding: 10px; margin: 0; } .feedback-ref { color: #8AB4F8; font-size: 14px; font-family: Google Sans; font-weight: 400; background-color: #FFF; border: 1px solid #ccc; padding: 3px; border-radius: 10px; margin: 2px; cursor: pointer; } .feedback-ref-clicked { color: #FFF; font-size: 14px; font-family: Google Sans; font-weight: 400; background-color: #1A73E8; border: 1px solid #ccc; padding: 3px; border-radius: 10px; margin: 2px; cursor: pointer; } .feedback-ref a { text-decoration: none; } .feedback-grad { background-image: radial-gradient(#AACAFF, #E7E7E7); border: 1px solid #ccc; } .feedback-button { text-decoration: none; font-family: Google Sans; font-size: 14px; border-radius: 12px; background: #5D93FF; color: #fff; width: 78px; height: 28px; margin-top: 10px; display: flex; justify-content: center; align-items: center; } .cdk-overlay-pane:has(.mat-mdc-select-panel) { width: auto !important; } .g-container { position: relative; } //-----chat------\\ .msg { border-radius: 10px; } .msg.right { background-color: #CCC; float: right; } .msg.left { background-color: #555; color: white; } .flex-expand { flex: 1 1 auto; } #messages { display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden; margin-top: 0; margin-bottom: 0; padding-left: 0 !important; list-style: none; } #messages li { list-style: none; margin-top: 1rem; } #messages img { width: 43px; height: auto; } .received { display: block; text-align: right; position: relative; } .user-feedback { width: 257px !important; height: 93px !important; border-radius: 16px; background: #FFF; margin-bottom: 0 !important; margin-left: 15px !important; } .user-feedback-title { color: #1F1F1F !important; font-family: "Google Sans"; font-size: 14px !important; font-style: normal; font-weight: 400 !important; line-height: 20px; margin-top: 20px !important; } mat-stepper { width: 824px; height: 128px; margin: 5px 20px 20px 15px; border-radius: 20px; } .sqlError { border-radius: 26px; background: #FFF; margin-bottom: 10px; width: 830px; color: #f78b8b !important; font-family: "Google Sans"; font-size: 14px; font-style: normal; font-weight: 400; line-height: 20px; margin-left: 20px; padding: 15px; min-height: 100px; display: flex; align-items: center; } .agentTab { margin-left: 15px; width: 830px; min-height: 300px; max-height: 600px; background: rgb(255, 255, 255); border-radius: 25px; align-items: center; overflow: auto; --mat-tab-animation-duration: 500ms; } mat-expansion-panel { box-shadow: none !important; } ================================================ FILE: frontend/src/app/agent-chat/agent-chat.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AgentChatComponent } from './agent-chat.component'; describe('AgentChatComponent', () => { let component: AgentChatComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AgentChatComponent] }) .compileComponents(); fixture = TestBed.createComponent(AgentChatComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/agent-chat/agent-chat.component.ts ================================================ import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild, signal } from '@angular/core'; import { HomeService } from '../shared/services/home.service'; import { format } from 'sql-formatter'; import { MatSnackBar } from '@angular/material/snack-bar'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { ChatService } from '../shared/services/chat.service'; @Component({ selector: 'app-agent-chat', templateUrl: './agent-chat.component.html', styleUrl: './agent-chat.component.scss' }) export class AgentChatComponent implements AfterViewInit { msg: any; @Input('example_user_question') example_user_question: any; @Input('ind') ind: any; emptyMsg: any = ''; @Input('suggestionList') suggestionList: any; showResult: boolean = false; private _destroy$ = new Subject(); showChart: boolean = false; showLoader: boolean = false; isOpen: boolean = false; selectedFeedbackOption: any; result: any; dataSet!: string; dataSetName!: string; displayedColumns: string[] = []; dataSource: any[] = []; feedbackForm: FormGroup = this.formBuilder.group({ feedbackCtrl: [''], }); resultLoader: boolean = false; @ViewChild("feedback") feedbackElement!: ElementRef; readonly panelOpenState = signal(false); constructor(public homeService: HomeService, private snackBar: MatSnackBar, private formBuilder: FormBuilder, public chatService: ChatService, private cdref: ChangeDetectorRef) { } ngOnInit() { // added this comment for test PR from frontend_dev this.homeService.knownSqlObservable?.pipe(takeUntil(this._destroy$)).subscribe((response: any) => { if (response && response != null) { this.suggestionList = JSON.parse(response); } }) this.dataSet = this.homeService.getSelectedDbGrouping(); this.dataSetName = this.homeService.getselectedDbName(); this.chatService.chatSessionObservable.pipe(takeUntil(this._destroy$)).subscribe((res) => { this.homeService.updateChatMsgs(res.chatMsgs) // this.msg = res.chatMsgs.at(this.ind) }) // this.msg = this.homeService.getChatMsgs().at(this.ind); this.showResult = true; } ngAfterContentChecked() { this.cdref.detectChanges(); } ngAfterViewInit() { this.msg = this.homeService.getChatMsgs().at(this.ind); this.feedbackElement?.nativeElement.scrollIntoView({ behavior: "smooth", block: "start" }); this.cdref.detectChanges(); } getResultforSql() { this.resultLoader = true; // Subscribe to the response data observable this.homeService.runQuery(this.msg?.generate_sql.GeneratedSQL, this.homeService.getSelectedDbGrouping(), this.msg?.user_question, this.homeService.getSessionId()) .pipe(takeUntil(this._destroy$)).subscribe((res: any) => { const data = JSON.parse(res.KnownDB); if (res && res.ResponseCode === 200) { if (data.length === 0) { this.emptyMsg = 'No data found'; } else { this.emptyMsg = ''; for (var obj in data) { if (data.hasOwnProperty(obj)) { for (var prop in data[obj]) { if (data[obj].hasOwnProperty(prop)) { if (this.displayedColumns.indexOf(prop) === -1) { this.displayedColumns.push(prop); } } } } } // console.log(this.ind) this.updateLocalMessage(this.ind, res, data); this.dataSource = data; } } else { this.updateLocalMessage(this.ind, res, data); this.emptyMsg = res?.Error; } this.resultLoader = false; }); } async tabClick(event: any, displayedColumns: any) { const tab = event?.tab?.textLabel; switch (tab) { case "Generated SQL": break; case "Result": if (!displayedColumns) { this.getResultforSql() }; break; case "Data Sources": break; } } visualizeBtn(msg: any, ind: any) { this.showLoader = true; let sql = format(msg.generate_sql.GeneratedSQL, { language: 'mysql' }) this.homeService.generateViz(this.msg.user_question, sql, msg.dataSource, this.homeService.getSessionId()).subscribe((res: any) => { const object = res.GeneratedChartjs; this.msg = { ...this.msg, "visualize": res } for (const [key, value] of Object.entries(object)) { let newvalue: string = (value as string).replace('chart_div', ind + '-chart_div'); this.onChange(newvalue, ind, key); } }) } onChange(value: any, ind: any, key: any) { this.showChart = true; this.showLoader = false; this.result = eval(value) } thumbsUp(sql: any, ind: any) { let chats = this.homeService.getChatMsgs(); const sqlExist = this.suggestionList.some((res: { example_user_question: any; example_generated_sql: any; }) => res.example_user_question === chats[ind]?.user_question && res.example_generated_sql === sql); if (!sqlExist) { // let concatedUserQuestionsList = chats.map((msg: any) => { // if (msg.author == 'user') { // return msg.user_question // } // }) // const concatenatedStr = concatedUserQuestionsList.filter((word) => word).reduce((accumulator, currentValue) => accumulator + ' , ' + currentValue); this.homeService.thumbsUp(sql, chats[ind]?.user_question, this.homeService.getSelectedDbGrouping(), this.homeService.getSessionId()).subscribe((res: any) => { if (res && res.ResponseCode === 201) { this.updateLocalMessage(ind, res, ""); this.showSnackbarCssStyles(res?.Message, 'Close', '10000') this.isOpen = true; } else { this.updateLocalMessage(ind, res, ""); this.showSnackbarCssStyles(res?.Error, 'Close', '10000') } }) } else { this.showSnackbarCssStyles('Data is present in the suggestion list', 'Close', '4000') } } updateLocalMessage(ind: any, res: any, data: any) { let localChatMsgs = this.msg; // Update message in case of result tab if (data) { localChatMsgs = { ...localChatMsgs, 'dataSource': data, 'displayedColumns': this.displayedColumns, 'dataSet': this.dataSet, 'run_query': res, 'ind': ind }; } // Update message in case of thumbsup else { localChatMsgs = { ...localChatMsgs, "embed_sql": res, 'ind': ind }; } this.msg = localChatMsgs; //this.homeService.updateChatMsgsAtIndex(localChatMsgs, this.ind); } showSnackbarCssStyles(content: any, action: any, duration: any) { let sb = this.snackBar.open(content, action, { duration: duration, panelClass: ["custom-style"] }); sb.onAction().subscribe(() => { sb.dismiss(); }); } showContentCopiedMsg() { this.showSnackbarCssStyles("Content Copied", 'Close', '4000') } closeFeedback() { this.isOpen = false; } thumbsDown() { this.isOpen = true; } submitFeedback(ind: any, comment: any) { this.isOpen = false; this.msg = { ...this.msg, "feedback": { "option": this.selectedFeedbackOption, "comment": comment } } } feedbackOption(val: any) { if (val == 0) { this.selectedFeedbackOption = 'Correct answer'; } else if (val == 1) { this.selectedFeedbackOption = 'Easy to understand' } else { this.selectedFeedbackOption = 'Quick results' } } ngOnDestroy() { this._destroy$.next(); } } ================================================ FILE: frontend/src/app/app-routing.module.ts ================================================ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LoginComponent } from './login/login.component'; import { UserJourneyComponent } from './user-journey/user-journey.component'; import { HomeComponent } from './home/home.component'; import { BusinessUserComponent } from './business-user/business-user.component'; const routes: Routes = [ { path: '', component: LoginComponent }, { path: 'user-journey', component: UserJourneyComponent }, { path: 'home-page', component: HomeComponent }, { path: 'business-mode', component: BusinessUserComponent }, ] @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } ================================================ FILE: frontend/src/app/app.component.html ================================================ ================================================ FILE: frontend/src/app/app.component.scss ================================================ ================================================ FILE: frontend/src/app/app.component.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], }).compileComponents(); }); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); it(`should have the 'genai-sa-ui-app' title`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app.title).toEqual('genai-sa-ui-app'); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('h1')?.textContent).toContain('Hello, genai-sa-ui-app'); }); }); ================================================ FILE: frontend/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.scss' }) export class AppComponent { title = 'data-engineering-ui-app'; config: any; constructor() {} ngOnInit() {} } ================================================ FILE: frontend/src/app/app.module.server.ts ================================================ import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { AppComponent } from './app.component'; import { AppModule } from './app.module'; @NgModule({ imports: [ AppModule, ServerModule, ], bootstrap: [AppComponent], }) export class AppServerModule {} ================================================ FILE: frontend/src/app/app.module.ts ================================================ import { CUSTOM_ELEMENTS_SCHEMA, NgModule, importProvidersFrom } from "@angular/core"; import { AppComponent } from "./app.component"; import { LoginComponent } from "./login/login.component"; import { LoginButtonComponent } from "./login-button/login-button.component"; import { LoginService } from "./shared/services/login.service"; import { SharedService } from "./shared/services/shared.service"; import { provideFirestore, initializeFirestore } from "@angular/fire/firestore"; import { getApp, initializeApp, provideFirebaseApp } from "@angular/fire/app"; import { BrowserModule } from "@angular/platform-browser"; import { UserJourneyComponent } from "./user-journey/user-journey.component"; import { AppRoutingModule } from "./app-routing.module"; import { HomeComponent } from "./home/home.component"; import { HeaderComponent } from "./header/header.component"; import { MatToolbarModule } from "@angular/material/toolbar"; import { MatIconModule } from "@angular/material/icon"; import { MatButtonModule } from "@angular/material/button"; import { RouterLink } from "@angular/router"; import { MatTabsModule } from "@angular/material/tabs"; import { MatDividerModule } from "@angular/material/divider"; import { MatSelectModule } from "@angular/material/select"; import { MatInputModule } from "@angular/material/input"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { CommonModule, NgFor, NgIf } from "@angular/common"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatExpansionModule } from '@angular/material/expansion'; import { MenuComponent } from "./menu/menu.component"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BusinessUserComponent } from "./business-user/business-user.component"; import { MatTableModule } from '@angular/material/table'; import { MatCardModule } from '@angular/material/card'; import { HTTP_INTERCEPTORS, HttpClientModule, provideHttpClient, withFetch } from '@angular/common/http'; import { HomeService } from './shared/services/home.service'; import { provideAuth, getAuth } from '@angular/fire/auth'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { ClipboardModule } from '@angular/cdk/clipboard'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { NgChartsModule } from 'ng2-charts'; import { AngularFireModule } from '@angular/fire/compat'; import { AngularFirestoreModule } from '@angular/fire/compat/firestore'; import { AngularFireAuth, AngularFireAuthModule } from '@angular/fire/compat/auth'; import { UserPhotoComponent } from "./user-photo/user-photo.component"; import { PrismComponent } from "./prism/prism.component"; import 'prismjs/components/prism-sql'; import { MatPaginatorModule } from "@angular/material/paginator"; import { OverlayModule } from "@angular/cdk/overlay"; import { GoogleChartsModule } from "angular-google-charts"; import { MatRadioModule } from '@angular/material/radio'; import { MatStepperModule } from '@angular/material/stepper'; import { STEPPER_GLOBAL_OPTIONS } from "@angular/cdk/stepper"; import { AgentChatComponent } from "./agent-chat/agent-chat.component"; import { AppHttpInterceptor } from "./http.interceptor"; import { firebaseConfig, FIRESTORE_DATABASE_ID } from "../assets/constants"; import { MatTreeModule } from "@angular/material/tree"; import { ScenarioListComponent } from "./scenario-list/scenario-list.component"; @NgModule({ declarations: [ AppComponent, LoginComponent, LoginButtonComponent, UserJourneyComponent, UserPhotoComponent, HomeComponent, HeaderComponent, MenuComponent, BusinessUserComponent, PrismComponent, AgentChatComponent, ScenarioListComponent ], imports: [ CommonModule, BrowserModule, ReactiveFormsModule, FormsModule, BrowserAnimationsModule, AppRoutingModule, MatToolbarModule, MatIconModule, MatButtonModule, RouterLink, MatTabsModule, MatDividerModule, NgIf, NgFor, MatSelectModule, MatInputModule, MatFormFieldModule, MatAutocompleteModule, MatListModule, MatSidenavModule, MatTableModule, MatExpansionModule, MatCardModule, HttpClientModule, MatProgressSpinnerModule, MatSnackBarModule, ClipboardModule, MatSlideToggleModule, NgChartsModule, AngularFireModule.initializeApp(firebaseConfig), AngularFirestoreModule, AngularFireAuthModule, MatPaginatorModule, OverlayModule, GoogleChartsModule, MatRadioModule, MatStepperModule, MatExpansionModule, MatTreeModule ], providers: [ { provide: STEPPER_GLOBAL_OPTIONS, useValue: { displayDefaultIndicatorType: false } }, { provide: HTTP_INTERCEPTORS, useClass: AppHttpInterceptor, multi: true }, provideHttpClient(withFetch()), importProvidersFrom([ provideFirebaseApp(() => initializeApp(firebaseConfig)), provideFirestore(() => { const app = getApp(); const providedFirestore = initializeFirestore(app, {}, FIRESTORE_DATABASE_ID); return providedFirestore; }), provideAuth(() => getAuth()), LoginService, SharedService, HomeService, AngularFireAuth ]), ], bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class AppModule { } ================================================ FILE: frontend/src/app/business-user/business-user.component.html ================================================
Access restricted: you dont have sufficient permissions to access this data source
follow up query
================================================ FILE: frontend/src/app/business-user/business-user.component.scss ================================================ input { font-family: 'Google Sans'; font-style: normal; font-weight: 500; font-size: 14px; } .name { border-radius: 25px; border: 1px solid #E9E9E9; background: #FFF; box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.15) inset; width: 925px; height: 48px; flex-shrink: 0; font-family: Google Sans; font-size: 14px; font-style: normal; font-weight: 500; line-height: normal; flex: 1 0 8em; margin: 5px; /* 8em * 16px = 128px */ } .name-err { border-radius: 25px; border: 1px solid red; background: #FFF; box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.15) inset; width: 925px; height: 48px; flex-shrink: 0; font-family: Google Sans; font-size: 14px; font-style: normal; font-weight: 500; line-height: normal; margin: 5px; } .input-flex { display: flex; flex-wrap: wrap; padding: 15px 10px } .suggestion-ref { border-radius: 11.5px; background: #D3E3FD; } .summarize-results { margin-top: 10px; } @media (max-width: 768px) { /* I need the code here */ .summarize-results { flex: 0 50%; } } ::ng-deep .custom-style { background-color: brown; color: white; border-radius: 10px; } .followUp { cursor: pointer; flex: 0 1 8em; } .container-fluid { display: flex; flex-direction: column; height: 100%; } .insight-results-err { font-family: Google Sans; font-size: 14px; } .cdk-overlay-pane:has(.mat-mdc-select-panel) { width: auto !important; } /*-----chat messages------*/ .chat-body { overflow-y: auto; position: relative; flex: 1 1 auto; border: 0; margin: 5px; vertical-align: baseline; background: #F3F6FC; border-radius: 25px; display: flex; flex-direction: column; justify-content: space-between; } .msg { border-radius: 10px; } .msg.right { background-color: #CCC; float: right; } .msg.left { background-color: #555; color: white; } .flex-expand { flex: 1 1 auto; } #messages { display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden; margin-top: 0; margin-bottom: 0; padding-left: 0 !important; list-style: none; } #messages li { list-style: none; margin-top: 1rem; } #messages img { width: 43px; height: auto; } .received { display: block; text-align: right; position: relative; } li.received p { padding: 5px 10px; display: inline-block; border-radius: 20px; margin-bottom: 0; color: #000; font-feature-settings: 'clig' off, 'liga' off; font-family: "Google Sans"; font-size: 14px; font-style: normal; font-weight: 400; line-height: 20px; letter-spacing: -0.154px; border-radius: 11.5px; border: 1px solid #B6CEFF; background: #DBE7FF; max-width: 500px; text-align: left; } li.sent { display: block; text-align: left; position: relative; color: #000; font-feature-settings: 'clig' off, 'liga' off; font-family: "Google Sans"; font-size: 14px; font-style: normal; font-weight: 400; line-height: 20px; letter-spacing: -0.154px; background: #F3F6FC; } li.sent div { border-radius: 26px; background: #FFF; margin-bottom: 10px; width: 520px; } li.sent p, a, img { color: #000; font-size: 14px; line-height: 1.5; font-weight: 400; padding: 5px 10px; display: inline-block; border-radius: 20px; } li.sent:after { display: block; content: ''; clear: both; } .caption_span { color: var(--grey, #575757); text-align: right; font-feature-settings: 'clig' off, 'liga' off; font-family: "Google Sans"; font-size: 10px; font-style: normal; font-weight: 400; line-height: 20px; letter-spacing: -0.154px; padding-left: 10px; } li.sent .caption_span { margin-right: 15px; color: var(--grey, #575757); font-feature-settings: 'clig' off, 'liga' off; font-family: "Google Sans"; font-size: 10px; font-style: normal; font-weight: 400; line-height: 20px; letter-spacing: -0.154px; } /*-----chat messages------*/ .welcomeTitle { color: #000; font-family: 'Google Sans'; font-size: 25px !important; font-style: normal; font-weight: 700 !important; line-height: 20px; letter-spacing: 0.25px; padding: 10px; width: 500px } .userMsg { color: #000; font-feature-settings: 'clig' off, 'liga' off; font-family: 'Google Sans'; font-size: 16px; font-style: normal; font-weight: 400; line-height: 22px; } .promptTitle { color: #525252; font-feature-settings: "clig" off, "liga" off; font-family: "Google Sans"; font-size: 16px; font-style: normal; font-weight: 400; line-height: 20px; padding-left: 10px; } /* Progress spinner css */ .meter { height: 18px; overflow: hidden; margin-bottom: 12px; border-radius: 10px; display: block; background: transparent; } .meter span { display: block; height: 100%; background: transparent; } .progress1 { transition: width 2s; border-radius: 10.5px; animation: progressBar1 7s linear 5s alternate; animation-delay: 0.1s; background: linear-gradient(80deg, rgba(66, 133, 244, 0.50)0%, rgba(198, 214, 253, 0.60) 41.37%, rgba(230, 222, 255, 0.70) 81.92%); animation-fill-mode: forwards; } .progress2 { transition: width 2s; border-radius: 10.5px; animation: progressBar1 10s linear 5s alternate; animation-delay: 1.5s; background: linear-gradient(100deg, rgba(66, 133, 244, 0.50)0%, rgba(198, 214, 253, 0.60) 41.37%, rgba(230, 222, 255, 0.70) 81.92%); animation-fill-mode: forwards; } .progress3 { transition: width 2s; border-radius: 10.5px; animation: progressBar1 5s linear 5s infinite alternate; animation-delay: 2.5s; animation-timing-function: ease-in-out; background: linear-gradient(100deg, rgba(66, 133, 244, 0.50)0%, rgba(198, 214, 253, 0.60) 41.37%, rgba(230, 222, 255, 0.70) 81.92%); animation-fill-mode: forwards; } @keyframes progressBar1 { from { background: linear-gradient(100deg, rgba(230, 222, 255, 0.70) 0%, rgba(198, 214, 253, 0.60) 41.37%, rgba(66, 133, 244, 0.50) 81.92%); border-radius: 10.5px; opacity: 0.2 } to { background: linear-gradient(100deg, rgba(66, 133, 244, 0.50)0%, rgba(198, 214, 253, 0.60) 41.37%, rgba(230, 222, 255, 0.70) 81.92%); border-radius: 10.5px; } } /*************************************************** * Generated by SVG Artista on 5/27/2024, 4:09:34 PM * MIT license (https://opensource.org/licenses/MIT) * W. https://svgartista.net **************************************************/ @-webkit-keyframes animate-svg-stroke-1 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @keyframes animate-svg-stroke-1 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @-webkit-keyframes animate-svg-fill-1 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } @keyframes animate-svg-fill-1 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } .svg-elem-1 { -webkit-animation: animate-svg-stroke-1 2s ease 0.8s both, animate-svg-fill-1 2s ease 0.8s both; animation: animate-svg-stroke-1 2s ease 0.8s both, animate-svg-fill-1 2s ease 0.8s both; } @-webkit-keyframes animate-svg-stroke-2 { 0% { stroke-dashoffset: 24.627429962158203px; stroke-dasharray: 24.627429962158203px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 24.627429962158203px; } } @keyframes animate-svg-stroke-2 { 0% { stroke-dashoffset: 24.627429962158203px; stroke-dasharray: 24.627429962158203px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 24.627429962158203px; } } .svg-elem-2 { -webkit-animation: animate-svg-stroke-2 2s ease 1.8s both, animate-svg-fill-2 2s ease 1.8s both; animation: animate-svg-stroke-2 2s ease 1.8s both, animate-svg-fill-2 2s ease 1.8s both; } @-webkit-keyframes animate-svg-stroke-3 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @keyframes animate-svg-stroke-3 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @-webkit-keyframes animate-svg-fill-3 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } @keyframes animate-svg-fill-3 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } .svg-elem-1 { -webkit-animation: animate-svg-stroke-1 2s ease 0.8s both, animate-svg-fill-1 2s ease 0.8s both; animation: animate-svg-stroke-1 2s ease 0.8s both, animate-svg-fill-1 2s ease 0.8s both; } .svg-elem-3 { -webkit-animation: animate-svg-stroke-3 2s ease 2.8s both, animate-svg-fill-3 2s ease 2.8s both; animation: animate-svg-stroke-3 2s ease 2.8s both, animate-svg-fill-3 2s ease 2.8s both; } @-webkit-keyframes animate-svg-stroke-4 { 0% { stroke-dashoffset: 17.556342124938965px; stroke-dasharray: 17.556342124938965px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 17.556342124938965px; } } @keyframes animate-svg-stroke-4 { 0% { stroke-dashoffset: 17.556342124938965px; stroke-dasharray: 17.556342124938965px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 17.556342124938965px; } } .svg-elem-4 { -webkit-animation: animate-svg-stroke-4 2s ease 3.8s both, animate-svg-fill-4 2s ease 3.8s both; animation: animate-svg-stroke-4 2s ease 3.8s both, animate-svg-fill-4 2s ease 3.8s both; } @-webkit-keyframes animate-svg-stroke-5 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @keyframes animate-svg-stroke-5 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @-webkit-keyframes animate-svg-fill-5 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } @keyframes animate-svg-fill-5 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } .svg-elem-5 { -webkit-animation: animate-svg-stroke-5 2s ease 4.8s both, animate-svg-fill-5 2s ease 4.8s both; animation: animate-svg-stroke-5 2s ease 4.8s both, animate-svg-fill-5 2s ease 4.8s both; } @-webkit-keyframes animate-svg-stroke-6 { 0% { stroke-dashoffset: 17.55635643005371px; stroke-dasharray: 17.55635643005371px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 17.55635643005371px; } } @keyframes animate-svg-stroke-6 { 0% { stroke-dashoffset: 17.55635643005371px; stroke-dasharray: 17.55635643005371px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 17.55635643005371px; } } .svg-elem-6 { -webkit-animation: animate-svg-stroke-6 2s ease 5.8s both, animate-svg-fill-6 2s ease 5.8s both; animation: animate-svg-stroke-6 2s ease 5.8s both, animate-svg-fill-6 2s ease 5.8s both; } @-webkit-keyframes animate-svg-stroke-7 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @keyframes animate-svg-stroke-7 { 0% { stroke-dashoffset: 30.274333882308138px; stroke-dasharray: 30.274333882308138px; } 100% { stroke-dashoffset: 0; stroke-dasharray: 30.274333882308138px; } } @-webkit-keyframes animate-svg-fill-7 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } @keyframes animate-svg-fill-7 { 0% { fill: transparent; } 100% { fill: rgb(66, 133, 244); } } .svg-elem-7 { -webkit-animation: animate-svg-stroke-7 2s ease 6.8s both, animate-svg-fill-7 2s ease 6.8s both; animation: animate-svg-stroke-7 2s ease 6.8s both, animate-svg-fill-7 2s ease 6.8s both; } /* Progress spinner css */ ================================================ FILE: frontend/src/app/business-user/business-user.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BusinessUserComponent } from './business-user.component'; describe('BusinessUserComponent', () => { let component: BusinessUserComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [BusinessUserComponent] }) .compileComponents(); fixture = TestBed.createComponent(BusinessUserComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/business-user/business-user.component.ts ================================================ import { Component, ChangeDetectorRef, Output, EventEmitter, Input, SimpleChanges, inject } from '@angular/core'; import { LoginService } from '../shared/services/login.service'; import { FormControl, FormGroup } from '@angular/forms'; import { HomeService } from '../shared/services/home.service'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Subject, Subscription, takeUntil } from 'rxjs'; import { Router } from '@angular/router'; import { GroupingModalComponent } from '../grouping-modal/grouping-modal.component'; import { MatDialog } from '@angular/material/dialog'; import { ChatService } from '../shared/services/chat.service'; export interface Tabledata { city_id: string; } @Component({ selector: 'app-business-user', templateUrl: './business-user.component.html', styleUrl: './business-user.component.scss' }) export class BusinessUserComponent { @Input('checkSideNav') checkSideNav: any @Input("selectedHistory") selectedHistory!: any; @Input('selectedGrouping') selectedGrouping: any @Input('userSessions') userSessions!: string; currentScenario: any; chatMsgs: any[] = []; userLoggedIn: boolean = false; photoURL: any; resultLoader: boolean = false; isSuggestions: boolean = true; suggestionList: any; showResult: boolean = false; generatedSql: any = { example_user_question: '', example_generated_sql: '', unfilteredSql: '' }; @Output() updateStyleEvent = new EventEmitter(); setErrorCSS: boolean = false; readonly dialog = inject(MatDialog); isOpen: boolean = false; dataSet: string | undefined; dataSetName!: string; userId: any; private _destroy$ = new Subject(); sessionId !: string; subscription!: Subscription; sub!: Subscription; //ind: number = 0; constructor(public loginService: LoginService, public homeService: HomeService, public chatService: ChatService, private snackBar: MatSnackBar, private change: ChangeDetectorRef, public router: Router) { this.loginService.getUserDetails().pipe(takeUntil(this._destroy$)).subscribe((res: any) => { this.userId = res.uid; this.userLoggedIn = true; this.photoURL = res?.photoURL }); } sqlSearchForm = new FormGroup({ name: new FormControl(), }); ngOnChanges(changes: SimpleChanges) { for (const propName in changes) { if (changes.hasOwnProperty(propName)) { switch (propName) { case 'selectedHistory': { if (this.selectedHistory) { this.showResult = false; this.sessionId = this.homeService.getSessionId() } } break; } } } } ngOnInit() { this.sessionId = ''; this.loadInitialChat(); } reloadComponent(self: boolean, urlToNavigateTo?: string) { //skipLocationChange:true means dont update the url to / when navigating console.log("Current route I am on:", this.router.url); const url = self ? this.router.url : urlToNavigateTo; this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { this.router.navigate([`/${url}`]).then(() => { console.log(`After navigation I am on:${this.router.url}`) }) }) } loadInitialChat() { this.chatService.chatSessionObservable.pipe(takeUntil(this._destroy$)).subscribe((res) => { this.chatMsgs = res.chatMsgs this.dataSet = this.homeService.getSelectedDbGrouping(); this.dataSetName = this.homeService.getselectedDbName(); this.sub = this.chatService.agentResponseLoader$.pipe(takeUntil(this._destroy$)).subscribe((res) => { this.resultLoader = res }) this.sessionId = this.homeService.getSessionId() }) } followUp(query: any, event?: any) { if (this.dataSet) { event?.preventDefault(); if (this.sqlSearchForm.controls.name?.value !== null) { this.showResult = false; this.chatService.addQuestion(query, this.userId, "followup") this.sqlSearchForm.controls['name'].setValue(""); this.chatService.agentResponseLoader.next(true) this.resultLoader = true; // this.generate_sql(query); } else { this.setErrorCSS = true; } } else { let dialogRef = this.dialog.open(GroupingModalComponent, { disableClose: true, width: '450px', }); dialogRef.afterClosed().subscribe(result => { }); } } suggestionResult(selectedsql: any) { this.showResult = true; this.chatService.addQuestion(selectedsql.example_user_question, this.userId, "followup") this.resultLoader = true; this.generatedSql.example_user_question = selectedsql.example_user_question; this.sqlSearchForm.controls['name'].setValue(""); this.updateStyleEvent.emit(this.showResult); //this.change.markForCheck(); } showSnackbarCssStyles(content: any, action: any, duration: any) { let sb = this.snackBar.open(content, action, { duration: duration, panelClass: ["custom-style"] }); sb.onAction().subscribe(() => { sb.dismiss(); }); } updateStyleItem(value: boolean) { this.updateStyleEvent.emit(value); } showContentCopiedMsg() { this.showSnackbarCssStyles("Content Copied", 'Close', '4000') } ngOnDestroy() { this.chatMsgs = []; //this.subscription.unsubscribe(); this._destroy$.next(); this.sessionId = ""; this.sub.unsubscribe() } } ================================================ FILE: frontend/src/app/grouping-modal/grouping-modal.component.html ================================================ ================================================ FILE: frontend/src/app/grouping-modal/grouping-modal.component.scss ================================================ .grouping-msg { font-family: "Google Sans"; font-size: "16px"; color : #d93035 } .closeBtn { font-family: "Google Sans"; font-size: "14px"; border: transparent; width: 100px; border-radius: 10px; } ::ng-deep .mat-dialog-container { border-radius: 40px !important; } .popup{ height: 110px; display: flex; justify-content: space-evenly; align-items: center; flex-direction: column; } ================================================ FILE: frontend/src/app/grouping-modal/grouping-modal.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GroupingModalComponent } from './grouping-modal.component'; describe('GroupingModalComponent', () => { let component: GroupingModalComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [GroupingModalComponent] }) .compileComponents(); fixture = TestBed.createComponent(GroupingModalComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/grouping-modal/grouping-modal.component.ts ================================================ import { Component } from '@angular/core'; import { MatDialogRef } from '@angular/material/dialog'; @Component({ selector: 'app-grouping-modal', standalone: true, imports: [], templateUrl: './grouping-modal.component.html', styleUrl: './grouping-modal.component.scss' }) export class GroupingModalComponent { constructor(public dialogRef: MatDialogRef) { } closeDialog() { this.dialogRef.close(); } } ================================================ FILE: frontend/src/app/header/header.component.html ================================================ Open Data QnA ================================================ FILE: frontend/src/app/header/header.component.scss ================================================ .title { color: #000; font-family: Google Sans; font-size: 20px; font-style: normal; font-weight: 400; line-height: normal; } ================================================ FILE: frontend/src/app/header/header.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HeaderComponent } from './header.component'; describe('HeaderComponent', () => { let component: HeaderComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HeaderComponent] }) .compileComponents(); fixture = TestBed.createComponent(HeaderComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/header/header.component.ts ================================================ import { Component } from '@angular/core'; @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrl: './header.component.scss' }) export class HeaderComponent { constructor() { } ngOnInit() { } } ================================================ FILE: frontend/src/app/home/home.component.html ================================================
Cymbal Customer Service Logo Cymbal Text Logo
================================================ FILE: frontend/src/app/home/home.component.scss ================================================ h1 { padding: 0 1rem; } h2 { padding: 1rem; } mat-toolbar { position: fixed; top: 0; z-index: 2; } // Move the content down so that it won't be hidden by the toolbar .content { padding-top: 3.5rem; @media screen and (min-width: 600px) { padding-top: 4rem; } } .flex-expand { flex: 1 1 auto; } /* Styles for tab labels */ ::ng-deep .mat-mdc-tab.mdc-tab--active .mdc-tab__text-label { color: #4D88FF !important; text-align: center; font-feature-settings: 'clig' off, 'liga' off; font-family: Google Sans; font-size: 16px; font-style: normal; font-weight: 500; line-height: 20px; } /* Styles for the active tab label */ ::ng-deep .mat-mdc-tab .mdc-tab__text-label { text-align: center; font-feature-settings: 'clig' off, 'liga' off; font-family: Google Sans; font-size: 16px; font-style: normal; font-weight: 500; line-height: 20px; letter-spacing: -0.154px; } /* Styles for the ink bar */ ::ng-deep .mat-mdc-tab.mdc-tab--active .mdc-tab-indicator__content--underline { stroke-width: 3px; stroke: #4D88FF !important; border-color: #4D88FF !important } ::ng-deep .mat-form-field.mat-focused .mat-form-field-underline { display: none; } .mat-form-field-appearance-legacy .mat-form-field-underline { background-color: blue; } .mat-mdc-form-field-focus-overlay { background-color: red; } .mdc-text-field--filled:not(.mdc-text-field--disabled) { background-color: red } ::ng-deep .mat-form-field-underline { display: none; } mat-form-field mat-select { border: none; } .mdc-text-field--outlined { border-color: red; } optgroup { font-size: 40px; } .mr-10 { margin-right: 7rem !important; } mat-sidenav-container { height: 100%; position: absolute; } // Move the content down so that it won't be hidden by the toolbar mat-sidenav { padding-top: 3.5rem; @media screen and (min-width: 600px) { padding-top: 4rem; } .entry { display: flex; align-items: center; gap: 1rem; padding: 0.75rem; } } // Move the content down so that it won't be hidden by the toolbar mat-sidenav-content { padding-top: 3.5rem; @media screen and (min-width: 600px) { padding-top: 4rem; } } .no-border { border: none; } ::ng-deep mat-expansion-panel-body { padding: 0 !important } ::ng-deep mat-expansion-panel-header { padding: 0 1px 0 0px !important } .toolbar { border-bottom: 1px solid #ebe6e6; background: #FFF; border-radius: 24px } ================================================ FILE: frontend/src/app/home/home.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HomeComponent } from './home.component'; describe('HomeComponent', () => { let component: HomeComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HomeComponent] }) .compileComponents(); fixture = TestBed.createComponent(HomeComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/home/home.component.ts ================================================ import { Component, ViewChild } from '@angular/core'; import { FormControl } from '@angular/forms'; import { HomeService } from '../shared/services/home.service'; import { ThemePalette } from '@angular/material/core'; import { MatSidenav } from '@angular/material/sidenav'; import { BreakpointObserver } from '@angular/cdk/layout'; import { LoginService } from '../shared/services/login.service'; import { Router } from '@angular/router'; import { Subject, Subscription, take, takeUntil } from 'rxjs'; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrl: './home.component.scss', }) export class HomeComponent { title = 'material-responsive-sidenav'; isCollapsed = true; groupingsListCtrl = new FormControl(''); private _destroy$ = new Subject(); groupingsList: any; groupingString: any; checkStyle: boolean | undefined; userType: String | undefined; checkSideNav: string = 'New Query'; @ViewChild(MatSidenav) sidenav!: MatSidenav; isMobile = true; selectedGrouping: any; photoURL: any; reloadComp: boolean = false; userId: any; userSessions: any = []; userHistory: any = []; Subscription!: Subscription selectedHistory: any; selectedScenario: any; constructor(private homeService: HomeService, private observer: BreakpointObserver, private _router: Router, private loginService: LoginService) { this.loginService.getUserDetails().subscribe(message => { this.userId = message.uid; this.photoURL = message?.photoURL }); } async ngOnInit() { if (!this.photoURL) { this._router.navigate(['']); } this.observer.observe(['(max-width: 800px)']).subscribe((screenSize) => { if (screenSize.matches) { this.isMobile = true; } else { this.isMobile = false; } }); if (this.userId) { this.Subscription = this.homeService.getUserSessions(this.userId) .pipe(takeUntil(this._destroy$)) .subscribe({ next: (res: any) => { this.userSessions = res; }, error: (error: any) => { throw error; }, complete: () => { //console.log("complete") } }) } this.groupingValAndKnownSql() } groupingValAndKnownSql() { this.homeService.setSelectedDbGrouping(""); this.groupingString = this.homeService.getAvailableDBList(); if (this.groupingString !== null && this.groupingString !== undefined) { this.groupingsList = JSON.parse(this.groupingString); this.homeService.currentSelectedGrouping.next(''); this.homeService.currentSelectedGroupingObservable.subscribe((res) => { this.groupingsListCtrl.setValue(res); if (!res) { this.homeService.sqlSuggestionList("", "").subscribe((data: any) => { if (data && data.ResponseCode === 200) { this.homeService.knownSqlFromDb.next(data.KnownSQL); } }) } }) } else { this.homeService.getAvailableDatabases().subscribe((res: any) => { if (res && res.ResponseCode === 200) { this.groupingsList = JSON.parse(res.KnownDB); } }); } } changeDb(dbtype: any) { let selectedDbtype = dbtype.target.value.split("-"); this.homeService.setSelectedDbGrouping(dbtype.target.value); this.homeService.setSessionId(''); this.homeService.setselectedDbName(selectedDbtype[1]) this.homeService.currentSelectedGrouping.next(dbtype.target.value) this.homeService.sqlSuggestionList(dbtype.target.value, selectedDbtype[1]).subscribe((data: any) => { if (data && data.ResponseCode === 200) { this.homeService.knownSqlFromDb.next(data.KnownSQL); } }) } updateBackgroundStyle(data: boolean) { this.checkStyle = data; } checkSideNavTAb(data: any) { if (data == 'New Query') { this.reloadComp = true; } this.checkSideNav = data; } sendHistory(data: any) { this.selectedHistory = data } toggleMenu() { if (this.isMobile) { this.sidenav.toggle(); } else { // do nothing for now } } ngOnDestroy() { this.userHistory = []; this.Subscription?.unsubscribe(); this._destroy$.next() } } ================================================ FILE: frontend/src/app/http.interceptor.ts ================================================ import { Injectable } from "@angular/core"; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http"; import { Observable, tap } from "rxjs"; import { LoginService } from "./shared/services/login.service"; @Injectable() export class AppHttpInterceptor implements HttpInterceptor { userDetails: any; idToken!: string; constructor(public loginService: LoginService) { this.loginService.getUserDetails().subscribe((res: any) => { this.userDetails = res }); this.loginService.getIdToken().subscribe((res: string) => { this.idToken = res }); } intercept(req: HttpRequest, next: HttpHandler): Observable> { req = req.clone({ headers: req.headers.append('Content-Type', 'application/x-www-form-urlencoded') }); req = req.clone({ headers: req.headers.append('Access-Control-Allow-Origin', '*') }); req = req.clone({ headers: req.headers.append('Authorization', `Bearer ${this.userDetails?.accessToken || this.idToken}`) }); const started = Date.now(); return next.handle(req).pipe(tap((event: any) => { const elapsed = Date.now() - started; //console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`); if (event instanceof HttpResponse) { let responseTime = elapsed / 1000 event.body.responseTime = `${responseTime} s` return event.body }; }) ) } } ================================================ FILE: frontend/src/app/login/login.component.html ================================================ ================================================ FILE: frontend/src/app/login/login.component.scss ================================================ .search { color: #FFFFFF; } .login-user { float: right; padding-right: 20px; } .login-container .mat-mdc-dialog-container .mdc-dialog__surface { border-radius: 44px !important; } .login-btn { position: absolute; left: 1400px; top: 30px; background: #F7F2FA; box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); border-radius: 100px; display: flex; flex-direction: row; justify-content: center; align-items: center; padding: 10px 24px; gap: 8px; flex: none; order: 0; align-self: stretch; flex-grow: 1; width: 39px; height: 20px; font-family: 'Google Sans'; font-style: normal; font-weight: 500; font-size: 14px; line-height: 20px; letter-spacing: 0.1px; color: #6750A4; } .login-page-background { background-image: radial-gradient(grey, black) !important; min-width: 100%; min-height: 100% } .title { /* Customer Service Modernization */ padding-left: 75px; color: #FFFFFF; width: 1328px; height: 115px; left: 159px; top: 280px; font-family: 'Google Sans'; font-style: normal; font-weight: 700; font-size: 61px; line-height: 100px; /* or 102% */ display: flex; align-items: center; letter-spacing: 0.5px; margin-top: 77px; } .demo { padding-left: 77px; width: 176px; height: 40px; left: 159px; margin-top: -25px; font-family: 'Google Sans'; font-style: normal; font-weight: 700; font-size: 40px; line-height: 40px; /* identical to box height, or 62% */ display: flex; align-items: center; letter-spacing: 0.5px; color: #FFFFFF; } .userJourney { padding-left: 75px; height: 40px; left: 159px; margin-top: 22px; font-family: 'Google Sans'; font-style: normal; font-weight: 400; font-size: 24px; line-height: 40px; display: flex; align-items: center; letter-spacing: 0.5px; color: #FFFFFF; } .desc { padding-left: 75px; color: #FFFFFF; width: 959px; left: 164px; margin-top: 23px; font-family: 'Google Sans'; font-style: normal; font-weight: 400; font-size: 18px; line-height: 20px; } .check { padding-left: 75px; left: 194px; top: 1051px; font-family: 'Google Sans'; font-style: normal; font-weight: 700; font-size: 18px; line-height: 20px; color: #FFFFFF; } .checkboxCss { left: 164px; top: 1049px; padding-left: 75px; left: 0%; right: 0%; top: 0%; bottom: 0%; font-family: 'Google Symbols'; font-style: normal; font-weight: 400; font-size: 24px; line-height: 50px; align-items: center; text-align: center; color: #FFFFFF; } .acceptButton { gap: 10px; margin-left: 70px; left: 164px; top: 1114px; background: #1A73E8; border-radius: 4px; width: 200px; height: 40px; font-family: "Google Sans"; font-style: normal; font-weight: 500; font-size: 18px; line-height: 20px; display: flex; align-items: center; text-align: center; justify-content: center; letter-spacing: 0.25px; flex: none; order: 0; flex-grow: 0; color: #FFFFFF; border: none !important; } //sign in background gray ::ng-deep .cdk-overlay-backdrop.cdk-overlay-backdrop-showing { opacity: 70 !important; background: rgba(0, 0, 0, 0.70) } ================================================ FILE: frontend/src/app/login/login.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LoginComponent } from './login.component'; describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [LoginComponent] }) .compileComponents(); fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/login/login.component.ts ================================================ import { Component, ElementRef } from '@angular/core'; import { LoginButtonComponent } from '../login-button/login-button.component'; import { Subscription } from 'rxjs'; import { Router } from '@angular/router'; import { Dialog } from '@angular/cdk/dialog'; import { LoginService } from '../shared/services/login.service'; import { MatSnackBar } from '@angular/material/snack-bar'; @Component({ // standalone: true, selector: 'app-login', templateUrl: './login.component.html', styleUrl: './login.component.scss' }) export class LoginComponent { photoURL: string | undefined; subscription: Subscription | undefined; acceptAndAgreeButton: boolean = true; loginError = false; loginErrorMessage: any constructor(private _router: Router, public loginService: LoginService, public dialog: Dialog, public snackbar: MatSnackBar) { this.loginService.getLoginError().subscribe((res: any) => { this.loginErrorMessage = res this.loginError = true; }); this.subscription = this.loginService.getUserDetails().subscribe(res => { this.userLoggedIn = true; this.photoURL = res?.photoURL }); } ngAfterViewInit() { if (!this.photoURL) { this.showLogIn() } } userLoggedIn: boolean = false; navigateToUserJourney() { if (!this.loginError) { this.userLoggedIn = true; this._router.navigate(['user-journey']); } else { this.showSnackbarCssStyles(this.loginErrorMessage, 'Close', 10000); } } showLogIn(): void { const dialogRef = this.dialog.open(LoginButtonComponent, { disableClose: true, width: '350px', panelClass: 'login-container' }); } checkboxChecked(event: any) { if (event.target.checked) { this.acceptAndAgreeButton = false } else { this.acceptAndAgreeButton = true } } showSnackbarCssStyles(content: any, action: any, duration: any) { let sb = this.snackbar.open(content, action, { duration: duration, panelClass: ["custom-style"] }); sb.onAction().subscribe(() => { sb.dismiss(); }); } } ================================================ FILE: frontend/src/app/login-button/login-button.component.html ================================================ ================================================ FILE: frontend/src/app/login-button/login-button.component.scss ================================================ .login-btn { align-items: center; margin-left: 75px; margin-top: 110px; /* label-text */ width: 90px; height: 36px; /* Auto layout */ display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 0px; gap: 8px; position: relative; width: 87px; height: 40px; /* M3/sys/light/surface-container-low */ background: #F7F2FA; /* M3/Elevation Light/1 */ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); border-radius: 100px; /* state-layer */ /* Auto layout */ display: flex; flex-direction: row; justify-content: center; align-items: center; padding: 10px 24px; gap: 8px; width: 87px; height: 40px; /* Inside auto layout */ flex: none; order: 0; align-self: stretch; flex-grow: 1; /* label-text */ /* M3/label/large */ font-family: 'Google Sans'; font-style: normal; font-weight: 500; font-size: 14px; line-height: 20px; /* identical to box height, or 143% */ display: flex; align-items: center; text-align: center; letter-spacing: 0.1px; /* M3/sys/light/primary */ color: #6750A4; /* Inside auto layout */ flex: none; order: 0; flex-grow: 0; } .popup { /* Rectangle 6452 */ height: 110px; display: flex; justify-content: space-evenly; align-items: center; flex-direction: column; } .signin-msg { font-family: "Google Sans"; font-size: "16px"; } .gsi-material-button { -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; -webkit-appearance: none; background-color: WHITE; background-image: none; border: 1px solid #747775; -webkit-border-radius: 4px; border-radius: 4px; -webkit-box-sizing: border-box; box-sizing: border-box; color: #1f1f1f; cursor: pointer; font-family: "Google Sans"; font-size: 14px; height: 40px; letter-spacing: 0.25px; outline: none; overflow: hidden; padding: 0 12px; position: relative; text-align: center; -webkit-transition: background-color .218s, border-color .218s, box-shadow .218s; transition: background-color .218s, border-color .218s, box-shadow .218s; vertical-align: middle; white-space: nowrap; width: auto; max-width: 400px; min-width: min-content; } .gsi-material-button .gsi-material-button-icon { height: 20px; margin-right: 12px; min-width: 20px; width: 20px; } .gsi-material-button .gsi-material-button-content-wrapper { -webkit-align-items: center; align-items: center; display: flex; -webkit-flex-direction: row; flex-direction: row; -webkit-flex-wrap: nowrap; flex-wrap: nowrap; height: 100%; justify-content: space-between; position: relative; width: 100%; } .gsi-material-button .gsi-material-button-contents { -webkit-flex-grow: 1; flex-grow: 1; font-family: "Google Sans"; font-weight: 500; overflow: hidden; text-overflow: ellipsis; vertical-align: top; } .gsi-material-button .gsi-material-button-state { -webkit-transition: opacity .218s; transition: opacity .218s; bottom: 0; left: 0; opacity: 0; position: absolute; right: 0; top: 0; } .gsi-material-button:disabled { cursor: default; background-color: #ffffff61; border-color: #1f1f1f1f; } .gsi-material-button:disabled .gsi-material-button-contents { opacity: 38%; } .gsi-material-button:disabled .gsi-material-button-icon { opacity: 38%; } .gsi-material-button:not(:disabled):active .gsi-material-button-state, .gsi-material-button:not(:disabled):focus .gsi-material-button-state { background-color: #303030; opacity: 12%; } .gsi-material-button:not(:disabled):hover { -webkit-box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15); box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .30), 0 1px 3px 1px rgba(60, 64, 67, .15); } .gsi-material-button:not(:disabled):hover .gsi-material-button-state { background-color: #303030; opacity: 8%; } ::ng-deep .mat-dialog-container { border-radius: 40px !important; } :host { display: block; background: #fff; border-radius: 20px !important; padding: 8px 16px 16px; } ================================================ FILE: frontend/src/app/login-button/login-button.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LoginButtonComponent } from './login-button.component'; describe('LoginButtonComponent', () => { let component: LoginButtonComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [LoginButtonComponent] }) .compileComponents(); fixture = TestBed.createComponent(LoginButtonComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/login-button/login-button.component.ts ================================================ import { Dialog } from '@angular/cdk/dialog'; import { Component } from '@angular/core'; import { LoginService } from '../shared/services/login.service'; import { SharedService } from '../shared/services/shared.service'; @Component({ selector: 'app-login-button', templateUrl: './login-button.component.html', styleUrl: './login-button.component.scss' }) export class LoginButtonComponent { photoURL: any; userLoggedIn: boolean = false; constructor(public fireservice: SharedService, public loginService: LoginService, public dialog: Dialog) { } getLogin() { this.fireservice.googleSignin().then((res => { this.userLoggedIn = true; this.photoURL = res?.photoURL; this.updateUserData(res); this.dialog.closeAll() })) } updateUserData(userDetails: any): void { this.loginService.sendUserDetails(userDetails); } } ================================================ FILE: frontend/src/app/menu/menu.component.html ================================================ New Query History

{{history.question}}

{{history.question}}

star_borderScenarios
================================================ FILE: frontend/src/app/menu/menu.component.scss ================================================ mat-icon { vertical-align: bottom; } .app-sidenav { color: #000; font-feature-settings: 'clig' off, 'liga' off; font-family: "Google Sans"; font-size: 16px; font-style: normal; font-weight: 500; line-height: 20px; letter-spacing: -0.154px; width: 350px; } .activeTab { background-color: #E8F0FE; border-radius: 25px; color: #4D88FF; cursor: pointer; } .inactiveTab { background-color: #FFF; border-radius: 25px; color: #444746; cursor: pointer; } .historyDetails { text-wrap: wrap; display: block; color: #444746; font-family: 'Google Sans'; font-size: 14px; margin-bottom: 10px; cursor: pointer; } .showMore { font-size: .875rem; font-weight: 500; line-height: 1.25rem; font-family: "Google Sans", "Helvetica Neue", sans-serif; letter-spacing: normal; border: transparent; border-radius: 20px; padding: 10px; width: 100%; margin-bottom: 10px; background: #FFF; } ::ng-deep.mat-expansion-indicator { margin-right: 10px } // .mdc-list-item { // cursor: auto !important; // } input[type="file"] { display: none; } .dropLabel { text-align: center; font-family: Google Sans; font-size: 14px; font-style: normal; font-weight: 400; line-height: 20px; cursor: pointer; display: flex; } .file-wrapper { border-color: transparent; height: 41px; background: #34A853; border-radius: 100px; font-family: "Google Sans"; font-style: normal; font-weight: 500; font-size: 14px; line-height: 20px; display: flex; align-items: center; text-align: center; justify-content: center; letter-spacing: 0.25px; color: #FFFFFF; cursor: pointer; } ::ng-deep .mat-expansion-panel-body { padding: 0 24px 0px !important; } .mat-expansion-panel { background: transparent !important; } .mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover { background: transparent !important; } ul, #myUL { list-style-type: none; } #myUL { margin: 0; padding: 0; } .caret { cursor: pointer; -webkit-user-select: none; /* Safari 3.1+ */ -moz-user-select: none; /* Firefox 2+ */ -ms-user-select: none; /* IE 10+ */ user-select: none; } .caret::before { content: "\25B6"; color: black; display: inline-block; margin-right: 6px; } .caret-down::before { -ms-transform: rotate(90deg); /* IE 9 */ -webkit-transform: rotate(90deg); /* Safari */ transform: rotate(90deg); } .scenariosInfoBtn { cursor: pointer; background: transparent; border: none; height: 24px; } .uploadScenarioBtn{ position: absolute; bottom:20px; width: 100%; } ================================================ FILE: frontend/src/app/menu/menu.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MenuComponent } from './menu.component'; describe('MenuComponent', () => { let component: MenuComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [MenuComponent] }) .compileComponents(); fixture = TestBed.createComponent(MenuComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/menu/menu.component.ts ================================================ import { Component, Input, EventEmitter, Output, signal, SimpleChanges, inject, ViewChild } from '@angular/core'; import { ThemePalette } from '@angular/material/core'; import { Router } from '@angular/router'; import { HomeService } from '../shared/services/home.service'; import { GroupingModalComponent } from '../grouping-modal/grouping-modal.component'; import { MatDialog } from '@angular/material/dialog'; import { ScenarioListComponent } from '../scenario-list/scenario-list.component'; import { ChatService } from '../shared/services/chat.service'; import { LoginService } from '../shared/services/login.service'; import { UploadTemplateComponent } from '../upload-template/upload-template.component'; @Component({ selector: 'app-menu', templateUrl: './menu.component.html', styleUrl: './menu.component.scss', }) export class MenuComponent { clickedMenuItem: 'Query' | 'New Query' | 'Reports' | 'History' | 'Operations Mode' | 'My workspace' | 'Team workspaces' | 'Recent' | 'Shared with me' | 'Trash' | 'Templates' | 'Scenarios' | undefined; color: ThemePalette = 'accent'; checked = false; disabled = true; @Input('userHistory') userHistory: any @Input('userSessions') userSessions: any; @Output() selectedTab = new EventEmitter(); @Output() selectedHistory = new EventEmitter(); panelOpenState = signal(false); scenarioPanelOpenState = signal(false) readonly dialog = inject(MatDialog); userType: any; recentHistory: any; showMoreHistory: any; selectedGrouping: string = ''; csvData: any; @ViewChild(ScenarioListComponent) childComponent!: ScenarioListComponent; userId: any; showUploadSection: boolean = false; constructor(public _router: Router, public homeService: HomeService, public chatService: ChatService, public loginService: LoginService) { this.clickedMenuItem = 'New Query'; } ngOnInit() { this.loginService.getUserDetails().subscribe((res: any) => { this.userId = res.uid; }); this.selectedTab.emit(this.clickedMenuItem); } ngOnChanges(changes: SimpleChanges) { for (const propName in changes) { if (changes.hasOwnProperty(propName)) { switch (propName) { case 'userSessions': { if (this.userSessions.length > 0) { //group user sessions based on session id let grouped = this.userSessions?.reduce( (result: any, currentValue: any) => { (result[currentValue['session_id']] = result[currentValue['session_id']] || []).push(currentValue); return result; }, {}); //map the grouped user sessions as a chatThread , sort and display in side nav let sessionToDisplayQuery: any = [] Object.keys(grouped).map(function (sessionId: string) { let chatThreadArray: any[] = grouped[sessionId]; let obj = { 'sessionId': sessionId, 'question': chatThreadArray[chatThreadArray.length - 1].user_question, 'chatThread': chatThreadArray, 'timestamp': chatThreadArray[chatThreadArray.length - 1].timestamp.seconds } sessionToDisplayQuery.push(obj); }); sessionToDisplayQuery?.sort((a: any, b: any) => { return b.chatThread[0].timestamp - a.chatThread[0].timestamp }); this.userHistory = sessionToDisplayQuery; this.recentHistory = this.userHistory.slice(0, 5); } break; } } } } } showMore() { this.showMoreHistory = this.userHistory.slice(5, 10) } onMenuClick(item: 'Query' | 'New Query' | 'Reports' | 'History' | 'Operations Mode' | 'My workspace' | 'Team workspaces' | 'Recent' | 'Shared with me' | 'Trash' | 'Templates' | 'Scenarios') { this.clickedMenuItem = item; this.selectedGrouping = this.homeService.getSelectedDbGrouping(); if (this.clickedMenuItem == 'New Query') { this.chatService.createNewSession(); this.homeService.currentSelectedGrouping.next('') this.homeService.setSessionId('') this.childComponent?.resetSelectedScenario() } this.selectedTab.emit(this.clickedMenuItem); } onClickHistory(chatThread: any) { this.childComponent?.resetSelectedScenario() this.selectedGrouping = this.homeService.getSelectedDbGrouping(); if (this.selectedGrouping) { this.homeService.updateChatMsgs(chatThread) this.chatService.createNewSession() this.homeService.setSessionId(chatThread[0].session_id) this.homeService.updateSelectedHistory(chatThread) this.chatService.addQuestion(chatThread[chatThread?.length - 1]?.user_question, this.userId, 'history', chatThread) this.selectedHistory.emit(chatThread); } else { this.openDialog(); } } openDialog() { let dialogRef = this.dialog.open(GroupingModalComponent, { disableClose: true, width: '450px', }); dialogRef.afterClosed().subscribe(result => { }); } uploadTemplate() { let dialogRef = this.dialog.open(UploadTemplateComponent, { disableClose: true, width: '450px', }); dialogRef.afterClosed().subscribe(result => { this.showUploadSection = result }); } onFileChange(fileInput: any) { if (fileInput) { const file: File = fileInput.files[0]; let reader: FileReader = new FileReader(); reader.readAsText(file); reader.onload = (e) => { let csv: any = reader.result; csv = csv.split('\n') for (let i = 0; i < csv.length; i++) { csv[i] = csv[i].replace(/(\r\n|\n|\r)/gm, ""); csv[i] = csv[i].split(','); if (i != 0) { // 0th element has the column header csv[i] = this.arrToObject(csv[i], csv[0]); } } this.csvData = csv.slice(1); } } } arrToObject(arr: any[], header: any[]) { let rv: any = {}; for (let i = 0; i < arr.length; ++i) if (arr[i] !== undefined && arr.length == header.length) { rv[header[i]] = arr[i]; } return rv; } } ================================================ FILE: frontend/src/app/prism/prism.component.html ================================================
  {{code}}
================================================ FILE: frontend/src/app/prism/prism.component.scss ================================================ mat-icon { vertical-align: bottom; } .app-sidenav { font-family: "Google Sans"; font-size: 14px; width: 200px; } .app-sidenav mat-list-item { cursor: pointer; } .active { background-color: #E8F0FE; border-radius: 25px; color: #4D88FF; } ================================================ FILE: frontend/src/app/prism/prism.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PrismComponent } from './prism.component'; describe('MenuComponent', () => { let component: PrismComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PrismComponent] }) .compileComponents(); fixture = TestBed.createComponent(PrismComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/prism/prism.component.ts ================================================ import { Component, Input, EventEmitter, Output, ViewChild, ElementRef } from '@angular/core'; import * as Prism from 'prismjs'; @Component({ selector: 'app-prism', templateUrl: './prism.component.html', styleUrl: './prism.component.scss' }) export class PrismComponent { @ViewChild('codeEle') codeEle!: ElementRef; @Input() code?: string; @Input() language?: string; constructor() { } ngAfterViewInit() { Prism.highlightElement(this.codeEle.nativeElement); } ngOnChanges(changes: any): void { if (changes?.code) { if (this.codeEle?.nativeElement) { this.codeEle.nativeElement.textContent = this.code; Prism.highlightElement(this.codeEle.nativeElement); } } } } ================================================ FILE: frontend/src/app/prism/prism.d.ts ================================================ declare module 'prismjs' ================================================ FILE: frontend/src/app/scenario-list/scenario-list.component.html ================================================
{{sc.name}} {{node.name}}
{{node.name}}
================================================ FILE: frontend/src/app/scenario-list/scenario-list.component.scss ================================================ .example-tree-invisible { display: none; } .example-tree ul, .example-tree li { margin-top: 0; margin-bottom: 0; list-style-type: none; } /* * This padding sets alignment of the nested nodes. */ .example-tree .mat-nested-tree-node div[role=group] { padding-left: 40px; } /* * Padding for leaf nodes. * Leaf nodes need to have padding so as to align with other non-leaf nodes * under the same parent. */ .example-tree div[role=group]>.mat-tree-node { padding-left: 25px; } .user-question { text-wrap: wrap; display: block; color: #444746; font-family: "Google Sans"; font-size: 14px; cursor: pointer; } .historyDetails { text-wrap: wrap; display: block; color: #444746; font-family: 'Google Sans'; font-size: 14px; margin-bottom: 10px; cursor: pointer; } .mat-tree { background-color: transparent !important; } ================================================ FILE: frontend/src/app/scenario-list/scenario-list.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ScenarioListComponent } from './scenario-list.component'; describe('ScenarioListComponent', () => { let component: ScenarioListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ScenarioListComponent] }) .compileComponents(); fixture = TestBed.createComponent(ScenarioListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/scenario-list/scenario-list.component.ts ================================================ import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core'; import { NestedTreeControl, TreeControl } from '@angular/cdk/tree'; import { MatTreeNestedDataSource } from '@angular/material/tree'; import { HomeService } from '../shared/services/home.service'; import { ChatService } from '../shared/services/chat.service'; import { LoginService } from '../shared/services/login.service'; /** * Scenario data with nested structure. * Each node has a name and an optional list of children. */ interface Question { name: string; child?: Question[]; } interface ScenarioNode { dataSource: MatTreeNestedDataSource; questions: Question[]; name: string; userGrouping: string } @Component({ selector: 'app-scenario-list', templateUrl: './scenario-list.component.html', styleUrl: './scenario-list.component.scss' }) export class ScenarioListComponent { @Input('csvData') csvData: any scenarioData: ScenarioNode[] = []; treeControl = new NestedTreeControl(node => node.child); hasChild = (_: number, node: Question) => !!node.child && node.child.length > 0; selectedGrouping: string = ''; selectedScenario: string = ''; userId: any; constructor(public homeService: HomeService, public chatService: ChatService, public loginService: LoginService) { this.loginService.getUserDetails().subscribe((res: any) => { this.userId = res.uid; }); } ngOnChanges(changes: SimpleChanges) { for (const propName in changes) { if (changes.hasOwnProperty(propName)) { switch (propName) { case 'csvData': { if (this.csvData) { this.formatCsvData() } } } } } } ngOnInit() { this.formatCsvData(); } formatCsvData() { this.scenarioData = []; let result: any = []; this.csvData.forEach((current: any) => { let desiredObj: any = result?.find((ele: any) => { if (ele.user_grouping == current.user_grouping && ele.scenario == current.scenario) { return ele } }); if (desiredObj === undefined) { result.push( { user_grouping: current.user_grouping, scenario: current.scenario, question: [current.question] }); } else { desiredObj.question.push(current.question); } }); result?.map((ele: any) => { if (ele.user_grouping) { var nestedQues: any = this.constructNestedTree(ele.question); let dataSource = new MatTreeNestedDataSource(); dataSource.data = [nestedQues]; this.scenarioData.push({ dataSource: dataSource, questions: [nestedQues], name: ele.scenario, userGrouping: ele.user_grouping }) } }) this.treeControl = new NestedTreeControl(node => node.child) } onClickScenario(question: any, scenario: any) { this.homeService.setSelectedDbGrouping(scenario.userGrouping); this.selectedGrouping = this.homeService.getSelectedDbGrouping(); this.homeService.currentSelectedGrouping.next(scenario.userGrouping) if (this.selectedScenario == scenario.name) { this.chatService.addQuestion(question.name, this.userId, 'scenario') this.chatService.agentResponseLoader.next(true) //this.chatService.setAgentResponseLoader(true) } else { this.chatService.createNewSession() this.chatService.addQuestion(question.name, this.userId, 'scenario') this.chatService.agentResponseLoader.next(true) } this.homeService.currentSelectedGroupingObservable.subscribe((res) => { this.selectedGrouping = res }) this.selectedScenario = scenario.name } resetSelectedScenario() { this.selectedScenario = ''; } constructNestedTree(questions: any[]) { if (questions.length == 1) { var ques: Question = { name: questions[0] } } else { var ques: Question = { name: questions[0], child: [this.constructNestedTree(questions.slice(1))] } } return ques; } } ================================================ FILE: frontend/src/app/shared/services/chat.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { ChatService } from './chat.service'; describe('ChatService', () => { let service: ChatService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(ChatService); }); it('should be created', () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/shared/services/chat.service.ts ================================================ import { Injectable } from '@angular/core'; import { Subject, takeUntil } from 'rxjs'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { HomeService } from './home.service'; export interface Message { author: string message?: any user_question: string generate_sql?: any run_query?: any visualize?: any dataSource?: any, displayedColumns?: any dataSet?: any ind?: any embed_sql?: any } interface ChatSession { chatMsgs: Message[] } @Injectable({ providedIn: 'root' }) export class ChatService { private chatSess: ChatSession = { chatMsgs: [] } public currentActiveSession = new BehaviorSubject(this.chatSess); chatSessionObservable = this.currentActiveSession.asObservable(); public agentResponseLoader = new BehaviorSubject(false); agentResponseLoader$ = this.agentResponseLoader.asObservable(); private _destroy$ = new Subject(); private currentSessionId: string = ''; private suggestionList: [] = []; selectedGrouping!: string; constructor(public homeService: HomeService) { this.homeService.currentSelectedGroupingObservable.pipe(takeUntil(this._destroy$)).subscribe((res) => { this.selectedGrouping = res }) this.homeService.knownSqlObservable?.pipe(takeUntil(this._destroy$)).subscribe((response: any) => { if (response && response != null) { this.suggestionList = JSON.parse(response); this.createNewSession() } }) } createNewSession() { const newSession: ChatSession = { chatMsgs: [{ 'author': 'agent', 'message': this.suggestionList, 'user_question': 'Looking for a specific insight or want to browse through your database? Ask what you are looking for in a natural language and Open Data QnA will translate to SQL and bring back results in natural language.' }] } this.currentSessionId = ''; this.currentActiveSession.next(newSession); } addToSessionThread(message: Message) { const desiredSession = this.currentActiveSession.value; if (desiredSession) { desiredSession.chatMsgs.push(message); if (message.author == 'agent') { this.agentResponseLoader.next(false) } this.currentActiveSession.next(desiredSession); } } addQuestion(question: string, userId: string, agentCase: string, selectedHistory?: any) { let newChatMsg: Message = { author: 'user', message: [], user_question: question } if (agentCase != 'history') { this.addToSessionThread(newChatMsg); } // do API call for agent response. switch (agentCase) { case 'followup': this.generate_sql(question, userId); break; case 'history': this.generate_history_thread(selectedHistory) break; case 'scenario': this.generate_sql(question, userId) break; } } generate_sql(question: string, userId: string) { this.currentSessionId = this.homeService.getSessionId() this.agentResponseLoader.next(true) this.homeService.generateSql(question, this.homeService.getSelectedDbGrouping(), this.currentSessionId, userId).subscribe((response: any) => { if (response !== undefined) { this.homeService.setSessionId(response.SessionID) this.currentSessionId = response.SessionID; const newChatMsg: Message = { 'author': 'agent', 'user_question': question, 'generate_sql': response, } this.addToSessionThread(newChatMsg); } }) } generate_history_thread(selectedHistory: any) { for (let i = selectedHistory?.length - 1; i >= 0; i--) { const newUserMsg: Message = { author: 'user', user_question: selectedHistory[i]?.user_question } this.addToSessionThread(newUserMsg); const newAgentMsg: Message = { 'author': 'agent', 'user_question': selectedHistory[i]?.user_question, 'generate_sql': { 'GeneratedSQL': selectedHistory[i]?.bot_response, 'SessionID': selectedHistory[i]?.session_id, "ResponseCode": 200 } } this.homeService.setSessionId(selectedHistory[i]?.session_id) this.addToSessionThread(newAgentMsg); } } } ================================================ FILE: frontend/src/app/shared/services/home.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { HomeService } from './home.service'; describe('HomeService', () => { let service: HomeService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(HomeService); }); it('should be created', () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/shared/services/home.service.ts ================================================ import { Injectable, inject } from '@angular/core'; import { HttpHeaders, HttpClient, HttpErrorResponse } from '@angular/common/http'; import { catchError, throwError, BehaviorSubject } from 'rxjs'; import { ENDPOINT_OPENDATAQNA } from '../../../assets/constants'; import { Firestore, collection, collectionData, doc, docData, orderBy, query, updateDoc, where } from '@angular/fire/firestore'; @Injectable({ providedIn: 'root' }) export class HomeService { public knownSqlFromDb = new BehaviorSubject(null); knownSqlObservable = this.knownSqlFromDb.asObservable(); public currentSelectedGrouping = new BehaviorSubject(''); currentSelectedGroupingObservable = this.currentSelectedGrouping.asObservable(); private databaseList: any; private selectedGrouping: any; public selectedDBType: any; selectedDbName: any; chatMsgs: any[] = []; selectedHistory: any private firestore: Firestore = inject(Firestore); session_id: any = ''; constructor(public http: HttpClient) { } ngOnInit() { } getUserSessions(userId: string) { const sessionCollection = collection(this.firestore, `session_logs`); const orderedCollection = query(sessionCollection, orderBy("timestamp", "desc")); const filter = query(orderedCollection, where("user_id", "==", userId)) return collectionData(filter, { idField: 'id' }) } getAvailableDatabases(): any { const header = { 'Content-Type': 'application/json', } const requestOptions = { headers: new HttpHeaders(header), }; return this.http.get(ENDPOINT_OPENDATAQNA + '/available_databases', requestOptions).pipe(catchError(this.handleError)) } sqlSuggestionList(grouping: any, dbtype: any) { const header = { 'Content-Type': 'application/json', } const requestOptions = { headers: new HttpHeaders(header), }; const body = { "user_grouping": grouping } this.selectedDBType = dbtype; return this.http.post(ENDPOINT_OPENDATAQNA + '/get_known_sql', body, requestOptions) .pipe(catchError(this.handleError)); } generateSql(userQuestion: any, grouping: any, session_id: any, user_id: any) { const header = { 'Content-Type': 'application/json', } const requestOptions = { headers: new HttpHeaders(header), }; const body = { "user_question": userQuestion, "user_grouping": grouping, "session_id": session_id, "user_id": user_id } let endpoint = ENDPOINT_OPENDATAQNA; return this.http.post(endpoint + "/generate_sql", body, requestOptions) .pipe(catchError(this.handleError)); } private handleError(error: HttpErrorResponse) { if (error.error instanceof ErrorEvent) { console.error('An error occurred:', error.error); } else { } return throwError( 'Something bad happened; please try again later.'); } setAvailableDBList(databaseList: string) { this.databaseList = databaseList; } getAvailableDBList(): string { return this.databaseList; } setSelectedDbGrouping(selectedDBGroup: any) { this.selectedGrouping = selectedDBGroup; } getSelectedDbGrouping(): string { return this.selectedGrouping; } setselectedDbName(databaseList: any) { this.selectedDbName = databaseList; } getselectedDbName(): string { return this.selectedDbName; } setSessionId(session_id: any) { this.session_id = session_id; } getSessionId(): string { return this.session_id; } getChatMsgs(): any[] { return this.chatMsgs } updateChatMsgs(chatMsgs: any) { this.chatMsgs = chatMsgs; } getSelectedHistory() { return this.selectedHistory } updateSelectedHistory(selectedHistory: any) { this.selectedHistory = selectedHistory } updateChatMsgsAtIndex(chatMsg: any, ind: any) { this.chatMsgs[ind] = chatMsg } runQuery(query: any, grouping: any, user_question: any, session_id: any) { const header = { 'Content-Type': 'application/json', } const requestOptions = { headers: new HttpHeaders(header), }; const body = { "generated_sql": query, "user_grouping": grouping, "user_question": user_question, "session_id": this.session_id } let endpoint = ENDPOINT_OPENDATAQNA; return this.http.post(endpoint + "/run_query", body, requestOptions); } thumbsUp(sql: any, user_question: any, selectedGrouping: any, session_id: any) { const header = { 'Content-Type': 'application/json', } const requestOptions = { headers: new HttpHeaders(header), }; const body = { user_grouping: selectedGrouping, generated_sql: sql, user_question: user_question, session_id: this.session_id } let endpoint = ENDPOINT_OPENDATAQNA; return this.http.post(endpoint + "/embed_sql", body, requestOptions) .pipe(catchError(this.handleError)); } generateViz(question: any, query: any, result: any, session_id: any) { const header = { 'Content-Type': 'application/json', } const requestOptions = { headers: new HttpHeaders(header), }; const body = { "user_question": question, "sql_generated": query, "sql_results": result, "session_id": this.session_id } return this.http.post(ENDPOINT_OPENDATAQNA + "/generate_viz", body, requestOptions) .pipe(catchError(this.handleError)); } } ================================================ FILE: frontend/src/app/shared/services/login.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { LoginService } from './login.service'; describe('LoginService', () => { let service: LoginService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(LoginService); }); it('should be created', () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/shared/services/login.service.ts ================================================ import { Injectable } from '@angular/core'; import { Observable, ReplaySubject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class LoginService { constructor() { } private userDetails = new ReplaySubject(1); userDetails$: Observable = this.userDetails.asObservable(); loginErrorMsg: any = new ReplaySubject(1); private idToken = new ReplaySubject(1); idToken$: Observable = this.idToken.asObservable(); getLoginError(): any { return this.loginErrorMsg; } updateLoginError(msg: any) { this.loginErrorMsg.next(msg) } getUserDetails(): Observable { return this.userDetails$; } getIdToken(): any { return this.idToken$; } setIdToken(token: any) { this.idToken.next(token); } sendUserDetails(message: any) { this.userDetails.next(message); } } ================================================ FILE: frontend/src/app/shared/services/shared.service.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { SharedService } from './shared.service'; describe('SharedService', () => { let service: SharedService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(SharedService); }); it('should be created', () => { expect(service).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/shared/services/shared.service.ts ================================================ import { Injectable, inject } from '@angular/core'; import { LoginService } from './login.service'; import { GoogleAuthProvider, signInWithPopup } from '@firebase/auth'; import { Auth } from '@angular/fire/auth'; @Injectable({ providedIn: 'root' }) export class SharedService { userData: any; private auth: Auth = inject(Auth); constructor(public loginservice: LoginService) { } async googleSignin() { const provider = new GoogleAuthProvider(); return await signInWithPopup(this.auth, provider) .then(async (result) => { const token = await this.auth.currentUser?.getIdToken(); this.loginservice.setIdToken(token); return result.user; }). catch((error) => { if (error.message.indexOf('Cloud Function') === 15) { const jsonStart = error.message.indexOf('{'); const jsonEnd = error.message.lastIndexOf('}'); const jsonString = error.message.substring(jsonStart, jsonEnd + 1); const errorObject = JSON.parse(jsonString); this.loginservice.updateLoginError(errorObject.error.message) } else { this.loginservice.updateLoginError(error.message) } }); } } ================================================ FILE: frontend/src/app/upload-template/upload-template.component.html ================================================ ================================================ FILE: frontend/src/app/upload-template/upload-template.component.scss ================================================ .grouping-msg { font-family: "Google Sans"; font-size: 16px; color: #d93035; margin-bottom: 20px; } .closeBtn { border-color: transparent; min-width: 146px; height: 41px; background: #1A73E8; border-radius: 100px; font-family: "Google Sans"; font-style: normal; font-weight: 500; font-size: 14px; line-height: 20px; display: flex; align-items: center; text-align: center; justify-content: center; letter-spacing: 0.25px; color: #FFFFFF; } ::ng-deep .mat-dialog-container { border-radius: 40px !important; } .popup { display: flex; justify-content: space-evenly; align-items: center; flex-direction: column; gap: 20px; margin: 20px; } #customers { font-family: 'Google Sans'; border-collapse: collapse; width: 100%; } #customers td, #customers th { border: 1px solid #ddd; padding: 8px; } #customers tr:nth-child(even) { background-color: #f2f2f2; } #customers tr:hover { background-color: #ddd; } #customers th { padding-top: 12px; padding-bottom: 12px; text-align: left; background-color: #04AA6D; color: white; } ================================================ FILE: frontend/src/app/upload-template/upload-template.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UploadTemplateComponent } from './upload-template.component'; describe('UploadTemplateComponent', () => { let component: UploadTemplateComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UploadTemplateComponent] }) .compileComponents(); fixture = TestBed.createComponent(UploadTemplateComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/upload-template/upload-template.component.ts ================================================ import { Component } from '@angular/core'; import { MatDialogRef } from '@angular/material/dialog'; @Component({ selector: 'app-upload-template', standalone: true, imports: [], templateUrl: './upload-template.component.html', styleUrl: './upload-template.component.scss' }) export class UploadTemplateComponent { constructor(public dialogRef: MatDialogRef) { } closeDialog() { this.dialogRef.close(true); } } ================================================ FILE: frontend/src/app/user-journey/user-journey.component.html ================================================

{{user.userTitle}}

{{user.userId}}

  • {{content}}

Watch the UI Walkthrough below

================================================ FILE: frontend/src/app/user-journey/user-journey.component.scss ================================================ .list-css { background-image: radial-gradient(grey, black); min-width: 100%; min-height: 100%; } .search { color: #FFFFFF; } .userTitle { width: 180px; height: 20px; top: 788px; left: 485px; font-family: Google Sans; font-size: 24px; font-weight: 700; line-height: 25px; letter-spacing: 0.10000000149011612px; text-align: left; color: #FFFFFF; display: inline; } .userId { top: 498px; left: 485px; font-family: "Google Sans"; font-size: 18px; font-weight: 400; line-height: 20px; letter-spacing: 0.10000000149011612px; text-align: left; color: #FFFFFF; display: inline; float: right; } .content { width: 612px; height: 162px; top: 426px; left: 689px; font-family: Google Sans; font-size: 18px; font-style: normal; font-weight: 400; line-height: 27px; letter-spacing: 0.10000000149011612px; text-align: left; color: #FFFFFF; } .user-text { font-family: Google Sans; font-size: 20px; font-weight: 400; font-style: normal; line-height: 27px; letter-spacing: 0.10000000149011612px; text-align: left; color: white; background: rgba(255, 255, 255, 0.2); border-radius: 19px; padding: 20px 40px; } .user-journey-btn { display: flex; align-items: center; gap: 10px; margin-left: 70px; width: 168px; height: 36px; left: 164px; top: 1114px; background: #1A73E8; border-radius: 4px; width: 200px; height: 40px; font-family: "Google Sans"; font-style: normal; font-weight: 500; font-size: 14px; line-height: 20px; display: flex; align-items: center; text-align: center; justify-content: center; letter-spacing: 0.25px; flex: none; order: 0; flex-grow: 0; color: #FFFFFF; border: none !important; } .align { align-items: center; } .list-group .list-group-item { border: none !important; } .list-group { margin-top: 100px; } .demoBtn { float: right; display: inline-flex; } .flex-container { display: flex; flex-direction: row; flex-wrap: wrap; width: 100%; } .flex-child { flex: 1; border: 2px solid yellow; } .flex-child:first-child { margin-right: 20px; } .gap-10 { gap: 10px; } .loading-spinner { margin-left: 25px; margin-top: 5px; } .video-demo { font-size: 18px; font-family: 'Google Sans'; display: flex; flex-direction: column; justify-content: center; align-items: center; gap : 20px; margin : 10px } .video-title { font-family: "Google Sans"; text-align: center; margin-bottom: 10px; /* Add a margin to separate the title from the video */ color: white; } ================================================ FILE: frontend/src/app/user-journey/user-journey.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserJourneyComponent } from './user-journey.component'; describe('UserJourneyComponent', () => { let component: UserJourneyComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserJourneyComponent] }) .compileComponents(); fixture = TestBed.createComponent(UserJourneyComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/user-journey/user-journey.component.ts ================================================ import { AfterViewInit, Component } from '@angular/core'; import { LoginService } from '../shared/services/login.service'; import { Subscription } from 'rxjs'; import { Router } from '@angular/router'; import { HomeService } from '../shared/services/home.service'; @Component({ selector: 'app-user-journey', templateUrl: './user-journey.component.html', styleUrl: './user-journey.component.scss' }) export class UserJourneyComponent implements AfterViewInit { photoURL: string | undefined; subscription: Subscription | undefined; showProgress: boolean = false loginError = false; loginErrorMessage: any; demoVideo = false; userJourneyList: any = [{ userId: "User journey 1", userTitle: "Business User", userContent: [ "This demo will help you to ask the questions in natural language, view the SQL, get results and visualize data", ] }]; constructor(public _router: Router, public loginService: LoginService, public homeService: HomeService) { this.subscription = this.loginService.getUserDetails().subscribe(message => { this.photoURL = message?.photoURL; if (!this.photoURL) { this._router.navigate(['']); } }); } onDemoVideoClick(){ this.demoVideo = true; } ngOnInit() { // this.loginService.getLoginError().subscribe((res: any) => { // this.loginErrorMessage = res // this.loginError = true; // if(this.loginError){ // this._router.navigate(['']); // } // }); } ngAfterViewInit(){ window.scroll(0, 0); } async navigateToHome(userTitle: String) { if (userTitle === 'Business User') { //this.homeService.checkuserType = 'Business'; this.showProgress = true; this.homeService.getAvailableDatabases().subscribe((res: any) => { if (res && res.ResponseCode === 200) { this.homeService.setAvailableDBList(res.KnownDB); this.showProgress = false; this._router.navigate(['home-page']); } }) } } ngOnDestroy() { this.subscription?.unsubscribe() } } ================================================ FILE: frontend/src/app/user-photo/user-photo.component.html ================================================ ================================================ FILE: frontend/src/app/user-photo/user-photo.component.scss ================================================ .login-user { float: right; padding-right: 20px; } ================================================ FILE: frontend/src/app/user-photo/user-photo.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserPhotoComponent } from './user-photo.component'; describe('UserPhotoComponent', () => { let component: UserPhotoComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserPhotoComponent] }) .compileComponents(); fixture = TestBed.createComponent(UserPhotoComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: frontend/src/app/user-photo/user-photo.component.ts ================================================ import { Component, inject } from '@angular/core'; import { LoginButtonComponent } from '../login-button/login-button.component'; import { Subscription } from 'rxjs'; import { Auth, User, user } from '@angular/fire/auth'; import { Router } from '@angular/router'; import { LoginService } from '../shared/services/login.service'; import { Dialog } from '@angular/cdk/dialog'; @Component({ selector: 'app-user-photo', templateUrl: './user-photo.component.html', styleUrl: './user-photo.component.scss' }) export class UserPhotoComponent { photoURL: string | undefined; subscription: Subscription | undefined; userLoggedIn: boolean = false; private auth: Auth = inject(Auth); user$ = user(this.auth); userSubscription: Subscription; constructor(private _router: Router, public dialog: Dialog, public loginService: LoginService) { this.dialog.closeAll(); this.userSubscription = this.user$.subscribe((aUser: User | null) => { //handle user state changes here. Note, that user will be null if there is no currently logged in user if (aUser) { this.dialog.closeAll(); this.userLoggedIn = true; this.loginService.sendUserDetails(aUser) if (aUser.photoURL) { this.photoURL = aUser.photoURL; } } else { this.userLoggedIn = false; this.showLogIn() } }) } // ngOnInit() { // if (!this.photoURL) { // this.showLogIn() // } // } showLogIn(): void { this.dialog.open(LoginButtonComponent, { disableClose: true, width: '350px', panelClass: 'login-container' }); } ngOnDestroy() { this.dialog.closeAll(); // when manually subscribing to an observable remember to unsubscribe in ngOnDestroy this.userSubscription.unsubscribe(); } } ================================================ FILE: frontend/src/assets/.gitkeep ================================================ ================================================ FILE: frontend/src/assets/constants.ts ================================================ export const firebaseConfig = { "projectId": "", "appId": "", "storageBucket": "", "apiKey": "", "authDomain": "", "messagingSenderId": "" }; export const ENDPOINT_OPENDATAQNA = 'https://opendataqna-kdr33rftkq-uc.a.run.app' export const FIRESTORE_DATABASE_ID = 'opendataqna-session-logs' ================================================ FILE: frontend/src/index.html ================================================ Open Data QnA ================================================ FILE: frontend/src/main.server.ts ================================================ export { AppServerModule as default } from './app/app.module.server'; ================================================ FILE: frontend/src/main.ts ================================================ import { AppModule } from './app/app.module'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; platformBrowserDynamic().bootstrapModule(AppModule) .catch((err) => console.error(err)); ================================================ FILE: frontend/src/styles/variables.scss ================================================ $bgColor_light: white; $bgColor_dark: black; $textColor_light: black; $textColor_dark: white; $borderColor_light: black; $borderColor_dark: white; // mixin that enables css variables in light mode @mixin lighten() { --bgColor: #{$bgColor_light}; --textColor: #{$textColor_light}; --borderColor: #{$borderColor_light}; } // mixin that enables css variables in dark mode @mixin darken() { --bgColor: #{$bgColor_dark}; --textColor: #{$textColor_dark}; --borderColor: #{$borderColor_dark}; } ================================================ FILE: frontend/src/styles.scss ================================================ // Custom Theming for Angular Material // For more information: https://material.angular.io/guide/theming @use '@angular/material' as mat; @import url('https://fonts.googleapis.com/icon?family=Google+Sans'); @import "prismjs/themes/prism.css"; // Plus imports for other components in your app. // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! @include mat.core(); // Define the palettes for your theme using the Material Design palettes available in palette.scss // (imported above). For each palette, you can optionally specify a default, lighter, and darker // hue. Available color palettes: https://material.io/design/color/ $genai-csm-primary: mat.define-palette(mat.$indigo-palette); $genai-csm-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $genai-csm-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // theming systems such as "color" or "typography". $genai-csm-theme: mat.define-light-theme((color: (primary: $genai-csm-primary, accent: $genai-csm-accent, warn: $genai-csm-warn, ))); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($genai-csm-theme); /* You can add global styles to this file, and also import other style files */ html, body { height: 100%; font-family: 'Google Sans'; } body { margin: 0; } $gap : 4vh; $padding : 4vh; $bp : (mobile : 480px, tablet : 768px, desktop : 1440px, ); @mixin query($display) { @each $key, $value in $bp { // defining max-width @if ($display ==$key) { @media (max-width: $value) { @content; } } } } .container { display: flex; //to lay .block-* classes in a column flex-direction: column; //Setting gap between the .block-* classes gap: $gap; // to set some padding & border inside padding: $padding; } ================================================ FILE: frontend/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [ "node" ] }, "files": [ "src/main.ts", "src/main.server.ts", "server.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: frontend/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "resolveJsonModule": true, "useDefineForClassFields": false, "lib": [ "ES2022", "dom" ] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: frontend/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine" ] }, "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: notebooks/0_CopyDataToBigQuery.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", " \n", " \n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "copyright" }, "outputs": [], "source": [ "# Copyright 2024 Google LLC\n", "#\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ] }, { "cell_type": "markdown", "metadata": { "id": "DRyGcAepAPJ5" }, "source": [ "\n", "# **Open Data QnA: Set up BigQuery Source**\n", "\n", "---\n", "\n", "This notebook shows how to copy a BigQuery public dataset to your GCP project \n", "\n", "\n", "This is accomplished through the three following steps: \n", "> i. Create a BigQuery dataset in your GCP project\n", "\n", "> ii. Create a table in the above dataset\n", "\n", "> iii. Copy data from the public dataset to the dataset on your project\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Change your Kernel to the created .venv with poetry from README.md**\n", "\n", "Below is the Kernel how it should look like before you proceed\n", "\n", "![Kernel](../utilities/imgs/Kernel%20Changed.png)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🔗 **1. Connect Your Google Cloud Project**\n", "Time to connect your Google Cloud Project to this notebook. " ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Updated property [core/project].\n", "Project has been set to three-p-o\n" ] } ], "source": [ "#@markdown Please fill in the value below with your GCP project ID and then run the cell.\n", "PROJECT_ID = input(\"Enter the project id (same as your Setup Project) to copy source data in bigquery for this solution\")\n", "\n", "# Quick input validation\n", "assert PROJECT_ID, \"⚠️ Please provide your Google Cloud Project ID\"\n", "\n", "# Configure gcloud.\n", "!gcloud config set project {PROJECT_ID}\n", "print(f'Project has been set to {PROJECT_ID}')" ] }, { "cell_type": "markdown", "metadata": { "id": "yygMe6rPWxHS" }, "source": [ "## 🔐 **2. Authenticate to Google Cloud**\n", "Authenticate to Google Cloud as the IAM user logged into this notebook in order to access your Google Cloud Project.\n", "\n", "You can do this within Google Colab or using the Application Default Credentials in the Google Cloud CLI." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "id": "PTXN1_DSXj2b" }, "outputs": [], "source": [ "# Authentication step\n", "\n", "\"\"\"Colab Auth\"\"\" \n", "# from google.colab import auth\n", "# auth.authenticate_user()\n", "\n", "\n", "\"\"\"Jupiter Notebook Auth\"\"\"\n", "import google.auth\n", "import os\n", "\n", "credentials, project_id = google.auth.default()\n", "\n", "os.environ['GOOGLE_CLOUD_QUOTA_PROJECT']=PROJECT_ID\n", "os.environ['GOOGLE_CLOUD_PROJECT']=PROJECT_ID" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Enable all the required APIs for the COPY\n", "\n", "!gcloud services enable \\\n", " cloudapis.googleapis.com \\\n", " compute.googleapis.com \\\n", " iam.googleapis.com \\\n", " bigquery.googleapis.com --project {PROJECT_ID}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ☁️ **Copy a Public Dataset to your GCP Project**\n", "\n", "Copy a table from the public dataset to ask questions against. A sample table is chosen below, feel free to choose a different one. \n", "\n", "Note: BigQuery does not allow tables to be copied across regions in certain cases. Therefore, BQ_DST_REGION is set to be BQ_SRC_REGION. Change this parameter to check if copy to your region of interest is allowed.\n", "\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# Details of source Dataset\n", "BQ_SRC_PROJECT = \"bigquery-public-data\"\n", "BQ_SRC_DATASETS = [\"imdb\", \"imdb\"]\n", "BQ_SRC_TABLES_LIST = [[\"title_principals\",\"title_crew\", \"title_basics\",\"name_basics\"], [\"reviews\", \"title_ratings\"]] # [] Specify empty list to copy 'all' tables, or a Specific list, eg: [\"table1\", \"table3\", \"table10\"]\n", "BQ_SRC_REGIONS = [\"us\", \"us\"]\n", "\n", "# Details of destination Dataset\n", "BQ_DST_PROJECT = PROJECT_ID\n", "BQ_DST_DATASETS =[ \"imdb_people\", \"imdb_ratings\"] # List of destinaion dataset names\n", "BQ_DST_REGIONS = BQ_SRC_REGIONS # Change if needed" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def createBQDataset(bq_project_id, dataset_name,dataset_region):\n", " from google.cloud import bigquery\n", " import google.api_core \n", "\n", " client=bigquery.Client(project=PROJECT_ID)\n", "\n", " dataset_ref = f\"{bq_project_id}.{dataset_name}\"\n", " \n", "\n", " try:\n", " client.get_dataset(dataset_ref)\n", " print(\"Destination Dataset exists\")\n", " except google.api_core.exceptions.NotFound:\n", " print(\"Cannot find the dataset hence creating.......\")\n", " dataset=bigquery.Dataset(dataset_ref)\n", " dataset.location=dataset_region\n", " client.create_dataset(dataset)\n", " \n", " return dataset_ref\n", "\n", "def createBQTable(bq_project_id,dataset_name, table_name):\n", " from google.cloud import bigquery\n", " import google.api_core \n", "\n", " client=bigquery.Client(project=PROJECT_ID)\n", "\n", " table_ref = client.dataset(dataset_name, project=bq_project_id).table(table_name)\n", "\n", " try:\n", " client.get_table(table_ref)\n", " print(f\"Destination Table {table_ref} exists\")\n", " \n", " except google.api_core.exceptions.NotFound:\n", " print(f\"Creating the table {table_ref}.......\")\n", " table = bigquery.Table(table_ref)\n", " client.create_table(table)\n", "\n", " return table_ref\n", "\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cannot find the dataset hence creating.......\n", "Creating the table three-p-o.imdb_people.title_principals.......\n", "Creating the table three-p-o.imdb_people.title_crew.......\n", "Creating the table three-p-o.imdb_people.title_basics.......\n", "Creating the table three-p-o.imdb_people.name_basics.......\n", "Cannot find the dataset hence creating.......\n", "Creating the table three-p-o.imdb_ratings.reviews.......\n", "Creating the table three-p-o.imdb_ratings.title_ratings.......\n", "Done!\n" ] } ], "source": [ "#Create destination table and copy table data\n", "from google.cloud import bigquery\n", "\n", "# Initialize BQ client\n", "client=bigquery.Client(project=PROJECT_ID)\n", "\n", "for BQ_SRC_DATASET, BQ_SRC_TABLES, BQ_SRC_REGION, BQ_DST_DATASET, BQ_DST_REGION, in zip(BQ_SRC_DATASETS, BQ_SRC_TABLES_LIST, BQ_SRC_REGIONS, BQ_DST_DATASETS, BQ_DST_REGIONS):\n", " \n", " # Create Destination Dataset (If the dataset already exists, delete the dataset (and the tables with in) and create an empty dataset)\n", " dst_dataset_ref=createBQDataset(BQ_DST_PROJECT,BQ_DST_DATASET,BQ_DST_REGION)\n", "\n", " if not BQ_SRC_TABLES:\n", " #if tables are not explicitly provided, get the list of tables from bigquery\n", " dataset_id = f'{BQ_SRC_PROJECT}.{BQ_SRC_DATASET}'\n", " bq_tables_obj = client.list_tables(dataset_id)\n", " BQ_SRC_TABLES = [table_obj.table_id for table_obj in bq_tables_obj]\n", " \n", " for BQ_SRC_TABLE in BQ_SRC_TABLES:\n", "\n", " dst_table_ref=createBQTable(BQ_DST_PROJECT,BQ_DST_DATASET,BQ_SRC_TABLE)\n", " src_table_ref = client.dataset(BQ_SRC_DATASET, project=BQ_SRC_PROJECT).table(BQ_SRC_TABLE)\n", "\n", " job_config = bigquery.CopyJobConfig(write_disposition=\"WRITE_TRUNCATE\")\n", "\n", " copy_job = client.copy_table(src_table_ref, dst_table_ref, job_config=job_config)\n", " # Wait for the job to complete and check for errors\n", " copy_job.result() \n", "\n", "print('Done!')\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### If all the above steps are executed suucessfully, the Bigquery Public dataset should be copied to your GCP project" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "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.4" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: notebooks/0_CopyDataToCloudSqlPG.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", " \n", " \n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "copyright" }, "outputs": [], "source": [ "# Copyright 2024 Google LLC\n", "#\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ] }, { "cell_type": "markdown", "metadata": { "id": "DRyGcAepAPJ5" }, "source": [ "\n", "# **Open Data QnA: Set up Dataset on CloudSQL for PostgreSQL**\n", "\n", "---\n", "\n", "This notebook shows how to copy a BigQuery public dataset to CloudSQL for PostgreSQL\n", "\n", "\n", "This is accomplished through the three following steps: \n", "> i. Set up PostgreSQL instance and databae on Google Cloud SQL\n", "\n", "> ii. Copy the BigQuery table to Cloud Storage Bucket\n", "\n", "> iii. Create the table in PostgreSQL databae using csv file in Cloud Storage Bucket\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Change your Kernel to the created .venv with poetry from README.md**\n", "\n", "Below is the Kernel how it should look like before you proceed\n", "\n", "![Kernel](../utilities/imgs/Kernel%20Changed.png)" ] }, { "cell_type": "markdown", "metadata": { "id": "p4W6FPnrYEE8" }, "source": [ "## 🔗 **1. Connect Your Google Cloud Project**\n", "Time to connect your Google Cloud Project to this notebook. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#@markdown Please fill in the value below with your GCP project ID and then run the cell.\n", "PROJECT_ID = input(\"Enter the project id (same as your Setup Project) to copy source data in bigquery for this solution\")\n", "\n", "# Quick input validations.\n", "assert PROJECT_ID, \"⚠️ Please provide your Google Cloud Project ID\"\n", "\n", "# Configure gcloud.\n", "!gcloud config set project {PROJECT_ID}\n", "print(f'Project has been set to {PROJECT_ID}')\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "yygMe6rPWxHS" }, "source": [ "## 🔐 **2. Authenticate to Google Cloud**\n", "Authenticate to Google Cloud as the IAM user logged into this notebook in order to access your Google Cloud Project.\n", "\n", "You can do this within Google Colab or using the Application Default Credentials in the Google Cloud CLI." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "PTXN1_DSXj2b" }, "outputs": [], "source": [ "\"\"\"Colab Auth\"\"\" \n", "# from google.colab import auth\n", "# auth.authenticate_user()\n", "\n", "\n", "\"\"\"Jupiter Notebook Auth\"\"\"\n", "import google.auth\n", "import os\n", "\n", "credentials, project_id = google.auth.default()\n", "\n", "os.environ['GOOGLE_CLOUD_QUOTA_PROJECT']=PROJECT_ID\n", "os.environ['GOOGLE_CLOUD_PROJECT']=PROJECT_ID" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Enable all the required APIs for the COPY\n", "\n", "!gcloud services enable \\\n", " cloudapis.googleapis.com \\\n", " compute.googleapis.com \\\n", " iam.googleapis.com \\\n", " sqladmin.googleapis.com \\\n", " bigquery.googleapis.com --project {PROJECT_ID}" ] }, { "cell_type": "markdown", "metadata": { "id": "noWgbDQQO7mr" }, "source": [ "## ☁️ **3. Set up Cloud SQL PostgreSQL Instance** \n", "A **Postgres** Cloud SQL instance is required for the following stages of this notebook.\n", "\n", "To connect and access our Postgres Cloud SQL database instance(s) we will leverage the [Cloud SQL Python Connector](https://github.com/GoogleCloudPlatform/cloud-sql-python-connector).\n", "\n", "The Cloud SQL Python Connector is a library that can be used alongside a database driver to allow users to easily connect to a Cloud SQL database without having to manually allowlist IP or manage SSL certificates. " ] }, { "cell_type": "markdown", "metadata": { "id": "ypjpse8yBRdI" }, "source": [ "💽 **Create a Postgres Instance**\n", "\n", "Running the below cell will verify the existence of a Cloud SQL instance or create a new one if one does not exist.\n", "\n", "> ⏳ - Creating a Cloud SQL instance may take a few minutes." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "_vIX7rNtVLhn" }, "outputs": [], "source": [ "#@markdown Please fill in the both the Google Cloud region and name of your Cloud SQL instance. Once filled in, run the cell.\n", "\n", "# Below are the recommended defaults; These can be changed to values of your choice\n", "PG_REGION = \"us-central1\" #@param {type:\"string\"}\n", "PG_INSTANCE = \"pg15-opendataqna\"\n", "PG_DATABASE = \"opendataqna-db\"\n", "PG_USER = \"pguser\"\n", "PG_PASSWORD = \"pg123\"\n", "\n", "# Quick input validations.\n", "assert PG_REGION, \"us-central1\"\n", "assert PG_INSTANCE, \"pg15-opendataqna\"\n", "\n", "# check if Cloud SQL instance exists in the provided region and create it if it does not exist\n", "database_version = !gcloud sql instances describe {PG_INSTANCE} --format=\"value(databaseVersion)\"\n", "if database_version[0].startswith(\"POSTGRES\"):\n", " print(\"Found existing Postgres Cloud SQL Instance!\")\n", "else:\n", " print(\"Creating new Cloud SQL instance...\")\n", " !gcloud sql instances create {PG_INSTANCE} --database-version=POSTGRES_15 \\\n", " --region={PG_REGION} --cpu=1 --memory=4GB --root-password={PG_PASSWORD} \\\n", " --database-flags=cloudsql.iam_authentication=On\n", "\n", "# Create a database on the instance and a user with password\n", "!gcloud sql databases create {PG_DATABASE} --instance={PG_INSTANCE}\n", "!gcloud sql users create {PG_USER} \\\n", "--instance={PG_INSTANCE} \\\n", "--password={PG_PASSWORD}" ] }, { "cell_type": "markdown", "metadata": { "id": "nzb0dFO6C4h6" }, "source": [ "## ➡️ **4. Migrate a public BigQuery database to PostgreSQL instance**\n", "Let's migrate a public BigQuery dataset over to the newly created PostgreSQL instance. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### A) Set up a Google Cloud Storage Bucket \n", "This bucket will be used to store the exported BigQuery public dataset." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "0q5uFF0sJnWK" }, "outputs": [], "source": [ "from google.cloud import storage\n", "from urllib.error import HTTPError\n", "\n", "# Choose a name for the bucket; You might have to choose a different name if the name below already exists\n", "BUCKET_NAME = str(PROJECT_ID+'-opendataqna') #@param {type:\"string\"} \n", "\n", "# Creating a bucket\n", "storage_client = storage.Client(project=PROJECT_ID)\n", "\n", "try: \n", " bucket = storage_client.bucket(BUCKET_NAME)\n", "\n", " if bucket.exists(): \n", " print(\"This bucket already exists.\")\n", "\n", " else:\n", " bucket = storage_client.create_bucket(BUCKET_NAME)\n", " print(f\"Bucket {bucket.name} created\")\n", "\n", "except:\n", " print(\"⚠️ This bucket already exists in another project. Make sure to give your bucket a unique name.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### B) Export BigQuery Dataset to the Bucket\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#@markdown Please choose a BigQuery Public dataset to export. You can leave the default values. Once filled in, run the cell.\n", "\n", "# Below are the recommended defaults; You may choose a different database to export\n", "BQ_PROJECT = \"bigquery-public-data\"\n", "BQ_DATABASE = \"census_bureau_international\"\n", "bq_tables = [] # Specify empty list to copy 'all' tables, or a Specific list, eg: [\"table1\", \"table3\", \"table10\"]\n", "\n", "\n", "# Quick input validations.\n", "assert BQ_PROJECT, \"⚠️ Please specify the BigQuery Project\"\n", "assert BQ_DATABASE, \"⚠️ Please specify the BigQuery Database\"\n", "\n", "from google.cloud import bigquery\n", "client = bigquery.Client(project=PROJECT_ID)\n", "dataset_id = f'{BQ_PROJECT}.{BQ_DATABASE}'\n", "\n", "if not bq_tables:\n", " bq_tables_obj = client.list_tables(dataset_id)\n", " bq_tables = [table_obj.table_id for table_obj in bq_tables_obj]\n", "\n", "print(f'List of tables in {dataset_id}: {bq_tables}')\n", "destination_uris = []\n", "\n", "for bq_table in bq_tables:\n", " # Export the bigquery data to Google Bucket\n", " destination_uri = f\"gs://{BUCKET_NAME}/{BQ_DATABASE}/{bq_table}.csv\"\n", " dataset_ref = bigquery.DatasetReference(BQ_PROJECT, BQ_DATABASE)\n", " table_ref = dataset_ref.table(bq_table)\n", "\n", " destination_uris.append(destination_uri)\n", "\n", " extract_job = client.extract_table(\n", " table_ref,\n", " destination_uri,\n", " # Location must match that of the source table.\n", " location=\"US\",\n", " ) # API request\n", " extract_job.result() # Waits for job to complete.\n", "\n", " print(f\"Exported {BQ_PROJECT}:{BQ_DATABASE}.{bq_table} to {destination_uri}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### C) Retrieve Data Types and Formats \n", "To migrate the exported .csv files to PostgreSQL, we need to fetch the Data Types and Format from the exported csv file.\n", "This needs to be done as we're setting up the PostgreSQL table and columns first (and need to provide the columns in the setup).\n", "We will load the .csv content into the table afterwards. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "field_names_list = []\n", "field_types_list = []\n", "\n", "for destination_uri in destination_uris:\n", "\n", " df = pd.read_csv(destination_uri)\n", " field_names = df.columns\n", " field_names_list.append(field_names)\n", " print(f'Column Names: {field_names}\\n')\n", " field_types = df.dtypes\n", " field_types_list.append(field_types)\n", " print(f'Column Names: {field_types}')\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### D) Build the SQL Query for Table Creation \n", "Every database is different. To acommodate for different table structures depending on which BigQuery dataset is being loaded in, we will build the SQL query for creating the required PostgreSQL table dynamically. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_sql(pg_schema, bq_table, field_names, field_types): \n", "\n", " cols = \"\" \n", "\n", " for i in range(len(field_names)): \n", " cols += str(field_names[i]) +\" \"+ str(field_types[i])\n", " if i < (len(field_names)-1): \n", " cols += \", \"\n", "\n", "\n", " sql = f\"\"\"CREATE TABLE {pg_schema}.{bq_table}({cols})\"\"\"\n", "\n", " return sql\n", "\n", "#Please specify the PGSchema or leave it as default (public)\n", "PG_SCHEMA = BQ_DATABASE\n", "create_table_sqls = []\n", "\n", "for bq_table, field_names, field_types in zip(bq_tables, field_names_list, field_types_list):\n", "\n", " sql = get_sql(PG_SCHEMA, bq_table, field_names, field_types)\n", " print(f'\\nsql for creating {bq_table} PostgreSQL table: \\n{sql} \\n')\n", " create_table_sqls.append(sql)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### E) Create the PostgreSQL Table" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import asyncio \n", "import asyncpg\n", "from google.cloud.sql.connector import Connector\n", "\n", "async def create_pg_schema(PROJECT_ID,\n", " PG_REGION,\n", " PG_INSTANCE,\n", " PG_PASSWORD, \n", " PG_DATABASE,\n", " PG_USER,\n", " PG_SCHEMA): \n", " \"\"\"Delete if PG Schema Exists and create a fresh copy\"\"\"\n", " loop = asyncio.get_running_loop()\n", " async with Connector(loop=loop) as connector:\n", " # Create connection to Cloud SQL database\n", " conn: asyncpg.Connection = await connector.connect_async(\n", " f\"{PROJECT_ID}:{PG_REGION}:{PG_INSTANCE}\", # Cloud SQL instance connection name\n", " \"asyncpg\",\n", " user=f\"{PG_USER}\",\n", " db=f\"{PG_DATABASE}\",\n", " password=f\"{PG_PASSWORD}\"\n", " )\n", "\n", " await conn.execute(f\"DROP SCHEMA IF EXISTS {PG_SCHEMA} CASCADE\") \n", "\n", " await conn.execute(f\"CREATE SCHEMA {PG_SCHEMA}\") \n", "\n", " await conn.close()\n", "\n", "\n", "async def create_pg_table(PROJECT_ID,\n", " PG_REGION,\n", " PG_INSTANCE,\n", " PG_PASSWORD,\n", " bq_tables, \n", " PG_DATABASE, \n", " create_table_sqls,\n", " PG_USER): \n", " \"\"\"Create PG Table from BQ Schema\"\"\"\n", " \n", " \n", " loop = asyncio.get_running_loop()\n", " async with Connector(loop=loop) as connector:\n", " # Create connection to Cloud SQL database\n", " conn: asyncpg.Connection = await connector.connect_async(\n", " f\"{PROJECT_ID}:{PG_REGION}:{PG_INSTANCE}\", # Cloud SQL instance connection name\n", " \"asyncpg\",\n", " user=f\"{PG_USER}\",\n", " db=f\"{PG_DATABASE}\",\n", " password=f\"{PG_PASSWORD}\"\n", " )\n", "\n", " \n", " for bq_table, sql in zip(bq_tables, create_table_sqls):\n", " # Replace the Data Types to work with PostgreSQL supported ones \n", " sql = sql.replace(\"object,\", \"TEXT,\").replace(\"int64\", \"INTEGER\").replace(\"float64\", \"DOUBLE PRECISION\")\n", "\n", "\n", " await conn.execute(f\"DROP TABLE IF EXISTS {bq_table} CASCADE\")\n", " \n", " # Create the table.\n", " await conn.execute(sql)\n", "\n", " await conn.close()\n", "\n", "# Delete schema if exists and create a fresh copy\n", "await(create_pg_schema(PROJECT_ID, PG_REGION, PG_INSTANCE, PG_PASSWORD, PG_DATABASE, PG_USER, PG_SCHEMA))\n", "# # Create PG Tables\n", "await(create_pg_table(PROJECT_ID, PG_REGION, PG_INSTANCE, PG_PASSWORD, bq_tables, PG_DATABASE, create_table_sqls,PG_USER))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### F) Import Data to PostgreSQL Table\n", "The below cell will iterate through each export file on our Google Cloud Storage Bucket and load it to the PostgreSQL instance. \n", "This may take a while, depending on the size of the BigQuery public dataset. You can optionally set the LIMIT parameter to limit how many export files will be loaded in. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "async def import_to_pg(PROJECT_ID,\n", " PG_REGION,\n", " PG_INSTANCE,\n", " PG_USER,\n", " PG_PASSWORD,\n", " PG_DATABASE,\n", " PG_SCHEMA,\n", " bq_tables, \n", " BUCKET_NAME): \n", " from google.cloud import storage\n", " import pandas as pd \n", " import asyncio\n", " import asyncpg\n", " from google.cloud.sql.connector import Connector\n", "\n", " storage_client = storage.Client(project=PROJECT_ID)\n", "\n", " # bucket = storage_client.get_bucket(BUCKET_NAME)\n", " # blobs = bucket.list_blobs()\n", "\n", " loop = asyncio.get_running_loop()\n", " async with Connector(loop=loop) as connector:\n", "\n", "\n", " # Create connection to Cloud SQL database\n", " conn: asyncpg.Connection = await connector.connect_async(\n", " f\"{PROJECT_ID}:{PG_REGION}:{PG_INSTANCE}\", # Cloud SQL instance connection name\n", " \"asyncpg\",\n", " user=f\"{PG_USER}\",\n", " password=f\"{PG_PASSWORD}\",\n", " db=f\"{PG_DATABASE}\",\n", " )\n", " \n", " for bq_table in bq_tables:\n", " URI = f\"gs://{BUCKET_NAME}/{PG_SCHEMA}/{bq_table}.csv\"\n", " print(f'URI:{URI}')\n", " df = pd.read_csv(URI)\n", " df = df.dropna()\n", " df.info() \n", "\n", " # Copy the dataframe to the table.\n", " tuples = list(df.itertuples(index=False))\n", "\n", " await conn.copy_records_to_table(\n", " bq_table, records=tuples, columns=list(df), schema_name=PG_SCHEMA, timeout=3600\n", " )\n", " await conn.close()\n", "\n", "# # Load Data into PG Table \n", "await(import_to_pg(PROJECT_ID, PG_REGION, PG_INSTANCE, PG_USER, PG_PASSWORD, PG_DATABASE, PG_SCHEMA,bq_tables, BUCKET_NAME))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### If all the above steps are executed suucessfully, the Bigquery public dataset should be copied to Cloud SQL for PostgreSQL on your GCP project" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "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.11.9" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: notebooks/1_Setup_OpenDataQnA.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", " \n", " \n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "copyright" }, "outputs": [], "source": [ "# Copyright 2024 Google LLC\n", "#\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ] }, { "cell_type": "markdown", "metadata": { "id": "DRyGcAepAPJ5" }, "source": [ "\n", "

Open Data QnA - Chat with your SQL Database

\n", "\n", "---\n", "\n", "This notebook first walks through the Vector Store Setup needed for running the Open Data QnA application. \n", "\n", "Currently supported Source DBs are: \n", "- PostgreSQL on Google Cloud SQL \n", "- BigQuery\n", "\n", "Furthermore, the following vector stores are supported \n", "- pgvector on PostgreSQL \n", "- BigQuery vector\n", "\n", "\n", "The setup part covers the following steps: \n", "> 1. Configuration: Intial GCP project, IAM permissions, Environment and Databases setup including logging on Bigquery for analytics\n", "\n", "> 2. Creation of Table, Column and Known Good Query Embeddings in the Vector Store for Retreival Augmented Generation(RAG)\n", "\n", "> 3. Setting up firestore for persisting the session history for multiturn\n", "\n", "\n", "Afterwards, you will be able to run the Open Data QnA Pipeline to generate SQL queries and answer questions over your data source. " ] }, { "cell_type": "markdown", "metadata": { "id": "jsWGZW_fUJjN" }, "source": [ "### 📒 Using this interactive notebook\n", "\n", "If you have not used this IDE with jupyter notebooks it will ask for installing Python + Jupyter extensions. Please go ahead install them\n", "\n", "Click the **run** icons ▶️ of each cell within this notebook.\n", "\n", "> 💡 Alternatively, you can run the currently selected cell with `Ctrl + Enter` (or `⌘ + Enter` on a Mac).\n", "\n", "> ⚠️ **To avoid any errors**, wait for each section to finish in their order before clicking the next “run” icon.\n", "\n", "This sample must be connected to a **Google Cloud project**, but nothing else is needed other than your Google Cloud project.\n", "\n", "You can use an existing project. Alternatively, you can create a new Cloud project [with cloud credits for free.](https://cloud.google.com/free/docs/gcp-free-tier)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Change your Kernel to the created .venv with poetry from README.md**\n", "\n", "Path would look like e.g. /home/admin_/opendata/.venv/bin/python or ~cache/user/opendataqna/.venv/bin/python\n", "\n", "Below is the Kernel how it should look like before you proceed.\n", "\n", "\n", "![Kernel](../utilities/imgs/Kernel%20Changed.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Set Python Module Path to Root" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os\n", "import sys\n", "\n", "module_path = os.path.abspath(os.path.join('..'))\n", "sys.path.append(module_path)" ] }, { "cell_type": "markdown", "metadata": { "id": "p4W6FPnrYEE8" }, "source": [ "### 🔗 **Connect Your Google Cloud Project**\n", "Time to connect your Google Cloud Project to this notebook. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Updated property [core/project].\n", "Project has been set to three-p-o\n" ] } ], "source": [ "#@markdown Please fill in the value below with your GCP project ID and then run the cell.\n", "PROJECT_ID = \"three-p-o\"\n", "\n", "# Quick input validations.\n", "assert PROJECT_ID, \"⚠️ Please provide your Google Cloud Project ID\"\n", "\n", "# Configure gcloud.\n", "!gcloud config set project {PROJECT_ID}\n", "print(f'Project has been set to {PROJECT_ID}')\n", "\n", "os.environ['GOOGLE_CLOUD_QUOTA_PROJECT']=PROJECT_ID\n", "os.environ['GOOGLE_CLOUD_PROJECT']=PROJECT_ID\n", "\n", "#If errors out for authentication restart the kernel and start from the previous cell" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ⚙️ **Enable Required API Services in the GCP Project**" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Operation \"operations/acat.p2-978842762722-9cbec4fc-9e93-41cf-a354-4a2ff66161d4\" finished successfully.\n" ] } ], "source": [ "#Enable all the required APIs for the Open Data QnA solution\n", "\n", "!gcloud services enable \\\n", " cloudapis.googleapis.com \\\n", " compute.googleapis.com \\\n", " iam.googleapis.com \\\n", " run.googleapis.com \\\n", " sqladmin.googleapis.com \\\n", " aiplatform.googleapis.com \\\n", " bigquery.googleapis.com \\\n", " firestore.googleapis.com --project {PROJECT_ID}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **Configure your inputs for the environments**\n", "\n", "This section assumes that a datasource is already set up in your GCP project. If a datasource has not been set up, use the notebooks below to copy a public data set from BigQuery to Cloud SQL or BigQuery on your GCP project\n", "\n", "\n", "Enabled Data Sources:\n", "* PostgreSQL on Google Cloud SQL (Copy Sample Data: [0_CopyDataToCloudSqlPG.ipynb](0_CopyDataToCloudSqlPG.ipynb))\n", "* BigQuery (Copy Sample Data: [0_CopyDataToBigQuery.ipynb](0_CopyDataToBigQuery.ipynb))\n", "\n", "Enabled Vector Stores:\n", "* pgvector on PostgreSQL \n", "* BigQuery vector\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 🤔 **Choose Data Source and Vector Store**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Sources to connect**\n", "- This solution lets you setup multiple data source at the same time.\n", "- You can group multiple tables from different datasets or schema into a grouping and provide the details\n", "- If your dataset/schema has many tables and you want to run the solution against few you should specifically choose a group for that tables only\n", "\n", "**Format for data_source_list.csv**\n", "\n", "**source | user_grouping | schema | table**\n", "\n", "**source** - Supported Data Sources. #Options: bigquery , cloudsql-pg\n", "\n", "**user_grouping** - Logical grouping or use case name for tables from same or different schema/dataset. When left black it default to the schema value in the next column\n", "\n", "**schema** - schema name for postgres or dataset name in bigquery \n", "\n", "**table** - name of the tables to run the solutions against. Leave this column blank after filling schema/dataset if you want to run solution for whole dataset/schema\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Fill out the parameters and configuration settings below. \n", "These are the parameters for setting configurations for the vector store tables to be created. \n", "\n", "Additionally, you can specify whether you have and want to use known-good-queries for the pipeline run and whether you want to enable logging.\n", "\n", "**Known good queries:** if you have known working user question <-> SQL query pairs, you can put them into the file `scripts/known_good_sql.csv`. This will be used as a caching layer and for in-context learning: If an exact match of the user question is found in the vector store, the pipeline will skip SQL Generation and output the cached SQL query. If the similarity score is between 90-100%, the known good queries will be used as few-shot examples by the SQL Generator Agent. \n", "\n", "**Logging:** you can enable logging. If enabled, a dataset is created in Big Query in your project, which will store the logging table and save information from the pipeline run in the logging table. This is especially helpful for debugging.\n", "\n", "**use_column_samples:** you can enable use column samples flag to let the pipeline select sample values of the columns from the source database. In some specific usecase where we need to understand the format or case sensitivity of the values this flag help LLM to have better understanding. Though this is one time setup, please be aware that turning this on mean getting samples from each column and it can be an expensive operation when there are lot many columns." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#[CONFIG]\n", "embedding_model = 'vertex' # Options: 'vertex' or 'vertex-lang'\n", "description_model = 'gemini-1.5-pro' # 'gemini-1.0-pro', 'gemini-1.5-pro', 'text-bison-32k', 'gemini-1.5-flash'\n", "vector_store = 'bigquery-vector' # Options: 'bigquery-vector', 'cloudsql-pgvector'\n", "logging = True # True or False \n", "kgq_examples = True # True or False\n", "use_session_history = True # True or False\n", "use_column_samples = True #True or False\n", "\n", "#[GCP]\n", "project_id = PROJECT_ID\n", "\n", "#[PGCLOUDSQL]\n", "# Default values for pgvector setup, change only if needed\n", "pg_region = 'us-central1'\n", "pg_instance = 'pg15-opendataqna'\n", "pg_database = 'opendataqna-db'\n", "pg_user = 'pguser'\n", "pg_password = 'pg123'\n", "\n", "#[BIGQUERY]\n", "# Name for the BQ dataset created for bigquery-vector and/or logging. Change names only if needed.\n", "bq_dataset_region = 'us-central1'\n", "bq_opendataqna_dataset_name = 'opendataqna'\n", "bq_log_table_name = 'audit_log_table'\n", "\n", "#Details for firestore to store the chat session history\n", "firestore_region='us-central1'\n", "## firestore_database is named as 'opendataqna-session-logs' (This is designed to not be customizable as the setup includes creation of composite indexes from backend)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Fill out the parameters and configuration settings below.\n", "These are the parameters for setting configurations for the vector store tables to be created.\n", "\n", "Additionally, you can specify whether you have and want to use known-good-queries for the pipeline run and whether you want to enable logging.\n", "\n", "**Known good queries:** if you have known working user question <-> SQL query pairs, you can put them into the file `scripts/known_good_sql.csv`. This will be used as a caching layer and for in-context learning: If an exact match of the user question is found in the vector store, the pipeline will skip SQL Generation and output the cached SQL query. If the similarity score is between 90-100%, the known good queries will be used as few-shot examples by the SQL Generator Agent.\n", "\n", "**Logging:** you can enable logging. If enabled, a dataset is created in Big Query in your project, which will store the logging table and save information from the pipeline run in the logging table. This is especially helpful for debugging.\n", "\n", "**use_column_samples = yes** if you want the solution to collect some samples values from the data source columns to imporve understanding of values. yes or no\n", "\n", "**use_column_samples:** you can enable use column samples flag to let the pipeline select sample values of the columns from the source database. In some specific usecase where we need to understand the format or case sensitivity of the values this flag help LLM to have better understanding. Though this is one time setup, please be aware that turning this on mean getting samples from each column and it can be an expensive operation when there are lot many columns." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Quick input verifications below:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\n", "# Input verification - Vector Store\n", "assert vector_store in {'bigquery-vector', 'cloudsql-pgvector'}, \"⚠️ Invalid VECTOR_STORE. Must be 'bigquery-vector' or 'cloudsql-pgvector'\"\n", "\n", "# Input verification - Firestore Region\n", "assert firestore_region, \"⚠️ Provide firestore region name\"\n", "\n", "if logging: \n", " assert bq_log_table_name, \"⚠️ Please provide a name for your log table if you want to use logging\"\n", "\n", "if vector_store == 'bigquery':\n", " assert bq_dataset_region, \"⚠️ Please provide the Data Set Region\"\n", " assert bq_opendataqna_dataset_name, \"⚠️ Please provide the name of the logging/vector store dataset on Bigquery\"\n", "\n", "elif vector_store == 'cloudsql-pg':\n", " assert pg_region, \"⚠️ Please provide Region of the Cloud SQL Instance\"\n", " assert pg_instance, \"⚠️ Please provide the name of the Cloud SQL Instance\"\n", " assert pg_database, \"⚠️ Please provide the name of the PostgreSQL Database on the Cloud SQL Instance\"\n", " assert pg_user, \"⚠️ Please provide a username for the Cloud SQL Instance\"\n", " assert pg_password, \"⚠️ Please provide the Password for the PG_USER\"\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 💾 **Save Configuration to File** \n", "Save the configurations set in this notebook to `config.ini`. The parameters from this file are used in notebooks and in various modeules in the repo" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from scripts import save_config\n", "\n", "save_config(embedding_model, description_model, vector_store, logging, kgq_examples, use_column_samples, PROJECT_ID,\n", " pg_region, pg_instance, pg_database, pg_user, pg_password, \n", " bq_dataset_region, bq_opendataqna_dataset_name, bq_log_table_name,firestore_region)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# **1. Vector Store Setup** (Run once)\n", "---\n", "\n", "This section walks through the Vector Store Setup needed for running the Open Data QnA application. \n", "\n", "It covers the following steps: \n", "> 1. Configuration: Environment and Databases setup including logging on Bigquery for analytics\n", "\n", "> 2. Creation of Table, Column and Known Good Query Embeddings in the Vector Store for Retreival Augmented Generation(RAG)\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ⚙️ **1.1 Database Setup for Vector Store**\n", "\n", "If BigQuery is your vector store, the dataset is created.\n", "\n", "If 'cloudsql-pgvector' is chosen as vector store, PostgreSQL Instance on CloudSQL (Note that this version of code supports only creating vector store on same instance as source)\n", "\n", "The cell will also create a dataset to store the log table on Big Query, **if** logging is enabled if its not bigquery vector." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from env_setup import create_vector_store\n", "# Setup vector store for embeddings\n", "create_vector_store() \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## **1.2 Create Embeddings in Vector Store for RAG** " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 🖋️ **Create Table and Column Embeddings**\n", "\n", "In this step, table and column metadata is retreived from the data source and embeddings are generated for both" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from env_setup import get_embeddings\n", "\n", "# Generate embeddings for tables and columns\n", "table_schema_embeddings, col_schema_embeddings = get_embeddings()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 💾 **Save the Table and Column Embeddings in the Vector Store**\n", "The table and column embeddings created in the above step are save to the Vector Store chosen" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from env_setup import store_embeddings\n", "\n", "# Store table/column embeddings (asynchronous)\n", "await(store_embeddings(table_schema_embeddings, col_schema_embeddings))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# **2. Firestore Database Setup**\n", "---\n", "\n", "This section walks through setting up the firestore DB to store the session history of the conversation for multiturn\n", "\n", "It covers the following steps: \n", "> 1. Creation Firestore Database\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from env_setup import create_firestore_db\n", "\n", "create_firestore_db(firestore_region)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🗄️ **3. Load Known Good SQL into Vector Store**\n", "Known Good Queries are used to create query cache for Few shot examples. Creating a query cache is highly recommended for best outcomes! \n", "\n", "The following cell will load the Natural Language Question and Known Good SQL pairs into our Vector Store. There pairs are loaded from `known_good_sql.csv` file inside scripts folder. If you have your own Question-SQL examples, curate them in .csv file before running the cell below. \n", "\n", "If no Known Good Queries are available at this time to create query cache, you can use [3_LoadKnownGoodSQL.ipynb](3_LoadKnownGoodSQL.ipynb) to load them later. \n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Format of the Known Good SQL File (known_good_sql.csv)\n", "\n", "prompt | sql | user_grouping [3 columns]\n", "\n", "prompt ==> User Question \n", "\n", "sql ==> SQL for the user question (Note that the sql should enclosed in quotes and only in single line. Please remove the line break)\n", "\n", "user_grouping ==>This name should exactly match the grouping name you mentioned while creating vector store" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from env_setup import create_kgq_sql_table, store_kgq_sql_embeddings\n", "\n", "# Create table for known good queries (if enabled)\n", "await(create_kgq_sql_table()) \n", "\n", "# Store known good query embeddings (if enabled)\n", "await(store_kgq_sql_embeddings()) \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 🥁 If all the above steps are executed suucessfully, the following should be set up:\n", "\n", "* GCP project and all the required IAM permissions\n", "\n", "* Environment to run the solution\n", "\n", "* Data source and Vector store for the solution" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "__________________________________________________________________________________________________________________" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "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.11.9" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: notebooks/2_Run_OpenDataQnA.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", " \n", " \n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "copyright" }, "outputs": [], "source": [ "# Copyright 2024 Google LLC\n", "#\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ] }, { "cell_type": "markdown", "metadata": { "id": "DRyGcAepAPJ5" }, "source": [ "\n", "

Open Data QnA - Chat with your SQL Database

\n", "\n", "\n", "The pipeline run covers the following steps: \n", "\n", "> 1. Take user question and generate sql in the dialect corresponding to data source\n", "\n", "> 2. Execute the sql query and retreive the data\n", "\n", "> 3. Generate natural language respose and charts to display\n", "\n", "> 4. Clean Up resources\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "jsWGZW_fUJjN" }, "source": [ "### 📒 Using this interactive notebook\n", "\n", "If you have not used this IDE with jupyter notebooks it will ask for installing Python + Jupyter extensions. Please go ahead install them\n", "\n", "Click the **run** icons ▶️ of each cell within this notebook.\n", "\n", "> 💡 Alternatively, you can run the currently selected cell with `Ctrl + Enter` (or `⌘ + Enter` on a Mac).\n", "\n", "> ⚠️ **To avoid any errors**, wait for each section to finish in their order before clicking the next “run” icon.\n", "\n", "This sample must be connected to a **Google Cloud project**, but nothing else is needed other than your Google Cloud project.\n", "\n", "You can use an existing project. Alternatively, you can create a new Cloud project [with cloud credits for free.](https://cloud.google.com/free/docs/gcp-free-tier)" ] }, { "cell_type": "markdown", "metadata": { "id": "RicDCkdI-hmp" }, "source": [ "# 🚧 **0. Pre-requisites**\n", "\n", "Make sure that you have completed the intial setup process using [1_Setup_OpenDataQnA.ipynb](1_Setup_OpenDataQnA.ipynb). If the 1_Setup_OpenDataQnA notebook has been run successfully, the following are set up:\n", "* GCP project and all the required IAM permissions\n", "\n", "* Data source and Vector store for the solution\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **Change your Kernel to the created .venv with poetry from README.md**\n", "\n", "Path would look like e.g. /home/admin_/opendata/.venv/bin/python or ~cache/user/opendataqna/.venv/bin/python\n", "\n", "Below is the Kernel how it should look like before you proceed.\n", "\n", "\n", "![Kernel](../utilities/imgs/Kernel%20Changed.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ⚙️ **1. Retreive Configuration Parameters**\n", "The notebook will load all the configuration parameters from the `config.ini` file in the root directory. \n", "Most of these parameters were set in the initial notebook `1_Setup_OpenDataQnA.ipynb` and save to the 'config.ini file.\n", "Use the below cells to retrieve these values and specify additional ones required for this notebook. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "sys.path.append(module_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Read your `PROJECT_ID` from the config.ini file, or set it manually below: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from utilities import PROJECT_ID\n", "\n", "#Only set the variable if you don't want the project id from config.ini to use\n", "#PROJECT_ID = ''" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🔐 **2. Authenticate and Connect to Google Cloud Project**\n", "Authenticate to Google Cloud as the IAM user logged into this notebook in order to access your Google Cloud Project.\n", "\n", "You can do this within Google Colab or using the Application Default Credentials in the Google Cloud CLI." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Colab Auth\"\"\" \n", "# from google.colab import auth\n", "# auth.authenticate_user()\n", "\n", "\n", "\"\"\"Google CLI Auth\"\"\"\n", "# !gcloud auth application-default login\n", "\n", "\n", "import google.auth\n", "credentials, project_id = google.auth.default()\n", "\n", "# Configure gcloud.\n", "!gcloud config set project {PROJECT_ID}\n", "print(f'Project has been set to {PROJECT_ID}')\n", "# !gcloud auth application-default set-quota-project {PROJECT_ID}\n", "\n", "import os\n", "os.environ['GOOGLE_CLOUD_QUOTA_PROJECT']=PROJECT_ID\n", "os.environ['GOOGLE_CLOUD_PROJECT']=PROJECT_ID" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ▶️ **3. Run the Open Data QnA Pipeline**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 🏃 **Run the Pipeline**\n", "\n", "The next cell executes the pipeline for answering natural language questions over structured data.\n", "\n", "The pipeline performs the following steps:\n", "\n", "1. **Agent Loading:** Initializes various agents for embedding questions, building SQL queries, validating SQL, debugging SQL, and generating responses.\n", "2. **Data Source and Vector Store Configuration:** Sets the data source (BigQuery or PostgreSQL) and vector store (BigQuery or PostgreSQL) based on provided parameters and input files.\n", "3. **Exact Match Search:** Attempts to find an exact match for the user's question in a knowledge graph cache (if enabled). If found, the cached SQL query is used.\n", "4. **Similar Match and Schema Retrieval:** If no exact match is found, retrieves similar questions and associated SQL queries from the knowledge graph (if enabled). Also retrieves relevant table and column schemas based on similarity to the question.\n", "5. **SQL Generation and Debugging:** Builds an initial SQL query using the retrieved information. If enabled, the debugger iteratively refines the query with potential validation and error feedback.\n", "6. **SQL Execution and Response Generation:** Executes the final SQL query (if enabled) against the data source and retrieves results. A response agent then generates a natural language answer based on the results.\n", "7. **Auditing:** Records the entire pipeline process, including generated SQL, responses, and potential errors, for later analysis.\n", "\n", "Args:\n", "\n", "* **session_id (str)** Session ID to identify the conversation\n", "\n", "* **user_question (str):** The natural language question to answer.\n", "\n", "* **user_grouping (str):** Based on what the grouping has been set across the table during setup same will be use to filter or support multiple approaches. Check you data_source_list.csv for the group name. If you didn't specify the group name it will default to `schema_name-source` format\n", "\n", "* **RUN_DEBUGGER (bool, optional):** Whether to run the SQL debugger. Defaults to True.\n", "\n", "* **EXECUTE_FINAL_SQL (bool, optional):** Whether to execute the final SQL query. Defaults to True.\n", "\n", "* **DEBUGGING_ROUNDS (int, optional):** The number of debugging rounds. Defaults to 2.\n", "\n", "* **LLM_VALIDATION (bool, optional):** Whether to use LLM for SQL validation during debugging. Defaults to True.\n", "\n", "* **Embedder_model (str, optional):** The name of the embedding model. Defaults to 'vertex'.\n", "\n", "* **SQLBuilder_model (str, optional):** The name of the SQL building model. Defaults to 'gemini-1.0-pro'.\n", "\n", "* **SQLChecker_model (str, optional):** The name of the SQL validation model. Defaults to 'gemini-1.0-pro'.\n", "\n", "* **SQLDebugger_model (str, optional):** The name of the SQL debugging model. Defaults to 'gemini-1.0-pro'.\n", "\n", "* **Responder_model (str, optional):** The name of the response generation model. Defaults to 'gemini-1.0-pro'.\n", "\n", "* **num_table_matches (int, optional):** The number of similar tables to retrieve. Defaults to 5.\n", "\n", "* **num_column_matches (int, optional):** The number of similar columns to retrieve. Defaults to 10.\n", "\n", "* **table_similarity_threshold (float, optional):** The similarity threshold for tables. Defaults to 0.3.\n", "\n", "* **column_similarity_threshold (float, optional):** The similarity threshold for columns. Defaults to 0.3.\n", "\n", "* **example_similarity_threshold (float, optional):** The similarity threshold for example questions. Defaults to 0.3.\n", "\n", "* **num_sql_matches (int, optional):** The number of similar SQL queries to retrieve. Defaults to 3.\n", "\n", "\n", "Returns:\n", "- **final_sql (str):** The final generated SQL query.\n", "- **response (pandas.DataFrame or str):** The result of executing the SQL query or an error message.\n", "- **_resp (str):** The final response generated by the response agent." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## DO NOT RE-RUN THE CELL UNTIL YOU WANT TO START NEW CONVERSATION" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\n", "\n", "import uuid\n", "\n", "session_id=str(uuid.uuid1())\n", "\n", "#Keep the session id to test for multi turn, change it for number conversation\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ❓ **Ask your Natural Language Question**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### CHANGE THE QUESTION FOR EACH FOLLOW UP. IF NEW CONVERSATION RE-RUN ABOVE CELL AND THEN CHANGE THE QUESTION" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Session ID ::: \" + str(session_id))\n", "\n", "#MovieExplorer-bigquery\n", "user_question = \"What are the top 5 most common genre's in the dataset?\"\n", "\n", "#follow up-1\n", "# user_question= 'Can you only do this for just movies'\n", "\n", "print(\"User Question:- \"+user_question)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from opendataqna import run_pipeline\n", "\n", "final_sql, response, _resp = await run_pipeline(session_id,\n", " user_question,\n", " user_grouping = 'MovieExplorer-bigquery',\n", " RUN_DEBUGGER=True,\n", " EXECUTE_FINAL_SQL=True,\n", " DEBUGGING_ROUNDS = 2, \n", " LLM_VALIDATION=False,\n", " Embedder_model='vertex', # Options: 'vertex' or 'vertex-lang'\n", " SQLBuilder_model= 'gemini-1.5-pro',\n", " SQLChecker_model= 'gemini-1.5-pro',\n", " SQLDebugger_model= 'gemini-1.5-pro',\n", " Responder_model= 'gemini-1.5-pro',\n", " num_table_matches = 5,\n", " num_column_matches = 10,\n", " table_similarity_threshold = 0.1,\n", " column_similarity_threshold = 0.1, \n", " example_similarity_threshold = 0.1, \n", " num_sql_matches=3)\n", "\n", " \n", "\n", "print(\"*\"*50 +\"\\nGenerated SQL\\n\"+\"*\"*50+\"\\n\"+final_sql)\n", "print(\"\\n\"+\"*\"*50 +\"\\nResults\\n\"+\"*\"*50)\n", "display(response)\n", "print(\"*\"*50 +\"\\nNatural Response\\n\"+\"*\"*50+\"\\n\"+_resp)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 📊 **Create Charts for the results** (Run only when you have proper results in the above cells)\n", "Agent provides two suggestive google charts to display on a UI with element IDs chart_div and chart_div_1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from opendataqna import visualize\n", "\n", "chart_js=''\n", "chart_js,invalid_response = visualize(session_id,user_question,final_sql,response) #sending \n", "# print(chart_js[\"chart_div_1\"])\n", "if not invalid_response:\n", " print(\"Chart Code Generated\")\n", "else:\n", " print(\"Error in visualization code generation: \"+ chart_js)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from IPython.display import HTML\n", "\n", "html_code = f'''\n", "\n", "\n", "
\n", "'''\n", "\n", "HTML(html_code)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "html_code = f'''\n", "\n", "\n", "
\n", "'''\n", "\n", "HTML(html_code)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🗑 **Clean Up Notebook Resources**\n", "Make sure to delete your Cloud SQL instance and BigQuery Datasets when your are finished with this notebook to avoid further costs. 💸 💰\n", "\n", "Uncomment and run the cell below to delete " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# # delete Cloud SQL instance\n", "# !gcloud sql instances delete {PG_INSTANCE} -q\n", "\n", "# #delete BigQuery Dataset using bq utility\n", "# !bq rm -r -f -d {BQ_DATASET_NAME}\n", "\n", "# #delete BigQuery 'Open Data QnA' Vector Store Dataset using bq utility\n", "\n", "# !bq rm -r -f -d {BQ_OPENDATAQNA_DATASET_NAME}\n", "\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "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.11.9" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: notebooks/3_LoadKnownGoodSQL.ipynb ================================================ { "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# Copyright 2024 Google LLC\n", "#\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "# **Open Data QnA: Cache Known Good Queries in Vector Store**\n", "\n", "---\n", "\n", "This notebook shows how to cahce known good queries in a Vector Store that has already been set up using [1_SetUpVectorStore.ipynb](1_SetUpVectorStore.ipynb). The queries are loaded into the vector store from the csv files (/scripts/known_good_sql.csv)\n", "\n", "Supported vector stores: \n", "- pgvector on PostgreSQL \n", "- BigQuery vector\n", "\n", "\n", "The notebook covers the following steps: \n", "> 1. Clean an existing embeddings table for known good queries (if loading_mode = 'refresh')\n", "\n", "> 2. Add known good queries from csv file to the embeddings table in the vector store" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🚧 **0. Pre-requisites**\n", "\n", "Make sure that you have completed the intial setup process using [1_SetUpVectorStore.ipynb](1_SetUpVectorStore.ipynb). If the 1_SetUpVectorStore notebook has been run successfully, the following are set up:\n", "* GCP project and all the required IAM permissions\n", "\n", "* **Environment to run the solution**\n", "\n", "* Data source and Vector store for the solution\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ⚙️ **1. Retrieve Configuration Parameters**\n", "The notebook will load all the configuration parameters from the `config.ini` file in the root directory. \n", "Most of these parameters were set in the initial notebook `1_SetUpVectorStore.ipynb` and save to the 'config.ini file.\n", "Use the below cells to retrieve these values and specify additional ones required for this notebook. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import os\n", "import sys\n", "module_path = os.path.abspath(os.path.join('..'))\n", "sys.path.append(module_path)\n", "\n", "import configparser\n", "config = configparser.ConfigParser()\n", "config.read(module_path+'/config.ini')\n", "\n", "PROJECT_ID = config['GCP']['PROJECT_ID']\n", "VECTOR_STORE = config['CONFIG']['VECTOR_STORE']\n", "PG_DATABASE = config['PGCLOUDSQL']['PG_DATABASE']\n", "PG_USER = config['PGCLOUDSQL']['PG_USER']\n", "PG_REGION = config['PGCLOUDSQL']['PG_REGION'] \n", "PG_INSTANCE = config['PGCLOUDSQL']['PG_INSTANCE'] \n", "PG_PASSWORD = config['PGCLOUDSQL']['PG_PASSWORD']\n", "BQ_OPENDATAQNA_DATASET_NAME = config['BIGQUERY']['BQ_OPENDATAQNA_DATASET_NAME']\n", "BQ_LOG_TABLE_NAME = config['BIGQUERY']['BQ_LOG_TABLE_NAME'] \n", "BQ_DATASET_REGION = config['BIGQUERY']['BQ_DATASET_REGION']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🔐 **2. Authenticate and Connect to Google Cloud Project**\n", "Authenticate to Google Cloud as the IAM user logged into this notebook in order to access your Google Cloud Project.\n", "\n", "You can do this within Google Colab or using the Application Default Credentials in the Google Cloud CLI." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Project has been set to three-p-o\n" ] } ], "source": [ "\"\"\"Colab Auth\"\"\" \n", "# from google.colab import auth\n", "# auth.authenticate_user()\n", "\n", "\n", "\"\"\"Google CLI Auth\"\"\"\n", "# !gcloud auth application-default login\n", "\n", "\n", "import google.auth\n", "import os\n", "\n", "credentials, project_id = google.auth.default()\n", "\n", "os.environ['GOOGLE_CLOUD_QUOTA_PROJECT']=PROJECT_ID\n", "os.environ['GOOGLE_CLOUD_PROJECT']=PROJECT_ID\n", "\n", "# Configure gcloud.\n", "print(f'Project has been set to {PROJECT_ID}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 💾 **3. Cache Knwon Good Queries**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Format of the Known Good SQL File (known_good_sql.csv)\n", "\n", "prompt | sql | user_grouping [3 columns]\n", "\n", "prompt ==> Natural Language Question corresponding to query\n", "\n", "sql ==> SQL for the user question (Note that the sql should enclosed in quotes and only in single line. Please remove the line break)\n", "\n", "user_grouping ==>This name should exactly match the user_grouping for Postgres Source or Big Query as data_source_list.csv" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Known Good SQL Found at Path :: /home/admin_/Open_Data_QnA/scripts/known_good_sql.csv\n", " prompt \\\n", "0 Which country had the highest population in 2023? \n", "1 List 10 countries with highest fertility rate \n", "2 What is the life expectancy for men and women ... \n", "3 Which countries had the highest and lowest inf... \n", "4 What is the population distribution by age and... \n", "5 What is the sex ratio at birth for China in 2023? \n", "6 What is the world average sex ratio at birth i... \n", "7 Which country has the highest boy to girl rati... \n", "8 What are the top 10 counties with lowest morta... \n", "9 What were the birth, death, and growth rates i... \n", "10 In 2023, what was the population for countrie... \n", "11 What are the top 3 countries with the highest ... \n", "12 What are the top 5 coutries with highest popul... \n", "13 Which country has highest male life expectancy? \n", "14 What are the birth and death rates of the top ... \n", "\n", " sql user_grouping \n", "0 SELECT country_name, midyear_population\\r\\nFRO... WorldCensus-cloudsql-pg \n", "1 SELECT country_name, total_fertility_rate\\r\\nF... WorldCensus-cloudsql-pg \n", "2 SELECT country_name, life_expectancy_male, lif... WorldCensus-cloudsql-pg \n", "3 WITH RankedInfantMortality AS (\\r\\n SELECT\\r\\... WorldCensus-cloudsql-pg \n", "4 SELECT age, sex, population\\r\\nFROM census_bur... WorldCensus-cloudsql-pg \n", "5 SELECT country_name, sex_ratio_at_birth, year\\... WorldCensus-cloudsql-pg \n", "6 SELECT AVG(sex_ratio_at_birth) AS world_avg_se... WorldCensus-cloudsql-pg \n", "7 SELECT country_name, sex_ratio_at_birth\\r\\nFRO... WorldCensus-cloudsql-pg \n", "8 SELECT country_name, mortality_rate_under5\\r\\n... WorldCensus-cloudsql-pg \n", "9 SELECT year, crude_birth_rate, crude_death_rat... WorldCensus-cloudsql-pg \n", "10 SELECT mp.midyear_population as Population, mp... WorldCensus-cloudsql-pg \n", "11 SELECT mp.country_name, mp.midyear_population,... WorldCensus-cloudsql-pg \n", "12 SELECT mp.country_name, \\r\\n mp.midyear_... WorldCensus-cloudsql-pg \n", "13 SELECT country_name, life_expectancy_male\\r\\nF... WorldCensus-cloudsql-pg \n", "14 SELECT bdg.country_name, bdg.crude_birth_rate,... WorldCensus-cloudsql-pg \n", "Known Good SQLs Loaded into a Dataframe\n" ] } ], "source": [ "# Find the csv file and load as dataframe\n", "import pandas as pd\n", "\n", "current_dir = os.getcwd()\n", "root_dir = os.path.expanduser('~') # Start at the user's home directory\n", "\n", "while current_dir != root_dir:\n", " for dirpath, dirnames, filenames in os.walk(current_dir):\n", " config_path = os.path.join(dirpath, 'known_good_sql.csv')\n", " if os.path.exists(config_path):\n", " file_path = config_path # Update root_dir to the found directory\n", " break # Stop outer loop once found\n", "\n", " current_dir = os.path.dirname(current_dir)\n", "\n", "print(\"Known Good SQL Found at Path :: \"+file_path)\n", "\n", "# Load the file\n", "df_kgq = pd.read_csv(file_path)\n", "df_kgq = df_kgq.loc[:, [\"prompt\", \"sql\", \"user_grouping\"]]\n", "df_kgq = df_kgq.dropna()\n", "print(df_kgq)\n", "\n", "print('Known Good SQLs Loaded into a Dataframe')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Specify mode for loading the known good sql\n", "\n", "The known good sql can loaded in two modes:\n", "* Append mode: Apended to the existing KGQ in the vector store \n", "* Refresh mode: Delete the existing KGQ and create of fresh copy from KGQ in known_good_sql.csv file" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "loading_mode = 'refresh' # Options 'append' or 'refresh'\n", "assert loading_mode in {'append', 'refresh'}, \"⚠️ Invalid loading_mode. Must be 'append' and 'refresh'\"" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "current dir: /home/admin_/Open_Data_QnA/notebooks\n", "root_dir set to: /home/admin_/Open_Data_QnA\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "WARNING: All log messages before absl::InitializeLog() is called are written to STDERR\n", "I0000 00:00:1721735219.442999 2463 config.cc:230] gRPC experiments enabled: call_status_override_on_cancellation, event_engine_dns, event_engine_listener, http2_stats_fix, monitoring_experiment, pick_first_new, trace_record_callops, work_serializer_clears_time_cache\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Adding Known Good Queries to the Vector store.....\n", "Done!!\n" ] } ], "source": [ "# If you have Known Good Queries, load them to known_good_sql.csv file; \n", "# These will be used as few shot examples for query generation. \n", "\n", "from embeddings.kgq_embeddings import setup_kgq_table, store_kgq_embeddings\n", "\n", "if loading_mode == 'refresh':\n", " # Delete any old tables and create a new table to KGQ embeddings\n", " if VECTOR_STORE=='bigquery-vector':\n", " await(setup_kgq_table(project_id=PROJECT_ID,\n", " instance_name=None,\n", " database_name=None,\n", " schema=BQ_OPENDATAQNA_DATASET_NAME,\n", " database_user=None,\n", " database_password=None,\n", " region=BQ_DATASET_REGION,\n", " VECTOR_STORE = VECTOR_STORE\n", " ))\n", "\n", " elif VECTOR_STORE=='cloudsql-pgvector':\n", " await(setup_kgq_table(project_id=PROJECT_ID,\n", " instance_name=PG_INSTANCE,\n", " database_name=PG_DATABASE,\n", " schema=None,\n", " database_user=PG_USER,\n", " database_password=PG_PASSWORD,\n", " region=PG_REGION,\n", " VECTOR_STORE = VECTOR_STORE\n", " ))\n", "\n", "\n", "print(\"Adding Known Good Queries to the Vector store.....\")\n", "# Add KGQ to the vector store\n", "if VECTOR_STORE=='bigquery-vector':\n", " await(store_kgq_embeddings(df_kgq,\n", " project_id=PROJECT_ID,\n", " instance_name=None,\n", " database_name=None,\n", " schema=BQ_OPENDATAQNA_DATASET_NAME,\n", " database_user=None,\n", " database_password=None,\n", " region=BQ_DATASET_REGION,\n", " VECTOR_STORE = VECTOR_STORE\n", " ))\n", "\n", "elif VECTOR_STORE=='cloudsql-pgvector':\n", " await(store_kgq_embeddings(df_kgq,\n", " project_id=PROJECT_ID,\n", " instance_name=PG_INSTANCE,\n", " database_name=PG_DATABASE,\n", " schema=None,\n", " database_user=PG_USER,\n", " database_password=PG_PASSWORD,\n", " region=PG_REGION,\n", " VECTOR_STORE = VECTOR_STORE\n", " ))\n", "print('Done!!')" ] } ], "metadata": { "kernelspec": { "display_name": "talktodata-Fy2pM2BF-py3.9", "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.10.12" } }, "nbformat": 4, "nbformat_minor": 2 } ================================================ FILE: opendataqna.py ================================================ import asyncio import argparse import uuid from agents import EmbedderAgent, BuildSQLAgent, DebugSQLAgent, ValidateSQLAgent, ResponseAgent,VisualizeAgent from utilities import (PROJECT_ID, PG_REGION, BQ_REGION, EXAMPLES, LOGGING, VECTOR_STORE, BQ_OPENDATAQNA_DATASET_NAME, USE_SESSION_HISTORY) from dbconnectors import bqconnector, pgconnector, firestoreconnector from embeddings.store_embeddings import add_sql_embedding #Based on VECTOR STORE in config.ini initialize vector connector and region if VECTOR_STORE=='bigquery-vector': region=BQ_REGION vector_connector = bqconnector call_await = False elif VECTOR_STORE == 'cloudsql-pgvector': region=PG_REGION vector_connector = pgconnector call_await=True else: raise ValueError("Please specify a valid Data Store. Supported are either 'bigquery-vector' or 'cloudsql-pgvector'") def generate_uuid(): """Generates a random UUID (Universally Unique Identifier) Version 4. Returns: str: A string representation of the UUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. """ return str(uuid.uuid4()) ############################ #_____GET ALL DATABASES_____# ############################ def get_all_databases(): """Retrieves a list of all distinct databases (with source type) from the vector store. This function queries the vector store (BigQuery or PostgreSQL) to fetch a list of unique databases, including their source type. The source type indicates whether the database is a BigQuery dataset or a PostgreSQL schema. Returns: tuple: A tuple containing two elements: - result (str or list): A JSON-formatted string containing the list of databases and their source types, or an error message if an exception occurs. - invalid_response (bool): A flag indicating whether an error occurred during retrieval (True) or if the response is valid (False). Raises: Exception: If there is an issue connecting to or querying the vector store. The exception message will be included in the returned `result`. """ try: if VECTOR_STORE=='bigquery-vector': final_sql=f'''SELECT DISTINCT user_grouping AS table_schema FROM `{PROJECT_ID}.{BQ_OPENDATAQNA_DATASET_NAME}.table_details_embeddings`''' else: final_sql="""SELECT DISTINCT user_grouping AS table_schema FROM table_details_embeddings""" result = vector_connector.retrieve_df(final_sql) result = result.to_json(orient='records') invalid_response=False except Exception as e: result="Issue was encountered while extracting databases in vector store:: " + str(e) invalid_response=True return result,invalid_response ############################ #_____GET SOURCE TYPE_____## ############################ def get_source_type(user_grouping): """Retrieves the source type of a specified database from the vector store. This function queries the vector store (BigQuery or PostgreSQL) to determine whether the given database is a BigQuery dataset ('bigquery') or a PostgreSQL schema ('postgres'). Args: user_grouping (str): The name of the database to look up. Returns: tuple: A tuple containing two elements: - result (str): The source type of the database ('bigquery' or 'postgres'), or an error message if not found or an exception occurs. - invalid_response (bool): A flag indicating whether an error occurred during retrieval (True) or if the response is valid (False). Raises: Exception: If there is an issue connecting to or querying the vector store. The exception message will be included in the returned `result`. """ try: if VECTOR_STORE=='bigquery-vector': sql=f'''SELECT DISTINCT source_type FROM `{PROJECT_ID}.{BQ_OPENDATAQNA_DATASET_NAME}.table_details_embeddings` where user_grouping='{user_grouping}' ''' else: sql=f'''SELECT DISTINCT source_type FROM table_details_embeddings where user_grouping='{user_grouping}' ''' result = vector_connector.retrieve_df(sql) result = (str(result.iloc[0, 0])).lower() invalid_response=False except Exception as e: result="Error at finding the datasource :: "+str(e) invalid_response=True return result,invalid_response ############################ ###_____GENERATE SQL_____### ############################ async def generate_sql(session_id, user_question, user_grouping, RUN_DEBUGGER, DEBUGGING_ROUNDS, LLM_VALIDATION, Embedder_model, SQLBuilder_model, SQLChecker_model, SQLDebugger_model, num_table_matches, num_column_matches, table_similarity_threshold, column_similarity_threshold, example_similarity_threshold, num_sql_matches, user_id="opendataqna-user@google.com"): """Generates an SQL query based on a user's question and database. This asynchronous function orchestrates a pipeline to generate an SQL query from a natural language question. It leverages various agents for embedding, SQL building, validation, and debugging. Args: session_id (str): Session ID to identify the chat conversation user_question (str): The user's natural language question. user_grouping (str): The name of the database to query. RUN_DEBUGGER (bool): Whether to run the SQL debugger. DEBUGGING_ROUNDS (int): The number of debugging rounds to perform. LLM_VALIDATION (bool): Whether to use LLM for validation. Embedder_model (str): The name of the embedding model. SQLBuilder_model (str): The name of the SQL builder model. SQLChecker_model (str): The name of the SQL checker model. SQLDebugger_model (str): The name of the SQL debugger model. num_table_matches (int): The number of table matches to retrieve. num_column_matches (int): The number of column matches to retrieve. table_similarity_threshold (float): The similarity threshold for table matching. column_similarity_threshold (float): The similarity threshold for column matching. example_similarity_threshold (float): The similarity threshold for example matching. num_sql_matches (int): The number of similar SQL queries to retrieve. Returns: tuple: A tuple containing: - final_sql (str): The final generated SQL query, or an error message if generation failed. - invalid_response (bool): True if the response is invalid (e.g., due to an error), False otherwise. """ try: if session_id is None or session_id=="": print("This is a new session") session_id=generate_uuid() ## LOAD AGENTS print("Loading Agents.") embedder = EmbedderAgent(Embedder_model) SQLBuilder = BuildSQLAgent(SQLBuilder_model) SQLChecker = ValidateSQLAgent(SQLChecker_model) SQLDebugger = DebugSQLAgent(SQLDebugger_model) re_written_qe=user_question print("Getting the history for the session.......\n") session_history = firestoreconnector.get_chat_logs_for_session(session_id) if USE_SESSION_HISTORY else None print("Grabbed history for the session:: "+ str(session_history)) if session_history is None or not session_history: print("No records for the session. Not rewriting the question\n") else: concated_questions,re_written_qe=SQLBuilder.rewrite_question(user_question,session_history) found_in_vector = 'N' # if an exact query match was found final_sql='Not Generated Yet' # final generated SQL process_step='Not Started' error_msg='' corrected_sql = '' DATA_SOURCE = 'Yet to determine' DATA_SOURCE,src_invalid = get_source_type(user_grouping) if src_invalid: raise ValueError(DATA_SOURCE) #vertexai.init(project=PROJECT_ID, location=region) #aiplatform.init(project=PROJECT_ID, location=region) print("Source selected as : "+ str(DATA_SOURCE) + "\nSchema or Dataset Name is : "+ str(user_grouping)) print("Vector Store selected as : "+ str(VECTOR_STORE)) # Reset AUDIT_TEXT AUDIT_TEXT = 'Creating embedding for given question' # Fetch the embedding of the user's input question embedded_question = embedder.create(re_written_qe) AUDIT_TEXT = AUDIT_TEXT + "\nUser Question : " + str(user_question) + "\nUser Database : " + str(user_grouping) process_step = "\n\nGet Exact Match: " # Look for exact matches in known questions IF kgq is enabled if EXAMPLES: exact_sql_history = vector_connector.getExactMatches(user_question) else: exact_sql_history = None # If exact user query has been found, retrieve the SQL and skip Generation Pipeline if exact_sql_history is not None: found_in_vector = 'Y' final_sql = exact_sql_history invalid_response = False AUDIT_TEXT = AUDIT_TEXT + "\nExact match has been found! Going to retrieve the SQL query from cache and serve!" else: # No exact match found. Proceed looking for similar entries in db IF kgq is enabled if EXAMPLES: AUDIT_TEXT = AUDIT_TEXT + process_step + "\nNo exact match found in query cache, retrieving relevant schema and known good queries for few shot examples using similarity search...." process_step = "\n\nGet Similar Match: " if call_await: similar_sql = await vector_connector.getSimilarMatches('example', user_grouping, embedded_question, num_sql_matches, example_similarity_threshold) else: similar_sql = vector_connector.getSimilarMatches('example', user_grouping, embedded_question, num_sql_matches, example_similarity_threshold) else: similar_sql = "No similar SQLs provided..." process_step = "\n\nGet Table and Column Schema: " # Retrieve matching tables and columns if call_await: table_matches = await vector_connector.getSimilarMatches('table', user_grouping, embedded_question, num_table_matches, table_similarity_threshold) column_matches = await vector_connector.getSimilarMatches('column', user_grouping, embedded_question, num_column_matches, column_similarity_threshold) else: table_matches = vector_connector.getSimilarMatches('table', user_grouping, embedded_question, num_table_matches, table_similarity_threshold) column_matches = vector_connector.getSimilarMatches('column', user_grouping, embedded_question, num_column_matches, column_similarity_threshold) AUDIT_TEXT = AUDIT_TEXT + process_step + "\nRetrieved Similar Known Good Queries, Table Schema and Column Schema: \n" + '\nRetrieved Tables: \n' + str(table_matches) + '\n\nRetrieved Columns: \n' + str(column_matches) + '\n\nRetrieved Known Good Queries: \n' + str(similar_sql) # If similar table and column schemas found: if len(table_matches.replace('Schema(values):','').replace(' ','')) > 0 or len(column_matches.replace('Column name(type):','').replace(' ','')) > 0 : # GENERATE SQL process_step = "\n\nBuild SQL: " generated_sql = SQLBuilder.build_sql(DATA_SOURCE,user_grouping,user_question,session_history,table_matches,column_matches,similar_sql) final_sql=generated_sql AUDIT_TEXT = AUDIT_TEXT + process_step + "\nGenerated SQL : " + str(generated_sql) if 'unrelated_answer' in generated_sql : invalid_response=True final_sql="This is an unrelated question or you are not asking a valid query" # If agent assessment is valid, proceed with checks else: invalid_response=False if RUN_DEBUGGER: generated_sql, invalid_response, AUDIT_TEXT = SQLDebugger.start_debugger(DATA_SOURCE,user_grouping, generated_sql, user_question, SQLChecker, table_matches, column_matches, AUDIT_TEXT, similar_sql, DEBUGGING_ROUNDS, LLM_VALIDATION) # AUDIT_TEXT = AUDIT_TEXT + '\n Feedback from Debugger: \n' + feedback_text final_sql=generated_sql AUDIT_TEXT = AUDIT_TEXT + "\nFinal SQL after Debugger: \n" +str(final_sql) # No matching table found else: invalid_response=True print('No tables found in Vector ...') AUDIT_TEXT = AUDIT_TEXT + "\nNo tables have been found in the Vector DB. The question cannot be answered with the provide data source!" # print(f'\n\n AUDIT_TEXT: \n {AUDIT_TEXT}') if LOGGING: bqconnector.make_audit_entry(DATA_SOURCE, user_grouping, SQLBuilder_model, user_question, final_sql, found_in_vector, "", process_step, error_msg,AUDIT_TEXT) except Exception as e: error_msg=str(e) final_sql="Error generating the SQL Please check the logs. "+str(e) invalid_response=True AUDIT_TEXT=AUDIT_TEXT+ "\nException at SQL generation" print("Error :: "+str(error_msg)) if LOGGING: bqconnector.make_audit_entry(DATA_SOURCE, user_grouping, SQLBuilder_model, user_question, final_sql, found_in_vector, "", process_step, error_msg,AUDIT_TEXT) if USE_SESSION_HISTORY and not invalid_response: firestoreconnector.log_chat(session_id,user_question,final_sql,user_id) print("Session history persisted") return final_sql,session_id,invalid_response ############################ ###_____GET RESULTS_____#### ############################ def get_results(user_grouping, final_sql, invalid_response=False, EXECUTE_FINAL_SQL=True): """Executes the final SQL query (if valid) and retrieves the results. This function first determines the data source (BigQuery or PostgreSQL) based on the provided database name. If the SQL query is valid and execution is enabled, it fetches the results using the appropriate connector. Args: user_grouping (str): The name of the database to query. final_sql (str): The final SQL query to execute. invalid_response (bool, optional): A flag indicating whether the SQL query is invalid. Defaults to False. EXECUTE_FINAL_SQL (bool, optional): Whether to execute the final SQL query. Defaults to True. Returns: tuple: A tuple containing: - result_df (pandas.DataFrame or str): The results of the SQL query as a DataFrame, or an error message if the query is invalid or execution failed. - invalid_response (bool): True if the response is invalid (e.g., due to an error), False otherwise. Raises: ValueError: If the data source is invalid or not supported. Exception: If there's an error executing the SQL query or retrieving the results. """ try: DATA_SOURCE,src_invalid = get_source_type(user_grouping) if not src_invalid: ## SET DATA SOURCE if DATA_SOURCE=='bigquery': src_connector = bqconnector else: src_connector = pgconnector else: raise ValueError(DATA_SOURCE) if not invalid_response: try: if EXECUTE_FINAL_SQL is True: final_exec_result_df=src_connector.retrieve_df(final_sql.replace("```sql","").replace("```","").replace("EXPLAIN ANALYZE ","")) result_df = final_exec_result_df else: # Do not execute final SQL print("Not executing final SQL since EXECUTE_FINAL_SQL variable is False\n ") result_df = "Please enable the Execution of the final SQL so I can provide an answer" invalid_response = True except ValueError: result_df= "Error has been encountered :: " + str(e) invalid_response=True else: # Do not execute final SQL result_df = "Not executing final SQL as it is invalid, please debug!" except Exception as e: print(f"An error occured. Aborting... Error Message: {e}") result_df="Error has been encountered :: " + str(e) invalid_response=True return result_df,invalid_response def get_response(session_id,user_question,result_df,Responder_model='gemini-1.0-pro'): try: Responder = ResponseAgent(Responder_model) if session_id is None or session_id=="": print("This is a new session") else: session_history =firestoreconnector.get_chat_logs_for_session(session_id) if USE_SESSION_HISTORY else None if session_history is None or not session_history: print("No records for the session. Not rewriting the question\n") else: concated_questions,re_written_qe=Responder.rewrite_question(user_question,session_history) user_question=re_written_qe _resp=Responder.run(user_question, result_df) invalid_response=False except Exception as e: print(f"An error occured. Aborting... Error Message: {e}") _resp= "Error has been encountered :: " + str(e) invalid_response=True return _resp,invalid_response ############################ ###_____RUN PIPELINE_____### ############################ async def run_pipeline(session_id, user_question, user_grouping, RUN_DEBUGGER=True, EXECUTE_FINAL_SQL=True, DEBUGGING_ROUNDS = 2, LLM_VALIDATION=False, Embedder_model='vertex', SQLBuilder_model= 'gemini-1.5-pro', SQLChecker_model= 'gemini-1.0-pro', SQLDebugger_model= 'gemini-1.0-pro', Responder_model= 'gemini-1.0-pro', num_table_matches = 5, num_column_matches = 10, table_similarity_threshold = 0.3, column_similarity_threshold = 0.3, example_similarity_threshold = 0.3, num_sql_matches=3): """Orchestrates the end-to-end SQL generation and response pipeline. This asynchronous function manages the entire process of generating an SQL query from a user's question, executing the query (if valid), and formulating a natural language response based on the results. Args: user_question (str): The user's natural language question. user_grouping (str): The name of the user grouping to query. RUN_DEBUGGER (bool, optional): Whether to run the SQL debugger. Defaults to True. EXECUTE_FINAL_SQL (bool, optional): Whether to execute the final SQL query. Defaults to True. DEBUGGING_ROUNDS (int, optional): The number of debugging rounds to perform. Defaults to 2. LLM_VALIDATION (bool, optional): Whether to use LLM for validation. Defaults to True. Embedder_model (str, optional): The name of the embedding model. Defaults to 'vertex'. SQLBuilder_model (str, optional): The name of the SQL builder model. Defaults to 'gemini-1.5-pro'. SQLChecker_model (str, optional): The name of the SQL checker model. Defaults to 'gemini-1.0-pro'. SQLDebugger_model (str, optional): The name of the SQL debugger model. Defaults to 'gemini-1.0-pro'. Responder_model (str, optional): The name of the responder model. Defaults to 'gemini-1.0-pro'. num_table_matches (int, optional): The number of table matches to retrieve. Defaults to 5. num_column_matches (int, optional): The number of column matches to retrieve. Defaults to 10. table_similarity_threshold (float, optional): The similarity threshold for table matching. Defaults to 0.3. column_similarity_threshold (float, optional): The similarity threshold for column matching. Defaults to 0.3. example_similarity_threshold (float, optional): The similarity threshold for example matching. Defaults to 0.3. num_sql_matches (int, optional): The number of similar SQL queries to retrieve. Defaults to 3. Returns: tuple: A tuple containing: - final_sql (str): The final generated SQL query, or an error message if generation failed. - results_df (pandas.DataFrame or str): The results of the SQL query as a DataFrame, or an error message if the query is invalid or execution failed. - _resp (str): The generated natural language response based on the results, or an error message if response generation failed. """ final_sql,session_id, invalid_response = await generate_sql(session_id, user_question, user_grouping, RUN_DEBUGGER, DEBUGGING_ROUNDS, LLM_VALIDATION, Embedder_model, SQLBuilder_model, SQLChecker_model, SQLDebugger_model, num_table_matches, num_column_matches, table_similarity_threshold, column_similarity_threshold, example_similarity_threshold, num_sql_matches) if not invalid_response: results_df, invalid_response = get_results(user_grouping, final_sql, invalid_response=invalid_response, EXECUTE_FINAL_SQL=EXECUTE_FINAL_SQL) if not invalid_response: _resp,invalid_response=get_response(session_id,user_question,results_df.to_json(orient='records'),Responder_model=Responder_model) else: _resp=results_df else: results_df=final_sql _resp=final_sql return final_sql, results_df, _resp ############################ #####_____GET KGQ_____###### ############################ def get_kgq(user_grouping): """Retrieves known good SQL queries (KGQs) for a specific database from the vector store. This function queries the vector store (BigQuery or PostgreSQL) to fetch a limited number of distinct user questions and their corresponding generated SQL queries that are relevant to the specified database. These KGQs can be used as examples or references for generating new SQL queries. Args: user_grouping (str): The name of the user grouping for which to retrieve KGQs. Returns: tuple: A tuple containing two elements: - result (str): A JSON-formatted string containing the list of KGQs (user questions and SQL queries), or an error message if an exception occurs. - invalid_response (bool): A flag indicating whether an error occurred during retrieval (True) or if the response is valid (False). Raises: Exception: If there is an issue connecting to or querying the vector store. The exception message will be included in the returned `result`. """ try: if VECTOR_STORE=='bigquery-vector': sql=f'''SELECT distinct example_user_question, example_generated_sql FROM `{PROJECT_ID}.{BQ_OPENDATAQNA_DATASET_NAME}.example_prompt_sql_embeddings` where user_grouping='{user_grouping}' LIMIT 5 ''' else: sql="""select distinct example_user_question, example_generated_sql from example_prompt_sql_embeddings where user_grouping = '{user_grouping}' LIMIT 5""".format(user_grouping=user_grouping) result = vector_connector.retrieve_df(sql) result = result.to_json(orient='records') invalid_response = False except Exception as e: result="Issue was encountered while extracting known good sqls in vector store:: " + str(e) invalid_response=True return result,invalid_response ############################ ####_____EMBED SQL_____##### ############################ async def embed_sql(session_id,user_grouping,user_question,generate_sql): """Embeds a generated SQL query into the vector store as an example. This asynchronous function takes a user's question, a generated SQL query, and a database name as input. It calls the `add_sql_embedding` function to create an embedding of the SQL query and store it in the vector store, potentially for future reference as a known good query (KGQ). Args: user_grouping (str): The name of the grouping associated with the query. user_question (str): The user's original question. generate_sql (str): The SQL query generated from the user's question. Returns: tuple: A tuple containing two elements: - embedded (str or None): The embedded SQL query if successful, or an error message if an exception occurs. - invalid_response (bool): A flag indicating whether an error occurred during embedding (True) or if the response is valid (False). Raises: Exception: If there is an issue with the embedding process. The exception message will be included in the returned `embedded` value. """ try: Rewriter=ResponseAgent('gemini-1.5-pro') if session_id is None or session_id=="": print("This is a new session") else: session_history =firestoreconnector.get_chat_logs_for_session(session_id) if USE_SESSION_HISTORY else None if session_history is None or not session_history: print("No records for the session. Not rewriting the question\n") else: concated_questions,re_written_qe=Rewriter.rewrite_question(user_question,session_history) user_question=re_written_qe embedded = await add_sql_embedding(user_question, generate_sql,user_grouping) invalid_response=False except Exception as e: embedded="Issue was encountered while embedding the SQL as example." + str(e) invalid_response=True return embedded,invalid_response def visualize(session_id,user_question,generated_sql,sql_results): try: Rewriter=ResponseAgent('gemini-1.5-pro') if session_id is None or session_id=="": print("This is a new session") else: session_history =firestoreconnector.get_chat_logs_for_session(session_id) if USE_SESSION_HISTORY else None if session_history is None or not session_history: print("No records for the session. Not rewriting the question\n") else: concated_questions,re_written_qe=Rewriter.rewrite_question(user_question,session_history) user_question=re_written_qe _viz=VisualizeAgent() js_chart = _viz.generate_charts(user_question, generate_sql,sql_results) invalid_response=False except Exception as e: js_chart="Issue was encountered while Generating Charts ::" + str(e) invalid_response=True return js_chart,invalid_response ############################ #######_____MAIN_____####### ############################ if __name__ == '__main__': # user_question = "How many movies have review ratings above 5?" # user_grouping='MovieExplorer-bigquery' parser = argparse.ArgumentParser(description="Open Data QnA SQL Generation") parser.add_argument("--session_id", type=str, required=True, help="Session Id") parser.add_argument("--user_question", type=str, required=True, help="The user's question.") parser.add_argument("--user_grouping", type=str, required=True, help="The user grouping specificed in the source list CSV file") # Optional Arguments for run_pipeline Parameters parser.add_argument("--run_debugger", action="store_true", help="Enable the debugger (default: False)") parser.add_argument("--execute_final_sql", action="store_true", help="Execute the final SQL (default: False)") parser.add_argument("--debugging_rounds", type=int, default=2, help="Number of debugging rounds (default: 2)") parser.add_argument("--llm_validation", action="store_true", help="Enable LLM validation (default: False)") parser.add_argument("--embedder_model", type=str, default='vertex', help="Embedder model name (default: 'vertex')") parser.add_argument("--sqlbuilder_model", type=str, default='gemini-1.5-pro', help="SQL builder model name (default: 'gemini-1.0-pro')") parser.add_argument("--sqlchecker_model", type=str, default='gemini-1.5-pro', help="SQL checker model name (default: 'gemini-1.0-pro')") parser.add_argument("--sqldebugger_model", type=str, default='gemini-1.5-pro', help="SQL debugger model name (default: 'gemini-1.0-pro')") parser.add_argument("--responder_model", type=str, default='gemini-1.5-pro', help="Responder model name (default: 'gemini-1.0-pro')") parser.add_argument("--num_table_matches", type=int, default=5, help="Number of table matches (default: 5)") parser.add_argument("--num_column_matches", type=int, default=10, help="Number of column matches (default: 10)") parser.add_argument("--table_similarity_threshold", type=float, default=0.1, help="Threshold for table similarity (default: 0.1)") parser.add_argument("--column_similarity_threshold", type=float, default=0.1, help="Threshold for column similarity (default: 0.1)") parser.add_argument("--example_similarity_threshold", type=float, default=0.1, help="Threshold for example similarity (default: 0.1)") parser.add_argument("--num_sql_matches", type=int, default=3, help="Number of SQL matches (default: 3)") args = parser.parse_args() # Use Argument Values in run_pipeline final_sql, response, _resp = asyncio.run(run_pipeline(args.session_id, args.user_question, args.user_grouping, RUN_DEBUGGER=args.run_debugger, EXECUTE_FINAL_SQL=args.execute_final_sql, DEBUGGING_ROUNDS=args.debugging_rounds, LLM_VALIDATION=args.llm_validation, Embedder_model=args.embedder_model, SQLBuilder_model=args.sqlbuilder_model, SQLChecker_model=args.sqlchecker_model, SQLDebugger_model=args.sqldebugger_model, Responder_model=args.responder_model, num_table_matches=args.num_table_matches, num_column_matches=args.num_column_matches, table_similarity_threshold=args.table_similarity_threshold, column_similarity_threshold=args.column_similarity_threshold, example_similarity_threshold=args.example_similarity_threshold, num_sql_matches=args.num_sql_matches )) # user_question = "How many +18 movies have a rating above 4?" # final_sql, response, _resp = asyncio.run(run_pipeline(user_question, # 'imdb', # RUN_DEBUGGER=True, # EXECUTE_FINAL_SQL=True, # DEBUGGING_ROUNDS = 2, # LLM_VALIDATION=True, # Embedder_model='vertex', # SQLBuilder_model= 'gemini-1.0-pro', # SQLChecker_model= 'gemini-1.0-pro', # SQLDebugger_model= 'gemini-1.0-pro', # Responder_model= 'gemini-1.0-pro', # num_table_matches = 5, # num_column_matches = 10, # table_similarity_threshold = 0.1, # column_similarity_threshold = 0.1, # example_similarity_threshold = 0.1, # num_sql_matches=3)) print("*"*50 +"\nGenerated SQL\n"+"*"*50+"\n"+final_sql) print("\n"+"*"*50 +"\nResults\n"+"*"*50) print(response) print("*"*50 +"\nNatural Response\n"+"*"*50+"\n"+_resp) ================================================ FILE: prompts.yaml ================================================ ################################################################################################# ## USE CASE SPECIFIC PROMPTS ## ################################################################################################# # CHOOSE PROMPT VARIABLE NAME STRICLY FOLLOWING THE NAMING CONVENTION BELOW # Variable Naming for use case prompt: usecase_{source_type}_{user_grouping} [Grab this values from the data_source_list.csv] # In the use case prompt, include any relevant context information to the LLMs including but not limited # common acronyms, column and table naming conventions (like prefix-Sufix used), buniess jargon and # KPI definitions w.r.t the tables and columns, guidance on how to handle ambiguity w.r.t questions # Use case prompt: source_type = 'bigquery'; user_grouping = 'MovieExplorer-bigquery' usecase_bigquery_MovieExplorer-bigquery: | The Movie Explorer Dataset is group of tables that contains details of movies and tv shows. We have titles, names of the personas worked in those movies. We also have the reviews and ratings of those movies as well. # Use case prompt: source_type = 'cloudsql-pg'; user_grouping = 'WorldCensus-cloudsql-pg' usecase_cloudsql-pg_WorldCensus-cloudsql-pg: | The World Census Dataset is group of tables ################################################################################################# ################################################################################################# ## GENERIC PROMPTS FOR DIFFERENT AGENTS ## ################################################################################################# # DO NOT CHANGE PROMPT VARIABLE NAME # Prompt for building sql query for bigquery buildsql_bigquery: | You are an Bigquery SQL guru. Your task is to write a Bigquery SQL query that answers the following question while using the provided context. - Join as minimal tables as possible. - When joining tables ensure all join columns are the same data_type. - Analyze the database and the table schema provided as parameters and undestand the relations (column and table relations). - Use always SAFE_CAST. If performing a SAFE_CAST, use only Bigquery supported datatypes. (i.e {specific_data_types}) - Always SAFE_CAST and then use aggregate functions - Don't include any comments in code. - Remove ```sql and ``` from the output and generate the SQL in single line. - Tables should be refered to using a fully qualified name with enclosed in ticks (`) e.g. `project_id.owner.table_name`. - Use all the non-aggregated columns from the "SELECT" statement while framing "GROUP BY" block. - Return syntactically and symantically correct SQL for BigQuery with proper relation mapping i.e project_id, owner, table and column relation. - Use ONLY the column names (column_name) mentioned in Table Schema. DO NOT USE any other column names outside of this. - Associate column_name mentioned in Table Schema only to the table_name specified under Table Schema. - Use SQL 'AS' statement to assign a new name temporarily to a table column or even a table wherever needed. - Table names are case sensitive. DO NOT uppercase or lowercase the table names. - Always enclose subqueries and union queries in brackets. - Refer to the examples provided below, if given. - When given question is out of context of from this session respond always with dummy SQL statement - {not_related_msg} - You always generate SELECT queries ONLY. If asked for other statements for DELETE or MERGE etc respond with dummy SQL statement - {not_related_msg} {usecase_context} {similar_sql} {tables_schema}
{columns_schema} # DO NOT CHANGE PROMPT VARIABLE NAME # Prompt for building sql query for PostgreSQL buildsql_cloudsql-pg: | You are an PostgreSQL SQL guru. Your task is to write a PostgreSQL query that answers the following question while using the provided context. VERY IMPORTANT:- Use ONLY the PostgreSQL available appropriate datatypes (i.e {specific_data_types}) while casting the column in the SQL. IMPORTANT:- In "FROM" and "JOIN" blocks always refer the table_name as schema.table_name. IMPORTANT:- Use ONLY the table name(table_name) and column names (column_name) mentioned in Table Schema (i.e {tables_schema}). DO NOT USE any other column names outside of this. IMPORTANT:- Associate column_name mentioned in Table Schema only to the table_name specified under Table Schema. NOTE:- Use SQL 'AS' statement to assign a new name temporarily to a table column or even a table wherever needed. - Only answer questions relevant to the tables or columns listed in the table schema If a non-related question comes, answer exactly - {not_related_msg} - You always generate SELECT queries ONLY. If asked for other statements for DELETE or MERGE etc answer exactly - {not_related_msg} - Join as minimal tables as possible. - When joining tables ensure all join columns are the same data_type. - Analyse the database and the table schema provided as parameters and understand the relations (column and table relations). - Don't include any comments in code. - Remove ```sql and ``` from the output and generate the SQL in single line. - Tables should be refered to using a fully qualified name including owner and table name. - Use table_alias.column_name when referring to columns. Example:- dept_id=hr.dept_id - Capitalize the table names on SQL "where" condition. - Use the columns from the "SELECT" statement while framing "GROUP BY" block. - Always refer the column-name with rightly mapped table-name as seen in the table schema. - Return syntactically and symantically correct SQL for Postgres with proper relation mapping i.e owner, table and column relation. - Refer to the examples provided i.e. {similar_sql} {usecase_context} {similar_sql} {tables_schema}
{columns_schema} debugsql_bigquery: | You are an BigQuery SQL guru. Your task is to troubleshoot a BigQuery SQL query. As the user provides versions of the query and the errors returned by BigQuery, return a new alternative SQL query that fixes the errors. It is important that the query still answers the original question. - Join as minimal tables as possible. - When joining tables ensure all join columns are the same data_type. - Analyze the database and the table schema provided as parameters and undestand the relations (column and table relations). - Use always SAFE_CAST. If performing a SAFE_CAST, use only Bigquery supported datatypes. - Always SAFE_CAST and then use aggregate functions - Don't include any comments in code. - Remove ```sql and ``` from the output and generate the SQL in single line. - Tables should be refered to using a fully qualified name with enclosed in ticks (`) e.g. `project_id.owner.table_name`. - Use all the non-aggregated columns from the "SELECT" statement while framing "GROUP BY" block. - Return syntactically and symantically correct SQL for BigQuery with proper relation mapping i.e project_id, owner, table and column relation. - Use ONLY the column names (column_name) mentioned in Table Schema. DO NOT USE any other column names outside of this. - Associate column_name mentioned in Table Schema only to the table_name specified under Table Schema. - Use SQL 'AS' statement to assign a new name temporarily to a table column or even a table wherever needed. - Table names are case sensitive. DO NOT uppercase or lowercase the table names. - Always enclose subqueries and union queries in brackets. {usecase_context} {similar_sql} {tables_schema}
{columns_schema} debugsql_cloudsql-pg: | You are an Postgres SQL guru. This session is trying to troubleshoot an Postgres SQL query. As the user provides versions of the query and the errors returned by Postgres, return a new alternative SQL query that fixes the errors. It is important that the query still answer the original question. - Remove ```sql and ``` from the output and generate the SQL in single line. - Rewritten SQL can't be igual to the original one. - Write a SQL comformant query for Postgres that answers the following question while using the provided context to correctly refer to Postgres tables and the needed column names. - All column_name in the query must exist in the table_name. - If a join includes d.country_id and table_alias d is equal to table_name DEPT, then country_id column_name must exist with table_name DEPT in the table column metadata - When joining tables ensure all join columns are the same data_type. - Analyse the database and the table schema provided as parameters and undestand the relations (column and table relations). - Don't include any comments in code. - Tables should be refered to using a fully qualified name including owner and table name. - Use table_alias.column_name when referring to columns. Example: dept_id=hr.dept_id - Capitalize the table names on SQL "where" condition. - Use the columns from the "SELECT" statement while framing "GROUP BY" block. - Always refer the column-name with rightly mapped table-name as seen in the table schema. - Return syntactically and symantically correct SQL for Postgres with proper relation mapping i.e owner, table and column relation. - Use only column names listed in the column metadata. - Always ensure to refer the table as schema.table_name. {usecase_context} {similar_sql} {tables_schema}
{columns_schema} # This is the prompt for Response agent that takes the user question and data from SQL Query execution and # generates a natural language response. Include additional instructions here if the natural language response # needs to be customized. nl_reponse: | You are a Data Assistant that helps to answer users' questions on their data within their databases. The user has provided the following question in natural language: {user_question} The system has returned the following result after running the SQL query: {sql_result} Provide a natural sounding response to the user question using only the data provided to you. validatesql: | Classify if the SQL query as valid or invalid The SQL written here is for SQL dialect for source type : {source_type} Only validate for this source - Validate the SQL for syntax - Check for semantic correctness based on the table and column details - Check for the data type compatibility {tables_schema}
{columns_schema} Question:- {user_question} SQL query:- {generated_sql} Respond using a valid JSON format with two elements valid and errors. Remove ```json and ``` from the output See example output below {{ "valid": true or false, "errors":errors }} # Prompt to suggest a chart type for a given user question and corresponding SQL query visualize_chart_type: | You are expert in generating visualizations. Some commonly used charts and when do use them:- - Text or Score card is best for showing single value answer - Table is best for Showing data in a tabular format. - Bullet Chart is best for Showing individual values across categories. - Bar Chart is best for Comparing individual values across categories, especially with many categories or long labels. - Column Chart is best for Comparing individual values across categories, best for smaller datasets. - Line Chart is best for Showing trends over time or continuous data sets with many data points. - Area Chart is best for Emphasizing cumulative totals over time, or the magnitude of change across multiple categories. - Pie Chart is best for Show proportions of a whole, but only for a few categories (ideally less than 6). - Scatter Plot is best for Investigating relationships or correlations between two variables. - Bubble Chart is best for Comparing and showing relationships between three variables. - Histogram is best for Displaying the distribution and frequency of continuous data. - Map Chart is best for Visualizing data with a geographic dimension (countries, states, regions, etc.). - Gantt Chart is best for Managing projects, visualizing timelines, and task dependencies. - Heatmap is best for Showing the density of data points across two dimensions, highlighting areas of concentration. -Do not add any explanation to the response. Only stick to format Chart-1, Chart-2 -Do not enclose the response with js or javascript or ``` Below is the Question and corresponding SQL Generated, suggest best two of the chart types Question : {user_question} Corresponding SQL : {generated_sql} Respond using a valid JSON format with two elements chart_1 and chart_2 as below {{"chart_1":suggestion-1, "chart_2":suggestion-2}} # Prompt for generation code for google charts. visualize_generate_chart_code: | You are expert in generating visualizations. Guidelines: -Do not add any explanation to the response. -Do not enclose the response with js or javascript or ``` You are asked to generate a visualization for the following question: {user_question} The SQL generated for the question is: {generated_sql} The results of the sql which should be used to generate the visualization are in json format as follows: {sql_results} Needed chart type is : {chart_type} Guidelines: - Generate js code for {chart_type} for the visualization using google charts and its possible data column. You do not need to use all the columns if not possible. - The generated js code should be able to be just evaluated as javascript so do not add any extra text to it. - ONLY USE the template below and STRICTLY USE ELEMENT ID {chart_div} TO CREATE THE CHART google.charts.load('current', ); google.charts.setOnLoadCallback(drawChart); drawchart function var data = with options Title=<> width=600, height=300, hAxis.textStyle.fontSize=5 vAxis.textStyle.fontSize=5 legend.textStyle.fontSize=10 other necessary options for the chart type var chart = new google.charts.(document.getElementById('{chart_div}')); chart.draw() Example Response: google.charts.load('current', {{packages: ['corechart']}}); google.charts.setOnLoadCallback(drawChart); function drawChart() {{var data = google.visualization.arrayToDataTable([['Product SKU', 'Total Ordered Items'], ['GGOEGOAQ012899', 456], ['GGOEGDHC074099', 334], ['GGOEGOCB017499', 319], ['GGOEGOCC077999', 290], ['GGOEGFYQ016599', 253], ]); var options = {{ title: 'Top 5 Product SKUs Ordered', width: 600, height: 300, hAxis: {{ textStyle: {{ fontSize: 12 }} }}, vAxis: {{ textStyle: {{ fontSize: 12 }} }}, legend: {{ textStyle: {{ fontSize: 12\n }} }}, bar: {{ groupWidth: '50%' }} }}; var chart = new google.visualization.BarChart(document.getElementById('{chart_div}')); chart.draw(data, options);}} ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "opendataqna" version = "0.1.0" description = "" authors = ["msubasioglu","l-sri","orpzs"] license = "Apache 2.0" readme = "README.md" [tool.poetry.dependencies] python = ">=3.9, <3.9.7 || >3.9.7, <4.0" google = "^3.0.0" google-api-python-client = "^2.120.0" google-cloud = "^0.34.0" google-cloud-aiplatform = "^1.43.0" pandas = "^2.2.1" pandas-gbq = "^0.21.0" cloud-sql-python-connector = {extras = ["asyncpg"], version = "^1.7.0"} sqlalchemy = "^2.0.27" pgvector = "^0.2.5" pg8000 = "^1.30.5" tabulate = "^0.9.0" langchain = "^0.1.9" fsspec = "^2024.2.0" gcsfs = "^2024.2.0" streamlit = "^1.32.2" google-cloud-bigquery = "^3.17.2" google-cloud-bigquery-connection = "^1.15.3" pyarrow = "^15.0.2" Flask = { version = "^2.1.0", extras = ["async"]} gunicorn = "^20.1.0" flask-cors = "^4.0.0" Werkzeug = "^2.3.7" google-cloud-firestore= "^2.16.0" firebase-admin = "^5.2.0" [tool.poetry.group.dev.dependencies] pyzmq ="<26.0.0" ipykernel = "^6.29.3" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ================================================ FILE: scripts/.~lock.Scenarios Sample.csv# ================================================ ,leelapriya,leelapriya.c.googlers.com,23.07.2024 14:37,file:///usr/local/google/home/leelapriya/.config/libreoffice/4; ================================================ FILE: scripts/Scenarios Sample.csv ================================================ user_grouping,scenario,question MovieExplorer-bigquery,Genres,What are the top 5 most common movie genres in the dataset? MovieExplorer-bigquery,Genres,How many are musicals? MovieExplorer-bigquery,Genres,Romance? MovieExplorer-bigquery,Movie,What is the average user rating of the God Father movie? MovieExplorer-bigquery,Movie,Which year was it released? MovieExplorer-bigquery,Movie,How long is it? MovieExplorer-bigquery,Movie,Who is the lead actor? MovieExplorer-bigquery,Movie,Director MovieExplorer-bigquery,Movie,Cast WorldCensus-cloudsql-pg,Life Expectancy,What is the life expectancy for men and women in a United States in 2022? WorldCensus-cloudsql-pg,Life Expectancy,In India? WorldCensus-cloudsql-pg,Life Expectancy,Which country has highest male life expectancy? WorldCensus-cloudsql-pg,Life Expectancy,Female life expectancy? WorldCensus-cloudsql-pg,Population Density,What are the top 5 coutries with highest population density in 2024? WorldCensus-cloudsql-pg,Population Density,What are the birth and death rates in these counties? WorldCensus-cloudsql-pg,Sex Ratio at Birth,What is the sex ratio at birth in China in 2023? WorldCensus-cloudsql-pg,Sex Ratio at Birth,Which country has the highest? WorldCensus-cloudsql-pg,Sex Ratio at Birth,Whats the world average? ================================================ FILE: scripts/__init__.py ================================================ from .save_config import save_config __all__ = ["save_config"] ================================================ FILE: scripts/copy_select_table_column_bigquery.py ================================================ import pandas as pd from google.cloud import bigquery def copy_tables(project_id, source_dataset, destination_dataset, df): client = bigquery.Client() unique_table_names = df['TableName'].unique() for table_name in unique_table_names: print(f"Processing table: {table_name}") dest_table_id = f"{project_id}.{destination_dataset}.{table_name}" orig_table_id = f"{project_id}.{source_dataset}.{table_name}" # Copy the table (all columns initially) job = client.copy_table(orig_table_id, dest_table_id) job.result() # Get columns to preserve columns_to_preserve = df[df['TableName'] == table_name]['ColumnName'].tolist() # Drop unwanted columns dest_table = client.get_table(dest_table_id) all_columns = [c.name for c in dest_table.schema] for column in all_columns: print(f'Cheking if column {column} should be preserved') if column not in columns_to_preserve: query = f"ALTER TABLE {dest_table_id} DROP COLUMN IF EXISTS {column}" client.query(query).result() # Update descriptions for column_name in columns_to_preserve: print(f'Updating description for column {column_name}') description = df[(df['TableName'] == table_name) & (df['ColumnName'] == column_name)]['Description'].iloc[0] query = f""" ALTER TABLE {dest_table_id} ALTER COLUMN {column_name} SET OPTIONS(description='{description}') """ try: client.query(query).result() except Exception as e: print(f"An error occurred while additon desciption to the column {column_name} in table {dest_table_id}: {e}") def add_table_description(project_id, dataset, df): client = bigquery.Client() for _, row in df.iterrows(): table_name = row['TableName'] table_description = row['TableDescription'] table_id = f"{project_id}.{dataset}.{table_name}" print(f'Updating description for Table - {table_name}') query = f""" ALTER TABLE {table_id} SET OPTIONS(description='{table_description}') """ try: client.query(query).result() except Exception as e: print(f"An error occurred while additon desciption to the column {column_name} in table {table_id}: {e}") def add_column_description(project_id, dataset, df): client = bigquery.Client() for _, row in df.iterrows(): table_name = row['TableName'] column_name = row['ColumnName'] column_description = row['ColumnDescription'] table_id = f"{project_id}.{dataset}.{table_name}" print(f'Updating description for Column - {table_name}.{column_name}') query = f""" ALTER TABLE {table_id} ALTER COLUMN {column_name} SET OPTIONS(description='{column_description}') """ try: client.query(query).result() except Exception as e: print(f"An error occurred while additon desciption to the column {column_name} in table {table_id}: {e}") if __name__ == "__main__": import os current_dir = os.path.dirname(__file__) # --- Read 'TablesAndColumns' sheet from Excel --- file_path = f"{current_dir}/tables_columns_descriptions.csv" df = pd.read_csv(file_path) # --- Drop rows with null values --- df.dropna(subset=['TableName', 'ColumnName'], inplace=True) # --- BigQuery setup --- client = bigquery.Client() project_id = "" source_dataset = "" destination_dataset = "" # # Copy Tables # copy_tables(project_id, source_dataset, # destination_dataset, # df=df) # Add table descriptions df_table_desc = df[['TableName', 'TableDescription']] # Select required columns df_table_desc = df_table_desc.dropna() # Handle missing ColumnDescriptions (optional) df_table_desc = df_table_desc.drop_duplicates(subset=['TableName', 'TableDescription']) # Drop duplicate table-column pairs add_table_description(project_id, destination_dataset, df_table_desc) # Add column descriptions df_col_desc = df[['TableName', 'ColumnName', 'ColumnDescription']] # Select required columns df_col_desc = df_col_desc.dropna() # Handle missing ColumnDescriptions (optional) df_col_desc = df_col_desc.drop_duplicates(subset=['TableName', 'ColumnName']) # Drop duplicate table-column pairs add_column_description(project_id, destination_dataset, df_col_desc) ================================================ FILE: scripts/data_source_list.csv ================================================ source,user_grouping,schema,table bigquery,MovieExplorer-bigquery,imdb_people,name_basics bigquery,MovieExplorer-bigquery,imdb_people,title_principals bigquery,MovieExplorer-bigquery,imdb_people,title_crew bigquery,MovieExplorer-bigquery,imdb_people,title_basics bigquery,MovieExplorer-bigquery,imdb_ratings,reviews bigquery,MovieExplorer-bigquery,imdb_ratings,title_ratings cloudsql-pg,WorldCensus-cloudsql-pg,census_bureau_international, ================================================ FILE: scripts/data_source_list_sample.csv ================================================ source,user_grouping,schema,table bigquery,MovieExplorer-bigquery,imdb_people,name_basics bigquery,MovieExplorer-bigquery,imdb_people,title_principals bigquery,MovieExplorer-bigquery,imdb_people,title_crew bigquery,MovieExplorer-bigquery,imdb_people,title_basics bigquery,MovieExplorer-bigquery,imdb_ratings,reviews bigquery,MovieExplorer-bigquery,imdb_ratings,title_ratings cloudsql-pg,WorldCensus-cloudsql-pg,census_bureau_international, ================================================ FILE: scripts/known_good_sql.csv ================================================ prompt,sql,user_grouping Which country had the highest population in 2023?,"SELECT country_name, midyear_population FROM census_bureau_international.midyear_population WHERE year = 2022 ORDER BY midyear_population DESC LIMIT 1",WorldCensus-cloudsql-pg List 10 countries with highest fertility rate,"SELECT country_name, total_fertility_rate FROM census_bureau_international.age_specific_fertility_rates WHERE year = 2023 ORDER BY total_fertility_rate DESC LIMIT 10 ",WorldCensus-cloudsql-pg What is the life expectancy for men and women in a United States and 2022?,"SELECT country_name, life_expectancy_male, life_expectancy_female FROM census_bureau_international.mortality_life_expectancy WHERE country_name = 'United States' AND year = 2022",WorldCensus-cloudsql-pg Which countries had the highest and lowest infant mortality rates in 2023?,"WITH RankedInfantMortality AS ( SELECT country_name, infant_mortality, RANK() OVER (ORDER BY infant_mortality ASC) AS LowestRank, RANK() OVER (ORDER BY infant_mortality DESC) AS HighestRank FROM census_bureau_international.mortality_life_expectancy WHERE year = 2023 ) SELECT country_name, infant_mortality FROM RankedInfantMortality WHERE LowestRank = 1 OR HighestRank = 1;",WorldCensus-cloudsql-pg What is the population distribution by age and sex in India for 2023?,"SELECT age, sex, population FROM census_bureau_international.midyear_population_agespecific WHERE country_name = 'United States' AND year = 2023",WorldCensus-cloudsql-pg What is the sex ratio at birth for China in 2023?,"SELECT country_name, sex_ratio_at_birth, year FROM census_bureau_international.age_specific_fertility_rates WHERE country_name = 'China' AND year = 2023",WorldCensus-cloudsql-pg What is the world average sex ratio at birth in 2023?,"SELECT AVG(sex_ratio_at_birth) AS world_avg_sex_ratio_at_birth FROM census_bureau_international.age_specific_fertility_rates WHERE year = 2023 AND sex_ratio_at_birth IS NOT NULL;",WorldCensus-cloudsql-pg Which country has the highest boy to girl ratio at birth in 2023?,"SELECT country_name, sex_ratio_at_birth FROM census_bureau_international.age_specific_fertility_rates WHERE year = 2023 AND sex_ratio_at_birth IS NOT NULL ORDER BY sex_ratio_at_birth DESC LIMIT 1;",WorldCensus-cloudsql-pg What are the top 10 counties with lowest mortality rate of children under 5 in 2022?,"SELECT country_name, mortality_rate_under5 FROM census_bureau_international.mortality_life_expectancy WHERE year = 2023 ORDER BY mortality_rate_under5 ASC LIMIT 10",WorldCensus-cloudsql-pg "What were the birth, death, and growth rates in SIngapore between 2019-2024?","SELECT year, crude_birth_rate, crude_death_rate, growth_rate FROM census_bureau_international.birth_death_growth_rates WHERE country_name = 'Singapore' AND year BETWEEN 2019 AND 2024",WorldCensus-cloudsql-pg "In 2023, what was the population for countries with a population growth rate above 3%?","SELECT mp.midyear_population as Population, mp.country_name, bdg.growth_rate FROM census_bureau_international.midyear_population mp JOIN census_bureau_international.birth_death_growth_rates bdg ON mp.country_code = bdg.country_code AND mp.year = bdg.year WHERE bdg.year = 2023 AND bdg.growth_rate > 3;",WorldCensus-cloudsql-pg What are the top 3 countries with the highest midyear populations in 2024 and their respective land areas?,"SELECT mp.country_name, mp.midyear_population, cna.country_area FROM census_bureau_international.midyear_population mp JOIN census_bureau_international.country_names_area cna ON mp.country_code = cna.country_code WHERE mp.year = 2023 ORDER BY mp.midyear_population DESC LIMIT 3;",WorldCensus-cloudsql-pg What are the top 5 coutries with highest population density in 2024? ,"SELECT mp.country_name, mp.midyear_population / cna.country_area AS population_density_persqkm FROM census_bureau_international.midyear_population mp JOIN census_bureau_international.country_names_area cna ON mp.country_code = cna.country_code WHERE mp.year = 2024 AND cna.country_area > 0 -- Exclude countries with zero or null area ORDER BY population_density_persqkm DESC LIMIT 5;",WorldCensus-cloudsql-pg Which country has highest male life expectancy?,"SELECT country_name, life_expectancy_male FROM census_bureau_international.mortality_life_expectancy WHERE year = 2022 -- Assuming you want the data for the year 2022 ORDER BY life_expectancy_male DESC LIMIT 1;",WorldCensus-cloudsql-pg What are the birth and death rates of the top 5 densest countries in 2024?,"SELECT bdg.country_name, bdg.crude_birth_rate, bdg.crude_death_rate, population_density_persqkm FROM census_bureau_international.birth_death_growth_rates bdg JOIN ( SELECT mp.country_code, mp.midyear_population / cna.country_area as population_density_persqkm FROM census_bureau_international.midyear_population mp JOIN census_bureau_international.country_names_area cna ON mp.country_code = cna.country_code WHERE mp.year = 2024 AND cna.country_area > 0 ORDER BY mp.midyear_population / cna.country_area DESC LIMIT 5 ) top_countries ON bdg.country_code = top_countries.country_code WHERE bdg.year = 2024;",WorldCensus-cloudsql-pg ================================================ FILE: scripts/save_config.py ================================================ import os import sys import configparser def is_root_dir(): """ Checks if the current working directory is the root directory of a project by looking for either the "/notebooks" or "/agents" folders. Returns: bool: True if either directory exists in the current directory, False otherwise. """ current_dir = os.getcwd() print("current dir: ", current_dir) notebooks_path = os.path.join(current_dir, "notebooks") agents_path = os.path.join(current_dir, "agents") return os.path.exists(notebooks_path) or os.path.exists(agents_path) def save_config(embedding_model, description_model, vector_store, logging, kgq_examples, use_session_history, use_column_samples, PROJECT_ID, pg_region, pg_instance, pg_database, pg_user, pg_password, bq_dataset_region, bq_opendataqna_dataset_name, bq_log_table_name, firestore_region): config = configparser.ConfigParser() if is_root_dir(): current_dir = os.getcwd() config.read(current_dir + '/config.ini') root_dir = current_dir else: root_dir = os.path.abspath(os.path.join(os.getcwd(), '..')) config.read(root_dir+'/config.ini') if not 'root_dir' in locals(): # If not found in any parent dir raise FileNotFoundError("config.ini not found in current or parent directories.") config['GCP']['PROJECT_ID'] = PROJECT_ID # config['CONFIG']['DATA_SOURCE'] = data_source config['CONFIG']['VECTOR_STORE'] = vector_store config['CONFIG']['EMBEDDING_MODEL'] = embedding_model config['CONFIG']['DESCRIPTION_MODEL'] = description_model config['CONFIG']['FIRESTORE_REGION'] = firestore_region # Save the parameters based on Vector Store Choices if vector_store == 'cloudsql-pgvector': config['PGCLOUDSQL']['PG_INSTANCE'] = pg_instance config['PGCLOUDSQL']['PG_DATABASE'] = pg_database config['PGCLOUDSQL']['PG_USER'] = pg_user config['PGCLOUDSQL']['PG_PASSWORD'] = pg_password config['PGCLOUDSQL']['PG_REGION'] = pg_region # config['PGCLOUDSQL']['PG_SCHEMA'] = pg_schema if vector_store := 'bigquery': config['BIGQUERY']['BQ_DATASET_REGION'] = bq_dataset_region config['BIGQUERY']['BQ_OPENDATAQNA_DATASET_NAME'] = bq_opendataqna_dataset_name config['BIGQUERY']['BQ_LOG_TABLE_NAME'] = bq_log_table_name if logging: config['CONFIG']['LOGGING'] = 'yes' config['BIGQUERY']['BQ_LOG_TABLE_NAME'] = bq_log_table_name else: config['CONFIG']['LOGGING'] = 'no' if kgq_examples: config['CONFIG']['KGQ_EXAMPLES'] = 'yes' else: config['CONFIG']['KGQ_EXAMPLES'] = 'no' if use_session_history: config['CONFIG']['USE_SESSION_HISTORY'] = 'yes' else: config['CONFIG']['USE_SESSION_HISTORY'] = 'no' if use_column_samples: config['CONFIG']['USE_COLUMN_SAMPLES'] = 'yes' else: config['CONFIG']['USE_COLUMN_SAMPLES'] = 'no' with open(root_dir+'/config.ini', 'w') as configfile: config.write(configfile) print('All configuration paramaters saved to file!') ================================================ FILE: scripts/tables_columns_descriptions.csv ================================================ TableName,ColumnName,TableDescription,ColumnDescription Table_1,Col11,Table_1_DESC,Col11_DESC Table_1,Col12,,Col12_DESC Table_1,Col13,,Col13_DESC Table_1,Col14,,Col14_DESC ,,, Table_2,Col21,Table_2_DESC,Col21_DESC Table_2,Col22,,Col22_DESC Table_2,Col23,,Col31_DESC ================================================ FILE: terraform/.gitignore ================================================ .terraform terraform.tfstate ================================================ FILE: terraform/README.md ================================================ # Infrastructure deployment The provided terraform streamlines the setup of this solution and serves as a blueprint for deployment. The script provides a one-click, one-time deployment option. However, it doesn't include CI/CD capabilities and is intended solely for initial setup. > [!NOTE] > Current version of the Terraform Google Cloud provider does not support deployment of a few resources, this soultion uses null_resource to create those resources using Google Cloud SDK. ## Table of Contents - [Terraform folder structure](#terraform-folder-structure) - [Structure](#structure) - [.tf Files Description](#tf-files-description) - [Scripts](#scripts) - [Template files](#template-files) - [Step 0: Prerequisites](#step-0-prerequisites) - [Data Sources](#data-sources-set-up) - [Enable Firebase](#enable-firebase) - [Local Configuration](#optional-local-configuration) - [Terraform Deployment](#terraform-deployment) - [Step 1a: One-click Deployment](#step-1-one-click-deployment) [choose either 1a or 1b] - [Step 1b: Step by step Deployment](#step-1-step-by-step-deployment) [choose either 1a or 1b] - [Step 2: Review your environment](#step-2-review-your-environment) - [Deployed Resources](#deployed-resources) - [Troubleshooting Known Issues](#troubleshooting-known-issues) ## Terraform folder structure #### Structure ``` Open_Data_QnA/ | |--->terraform/ |---> .tf files |---> scripts/ | |--> .sh, .py |---> templates/ |--> .tftpl files ``` 1. .tf files - terraform scripts to spin up resources. 1. scripts/ - A folder containing all the required shell scripts and python files. 1. templates/ - the templates in this directory are used to replace all necessary configuration values for the application in config.ini and constants.ts #### .tf Files Description 1. versions.tf - Contains provider blocks with appropriate versions that are needed to deploy the resources 1. locals.tf - Contains the list of APIs to be enabled and BQ tables to be created. 1. main.tf - Imports data about the mentioned project and also activates required APIs in the project. 1. iam.tf - Handles implementation of all the necessary IAM roles to various members like service accounts and users. 1. bq.tf - This script is responsible for creating Bigquery dataset. 1. pg-vector.tf - This is responsible for spinning up the CloudSQL instance, database, username and password. 1. embeddings-setup.tf - This script is responsible for the below tasks. * Update [config.ini](../config.ini) with values provided in variables.tf / terraform.tfvars. * Fetch list of data sources from [data_source_list.csv](../scripts/data_source_list.csv). * Create vector embeddings for the metadata associated with the listed tables. * Fetch known good sqls from [known_good_sql.csv](../scripts/known_good_sql.csv) (if opted) and create vector embeddings for the same. * Store table metadata embeddings and known good sql embeddings to the vector data store. 1. backend.tf - Creates firstore database and cloud run service with dummy container image. 1. frontend.tf - Responsible for creating the below resources. * Updates firebase.json * Creates firebase web app * Imports config data about the web app. * Updates [constants.ts](../frontend/src/assets/constants.ts) with relevant values from web app's config. 1. outputs.tf - Contains the important details about the resources that will be deployed. Example: instance name, ip address etc, cloud run url. This will be printed out on the console when the terraform executes successfully. 1. variables.tf - Contains the list of variables, their default values and descriptions. 1. terraform.tfvars - Can be used to override default values in variables.tf #### Scripts 1. [deploy-all.sh](scripts/deploy-all.sh) - This shell script is used for one-click deployment. this executes the terraform scripts as well as the gcloud commands for deploying latest cloud build images for backend and frontend 1. [install-dependencies.sh](scripts/install-dependencies.sh) - This script installs poetry module which is used to manage the dependencies required for the application. This shell script is used within the terraform script [embeddings-setup.tf](embeddings-setup.tf) 1. [create-and-store-embeddings.py](scripts/create-and-store-embeddings.py) - This is a python file that executes the relevant functions to create and store vector embeddings 1. [execute-python-files.sh](scripts/execute-python-files.sh) - This shell script is used within the terraform script [embeddings-setup.tf](embeddings-setup.tf) to execute the [create-and-store-embeddings.py](scripts/create-and-store-embeddings.py) 1. [execute-gcloud-cmd.sh](scripts/execute-gcloud-cmd.sh) - This shell script can overwrite an org policy or delete the over-written rule based on the input parameters. 1. [copy-firebase-json.sh](scripts/copy-firebase-json.sh) - This script is used to overwrite firebase.json file with the information required by the application. This is information is copied from [firebase_setup.json](../frontend/firebase_setup.json) 1. [backend-deployment.sh](scripts/backend-deployment.sh) - This shell script updates the cloud run service created by terraform, with the latest code. 1. [frontend-deployment.sh](scripts/frontend-deployment.sh) - This shell script submits the latest build for the frontent code. #### Template Files 1. [config.ini.tftpl](./templates/config.ini.tftpl) - This template is used to populate [config.ini](../config.ini) with corresponding values from variables.tf or terraform.tfvars. This file is populated via [embeddings-setup.tf](embeddings-setup.tf). 1. [constants.ts.tftpl](./templates/constants.ts.tftpl) - This template is used to populate [constants.ts](../frontend/src/assets/constants.ts). This file is populated via [frontend.tf](frontend.tf) ## Step 0: Prerequisites Prior to executing terraform, ensure that the below mentioned steps have been completed. #### Data Sources Set Up 1. Source data should already be available. If you do not have readily available source data, use the notebooks [0_CopyDataToBigQuery.ipynb](../notebooks/0_CopyDataToBigQuery.ipynb) or [0_CopyDataToCloudSqlPG.ipynb](../notebooks/0_CopyDataToCloudSqlPG.ipynb) based on the preferred source to populate sample data. 2. Ensure that the [data_source_list.csv](../scripts/data_source_list.csv) is populated with the list of datasources to be used in this solution. Terraform will take care of creating the embeddings in the destination. Use [data_source_list_sample.csv](../scripts/data_source_list_sample.csv) to fill the [data_source_list.csv](../scripts/data_source_list.csv) 3. If you want to use known good sqls for few shot prompting, ensure that the [known_good_sql.csv](../scripts/known_good_sql.csv) is populated with the required data. Terraform will take care of creating the embeddings in the destination. #### Enable Firebase Firebase will be used to host the frontend of the application. 1. Go to https://console.firebase.google.com/ 1. Select add project and load your Google Cloud Platform project 1. Add Firebase to one of your existing Google Cloud projects 1. Confirm Firebase billing plan 1. Continue and complete #### (Optional) Local configuration If you are running this outside Cloud Shell you need to set up your Google Cloud SDK Credentials ```shell gcloud config set project gcloud auth application-default set-quota-project ``` #### Terraform deployment > [!NOTE] > Terraform apply command for this application uses gcloud config to fetch & pass the set project id to the scripts. Please ensure that gcloud config has been set to your intended project id before proceeding. 1. Install [terraform 1.7 or higher](https://developer.hashicorp.com/terraform/install). 1. [OPTIONAL] Update default values of variables in [variables.tf](variables.tf) according to your preferences. You can find the description for each variable inside the file. This file will be used by terraform to get information about the resources it needs to deploy. If you do not update these, terraform will use the already specified default values in the file. 1. Move to the terraform directory in the terminal: ```Open_Data_QnA/terraform```. 1. There are 2 ways to deploy this application: * **[One-click deployment](#step-1-one-click-deployment)** : Use this method when you want to do a single click deployment i.e execute terraform and shell scripts for frontend & backend services deployment all at once. * **[Step by step deployment](#step-1-step-by-step-deployment)**: Use this method if you want to deploy teraaform resources, frontend and backend services separately after manual validation of configuration files. > [!IMPORTANT] > The Terraform scripts require specific IAM permissions to function correctly. The user needs either the broad `roles/resourcemanager.projectIamAdmin` role or a custom role with tailored permissions to manage IAM policies and roles. > Additionally, one script TEMPORARILY disables Domain Restricted Sharing Org Policies to enable the creation of a public endpoint. This requires the user to also have the `roles/orgpolicy.policyAdmin` role. ## Step 1: One-Click Deployment ### Deployment command and script overview ```sh sh ./scripts/deploy-all.sh ``` This script will perform the following steps: 1. **Run terraform scripts** - These terraform scripts will generate all the GCP resources and configurations files required for the frontend & backend. It will also generate embeddings and store it in the destination vector db. 1. **Deploy cloud run backend service with latest backend code** - The terraform in the previous step uses a dummy container image to deploy the initial version of cloud run service. This is the step where the actual backend code gets deployed. 1. **Deploy frontend app** - All the config files, web app etc required to create the frontend are deployed via terraform. However, the actual UI deployment takes place in this step. ### After deployment ***Auth Provider*** You need to enable at least one authentication provider in Firebase, you can enable it using the following steps: 1. Go to https://console.firebase.google.com/project/your_project_id/authentication/providers (change the `your_project_id` value) 2. Click on Get Started (if needed) 3. Select Google and enable it 4. Set the name for the project and support email for project 5. Save ## Step 1: Step by step Deployment #### Start Terraform deployment ```sh terraform init terraform apply -var=project_id=$(gcloud config get project) ``` This terraform will generate all configurations files required in the frontend and backend_apis. It will also generate embeddings and store it in the destination vector db #### After Terraform deployment ***Auth Provider*** You need to enable at least one authentication provider in Firebase, you can enable it using the following steps: 1. Go to https://console.firebase.google.com/project/your_project_id/authentication/providers (change the `your_project_id` value) 2. Click on Get Started (if needed) 3. Select Google and enable it 4. Set the name for the project and support email for project 5. Save #### Validate the config files This deployment uses the templates in the [templates/](templates/) directory to replace all necessary configuration values for the application. Before deploying the application check that all the values have been populated correctly in the [config.ini](../config.ini) file and [constants.ts](../frontend/src/assets/constants.ts) file. #### Application deployment To deploy the backend cloud run service for the application, run the following command. You will find all the needed values from terraform output. Expected working directory : ```Open_Data_QnA/terraform``` ``` sh scripts/backend-deployment.sh --servicename --project --region --sa ``` To deploy the frontent of the application, you need to execute the below command: ``` sh scripts/frontend-deployment.sh --project --region ``` ## Step 2: Review your environment Once deployment is completed the scripts (terraform & shell scripts) will output relevant resource values. Resulting example outputs: ```sh instance_name = "pg15-opendataqna-2" postgres_db = "opendataqna-db-2" public_ip_address = "XX.XXX.XXX.XXX" service_account = "opendataqna@your-project-id.iam.gserviceaccount.com" service_name = "opendataqna-v2" service_url = "https://opendataqna-xxxxxxxx.a.run.app" ``` Your url to access the frontend is usually in this format: "https://your-project-id.web.app" ## Deployed resources This deployment creates all the resources described in the main [README.md](../README.md) file, the following is a list of the created resources: 1. Enable Google Cloud Service APIs. List can be found in [locals.tf](locals.tf) 2. [BiqQuery](https://console.cloud.google.com/bigquery) Dataset and tables: - **1 Dataset**: where the log table and embedding tables will be created. - **Tables**: - **Audit Log table**: This table will store all application logs - **Table Metadata Embeddings table**: Created only when Bigquery is chosen as the vector db. This will contain all table meta data and its text embeddings. - **Column Embeddings table**: Created only when Bigquery is chosen as the vector db. This will contain all column meta data and its text embeddings. - **Example SQL table**: Created only when Bigquery is chosen as the vector db and marked kgq_examples='yes' in tfvars. This contains all the known good sqls and their respective text embeddings. You need to populate the [known_good_sql.csv](../scripts/known_good_sql.csv) before deployment. Terraform will take care of creation of embeddings in the destination. 3. **CloudSQL instance**: Created if cloudsql-pgvector is chosen as the vector store. 4. **pg-vector database**: Created if cloudsql-pgvector is chosen as the vector store. This database will store all text embeddings. - **Tables**: - **Table Metadata Embeddings table**: Created only when cloudsql-pgvector is chosen as the vector db. This will contain all table meta data and its text embeddings. - **Column Embeddings table**: Created only when cloudsql-pgvector is chosen as the vector db. This will contain all column meta data and its text embeddings. - **Example SQL table**: Created only when cloudsql-pgvector is chosen as the vector db and marked kgq_examples='yes' in tfvars. This contains all the known good sqls and their respective text embeddings. > [!NOTE] > The above mentioned tables in **CloudSQL & BigQuery** are created via the python script [create-and-store-embeddings.py](scripts/create-and-store-embeddings.py) > [!IMPORTANT] > If you have an existing CloudSQL instance that you want to use for storing vector embeddings, then update the variable - **use_existing_cloudsql_instance = "yes"**. Terraform will not create any new cloudsql instance, database or user name & password. It will be assumed that these resources already exist. 5. A backend service account for cloud run service with the required permissions 6. All embedding tables will be populated via terraform. 7. [Cloud Run](https://console.cloud.google.com/run) for backend APIs 8. A Firestore database for storing chat history 9. Firebase web app for hosting frontend. ## Troubleshooting Known Issues 1. #### `pipx: command not found` * This error will be caused by execution of [install-dependencies.sh](./scripts/install-dependencies.sh) file. * The error indicates that even though you've installed pipx, your shell isn't aware of its location. This usually means the directory where pipx is installed hasn't been added to your system's PATH environment variable. * Resolution: 1. We have already tried to ensure that pipx gets added to PATH via execution of the command `export PATH="$PATH:$(python3 -c "import sysconfig; print(sysconfig.get_paths()['scripts'])")"` in the [install-dependencies.sh](./scripts/install-dependencies.sh) file. This command is specifically designed to return the path to the global site-packages directory (where system-wide packages are installed) and add to PATH temporarily. However, This command might not be giving you the correct directory where pipx is installed. 2. If you encounter this error, you can try replacing the above command with `export PATH="$PATH:$(python3 -m site --user-base)/bin"`. This command is specifically designed to return the path to the user-specific site-packages directory and add to PATH temporarily. 3. If the above step also fails, you will need to manually find the PATH where pipx is installed. Once you know the location, just replace `export PATH="$PATH:$(python3 -c "import sysconfig; print(sysconfig.get_paths()['scripts'])")"` inside [install-dependencies.sh](./scripts/install-dependencies.sh) with `export PATH="$PATH:/path/to/pipx/directory"`. Replace `/path/to/pipx/directory` with the actual path you found. 1. #### `poetry: command not found` * This error will be caused by execution of [install-dependencies.sh](./scripts/install-dependencies.sh) file. * The error indicates that even though you've installed poetry, your shell isn't aware of its location. This usually means the directory where pipx is installed hasn't been added to your system's PATH environment variable. * Resolution: 1. Although we have ensured that poetry will automatically get added to PATH via commands `pipx ensurepath` and `source ~/.bashrc` in the [install-dependencies.sh](./scripts/install-dependencies.sh) file, if the script still fails with this error, you need to manually find the path where poetry is installed. 2. Add the new PATH to `~/.bashrc` file and source it before re-running the script again. 1. #### `aiohttp.client_exceptions.ClientConnectorCertificateError` * `aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host sqladmin.googleapis.com:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)')]` * SSL/TLS and Certificates: Secure connections over the internet (HTTPS) rely on SSL/TLS protocols. These protocols use digital certificates to verify the identity of the server you're connecting to. Your system needs a set of trusted root certificates to validate these server certificates. * Missing Root Certificates: If your system lacks the necessary root certificates or they are outdated, it can't establish trust with the server, leading to the CERTIFICATE_VERIFY_FAILED error. * Resolution: 1. Install or Update certifi - certifi is a package that provides a curated collection of Root Certificates for validating the trustworthiness of SSL certificates while making secure network requests. Install or update it using pip: ``` pip install --upgrade certifi ``` 2. Set the SSL_CERT_FILE environment variable Tell your Python environment to use the certificates provided by certifi: ``` export SSL_CERT_FILE=$(python -c "import certifi; print(certifi.where())") ``` This command finds the location of the certificate file provided by certifi and sets the SSL_CERT_FILE environment variable to that path. 3. Retry your code. 1. #### `Firebase project not found` * `Error creating WebApp: googleapi: Error 404: Firebase project XXXXXXXXX not found.` * This error is encountered when you fail to add firebase to your project before execution of the scripts. * Follow the instructions for [enabling firebase](#enable-firebase) under [pre-requisites](#prerequisites) section of the README 1. #### `Error: Provider produced inconsistent result after apply` * Sometimes, when Terraform creates or updates a resource on your cloud provider, there can be a slight delay before that change is fully reflected in the provider's system. This can cause Terraform to see an inconsistency between what it expects and what the provider reports, leading to an error. * As a result, you might encounter an error message if Terraform attempts to read information about a newly created or updated resource before the provider's system has fully processed the change. * Solutions: 1. **Manual Deletion and Retry**: Delete the problematic resource directly on the remote system and re-run terraform apply. 2. **Terraform Import**: Use `terraform import` to synchronize the resource's state from the remote system into your Terraform state file, then re-run `terraform apply`. Follow the [How to Import Resources into a Remote State Managed by Terraform Cloud](https://support.hashicorp.com/hc/en-us/articles/360061289934-How-to-Import-Resources-into-a-Remote-State-Managed-by-Terraform-Cloud) for detailed steps. ================================================ FILE: terraform/backend.tf ================================================ resource "google_firestore_database" "chathistory_db" { project = var.project_id name = "opendataqna-session-logs" location_id = var.firestore_region type = "FIRESTORE_NATIVE" depends_on = [ module.project_services ] } resource "google_cloud_run_service" "backend" { name = var.cloud_run_service_name location = var.deploy_region project = var.project_id template { spec { containers { image = "us-docker.pkg.dev/cloudrun/container/hello" } service_account_name = module.genai_cloudrun_service_account.email } } traffic { percent = 100 latest_revision = true } depends_on = [ module.project_services,null_resource.org_policy_temp, module.genai_cloudrun_service_account, local_file.config_ini,] } ================================================ FILE: terraform/bq.tf ================================================ /** * Copyright 2024 Google LLC * * 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. */ module "bigquery" { source = "terraform-google-modules/bigquery/google" version = "~> 7.0" dataset_id = var.bq_opendataqna_dataset dataset_name = var.bq_opendataqna_dataset project_id = var.project_id location = var.bq_dataset_region default_table_expiration_ms = null depends_on = [module.project_services] } ================================================ FILE: terraform/embeddings-setup.tf ================================================ /** * Copyright 2024 Google LLC * * 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. */ resource "local_file" "config_ini" { content = templatefile("${path.module}/templates/config.ini.tftpl", { embedding_model = var.embedding_model, description_model = var.description_model, vector_store = var.vector_store, debugging = var.debugging, logging = var.logging, kgq_examples = var.kgq_examples, firestore_region = var.firestore_region, use_column_samples = var.use_column_samples, use_session_history = var.use_session_history, project_id = var.project_id, pg_region = var.pg_region, pg_instance = var.pg_instance, pg_database = var.pg_database, pg_user = var.pg_user pg_password = var.pg_password bq_dataset_region = var.bq_dataset_region bq_opendataqna_dataset = var.bq_opendataqna_dataset bq_log_table = var.bq_log_table } ) filename = "../config.ini" } resource "null_resource" "install_dependencies" { triggers = { always_run = "${timestamp()}" } provisioner "local-exec" { working_dir = "${path.module}" command = "sh ${path.module}/scripts/install-dependencies.sh" } } resource "null_resource" "create_and_store_embeddings" { depends_on = [local_file.config_ini, null_resource.install_dependencies,module.bigquery, google_sql_database_instance.pg15_opendataqna[0]] triggers = { always_run = "${timestamp()}" } provisioner "local-exec" { working_dir = "${path.module}" command = "sh ${path.module}/scripts/execute-python-files.sh './scripts'" } } ================================================ FILE: terraform/frontend.tf ================================================ resource "google_firebase_project" "default" { provider = google-beta project = var.project_id provisioner "local-exec" { working_dir = "${path.module}" command = "sh ${path.module}/scripts/copy-firebase-json.sh" } } resource "google_firebase_web_app" "app_frontend" { provider = google-beta project = var.project_id display_name = var.firebase_web_app_name } data "google_firebase_web_app_config" "app_frontend_config" { provider = google-beta web_app_id = google_firebase_web_app.app_frontend.app_id project = var.project_id } resource "local_file" "constants_ts" { depends_on = [ google_firebase_web_app.app_frontend, google_cloud_run_service.backend ] content = templatefile("${path.module}/templates/constants.ts.tftpl", { projectId = var.project_id appId = google_firebase_web_app.app_frontend.app_id apiKey = data.google_firebase_web_app_config.app_frontend_config.api_key authDomain = data.google_firebase_web_app_config.app_frontend_config.auth_domain storageBucket = lookup(data.google_firebase_web_app_config.app_frontend_config, "storage_bucket", "") messagingSenderId = lookup(data.google_firebase_web_app_config.app_frontend_config, "messaging_sender_id", "") endpoint_opendataqna = google_cloud_run_service.backend.status[0].url } ) filename = "../frontend/src/assets/constants.ts" } ================================================ FILE: terraform/iam.tf ================================================ /** * Copyright 2024 Google LLC * * 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. */ resource "null_resource" "org_policy_temp" { depends_on = [module.project_services] provisioner "local-exec" { working_dir = "${path.module}" command = "sh ${path.module}/scripts/execute-gcloud-cmd.sh ${var.project_id} YES" } } resource "null_resource" "delete_org_policy_temp" { provisioner "local-exec" { working_dir = "${path.module}" command = "sh ${path.module}/scripts/execute-gcloud-cmd.sh ${var.project_id} NO" } depends_on = [module.project_services, null_resource.org_policy_temp, google_cloud_run_service.backend ] } module "genai_cloudrun_service_account" { source = "terraform-google-modules/service-accounts/google" version = "~> 4.0" project_id = var.project_id names = [var.service_account] project_roles = [ "${var.project_id}=>roles/cloudsql.client", "${var.project_id}=>roles/bigquery.admin", "${var.project_id}=>roles/aiplatform.user", "${var.project_id}=>roles/datastore.owner" ] depends_on = [module.project_services] } resource "google_project_iam_member" "default_ce_sa_role" { for_each = toset([ "roles/storage.admin", "roles/artifactregistry.admin", "roles/firebase.admin", "roles/cloudbuild.builds.builder", "roles/logging.logWriter" ]) role = each.key member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" project = var.project_id depends_on = [module.project_services] } resource "google_project_iam_member" "default_cloudbuild_sa_role" { for_each = toset([ "roles/firebase.admin", "roles/artifactregistry.admin", "roles/serviceusage.apiKeysAdmin", "roles/cloudbuild.builds.builder" ]) role = each.key member = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com" project = var.project_id depends_on = [module.project_services] } resource "google_cloud_run_service_iam_member" "invoker" { location = google_cloud_run_service.backend.location project = google_cloud_run_service.backend.project service = google_cloud_run_service.backend.name role = "roles/run.invoker" member = "allUsers" depends_on = [ google_cloud_run_service.backend ] } ================================================ FILE: terraform/locals.tf ================================================ /** * Copyright 2024 Google LLC * * 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. */ locals { services = [ "cloudapis.googleapis.com", "iam.googleapis.com", "run.googleapis.com", "sqladmin.googleapis.com", "aiplatform.googleapis.com", "bigquery.googleapis.com", "storage.googleapis.com", "cloudresourcemanager.googleapis.com", "serviceusage.googleapis.com", "firebase.googleapis.com", "firebasehosting.googleapis.com", "logging.googleapis.com", "monitoring.googleapis.com", "cloudbuild.googleapis.com", "firestore.googleapis.com" ] } ================================================ FILE: terraform/main.tf ================================================ /** * Copyright 2024 Google LLC * * 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. */ data "google_project" "project" { project_id = var.project_id } module "project_services" { source = "terraform-google-modules/project-factory/google//modules/project_services" version = "~> 14.5" project_id = var.project_id enable_apis = true activate_apis = local.services disable_services_on_destroy = false } ================================================ FILE: terraform/outputs.tf ================================================ output "project_number" { value = data.google_project.project.number } output "project_id" { value = var.project_id } output "instance_name" { value = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no" ? google_sql_database_instance.pg15_opendataqna[0].name : null description = "The instance name for the master instance" } output "public_ip_address" { description = "The first public (PRIMARY) IPv4 address assigned for the master instance" value = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no" ? google_sql_database_instance.pg15_opendataqna[0].public_ip_address : null } output "postgres_db" { description = "database name" value = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no" ? google_sql_database.pg_db[0].name : null } output "sql_user" { description = "user name generated for the instance" value = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no" ? google_sql_user.pguser[0] : null sensitive = true } output "sql_user_password" { description = "user name generated for the instance" value = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no" ? google_sql_user.pguser[0].password : null sensitive = true } output "service_name" { value = google_cloud_run_service.backend.name description = "Name of the created service" } output "revision" { value = google_cloud_run_service.backend.status[0].latest_ready_revision_name description = "Deployed revision for the service" } output "service_url" { value = google_cloud_run_service.backend.status[0].url description = "The URL on which the deployed service is available" } output "firebase_appId" { value = google_firebase_web_app.app_frontend.app_id } output "firebase_apiKey" { value = data.google_firebase_web_app_config.app_frontend_config.api_key } output "firebase_authDomain" { value = data.google_firebase_web_app_config.app_frontend_config.auth_domain } output "firebase_storageBucket" { value = lookup(data.google_firebase_web_app_config.app_frontend_config, "storage_bucket", "") } output "firebase_messagingSenderId" { value = lookup(data.google_firebase_web_app_config.app_frontend_config, "messaging_sender_id", "") } output "firebase_measurementId" { value = lookup(data.google_firebase_web_app_config.app_frontend_config, "measurement_id", "") } output "hosting_url" { value = google_firebase_web_app.app_frontend.app_urls } output "service_account"{ value = module.genai_cloudrun_service_account.email } output "deploy_region" { value = var.deploy_region } ================================================ FILE: terraform/pg-vector.tf ================================================ resource "google_sql_database_instance" "pg15_opendataqna" { count = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no"? 1:0 name = var.pg_instance project = var.project_id region = var.pg_region database_version = "POSTGRES_15" root_password = "abcABC123!" settings { tier = "db-custom-2-7680" password_validation_policy { min_length = 6 reuse_interval = 2 complexity = "COMPLEXITY_DEFAULT" disallow_username_substring = true password_change_interval = "30s" enable_password_policy = true } } deletion_protection = false depends_on = [ module.project_services ] } resource "google_sql_database" "pg_db" { count = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no"? 1:0 name = var.pg_database project = var.project_id instance = google_sql_database_instance.pg15_opendataqna[count.index].name depends_on = [google_sql_database_instance.pg15_opendataqna] } resource "google_sql_user" "pguser" { count = var.vector_store=="cloudsql-pgvector" && var.use_existing_cloudsql_instance=="no"? 1:0 name = var.pg_user project = var.project_id instance = google_sql_database_instance.pg15_opendataqna[count.index].name password = var.pg_password depends_on = [ google_sql_database_instance.pg15_opendataqna, ] } ================================================ FILE: terraform/scripts/backend-deployment.sh ================================================ usage() { echo "Usage: $0 --servicename --project --region --sa " exit 1 # Indicate an error } # Function to validate a parameter's value validate_param() { local param_name="$1" local param_value="$2" if [ -z "$param_value" ]; then echo "Error: Parameter '$param_name' cannot be empty." usage # Show usage and exit if the value is empty fi } # Check if enough arguments are provided if [ $# -lt 8 ]; then echo "Error: Insufficient arguments." usage fi # Parse and validate named parameters while [ $# -gt 0 ]; do case "$1" in --servicename) validate_param "$1" "$2" SERVICE_NAME=$2 shift 2 # Move to the next parameter ;; --project) validate_param "$1" "$2" PROJECT_ID=$2 shift 2 # Move to the next parameter ;; --region) validate_param "$1" "$2" DEPLOY_REGION=$2 shift 2 ;; --sa) validate_param "$1" "$2" SERVICE_ACCOUNT=$2 shift 2 ;; *) echo "Error: Unknown parameter '$1'." usage ;; esac done # Deploys backend to Cloud Run using the provided region and service account main(){ pwd cd ../backend-apis echo "Setting orgpolicy to allow all IAM domains" gcloud resource-manager org-policies set-policy --project=$PROJECT_ID policy.yaml || exit 1 #This command will create policy that overrides to allow all domain cd ../ echo "Deploying cloud run service.." pwd gcloud beta run deploy $SERVICE_NAME --region $DEPLOY_REGION --source . --service-account=$SERVICE_ACCOUNT --service-min-instances=1 --allow-unauthenticated --project=$PROJECT_ID || exit 1 echo "Deleting the previously create dorg policy.." gcloud resource-manager org-policies delete iam.allowedPolicyMemberDomains --project=$PROJECT_ID || exit 1 } main ================================================ FILE: terraform/scripts/copy-firebase-json.sh ================================================ cd ../frontend/ rm firebase.json .firebaserc cp firebase_setup.json firebase.json ================================================ FILE: terraform/scripts/create-and-store-embeddings.py ================================================ import os import sys import asyncio module_path = os.path.abspath(os.path.join('..', '..')) sys.path.append(module_path) print(f"module_path: {module_path}") os.chdir(module_path) current_dir = os.getcwd() print(f"current_dir : {current_dir}") from env_setup import get_embeddings, store_embeddings, store_kgq_sql_embeddings async def create_and_store_embeddings(): # Generate embeddings for tables and columns table_schema_embeddings, col_schema_embeddings = get_embeddings() # Store table/column embeddings (asynchronous) await(store_embeddings(table_schema_embeddings, col_schema_embeddings)) asyncio.run(create_and_store_embeddings()) # Store known good query embeddings (if enabled) asyncio.run(store_kgq_sql_embeddings()) ================================================ FILE: terraform/scripts/deploy-all.sh ================================================ echo "#####################################################################################" echo " STARTING DEPLOYMENT " echo "#####################################################################################" original_dir=$(pwd) echo "Current Working Directory: $original_dir" echo "-------------------------------------------------------------------------------------" echo " EXECUTING TERRAFORM " echo "-------------------------------------------------------------------------------------" terraform init && \ terraform apply -var=project_id=$(gcloud config get project) -auto-approve || exit 1 echo "-------------------------------------------------------------------------------------" echo " TERRAFORM EXECUTION SUCCESSFUL " echo "-------------------------------------------------------------------------------------" echo "The below values from terraform output will be used for deployment of frontend and backend services:" cloudrun_service_name=$(terraform output -raw service_name) echo "cloudrun_service_name: $cloudrun_service_name" project_id=$(terraform output -raw project_id) echo "project_id: $project_id" deploy_region=$(terraform output -raw deploy_region) echo "deploy_region: $deploy_region" service_account=$(terraform output -raw service_account) echo "service_account: $service_account" echo "-------------------------------------------------------------------------------------" echo " DEPLOYING BACKEND CLOUD RUN " echo "-------------------------------------------------------------------------------------" sh scripts/backend-deployment.sh --servicename $cloudrun_service_name --project $project_id --region $deploy_region --sa $service_account || exit 1 echo "-------------------------------------------------------------------------------------" echo " BACKEND DEPLOYMENT SUCCESSFUL " echo "-------------------------------------------------------------------------------------" echo "Current working directory: $original_dir" echo "-------------------------------------------------------------------------------------" echo " DEPLOYING FRONTEND SERVICE " echo "-------------------------------------------------------------------------------------" sh scripts/frontend-deployment.sh --project $project_id --region $deploy_region || exit 1 echo "-------------------------------------------------------------------------------------" echo " FRONTEND DEPLOYMENT SUCCESSFUL " echo "-------------------------------------------------------------------------------------" echo "#####################################################################################" echo " APPLICATION DEPLOYMENT COMPLETED! PLEASE FOLLOW README FOR NEXT STEPS " echo "#####################################################################################" ================================================ FILE: terraform/scripts/execute-gcloud-cmd.sh ================================================ export PROJECT_ID=$1 export SETPOLICY=$2 cd ../backend-apis if [ $SETPOLICY = "YES" ] then echo "Enforcing Org Policy.." gcloud resource-manager org-policies set-policy --project=$PROJECT_ID policy.yaml #This command will create policy that overrides to allow all domain else echo "Deleting Org Policy.." gcloud resource-manager org-policies delete iam.allowedPolicyMemberDomains --project=$PROJECT_ID fi ================================================ FILE: terraform/scripts/execute-python-files.sh ================================================ #!/bin/bash # Get the directory of the current script SCRIPT_DIR=$(dirname "$0") echo "$SCRIPT_DIR" source ~/.bashrc # Activate the poetry virtual environment source "$(poetry env info --path)/bin/activate" # Navigate to the correct directory (assuming the script is in the 'scripts' folder) cd "$SCRIPT_DIR/" pwd # Execute the Python file within the virtual environment poetry run python create-and-store-embeddings.py ================================================ FILE: terraform/scripts/frontend-deployment.sh ================================================ usage() { echo "Usage: $0 --project --region " exit 1 # Indicate an error } # Function to validate a parameter's value validate_param() { local param_name="$1" local param_value="$2" if [ -z "$param_value" ]; then echo "Error: Parameter '$param_name' cannot be empty." usage # Show usage and exit if the value is empty fi } # Check if enough arguments are provided if [ $# -lt 4 ]; then echo "Error: Insufficient arguments." usage fi while [ $# -gt 0 ]; do case "$1" in --project) validate_param "$1" "$2" PROJECT_ID=$2 shift 2 # Move to the next parameter ;; --region) validate_param "$1" "$2" REGION=$2 shift 2 ;; *) echo "Error: Unknown parameter '$1'." usage ;; esac done main(){ pwd cd ../.. git clone https://github.com/GoogleCloudPlatform/cloud-builders-community.git || exit 1 cd cloud-builders-community/firebase gcloud builds submit --region=$REGION . --project=$PROJECT_ID || exit 1 cd ../.. rm -rf cloud-builders-community/ || exit 1 cd Open_Data_QnA/frontend gcloud builds submit . --config frontend.yaml --substitutions _FIREBASE_PROJECT_ID=$PROJECT_ID --project=$PROJECT_ID || exit 1 } main ================================================ FILE: terraform/scripts/install-dependencies.sh ================================================ #!/bin/bash pwd pip3 install pipx export PATH="$PATH:$(python3 -c "import sysconfig; print(sysconfig.get_paths()['scripts'])")" echo "$PATH" pipx install poetry pipx ensurepath source ~/.bashrc poetry --version poetry lock poetry install poetry env info ================================================ FILE: terraform/templates/config.ini.tftpl ================================================ [CONFIG] embedding_model = ${embedding_model} description_model = ${description_model} vector_store = ${vector_store} debugging = ${debugging} logging = ${logging} kgq_examples = ${kgq_examples} firestore_region = ${firestore_region} use_session_history = ${use_session_history} use_column_samples = ${use_column_samples} [GCP] project_id = ${project_id} [PGCLOUDSQL] pg_region = ${pg_region} pg_instance = ${pg_instance} pg_database = ${pg_database} pg_user = ${pg_user} pg_password = ${pg_password} [BIGQUERY] bq_dataset_region = ${bq_dataset_region} bq_opendataqna_dataset_name = ${bq_opendataqna_dataset} bq_log_table_name = ${bq_log_table} ================================================ FILE: terraform/templates/constants.ts.tftpl ================================================ export const firebaseConfig = { "projectId": "${projectId}", "appId": "${appId}", "storageBucket": "${storageBucket}", "apiKey": "${apiKey}", "authDomain": "${authDomain}", "messagingSenderId": "${messagingSenderId}" }; export const ENDPOINT_OPENDATAQNA = '${endpoint_opendataqna}' export const FIRESTORE_DATABASE_ID = 'opendataqna-session-logs' ================================================ FILE: terraform/terraform.tfvars.sample ================================================ project_id = "projectv1-340909" vector_store = "cloudsql-pgvector" embedding_model = "vertex" description_model = "gemini-1.5-pro" debugging = "yes" logging = "yes" kgq_examples = "yes" use_session_history = "yes" use_column_samples = "yes" use_existing_cloudsql_instance = "no" pg_instance = "pg15-opendataqna-2" pg_region = "us-central1" pg_database = "opendataqna-db-2" pg_user = "pguser2" pg_password = "Pguser@!12345" bq_opendataqna_dataset = "opendataqna2" bq_dataset_region = "us-central1" bq_log_table = "audit_log_table" firestore_region = "us-central1" service_account = "opendataqna-v2-sa-3" deploy_region = "us-central1" cloud_run_service_name = "opendataqna-v2-run-3" firebase_web_app_name = "opendataqna-v2-chatbot-3" ================================================ FILE: terraform/variables.tf ================================================ /** * Copyright 2024 Google LLC * * 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. */ variable "project_id" { type = string } variable "embedding_model" { type = string default = "vertex" description = "name of the model that you want to use to create embeddings. Options: 'vertex' or 'vertex-lang'" } variable "description_model" { type = string default = "gemini-1.5-pro" description = "name of the model that you want to use to generate missing description for tables and columns. Options: 'gemini-1.0-pro', 'gemini-1.5-pro', 'text-bison-32k', 'gemini-1.5-flash'" } variable "vector_store" { type = string default = "bigquery-vector" description = "name of the datastore you want to use to store text embeddings of your meta data. Options: bigquery-vector, cloudsql-pgvector" } variable "debugging" { type = string default = "yes" description = "yes, if you want to enable debugging. No, otherwise" } variable "logging" { type = string default = "yes" description = "yes, if you want to enable application logging. No, otherwise" } variable "kgq_examples" { type = string default = "yes" description = "yes, if you want to use known good sqls for few shot prompting and creating cache. No, otherwise" } variable "use_session_history" { type = string default = "yes" description = "yes, if you want to use current session's questions without re-evaluating them. No, otherwise" } variable "use_column_samples" { type = string default = "no" description = "yes, if you want to add some sample column values to the embeddings to enrich it with more information. No, otherwise" } variable "use_existing_cloudsql_instance" { default = "no" type = string description = "If you want to use an existing cloudsql instance to store the vector embeddings, then choose 'yes' else choose 'no'. Terraform will create a new cloudsql instance if 'no' is chosen." } variable "pg_instance" { default = "pg15-opendataqna" type = string description = "Name of the Cloudsql postgres instance to store vector embeddings. Keep this empty if vector db is bigquery." } variable "pg_region" { default = "us-central1" type = string description = "Location of the pg_instance" } variable "pg_database" { type = string default = "opendataqna-db" description = "Name of the Database associated with pg_instance" } variable "pg_user" { type = string default = "pguser" description = "user name for the database" } variable "pg_password" { type = string default = "pg123" description = "password for pg_user" } variable "bq_opendataqna_dataset" { type = string default = "opendataqna" description = "This dataset will be used to store text embeddings and application logs. If pg-vector is chosen as vector db, only application logs will be stored here." } variable "bq_dataset_region" { type = string default = "us-central1" description = "Location of bq_opendataqna_dataset." } variable "bq_log_table" { type = string default = "audit_log_table" description = "Name of the table where audit logs will be stored. This table will be create under the bq_opendataqna_dataset." } variable "firestore_region" { type = string default = "us-central1" description = "Location of the firestore database." } variable service_account { type = string default = "opendataqna" description = "service account used by backend service" } variable "deploy_region" { type = string default = "us-central1" description = "region where cloudrun service will be deployed" } variable "cloud_run_service_name" { type = string default = "opendataqna" description = "name of the cloud run service where backend apis will be deployed" } variable "firebase_web_app_name" { type = string default = "opendataqna" description = "name of the firebase web app." } ================================================ FILE: terraform/versions.tf ================================================ /** * Copyright 2024 Google LLC * * 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. */ terraform { required_version = ">= 1.3" required_providers { google = { source = "hashicorp/google" version = ">= 4.64, < 6" } google-beta = { source = "hashicorp/google-beta" version = ">= 4.64, < 6" } local = { source = "hashicorp/local" } } } ================================================ FILE: utilities/__init__.py ================================================ import configparser import os import sys import yaml config = configparser.ConfigParser() def is_root_dir(): """ Checks if the current working directory is the root directory of a project by looking for either the "/notebooks" or "/agents" folders. Returns: bool: True if either directory exists in the current directory, False otherwise. """ current_dir = os.getcwd() print("current dir: ", current_dir) notebooks_path = os.path.join(current_dir, "notebooks") agents_path = os.path.join(current_dir, "agents") return os.path.exists(notebooks_path) or os.path.exists(agents_path) def load_yaml(file_path: str) -> dict: with open(file_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) if is_root_dir(): current_dir = os.getcwd() config.read(current_dir + '/config.ini') root_dir = current_dir else: root_dir = os.path.abspath(os.path.join(os.getcwd(), '..')) config.read(root_dir+'/config.ini') if not 'root_dir' in locals(): # If not found in any parent dir raise FileNotFoundError("config.ini not found in current or parent directories.") print(f'root_dir set to: {root_dir}') def format_prompt(context_prompt, **kwargs): """ Formats a context prompt by replacing placeholders with values from keyword arguments. Args: context_prompt (str): The prompt string containing placeholders (e.g., {var1}). **kwargs: Keyword arguments representing placeholder names and their values. Returns: str: The formatted prompt with placeholders replaced. """ return context_prompt.format(**kwargs) # [CONFIG] EMBEDDING_MODEL = config['CONFIG']['EMBEDDING_MODEL'] DESCRIPTION_MODEL = config['CONFIG']['DESCRIPTION_MODEL'] # DATA_SOURCE = config['CONFIG']['DATA_SOURCE'] VECTOR_STORE = config['CONFIG']['VECTOR_STORE'] #CACHING = config.getboolean('CONFIG','CACHING') #DEBUGGING = config.getboolean('CONFIG','DEBUGGING') LOGGING = config.getboolean('CONFIG','LOGGING') EXAMPLES = config.getboolean('CONFIG', 'KGQ_EXAMPLES') USE_SESSION_HISTORY = config.getboolean('CONFIG', 'USE_SESSION_HISTORY') USE_COLUMN_SAMPLES = config.getboolean('CONFIG','USE_COLUMN_SAMPLES') #[GCP] PROJECT_ID = config['GCP']['PROJECT_ID'] #[PGCLOUDSQL] PG_REGION = config['PGCLOUDSQL']['PG_REGION'] # PG_SCHEMA = config['PGCLOUDSQL']['PG_SCHEMA'] PG_INSTANCE = config['PGCLOUDSQL']['PG_INSTANCE'] PG_DATABASE = config['PGCLOUDSQL']['PG_DATABASE'] PG_USER = config['PGCLOUDSQL']['PG_USER'] PG_PASSWORD = config['PGCLOUDSQL']['PG_PASSWORD'] #[BIGQUERY] BQ_REGION = config['BIGQUERY']['BQ_DATASET_REGION'] # BQ_DATASET_NAME = config['BIGQUERY']['BQ_DATASET_NAME'] BQ_OPENDATAQNA_DATASET_NAME = config['BIGQUERY']['BQ_OPENDATAQNA_DATASET_NAME'] BQ_LOG_TABLE_NAME = config['BIGQUERY']['BQ_LOG_TABLE_NAME'] # BQ_TABLE_LIST = config['BIGQUERY']['BQ_TABLE_LIST'] #[FIRESTORE] FIRESTORE_REGION = config['CONFIG']['FIRESTORE_REGION'] #[PROMPTS] PROMPTS = load_yaml(root_dir + '/prompts.yaml') __all__ = ["EMBEDDING_MODEL", "DESCRIPTION_MODEL", #"DATA_SOURCE", "VECTOR_STORE", #"CACHING", #"DEBUGGING", "LOGGING", "EXAMPLES", "PROJECT_ID", "PG_REGION", # "PG_SCHEMA", "PG_INSTANCE", "PG_DATABASE", "PG_USER", "PG_PASSWORD", "BQ_REGION", # "BQ_DATASET_NAME", "BQ_OPENDATAQNA_DATASET_NAME", "BQ_LOG_TABLE_NAME", # "BQ_TABLE_LIST", "FIRESTORE_REGION", "PROMPTS" "root_dir", "save_config"]